プログラミング言語 Swift で iOS アプリを作る。iOS アプリは Apple が整備する Cocoa Touch と呼ばれるフレームワーク群を利用して構成される。Cocoa Touch の主要なフレームワークは Foundation
と UIKit
である。Foundation
は文字列やコレクションといった基本的なクラスから、並行処理やネットワーク処理のためのクラスまで、基本的なツールが揃っている。また Foundation
は iOS だけでなく、macOS や watchOS そして tvOS においても主要なフレームワークである。 UIKit
は iOS の GUI フレームワークであり、アプリケーションを構成するための重要な機能のほとんどを担っている。GUI フレームワークはプラットフォーム毎に異なるものが用意されており、macOS では AppKit
、watchOS では WatchKit
を用いる。ただし tvOS においては UIKit
の多くがそのまま利用でき、加えて TVMLKit
というサーバーから配信されたマークアップ言語で GUI を構築できる仕組みも備えている。
はじめにアプリ全体をどのように構成するか検討する。iOS の UIKit
フレームワークも一般的な MVC の考え方を踏襲しているが、view controller が非常に大きな役割を果たす。
View controller は UIViewController
のサブクラスで、自身が管理するひとつの view (UIView
) を持つ。View controller は管理下の view を更新し、また view において発生したイベントを受け取ってハンドリングする。必要に応じて model を変更したり、あるいは model の状態を view に反映させたりする。
View controller は複数の child view controller を持つことができる。そのような child view controller を持つような view controller を container view controller と呼ぶ。アプリの画面はひとつ以上の view controller で構成され、UIWindow
が持つひとつの rootViewController
の下に、必要に応じて複数の view controller が重なり、あるいは遷移して、アプリの機能を提供する。つまり view controller は UI の中心となるコンポーネントである。
View controller の様々な機能については “View Controller Programming Guide for iOS” を参照すること。
View は画面の表示を司るコンポーネントである。iOS アプリにおいては UIView
とそのサブクラスにあたる。View は複数の subview を内包することができ、view を重なり合わせて画面をつくる。アプリは原則的にひとつのウインドウ (UIWindow
) を持ち、その上に必要な view をいくつも載せていく。
View には view controller によって直接的に管理されるものと、その subview として表示されるだけの view controller と対応しない view がある。そのような view controller や view の階層によってアプリの画面は構築されている。
┌──────────UIWindow──────────┐
│ ┌─────────UIView───────────┴┐ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
│ │ │ ┃ ┃
│ │ │ ┃ UIViewController ┃
│ │ ◀─────┃ (root view controller) ┃──┐
│ │ │ ┃ ┃ │
│ │ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
│ │ ┌─────────UIView──────────┴─┐ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │
│ │ │ │ ┃ ┃ │
│ │ │ │ ┃ UIViewController ┃ │
│ │ │ ┌───UILabel───┐ ◀───┃ (child view controller) ┃◀─┘
│ │ │ └─────────────┘ │ ┃ ┃
│ │ │ ┌─UIImageView─┐ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ └─────────────┘ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
└─┤ │ │
└─┤ │
└───────────────────────────┘
View に関する様々なトピックは “View Programming Guide for iOS” に詳しい。
UIKit
フレームワークは、iOS のユーザーインターフェイスを作るために重要な機能のほとんどを提供している。UIKit
を学ぶことは、すなわち iOS アプリの開発を学ぶことである。
UIKit の提供する UI コンポーネントは Apple の “UIKit User Interface Catalog” にまとめられている。
Model はアプリケーションの中心を成す、主としてビジネスロジックを担う部分である。データやリソースを抽象化したデータモデルとしての役割も持つ。基本的にはほとんどのロジックはここに集約されるべきであり、また単体テストがしやすいコンポーネントである。
Model の役割は広く、必要に応じてより細分化された名前で呼ぶことになるかもしれない。しかし以下では view や view controller に相当するもの以外のほとんどすべてを、単に model であるとみなす。
ここから、実際に Swift 言語で iOS アプリを作ってみる。Web API から情報を取得して一覧表示する。例として GitHub の検索 API をとりあげ、GitHub のリポジトリを検索できるアプリを作ってみる。
完成したサンプルコードを GitHub で公開しているので、適宜参照すること。
https://github.com/hatena/swift-sample-GitHubSearch-2016
Xcode から新しくプロジェクトを作る。
テンプレートの選択を求められるので、今回は Master-Detail Application を選んで Next。
適当なディレクトリを選択して保存する。
ここで .gitignore
を適切に設定し、git init
しておく。GitHub の .gitignore
をコピーするのがよい。
アプリを build するとソースコードがコンパイルされる。Xcode 左上の Run を押すことで、build して選択したターゲットデバイスでアプリを起動できる。
Run するとデバッグ用の build が行われ(最適化が省略されたり、開発用のバイナリになる)、自動的にデバッガーが接続される。止めるときは Xcode 左上の Stop を押す。
アプリの開発中はこのような工程を何度も行って、少しずつアプリを作る。必要に応じて iOS Simulator や USB で接続された iOS の実機をターゲットデバイスとして選ぶ。
アプリの UI は Storyboard ファイルを使って作る。Storyboard は Xcode の Interface Builder 機能を用いてグラフィカルに編集可能である。Storyboard 上には scene と呼ばれる view と view controller の組が並べられ、その view を編集して UI を作る。また view controller 同士を segue (UIStoryboardSegue
) と呼ばれる線で繋ぐことで、画面の遷移などを表現する。
まずは最初から追加されている Main.storyboard
ファイルを開く。すでにいくつかの scene が追加されているが、UISplitViewController
や UINavigationController
から接続された MasterViewController
と DetailViewController
のふたつの view controller が主な興味の対象である。
Child view controller を持つような view controller を container view controller と言う。View controller の階層を作ることで、遷移元と遷移先の view controller を入れ替えることで画面遷移させられるほか、一つの画面を複数の view controller に分割することもできる。
UISplitViewController
は画面幅が広い場合に、ナビゲーションを表示する view controller と詳細を表示する view controller を左右に並べて表示するための container view controller である。画面のサイズに応じて最適な UI に切り替えることを、iOS では adaptive という単語で表現している。UINavigationController
は、スタック様に行き来する画面遷移を含んだナビゲーションのための container view controller である。上部の UINavigationBar
を用いて一つ前の view controller に戻ることができる。
このように UIKit には様々な container view controller が用意されており、それぞれ役割を持っている。
最初から Storyboard に配置されているこれらの view controller は、そのまま利用する。
MasterViewController
は UITableViewController
のサブクラスで、 UITableView
による一覧表示を管理する view controller となる。今回のアプリでは最初の画面となり、GitHub のリポジトリ一覧を表示することになる。DetailViewController
は通常の UIViewController
のサブクラスで、リポジトリの詳細を表示する画面になる予定である。
Interface Builder の右側のインスペクタは、選択中の要素の詳細を変更できる。Identifier Inspector タブから Custom Class の設定を確認することで、それぞれの scene に設定された view controller を確かめることができる。
Attributes Inspector タブで view controller の Is Initial View Controller にチェックが入っている scene が、その Storyboard における最初の画面である。Main.storyboard
はアプリの Main Storyboard file なので(Info.plist
ファイルで設定できる)、この Storyboard における最初の画面はアプリにとっても最初の画面である。
Scene と scene を繋ぐ segue には、大きく分けて二つの種類がある。一つは UINavigationController
から繋がる root view controller などの relationship segue である。これは view controller 同士の関係性を定めるものであり、特定の種類の view controller からしか繋げられない。もう一つは画面の遷移を表現する segue で、Show, Show Detail, Present Modally, Present as Popover などの Kind が選べる。遷移の segue は、遷移元となる view controller そのものから設定されるものや、配置された button (UIButton
) などから設定されるものがある。Button などから設定された segue は自動的に機能するが、view controller から設定された segue は performSegueWithIdentifier(_:sender:)
メソッドを呼び出さなければならない。また segue には Identifier があり、Kind などとともに Attributes Inspector から設定できる。
Storyboard 上の要素に Swift からアクセスしたい場合は、@IBOutlet
を利用する。View controller に @IBOutet
属性を指定したインスタンス変数を定義し、Interface Builder で副ボタンクリック(右クリック)やドラッグアンドドロップして要素と接続する。同様に button (UIButton
) などが引き起こす動作を設定するには、@IBAction
属性を指定したメソッドを view controller に定義して、接続する。
Storyboard 上に要素を配置するには、右下の Object library から必要なパーツを選ぶ。Segue や @IBAction
, @IBOutlet
などは、副ボタンクリック(右クリック)やドラッグアンドドロップによって設定できる。
ここから GitHub のリポジトリ検索 API を利用する。
https://api.github.com/search/repositories?q=Hatena&page=1
というような URL で以下のような JSON を返す。
{
"total_count": 583,
"incomplete_results": false,
"items": [
{
"id": 3946028,
"name": "Hatena-Textbook",
"full_name": "hatena/Hatena-Textbook",
"owner": {
"login": "hatena",
"id": 14185,
"avatar_url": "https://avatars.githubusercontent.com/u/14185?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/hatena",
"html_url": "https://github.com/hatena",
"type": "Organization"
},
"private": false,
"html_url": "https://github.com/hatena/Hatena-Textbook",
"description": "はてな研修用教科書",
"fork": false,
"url": "https://api.github.com/repos/hatena/Hatena-Textbook",
"created_at": "2012-04-06T02:04:23Z",
"updated_at": "2015-08-02T04:44:15Z",
"pushed_at": "2015-02-26T06:30:33Z",
"homepage": "",
"size": 614,
"stargazers_count": 879,
"watchers_count": 879,
"language": null,
"forks_count": 72,
"open_issues_count": 2,
"forks": 72,
"watchers": 879,
"default_branch": "master",
"score": 36.982796
},
...
]
}
iOS アプリからの HTTP 通信では、NSURLRequest
を NSURLSession
を介して送信してレスポンスを得る。レスポンスは NSURLResponse
かそのサブクラスの NSHTTPURLResponse
で、レスポンスデータはバイナリの NSData
である。NSJSONSerialization
を利用することでバイナリから JSON の内容を得られる。 これらは全て Foundation.framework が提供する機能である。これを実際に行うのは以下のようなコードになる。
import Foundation
let URL = NSURL(string: "https://api.github.com/search/repositories?q=Hatena&page=1")!
let request = NSMutableURLRequest(URL: URL)
request.HTTPMethod = "GET"
request.addValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
if let error = error {
print(error)
}
if let data = data {
print(try? NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject])
}
}
task.resume()
NSMutableURLRequest
を用いてリクエストを構築し、NSURLSession
から NSURLSessionDataTask
を作る。通信の結果は completionHandler
のクロージャに渡され、バイナリの NSData
から NSJSONSerialization
で Foundation のオブジェクトにできる。
- NSURL Class Reference
- NSURLRequest Class Reference
- NSURLResponse Class Reference
- NSURLSession Class Reference
- NSURLSessionDataTask Class Reference
- NSJSONSerialization Class Reference
この API の場合は、前述した JSON のオブジェクトが返ってくるはずなので、[String: AnyObject]
型の辞書を取り出すことができる。ここから、検索結果の個々のアイテムの名前を取得すると、以下のようになる。
let data: NSData!
var JSON: [String: AnyObject]?
do {
JSON = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject]
} catch {
print(error)
}
if let JSON = JSON {
if let items = JSON["items"] as? [AnyObject] {
for case let item as [String: AnyObject] in items {
if let name = item["name"] as? String {
print(name)
}
}
}
}
見ての通り、型を確かめるためのコードが続くことになる。これでは使いにくいので、モデルオブジェクトとマッピングしていくことを検討する。
それぞれの API から返ってくる JSON のフォーマットは常に一貫している。このデータ構造を Swift の struct にすることを考える。この API から返ってくるデータの構造を簡略化すると以下のようになり、全体を覆う JSON のオブジェクトがあり、その items
キーの内部に repository を表す JSON オブジェクトの配列があり、repository の owner
キーには user を表す JSON オブジェクトがある。それ以外のキーは、文字列や数値、真偽値などのプリミティブな値である。
{
// Search result
"items": [
{
// Repository
"owner": {
// User
...
},
...
},
...
],
...
}
これらは、SearchResult
, Repository
, User
の3つの型で表現できる。はじめに、いちばん簡単な User
部分について見てみる。JSON のオブジェクトは以下のようになっている。
{
"login": "hatena",
"id": 14185,
"avatar_url": "https://avatars.githubusercontent.com/u/14185?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/hatena",
"html_url": "https://github.com/hatena",
"type": "Organization"
}
Swift の struct でこれを表すと以下のようになる。
struct User {
let login: String
let id: Int
let avatarURL: NSURL
let gravatarID: String
let URL: NSURL
let receivedEventsURL: NSURL
let type: String
}
ここに init?(JSON: [String: AnyObject])
のようなイニシャライザをつけると、以下のようにできる。
init?(JSON: [String: AnyObject]) {
guard
let login = JSON["login"] as? String,
let id = JSON["id"] as? Int,
let avatarURL = (JSON["avatar_url"] as? String).flatMap(NSURL.init(string:)),
let gravatarID = JSON["gravatar_id"] as? String,
let URL = (JSON["url"] as? String).flatMap(NSURL.init(string:)),
let receivedEventsURL = (JSON["received_events_url"] as? String).flatMap(NSURL.init(string:)),
let type = JSON["type"] as? String
else {
return nil
}
self.login = login
self.id = id
self.avatarURL = avatarURL
self.gravatarID = gravatarID
self.URL = URL
self.receivedEventsURL = receivedEventsURL
self.type = type
}
これで作られた User
型のインスタンスは完全に型付けされており、安全である。
同様のことを SearchResult
や Repository
でも行うとよいが、些か冗長ではある。要するに個々のキーに対して正しい型の値が入っていることが保障されればよいので、これを簡単にするユーティリティを作る。
enum JSONDecodeError: ErrorType {
case MissingRequiredKey(String)
case UnexpectedType(key: String, expected: Any.Type, actual: Any.Type)
}
struct JSONObject {
let JSON: [String: AnyObject]
func get<T>(key: String) throws -> T {
guard let value = JSON[key] else {
throw JSONDecodeError.MissingRequiredKey(key)
}
guard let typedValue = value as? T else {
throw JSONDecodeError.UnexpectedType(key: key, expected: T, actual: value.dynamicType)
}
return typedValue
}
}
これは、JSON オブジェクトの [String: AnyObject]
から初期化できる struct で、get<T>(_:) throws -> T
というメソッドを持っている。型パラメータ T
があるとき、特定のキーについて値が存在し、それが T
型であることを保障する。値が存在しなかったり型が異なっている場合にはエラーを投げる。これを用いると、User
の新たなイニシャライザ init(JSON: JSONObject) throws
は以下のように書ける。
init(JSON: JSONObject) throws {
self.login = try JSON.get("login")
self.id = try JSON.get("id")
self.avatarURL = NSURL(string: try JSON.get("avatar_url"))!
self.gravatarID = try JSON.get("gravatar_id")
self.URL = NSURL(string: try JSON.get("url"))!
self.receivedEventsURL = NSURL(string: try JSON.get("received_events_url"))!
self.type = try JSON.get("type")
}
型パラメータ T
はコンテキストから推論できるので書く必要がない。こうすることで記述が簡略化され、さらに失敗した場合にその原因を調べることが容易になる。
もし存在しなくてもよい property がある場合は、その型を Optional
にしておく。そして JSONObject
に以下のメソッドを付け足す。
func get<T>(key: String) throws -> T? {
guard let value = JSON[key] else {
return nil
}
if value is NSNull {
return nil
}
guard let typedValue = value as? T else {
throw JSONDecodeError.UnexpectedType(key: key, expected: T, actual: value.dynamicType)
}
return typedValue
}
キーが存在しないか、値が NSNull
の場合にエラーを投げず、そのまま nil
値を返している。このため返り値の型は T?
である。この場合、先ほどの get<T>(_:) throws -> T
といま作った get<T>(_:) throws -> T?
は、返り値が T
か T?
かの違いしかない。Swift のオーバーロード機能によって、呼び出すコンテキストから適切な方が選択される。
これらの機能を利用し、またはさらに拡張することで、JSON オブジェクトからそれぞれ何らかの型にマッピングしたオブジェクトが得られるはずである。
レスポンスの抽象化ができたところで、次は Web API について検討する。最初に HTTP 通信した際には、NSURLRequest
や NSURLSession
をその場で作成していた。ふつうアプリが利用する Web API は多岐に渡るので、毎度このような書き方をしているといかにも冗長だ。
一般的な Web API では、エンドポイント毎にリクエストとレスポンスのフォーマットが決まっている。ここでいうエンドポイントというのは、API の URL と HTTP メソッドの組を指す。これをモデリングすると、以下のようにできる。
protocol JSONDecodable {
init(JSON: JSONObject) throws
}
enum HTTPMethod: String {
case OPTIONS
case GET
case HEAD
case POST
case PUT
case DELETE
case TRACE
case CONNECT
}
protocol APIEndpoint {
var URL: NSURL { get }
var method: HTTPMethod { get }
var query: [String: String]? { get }
var headers: [String: String]? { get }
associatedtype ResponseType: JSONDecodable
}
extension APIEndpoint {
var method: HTTPMethod {
return .GET
}
var query: [String: String]? {
return nil
}
var headers: [String: String]? {
return nil
}
}
APIEndpoint
protocol は、Web API のエンドポイントを抽象化している。リクエスト先の URL と HTTP メソッドを持ち、またレスポンスの型を associated type として持つ。レスポンスは JSONObject
から初期化できるように、JSONDecodable
protocol を新たに用意した。先ほどの JSON のモデルオブジェクトは容易に準拠できる。またデフォルト値を指定しても問題ないようなものには protocol extension でデフォルトを与えた。これらを用いれば、NSURLRequest
を作るのも用意である。
extension APIEndpoint {
var URLRequest: NSURLRequest {
let components = NSURLComponents(URL: URL, resolvingAgainstBaseURL: true)
components?.queryItems = query?.map(NSURLQueryItem.init)
let req = NSMutableURLRequest(URL: components?.URL ?? URL)
req.HTTPMethod = method.rawValue
for (key, value) in headers ?? [:] {
req.addValue(value, forHTTPHeaderField: key)
}
return req
}
}
NSURLComponents
や NSURLQueryItem
を利用することで、URL を作るのが簡単になる。
ここまでくれば、リクエストを送るのも簡単である。
enum APIError: ErrorType {
case EmptyBody
case UnexpectedResponseType
}
enum APIResult<Response> {
case Success(Response)
case Failure(ErrorType)
}
extension APIEndpoint {
func request(session: NSURLSession, callback: (APIResult<ResponseType>) -> Void) -> NSURLSessionDataTask {
let task = session.dataTaskWithRequest(URLRequest) { (data, response, error) in
if let e = error {
callback(.Failure(e))
} else if let data = data {
do {
guard let dic = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] else {
throw APIError.UnexpectedResponseType
}
let response = try ResponseType(JSON: JSONObject(JSON: dic))
callback(.Success(response))
} catch {
callback(.Failure(error))
}
} else {
callback(.Failure(APIError.EmptyBody))
}
}
task.resume()
return task
}
}
基本的には NSURLSession
を利用しているだけだが、associated type の ResponseType
を利用してレスポンスの型を決定している。また callback には APIResult
enum を使うことで、成功か失敗かの2値で表現できる。
これで一般的な Web API との通信が書けるようになったが、今回は GitHub の API のためにさらに特殊化することを考える。
protocol GitHubEndpoint: APIEndpoint {
var path: String { get }
}
private let GitHubURL = NSURL(string: "https://api.github.com/")!
extension GitHubEndpoint {
var URL: NSURL {
return NSURL(string: path, relativeToURL: GitHubURL)!
}
var headers: [String: String]? {
return [
"Accept": "application/vnd.github.v3+json",
]
}
}
GitHub の API は URL を https://api.github.com/
以下に限定できる。これを利用して、APIEndpoint
protocol を継承した GitHubEndpoint
protocol を作成する。これは var URL: NSURL { get }
にデフォルト実装を与え、新たな var path: String { get }
property から URL を生成する。さらにここでは HTTP ヘッダーにもデフォルト実装を加えた。
これらを活用すると、リポジトリの検索 API は以下のようにできる。
struct SearchRepositories: GitHubEndpoint {
var path = "search/repositories"
var query: [String: String]? {
return [
"q" : searchQuery,
"page" : String(page),
]
}
typealias ResponseType = SearchResult<Repository>
let searchQuery: String
let page: Int
init(searchQuery: String, page: Int) {
self.searchQuery = searchQuery
self.page = page
}
}
これは GitHubEndpint
protocol と APIEndpoint
protocol に準拠する。利用するのも非常に簡単で、以下のように書くだけでよい。
SearchRepositories(searchQuery: "Hatena", page: 0).request(NSURLSession.sharedSession()) { (result) in
switch result {
case .Success(let searchResult):
print(searchResult)
case .Failure(let error):
print(error)
}
}
ここで検索結果を表示する画面を作っていく。UITableViewController
のサブクラス MasterViewController
を開く。テンプレート的な実装が書いてあるが、不要なところは消しておく。
UITableView
への表示は、UITableViewDataSource
と UITableViewDelegate
のふたつのデリゲートを実装することで行う。iOS アプリの実装ではこのようなデリゲートパターンを多用する。
protocol UITableViewDataSource
は、UITableView
に表示する内容を提供するためのものである。必ず実装しなければならないふたつのメソッドがあり、func tableView(_:numberOfRowsInSection:)
は行数を返し、func tableView(_:cellForRowAtIndexPath:)
においてそれぞれの行 (Cell) を返す。
protocol UITableViewDelegate
は内容を提供する以外の様々な役割を果たす。例えば特定の行がこれから表示されることを伝える func tableView(_:willDisplayCell:forRowAtIndexPath:)
などがある。
UITableViewController
では、表示される UItableView
の二つのデリゲートは view controller 自身になる。MasterViewController
でもこれらを実装していく。
まずは表示するデータの元となる配列を property var repositories: [Repository] = []
として用意する。このとき、配列の内容に変化があったら UITableView
をリロードしたいので、property observer を設定しておく。
var repositories: [Repository] = [] {
didSet {
tableView.reloadData()
}
}
これを元にして UITableViewDataSource
の必須のメソッドを実装する。
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return repositories.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let repository = repositories[indexPath.row]
cell.textLabel?.text = repository.name
return cell
}
行数は配列の count
と一致するはずである。また UITableViewCell
は、パフォーマンスのために UITableView
の中で再利用される。dequeueReusableCellWithIdentifier(_:forIndexPath:)
で、UITableView
から再利用するための UITableViewCell
を取得する。このとき、identifier 引数と一致する reuse identifier の UITableViewCell
を事前に UITableView
に登録しておく必要がある。いくつかの方法があるが、Storyboard 上で設定しておくのが簡単である。
- UITableView Class Reference
- UITableViewCell Class Reference
- UITableViewDelegate Protocol Reference
- UITableViewDataSource Protocol Reference
- UITableViewController Class Reference
このような実装があれば、後は var repositories: [Repository]
プロパティを操作するだけでよい。ここでは viewDidLoad()
メソッドの中で API からリポジトリ一覧を取得する。
override func viewDidLoad() {
super.viewDidLoad()
SearchRepositories(searchQuery: "Hatena", page: 0).request(NSURLSession.sharedSession()) { (result) in
switch result {
case .Success(let searchResult):
dispatch_async(dispatch_get_main_queue()) {
self.repositories.appendContentsOf(searchResult.items)
}
case .Failure(let error):
print(error)
}
}
// 残りの処理
}
コールバックの中で dispatch_async
関数を利用している。ネットワーク通信は非同期に行われメインスレッドでは処理しない一方で、ユーザーインターフェイスは必ずメインスレッドから操作しなければならない。これは GUI システムの多くに存在する制約である。ここで利用している dispatch_async
や dispatch_get_main_queue
関数は、GCD (Grand Central Dispatch) と呼ばれるものの一部である。GCD は内部にスレッドプールを持ち、キューとタスクの概念によって粒度の小さいタスクでも効率的に並列実行できるようになっている。今回は単にメインスレッドで処理したいだけなので、メインスレッドと紐付けられた main queue を取得し、そこで実行するようにしている。
Web API と通信する際、viewDidLoad()
メソッドを利用した。これは view controller のライフサイクルメソッドの一つである。View controller にはライフサイクルが存在し、生成されて表示され、さらに非表示になるまで、いくつもの段階を追うことができる。このライフサイクルに合わせて処理を行うことが非常に重要である。
よく使われるライフサイクルメソッドは以下のようなものである。
loadView()
viewDidLoad()
viewWillAppear(_:)
viewDidAppear(_:)
viewWillDisappear(_:)
viewDidDisappear(_:)
loadView()
は UIViewController
の var view: UIView!
property が nil
のとき、view
にアクセスすると呼び出される。デフォルトでは view
property に空の UIView
のインスタンスをセットする。また Storyboard などを利用していれば、紐付けられた view が読み込まれる。このメソッドを override して、独自の view を読み込むようにしてもよい。UITableViewController
の場合はここで UITableView
のインスタンスがセットされている。こうして view
が読み込まれると、次に viewDidLoad()
が呼び出される。この時点で view が生成されていることが保障されるので、これを前提とした追加の処理を行うことができる。
viewWillAppear(_:)
や viewDidAppear(_:)
は画面上に view が表示される前後にそれぞれ呼び出される。同様に viewWillDisappear(_:)
や viewDidDisappear(_:)
は view が画面上から消える前後に呼び出される。画面の表示に関する処理は、これらのメソッドを override して実装することになる。
Cell が選択されたら画面遷移をして、詳細が表示されるようにする。詳細画面は DetailViewController
を利用する。テンプレートではすでに、cell が選択されたら詳細画面が表示されるようになっている。これは、cell から Show Detail Segue が伸びて view controller と接続されているためである。
このままでも画面の遷移はできているが、どの cell が選択され、選択された cell が表していた Repository
は何なのかが詳細画面に伝わっていない。prepareForSegue(_:sender:)
メソッドを override して、遷移先の画面に情報を渡すことができる。
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let repository = repositories[indexPath.row]
let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
controller.repository = repository
}
}
}
Storyboard で設定された segue の identifier に合わせて処理を行うことができる。
遷移先の view controller である DetailViewController
では Repository
のデータを表示する。
class DetailViewController: UIViewController {
@IBOutlet weak var detailDescriptionLabel: UILabel!
var repository: Repository? {
didSet {
configureView()
}
}
private func configureView() {
if let repository = repository {
if let label = detailDescriptionLabel {
label.text = repository.name
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
configureView()
}
}
最初に @IBOutlet
属性が付加された detailDescriptionLabel
property がある。これは view が読み込まれる際に、Storyboard で設定した UILabel
が自動的に接続される。このような設定は Interface Builder 上で行うことができる。View が読み込まれる前は nil
値になっているので注意する必要がある。
@IBOutlet
と同様に、@IBAction
属性を付けたメソッドは Interface Builder から参照できる。UIButton
などの UIControl
オブジェクトから、UIControlEvents.TouchUpInside
などのイベントを接続し、必要な処理を行うようにすることができる。
UIKit における操作可能な UI 要素の多くは UIControl
を継承している。それらは例えば UIButton
や UISlider
など、様々な形態を取る。これらは UIControlEvents
を発生させ、target-action パラダイムによりイベントを送信する。UIControl
の addTarget(_:action:forControlEvents:)
メソッドを呼び出し、イベント送信先のオブジェクト (target) と、メソッドへの参照である Selector
(action)、送信されるべきイベントの種類である UIControlEvents
を指定する。Selector
はメソッドの参照を #selector()
式に与えることで得られる。
例えば UIButton
なら button.addTarget(self, action: #selector(self.buttonDidTapped(_:)), forControlEvents: .TouchUpInside)
などとすると、func buttonDidTapped(sender: UIButton)
メソッドを呼び出させることができる。これは Interface Builder と @IBAction
設定できるものと同じである。
UIControl
を継承していない view や、あるいは単純ではないジェスチャーを扱うために、UIGestureRecognizer
を利用することもできる。UIGestureRecognizer
の種々のサブクラスを UIView
に addGestureRecognizer(_:)
で追加することができ、タップやスワイプなどのジェスチャーに対する target や action を設定できる。
より低レベルなイベントをハンドリングするためには、UIEvent
と UIResponder
の機能を利用する。UIView
や UIViewController
は UIResponder
のサブクラスである。UIKit は、例えば画面上で発生したタッチイベントを、イベントが起きた位置の view に UIEvent
として渡す。これは UIResponder
の nextResponder()
を辿って、どこかでハンドリングされるまで伝播していく。UIControl
や UIGestureRecognizer
はこの仕組みの上で、特定のパターンを target-action で伝えてくれる。
- UIControl Class Reference
- UIGestureRecognizer Class Reference
- UIResponder Class Reference
- UIEvent Class Reference
UIView
のレイアウトは、Auto Layout と呼ばれる仕組みによって行われる。Auto Layout では、NSLayoutConstraint
オブジェクトによって表現される制約を組み合わせることで、view のレイアウトを解決する。例えば、view の横幅や高さに関する制約や、他の view との位置関係を決める制約を作ることができる。ひとつの NSLayoutConstraint
にはふたつの item (view など)とそれぞれの attribute (「高さ」や「水平方向の中心」、「ベースライン」など)、そして relation (等しさや大小関係)と priority が存在する。
UIView
は intrinsicContentSize()
メソッドで自身が表示されるべき最適なサイズを返すことができる。短い文字列を表示する UILabel
であればちょうど文字列が収まるサイズに相当する。高さや幅に関する制約がなければ自動的にそのサイズに決まる。
NSLayoutConstraint
には整数値で表現される priority
があり、制約同士が矛盾する場合には大きい方から順に優先される。最大値の 1000
は、必ず充たされなければならない制約である。また UIView
にも、intrinsicContentSize()
より拡がらないための priority contentHuggingPriorityForAxis(_:)
と、狭まらないための priority contentCompressionResistancePriorityForAxis(_:)
があり、それぞれデフォルトでは 250
と 750
の値が設定されている。つまり狭まりにくく拡がりやすいようになっている。
NSLayoutConstraint
は Interface Builder から GUI 上で設定することができるほか、イニシャライザ init(item:attribute:relatedBy:toItem:attribute:multiplier:constant:)
や visual format language で初期化できる。また UIView
の layout anchor を利用することもできる。Visual format language では、例えば H:|-8-[view]-8-|
のような書式で、複雑な複数の制約を一度に作ることができる。Layout anchor は NSLayoutAnchor
のインスタンスで、例えばふたつの UIView
の var topAnchor: NSLayoutYAxisAnchor { get }
property を使って view1.topAnchor.constraintEqualToAnchor(view2.topAnchor)
とすると、view の上辺が揃う制約を作ることができる。
Auto Layout の詳細は Apple のドキュメント “Auto Layout Guide” で説明されている。
課題では、これまで作ってきた Intern::Diary
の iOS アプリを作る。必要となる JSON API を用意し、これを利用したアプリを作成する。
iOS アプリで日記の記事一覧を表示できるようにする。表示には UITableView
を使うこと。また個別の記事を選択したとき、個別の記事画面に遷移するようにすること。
創意工夫をしてより便利なアプリにする。
Xcode でアプリを開発する際には、ふたつの方法で自動テストすることができる。プログラムの個々のモジュールが期待どおりに動作することを確かめるユニットテストと、アプリを操作して正しく動作することを確認する UI テストである。Xcode においては、これらは XCTest
フレームワークの機能として提供される。これらの機能はプロジェクトエディタでテスト用のターゲットを追加することで利用できる。
Xcode の自動テストは、XCTestCase
のサブクラスとして記述する。メソッド名が test
から始まるものがテストメソッドで、setUp()
や tearDown()
は全てのテストメソッドの前後に実行される。テストメソッドの内部で XCAsset
から始まる関数群を呼び出し、個々のアサーションとする。
このように自動テストは、テストケースクラスとテストメソッド、そしてアサーションを組み合わせて作られる。
import XCTest
@testable import GitHub
class GitHubTests: XCTestCase {
override func setUp() {
super.setUp()
}
override func tearDown() {
super.tearDown()
}
func testExample() {
XCTAssertEqual(1, 1, "Message")
}
}
ユニットテストでは、単にプログラムの特定の部分についてテストを行う。主に公開 API について、その振る舞いが正しいことをテストする。
テスト対象の Swift のモジュールは、テストコードとは異なるモジュールになっている。すなわち import
する必要があり、また public
の可視性を持ったシンボルしか参照できない。ただし @testable import
することで internal
の可視性を持つものを参照することができる。
UI テストでは、アプリを実際に起動してから個々のアサーションを実行することになる。この場合も基本的な書き方はユニットテストと同じである。まずは setUp()
でアプリを起動する。UI テストでは XCUI
から始まるクラスが利用できる。
override func setUp() {
super.setUp()
continueAfterFailure = false
XCUIApplication().launch()
}
テストメソッドでは、UI を操作してその結果が正しいことをアサーションしていくことになる。UI を操作するコードは一般に煩雑であり、手で書くのは比較的難しいので、Xcode では人間の操作を記録することができるようになっている。
Xcode のテストについては “Abount Testing with Xcode” が詳しい。
iOS/macOS プログラミングにおけるデバッグの手法を簡単に紹介する。
ログを出してデバッグする。原始的な方法ではあるが柔軟でもある。
let dict = [ "a" : "b" ]
print("Dictionary: \(dict)")
必要に応じて CustomStringConvertible
などを実装しておくとよい。
ブレークポイントを設定することで、実行中のプログラムを特定の位置で止めることができる。Xcode から GUI でブレークポイントを操作できる。
例外発生時に止まるブレークポイントを設定することができ、例外の原因を辿りやすくなる。
デバッガについては “Debugging with Xcode” が詳しい。
Xcode に付属する Instruments を使うと、さらに高度な解析が簡単に行える。メモリリークの発見やパフォーマンスのチューニングなど、様々に利用できる。詳しくは “Instruments User Guide” を参照すること。
ライブラリを利用する際にはいくつかの方法がある。ここでは CocoaPods や Carthage を紹介する。
CocoaPods はコミュニティによって開発されている Ruby 製のツールである。下記のような Gemfile
を置いて bundle install
する。以降は bundle exec pod
で pod
コマンドを利用する。
source 'https://rubygems.org'
gem 'cocoapods'
bundle exec pod init
すると空の Podfile
ができる。ここに必要なライブラリを書く。テストにだけ必要なライブラリなどは target 毎に書くとよい。
platform :ios, '9.0'
use_frameworks!
pod 'AFNetworking'
Podfile を保存したらそのディレクトリで bundle exec pod install
する。
ここで一度 Xcode のプロジェクトを閉じて、プロジェクトと同じディレクトリの .xcworkspace
という拡張子のファイルを開く。開くと左サイドバーのプロジェクトナビゲーターに、元々あるプロジェクトに加えて Pods というプロジェクトが表示される。こうすることで、アプリをビルドするときに CocoaPods 管理下のライブラリを同時にビルドできる。
Carthage は近年新しく登場したツールで、Swift で書かれている。Homebrew を利用して brew install carthage
でインストールできる。プロジェクトのあるディレクトリに Cartfile
というファイル名で以下のようなファイルを保存する。
github "AFNetworking/AFNetworking"
github
や git
でライブラリのリポジトリを指定する。ここで carthage update
を実行すると、Carthage/Build ディレクトリにフレームワークが生成される。
Xcode でアプリのターゲットの設定を開き、General タブの Linked Frameworks and Libraries からいま生成したフレームワークを選択する。さらに Build Phases を開いて + ボタンを押し、New Run Script Phase を選択する。ここに以下のようなシェルスクリプトを設定する。
/usr/local/bin/carthage copy-frameworks
そして Input Files に $(SRCROOT)/Carthage/Build/iOS/AFNetworking.framework
など、必要な全てのフレームワークを設定する。
ここで紹介した CocoaPods も Carthage も、Dynamic Framework という形でライブラリを読み込めるようにしてくれる。ここで Dynamic Framework と呼んでいるのは、Apple の Framework 形式の形で作られた実行ファイルを含むディレクトリであり、アプリの実行時に動的リンクされるものである。これらは Swift 上ではモジュールを形成するので、これらの方法で用意したライブラリは import
することで利用できる。