代码下载
在应用中,用户可以创建一个简介来描述他们自已的个人情况。为了让用户可以编辑自己的简介,需要添加一个编辑模式并设计一个偏好设置界面。这里使用多种通用控件来展示用户的各种数据,并在用户保存他们所做的数据修改时更新地标数据模型。
按照步骤在下面的项目工程中一步步进行实践,项目文件。
展示用户简介
应用在本地存储了一些配置和用户偏好设置。在用户编辑这些数据前,会被展示在一个没有编辑按钮的概要视图上。
1、在工程的 Model 组中创建一个名为Profile.swift的文件,并在这个新文件中定义一个用户配置:
struct Profile {
var username: String
var prefersNotifications = true
var seasonalPhoto = Season.winter
var goalDate = Date()
static let `default` = Profile(username: "g_kumar")
enum Season: String, CaseIterable, Identifiable {
case spring = "🌷"
case summer = "🌞"
case autumn = "🍂"
case winter = "☃️"
var id: String { rawValue }
}
}
2、接下来,在Views组下创建一个名为Profiles的新组,然后向该组添加一个名为ProfileHost的视图,该视图带有显示存储的概要文件的用户名的文本视图。ProfileHost视图将承载概要信息的静态摘要视图和编辑模式。在稍后引入模型数据 Profile 之前,可以将这里的 draftProfile 设置为默认的 Profile 作为占位符:
struct ProfileHost: View {
@State var draftProfile = Profile.default
var body: some View {
Text("Profile for: \(draftProfile.username)")
}
}
3、在Profiles组中创建另一个名为 ProfileSummary 的视图,该视图会持有一个Profile实例,并显示用户的基本信息。Profile概要视图持有一个Profile对像的原因是,因为它的父视图ProfileHost管理着视图的状态,它不能与Profile进行绑定:
struct ProfileSummary: View {
var profile: Profile
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
Text(profile.username).bold()
.font(.title)
Text("Notifications: \(profile.prefersNotifications ? "On": "Off" )")
Text("Seasonal Photos: \(profile.seasonalPhoto.rawValue)")
Text("Goal Date: ") + Text(profile.goalDate, style: .date)
}
}
}
}
4、更新ProfileHost文件,显示新的概要视图:
struct ProfileHost: View {
@State var draftProfile = Profile.default
var body: some View {
VStack(alignment: .leading, spacing: 20) {
ProfileSummary(profile: draftProfile)
}.padding()
}
}
5、在 Hikes 文件夹中创建一个名为HikeBadge的新视图,这个新视图由Badge视图和一些描述性文字构成。Badge仅仅是一个图形,在HikeBadge视图中的文本与accessibility(label:)属性修改器一起,可以让这个徽章对用户更加清晰。注意frame(width:height:)的两种不同的用法用来配置徽章以不同的缩放尺寸显示,徽章的绘制逻辑产生的结果取决于它所呈现的frame的大小。为了确保理想的外观,在300 x 300点的frame中渲染。要获得最终图形所需的尺寸,然后缩放渲染结果并将其放置在相对较小的frame中:
struct HikeBadge: View {
var name: String
var body: some View {
VStack {
Badge().frame(width: 300, height: 300)
.scaleEffect(1.0/3.0)
.frame(width: 100, height: 100)
Text(name).font(.caption)
.accessibilityLabel("Badge for \(name).")
}
}
}
6、更新ProfileSummary文件,添加几个不同的徽章代表用户得到的不同徽章。通过包含HikeView来完成ProfileSummary。为了使用 hike 数据,还需要添加一个ModelData环境属性:
struct ProfileSummary: View {
@Environment(ModelData.self) var modelData
var profile: Profile
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
Text(profile.username).bold()
.font(.title)
Text("Notifications: \(profile.prefersNotifications ? "On": "Off" )")
Text("Seasonal Photos: \(profile.seasonalPhoto.rawValue)")
Text("Goal Date: ") + Text(profile.goalDate, style: .date)
Divider()
VStack(alignment: .leading) {
Text("Completed Badges").font(.headline)
ScrollView(.horizontal) {
HStack {
HikeBadge(name: "First Hike")
HikeBadge(name: "Earth Day").hueRotation(Angle(degrees: 90))
HikeBadge(name: "Tenth Hike").grayscale(0.5)
.hueRotation(Angle(degrees: 45))
}.padding(.bottom)
}
}
Divider()
VStack {
Text("")
HikeView(hike: ModelData().hikes[0])
}
}
}
}
}
#Preview {
ProfileSummary(profile: Profile.default).environment(ModelData())
}
7、在CategoryHome中,使用toolbar修改器向导航栏添加一个用户配置文件按钮,并在用户点击它时显示ProfileHost视图,添加listStyle修改器以选择更适合内容的列表样式:
struct CategoryHome: View {
@Environment(ModelData.self) var modelData: ModelData
@State private var showingProfile = false
var body: some View {
NavigationSplitView {
List {
modelData.features[0]
.image
.resizable()
.scaledToFill()
.frame(height: 200)
.clipped()
.listRowInsets(EdgeInsets())
ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: modelData.categories[key] ?? [])
}.listRowInsets(EdgeInsets())
}.listStyle(.inset)
.navigationTitle("Featured")
.toolbar(content: {
Button {
showingProfile.toggle()
} label: {
Label("User Profile", systemImage: "person.crop.circle")
}
}).sheet(isPresented: $showingProfile, content: {
ProfileHost().environment(modelData)
})
} detail: {
Text("Select a Landmark")
}
}
}
添加编辑模式
用户需要能够在浏览模式和编辑模式之间进行切换来查看或者修改用户简介的信息。通过在ProfileHost上添加一个Edit Button,然后创建一个用来编辑简介信息的页面。
1、选择ProfileHost并将模型数据作为环境属性添加到预览中。尽管这个视图没有使用带有@Environment属性包装器的属性,但是这个视图的子视图ProfileSummary使用了。因此,如果没有这个修改器,预览将失败。
2、添加一个环境视图属性,该属性与环境的.editmode无关。SwiftUI在环境中可以使用@Environment属性包装器对访问的值提供存储。前面使用@Environment来检索存储在环境中的类。在这里,使用它来访问内置于环境中的editMode值,以读取或写入编辑作用域。
3、创建一个Edit按钮,用于打开和关闭环境的editMode值。EditButton控制在上一步中访问的editMode环境值。
struct ProfileHost: View {
@Environment(\.editMode) var editMode
@State var draftProfile = Profile.default
var body: some View {
VStack(alignment: .leading, spacing: 20) {
ProfileSummary(profile: draftProfile)
}.padding()
}
}
#Preview {
ProfileHost().environment(ModelData())
}
4、更新UserData类,包含一个Profile实例,即使用户简介页面消失后也可以存储编辑后的信息:
class ModelData {
var landmarks: [Landmark] = load("landmarkData.json")
var hikes: [Hike] = load("hikeData.json")
var profile = Profile.default
var categories: [ String: [Landmark] ] {
Dictionary(grouping: landmarks) { $0.category.rawValue }
}
var features: [Landmark] {
landmarks.filter { $0.isFeatured }
}
}
5、从环境变量中读取用户简介信息,并把数据传递给ProfileHost视图的控件上进行展示。为了在编辑状态下修改简介信息后确认修改前避免更新全局状态(例如在编辑用户名的过程中),编辑视图在一个备份属性中进行相应的修改操作,确认修改后,才把备份属性同步到全局应用状态中。
6、添加一个条件视图,可以用来显示静态用户简介视图或者是编辑模式的用户简介视图。当前的编辑模式只支持静态文本框的编辑。
struct ProfileHost: View {
@Environment(\.editMode) var editMode
@Environment(ModelData.self) var modelData
@State var draftProfile = Profile.default
var body: some View {
VStack {
HStack {
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
} else {
Text("Profile Editor")
}
}.padding()
}
}
定义简介编辑器
用户简介编辑器包含几个单独的控件用来修改对应简介信息。在简介中,一些项例如徽章是不可以编辑修改的,所以它们不会出现在简介编辑器中。为了保持简介在编辑模式和浏览模式的一致性,需要按照简介页面各项相同的顺序进行添加。
1、创建一个名为ProfileEditor的新视图,并绑定用户简介中的草稿。视图中的第一个控件是TextField,用来更新用户名字段值。创建TextField时要提供一个标签和一个绑定字符串。
2、添加一个切换开关,用来设置用户是否接收相关地标事件的推送通知。这个Toggle控件打开和关闭正好对应着布尔值的true或false。
3、把一个Picker和一个Text放在VStack结构里,让这个地标可以选择不同季节。
4、最后,在季节图片选择器下方添加一个DatePicker,用来修改地标的目标浏览日期
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")
Spacer()
TextField("Username", text: $profile.username)
.foregroundStyle(.secondary)
.multilineTextAlignment(.trailing)
}
Toggle(isOn: $profile.prefersNotifications, label: {
Text("Enable Notifications")
})
Picker("Seasonal Photo", selection: $profile.seasonalPhoto) {
ForEach(Profile.Season.allCases) { season in
Text(season.rawValue).tag(season)
}
}
DatePicker(selection: $profile.goalDate, in: dateRange, displayedComponents: .date) {
Text("Goal Date")
}
}
}
}
#Preview {
ProfileEditor(profile: .constant(.default))
}
更新ProfileHost中的条件内容,让它包含条件编辑器并把简单的绑定关系传递给简介编辑器。现在当你点击Edit按钮,简介视图就会变成编辑模式了。:
struct ProfileHost: View {
@Environment(\.editMode) var editMode
@Environment(ModelData.self) var modelData
@State var draftProfile = Profile.default
var body: some View {
VStack {
HStack {
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
} else {
ProfileEditor(profile: $draftProfile)
}
}.padding()
}
}
延迟编辑传播
在编辑模式时,使用用户简介信息的备份进行修改,当用户确认进行修改后,再用修改的备份信息覆盖真正的用户信息。直到用户退出编辑模式前都不让编辑的备份生效。
1、在ProfileHost视图上添加一个取消按钮。不像编辑模式按钮提供的完成按钮,取消按钮不会应用修改后的简介备份信息到实际的简介数据上。
2、将正确的简介数据使用onAppear(perform:)和onDisappear(perform:)填充到编辑器,当用户点击完成按钮后持久更新用户简介数据。否则下一次进入编辑模式时,将使用上一次的用户简介数据来展示。
struct ProfileHost: View {
@Environment(\.editMode) var editMode
@Environment(ModelData.self) var modelData
@State var draftProfile = Profile.default
var body: some View {
VStack(alignment: .leading, spacing: 20) {
HStack {
if editMode?.wrappedValue == .active {
Button("Cancel", role: .cancel) {
draftProfile = modelData.profile
editMode?.animation().wrappedValue = .inactive
}
}
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
} else {
ProfileEditor(profile: $draftProfile)
.onAppear(perform: {
draftProfile = modelData.profile
})
.onDisappear(perform: {
modelData.profile = draftProfile
})
}
}.padding()
}
}