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