【第2回】日本語版SwiftUIチュートリアル【リストとナビゲーション】
日本語版SwiftUIチュートリアル
本チュートリアルは,Appleが掲載している公式のSwiftUIチュートリアルを「日本語で」そして「詳細に」解説し直すことを目的とした記事です.
この手の記事は,他にも似たようなものがありますが,全てを網羅しているわけではなく,部分的に解説している記事が多数です.
そこで,SwiftUIを使ってアプリ開発をしたい方々のために,そして勉強している自分自身のために,日本語版SwiftUIチュートリアルの完全版を書きます.
なお,本記事の難易度としては,初級者も見様見真似で作れるようにはなっていますが,それでは物足りないという中級者のために,細かい文法的な解説も随所に入れていこうと思います.
注意として,本記事はSwiftの言語解説はあまり含まないので,以下の記事を別タブで開いておいて,分からなければ逐一確認してみると良いと思います.
ちなみに出来上がるアプリは,上図に示すような観光地アプリです.
各観光地の位置情報を確認したり,その観光地をお気に入りに追加したりできるアプリを目指します.
第1回は????
第2回:リストとナビゲーション ~動的なビュー生成~
第2回は,リストの生成とリンクナビゲーションの解説が主になります.
また,前回は静的なビューしか生成していませんでしたが,今回は複数のデータをもとにビューを動的に生成する,といったことも目指します.
ただ,ここで先に言っておきたいのは,Step 1とStep 2はSwiftUIのチュートリアルとしては,少しコアな話が多く,それをしっかり読み解くとなると難易度は高めです.
読んでいて少しでも「あ,挫折しそう!」となったら,諦めてコピペで済ましてしまうの良い手だと思います.
とりあえずプロジェクトを完成させる,これを第一目標にして頑張ってください.
Step 1. モデルを作る
アプリ開発においてモデル (Model) は重要なパーツです.
MVCモデル (Model View Controller) のMにあたります.
モデルは,その名の通りひとつのデータの集まりを示しており,データベースなどと連携して,ビューやコントローラ側がデータを扱いやすくする役割を担います.
今回作るアプリでは,観光地ひとつひとつを示すモデルを作りましょう.
まずはModelsディレクトリを作りましょう.
File > New > Group から作成できます.
そのあと,Modelsディレクトリに新しく「Landmark.swift」を作りましょう!
1 2 3 4 5 | import SwiftUI struct Landmark{ } |
モデルは通常,構造体やクラスを用いて作成します.(モデルにプレビューは必要ないので消しちゃってください)
まだ空っぽですが,モデルを構築していきましょう.
まず,何がモデルのプロパティとして必要かを考えます.
とりあえず以下のようなプロパティを持ったモデルを考えてみることとします.
とりあえず素直に実装してみます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import SwiftUI struct Landmark{ var id: Int var name: String var imageName: String var coordinates: Coordinates var state: String var park: String var category: Category enum Category: String, CaseIterable{ case featured = "Featured" case lakes = "Lakes" case rivers = "Rivers" case mountains= "Mountains" } } struct Coordinates { var latitude: Double var longitude: Double } |
このとき,扱いやすくするために,座標とカテゴリは別で構造体,列挙型 (enum) として定義しておきます.
(ここ,本家だと case mountains= "Mountains" が無くて,そのまま進めると「Landmark.app may have crashed」するので気をつけてください!)
enumについている, CaseIterable はその名の通り,全ケースを Category.allCases で配列として取得できるようになる便利なプロトコルです.
そして,座標データをさらに扱いやすくするために, CoreLocation モジュールが提供する,経緯度座標系オブジェクト CLLocationCoordinate2D をプロパティとして追加しておきます.
(ここは深く理解しようとしなくて良いポイント)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | struct Landmark{ var id: Int var name: String var imageName: String fileprivate var coordinates: Coordinates var state: String var park: String var category: Category var locationCoordinate: CLLocationCoordinate2D { CLLocationCoordinate2D( latitude: coordinates.latitude, longitude: coordinates.longitude) } enum Category: String, CaseIterable{ case featured = "Featured" case lakes = "Lakes" case rivers = "Rivers" case mountains= "Mountains" } } |
このとき,もともとの座標データ coordinates は,外部ファイルから間違って呼び出しがされないように fileprivate アクセス修飾子をつけておきます.
また,画像も画像名だけだと,どうにもならないので, Image オブジェクトとして扱えるようなプロパティも追加しておきます.
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 | struct Landmark{ var id: Int var name: String fileprivate var imageName: String fileprivate var coordinates: Coordinates var state: String var park: String var category: Category var locationCoordinate: CLLocationCoordinate2D { CLLocationCoordinate2D( latitude: coordinates.latitude, longitude: coordinates.longitude) } enum Category: String, CaseIterable{ case featured = "Featured" case lakes = "Lakes" case rivers = "Rivers" case mountains= "Mountains" } } extension Landmark { var image: Image { ImageStore.shared.image(name: imageName) } } |
ここでは, extension で Landmark 構造体に追加する形で定義してみましたが,別に最初から内部で定義しても特に変わらないはずです.
また,このときも画像名を示すプロパティも使わないので, fileprivate で隠蔽しましょう.
で,実はここで ImageStore.shared.image(...) が解決できなくて怒られます.
チュートリアルでは,画像名からイメージオブジェクトに変換するクラス定義の解説を端折っているんですよね.
その話はこの後で説明しますが,まずはモデルの大枠を完成させましょう.
本モデルはResources内にある以下のようなJSONデータからデータを受け取って,今作ったモデルをもとにオブジェクトを生成することを想定しています.
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 | [ { "name": "Turtle Rock", "category": "Rivers", "city": "Twentynine Palms", "state": "California", "id": 1001, "isFeatured": true, "isFavorite": true, "park": "Joshua Tree National Park", "coordinates": { "longitude": -116.166868, "latitude": 34.011286 }, "imageName": "turtlerock" }, { "name": "Silver Salmon Creek", "category": "Lakes", "city": "Port Alsworth", "state": "Alaska", "id": 1002, "isFeatured": false, "isFavorite": false, "park": "Lake Clark National Park and Preserve", "coordinates": { "longitude": -152.665167, "latitude": 59.980167 }, "imageName": "silversalmoncreek" }, .... ] |
それを,コーディングしやすくするために,モデルにいくつかプロトコルを付与していきます.
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 | struct Landmark: Hashable, Codable{ var id: Int var name: String fileprivate var imageName: String fileprivate var coordinates: Coordinates var state: String var park: String var category: Category var locationCoordinate: CLLocationCoordinate2D { CLLocationCoordinate2D( latitude: coordinates.latitude, longitude: coordinates.longitude) } enum Category: String, CaseIterable, Hashable, Codable{ case featured = "Featured" case lakes = "Lakes" case rivers = "Rivers" case mountains= "Mountains" } } extension Landmark { var image: Image { ImageStore.shared.image(name: imageName) } } struct Coordinates: Hashable, Codable { var latitude: Double var longitude: Double } |
Hashable は自動でハッシュ値を生成してくれるプロトコルで, Codable はJSONからオブジェクトを生成する際に役立つプロトコルです.
といっても,ピンとこないと思うのでこのチュートリアルでは,「そういうもんなんだな」と思っておいてください.
Srep 2. データ処理部
さて,ここは本家チュートリアルにはありませんが,重要な部分です.
コードが複雑なのと,SwiftUIのチュートリアルでは要らないと判断されたのか,本家では作ってあるファイルをポンと渡すだけでしたが,ここでは少しだけ解説をしてみようかなと思います.
まずはコード全貌を見せます.
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | import UIKit import SwiftUI import CoreLocation let landmarkData: [Landmark] = load("landmarkData.json") func load<T: Decodable>(_ filename: String) -> 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(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)! } } |
とりあえず,Modelsディレクトリに,Data.swiftを新たに作成し,以上をコピペしてください.
ちなみに,このファイルだけで以下を担っています.
- JSONからオブジェクトの生成
- 画像名からイメージオブジェクトの生成
...
さて,ここからはコードの解説になります,飛ばして構いません.(???? 飛ばす場合はこちら)
と言っても,全ては解説しきれませんが,知識に貪欲なみなさまに向けてちょっとだけ.
1 | let landmarkData: [Landmark] = load("landmarkData.json") |
これは,JSONファイルを読み込んで, Landmark オブジェクト配列を作っていますね.
ただ,この load(...) は標準である関数ではないので,その下で定義しているわけですね.
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 | func load<T: Decodable>(_ filename: String) -> T { // ラベルなしで引数を受け取る let data: Data // 受け取ったファイル名からURL?オブジェクトを取得 // URLオブジェクトはただの文字列を持つオブジェクトではなく,URL操作が容易なオブジェクト guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else { // guard-else文はnilになったときelseになるような構文 fatalError("Couldn't find \(filename) in main bundle.") } // URLからDataオブジェクトに変換 do { data = try Data(contentsOf: file) } catch { fatalError("Couldn't load \(filename) from main bundle:\n\(error)") } // さらにJSONデータからの変換を試みる do { let decoder = JSONDecoder() // もし成功すれば,ジェネリクスとして受け取ったDecodableなクラス・構造体<T>をデコードして返す return try decoder.decode(T.self, from: data) } catch { fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)") } } |
随所にコメントを挿入してみました.
この関数の func load<T: Decodable> といった定義はジェネリクスと言い,この場合 Decodable なクラスや構造体を T という名前で置き換えていることになります.
ちなみに, Codable = Encodable + Decodable という定義になっていて,今回は T = Landmark となります.
その他,個々の標準であるものについては解説しませんが,やっている処理はなんとなく分かったかと思います.
細かく読んでみると,大した処理はしていませんね.
次は画像の処理部です,こちらの方が読むのは難しいです.
まずはプロパティから見てみましょう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 継承しないので final final class ImageStore { // Key: String => CGImage となるような辞書型を定義 typealias _ImageDictionary = [String: CGImage] // imageを先の辞書型の空配列として初期化 fileprivate var images: _ImageDictionary = [:] // 画像のスケーリング定数 (前回 2x にスケールしたので) fileprivate static var scale = 2 // シングルトン (このクラスで生成されるオブジェクトは1つだけに制限するデザインパターン) を定義 // これをするために,値型の構造体ではなく参照型のクラスにしたと思われる static var shared = ImageStore() .... } |
こちらもコメントで簡単に解説をつけました.
シングルトンについては,詳細は各自調べてください,ここでは深く解説しません.
次にメソッドです.
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 | // 3. 画像名からImageオブジェクトを返すメソッド func image(name: String) -> Image { // インデックス取得 (& 登録されていなければ登録) let index = _guaranteeImage(name: name) // CGImageから,加工したImageを返す return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(name)) } // 1. 画像名からCGImage (CoreGraphics Image) オブジェクトに変換するメソッド static func loadImage(name: String) -> CGImage { guard // URLとして読み込み let url = Bundle.main.url(forResource: name, withExtension: "jpg"), // URLからCGImageSourceを生成 let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil), // Sourceの先頭インデックスにある画像データをCGImageとして取得 let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { // どこかでnilならエラー fatalError("Couldn't load image \(name).jpg from main bundle.") } return image } // 2. 画像名から辞書のインデックスを取得するメソッド 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)! } |
これも,コメントで簡単に解説をつけました.
コードを読む順番は関数上部のコメントを辿ってみてください.
SwiftUIには,いくつか画像を扱うクラスがありますが, CGImage は画像処理用, Image は加工済みで,かつビューで扱うためのもの,という認識で良いかと思います.
Step 3. リストビューを作る前に構成要素を作る
まずはじめに,「ContentView.swift」を「LandmarkDetail.swift」に変更しておきます.
このとき構造体名もそれに変えて欲しいのですが,Xcodeのリファクタリング機能を使いましょう.
変更したい物にカーソルを当てて (今回は struct ContentView ),Editor > Refactor > Rename で変更してください.
そうしたら,一旦それは置いておいて新たに「LandmarkRow.swift」を作成しましょう.
これは,リストビューの一つ一つの行データを構成する構造体を作るためです.
そうしたら,先ほどJSONから読み込んで生成した,観光地モデル使うためのプロパティを定義します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import SwiftUI struct LandmarkRow: View { var landmark: Landmark var body: some View { Text("Hello, World!") } } struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { LandmarkRow() } } |
ただ,このままではプレビューが表示されません.
プレビューとしたら,「どの観光地データを使えば良いのかわからない」状態だからです.
なので,仮で一つ目のデータを渡しておいて,実装していきます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import SwiftUI struct LandmarkRow: View { var landmark: Landmark var body: some View { Text("Hello, World!") } } struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { LandmarkRow(landmark: landmarkData[0]) } } |
これで,プレビューが表示されるようになったはずです.
※ここで「Landmark.app may have crashed (≒JSONからオブジェクトが作れないエラー)」したら,Models > Landmark.swiftに, case mountains = "Mountains" が無いことが考えられます
そうしたら早速,観光地データが反映されているか確認してみましょう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import SwiftUI struct LandmarkRow: View { var landmark: Landmark var body: some View { Text(landmark.name) } } struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { LandmarkRow(landmark: landmarkData[0]) } } |
問題なさそうですね!
そうしたら,デザインを整えていきましょう!
1 2 3 4 5 6 7 8 9 | struct LandmarkRow: View { var landmark: Landmark var body: some View { HStack{ Text(landmark.name) } } } |
1 2 3 4 5 6 7 8 9 10 11 | struct LandmarkRow: View { var landmark: Landmark var body: some View { HStack{ landmark.image Text(landmark.name) Spacer() } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct LandmarkRow: View { var landmark: Landmark var body: some View { HStack{ landmark.image .resizable() .frame(width: 50, height: 50) Text(landmark.name) Spacer() } } } |
理想的なデザインに仕上がりました.
Step 4. 複数のデータを使ったプレビュー表示
今,試しに一つ目の観光データのプレビューで,動作を確認していますが,他のデータでもうまく動くでしょうか?
ということで試してみましょう!
1 2 3 4 5 | struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { LandmarkRow(landmark: landmarkData[1]) } } |
問題はなさそうですが,ちょっと手間ですね.
そこで,複数のプレビューを同時に表示させる方法を紹介します.
まずは,こんなに広いキャンパスは必要ないので必要最低限の大きさにします.
1 2 3 4 5 6 | struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { LandmarkRow(landmark: landmarkData[1]) .previewLayout(.fixed(width: 300, height: 70)) } } |
そうしたら, Group { } を使って複数のオブジェクトを一つのプレビューにまとめていきます.
1 2 3 4 5 6 7 8 9 10 11 12 | struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { Group { LandmarkRow(landmark: landmarkData[0]) .previewLayout(.fixed(width: 300, height: 70)) LandmarkRow(landmark: landmarkData[1]) .previewLayout(.fixed(width: 300, height: 70)) LandmarkRow(landmark: landmarkData[2]) .previewLayout(.fixed(width: 300, height: 70)) } } } |
様々なデータで,しっかりと理想通りの動作をしていることが一つのプレビューでわかるようになりました.
ただ,ちょっとコードが乱雑になってきましたね.
この .previewLayout(...) は View オブジェクトであれば適用可能であり, Group { } もその一つなので以下のようにまとめることができます.
1 2 3 4 5 6 7 8 9 10 | struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { Group { LandmarkRow(landmark: landmarkData[0]) LandmarkRow(landmark: landmarkData[1]) LandmarkRow(landmark: landmarkData[2]) } .previewLayout(.fixed(width: 300, height: 70)) } } |
とてもスッキリとしたコードになり,可読性も向上しましたね.
こういった小さなビューに限らず,プレビューを複数同時に描画させることで,作業効率の向上や,出来上がるアプリのイメージがしやすくなるので,上手く使っていきましょう.
Step 5. 観光地データをリストとして積み上げる
さて,第2回も後半戦です.
新たに,「LandmarkList.swift」を作成してください.
そうしたら,早速 List { } を使って,先ほど作った行データを積み上げてみましょう!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import SwiftUI struct LandmarkList: View { var body: some View { List { LandmarkRow(landmark: landmarkData[0]) LandmarkRow(landmark: landmarkData[1]) } } } struct LandmarkList_Previews: PreviewProvider { static var previews: some View { LandmarkList() } } |
このように, List { } も VStack { } のように垂直方向にビューを積み上げてくれる構造体ですが,こちらは予めリストデザインが決まっています.
またリストはスクロールが可能である点も一つの特徴です.
そうしたら,今は観光地データを固定で2つ,お試しで渡しているだけなので,全ての観光地データを動的にリストビューとして表示させましょう.
1 2 3 4 5 6 7 | struct LandmarkList: View { var body: some View { List(landmarkData) { landmark in LandmarkRow(landmark: landmark) } } } |
と,やりたいのですがこのままではダメです.
List(...) { } でオブジェクト配列を扱う時,各オブジェクトが常に識別可能である必要があります.
なぜかというと,オブジェクトのプロパティが今後変更になった時,ビューの再描画をすぐに行わなければならないからです.
つまり,各オブジェクトは一意に定まるIDなどのキーが必要で,それを List(...) { } に教えてあげる必要があります.
今回は各観光地オブジェクトは id という固有の値を持っていますのでそれを利用したいのですが,以下のように書く必要があります.
1 2 3 4 5 6 7 | struct LandmarkList: View { var body: some View { List(landmarkData, id: \.id) { landmark in LandmarkRow(landmark: landmark) } } } |
しかし,これも可読性が悪くなるので,SwiftUIでは最初に書いた綺麗なコードでも動作するようにするためのプロトコル Identifiable が用意されています.
これをモデルに付与します.
1 2 3 | struct Landmark: Hashable, Codable, Identifiable{ .... } |
これで,この構造体は id が識別子として自動で認識されるようになります.
すると,先のコードで理想的なプレビューが表示されるはずです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import SwiftUI struct LandmarkList: View { var body: some View { List(landmarkData) { landmark in LandmarkRow(landmark: landmark) } } } struct LandmarkList_Previews: PreviewProvider { static var previews: some View { LandmarkList() } } |
( List(landmarkData) { landmark in ... } の構文を見て「これなんだっけな」と思ったら,クロージャを見てください)
...
さて,ここでちょっとした補足を入れます.
先ほど,一瞬だけ List(landmarkData, id: \.id) が登場しましたが, \.id ←これは何でしょうか?
これはKey-Path Expressionと呼ばれる表記です.
これは一言で言い表すのであれば「プロパティへ動的にアクセスするための表記」です.
配列に対して,どのプロパティを見るかを決める時に使います.
例えば,年齢プロパティ age: Int を持つ User オブジェクトの配列, users があったとします.
それを,年齢でソートする時には以下のように表記します.
1 | users.sorted(by: \.age) |
どのオブジェクトにおいても age プロパティを見る,という意味がなんとなく理解できるかと思います.
Step 6. リスト⇄詳細ビューのナビゲーション
次に,リスト (LandmarkList.swift) と各観光地データ (LandmarkDetail.swift)を繋げます.
これをナビゲーションと呼びます.
ビューをナビゲーションとする場合は, NavigationView { } でそのビューを囲みます.
1 2 3 4 5 6 7 8 9 | struct LandmarkList: View { var body: some View { NavigationView{ List(landmarkData) { landmark in LandmarkRow(landmark: landmark) } } } } |
そして,ナビゲーションビューとなった各要素 (今回はリストビュー) にはタイトルをつけることができます.
1 2 3 4 5 6 7 8 9 10 | struct LandmarkList: View { var body: some View { NavigationView{ List(landmarkData) { landmark in LandmarkRow(landmark: landmark) } .navigationBarTitle(Text("Landmarks")) } } } |
このナビゲーションバータイトルの役割はすぐ後にわかります.
そうしたら,各行データをリンク付していきます.
リンク先は「LandmarkDetail.swift」です.
1 2 3 4 5 6 7 8 9 10 11 12 | struct LandmarkList: View { var body: some View { NavigationView{ List(landmarkData) { landmark in NavigationLink(destination: LandmarkDetail()) { LandmarkRow(landmark: landmark) } } .navigationBarTitle(Text("Landmarks")) } } } |
しっかりと,ナビゲーションが動作していますね.
ここで,ナビゲーションバータイトルは,前ページに戻るリンクテキストとしても使われていますね.
とてもアプリっぽくなってきましたが,まだ全ての行先の観光地詳細が「Turtle Rock」のままなので次は,その部分を動的なビューになるようにしていきます.
Step 7. 各要素を動的なビューにしていく
まずは,「CircleImage.swift」から.
まだ,この構造体では画像が静的なので,プロパティを新たに持たせることで,引数によって描画するビューが変わるようにします.
(以降のコード修正をすると,本ステップ最後の操作が終わるまでプレビューは表示されません)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import SwiftUI struct CircleImage: View { var image: Image var body: some View { image .clipShape(Circle()) .overlay( Circle().stroke(Color.white, lineWidth: 4)) .shadow(radius: 10) } } struct CircleImage_Previews: PreviewProvider { static var previews: some View { CircleImage(image: Image("turtlerock")) } } |
次は,「MapView.swift」です.
こちらも座標を動的に変えたいので,それをプロパティとします.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import SwiftUI import MapKit struct MapView: UIViewRepresentable { var coordinate: CLLocationCoordinate2D func makeUIView(context: Context) -> MKMapView { MKMapView(frame: .zero) } func updateUIView(_ uiView: MKMapView, context: Context) { // let coordinate = CLLocationCoordinate2D( // latitude: 34.011286, longitude: -116.166868) let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0) let region = MKCoordinateRegion(center: coordinate, span: span) uiView.setRegion(region, animated: true) } } struct MapView_Previews: PreviewProvider { static var previews: some View { MapView(coordinate: landmarkData[0].locationCoordinate) } } |
最後に,「LandmarkDetail.swift」.
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 37 38 39 40 | import SwiftUI struct LandmarkDetail: View { var landmark: Landmark var body: some View { VStack { MapView(coordinate: landmark.locationCoordinate) .edgesIgnoringSafeArea(.top) .frame(height: 300) CircleImage(image: landmark.image) .offset(y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { Text(landmark.name) .font(.title) HStack { Text(landmark.park) .font(.subheadline) Spacer() Text(landmark.state) .font(.subheadline) } } .padding() Spacer() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { LandmarkDetail(landmark: landmarkData[0]) } } |
そうしたら,アプリ立ち上げた際の最初のビューを「SceneDelegate.swift」で設定しましょう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). // Create the SwiftUI view that provides the window contents. // let contentView = LandmarkDetail() // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: LandmarkList()) self.window = window window.makeKeyAndVisible() } } .... } |
最後に,ナビゲーションを修正しましょう!
1 2 3 4 5 6 7 8 9 10 11 12 | struct LandmarkList: View { var body: some View { NavigationView{ List(landmarkData) { landmark in NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) } } .navigationBarTitle(Text("Landmarks")) } } } |
ここまでくると,全てのプレビューがまた上手く動作するようになるはずです.
Step 8. プレビュー端末を変える
ここまでは,一種類の端末 (本チュートリアルではiPhone SE 2) で動作確認をしてきました.
しかし,実際にはユーザの端末は一意ではありません.
そこで,プレビューに使われる端末を複数指定して同時に描画することも可能です.
まずは一つの端末を設定してみます.
1 2 3 4 5 6 | struct LandmarkList_Previews: PreviewProvider { static var previews: some View { LandmarkList() .previewDevice(PreviewDevice(rawValue: "iPhone X")) } } |
そして2台以上の端末でプレビューを描画するには,以下のようにします.
1 2 3 4 5 6 7 8 9 | struct LandmarkList_Previews: PreviewProvider { static var previews: some View { ForEach(["iPhone SE", "iPhone X"], id: \.self) { deviceName in LandmarkList() .previewDevice(PreviewDevice(rawValue: deviceName)) .previewDisplayName(deviceName) } } } |
以上,第2回の内容はここまでとなります.
第2回:おわりに
お疲れ様でした!
第2回も,重要なSwiftUIの機能をたくさん紹介しました.
ここまで一緒に実装してきた方であれば,なんとなく仕様も分かってきて楽しくなってきているのではないでしょうか?
さて,次回はユーザ入力の扱い方について解説していきます.