SwiftUI Tutorialsを読み解く

# この記事は何？
[SwiftUI Tutorials: Building Lists and Navigation](https://developer.apple.com/tutorials/swiftui/building-lists-and-navigation)をフルスクラッチからハンズオンするにあたって、Data.swiftファイルのソースコードを調べました。

## Data.swift
チュートリアルはサンプルコードが用意されていて、_SwiftUI_の要点を学習できるようになっています。ただ、実践していると序盤に`ImageStore`という不明なクラスが突如、現れます。
この`ImageStore`を定義しているファイルが、Data.swiftです。
ちなみに、ナビゲータエリアのModelsフォルダに入っています。
`ImageStore`は単なる「画像データを管理してくれるクラス」と理解すれば、チュートリアル自体は問題なく読み進めることができますが、ソースコードを見てみるとSwift初学者にはなかなかちょうどいい難易度の教材かと思いました。
忘備録がわりに解説しておきます。

## 実行環境
- macOS 10.15 Catalina bata7
- Xcode11 bata7

# コードを読む
SwiftUIチュートリアルのプロジェクトに含まれているData.swiftですが、このファイル自体にSwfitUIの概念はほとんど含まれていません。
以下のようなSwiftプログラミングのちょっと高度な概念を見ることができます。
- ジェネリクス
- プロトコル
- タイプエイリアス
- `guard-Let` 構文
- `do-catch` 構文
- `fileprivate` キーワード
- ...

## ソースコード全体
大まかな構成として、次の4つに分けて読むことができます。
1. フレームワークのインポート
2. `landmarkData` 定数
3. `load<T>(_:as:)` 関数
4. `ImageStore` クラス

````swift:Data.swift
import UIKit
import SwiftUI
import CoreLocation

let landmarkData: [Landmark] = load("landmarkData.json")

func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
    let data: Data
    
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
        else {
            fatalError("Couldn't find \(filename) in main bundle.")
    }
    
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }
    
    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

final class ImageStore {
    typealias _ImageDictionary = [String: CGImage]
    fileprivate var images: _ImageDictionary = [:]

    fileprivate static var scale = 2
    
    static var shared = ImageStore()
    
    func image(name: String) -> Image {
        let index = _guaranteeImage(name: name)
        
        return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(verbatim: name))
    }

    static func loadImage(name: String) -> CGImage {
        guard
            let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
            let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
            let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
        else {
            fatalError("Couldn't load image \(name).jpg from main bundle.")
        }
        return image
    }
    
    fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
        if let index = images.index(forKey: name) { return index }
        
        images[name] = ImageStore.loadImage(name: name)
        return images.index(forKey: name)!
    }
}
````

### 1. フレームワークのインポート
3つのフレームワークをインポートしています。
このうち、`UIKit`の機能はこのファイル内で使用されていないので、記述を削除してもビルドに成功します。

````Swift:Data.swift
import UIKit
import SwiftUI
import CoreLocation

...
````

2つ目の`SwiftUI`フレームワークは、以下のコードで使用されています。

````swift:ImageStoreクラス
func image(name: String) -> Image {
    ....
}
````

`-> Image` の部分です。
この `Image` がいわゆる__SwiftUIビュー__です。

3つ目の `CoreLocation` は座標を扱う機能として、プロジェクト全体にわたって随所に使用されています。
ただし、このファイル内に限っては使用されていないので、削除してもビルドに成功します。

### 2. landmarkData 定数

`[Landmark]`型のコレクションです。
`Landmark`は観光名所を表現した構造体で、名前や画像ファイル名、座標、所在する州名、カテゴリなどの情報を持っています。

````Swift:Data.swift
let landmarkData: [Landmark] = load("landmarkData.json")
````

このコレクションを返す `load()` 関数は、直後に定義されています。

### 3. load < T >(_: as: ) 関数

この関数は、プロジェクトリソースのJSON形式データから`[Landmark]`型のコレクションを返します。

````Swift:Data.swift
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
    let data: Data
    
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
        else {
            fatalError("Couldn't find \(filename) in main bundle.")
    }
    
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }
    
    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}
````

宣言を見ると、以下のことがわかります。

- 1つめのパラメータは、`String`型（ラベルなし、引数名 `filename`）
- 2つめのパラメータは、任意の型 `T`（ラベル `as`、引数名 `type`）
- 任意の型 `T` を返す

````Swift:関数の宣言
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
    ...
}
````

