Swift2でiOSアプリの開発を始める前に知っておくべきこと

こんにちは、プラットフォーム開発グループの山口洋平です。

現在、新規iOSアプリをSwiftを使って開発しています。

Swift2

先月WWDC2015が開催され、そこで Swift2 が発表されました。

Swiftのオープンソース化など色々と話題になりましたが、Swift自体も様々な機能拡張が行われ、これからのiOSアプリ開発は Swift2 が主流になっていくと思われます。

Swift2を使ったアプリの申請は9月頃まで待つ必要がありますが、今回は先立ってSwift2を使ってコードを書くなら知っておくべき機能、テクニックについて紹介していきたいと思います。

動作確認環境

Xcode 7.0 β4 で動作確認を行いました。 β版なので、今回紹介するコードは今後動かなくなる可能性があります。

guard による早期リターン

Optional型を扱うとき、その値がnilなら早く処理を終えたいと思うことが何度もありました。 この場合、以下のようにネストが1つ深くなってしまうか、Forced Unwrapping する必要がありました。

1
2
3
4
5
6
7
8
9
10
11
12
13
if let unwrappedName = userName {
    print("Your username is \(unwrappedName)")
} else {
    return
}
// あるいは
if userName == nil {
    return
}
print("Your username is \(userName!)") // forced unwrapping!

Swift2から新規に追加された guard を使えば、こういった状況にスッキリ書けます。

1
2
3
4
5
guard let unwrappedName = userName else {
    return
}
print("Your username is \(unwrappedName)")

if文に似ていますが、guard に与えられた条件が満たされなかった場合にelse節の処理が実行されます。 guard let でバインドした変数は、guard 以降の行でも参照できるようになります。

case によるパターンマッチ

Swiftでは、enum に Associated Values といって値を関連付けて保存させることができます。

1
2
3
4
enum Barcode {
    case UPCA(Int, Int, Int, Int)
    case QRCode(String)
}

Swift1.2までだと関連付けられた値を取り出すのに、以下のように switch 文を書くしかありませんでした。

1
2
3
4
5
6
7
8
let barcode = Barcode.QRCode("ABCDEFGHIJKLMNOP")
switch barcode {
case .QRCode(let productCode):
    print("QR code: \(productCode).")
default:
    break
}

値を取り出すとき処理をenumのメソッドにするという手もありますが、ある特定の場合(case)の値を取り出したいだけなのに default: break のような余計なコードを書く必要があります。 個人的には default 節も新しい場合(case)が追加されたときに見過ごしやすいので、むやみには書きたくないです。

Swift2から case のパターンマッチが進化して、enumの値をピンポイントで取り出せるようになりました。

1
2
3
if case .QRCode(let productCode) = barcode {
    print("QR code: \(productCode).")
}

enum以外にも様々なパターンマッチができるので、続けて紹介します。

Optional型のパターンマッチ

1
2
3
4
5
6
7
8
9
10
11
let n:Int? = 10
// "case let xxx?" を Optional Pattern と呼ぶ
if case let number? = n {
    print(number)
}
// Optionalもenumなので、実質同じ
if case .Some(let number) = n {
    print(number)
}

タプル型のパターンマッチ

1
2
3
4
5
6
7
8
9
10
11
let xyz = (1, 2, 3)
// マッチする
if case (1, let y, let z) = xyz {
    print("\(y), \(z)") // 出力される
}
// これはマッチしない
if case (2, let y, let z) = xyz {
    print("\(y), \(z)")
}

範囲演算子のパターンマッチ

1
2
3
4
5
let age = 20
if case 10...25 = age {
    print("OK")
}

以上、case によるパターンマッチを紹介しました。 自由度が高くて使いこなすのは時間がかかりそうですが、是非マスターしたいです。

for文でのフィルタリング

コンテナ型の値をイテレーションしていく時に、条件にマッチしたもののみ列挙します。 Array.filter のようなものですが、CollectionType Protocolを実装しているクラスであれば for 文でのフィルタリングができます。

for文でのフィルタリングを使えば、以下のようなコードが

1
2
3
4
5
6
7
let array = [1,2,3,4,5]
for number in array {
    if number % 2 == 0 {
        print(number)
    }
}

こう書けます。

1
2
3
4
5
let array = [1,2,3,4,5]
for number in array where number % 2 == 0 {
  print(number) // 2, 4
}

また、上で紹介した case とともに使うことも可能です。

