DarkMode 対応したらアプリ起動時にクラッシュするようになってしまった

事象

DarkMode 対応したら、アプリ起動時にクラッシュするようになってしまった。

クラッシュ内容は、アプリ起動時に表示する VC である TopViewController の viewDidLoad() 内での EXC_BREAKPOINT で、 iOS 13 系の端末のみで発生する模様。

スタックトレースを一部抜粋するとこんな感じ。

Crashed: com.apple.main-thread
0  MyApp                 0x10022293c TopViewController.viewDidLoad() + 112 (TopViewController.swift:112)
1  MyApp                 0x100222fb8 @objc TopViewController.viewDidLoad() (<compiler-generated>)
2  UIKitCore                      0x1ba9b00e4 -[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 104
3  UIKitCore                      0x1ba9b4d18 -[UIViewController loadViewIfRequired] + 952
4  UIKitCore                      0x1ba9b5104 -[UIViewController view] + 32

調査 & 対応

このクラッシュ、特定のユーザは 100% 起こるようなのにも関わらず、自分の手元では全く再現できないので調査に大苦戦した。ちなみに、クラッシュが生じるようになったバージョンでは viewDidLoad() 内のコードは一切変えていなかった。

再現できてしまえば直したも同然なので、端末の設定をいろいろ変えて再現できるか確認した。

の各項目をいろいろ変えてみたけど再現できず...

再現できなければ、いろいろ試してリリースして様子を見るしかないので、いろいろコードを修正してリリースしてみた。 しかし、一向に直らず。

コードというよりもプロジェクトの設定が悪いんじゃないかと思っていろいろ確認していたら、アプリの Main Target の General の Main Interface に、アプリ起動時に表示する VC である TopViewController が設定してあった。

この項目は Info.plist の UIMainStoryboardFile として設定されるようで、Apple のドキュメントを見てみると

When this key is present, the main storyboard file is loaded automatically at launch time and its initial view controller installed in the app’s window.
iOS Keys

と書いてあった。

自分のアプリでは、以下のように AppDelegate の application(_:,didFinishLaunchingWithOptions:) 内で TopViewController を初期化して window.rootViewController にセットしていたので、 Main Interface の指定と合わせると二重に画面の初期化処理が走ってしまっているみたいだった。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    window?.rootViewController = TopViewController()
    window?.makeKeyAndVisible()

これはこれで問題なので、 Main Interface での指定は削除(何も設定しないように)して、以下のように初期化処理をコードに集約させた。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.rootViewController = TopViewController()
    window?.makeKeyAndVisible()

このバージョンをリリースしてみたところ、なんと、無事クラッシュが治まっていた。

UIKit のコードは読めないので詳細な理由はわからないけど、DarkMode 対応することで View の初期化処理がいろいろ変わるとかそんな感じだと思う。

とにかく、めちゃくちゃ安心した。

まとめ

今までは問題なかった実装や設定も、 iOSSDK のアップデート等で動かなくなってしまうことがあるので、Apple のドキュメントをちゃんと読んで実装するの大事。

AssociatedValue の有りと無しの case を複数持つ Enum を Decode する

AssociatedValue が無い case が1つ

enum Food {
    case hamburger(topping: String)
    case pizza
}

上記のような Enum がありまして、これを Decodable に準拠すると

extension Food: Decodable {
    enum CodingKeys: String, CodingKey {
        case hamburger
        case pizza
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try container.decodeIfPresent(String.self, forKey: .hamburger) {
            self = .hamburger(topping: value)
        } else if try container.decodeNil(forKey: .pizza) {
            self = .pizza
        } else {
            let context = DecodingError.Context(
                codingPath: container.codingPath,
                debugDescription: "Unknown case"
            )
            throw DecodingError.dataCorrupted(context)
        }
    }
}

こんな感じに書けます。 動作を確認するとそれぞれの case で decode できています。

let jsonString = """
{
    "hamburger": "cheeze"
}
"""
let jsonData = jsonString.data(using: .utf8)!
let food = try JSONDecoder().decode(Food.self, from: jsonData)
// => hamburger(topping: "cheeze")
let jsonString = """
{
    "pizza": null
}
"""
let jsonData = jsonString.data(using: .utf8)!
let food = try JSONDecoder().decode(Food.self, from: jsonData)
// => pizza

AssociatedValue が無い case が2つ

ただ、以下のように AssociatedValue の無い case がもう一つ追加されるとどうでしょうか。

enum Food {
    case hamburger(topping: String)
    case pizza
    case kebabu // New!!!
}

decode メソッドは先ほどと同様に記述すると以下のようなコードをイメージするかと思います。

extension Food: Decodable {
    enum CodingKeys: String, CodingKey {
        case hamburger
        case pizza
        case kebabu
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try container.decodeIfPresent(String.self, forKey: .hamburger) {
            self = .hamburger(topping: value)
        } else if try container.decodeNil(forKey: .pizza) {
            self = .pizza
        } else if try container.decodeNil(forKey: .kebabu) { // New!!!
            self = .kebabu
        } else {
            let context = DecodingError.Context(
                codingPath: container.codingPath,
                debugDescription: "Unknown case"
            )
            throw DecodingError.dataCorrupted(context)
        }
    }
}

しかし、これで kebabu を decode しようとすると DecodingError.keyNotFound が throw されてしまいます。

let jsonString = """
{
    "kebabu": null
}
"""
let jsonData = jsonString.data(using: .utf8)!
let food = try JSONDecoder().decode(Food.self, from: jsonData)
//  ▿ keyNotFound : 2 elements
//    - .0 : CodingKeys(stringValue: "pizza", intValue: nil)
//    ▿ .1 : Context
//      - codingPath : 0 elements
//      - debugDescription : "No value associated with key CodingKeys(stringValue: \"pizza\", intValue: nil) (\"pizza\")."
//      - underlyingError : nil

これは、AssociatedValue の無い case を decode しているメソッド decodeNil(forKey:) は、 Key が存在しないと DecodingError.keyNotFound を throw するため、 pizza の時点で DecodingError.keyNotFound が throw されてしまっているからです。

解決案1

try? を使って DecodingError.keyNotFound が throw されないようにしてみましょう。

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try container.decodeIfPresent(String.self, forKey: .hamburger) {
            self = .hamburger(topping: value)
        } else if (try? container.decodeNil(forKey: .pizza)) ?? false { // try? で Error を throw しないように
            self = .pizza
        } else if try container.decodeNil(forKey: .kebabu) {
            self = .kebabu
        } else {
            let context = DecodingError.Context(
                codingPath: container.codingPath,
                debugDescription: "Unknown case"
            )
            throw DecodingError.dataCorrupted(context)
        }
    }

