おはよう
さてさてSwiftUIのチュートリアル2をやっていくよ。
前回
チュートリアル1:Viewを作って、組み合わせてみよう!
#チュートリアル2: リストとナビゲーション画面を作ってみよう!
SwiftUI Essentials : Building Lists and Navigation
今回はランドマークの一覧画面と、それぞれの詳細ページを表示できるビューを作っていきます。
###Section1:ランドマークモデルを作ってみよう。
チュートリアル1では各要素を直書きしていたね。
ここではモデルを作ることで、呼び出した値を要素に反映する方法を学ぶよ。
呼び出すデータは、チュートリアルページからダウンロードした、「landmarkData.json」を使用するよ。
作っているプロジェクトの上に、該当ファイルをドラッグすると、こんな感じで追加がされるよ。

続いて新しく「Landmark.swift」というファイルを作ります。
こちらは、これまで作ってきたViewファイルではなく、データの読込処理のためのファイルなので、こちらのSwift Fileというテンプレートを使用します。

こちらには、landmarkData.jsonを呼び出した時の、項目名と値に一致するような要素を記載していきます。
ここでつけているプロパティ「Hashable」と「Codable」は、それぞれ下記のような意味です。
特徴 | |
---|---|
Hashable | ハッシュ値として定義するときに使うよ。これをつけることで、辞書のキー項目や、集合として使用することができるようになる。 |
Codable | JSONなどの外部表現との互換するために、データ型をエンコード・デコード可能にするよ。 |
参考ページ:
Encoding and Decoding Custom Types
Assets.xcassetsに画像を追加して、Landmark.swiftにもImageファイルを呼び出すための項目を付け足すよ。このとき「import SwiftUI」を追加し忘れないように! Imageがないよーって怒られる。
imageNameがprivateなのは、表示するファイル名を呼び出すのにこの項目を使い、実際画面に表示するのは、Image自体だから、とのこと。
landmarkData.jsonのcoordinates(=座標)は、structsにして保持するよ。
続いて、CoreLocationというデバイスの位置情報を読み取るためのフレームワークを使用します。
CLLocationCoordinate2DというのはWGS 84という測定系に基づいた形で場所を紐づけるための構造体で、そこにCoordinates型の変数coordinatesのlatitude, longitudeの値を渡しています。
イメージとしては、Jsonで読みこんだ値がcoordinatesに格納され、その格納された値をフレームワークに渡している感じかな。
最後に、新しくModelData.swiftファイルを作成し、JSONを読み込むメソッドを実装していきます。
プロジェクト内のリソースにアクセスするのに使うのが、「Bundle(バンドル)」。
これを使うことで、アプリ内のリソースを簡単にロードすることができるよ。
特にその中でもBundle.mainは、現在実行中のコードを含むバンドルディレクトリを指しているみたい。
なので、コードとしては下記のような意味になるのかな?
import Foundation
// 1. 実行しているところ。
var landmarks: [Landmark] = load("landmarkData.json")
//2.1で使用しているload関数の中身。データの入ったjsonファイルの名前が引数として渡されている。
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
//3.Bundle.main = 今動いているアプリのディレクトリ。その中から、"landmarkData.json"というターゲットファイルを取得している。
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
// 4.Dataはメモリ内のバイトバッファの構造体。取り込んだファイルの中身を、バイト列として読み込みしている感じかな?
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
//5.Jsonのデコーダーを作って、dataに格納されたデータをデコード
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
最後に、今プロジェクト直下に作成されているファイルを、Model, Views, Recoursesフォルダに格納し直して、整理整頓ー。
##Section2:行表示をしよう!
Section1ではモデルをJSONデータからモデルを作成したね。次はModelからデータを読み出して表示をしてみるよ。
新しいSwiftUIファイル「LandmarkRow.swift」を作って、まずはプレビュー画面にモデルから取得した値を表示指定みよう。実行の流れ1、2、3の順だよ。
import SwiftUI
struct LandmarkRow: View {
// 2.Landmark.swiftファイルでLandmarkを定義したね。その構造体を元に、変数「landmark」を定義。
var landmark : Landmark
var body: some View {
// 3.渡されたJSONファイル1行目のname項目の値をTEXTとして表示
Text(landmark.name)
}
}
struct LandmarkRow_Previews: PreviewProvider {
static var previews: some View {
//1.プレビューをするとLandmarkRowが実行されるよ。そのさいの引数がlandmakrs[0]
//ここで指定しているlandmarksはModelDataで定義したものだよ。
//こちらにはJSONファイルからloadしたデータが入っているよ。
//なので、ここでは、landmarkとしてJSONファイルの1行目を渡しているんだね。
LandmarkRow(landmark: landmarks[0])
}
}
おお、JSONファイルの1行目、nameの項目に格納された値が表示されているね。
あとは第一回で習ったHStackとImageを駆使して、Imageの横に文字が表示されるように変更するよ。
Section2でのプレビューと比べると、サイズが変わっているのがわかるね。
##Section3: 行プレビューをカスタムしちゃおう!
Section2ではJSONの1行目を呼び出して、画像をつけて表示したね。次は、JSONのデータを有効活用してみよう。
Section2でlandmarks[0]と、jsonの1行目を取り出してその名前を表示したね。0→1に変更すると表示も変わるよ。
このプレビューの表示を変えていくよ。
static var previews: some View {
LandmarkRow(landmark: landmarks[1])
//previewLayoutはプレビュー用のレイアウトを上書きするのに使うよ。
//ここでは縦横のサイズを指定しているんだね。
.previewLayout(.fixed(width: 300, height: 70))
}
}

