100 Days of SwiftUI(DAY 98(Project19 part 3))

3つのトピックに取り組む必要があります。ここでは、サイズクラスのサポートを追加し、施設に関する詳細情報を表示し、ユーザーがお気に入りのリゾートをマークできるようにします。

サイズクラスに応じてビューのレイアウトを変更する
Changing a view’s layout in response to size classes

SwiftUIは、アプリの現在のサイズクラスを監視するために2つの環境値を提供します。つまり、実際には、スペースが制限されている場合と、スペースが豊富な場合に別のレイアウトを表示できます。
2行2列のグリッド、つまり2行で、各行に2つのビューがあります。スペースが制限されている場合はこれは見栄えがしますが、スペースが大きい場合は、すべてを1行に並べた方が見栄えがよくなります。

通常サイズのクラスかコンパクトサイズのクラスか

縦長のすべてのiPhone幅はコンパクトで、高さは標準です。
横向きのほとんどのiPhoneの幅と高さはコンパクトです。
横長の大型iPhone(プラスサイズデバイスと最大デバイス)は、幅が通常で高さがコンパクトです。
両方の向きのすべてのiPadは、規則的な幅と規則的な高さを持っています。

私たちが気にしているのは、これらの2つの水平方向のオプションだけです。スペースが多い(通常)か、スペースが制限されている(コンパクト)かです。

スペースが少ない場合は、現在のネストされたVStackアプローチを維持して、すべてを1行にまとめようとはしませんが、スペースが空いている場合は、それを捨てて、ビューを直接親に配置しHStackます。

import SwiftUI

struct SkiDetailsView: View {
    let resort: Resort

    var body: some View {
        Group {
                Text("Elevation: \(resort.elevation)m").layoutPriority(1)
                Spacer().frame(height: 0)
                Text("Snow: \(resort.snowDepth)cm").layoutPriority(1)
            }
    }
}

struct SkiDetailsView_Previews: PreviewProvider {
    static var previews: some View {
        SkiDetailsView(resort: Resort.example)
    }
}


import SwiftUI

struct ResortDetailsView: View {
    let resort: Resort
    
//    var size: String {
//        ["Small", "Average", "Large"][resort.size - 1]
//    }
    
    var size: String {
        switch resort.size {
        case 1:
            return "Small"
        case 2:
            return "Average"
        default:
            return "Large"
        }
    }
    
    var price: String {
        String(repeating: "$", count: resort.price)
    }
    
    var body: some View {
        Group {
                Text("Size: \(size)").layoutPriority(1)
                Spacer().frame(height: 0)
                Text("Price: \(price)").layoutPriority(1)
            }
    }
}

struct ResortDetailsView_Previews: PreviewProvider {
    static var previews: some View {
        ResortDetailsView(resort: Resort.example)
    }
}

//
//  ResortView.swift
//  SnowSeeker

import SwiftUI

struct ResortView: View {
    //通常サイズのクラスかコンパクトサイズのクラスか
    @Environment(\.horizontalSizeClass) var sizeClass
    
    let resort: Resort

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 0) {
                Image(decorative: resort.id)
                    .resizable()
                    .scaledToFit()

                Group {
                    HStack {
                        if sizeClass == .compact {
                            Spacer()
                            VStack { ResortDetailsView(resort: resort) }
                            VStack { SkiDetailsView(resort: resort) }
                            Spacer()
                        } else {
                            ResortDetailsView(resort: resort)
                            Spacer().frame(height: 0)
                            SkiDetailsView(resort: resort)
                        }
                    }
                    .font(.headline)
                    .foregroundColor(.secondary)
                    .padding(.top)
                    
                    Text(resort.description)
                        .padding(.vertical)

                    Text("Facilities")
                        .font(.headline)

//                    Text(resort.facilities.joined(separator: ", "))
//                    .padding(.vertical)
                    Text(ListFormatter.localizedString(byJoining: resort.facilities))
                        .padding(.vertical)
                }
                .padding(.horizontal)
            }
        }
        .navigationBarTitle(Text("\(resort.name), \(resort.country)"), displayMode: .inline)
    }
}

struct ResortView_Previews: PreviewProvider {
    static var previews: some View {
        ResortView(resort: Resort.example)
    }
}

アラートをオプションの文字列にバインドする
Binding an alert to an optional string

//
//  Facility.swift
//  SnowSeeker_2


import SwiftUI

struct Facility: Identifiable {
    let id = UUID()
    var name: String