こうすることで、 kebabu を decode することができます。

let jsonString = """
{
    "kebabu": null
}
"""
let jsonData = jsonString.data(using: .utf8)!
let food = try JSONDecoder().decode(Food.self, from: jsonData)
// => kebabu

しかし、 if 分の順序を変えると壊れてしまったり、DecodingError.keyNotFound 以外の DecodingError も throw されなくなってしまうので、あまり好ましくなさそうです。

解決案2

throw された DecodingError.keyNotFound を catch するようにしてみます。

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try container.decodeIfPresent(String.self, forKey: .hamburger) {
            self = .hamburger(topping: value)
        } else {
            do {
                if try container.decodeNil(forKey: .pizza) {
                    self = .pizza
                } else {
                    let context = DecodingError.Context(
                        codingPath: container.codingPath,
                        debugDescription: "Unknown case"
                    )
                    throw DecodingError.dataCorrupted(context)
                }
            } catch DecodingError.keyNotFound {
                if try container.decodeNil(forKey: .kebabu) {
                    self = .kebabu
                } else {
                    let context = DecodingError.Context(
                        codingPath: container.codingPath,
                        debugDescription: "Unknown case"
                    )
                    throw DecodingError.dataCorrupted(context)
                }
            } catch {
                throw error
            }
        }
    }

この方法であれば案1で述べたデメリットが解消されそうですが、記述量が多く、 case が増える度に do catch がネストしていってしまいます。

解決案3

これは案2の改良版で、DecodingError.keyNotFound を握りつぶすメソッドを生やします。