複数設置する場合には、Groupを使ってコンテンツ同士をグループ化。そこに対して、previewLayoutをつけると、すっきり描けるよ。
▼ちょっとまどろっこしいコード
struct LandmarkRow_Previews: PreviewProvider {
static var previews: some View {
Group {
LandmarkRow(landmark: landmarks[0])
.previewLayout(.fixed(width: 300, height: 70))
LandmarkRow(landmark: landmarks[1])
.previewLayout(.fixed(width: 300, height: 70))
}
}
}
▼すっきりしたコード
struct LandmarkRow_Previews: PreviewProvider {
static var previews: some View {
Group {
LandmarkRow(landmark: landmarks[0])
LandmarkRow(landmark: landmarks[1])
}
.previewLayout(.fixed(width: 300, height: 70))
}
}
##Section4:リストを作成してみよう!
Listを使って、よくあるメニューリストを作ってみるよ。
LandmarkList.swiftを作成するよ。
Section3ではLandmarkRow_Previewsを主に編集したけれど、今回は本体のViewを編集します。
Section3で作ったLandmarkRowインスタンスを呼び出します。
struct LandmarkList: View {
var body: some View {
//List 別個に作られたLandmarkRowをリスト形式で表示するのに使うよ。
List {
LandmarkRow(landmark: landmarks[0])
LandmarkRow(landmark: landmarks[1])
}
}
}

