おはよう。だよ。
さてさて引き続き、SwiftUIチュートリアル3を勉強していくよ。
20分でできるって書いてあるけれど、調べならやっていると一日余裕で超えるね。能力差?
以前も書きましたが、チュートリアルに沿って構文とか意味とか覚えていこう!という趣旨なので、間違っているところとかあれば教えていただけると幸いです。
教材
[Introducing SwiftUI]
(https://developer.apple.com/tutorials/swiftui)
##チュートリアル3:ユーザーからの入力を操ろう!
Handling User Input
###Section1: ユーザーのお気に入り登録をしちゃおう
「Landmark.swift」で作業をするよ。
項目にisFavoriteというお気に入りかどうかを判断する変数を定義するよ。
point1-1
画像のように読み込んでいるlandmarkData.jsonにも同名の項目をつけることで、この項目名でjsonの項目とモデルのキーを結びつけているよ。
続いて、「LandmarkRow.swift」で作業をするよ。
if landmark.isFavorite {
// point1-2
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
///以下略
point1-2:systemName
「SF Symbols」というSwiftUIの中で使用できるアイコンのセットがあり、それを使用するときにsystemName:XX--アイコン名--XXという指定の仕方をする。
ちなみに「〜.fill」が塗りつぶしアイコンになるよ。
###Section2: リストにフィルターかけちゃおう
「LandmarkList.swift」で作業をするよ。
import SwiftUI
struct LandmarkList: View {
//point2-1
@State private var showFavoritesOnly = true
//point2-2
var filteredLandmarks: [Landmark] {
landmarks.filter { landmark in
(!showFavoritesOnly || landmark.isFavorite)
}
}
var body: some View {
NavigationView {
List(filteredLandmarks) { landmark in
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
point2-1:@State
読み書き可能な値として管理される場合に使うプロパティだよ、
ユーザーからの入力によってリストの見え方を変えたい場合、
@Stateをつけることで、変更を監視して、変更されるとその内容によってViewを再描画してくれるよ。
またこれはPrivateにして、呼び出しているViewそのものと、その子View内に限定して使用する必要があるよ。
参考:state
point2-2:filter
配列.filter {要素 in
条件
}
のような形で処理が書かれていて、
配列の1行1行が、条件に一致している場合はそれを返すようになっているよ。
24行目のListに渡す値によって、見え方がこんな感じで変わります。
変更前:リストに対してlandmarksのデータ自体を渡しているため、全件表示されている。
変更後:リストに対して、filterをかけた結果を渡している。今回はshoFavoirteOnly = trueなので、絞り込まれた状態になる!
絞り込まれたね
###Section3:Toggleボタンで表示を画面から操作してみよう!
次は、showFavoriteOnlyのtrue/falseの切り替えを画面から実施できるようにするよ。
landmarkList.swiftで作業をするよ。
var body: some View {
NavigationView {
List { // point3-1
Toggle (isOn:$showFavoritesOnly) { //point3-2
Text("Favorites Only")
}
ForEach(filteredLandmarks) { landmark in //point3-3
NavigationLink( destination: LandmarkDetail(landmark: landmark)){
LandmarkRow(landmark: landmark)
}
}
}
.navigationTitle("Landmarks")
}
}
}
point3-1:List
動的な要素と静的な要素を組み合わせるために、Listに直接landmarksを渡す形から、Listの中でネストする形に変更する。
point3-2:Toggle
Toggle(isOn : バインドする変数) {
Toggleのラベル
}
のような形で使用します。
今回は$showFavoritesOnlyにバインドされている。
これでToggleがOn,OFFによってshowFavoritesOnlyのTrue,Falseが切り替わるようになったよ。
point3-3:ForEach
showFavoritesOnlyが切り替わったら、リストもきりかえる必要があるため、ForEachを使う・・?のかな・・?
いまいちわからなかったので、もう少し調べます。
###Section4:ストレージを監視可能なオブジェクトにしよう。
Section4〜6ではユーザーがお気に入りをチェックしたり、外したりできるようにしていきます。
Section4ではまず、データモデルの整備をおこないます。
「ModelData.swift」で作業をするよ。
//point4-1
final class ModelData : ObservableObject {
//point 4-2
@Published var landmarks: [Landmark] = load("landmarkData.json")
}
///以下略
point4-1 ObservableObject
値の変更などのイベントの発行と監視ができるのがCombineフレームワーク。
これを使用することで、イベント処理を組み合わせて、非同期処理を楽に処理できるんだなーと理解しました。
ObservableObjectはそのフレームワークに準拠したかたちのクラスであることを定義していると思われます。@Statusでも同じように値の変更をViewに知らせる役割があったけれど、それをオブジェクト単位で付与できるのがObservableObjectかな。
[参考:Combine]https://developer.apple.com/documentation/combine
参考:ObservableObject
point4-2 Published
Combineフレームワークの中で、ポイントとなってくるのが、「Publisher」と「Subscriber」の二つです。
Publisher = イベント、データを発行する側
Subscriber = イベント、データを受け取る側
今回は、landmarkData.jsonの値の変更を監視して、何か変更された時Subscriberに知らせる必要があります。
なので、landmarksに@Publisherをつけて、Publisherとして定義してあげます。
参考:publisher
###Section5:ビューにモデルを使おう。
続いてはView側の修正をしていくよ。
チュートリアルとは順序が反対になるのですが、個人的にデータの流れを知りたいタイプなので、
Step6から実施します。
「LandmarksApp.swift」で作業します。
import SwiftUI
@main
struct LandmarksApp: App {
// point 5-1
@StateObject private var modelData = ModelData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelData)
}
}
}
point5-1:@StateObject
StateObjectは監視対象のオブジェクトをインスタント化する時に使用するよ。これを宣言することで、初期値の設定も行なっています。
そして作業しているLandmarksApp.swiftはアプリの数あるViewの中で最上位のViewだよね。
ここで、ModelData.swiftのModelDataクラスから「modelData」という新しいインスタンスを作成して、
ContentViewの環境オブジェクトとして渡しているよ。
import SwiftUI
struct ContentView: View {
var body: some View {
LandmarkList()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView() // point5-2
.environmentObject(ModelData())
}
}
point5-2:プレビュー用にenvironmentObject
次のContentViewでは実際のViewは特に変更はしなけれど、Previewに対しては同じように、
環境オブジェクトとして、ModelDateクラスを渡しているよ。
本来のアプリを起動した際に使用するルートでは、上位ViewですでにModelDataが渡されているね。
でも、開発中はView単体でプレビューすることもあり、そんな時だと「ModelDataがない! プレビューできない!」って失敗しちゃうので、
Viewごとに値を渡しているイメージかな。
同じように、LandmarkList.swift、LandmarkRow.swiftでも、プレビュー用に個々で値を設定しているよ。
最後に、「LandmarkList.swift」での処理をみてみるよ。
struct LandmarkList: View {
// point 5-3
@EnvironmentObject var modelData : ModelData
@State private var showFavoritesOnly = true
var filteredLandmarks :[Landmark]{
// point5-4
modelData.landmarks.filter { landmark in
(!showFavoritesOnly || landmark.isFavorite)
}
}
変更を加えたのは、pointをつけた2箇所です。
point5-3:@EnvironmentObject
ObservableObjectに準拠したModelとViewをつなぐ役割で、監視対象のModelが変更されると、現在のViewを作成しなおす。
@ObservableObjectというものもあり、同じくObservableObjectプロトコルで作られたClassをView側で使う時につけるものです。
違いとしては、@EnvironmentObjectだと、View同士の階層を気にせずそのアプリ全体で使用することができることがあげられます。
なので、今回みたいにViewAがViewBを読み出して・・のように親子、祖父親子と階層が出来上がっていて、
かつ、データ自体は1箇所という場合には、EnvironmentObjectとして、設定した方が良いよね..ってことかな?
point5-4
Model側でもModelDataクラスの中で、landmarkを定義したので、View側でも同じように読み出します。
###Section6:お気に入りボタンをつけよう!
さてさてここまでModelからViewにお気に入りのOn,OFFを知らせるための準備をしてきたので、
最後に実際にお気に入り処理を追加してみるよ。
新しくSwiftUIファイル「FavoriteButton.swift」を作成するよ。
import SwiftUI
struct FavoriteButton: View {
// point6-1
@Binding var isSet : Bool
var body: some View {
// point6-2
Button(action: {
isSet.toggle()
}){
Image(systemName: isSet ? "star.fill":"star")
.foregroundColor(isSet ? Color.yellow : Color.gray)
}
}
}
struct FavoriteButton_Previews: PreviewProvider {
static var previews: some View {
FavoriteButton(isSet: .constant(true))
}
}
これを「LandmarkDetail.swift」から呼び出すよ。
import SwiftUI
struct LandmarkDetail: View {
@EnvironmentObject var modelData :ModelData
var landmark: Landmark
var landmarkIndex: Int {
// point 6-3
modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
MapView(coordinate: landmark.locationCoordinate)
.ignoresSafeArea(edges: .top)
.frame(height: 300)
CircleImage(image : landmark.image)
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
.foregroundColor(.primary)
//Point 6-1
FavoriteButton(isSet:$modelData.landmarks[landmarkIndex].isFavorite)
}
HStack {
Text(landmark.park)
Spacer()
Text(landmark.state)
}
.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 let modelData = ModelData()
static var previews: some View {
LandmarkDetail(landmark : modelData.landmarks[0]).environmentObject(modelData)
}
}
point6-1:Binding
バインディングを使用することで表示側のViewとデータを格納しているプロパティ、双方からの読み書きができるようになるよ。
まずは、FavoriteButtonの方で、スターのOn,Offによって、True,Falseを変更できるような処理を書いているよね。
そのボタンを、呼び出しているLandmarkDetail.swift側のPoint6-1がついている部分で
isSetとlandmark.isFavoriteを結びつけているため、
isFavoriteが初期値としてisSetに渡され、isSetが変わる=>isFavoriteが変わるようになるよ。
参考:Binding
point6-2:Button
Buttion(action : トリガーとなる処理){
ラベル
}
のように使用するよ。ラベル部分はText(XXX)ではなく、アイコンでisSetのTrue,Falseによってスターの塗りつぶしと色を変えているよ。
[参考:Button]https://developer.apple.com/documentation/swiftui/button
point6-3:firstIndex(where:)
whereに条件を書いて、配列の中で一番最初に条件に一致するIndexを返すよ。
今回は配列のidと現在のLandmarkListから渡ってきたlandmark.idを比較して、一致しているIndexを返しています。
https://developer.apple.com/documentation/swift/array/2994722-firstindex
以上で、チュートリアル3が終了です。
###まとめ
StateやEnvironmentObjectなど、双方向のデータやり取りに必要なプロパティについて学んだよ。
なんとなーくのイメージしか掴めていないので、もっと調べて、使い分けとか自信持ってできるようになりたい・・・
また、今回はJSONから呼び出したけれど、APIなど外部から取得した値を使ってリストを表示する場合はどうやって書くのかな、とか
色々試してみたい箇所が出てきたよ。
間違っている箇所や、こう理解すると良いよ、といったアドバイスがあれば教えていただけると幸いです。
では