    var icon: some View {
        let icons = [
            "Accommodation": "house",
            "Beginners": "1.circle",
            "Cross-country": "map",
            "Eco-friendly": "leaf.arrow.circlepath",
            "Family": "person.3"
        ]

        if let iconName = icons[name] {
            let image = Image(systemName: iconName)
                            .accessibility(label: Text(name))
                            .foregroundColor(.secondary)
            return image
        } else {
            fatalError("Unknown facility type: \(name)")
        }
    }

    var alert: Alert {
        let messages = [
            "Accommodation": "This resort has popular on-site accommodation.",
            "Beginners": "This resort has lots of ski schools.",
            "Cross-country": "This resort has many cross-country ski routes.",
            "Eco-friendly": "This resort has won an award for environmental friendliness.",
            "Family": "This resort is popular with families."
        ]

        if let message = messages[name] {
            return Alert(title: Text(name), message: Text(message))
        } else {
            fatalError("Unknown facility type: \(name)")
        }
    }
}
/*
struct Facility_Previews: PreviewProvider {
    static var previews: some View {
        Facility()
    }
}
 */

struct Facility_Previews: PreviewProvider {
    static var previews: some View {
        Text("Hello, World!")
    }
}


//
//  Resort.swift
//  SnowSeeker

import Foundation

struct Resort: Codable, Identifiable {
    let id: String
    let name: String
    let country: String
    let description: String
    let imageCredit: String
    let price: Int
    let size: Int
    let snowDepth: Int
    let elevation: Int
    let runs: Int
    let facilities: [String]
    
    //Bundle-Decodable.swift関連
    static let allResorts: [Resort] = Bundle.main.decode("resorts.json")
    static let example = allResorts[0]
    //以下も有り
    //static let example = (Bundle.main.decode("resorts.json") as [Resort])[0]
    
    var facilityTypes: [Facility] {
        facilities.map(Facility.init)
    }
}


//
//  ResortView.swift
//  SnowSeeker


import SwiftUI

struct ResortView: View {
    //通常サイズのクラスかコンパクトサイズのクラスか
    @Environment(\.horizontalSizeClass) var sizeClass
    //@State private var selectedFacility: String?
    @State private var selectedFacility: Facility?
    
    let resort: Resort

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 0) {
                Image(decorative: resort.id)
                    .resizable()
                    .scaledToFit()

                Group {
                    HStack {
                        if sizeClass == .compact {
                            Spacer()
                            VStack { ResortDetailsView(resort: resort) }
                            VStack { SkiDetailsView(resort: resort) }
                            Spacer()
                        } else {
                            ResortDetailsView(resort: resort)
                            Spacer().frame(height: 0)
                            SkiDetailsView(resort: resort)
                        }
                    }
                    .font(.headline)
                    .foregroundColor(.secondary)
                    .padding(.top)
                    
                    Text(resort.description)
                        .padding(.vertical)

                    Text("Facilities")
                        .font(.headline)

//                    Text(resort.facilities.joined(separator: ", "))
//                    .padding(.vertical)
//                    Text(ListFormatter.localizedString(byJoining: resort.facilities))
//                        .padding(.vertical)
                    HStack {
                        ForEach(resort.facilityTypes) { facility in
                            facility.icon
                                .font(.title)
                                .onTapGesture {
                                    self.selectedFacility = facility
                                }
                        }
                    }
                    .padding(.vertical)
                    
                }
                .padding(.horizontal)
            }
        }
        .navigationBarTitle(Text("\(resort.name), \(resort.country)"), displayMode: .inline)
        .alert(item: $selectedFacility) { facility in
            facility.alert
        }
    }
}

//----------------------------
extension String: Identifiable {
    public var id: String { self }
}

struct ResortView_Previews: PreviewProvider {
    static var previews: some View {
        ResortView(resort: Resort.example)
    }
}


ユーザーにお気に入りのマークを付ける
Letting the user mark favorites

このプロジェクトの最後のタスクは、ユーザーがお気に入りのリゾートにお気に入りを割り当てられるようにすることです。これは、これまでに説明した手法を使用して、ほとんどが簡単です。
ユーザーが好むリゾートID Favoritesを持つ新しいクラスを作成Setします。
それを与えるadd()、remove()とcontains()もにすべての変更を保存しているときにSwiftUIに更新通知を送信し、データを操作する方法UserDefaults。
Favoritesクラスのインスタンスを環境に注入する。
適切なメソッドを呼び出す新しいUIを追加します。

Swiftのセットには、要素を追加、削除、およびチェックするためのメソッドが既に含まれobjectWillChangeていますが、変更を加えて、SwiftUIに変更が発生したことを通知したり、save()メソッドを呼び出してユーザーの変更を永続化したりできるようにします。これは、privateアクセス制御を使用してお気に入りセットにマークを付けることができることを意味します。そのため、誤ってメソッドをバイパスして保存を逃すことがありません。


