この記事は何?
SwiftUI Tutorials: 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つに分けて読むことができます。
- フレームワークのインポート
-
landmarkData
定数 -
load<T>(_:as:)
関数 -
ImageStore
クラス
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
の機能はこのファイル内で使用されていないので、記述を削除してもビルドに成功します。
import UIKit
import SwiftUI
import CoreLocation
...
2つ目のSwiftUI
フレームワークは、以下のコードで使用されています。
func image(name: String) -> Image {
....
}
-> Image
の部分です。
この Image
がいわゆる__SwiftUIビュー__です。
3つ目の CoreLocation
は座標を扱う機能として、プロジェクト全体にわたって随所に使用されています。
ただし、このファイル内に限っては使用されていないので、削除してもビルドに成功します。
2. landmarkData 定数
[Landmark]
型のコレクションです。
Landmark
は観光名所を表現した構造体で、名前や画像ファイル名、座標、所在する州名、カテゴリなどの情報を持っています。
let landmarkData: [Landmark] = load("landmarkData.json")
このコレクションを返す load()
関数は、直後に定義されています。
3. load < T >(_: as: ) 関数
この関数は、プロジェクトリソースのJSON形式データから[Landmark]
型のコレクションを返します。
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
を返す
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
...
}
<T: Decodable>
は、Decodable
プロトコルに準拠したジェネリクスです。
Decodable
に準拠していれば、あらゆる型を引数に受け取ることができることになります。
また、2つ目のパラメータには既定値として、= T.self
が割り当てられています。
このため、呼び出し時には省略可能です。
次に、リソースからJSONデータを取得して定数 file
に割り当てています。
guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else {
fatalError("Couldn't find \(filename) in main bundle.")
}
guard-let
構文によって、早期エラー検出して安全に記述しています。
この後、do-try-catch
エラーハンドリングをしながら、ファイル情報をデータ化します。
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
この時点で、定数 data
にはJSONファイルのバイナリデータが割り当てられていると考えられます。
最後に、バイナリデータをコレクション型にデコードして返します。
このメソッド全体が、JSON
ファイルをコードで扱えるコレクションに変換するお手本になりそうな関数になっているので、覚えておいてもいいかもしれません。
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
という名前で呼び出せるように定義しています。
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)!
}
}
変数
まず、宣言されている変数の型と役割を確認しておきます。
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
キーワードがあるので、静的メソッド (いわゆるタイプメソッド)です。つまり、インスタンス化せずに直接、型から呼び出せます。
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
構文を使って、早期エラー回避しながら以下の処理を行なっています。
- パラメータとして受け取った画像名を使って、パスが割り当てられた定数
url
を生成 -
url
を辿って、読み取る画像ソースを割り当てた定数imageSource
を生成 -
imageSource
の先頭インデックスに関連付けられた画像データをimage
に割り当てる
要は、プロジェクトに含まれている Resource フォルダ内にある画像を取得する手順です。
ここで使用されている CGImageSourceCreateWithURL()
メソッドとCGImageSourceCreateImageAtIndex()
メソッドは Image I/O
フレームワークで定義されている画像処理APIです。
特にインポート宣言をしていないことから、Swift標準ライブラリに内容されていると考えられます。
最後に、CGImage
型の画像データを1つだけ返しています。
次に、_guaranteeImage(name:)
メソッドです。
宣言は _guaranteeImage(name: String) -> _ImageDictionary.Index
と記述されています。
このメソッドは、パラメータとして受け取った「画像名に基づいて辞書を検索し、対応する画像のインデックスを返す」機能を果たします。
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
型です。
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
型イニシャライザにパラメータとして渡され、そのまま返り値になっています。
考察
Apple謹製のサンプルコードだけあって、いろんなテクニックが詰め込まれているのに、短く分割されて、理解しやすいと感じました。
ただ、辞書コレクションである images
変数を操作するのにインデックスを多用する理由はなんでしょう?
images.values[index]
のようにキーを使って値を取り出さず、わざわざインデックスを指定しています。
もしかすると、チュートリアルを進めていく途中で理解できるのかもしれません。
続き
Modelグループにあるもう1つのファイルLandmark.swiftの解説はこちら
動画で学習する
このページの内容を動画で実践・解説した講座を、世界最大級の動画学習プラットフォームUdemyでリリースしました。以下のリンクをクリックすると、最初のセクションを無料で視聴できます。
Udemyで「速習!SwiftUI」講座を視聴する