HACKING WITH SWIFT の SwiftUI Tutorial をやってみた

今更だけど SwiftUI を触ってみた。
まずは Apple の Tutorial をとりあえず一通りこなして、次に HACKING WITH SWIFT の Tutorial も終了。


HACKING WITH SWIFT Tutorial の Challenge 1, 2, 3 をやってみたのでコードのあれこれをメモ。

SwiftUI 完全に理解した!

環境とコード

Tutorial を試した環境は macOS Catalina 10.15.1, Xcode 11.2.1

Challenge 3 まで終了したコードは GitHub に置いてあります。

Challenge 1 オーダーボタンのフォント、バックグラウンドなどをカスタマイズする

英文だと Customize the “Order This” button with a custom font, background, and more.
Tutorial の中に出て来た「Order This」ボタンを色々とカスタマイズするお題。
オリジナルのコードはこれ。

            Button("Order This") {
                self.order.add(item: self.item)
            }.font(.headline)

既にフォントは .headline に指定してある。あとはバックグラウンドのカラーを変えたりすると良いかな。

ボタンにバックグラウンドカラーを付けるのは今の iOS 風でない気もするけど、一昔前って感じに仕上げてみることにした。

            Button(action: {
                self.order.add(item: self.item)
            }) {
                HStack {
                    Image(systemName: "cart.badge.plus")
                    Text("Order This")
                        .font(.headline)
                }
            }
            .padding(8)
            .background(Color(red: 0.9, green: 0.9, blue: 0.9))
            .cornerRadius(4)

Button の書き方は色々とあるのだけど、オリジナルとは違う Button(action: () -> Void, label: () -> _) で書いてる。
action: 部分はオリジナルそのまま。
label: 部分で HStack を使ってカートの画像と “Order This” のテキストを横に並べてる。
そしてボタン全体を padding で少し広げてバックグランドカラーを薄いグレーにして角を少し丸めた。

Challenge 2. CheckView に受け取り時間を選択する項目を加える

英文だと Add “Pickup time” to CheckoutView, with the options “Now”, “Tonight”, and “Tomorrow Morning”.
単純に “Now”, “Tonight”, “Tomorrow Morning” を選択する Picker を実装すれば良いのかな?と思ったので、支払い方法の選択と同じ様に実装。

まずは Picker に表示する選択肢のリストと @State で選択した値のやりとりをするプロパティを宣言。

    static let pickupTimes = ["Now", "Tonight", "Tomorrow Morning"]
    @State private var pickupTime = 0

あとは支払い方法の選択をする Picker と同じ様に Pickup time 用の Section を実装。

            Section(header: Text("Picup time")) {
                Picker("time:", selection: $pickupTime) {
                    ForEach(0 ..< Self.pickupTimes.count) {
                        Text(Self.pickupTimes[$0])
                    }
                }
            }

支払い方法の選択とまんま同じコードで面白みに欠けるけど SegmentedPickerStyle にしてもチップの選択と同じだし…

Challenge 3 ItemDetail にお気に入りボタンを付けてお気に入りタブを加える

英文だと Add a Favorite navigation bar item to ItemDetail and a Favorites tab showing them all.
ItemDetail で設定したお気に入り情報がタブに新たに追加された「お気に入りビュー」にも反映されるって問題。

あっちの値がこっちの値に反映されるので ObservableObject の @Published なプロパティを @EnvironmentObject で使うって感じかな?と。
反映される値はお気に入りのメニューなので MenuItem の id を記録するもの。お気に入りは一つじゃないので MenuItem の id の配列、[UUID] になる。
Apple Tutorial やったからか UserData に favorites: [UUID] が増えるので良いんじゃない?と思ったのでこんなクラスを用意。

class UserData: ObservableObject {
    @Published var favorites: [UUID] = []
    
    func isFavorite(_ id: UUID) -> Bool {
        return favorites.contains(id)
    }
    
    func toggleFavorite(_ id: UUID) {
        if let index = favorites.firstIndex(of: id) {
            favorites.remove(at: index)
        }
        else {
            favorites.append(id)
        }
    }
}

@Published var favorites: [UUID] = [] がそのユーザのお気に入りメニューを保存する配列。
isFavorite は渡されたメニューがお気に入りかどうかを確認する関数。
toggleFavorite は渡されたメニューのお気に入り情報をアップデートする関数。

あとはこの UserData を @EnvironmentObject で使ってそれぞれのビューをアップデートする様にすれば良い。

まずは ItemDetail
UserData を @EnvironmentObject で宣言

   @EnvironmentObject var userData: UserData

お気に入りボタンは var favoriteButton: some View として computed property にした。

    var favoriteButton: some View {
        Button(action: {
            self.userData.toggleFavorite(self.item.id)
        }) {
            Image(systemName: self.userData.isFavorite(self.item.id) ? "heart.fill" : "heart")
                .imageScale(.large)
            .padding()
        }
    }

action: 部分は userData に toggleFavorite(self.item.id) でそのメニューのお気に入りが押されたことを通知。
label: 部分は self.userData.isFavorite(self.item.id) ? “heart.fill” : “heart” でお気に入りかどうかに合わせて画像を変更している。

次にお気に入りタブで表示するお気に入りのリスト画面。これは FavoriteView として以下の様に実装。

struct FavoriteView: View {
    @EnvironmentObject var userData: UserData
    
    let menu = Bundle.main.decode([MenuSection].self, from: "menu.json")

    var body: some View {
        NavigationView {
            List {
                ForEach(menu) { section in
                    Section(header: Text(section.name)) {
                        ForEach(section.items) { item in
                            if self.userData.isFavorite(item.id) {
                                ItemRow(item: item)
                            }
                        }
                    }
                }
            }
            .navigationBarTitle("Favorite")
            .listStyle(GroupedListStyle())
        }
    }
}

struct FavoriteView_Previews: PreviewProvider {
    static var userData = UserData()
    
    static var previews: some View {
        FavoriteView().environmentObject(userData)
    }
}

ContentView と特に変わらない。
メニューのアイテムを表示する ForEach 内部で if self.userData.isFavorite(item.id) としてお気に入りのメニューだけ表示する様にしているだけ。

あとは最初に表示される AppView に FavoriteView のタブを増やす。

            FavoriteView()
                .tabItem {
                    Image(systemName: "heart")
                    Text("Favorite")
            }

最後に SceneDelegate で contentView に UserData を渡す。

        let contentView = AppView().environmentObject(order).environmentObject(userData)

もちろん、最初に var userData = UserData() としておくのも忘れない!

あ、プレビューで動作確認がしたい場合は xxx_Previews にもちゃんと static let userData = UserData() と .environmentObject(userData) を忘れない様に。

最後に

Apple の Tutorial の次に HACKING WITH SWIFT の Tutorial をやってみたのは自分としては良かった。
Apple の Tutorial の時は「ん?」と思っても先が楽しみでどんどん進んでしまったけど、こちらの Tutorial ではじっくり取り組めたので「なるほど」となる部分が結構あった。

ForEach は配列に入ってれば OK と思ってたり、@State でラップされたプロパティに直で値を入れようとして???となったり、色々と躓いていた部分がなんとか分かってきた。

と言うわけで、もう少し SwiftUI 触ってみるつもり。

Leave a Reply

Your email address will not be published. Required fields are marked *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)