##Section5:リストを動的に作成しよう!
さてさてアプリ画面ぽくなってきたね。でも、表示する内容は直書き・・とまでは言わないけれど、jsonの1行目、2行目と指定して表示しているよね。今度は、それを動的に作成するよ。
先ほど作った静的なLandmarkRowを消して、かわりにModelData.swiftで定義したlandmarksをListに渡すよ。
ついでに、リストに対する理解を深めるために、
①静的にリストを作る方法
②動的にリストを作る方法
③動的にリストを作る方法(チュートリアルStep2で載っているやり方)
を載せるね。
import SwiftUI
// リスト② で呼び出しているemojisの型。ここで重要なのが「Identifiable」
// リストに渡す値は一意である必要があるので、「Identifiable」に準拠しているよーと宣言すると良い。
struct Facemark: Identifiable {
let name: String
let emoji:String
let id = UUID()・
}
private var emojis = [
Facemark(name:"にっこり",emoji:"😀"),
Facemark(name:"おこ",emoji:"😡"),
Facemark(name:"尊い",emoji:"😭"),
Facemark(name:"いつもの", emoji:"☺️")
]
struct LandmarkList: View {
var body: some View {
// リスト① 静的リストを作るときはこんな感じだよ。
List{
Text("こんな感じで");
Text("Textを配置すると");
Text("静的なリストが");
Text("できるよ");
}
// リスト② リストの引数にコレクションを渡すと、Listは値を1つ=1行で読み出して表示をします。
// $0はクロージャーの引数名を省略したときに使われるもので,$0=引数の1個目、$1=引数の2個目という感じで表示がさレます。
List(emojis){
Text($0.name);
Text($0.emoji);
}
// リスト③ チュートリアルに載っているリストの作り方。
// こちらはidをいう一意の値を引数で指定することで、識別可能な状態にしています。
//ちょっと変わった書き方に見えるけれど、これはクロージャーという無名変数を使っているよ。意味はこんな感じ。
// (引数名:引数の型) { XX-戻り値の型-XX in
// 処理
// }
List(landmarks, id: \.id) { landmark in
LandmarkRow(landmark: landmark)
}
}
}
ポイントとしてはListは一意な値を引数に設定することが必要!
##Section6:ナビゲーションを使ってListから詳細に移動するようにしてみよう!
LandmarkDetail.swiftを作成して、チュートリアル1で作ったContentViewの中身を貼り付けるよ。
代わりにContentView.swiftは、中身をLandmarkListを呼び出すように変更。
struct LandmarkList: View {
var body: some View {
// NavigationViewを使うと、ページ間のビュー移動をカスタマイズできます。
// NavigationLinkで指定しているのが、クリックしたときの遷移先。
//navigationTitleはリスト上部にタイトルを表示するのに使う。
NavigationView {
List(landmarks) { landmark in
NavigationLink( destination: LandmarkDetail()){
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
動きは実際に作ってみていただきたいところですが、リストからクリックすると詳細ページに飛んで、
詳細ページからリストへの戻りボタンも自動で作成されているのが便利ポイントだなと思いました。
##Section7:詳細ページもデータから作ってみよう!
続いては、LandmarkDetailの編集だよ。
詳細ページには、テキストの他に画像、MAPもあり、それぞれ、imageNameとcoordinates(座標)をJSONから取得した値を渡すことで実装していきます。
ここから複数のファイルを変更することになるので、チュートリアルの順序とあべこべになってしまいますが、
まずはデータの流れをみていきますー。
先ほど作成したLandmarkList.swiftで、クリックした時のリンクに、LandmarkDetailを指定したね。
ここに、引数としてlandmarkを渡してあげます。
struct LandmarkList: View {
var body: some View {
NavigationView {
List(landmarks) { landmark in
// ここでlandmark型のlandmark値を渡しているよ。内容としてはそのリスト1行を作っているjsonデータを渡している感じだね。
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
受け取り側のLandmarkDetail.swiftで引数としてlandmarkを定義して、LandmarkListから渡された値を受け取れるようにして、今度はMapViewにlandmark.locationCoordicateを、 CircleImageにlandmark.imageを渡してあげるよ。
合わせて、Detailの直書きしているタイトルや説明文をlandmarkの該当の値に変更します。
完成形としてはこんな感じ。
struct LandmarkDetail: View {
var landmark: Landmark
var body: some View {
ScrollView {
// ここでMapViewに引数として座標の値を渡しているよ。
MapView(coordinate: landmark.locationCoordinate)
.ignoresSafeArea(edges: .top)
.frame(height: 300)
//ここでCircleImageに画像名を渡しているよ。
CircleImage(image : landmark.image)
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
// このあたりは今まで Text("tutlerock")みたいに、直書きだったのを変更しているよ。
Text(landmark.name)
.font(.title)
.foregroundColor(.primary)
HStack {
Text(landmark.park)
Spacer()
Text(landmark.state)
}
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text("About ¥(landmaark.name)")
.font(.title2)
Text(landmark.description)
}
.padding()
}
.navigationTitle(landmark.name)
.navigationBarTitleDisplayMode(.inline)
}
}
座標のデータを渡されたMapViewはこんな感じで、変更します。
struct MapView: View {
// LandmarkDetailからの引数としてcoordinateを受け取れるようにする。
var coordinate : CLLocationCoordinate2D
@State private var region = MKCoordinateRegion()
var body: some View {
Map(coordinateRegion: $region)
//onAppearは地図を描画の時に呼ばれるコールバック関数だよ。
//なので、下記の記述は地図を描画する時に、setRegion関数を実行するってことだね。
.onAppear {
setRegion(coordinate)
}
}
// 座標に基づいて領域を表示するための計算をprivate関数で記載
private func setRegion(_ coordinate : CLLocationCoordinate2D) {
region = MKCoordinateRegion(
center : coordinate,
span : MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
)
}
}
こちらはCricleImage.swift 画像の表示です。
import SwiftUI
struct CircleImage: View {
// 引数としてImageを取得
var image: Image
// そのImageに対して、形の整形をする。
var body: some View {
image
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 7)
}
}
これでListをクリックすると、詳細ページに移動するようになったよ
##Section8: プレビューを動的に作成してみよう!
デバイスのサイズによって実際表示される画面は変わるよね。
なので、ここでは、プレビューデバイスを変更して、いろんなサイズでのアプリ画面を確認できるようにするよ。
プレビューのデバイスを指定するにはpreviewDeviceを追加します。
ここで指定するモデルは、Xcodeのデバイスメニューで選ばれている名前orモデル番号で指定します。
もちろん、ここからプレビューを変えてもOK!
LandmarkList()
.previewDevice(PreviewDevice(rawValue: "iPhone SE (2nd generation)"))
一気に複数端末でプレビューしたい場合には、ForEachを使用して複数回LandmarkListを実行するようにすればOK
// ForEach(Data,ID)のようにデータを渡す。
// プレビューデバイス="iPhone SE(2nd)"として、Listの表示を1回、
// プレビューデバイス= "iPhone XS Max"としてListの表示を1回する。
ForEach(["iPhone SE(2nd)","iPhone XS Max"], id : \.self) {deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
}
こうすれば色々な端末でアプリを動かした時の様子が一目で分かって作りやすいね。
最後に理解度チェックをやって終了!
###まとめ
用意されているフレームワークやオブジェクトを有効活用すると、簡単にそれっぽい画面が作れて良いね!
間違っているところがあれば、ご指摘ください。