try! Swift の宿題: TableViewをSwiftらしく

Avatar tnantoka 2016-03-14

こんにちは、@tnantokaです。
2016年3月2〜4日に渋谷で開催された、try! Swiftカンファレンスに参加してきました。
セッション内容についてはすでに素晴らしいまとめがありますので、そちらをご覧ください。
また、Realmさんのサイトでも録画が公開される予定とのことです。
大変素晴らしいセッションばかりで大満足だったのですが、登壇者の方々との力の差を感じました…。
少しでも近づくために、セッションで聞いた内容をもとに手を動かしていきたいと思います。
1回目の今回は、Swiftらしさを意識しながら簡単なTableViewアプリケーションを作ってみます。

作るもの

AppBoardというアプリを作ります。
アプリ一覧を作成して、各アプリの詳細画面からQRコードでストアに飛べる、というシンプルなアプリです。
今回のカンファレンスで自分のアプリの紹介が面倒だったという問題を解消したくて開発しました。
主な画面は以下の4つです。
  • ボード一覧
  • アプリ一覧
  • 検索・追加
  • アプリ詳細

データソースとモデル

文化を調和させるSwiftらしいTable View Controllerの使い方を参考に、変更しない部分を切り出すということを意識して実装しました。
結果的にデータソースはこうなりました。
名前 概要
SourceType Cell追加・削除などデータに依存しない処理。
DataSource DataTypeに依存する処理。
Configuration Cellの描画などTableViewControllerごとに変わる処理。
DataType モデルが準拠すべきプロトコル。
モデルはこんな感じです。(データベースはもちろんRealmです!)
名前 ItemType 概要
Account Board Boardを持つユーザー。現在は固定のオブジェクト。
Board App アプリ一覧を持つボード。
Search Software iTunes APIを検索する時のモデル。

TableView

先ほど定義したデータソース・モデルを使ってTableViewを実装します。
import UIKit
class TableViewController<Data: DataType>: UITableViewController, UISearchBarDelegate {
    let configuration: Configuration<Data>
    let searchBar = UISearchBar()
    init(configuration: Configuration<Data>) {
        self.configuration = configuration
        super.init(style: .Plain)
        tableView.dataSource = configuration
        tableView.delegate = configuration
        if configuration.addable {
            navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "add")
        }
        if configuration.searchable {
            searchBar.delegate = self
            navigationItem.titleView = searchBar
        }
    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        if configuration.searchable {
            searchBar.becomeFirstResponder()
        }
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    func add() {
        configuration.addItemTo(tableView)
    }
    // MARK: - UISearchBarDelegate
    func searchBarSearchButtonClicked(searchBar: UISearchBar) {
        guard let text = searchBar.text where !text.isEmpty else { return }
        configuration.searchItemsIn(tableView, query: text)
    }
}
若干の無理やり感はありますが、変更部分を抽出することで、それぞれ表示内容の違う以下の3つのTableを1つのViewControllerで実装できました。
  • Boards(.Value1、Image無し)
  • Apps(.Subtitle、Image有り)
  • Boards(.Default、Image有り)
慣れてくればもう少し上手く書けそうです。

Coordinators

実践的 Boundariesを参考に、各コントローラーは直接他のコントローラーを参照せず、Coordinatorに遷移を管理させました。
ApplicationCoordinatorのおかげでAppDelegateがシンプルになるので気に入っていますが、その他の部分はとりあえず使ってみた、という感じでまだまだ使いこなせていません。

ネットワーク

Protocol-Oriented Programming in Networking を参考に、ネットワーク通信はAPIKitHimotokiを使って実装しました。
以下の様なiTunes API依存の部分を作っただけで、他の部分はhttps://github.com/tryswift/RxPagination を拝借してそのまま動いています。
import Foundation
import APIKit
import Himotoki
class ITunesAPI {
}
protocol ITunesRequestType: RequestType {
}
extension ITunesRequestType {
    var baseURL: NSURL {
        return NSURL(string: "https://itunes.apple.com")!
    }
}
extension ITunesRequestType where Response: PaginationResponseType, Response.Element.DecodedType == Response.Element {
    func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? {
        let elements = try? decodeArray(object, rootKeyPath: "results") as [Response.Element]
        return elements.map { Response(elements: $0, hasNextPage: false) }
    }
}
extension ITunesAPI {
    struct SearchSoftwaresRequest: ITunesRequestType, PaginationRequestType {
        typealias Response = PaginationResponse<Software>
        let query: String
        let page: Int
        var locale = NSLocale.currentLocale()
        var method: HTTPMethod {
            return .GET
        }
        var path: String {
            return "/search"
        }
        var parameters: [String: AnyObject] {
            let lang = locale.localeIdentifier
            let country = locale.objectForKey(NSLocaleCountryCode) as? String ?? ""
            return [
                "media": "software",
                "entity": "software",
                "lang": lang,
                "country": country,
                "limit": 20,
                "term": query
            ]
        }
        init(query: String, page: Int = 1) {
            self.query = query
            self.page = page
        }
        func requestWithPage(page: Int) -> SearchSoftwaresRequest {
            return SearchSoftwaresRequest(query: query, page: page)
        }
    }
}

完成

ひとまず動くアプリができました。

各セッションの内容を復習しつつ書きましたが、まだ消化しきれていない部分もあり、修正したいところがたくさんあります。
この先、リファクタリングを躊躇なく進めるためにも、次回はこのプロジェクトにテストを追加しようと思います。
それでは。

ソースコード

https://github.com/tnantoka/AppBoard/releases/tag/v1.0.0

関連セッション