1
2
3
4
5
let array:[Int?] = [1, 2, nil, 3, 4, nil, 5]
for case .Some(let number) in array where number % 2 == 0 {
    print(number)   // 2, 4
}

@testable

今までは、Swiftで自動テストを動かすには

  1. テストしたいファイルをテストターゲットに含めるか
  2. テストしたいクラスやメソッドに public とつける

のどちらかをする必要がありました。

アプリを開発するときに最初は前者の方法をとっていたのですが、DBにRealmをつかっていると、 テストコード側でRealmモデルを dynamic cast できないという問題があり、後者のpublicをつけるやり方をとっていました。 しかしながら、外部に公開する必要のないクラスをテストするために、本来publicを付ける必要があるのはイケてないですね。

Xcode7からは @testable を使えば internal なクラスやメソッドが参照できるようになります。 使い方は以下のように、テストコード側で @testable を付けるだけでOKです。

1
@testable import MyApp

今までテストしたいクラスに public といちいちつけていたので、@testable は地味に嬉しいですね。

Error Handling

Swift2により新たにエラーハンドリングの仕組みが導入されました。 さっそく使い方から見ていきます。

使い方

まずはエラーの種類を定義します。エラーを enum で定義して、ErrorType プロトコルを実装します。

1
2
3
4
5
enum SignUpViewErrorType : ErrorType {
  case IDLengthTooShort
  case IDAlreadyUsed
  case PasswordLengthTooShort
}

次はエラーを投げたり、補足したりする部分です。

  • エラーを投げる→throw
  • エラーを捕捉する→catch

を使います。

以下のサンプルコードではユーザの登録画面をイメージしてください。 登録したいIDとパスワードを入力して、登録ボタンをタップします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 登録ボタンをタップしたときの IBAction
@IBAction func didTapSignUpButton(sender: UIButton) {
  let userId   = self.userIdTextField.text ?? ""
  let password = self.passwordTextField.text ?? ""
  do {
    // エラーを投げるメソッドを呼び出すときは try を付ける
    try self.signUp(userId, password: password)
    // エラーを補足したい場合は
    //      catch <エラーの種類>
    } catch SignUpViewErrorType.IDLengthTooShort {
      print("IDが短いです")
    } catch SignUpViewErrorType.PasswordLengthTooShort {
      print("パスワードが短いです")
    } catch let error {
      print(error)
    }
}
// アカウント登録処理を実行する
// Validationをして、入力が妥当でなければエラーを投げる
// エラーを投げるメソッドに対して throws を付ける
func signUp(userId: String, password: String) throws {
  if userId.characters.count < 4  {
    // IDが短いというエラーを投げる
    throw SignUpViewErrorType.IDLengthTooShort
  }
  if password.characters.count < 4 {
    // パスワードが短いというエラーを投げる
    throw SignUpViewErrorType.PasswordLengthTooShort
  }
  // 登録APIをリクエストする
  ...
}

defer と組み合わせる

この登録画面に対して、「ボタンをタップしたら押せなくして、エラーが起こったり通信が終わるとボタンを押せるようにする」 という処理を加えるとします。ボタンを連続でタップされるのを防止するためです。

処理が成功しようが、失敗しようが必ず実行させたい処理がある場合は defer が使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@IBAction func didTapSignUpButton(sender: UIButton) {
    let userId   = self.userIdTextField.text ?? ""
    let password = self.passwordTextField.text ?? ""
  // ボタンを押せなくする
  sender.enabled = false
  do {
    defer {
      // ボタンを押せるようにする
      // エラーが発生しようが、しまいが必ず実行される
      sender.enabled = true
    }
    // エラーを投げるメソッドを呼び出すときは try を付ける
    try self.signUp(userId, password: password)
    // エラーを補足したい場合は
    //      catch <エラーの種類>
  } catch SignUpViewErrorType.IDLengthTooShort {
    print("IDが短いです")
  } catch SignUpViewErrorType.PasswordLengthTooShort {
    print("パスワードが短いです")
  } catch let error {
    print(error)
  }
}

defer は do と 最初の catch のブロックから抜ける直前で呼び出されます。 本来であればエラーが起こったり、通信が終わるときにボタンを有効にする処理を随所にいれる必要がありますが、deferを使えば1箇所にすっきりまとめることができます。

エラーを無視する

エラーは補足するだけして、エラーの中身はどうでもいいというのであればアンダースコアが使えます。