`<T: Decodable>` は、`Decodable`プロトコルに準拠したジェネリクスです。
`Decodable`に準拠していれば、あらゆる型を引数に受け取ることができることになります。
また、2つ目のパラメータには既定値として、`= T.self` が割り当てられています。
このため、呼び出し時には省略可能です。

次に、リソースからJSONデータを取得して定数 `file` に割り当てています。

````Swift:load()関数
guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else {
    fatalError("Couldn't find \(filename) in main bundle.")
}
````

`guard-let` 構文によって、早期エラー検出して安全に記述しています。
この後、`do-try-catch` エラーハンドリングをしながら、ファイル情報をデータ化します。

````Swift:load()関数
do {
    data = try Data(contentsOf: file)
} catch {
    fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
````

この時点で、定数 `data` にはJSONファイルのバイナリデータが割り当てられていると考えられます。
最後に、バイナリデータをコレクション型にデコードして返します。

このメソッド全体が、`JSON`ファイルをコードで扱えるコレクションに変換するお手本になりそうな関数になっているので、覚えておいてもいいかもしれません。


````Swift:load()関数    
do {
    let decoder = JSONDecoder()
    return try decoder.decode(T.self, from: data)
} catch {
    fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
````

`JSONDecoder`型のインスタンスを生成して、`decoder(_:from:)`メソッドを呼び出すことでこの作業を行なっています。
ここで、`T.self` という値が使われていますが、これはパラメータ `type` の既定値と同じです。
`type` ではなく、`T.self` とわざわざ記述している理由はわかりません...。

ともかく、このファイルのグローバル定数 `landmarkData` に、JSONデータから取り出したコレクションが適切な型で割り当てられました。

### 4. ImageStore クラス

ここまでの1~3は、`ImageStore` クラスを構築するための準備と言えます。
`ImageStore`型は `final` キーワードによって、これ以上は継承できないように制限されています。
このクラスには、3つの変数と3つのメソッドが定義されています。

- 変数 `images`
- 変数 `scale`
- 変数 `shared`
- `image(name:)` メソッド
- `loadImage(name:)` メソッド
- `_guaranteeImage(name:)` メソッド

また、`typealias` によって `[String: CGImage]` 型を `_ImageDictionary` という名前で呼び出せるように定義しています。

````Swift:Data.swiftに定義されているImageStore型
final class ImageStore {
    typealias _ImageDictionary = [String: CGImage]
    fileprivate var images: _ImageDictionary = [:]

    fileprivate static var scale = 2
    
    static var shared = ImageStore()
    
    func image(name: String) -> Image {
        let index = _guaranteeImage(name: name)
        
        return Image(images.values[index], 
                     scale: CGFloat(ImageStore.scale),
                     label: Text(verbatim: name))
    }

    static func loadImage(name: String) -> CGImage {
        guard
            let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
            let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
            let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
        else {
            fatalError("Couldn't load image \(name).jpg from main bundle.")
        }
        return image
    }
    
    fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
        if let index = images.index(forKey: name) { return index }
        
        images[name] = ImageStore.loadImage(name: name)
        return images.index(forKey: name)!
    }
}
````

#### 変数
まず、宣言されている変数の型と役割を確認しておきます。

````Swift:ImageStore型
fileprivate var images: _ImageDictionary = [:]    
fileprivate static var scale = 2    
static var shared = ImageStore()
````

変数 `images` は、`_ImageDictionary` 型と記述されていますが、実際には `[String: CGImage]` という辞書コレクションです。ここには観光地名と画像が割り当てられます。
初期値はカラの状態です。
`filepribete` キーワードによって、ファイル外からのアクセスが制限されています。

変数 `scale` は、画像データを表示するときの倍率と考えてください。実際には、SwiftUIビューである `Image` の初期化メソッドを呼び出す際、パラメータとして渡されます。
初期値は `2` になっています。
こちらもファイル外からのアクセスを制限しています。

変数 `shared` は、割り当てられている値が自身の型 `ImageStore` であることからも分かるように、_シングルトン_というデザインパターンを実現するために利用されています。
シングルトンは簡単にいうと、共有される単一のインスタンスです。
`ImageStore` が構造体ではなく、クラスによって定義されている理由もシングルトンパターンを実現するためだと思われます。構造体だと暗黙的コピーが発生する _Copy on Write_ によって、__インスタンスが1つではなくなる__のでシングルトンの要件を満たせません。

#### メソッド
続いて、メソッドを見ていきます。

最初に、`loadImage(name:)` メソッドです。
宣言に `loadImage(name: String) -> CGImage` と記述されていることから、「画像名を渡すと、適切な画像データを返す」機能を果たすことがわかります。
また、このメソッドは `static` キーワードがあるので、_静的メソッド_ （いわゆるタイプメソッド）です。つまり、インスタンス化せずに直接、型から呼び出せます。

````Swift:ImageStore型    
static func loadImage(name: String) -> CGImage {
    guard let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
          let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
          let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
    else {
        fatalError("Couldn't load image \(name).jpg from main bundle.")
    }
    return image    
}
````

`guard-let` 構文を使って、早期エラー回避しながら以下の処理を行なっています。

1. パラメータとして受け取った画像名を使って、パスが割り当てられた定数 `url`を生成
2. `url` を辿って、読み取る画像ソースを割り当てた定数 `imageSource` を生成
3. `imageSource`の先頭インデックスに関連付けられた画像データを `image` に割り当てる

要は、プロジェクトに含まれている _Resource_ フォルダ内にある画像を取得する手順です。

ここで使用されている `CGImageSourceCreateWithURL()` メソッドと`CGImageSourceCreateImageAtIndex()` メソッドは `Image I/O` フレームワークで定義されている画像処理APIです。

- [CGImageSourceCreateWithURL](https://developer.apple.com/documentation/imageio/1465262-cgimagesourcecreatewithurl)
- [CGImageSourceCreateImageAtIndex](https://developer.apple.com/documentation/imageio/1465011-cgimagesourcecreateimageatindex)

特にインポート宣言をしていないことから、Swift標準ライブラリに内容されていると考えられます。
最後に、`CGImage`型の画像データを１つだけ返しています。

次に、`_guaranteeImage(name:)` メソッドです。
宣言は `_guaranteeImage(name: String) -> _ImageDictionary.Index` と記述されています。
このメソッドは、パラメータとして受け取った「画像名に基づいて辞書を検索し、対応する画像のインデックスを返す」機能を果たします。

````Swift:ImageStore型
fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
    if let index = images.index(forKey: name) { return index }
        
    images[name] = ImageStore.loadImage(name: name)        
    return images.index(forKey: name)!    
}
````

`if-let` 構文によって、辞書コレクション内のキーに画像名が見つかった場合は、その `index` を返すようにしています。

辞書でキーがヒットせず、`nil` になった場合は、パラメータ `name` と対応する画像データを_Resource_ フォルダから取得して、変数 `images` コレクションに追加します。
最後に、`images` コレクションにある追加済み画像のインデックスを返します。

残るは、`image(name)` メソッドです。
宣言は、`image(name: String) -> Image` と記述されています。
このメソッドの機能が「画像名を渡すと、適切な画像を返す」ことだとわかります。
返り値の型は、SwiftUIビューの `Image` 型です。

````Swift:ImageStore型
func image(name: String) -> Image {
    let index = _guaranteeImage(name: name)
        
    return Image(images.values[index], 
                 scale: CGFloat(ImageStore.scale), 
                 label: Text(verbatim: name))    
}
````

パラメータとして受け取った画像名 `name` をそのまま、`_guaranteeImage(name:)` に渡しています。
帰ってきたインデックスを基に、`images` コレクションから画像データを取得できます。
実際には、`Image`型イニシャライザにパラメータとして渡され、そのまま返り値になっています。

![ImageStore_Concept.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/5738ce12-8055-23a2-534f-0b902533f388.png)

# 考察
Apple謹製のサンプルコードだけあって、いろんなテクニックが詰め込まれているのに、短く分割されて、理解しやすいと感じました。

ただ、辞書コレクションである `images` 変数を操作するのにインデックスを多用する理由はなんでしょう？
`images.values[index]`のようにキーを使って値を取り出さず、わざわざインデックスを指定しています。

もしかすると、チュートリアルを進めていく途中で理解できるのかもしれません。

## 続き
Modelグループにあるもう１つのファイルLandmark.swiftの解説は[こちら](https://qiita.com/chino_tweet/items/6c9b8404a59544620190)

## 動画で学習する
このページの内容を動画で実践・解説した講座を、世界最大級の動画学習プラットフォームUdemyでリリースしました。以下のリンクをクリックすると、最初のセクションを無料で視聴できます。
[Udemyで「速習！SwiftUI」講座を視聴する](https://www.udemy.com/course/startswiftui/)