extension KeyedDecodingContainer where K : CodingKey {
    func decodeNilIfPresent(forKey key: K) throws -> Bool {
        do {
            return try decodeNil(forKey: key)
        } catch DecodingError.keyNotFound {
            return false
        } catch {
            throw error
        }
    }
}
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try container.decodeIfPresent(String.self, forKey: .hamburger) {
            self = .hamburger(topping: value)
        } else if try container.decodeNilIfPresent(forKey: .pizza) {
            self = .pizza
        } else if try container.decodeNilIfPresent(forKey: .kebabu) {
            self = .kebabu
        } else {
            let context = DecodingError.Context(
                codingPath: container.codingPath,
                debugDescription: "Unknown case"
            )
            throw DecodingError.dataCorrupted(context)
        }
    }

これであれば案1、案2のデメリットがカバーされていそうです。

解決案 番外編

そもそも data に null を入れない。   例えば Int とかを入れておいて、 decodeNil(forKey:) ではなく decodeIfPresent(_:,forKey:) を使って値の有無を見るという感じです。

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try container.decodeIfPresent(String.self, forKey: .hamburger) {
            self = .hamburger(topping: value)
        } else if try container.decodeIfPresent(Int.self, forKey: .pizza) != nil {
            self = .pizza
        } else if try container.decodeIfPresent(Int.self, forKey: .kebabu) != nil {
            self = .kebabu
        } else {
            let context = DecodingError.Context(
                codingPath: container.codingPath,
                debugDescription: "Unknown case"
            )
            throw DecodingError.dataCorrupted(context)
        }
    }
let jsonString = """
{
    "kebabu": 1
}
"""
let jsonData = jsonString.data(using: .utf8)!
let food = try JSONDecoder().decode(Food.self, from: jsonData)
// => kebabu

この方法であれば、 Int 以外の期待しない型が入っている場合に DecodingError.typeMismatch を throw してくれるようになります。

Swiftにおけるインポートとリンクの復習

先日行われた iOSDC 2019 に参加してきました。

岸川さんによる「Swiftにおけるインポートとリンクの仕組みを探る」を聞いてきました。 ふわっとしか理解できていなかった部分だったので発表内容を復習しようと思い、改めて資料の内容を書き出し、 Xcodeで触ったりしながら自分なりに整理してみました。完全に理解したというには到底及ばないですが、以前に比べるとだいぶ理解が深まりました。分かりやすく発表をしてくださって大感謝です。

色々触りながら学びがあればどんどん加筆していこうと思っています。

誤っている箇所があれば指摘していただけると嬉しいです。

ライブラリについて

形式

  • FrameworkとLibraryの2種類がある。
  • Frameworkはバンドルを持ち、Libraryは持たない。
  • それぞれ、書かれた言語、実行環境のアーキテクチャ、プラットフォームにも依存する。
  • エラーになった場合に組み合わせが多いので解決が難しいが、1つずつ潰していけば大丈夫。

Framework

  • 拡張子が .framework のもの。
  • バンドルを持つ。
  • ダイナミック、スタティックがあるがアイコン同じなので見た目でわからない。
    • 新規作成した時のデフォルトはダイナミック。
    • BuildSettings の Mach-O を static にするとスタティックになる。
  • また、それぞれModuleを含む含まないの違いがある。

Library

  • バンドル、Moduleは持たない。
ダイナミック
  • 拡張子が .dylib のもの。
スタティック
  • 拡張子が .a のもの。

バンドル

  • 一定の規則に従ったディレクトリ構造のこと。
  • 例えば、Appバンドル、Projectバンドル、Settingsバンドル、Frameworkバンドルといったものがある。
  • FrameworkはFrameworkのディレクトリ構造に従ったもの。
  • バンドルはコード以外のものを含めることができる。

Module

  • Moduleディレクトリ配下のもの。
  • PublicなAPIの宣言がされている。
  • Swift でライブラリをインポートするためにはModuleが必要。
  • ObjC/CのLibraryをインポートするためには、ヘッダファイルを Module MapでModuleに変換する。
  • SwiftのLibraryではコンパイラがModuleを自動生成する。
  • ModuleはFrameworkバンドル外に置くこともでき、その場合には SWIFT_INCLUDE_PATHS にModule Mapがあるディレクトリの親ディレクトリを指定する。
    • Swift製の場合はわざわざ出す必要はないけどObjC製とかそもそもModule Mapがないときに有効。
  • Module Stability によってどのバージョンでコンパイルされたModuleもimportできるようになった。

