-
Notifications
You must be signed in to change notification settings - Fork 314
Working with UI Controls
在
Landmarksapp 中,用户可以创建个人简介来展示自己。为了让用户能修改个人简介,我们需要添加一个编辑模式,并设计一个偏好设置界面。我们将使用多种常用的 UI 控件来处理数据,并在用户保存修改时更新
Landmarks模型。
- 预计完成时间:25 分钟
- 项目文件:下载
Landmarks app 在本地保存一些详细配置和偏好设置。在用户编辑他们的简介前,会在一个没有修改控件的摘要视图中显示出来。
1.1 在 Landmark 文件夹里创建一个新文件夹 Profile ,然后在里面创建一个新文件 ProfileHost.swift 。
ProfileHost 视图负责用户信息的静态摘要视图以及编辑模式。
ProfileHost.swift
import SwiftUI struct ProfileHost: View { @State var draftProfile = Profile.default var body: some View { Text("Profile for: \(draftProfile.username)") } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost() } }
1.2 在 Home.swift 中,把静态的 Text 换成上一步中创建的 ProfileHost 。
现在主屏幕中的 profile 按钮会显示一个带有用户信息的模态。
Home.swift
import SwiftUI struct CategoryHome: View { var categories: [String: [Landmark]] { Dictionary( grouping: landmarkData, by: { 0ドル.category.rawValue } ) } var featured: [Landmark] { landmarkData.filter { 0ドル.isFeatured } } @State var showingProfile = false // @EnvironmentObject var userData: UserData // var profileButton: some View { Button(action: { self.showingProfile.toggle() }) { Image(systemName: "person.crop.circle") .imageScale(.large) .accessibility(label: Text("User Profile")) .padding() } } var body: some View { NavigationView { List { FeaturedLandmarks(landmarks: featured) .scaledToFill() .frame(height: 200) .clipped() .listRowInsets(EdgeInsets()) ForEach(categories.keys.sorted(), id: \.self) { key in CategoryRow(categoryName: key, items: self.categories[key]!) } .listRowInsets(EdgeInsets()) NavigationLink(destination: LandmarkList()) { Text("See All") } } .navigationBarTitle(Text("Featured")) .navigationBarItems(trailing: profileButton) .sheet(isPresented: $showingProfile) { // ProfileHost() .environmentObject(self.userData) // } } } } struct FeaturedLandmarks: View { var landmarks: [Landmark] var body: some View { landmarks[0].image.resizable() } } struct CategoryHome_Previews: PreviewProvider { static var previews: some View { CategoryHome() } }
1.3 创建一个新视图 ProfileSummary ,它持有一个 Profile 实例并显示一些基本用户信息。
ProfileSummary 持有一个 Profile 值要比一个简介的绑定更合适,因为它的父视图ProfileHost 负责管理它的 state 。
ProfileSummary.swift
import SwiftUI struct ProfileSummary: View { var profile: Profile static let goalFormat: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .none return formatter }() var body: some View { List { Text(profile.username) .bold() .font(.title) Text("Notifications: \(self.profile.prefersNotifications ? "On": "Off" )") Text("Seasonal Photos: \(self.profile.seasonalPhoto.rawValue)") Text("Goal Date: \(self.profile.goalDate, formatter: Self.goalFormat)") } } } struct ProfileSummary_Previews: PreviewProvider { static var previews: some View { ProfileSummary(profile: Profile.default) } }
1.4 更新 ProfileHost 来显示简介视图。
ProfileHost.swift
import SwiftUI struct ProfileHost: View { @State var draftProfile = Profile.default var body: some View { // VStack(alignment: .leading, spacing: 20) { ProfileSummary(profile: draftProfile) } .padding() // } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost() } }
1.5 创建一个新视图 HikeBadge ,这个视图组合了 绘制路径和形状 中的徽章以及一些远足的描述文本。
徽章只是一个图形,因此 HikeBadge 中的文本以及 accessibility(label:) 修饰符让徽章对其他用户来说含义更加清晰。
注意:两个调用 frame(width:height:) 的修饰符让徽章以 300 ×ばつ 300 点的设计尺寸进行缩放渲染。
HikeBadge.swift
import SwiftUI struct HikeBadge: View { var name: String var body: some View { VStack(alignment: .center) { Badge() .frame(width: 300, height: 300) .scaleEffect(1.0 / 3.0) .frame(width: 100, height: 100) Text(name) .font(.caption) .accessibility(label: Text("Badge for \(name).")) } } } struct HikeBadge_Previews: PreviewProvider { static var previews: some View { HikeBadge(name: "Preview Testing") } }
1.6 更新 ProfileSummary ,给它添加几个具有不同色调的徽章以及获得徽章的原因。
ProfileSummary.swift
import SwiftUI struct ProfileSummary: View { var profile: Profile static let goalFormat: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .none return formatter }() var body: some View { List { Text(profile.username) .bold() .font(.title) Text("Notifications: \(self.profile.prefersNotifications ? "On": "Off" )") Text("Seasonal Photos: \(self.profile.seasonalPhoto.rawValue)") Text("Goal Date: \(self.profile.goalDate, formatter: Self.goalFormat)") // VStack(alignment: .leading) { Text("Completed Badges") .font(.headline) ScrollView { HStack { HikeBadge(name: "First Hike") HikeBadge(name: "Earth Day") .hueRotation(Angle(degrees: 90)) HikeBadge(name: "Tenth Hike") .grayscale(0.5) .hueRotation(Angle(degrees: 45)) } } .frame(height: 140) } // } } } struct ProfileSummary_Previews: PreviewProvider { static var previews: some View { ProfileSummary(profile: Profile.default) } }
1.7 引入 动画视图与转场 中的 HikeView 来完成 ProfileSummary 。
ProfileSummary.swift
import SwiftUI struct ProfileSummary: View { var profile: Profile static let goalFormat: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .none return formatter }() var body: some View { List { Text(profile.username) .bold() .font(.title) Text("Notifications: \(self.profile.prefersNotifications ? "On": "Off" )") Text("Seasonal Photos: \(self.profile.seasonalPhoto.rawValue)") Text("Goal Date: \(self.profile.goalDate, formatter: Self.goalFormat)") VStack(alignment: .leading) { Text("Completed Badges") .font(.headline) ScrollView { HStack { HikeBadge(name: "First Hike") HikeBadge(name: "Earth Day") .hueRotation(Angle(degrees: 90)) HikeBadge(name: "Tenth Hike") .grayscale(0.5) .hueRotation(Angle(degrees: 45)) } } .frame(height: 140) } // VStack(alignment: .leading) { Text("Recent Hikes") .font(.headline) HikeView(hike: hikeData[0]) } // } } } struct ProfileSummary_Previews: PreviewProvider { static var previews: some View { ProfileSummary(profile: Profile.default) } }
用户需要在个人简介中切换浏览模式和编辑模式。我们会通过在现有的 ProfileHost 中添加一个 EditButton 来实现编辑模式,并且创建一个带有编辑单个数据控件的视图。
2.1 创建一个 Environment 视图属性,并输入 \.editMode 。
我们可以使用此属性来读取和写入当前编辑范围。
ProfileHost.swift
import SwiftUI struct ProfileHost: View { // @Environment(\.editMode) var mode // @State var draftProfile = Profile.default var body: some View { VStack(alignment: .leading, spacing: 20) { ProfileSummary(profile: draftProfile) } .padding() } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost() } }
2.2 创建一个可以切换环境中编辑模式开关的 Edit 按钮。
ProfileHost.swift
import SwiftUI struct ProfileHost: View { @Environment(\.editMode) var mode @State var draftProfile = Profile.default var body: some View { VStack(alignment: .leading, spacing: 20) { // HStack { Spacer() EditButton() } // ProfileSummary(profile: draftProfile) } .padding() } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost() } }
2.3 更新 UserData 类,让其包括一个用户个人资料的实例,即使在用户关闭个人简介视图之后仍然保留数据。
UserData.swift
import Combine import SwiftUI final class UserData: ObservableObject { @Published var showFavoritesOnly = false @Published var landmarks = landmarkData // @Published var profile = Profile.default // }
2.4 从环境中读取用户的配置文件数据,然后将数据的控制权传递给 ProfileHost 。
为了避免在任何编辑确认之前(例如在用户输入名称时)更新 app 的全局状态,编辑视图会对自身进行复制。
ProfileHost.swift
import SwiftUI struct ProfileHost: View { @Environment(\.editMode) var mode // @EnvironmentObject var userData: UserData // @State var draftProfile = Profile.default // // var body: some View { VStack(alignment: .leading, spacing: 20) { HStack { Spacer() EditButton() } ProfileSummary(profile: draftProfile) } .padding() } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost() } }
2.5 添加一个条件视图,来显示静态简介或编辑模式的视图。
注意:目前,编辑模式只是一个静态的文本。
ProfileHost.swift
import SwiftUI struct ProfileHost: View { @Environment(\.editMode) var mode @EnvironmentObject var userData: UserData @State var draftProfile = Profile.default var body: some View { VStack(alignment: .leading, spacing: 20) { HStack { Spacer() EditButton() } // if self.mode?.wrappedValue == .inactive { ProfileSummary(profile: userData.profile) } else { Text("Profile Editor") } // } .padding() } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost() } }
用户简介编辑器主要包含了更改详情时的不同控件。简介中徽章之类某些项目是用户编辑不了的,因此它们不会出现在编辑器中。
为了与信息摘要保持一致,我们会在编辑器中以相同的顺序添加信息详情。
3.1 创建一个新视图 ProfileEditor ,然后给用户信息的草稿副本引入一个绑定。
视图中第一个控件是一个 TextField ,它控制并更新一个字符串的绑定,在此例子中则是用户选择的显示名称。
ProfileEditor.swift
import SwiftUI struct ProfileEditor: View { @Binding var profile: Profile var body: some View { List { HStack { Text("Username").bold() Divider() TextField("Username", text: $profile.username) } } } } struct ProfileEditor_Previews: PreviewProvider { static var previews: some View { ProfileEditor(profile: .constant(.default)) } }
3.2 更新 ProfileHost 中的条件内容,引入 ProfileEditor 并给它传递一个信息的绑定。
现在当你点击 Edit 后,信息编辑视图就会显示。
ProfileHost.swift
import SwiftUI struct ProfileHost: View { @Environment(\.editMode) var mode @EnvironmentObject var userData: UserData @State var draftProfile = Profile.default var body: some View { VStack(alignment: .leading, spacing: 20) { HStack { Spacer() EditButton() } if self.mode?.wrappedValue == .inactive { ProfileSummary(profile: userData.profile) } else { // ProfileEditor(profile: $draftProfile) // } } .padding() } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost() } }
3.3 添加接收地标相关事件通知的开关,它与用户偏好相对应。
开关是只有 on 或 off 的控件,所以它很适合像 yes 或 no 之类的 Boolean 值。
ProfileEditor.swift
import SwiftUI struct ProfileEditor: View { @Binding var profile: Profile var body: some View { List { HStack { Text("Username").bold() Divider() TextField("Username", text: $profile.username) } // Toggle(isOn: $profile.prefersNotifications) { Text("Enable Notifications") } // } } } struct ProfileEditor_Previews: PreviewProvider { static var previews: some View { ProfileEditor(profile: .constant(.default)) } }
3.4 将一个 Picker 控件和它的标签放在一个 VStack 中,使地标照片具有可选择的季节。
ProfileEditor.swift
import SwiftUI struct ProfileEditor: View { @Binding var profile: Profile var body: some View { List { HStack { Text("Username").bold() Divider() TextField("Username", text: $profile.username) } Toggle(isOn: $profile.prefersNotifications) { Text("Enable Notifications") } // VStack(alignment: .leading, spacing: 20) { Text("Seasonal Photo").bold() Picker("Seasonal Photo", selection: $profile.seasonalPhoto) { ForEach(Profile.Season.allCases, id: \.self) { season in Text(season.rawValue).tag(season) } } .pickerStyle(SegmentedPickerStyle()) } .padding(.top) // } } } struct ProfileEditor_Previews: PreviewProvider { static var previews: some View { ProfileEditor(profile: .constant(.default)) } }
3.5 最后,在季节选择器的下面添加一个 DatePicker ,用来修改到达地标的日期。
ProfileEditor.swift
import SwiftUI struct ProfileEditor: View { @Binding var profile: Profile // var dateRange: ClosedRange<Date> { let min = Calendar.current.date(byAdding: .year, value: -1, to: profile.goalDate)! let max = Calendar.current.date(byAdding: .year, value: 1, to: profile.goalDate)! return min...max } // var body: some View { List { HStack { Text("Username").bold() Divider() TextField("Username", text: $profile.username) } Toggle(isOn: $profile.prefersNotifications) { Text("Enable Notifications") } VStack(alignment: .leading, spacing: 20) { Text("Seasonal Photo").bold() Picker("Seasonal Photo", selection: $profile.seasonalPhoto) { ForEach(Profile.Season.allCases, id: \.self) { season in Text(season.rawValue).tag(season) } } .pickerStyle(SegmentedPickerStyle()) } // .padding(.top) VStack(alignment: .leading, spacing: 20) { Text("Goal Date").bold() DatePicker( "Goal Date", selection: $profile.goalDate, in: dateRange, displayedComponents: .date) } // .padding(.top) } } } struct ProfileEditor_Previews: PreviewProvider { static var previews: some View { ProfileEditor(profile: .constant(.default)) } }
要使编辑在用户退出编辑模式之后才生效,我们需要在编辑期间使用信息的草稿副本,然后仅在用户确认编辑时将草稿副本分配给真实副本。
4.1 给 ProfileHost 添加一个确认按钮。
与 EditButton 提供的 Cancel 按钮不同, Done 按钮会在其操作闭包中将编辑应用于实际的数据。
ProfileHost.swift
import SwiftUI struct ProfileHost: View { @Environment(\.editMode) var mode @EnvironmentObject var userData: UserData @State var draftProfile = Profile.default var body: some View { VStack(alignment: .leading, spacing: 20) { HStack { // if self.mode?.wrappedValue == .active { Button("Cancel") { self.draftProfile = self.userData.profile self.mode?.animation().wrappedValue = .inactive } } // Spacer() EditButton() } if self.mode?.wrappedValue == .inactive { ProfileSummary(profile: userData.profile) } else { ProfileEditor(profile: $draftProfile) } } .padding() } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost() } }
4.2 使用 onAppear(perform:) 和 onDisappear(perform:) 修饰符将正确的简介数据填充到编辑器中,并在用户点击完成按钮时更新持久性简介文件。
否则,下次编辑模式激活时会显示旧值。
ProfileHost.swift
import SwiftUI struct ProfileHost: View { @Environment(\.editMode) var mode @EnvironmentObject var userData: UserData @State var draftProfile = Profile.default var body: some View { VStack(alignment: .leading, spacing: 20) { HStack { if self.mode?.wrappedValue == .active { Button("Cancel") { self.draftProfile = self.userData.profile self.mode?.animation().wrappedValue = .inactive } } Spacer() EditButton() } if self.mode?.wrappedValue == .inactive { ProfileSummary(profile: userData.profile) } else { ProfileEditor(profile: $draftProfile) // .onAppear { self.draftProfile = self.userData.profile } .onDisappear { self.userData.profile = self.draftProfile } // } } .padding() } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost() } }