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

引数のクロージャの属性【Swift実践入門読書メモ】

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

引数のクロージャの属性

  • 第4章 3節 pp.120-124

@escaping 属性

関数に引数として渡されたクロージャが、関数のスコープ外で保持される可能性があることを示す属性です。

こんな感じで使われます。

func execClosure(_ closure: @escaping () -> Void) {
    ...
}

ということは、逆に言えば @escaping 属性がついていなければスコープ外で保持されることがないということです。そのため、 クロージャ内で self を参照していたりしても循環参照にならないため、 [weak self] 等を用いて弱参照にする必要がなくなります。

以前、以下のエントリで循環参照について考えた際には、どんな時に循環参照になってしまうのか考えるのが大変なので、問答無用で [weak self] を使用するのが楽そうと思いましたが、 @escaping 属性の有無で判別することができるようです。

komaji504.hateblo.jp

でも結局 @escaping 属性をつけるかどうかは人間が考える必要があって大変、ということはなく @escaping 属性をつけるべきときにつけていない場合は、コンパイルエラーとなるので気づくことができます。

class SampleClass {
    var closure: (() -> Void)?
    func execClosure(_ closure: () -> Void) {
        self.closure = closure
    }
}
Playground execution failed: error: SampleApp.playground:4:24: error: assigning non-escaping parameter 'closure' to an @escaping closure
        self.closure = closure
                       ^
SampleApp.playground:3:24: note: parameter 'closure' is implicitly non-escaping
    func execClosure(_ closure: () -> Void) {
                       ^
                       @escaping

そのため、@escaping 属性がついていれば循環参照の恐れがあるので、クロージャによってキャプチャされる変数は [weak self] 等を用いて弱参照にしておく、ついていなければ強参照のまま利用するというようにすると良さそうです。

@autoclosure 属性

引数をクロージャで包むことで遅延評価を実現するための属性です。

引数の遅延評価が有効なケース

以下の printResults(result1:result2:) は、第一引数の値が true なら最初の分岐以降の処理は実行されません。

func printResults(result1: Bool, result2: Bool) {
    if result1 {
        print("result1 is true")
    } else if result2 {
        print("result1 is false, result2 is true")
    } else {
        print("result1 is false, result2 is true")
    }

処理の順序はこんな感じになります。

  • result1 を評価する
    • true なら print("result1 is true") して終了
    • false なら result2 を評価する
      • true なら print("result1 is false, result2 is true") して終了
      • false なら else 節に入って print("result1 is false, result2 is true") して終了

ですが、以下のように引数に関数の実行結果を直接渡している場合には、渡された関数は関数内の処理に入る前に実行されます。

printResult(result1: result1(), result2: result2())
  • result1() を評価する ← 関数の処理前
  • result2() を評価する ← 関数の処理前
  • result1() の結果である result1 を評価する
    • true なら print("result1 is true") して終了
    • false なら result2() の結果である result2 を評価する
      • true なら print("result1 is false, result2 is true") して終了
      • false なら else 節に入って print("result1 is false, result2 is true") して終了

このとき、 result1() の結果が true になるのであれば result2() の評価は無駄になってしまいます。
これを防ぐために遅延評価が有効となります。

クロージャで包む

関数の実行結果を直接渡すと関数内の処理に入る前に実行されてしまいますが、以下のようにクロージャで包むと遅延評価を行うことができます。

func printResults(result1: Bool, result2: () -> Bool) {
    if result1 {
        print("result1 is true")
    } else if result2() {
        print("result1 is false, result2 is true")
    } else {
        print("result1 is false, result2 is true")
    }
}

printResults(result1: true, result2: { return result2() })

ですが、これだと printResults(result1:result2:) のインターフェースが変わってしまいます(第二引数に渡す型が Bool から () -> Bool に変わっている) 。
そこで @autoclosure の登場です。

@autoclosure をつけると暗黙的にクロージャに包んでくれるので、関数のインターフェースは変わらずに関数を実行することができます。

func printResults(result1: Bool, result2: @autoclosure () -> Bool) {
    if result1 {
        print("result1 is true")
    } else if result2() {
        print("result1 is false, result2 is true")
    } else {
        print("result1 is false, result2 is true")
    }
}

printResults(result1: true, result2: result2())

@autoclosure をつけるとインターフェースが変わらないのは確かに良いなあと思いました。

ですが、あまり遅延評価を要するコードを書く機会がない (書くべきところに気づけていない?) ので、自分が使うことはあまりないような気がしました。

合わせて読みたい

  • 弱参照による循環参照への対処
    • 第11章 2節 pp.274-275
  • クロージャ
    • 第11章 3節 pp.276-285