iOSDC Japan 2016 に参加してきた!
8/19, 20 にかけて開催された iOSDC Japan 2016 に参加してきました!
記念すべき第一回でした!
次回開催についてですが、 SNS やブログ等で拡散されていけばあるかもということでした。是非来年も開催されてほしいです!
iOSDC Japan 2016 の詳細はこちら↓
自分は iOS 開発を始めて期間が浅く、話を聞くのでいっぱいいっぱいで全然メモを取れませんでしたが、わずかに残したのでそれをちょろっと書きます。発表資料を公開されている方もいるので、そちらのリンクも貼っておきます。
AB Tests in Mobile App
kazunori kikuchi (@kichikuchi) さん
iOS アプリ開発の補助ツールのベストプラクティス
宇佐見 公輔 (@usamik26) さん
ライブラリ管理
Carthage とか
- アプリとライブラリが分離される
- バージョンが分かる
リソース取り込み
SwiftGen とか
コードチェック
SwiftLint とか
- Warning を見逃さない
- 量が多すぎて辛いならば、チェックをゆるくして、チェックを継続したほうが良い
- そうすれば大事な Warning を見落とさない
- SwiftLint の autoformat とかのコードフォーマットで自動変換できる
デザイナーにStoryboardをお任せする技術
Hiroki Kato (@cockscomb) さん
iOSアプリのリモートサポートツール「ミレタ」の作り方 #WebRTC #Swift #PrivatePod
Yuichiro Masui (@masuidrive) さん
Swift で JavaScript 始めませんか?
熊谷 友宏 (@es_kumagai) さん
www.slideshare.net
Xcode で快適なデバッグライフを追い求める
Toshihiro Morimoto @dealforest さん speakerdeck.com
クラッシュしたら AppDelegate だった
ExceptionBreakpoint を追加
exception が発生したタイミングで breakpoint
意図とは違うタイミングで break してしまうことがDiagonostics を設定
不正なメモリ操作を検知
どの ViewController かわからない
Symbolic Breakpoint with action を使って viewDidLoad と viewWillApear が呼ばれた時に break されるようにする
起動時に ViewController を指定したい
EnvironmentVariables で設定する
端末のログやファイルを取得したい
LLDB を使って slack に送信
全然メモ取っていなくてすみません。。。
constraint を動的に追加 / 削除する
AutoLayout でレイアウトに必要な constraint を追加する場合、 Storyboard 上で指定する方法と、コードで指定する方法があります。
Storyboard では静的な constraint であれば簡単に追加できますが、動的に追加、そして削除したい場合には、コードで指定しなければならないと思います。
以下では、対象の constraint はつくられているとして、その constraint を動的に追加 / 削除する方法を記述します ( iOS8 以上) 。
方法は、メソッドによる指定とプロパティによる指定の 2 通りがあります。
メソッドによる指定
これらのメソッドは、引数に [NSLayoutConstraint]
をとるため、複数の constraint を 1 行で指定することができます。
let constraints = [hogeConstraint, fugaConstraint] NSLayoutConstraint.activateConstraints(constraints) // constraint の追加 NSLayoutConstraint.deactivateConstraints(constraints) // constraint の削除
プロパティによる指定
単一の constraint を指定する場合にはこちらの方法で良いかと思います。
個人的にはこちらの方が読みやすくて好きです。
hogeConstraint.active = true // constraint の追加 hogeConstraint.active = false // constraint の削除
注意点
注意したいのは、 削除した constraint を再び追加する場合です。
具体的には、 Stroyboard 上で指定した constraint を IBOutlet で接続し、それを状況に応じて削除、そして再び追加するということをしたい場合です。
このとき、constraint の参照を weak として IBOutlet で接続してしまうと、削除したのちに再び追加しようとするとエラーになってしまいます。
@IBOutlet internal var hogeConstraint: NSLayoutConstraint! // weak 参照 hogeConstraint.active = false hogeConstraint.active = true // 実行時エラーになる
このエラーを解消するためには、 参照を strong にする必要があるようでした。
@IBOutlet internal var hogeConstraint: NSLayoutConstraint! // strong 参照 hogeConstraint.active = false hogeConstraint.active = true // エラーにならない
追記
strong にする必要があると書きましたが、強参照にすると循環参照の恐れがあるので、weak のままで true にする直前にフォースアンラップ等で nil チェックをする方が良さそうかなという気がしています。
参照
UITableView のセルの高さを可変にする
高さが可変の UILabel や UIImageView を UITableView のセルに配置したときには、もちろんセルの高さも可変にしたいと思います。
その方法を書きます。
ちなみに UILabel の行数を可変にする方法はこちらのエントリで書きました。
セルの高さを可変にする
と言っても簡単で、以下のメソッドを実装してセルの見積もりの高さと、実際の高さを UITableViewAutomaticDimension
で指定するだけです。
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { return 100 // セルの高さの見積もり } func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { return UITableViewAutomaticDimension }
UITableView は estimatedRowHeight
, rowHeight
プロパティを持っているので、上記のメソッドを実装せずに、以下のように viewDidLoad()
等で直接指定しても大丈夫です。
override func viewDidLoad() { super.viewDidLoad() tableView.estimatedRowHeight = 100 // セルの高さの見積もり tableView.rowHeight = UITableViewAutomaticDimension }
見積もりの高さ
UITableView では UILabel 等の高さからセルの高さを計算させる必要があるのですが、計算処理が複雑な場合に負荷が大きくなってしまいます。
そこで、見積もりの高さを指定することで、 実際にセルを描画するタイミングまで見積もりの高さを使って計算させ、実際の計算を遅延させることができます。
そのため、この高さの指定は大体で構いません。
注意点
estimatedRowHeight
の値を CGFloat.min
で指定すると、 iPhone5 等の 32ビットCPU のデバイスではクラッシュしてしまいます。
また、 64ビットCPU のデバイスでも、描写されたセルの個数が指定した数でなかったりすることがあるようなので、 CGFloat.min
で指定せずに、マジックナンバーで指定するのがよさそうです。
UILabel の行数を可変にする
Storyboard で UILabel を選択し、 Attributes inspector から Lines を 0 にするだけです。
この値が UILabel の行数で、デフォルトの 1 のままだと 1 行固定となり、 テキストが複数行にわたる場合は、 ...
と省略されて表示されるようになります。
今回のように 0 にすると、行数にかかわらず複数行で表示することが可能となります。
ちなみに、UILabel の height が固定になっていると、その height で表示できる行数だけしか表示されず、以降のテキストは ...
と省略されてしまいます。
そのため、この設定とは別に高さが可変になるような Auto Layout も設定しなければならないと思うので注意が必要です。
Objective-C の Category と Swift の Extension
既存のクラスを拡張したいときに、 Objective-C であれば Category 、 Swift であれば Extension により実装していきます。 それぞれの実装方法の例です。
Objective-C
NSDate を拡張して sampleMethod
というメソッドを実装したい場合、 Objective-C では以下のように記述します。
// NSDate+SampleProject.h #import <Foundation/Foundation.h> @interface NSDate (SampleProject) - (void)sampleMethod; @end
// NSDate+SampleProject.m @implementation - (void)sampleMethod { // hoge } @end
Swift
上記と同じメソッドを Swift の Extension で記述すると以下のようになります。
// NSDate+SampleProject.swift import Foundation extension NSDate { func sampleMethod() { // hoge } }
Objective-C から Swift の Extension を利用する
ちなみに、 Objective-C から Swift の Extension を利用したいときは、 Objective-C ファイルで #import "sampleProject-Swift.h"
を記述しておくだけで大丈夫です。
Swift でページングを実装する
UICollectionView を使って一覧画面を作成していて、画面を一番下までスクロールしたら次ページをロードして表示する、というようにページング処理をさせたいときには、 UICollectionReusableView と UIActivityIndicatorView を使うと良いそうです。
これらを使ってページング処理をさせるときには、 UICollectionReusableView でフッターとなる View を作成し、 UIActivityIndicatorView でローディングインジケータを作成することになります。
ページング処理の大まかな流れは以下の通りです。
- 画面を一番下までスクロールするとフッターが表示される。
- フッターが表示されたらインジケータを表示する。ぐるぐるさせる。
- 次ページを読み込む処理を実行する。
- 次ページを読み込む処理が完了したらぐるぐるやめる。インジータを隠す。
フッターが表示されたら という部分は、 collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath)
を使うことで実装できます。
具体的には以下のように記述します。
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView { if kind == UICollectionElementKindSectionFooter { let reusableView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "UICollectionReusableView に設定した Idenfier", forIndexPath: indexPath) // インジケータをぐるぐるさせる処理 // 次ページを読み込む処理 return reusableView } return UICollectionReusableView() }
Alamofire で APIClient を書いてみた
APIClient はどういう設計が良いのかあとずっと悩んでいたのですが、なんとなく形になったような気がしたので書いてみます。
Swift は型安全な言語なのですが、 API から取得される値は様々な型を取り得るので、それを汎用的に扱えるようにするためにはどうしたら良いなかあ、型の扱い方が難しいなあと感じました。
初心者なので鵜呑みにはしないでくださいな。
APIClient
サーバ側で 404 等のエラーが 発生した際は ["error": "エラーメッセージ"]
というレスポンスボディが返ってくることを想定しています。
Alamofire では、リクエストメソッドを Alamofire.Method で指定する必要があり、APIClient を使う Class を記述したファイル内で、毎回 import Alamofire
しなければならなそうで嫌だなあと思ったので、 Enum を使って Alamofire.Method に変換できるようにしました。
サーバ側でエラーが発生した際には、リクエスト自体には成功しているので response.result.isSuccess()
が true となります。
そのため、 response.result.isSuccess()
が false となるのはリクエストそのものに失敗しているときだと思うのですが、アプリ側では、サーバ側のエラーとリクエストそのものが失敗したときのエラーを同じエラーとして扱いたいなあと思ったので、 APIResponse という Struct を用意して、 response の値を APIResponse に変換するようにしてみました。
import Alamofire class APIClient { static let host = "http://localhost:3000/" static func httpRequest(method: RequestMethod, endpoint: String, parameters: [String: AnyObject]?, handler: (APIResponse) -> Void) { Alamofire.request(method.toAlamofile(), host + endpoint, parameters: parameters) .responseJSON { response in if response.result.isSuccess { handler(APIResponse(code: response.response!.statusCode, value: response.result.value!)) } else { handler(APIResponse(value: ["error": "通信に失敗しました"])) } } } } enum RequestMethod { case GET case POST case PUT case DELETE func toAlamofile() -> Alamofire.Method { switch self { case .GET: return .GET case .POST: return .POST case .PUT: return .PUT case .DELETE: return .DELETE } } } struct APIResponse { var status: APIStatus var value: AnyObject? var errorMessage: String? enum APIStatus { case Success case Failure func isSuccess() -> Bool { switch self { case .Success: return true case .Failure: return false } } } init(code: Int = 0, value: AnyObject) { switch code { case 200...299: status = APIStatus.Success self.value = value default: status = APIStatus.Failure self.errorMessage = value["error"] as? String } } }
使うとき
リクエスト先となるエンドポイントと、リクエストパラメータを用意します。
response には APIResponse のインスタンスが入ってきます。 response.status.isSuccess()
でリクエストに成功したかを判断し、成功 or 失敗 の処理を分けて記述します。
func test() { let endpoint = "accounts" // エンドポイント let parameters = ["page": 1] // リクエストパラメータ APIClient.httpRequest(.GET, endpoint: endpoint, parameters: parameters) { response in if response.status.isSuccess() { print(response.value!) // 通信に成功したときの処理 } else { print(response.errorMessage!) // 通信に失敗したときの処理 } } }