Module MapとBridging Headerの違い

  • Module MapはBridging Headerの上位互換。
  • Bridging Headerはglobal importになってしまうので依存関係が分かりづらくなる。
  • Bridging Headerはアプリターゲットでしか使えない(Framework開発では使用できない)。
  • Module Map も自分で書くことができる。

インポート

  • 外部ライブラリが公開しているシンボル(クラス・関数など)を自分のコードベースで参照できるようにするための構文、言語機能。
  • 各ファイルをコンパイルする時点で解決される。
  • FrameworkやLibraryをインポートするということは、その中にあるモジュールを参照できるようにしているとういうこと。

リンク

  • ソースコードコンパイルして生成されたオブジェクトファイル、および外部ライブラリまとめて1つの実行形式ファイル、あるいはライブラリを生成すること。
  • 全てのソースコードコンパイルした後にリンカーが行う。
  • ダイナミック、スタティック全てのシンボルを解決する必要がある。
  • コンパイルして生成されたオブジェクトファイルのリンクは失敗しない。
  • ダイナミックリンクとスタティックリンクがある。

スタティックリンク

  • ビルド時にリンクが行われる。
  • ターゲットことに同じものが作られるのでシンボルが衝突するということが起こる。
    • これを防ぐのはなかなか難しい。

ダイナミックリンク

  • ビルド時には参照だけされて実行時にリンクされる。
  • 複数のターゲットから同じものが参照される。

リンクするための設定

Xcode で行う場合

  • Linked Frameworks and Libraries に追加する。
  • ただ、Xcode はデフォルトで Automatic Linking が有効になっているため、ここに追加しなくてもインポートするだけでリンク設定がされる。
  • Automatic Linking を無効にするためには OTHER_SWIFT_FLAGS に -Xfrontend -disable-autolink-framework -Xfrontend <FrameworkName> を指定する。

マニュアルでリンクするための設定

Frameworkの場合

  • FRAMEWORK_SEARCH_PATHS にFrameworkがある親ディレクトリのパスを指定する。
  • OTHER_LD_FLAGS に -l framework <FrameworkName> を指定する。
  • Dog.framework の場合には -l framework Dog

Libraryの場合

  • LIBRARY_SEARCH_PATHS にライブラリがある親ディレクトリのパスを指定する。
  • OTHER_LD_FLAGS に -l <LibraryName> を指定する。
  • libCat.a の場合には -lCat

Carthage で Framework を公開する

先日 Framework を作って Carthage でインストールできるようにしたので、その手順を書いておきます。
作ったものはこちらで紹介しています。

komaji504.hateblo.jp

Workspace や Project を作る

ディレクトリ構成

Workspace や Project のディレクトリ構成ですが、今回は以下のようにしてみました。
あくまで一例なので、他の方が作られたものを参考にしたりして、自分好みでわかりやすい構成にしていけば良いのかなと思います。

GradientAnimationView
├── Demo
│   ├── Demo.xcodeproj
│   │   ├── project.pbxproj
│   │   └── project.xcworkspace
│   │       └── contents.xcworkspacedata
│   ├── Source
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   │   └── AppIcon.appiconset
│   │   │       └── Contents.json
│   │   ├── Base.lproj
│   │   │   ├── LaunchScreen.storyboard
│   │   │   └── ViewController.storyboard
│   │   ├── Info.plist
│   │   └── ViewController.swift
│   └── demo.gif
├── GradientAnimationView
│   ├── GradientAnimationView.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   │   │   └── contents.xcworkspacedata
│   │   └── xcshareddata
│   │       └── xcschemes
│   │           └── GradientAnimationView.xcscheme
│   └── Source
│       ├── GradientAnimationView.h
│       ├── GradientAnimationView.swift
│       └── Info.plist
├── GradientAnimationView.xcworkspace
│   ├── contents.xcworkspacedata
│   └── xcshareddata
│       └── WorkspaceSettings.xcsettings
├── LICENSE
└── README.md