//
//  Favorites.swift
//  SnowSeeker

import SwiftUI

class Favorites: ObservableObject {
    // the actual resorts the user has favorited
    private var resorts: Set<String>

    // the key we're using to read/write in UserDefaults
    private let saveKey = "Favorites"

    init() {
        // load our saved data

        // still here? Use an empty array
        self.resorts = []
    }

    // returns true if our set contains this resort
    func contains(_ resort: Resort) -> Bool {
        resorts.contains(resort.id)
    }

    // adds the resort to our set, updates all views, and saves the change
    func add(_ resort: Resort) {
        objectWillChange.send()
        resorts.insert(resort.id)
        save()
    }

    // removes the resort from our set, updates all views, and saves the change
    func remove(_ resort: Resort) {
        objectWillChange.send()
        resorts.remove(resort.id)
        save()
    }

    func save() {
        // write out our data
    }
}



//
//  ResortView.swift
//  SnowSeeker


import SwiftUI

struct ResortView: View {
    @EnvironmentObject var favorites: Favorites
    
    //通常サイズのクラスかコンパクトサイズのクラスか
    @Environment(\.horizontalSizeClass) var sizeClass
    //@State private var selectedFacility: String?
    @State private var selectedFacility: Facility?
    
    let resort: Resort

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 0) {
                Image(decorative: resort.id)
                    .resizable()
                    .scaledToFit()

                Group {
                    HStack {
                        if sizeClass == .compact {
                            Spacer()
                            VStack { ResortDetailsView(resort: resort) }
                            VStack { SkiDetailsView(resort: resort) }
                            Spacer()
                        } else {
                            ResortDetailsView(resort: resort)
                            Spacer().frame(height: 0)
                            SkiDetailsView(resort: resort)
                        }
                    }
                    .font(.headline)
                    .foregroundColor(.secondary)
                    .padding(.top)
                    
                    Text(resort.description)
                        .padding(.vertical)

                    Text("Facilities")
                        .font(.headline)

//                    Text(resort.facilities.joined(separator: ", "))
//                    .padding(.vertical)
//                    Text(ListFormatter.localizedString(byJoining: resort.facilities))
//                        .padding(.vertical)
                    HStack {
                        ForEach(resort.facilityTypes) { facility in
                            facility.icon
                                .font(.title)
                                .onTapGesture {
                                    self.selectedFacility = facility
                                }
                        }
                    }
                    .padding(.vertical)
                    
                }
                .padding(.horizontal)
            }
            // For Favorites
            Button(favorites.contains(resort) ? "Remove from Favorites" : "Add to Favorites") {
                if self.favorites.contains(self.resort) {
                    self.favorites.remove(self.resort)
                } else {
                    self.favorites.add(self.resort)
                }
            }
            .padding()
        }
        .navigationBarTitle(Text("\(resort.name), \(resort.country)"), displayMode: .inline)
        .alert(item: $selectedFacility) { facility in
            facility.alert
        }
    }
}

//----------------------------
extension String: Identifiable {
    public var id: String { self }
}

struct ResortView_Previews: PreviewProvider {
    static var previews: some View {
        ResortView(resort: Resort.example)
    }
}



//
//  ContentView.swift
//  SnowSeeker


import SwiftUI


struct ContentView: View {
    let resorts: [Resort] = Bundle.main.decode("resorts.json")
    @ObservedObject var favorites = Favorites()

    var body: some View {
        
        NavigationView {
            List(resorts) { resort in
                //NavigationLink(destination: Text(resort.name)) {
                NavigationLink(destination: ResortView(resort: resort)) {
                    Image(resort.country)
                        .resizable()
                        .scaledToFill()
                        .frame(width: 40, height: 25)
                        .clipShape(
                            RoundedRectangle(cornerRadius: 5)
                        )
                        .overlay(
                            RoundedRectangle(cornerRadius: 5)
                                .stroke(Color.black, lineWidth: 1)
                        )

                    VStack(alignment: .leading) {
                        Text(resort.name)
                            .font(.headline)
                        Text("\(resort.runs) runs")
                            .foregroundColor(.secondary)
                    }
                    .layoutPriority(1)
                    // For Favorites
                    if self.favorites.contains(resort) {
                        Spacer()
                        Image(systemName: "heart.fill")
                        .accessibility(label: Text("This is a favorite resort"))
                            .foregroundColor(.red)
                    }
                }
            }
            .navigationBarTitle("Resorts")
            
            WelcomeView()
        }
        .environmentObject(favorites)
        
    }
}

extension View {
    func phoneOnlyStackNavigationView() -> some View {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
        } else {
            return AnyView(self)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}