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() } }