iOSDC Japan 2016 に参加してきた!

8/19, 20 にかけて開催された iOSDC Japan 2016 に参加してきました!
記念すべき第一回でした!
次回開催についてですが、 SNS やブログ等で拡散されていけばあるかもということでした。是非来年も開催されてほしいです!

iOSDC Japan 2016 の詳細はこちら↓

iosdc.jp

自分は iOS 開発を始めて期間が浅く、話を聞くのでいっぱいいっぱいで全然メモを取れませんでしたが、わずかに残したのでそれをちょろっと書きます。発表資料を公開されている方もいるので、そちらのリンクも貼っておきます。


AB Tests in Mobile App

kazunori kikuchi (@kichikuchi) さん

speakerdeck.com

iOS アプリ開発の補助ツールのベストプラクティス

宇佐見 公輔 (@usamik26) さん

speakerdeck.com

ライブラリ管理

Carthage とか

  • アプリとライブラリが分離される
  • バージョンが分かる

リソース取り込み

SwiftGen とか

コードチェック

SwiftLint とか

  • Warning を見逃さない
  • 量が多すぎて辛いならば、チェックをゆるくして、チェックを継続したほうが良い
  • そうすれば大事な Warning を見落とさない
  • SwiftLint の autoformat とかのコードフォーマットで自動変換できる

デザイナーにStoryboardをお任せする技術

Hiroki Kato (@cockscomb) さん

iosdc.jp

iOSアプリのリモートサポートツール「ミレタ」の作り方 #WebRTC #Swift #PrivatePod

Yuichiro Masui (@masuidrive) さん

iosdc.jp

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 チェックをする方が良さそうかなという気がしています。

参照

tomoyaonishi.hatenablog.jp

UITableView のセルの高さを可変にする

高さが可変の UILabel や UIImageView を UITableView のセルに配置したときには、もちろんセルの高さも可変にしたいと思います。

その方法を書きます。

ちなみに UILabel の行数を可変にする方法はこちらのエントリで書きました。

komaji504.hateblo.jp

セルの高さを可変にする

と言っても簡単で、以下のメソッドを実装してセルの見積もりの高さと、実際の高さを 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 にするだけです。

f:id:komaji504:20160731171427p:plain

この値が 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 でローディングインジケータを作成することになります。

ページング処理の大まかな流れは以下の通りです。

  1. 画面を一番下までスクロールするとフッターが表示される。
  2. フッターが表示されたらインジケータを表示する。ぐるぐるさせる。
  3. 次ページを読み込む処理を実行する。
  4. 次ページを読み込む処理が完了したらぐるぐるやめる。インジータを隠す。

フッターが表示されたら という部分は、 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!)  // 通信に失敗したときの処理
        }
    }
}