1
2
3
4
5
6
7
8
9
10
@IBAction func didTapSignUpButton(sender: UIButton) {
  let userId   = self.userIdTextField.text ?? ""
  let password = self.passwordTextField.text ?? ""
  do {
    try self.signUp(userId, password: password)
  } catch _ {
    print("何らかのエラー")
  }
}

エラーが発生したらクラッシュさせる

エラーが投げられたら、あえてクラッシュを発生させたい場合は try! が使えます。 try! の場合 do…catch は省略します。

1
2
3
4
5
6
@IBAction func didTapSignUpButton(sender: UIButton) {
  let userId   = self.userIdTextField.text ?? ""
  let password = self.passwordTextField.text ?? ""
  try! self.signUp(userId, password: password)
}

非同期処理でError Handling使えるのか?

結論から言うと、非同期処理のコールバック内で throw されたエラーは呼び出し元まで伝播させて、捕捉することはできません。

例えば、次のように signUp メソッドにコールバックでレスポンスを受け取るAPIリクエスト処理を追加するとします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Resultパターン
enum Result<T> {
    case Success(T)
    case Failure(NSError)
}
struct APIClient {
    // 登録APIをリクエストする
    static func signUp(userId:String, password: String, completion: (Result<[String:AnyObject]>->Void)) {
        // レスポンスはとりあえず適当に返す
        let error = NSError(domain: NSURLErrorDomain, code: 1000, userInfo: nil)
        completion(Result.Failure(error))
    }
}
// アカウント登録処理を実行する
func signUp(userId: String, password: String) throws {
    ....
    // 登録APIをリクエストする
    APIClient.signUp(userId, password: password) { result in
      if case .Failure(let error) = result where error.code == 1000 {
        // ID登録済みのエラーを投げる
        throw SignUpViewErrorType.IDAlreadyUsed
      }
      // do something.
    }
}

このコードは現状では

1
APIClient.signUp(userId, password: password) { result in

の行で、以下のようなコンパイルエラーとなります。

1
invalid conversion from throwing function of type ‘(_) throws -> Void’ to non-throwing function type ‘(Result<String:AnyObject>) -> Void’

メソッドのシグネチャ(型)が違うからキャストできないと言われているのですが、 その違いとはエラーをthrowするかどうかです。 APIClient.signUp の引数ではthrowをしないコールバックの定義になっているに対し、実際に使うコールバックの中でthrowしているためこのコンパイルエラーが発生しています。

現状、メソッド定義の引数に渡すコールバックがthrowするという指定はできないので、 このコンパイルエラーを解消するにはコールバック内に do … catch のブロックを書くか、そもそもコールバック内で throw しないかのどちらかだと思います。

ということから、現状では

  • 同期的はエラー処理は Error Handling
  • 非同期なエラー処理は Result

で使い分けるのが良いかと思います。

エラーハンドリング考察

初見ではthrowやcatchが出てきて、Swiftに例外機能入れるのはどうなのかと思いましたが、 実際は以下のような NSErrorPointer を渡してエラーを受け取るパターンのシンタックスシュガーと考えることができます。

1
2
3
4
5
6
7
8
9
// Swift1
extension NSData {
    class func bookmarkDataWithContentsOfURL(bookmarkFileURL: NSURL, error:NSErrorPointer) -> NSData?
}
// Swift 2
extension NSData {
    class func bookmarkDataWithContentsOfURL(bookmarkFileURL: NSURL) throws -> NSData
}

ですので、C++などのプログラミング言語の例外機構と違って、スタックトレースを巻き戻すなど重たい処理はされないはずです。 例外(Exception)ではなく、Error Handling と呼んでいるのもこの区別のためだと思います。

エラーが発生する可能性があるメソッドを普通のメソッドのように呼びだそうとすると、 「Can call throw but is not marked with try.」というコンパイルエラーになります。 エラーハンドリングを開発者に強制させる点は安全なプログラミングにつながり、こういった枠組みを提供できたことは大きな価値があると思います。

まとめ

Swift2から利用できる新機能について紹介しました。他にも Availability や Protocol Extension など紹介できていない機能もあります。 今回は広く紹介しましたが、今後は実際にSwift2を実戦投入してたまった知見やベストプラクティスを紹介して行ければと思っています。 

最後に、リクルートライフスタイルでは、新規iOSアプリにSwiftを積極的に採用しています。 Swiftでコードを書きたい方は是非弊社へ!

エンジニアサイト | リクルートライフスタイル RECRUIT LIFESTYLE

参考