Workspace を作る

Workspace は必須ではないのですが、作ってその中に Framework とそれを使ったデモアプリを入れておくと、Framework を開発しながらすぐにデモアプリで動作確認ができるので開発が楽になります。
利用者もデモアプリを実行して Framework がどんなものかを簡単に触ることができるので親切かと思います。

Demo Project を作る

デモアプリとなる Project で、 Framework の開発時にはこの Project を実行して動作確認しながら進めて行きます。
今回は Single View App の Project を Demo という名前で作りました。

Framework Project を作る

Cocoa Touch Framework として Project を作ります。

開発する

あとは思いのままに Framework を作っていきます。 インタフェースには Public や Open を適切に付与します。これらを付与したもの以外は外部から参照できません。

ビルド対象を Framework に変更してビルドすることで、 Public や Open が付与されたものを Demo Project から参照できるようになるので、 Demo Proejct の方で Framework を使ったアプリを作っていきます。

動作確認する際には、ビルド対象を Demo に変更して実行するだけです。

Cathage でインストールできるようにする

以下の手順で開発した Framework の Scheme を Shared にすることでインストールできるようになります。

ビルド対象を選択するところから Manage Schemes を選択します。

f:id:komaji504:20180614235738p:plain

Framework Project の右端にある Shared のチェックボックスにチェックを入れます。

f:id:komaji504:20180614235733p:plain

READEME を書く

概要や使い方を書きます。

Shields.io: Quality metadata badges for open source projects

こちらから適切なバッジを選んだり作ったりしてつけるとそれっぽくなります。 以下のようなバッジを簡単に作ることができます。

Language Platform Carthage compatible License

また、View 系の Framework の場合、動作がわかる GIF を載せておくと親切と思います。 GIF ファイルは Quick Time Player と ffmpeg から簡単に作れます。雑ですが、以下のようにして作れます。

動画を撮影する

  • Quick Time Player を起動
  • メニューバー
    • ファイル
      • 新規画面収録 (または Control + Command + N)
  • 録画ボタンをクリック
  • 録画対象を選択して録画
    • 全体であればクリックするだけ
    • 一部であればドラッグすることで録画対象を選択できる
      • その後録画ボタンをクリック
        • 録画ボタンをクリックすると同時にカーソルを素早く移動すればカーソルを映さないこともできる
  • 録画を止める
    • Control + Command + Esc

動画をGIFに変換する

brew install ffmpeg
ffmpeg -i input.mov -r 10 output.gif

リリースする

タグをアップロードする

git コマンドでタグをつけてリモートリポジトリにプッシュします。

git tag 0.1.0 -m "コメント"
git push origin 0.1.0

リリースノートを作成する

GitHub の Release ページの Draft a new release ボタンをクリックするとリリース作成画面が開きます。 f:id:komaji504:20180615001135p:plain


リリース対象のタグ・リリースの名前・リリース内容を埋めます。 f:id:komaji504:20180615001132p:plain


すると以下のようなリリースノートが作成されます。 f:id:komaji504:20180615001139p:plain

※上の画像は以前 LabelPicker という Framework を作ったときのものです。

おわりに

以上で作業は完了です。
初めて作った時は色々と手こずったのですが、 carthage update で自分が作った Framework がインストールできた時は感動しました!
初めての方もぜひチャレンジしてみてください〜

グラデーションをアニメーションさせるライブラリ作った【iOS】

FacebookiOS アプリでは、起動時にコンテンツを表示するまでの間、テキストや画像を表す灰色で塗りつぶした View を表示しています。そうすることで、この後にどういったコンテンツが表示されるのかということを伝えることができます。

加えて、ローディングインジケータとして、 UIAcitivityIndicator のようなぐるぐるを表示するのではく、その灰色で塗りつぶした View をグラデーションさせ、グラデーションをアニメーションさせています。
コンテンツとは切り離されたぐるぐるを表示するのではく、コンテンツそのものにエフェクトを加えることで、より自然な表現になっていると思います。

今回は、この Facebook アプリと同じ表現をするためのライブラリを作ったので紹介します。

