【入門者向け解説】SwiftUIのチュートリアルでiOSアプリ開発を体験してみたのでまとめ【第2回 ListとNavigation Link】
目次
こんにちは。
ふと「iOSアプリの開発ができるようになったら面白そうでは?」と思い至り、Apple公式チュートリアルを使ってアプリ開発の勉強をしてみました。
本記事では備忘録として要点をまとめておきます。
iPhoneのアプリってこうやって作るのか〜と入門者・初心者の方はイメージが湧くはず。
実際に挑戦してみたい方はこちらのApple公式チュートリアルに取り組んでみてください。(英語のみ)
こんな感じのサイトが公式で用意されていて、およそ4時間25分でチュートリアルを完了できるとのこと。
全体で4章にわかれていて、本記事は1章の中の2つめの内容をまとめてみます。
すでにチュートリアルに取り組んだ方も、本記事を読めば復習になるかと思います。ぜひ本文をチェックしてみてください。
第一回はこちら 【第一回 Viewで画面を構成】
チュートリアルの全体はこのようなChapter構成になっています。(タイトルは自己流で和訳しています)
- SwiftUIの基本
- Viewの作成と画面構成
- ListとNavigation Link ←今ここ
- ユーザーからの入力を扱う
- 描画とアニメーション
- PathsとShapes
- Viewと遷移のアニメーション
- アプリデザインとレイアウト
- 複雑なUIの組み合わせ
- UIコントロール
- フレームワークの統合
- UIKitを使ったインターフェース
- watchOSアプリ
- macOSアプリ
第一回のおさらい
SwiftUIチュートリアルでは「Landmark」というランドマークの写真や説明、マップ位置情報を表示するアプリを作っています。
第一回ではViewというアプリの画面構成をデザインする基本的な方法を学びました。Modifierを使って簡単に要素のデザインを変えることもできました。
今回の完成形のイメージ
1-1で作った画面に機能を追加して、このような画面ができます。
今回のチュートリアルのポイントは
- リストで同じ構造のViewを繰り返し表示できる
- ナビゲーションリンクが作成できる
- リストをタップするとその中身の詳細画面(前回1-1で作った画面)にアクセスできる
- 複数のデバイスでアプリの見た目のプレビューをシミュレーションできる
といった感じです。どれもアプリ開発で役に立ちそうな実用的な内容ですね!
作成するファイルの構成
それでは早速1-2で作成するファイル群を見てみましょう。
最終的に次の図の左側のような構成になりました。
それでは順番に見ていきましょう。
必要なファイルをダウンロードしてプロジェクトフォルダに移動する
前準備として、チュートリアルのページからProject filesをダウンロードしてください。
以下の2つが必要になります。
- landmarkData.json
- Landmarkのプロジェクトフォルダにコピペする
- Resources/Images/ 内のランドマーク画像ファイル群
- アセットカタログにドラッグ・アンド・ドロップする
リストで繰り返し表示するモデルのJSONファイルを用意
まずはランドマーク情報を読み込む部分を作成します。
先程用意したJSONファイルはリストで取り出したい中身となるランドマークの情報です。
[
{
"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
},
"description": "hogehoge..."
},
...
]
この内容をSwiftUIのViewからアクセスして、取り出していくわけです。
リストのように繰り返し同じ構造の情報を取り出したい場合は、JSONファイルで構造データとして記述することで動的に取り出すことができるようになります。つまり、ひとつひとつのランドマーク情報をViewに書き込む(=ハードコーディングする)必要がなくなります。
JSONファイルをSwiftUIで扱う方法については次に記載します。
繰り返し扱う構造の型をモデルとして定義し、JSONファイルをロードする
上記のJSONファイルで作成したランドマーク情報をプログラム内で効率よく扱うために、「型」というルールを定義します。これをモデルと呼びます。
ここでは2つのファイルを作成します。
- Landmark.swift
- ModelData.swift
作成する方法はXcodeのメニューでFile > New > File > Swift file
です。
モデルを定義するLandmark.swiftの中身は以下のようになります。
import Foundation
import SwiftUI
import CoreLocation
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var name: String
var park: String
var state: String
var description: String
private var imageName: String
var image: Image {
Image(imageName)
}
private var coordinates: Coordinates
var locationCoodinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable{
var latitude: Double
var longitude: Double
}
}
設計したJSONファイルの構造に倣ってプロパティ(property)の型を定義しました。プロパティとはvar
で宣言する変数のことです。
struct Landmark:
に続く部分をプロトコルといいます。Identifiable
プロトコルを書いて要素を一意に定まっていることを宣言しないと、後のList()
初期化でエラーになります。
CoreLocation
はMapKitフレームワークのUIで使うライブラリです。coordinate
プロパティで位置情報を扱えるようにします
余談ですが、coordinate
のスペルが難しすぎて途中でエラーになりました...
swift cannnot find coodinates in scope
import SwiftUI
やimport CoreLocation
を書き忘れていても、同様のエラーが出て「scopeに見つからないよ」と注意されます。
続いてModelData.swiftの中身を見てみます。
import Foundation
var landmarks: [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)")
}
}
- 冒頭で
landmarks :
という配列を宣言しload
関数でJSONファイルを読み込む - 配列の中身はLandmark.swiftで定義した
Landmark
の型 - JSONファイルを読み込むための
load
関数を宣言
呼び出しが関数より先にきても良いというのは独特ですね。
load
関数の細かい説明はチュートリアルの中にもありませんでしたが、要はJSONファイルをロード(フェッチ)して、問題があればエラーを吐き出せるようになっています。(余裕があれば細かく読み込んで勉強したいところですが、そのままコピペでも他のアプリに使いまわせそうです。)
リストで繰り返し表示する「1行」を作成する
リストを表示するために、次の2つのSwiftUI Viewファイルを作成します。
- LandmarkRow.swift
- LandmarkList.swift
それぞれ「行」と「行を繰り返し表示するリスト」の関係です。
LandmarkRow.swiftの中身はこんな感じ
import SwiftUI
struct LandmarkRow: View {
var landmark: Landmark
var body: some View {
HStack{
landmark.image
.resizable()
.frame(width: 50, height: 50)
Text(landmark.name)
Spacer()
}
}
}
struct LandmarkRow_Previews: PreviewProvider {
static var previews: some View {
Group{
LandmarkRow(landmark: landmarks[0])
LandmarkRow(landmark: landmarks[1])
}
.previewLayout(.fixed(width:300,height:70))
}
}
Viewブロックでは先程作ったモデルをlandmark
というプロパティとして宣言し、そのimage
とname
を水平に配置しています。あとSpacer()
も並べて右側を余白にしています。
PreviewProviderブロックではJSONファイルを読み込んだlandmarks
配列から1番目[0]
と2番目[1]
をViewブロックに渡しています。Group{}
というコンテナでくくってModifierを後に繋げるとコンテナ内の子要素すべてに適用されます。
こんな感じでプレビューされました。これらがリストに表示される「1行」です。
リストを表示させ、それぞれの行にナビゲーションリンクをつける
次にLandmarkList.swiftの中身を見てみます。今回のキモ。
import SwiftUI
struct LandmarkList: View {
var body: some View {
NavigationView{
List(landmarks) {
landmark in
NavigationLink{
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
分解してみてみると以下のようになっています。
List(landmarks) { landmark in ... }
- この部分で
landmarks
配列の中身をlandmark
として取り出して繰り返し処理します。
- この部分で
NavigationLink{B} label:{A}
- この部分で
A
をラベルとして表示し、タップするとB
に遷移するリンクを作成します。
- この部分で
.navigationTitle("")
- この部分でリストのタイトルをつけます
- 上記すべてを
NavigationView{}
で括るとナビゲーションとして機能するViewが作成されます
これで冒頭の完成形スクショ画像のようなリスト型のナビゲーションリンクが作成できました。
補足 : ListのIdentifierについて
リストの初期化としてlandmarks配列を呼び出していますが、このとき配列の要素はid
など固有の値で識別できるようにする必要があります。
2種類のやり方があります。
List(landmarks, id: \.id)
で初期化する- Landmark.swiftに
identifiable
プロトコルを追加する
識別できないとこのようなエラーが出ます。
Initializer 'init(_:rowContent:)' requires that 'Landmark' conform to 'Identifiable'
モデルを使って動的にViewを表示する
続いて、ナビゲーションリンクをタップしてアクセスするページにランドマーク情報を表示する部分を実装します。
前回はランドマークturtlerock
の情報をハードコーディングしていたので、この部分を今回作成したモデルLandmark
から引っ張ってくることで、動的なViewを作成することができます。
前回作ったContentView
のbody
中身をLandmarkDetail.swiftという新規ファイルのbody
にコピペし、ハードコーディングしたturtlerock
の情報の代わりにlandmark
モデルの属性を呼び出します。
import SwiftUI
struct LandmarkDetail: View {
var landmark: Landmark // モデルをlandmarkプロパティにセット
var body: some View {
ScrollView{
MapView(coordinate: landmark.locationCoodinate) // 位置情報
.ignoresSafeArea(edges: .top)
.frame(height: 240)
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)
}
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text("About \(landmark.name)")
.font(.title2)
Text(landmark.description) // 説明文
}
.padding()
}
.navigationTitle(landmark.name) // タイトル
.navigationBarTitleDisplayMode(.inline)
}
}
struct LandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
LandmarkDetail(landmark: landmarks[1]) // プレビュー用に配列の1つ目をセット
}
}
また、前回作成した以下のViewにもハードコーディングされている箇所があるため、書き換えます。
MapView.swift
- coordinateプロパティを追加
- PreviewProviderにプレビュー用のサンプルパスを渡す
- setRegionメソッドを追加
- coordinateの値に応じてregionを更新する
- onAppear modifierを追加し、現在のcoordinateに応じてregionを更新する計算を走らせる
import SwiftUI
import MapKit
struct MapView: View {
var coordinate: CLLocationCoordinate2D
@State private var region = MKCoordinateRegion()
var body: some View{
Map(coordinateRegion: $region)
.onAppear{
setRegion(coordinate)
}
}
private func setRegion(_ coordinate: CLLocationCoordinate2D){
region = MKCoordinateRegion(
center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
)
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView(coordinate: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868))
}
}
CircleImage.swift
- imageプロパティを追加
- PreviewProviderにプレビュー用のサンプルパスを渡す
import SwiftUI
struct CircleImage: View {
var image: Image
var body: some View {
image
.clipShape(Circle())
.overlay{
Circle().stroke(.white, lineWidth: 4)
}
.shadow(radius: 7)
}
}
struct CircleImage_Previews: PreviewProvider {
static var previews: some View {
CircleImage(image: Image("turtlerock"))
}
}
以上の作業が終わると、こんな感じにプレビューされます。各属性がJSONファイルから正しく読み込めてます。
ナビゲーションリンクの動作確認
ContentViewの中身はLandmarkDetailに移動したので、元のContentViewからはLandmarkListを参照するようにします。アプリの入り口をリストのナビゲーションリンクにするためです。
ContentView.swift
struct ContentView: View {
var body: some View {
LandmarkList()
}
}
LivePreviewモードでListをクリック(タップ)すると、Detailページに移動することが確認できました。
また、Detailページには左上にListに戻るボタンも自動で付加されています。これは便利。
ファイルのグループ分け
作成したファイルが増えてきたのでグループごとにまとめます。
方法は、まとめたいファイルを選択してからFile > New > Group from Selection
でグループを作成できます。
Views, Model, Resourcesといったグループに分類しました。
(冒頭と同じ図です)
アプリ画面をシミュレーションするデバイスを並べる
繰り返し処理にはList{}
の代わりにForEach()
文も使えます。
これで複数のデバイスで表示される見た目を同時にプレビューできます。めっちゃメモリ食いそう。
LandmarkList.swift
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone SE (2nd generation)", "iPhone XS Max"], id: \.self) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}
}
}
以上で全ファイルの中身を見終えました。お疲れさまでした。
まとめ
というわけでSwiftUIを使ったiOSアプリ開発をやってみよう!ということでApple公式チュートリアルのChapter1-2をまとめてみました。
今回の要点
- Listと、Identifiableもしくはidを使って動的に要素が並んだリストが作成できる
- NavigationLinkで画面タップして遷移するリンクを作成できる
- 繰り返し処理にはListの他にForEach文も使える
次回はChapter1-3でユーザ入力に対応させます。この調子でSwiftUIを学んでいきたいと思います。
オマケ : SwiftUIの独学に役立つ参考書籍
SwiftUIの解説本も発売されており、Kindleでも読めます。
こちらの本は発行が2019/12と少し古いですが、一番詳しく書かれている本のようです。(by Amazonレビュアー)
他の本は軒並み評価が低いので、買うとしたらコレかと。
ご参考になれば幸いです。
それでは〜