GradientAnimationView

github.com

同じようなことができるライブラリはたくさんあるかと思いますが、その中でも一番シンプルになっているかと思います。 自分がやりたかったことは、グラデーションを左から右にスライドするアニメーションをさせるということだけだったので、それだけができるようにしています。

Carthage に対応していますが、1ファイルだけなのでコピペするのでも良いです。

使い方

対象の View に対して addSubView するだけです。

let gradientAnimationView = GradientAnimationView()
view.addSubview(gradientAnimationView)

そうすると、このようなグラデーションが左から右へアニメーションする View を作ることができます。

GradientAnimationView

オプション

以下を設定できます。

// 角丸にする
gradientAnimationView.cornerRadius = 5.0

// グラデーションの両端の色
gradientAnimationView.sideColor = .gray

// グラデーションの中央の色
gradientAnimationView.centerColor = .lightGray

// アニメーションの時間
gradientAnimationView.animationDuration = 1.2

// アニメーション間の時間
gradientAnimationView.waitingDuration = 0.6

今後

Storyborad から設定できたら便利かと思うのですが今はできないので対応したいと思います。

UICollectionView の UICollectionReusableView の位置がおかしい時

class SampleListViewController: UIViewController, UICollectionViewDatasource, UICollectionDelegate {
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionView.register(
                UINib(nibName: "FooterIndicatorView", bundle: nil),
                forSupplementaryViewOfKind: UICollectionElementKindSectionFooter,
                withReuseIdentifier: "FooterIndicatorView"
            )
        }
    }
    var footerIndicatorView: UICollectionReusableView!

    .
    .
    .

    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        guard kind == UICollectionElementKindSectionFooter else {
            return UICollectionReusableView()
        }
        
        if footerIndicatorView != nil {
            footerIndicatorView = collectionView.dequeueReusableSupplementaryView(
                ofKind: kind,
                withReuseIdentifier: "FooterIndicatorView",
                for: indexPath
            )
        }
        
        return footerIndicatorView
    }
}

UICollectionView のフッターにローディングインジケータを表示したくて、上記の用に実装していたとします。
インジケータのアニメーションを操作するために、インジケータのインスタンスが生成されたらプロパティに保持しています。

また、インジケータのインスタンスを毎回生成するのが勿体無いと思って、インスタンスが作られていなかったら (nil だったら) dequeue してプロパティに格納して以降はそれを使い回すというように実装していました。

位置がおかしい

この時、 インジケータがセクションの最後のセルのすぐ下に表示されずに、常に UICollectionView の最上部 (しかもセルに被って) 表示されてしまうというバグに遭遇しました。

対応

色々試していたところ、インスタンスが作られていなかったら (nil だったら) という if 文を消して、毎回 dequeue するようにしたら、意図した通りに表示されました。

なぜ?

Apple のドキュメントを読んでみたのですが、関係ありそうなことは書いていなかったので (たぶん) 、とりあえずライフサイクル的にダメなんだろうなあという感じでした。

dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:) - UICollectionView | Apple Developer Documentation

for-case 文【Swift実践入門読書メモ】

Swift実践入門を読んでみて、気になったところのメモです。

for-case 文

  • 第3章 3節 pp.91-92

Sequence プロトコルに準拠した型の要素のうち、パターンにマッチするものだけを列挙する繰り返し文です。

for 文にも case によるパターンマッチが行えて

let numbers: [Int] = [1, 2, 3, 4, 5]

for case 2..4 in numbers {
    ...
}

とか

enum Food {
    case hamburger, pizza, sushi
}

let foods: [Food] = [
    .hamburger,
    .pizza,
    .sushi
]

for case .hamburger in foods {
    ...
}

みたいなことができます。


いまいち使い所がわからなかったですが、 以下のエントリで書いた if-case といい今回の for-case といい、色々なところでパターンマッチが行えるのかふむふむと思いました。

komaji504.hateblo.jp

今後パターンマッチを行う際には、どの 〇〇-case を使うとスッキリ書けるかを意識しながらコードを書いてみようと思います。

何か良きケースがあったら追記します。

合わせて読みたい

  • パターンマッチ
    • 第3章 6節 pp.100-104