在Landmarks应用程序中,用户可以创建个人资料来表达自己的个性。为了让用户能够更改他们的个人资料,您将添加编辑模式并设计首选项屏幕。
您将使用各种常见的用户界面控件进行数据输入,并在用户保存更改时更新Landmarks模型类型。
展示用户资料
1). 创建数据模型
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 }
}
}
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). 创建页面
创建ProfileSummary页面用户展示用户信息
swift
// 存储展示用户信息
struct ProfileSummary: View {
var profile:Profile
var body: some View {
VStack(alignment: .leading) {
Text.init(profile.username)
.bold()
.font(.title)
Text("Notifications: \(profile.prefersNotifications ? "On" : "Off")")
Text("Seasonal Photots: \(profile.seasonalPhoto.rawValue)")
Text("Goal Date: ") + Text(profile.goalDate, style: .date)
}
}
}
struct ProfileSummary_Previews: PreviewProvider {
static var previews: some View {
ProfileSummary(profile: Profile.default)
}
}
// 存储展示用户信息
struct ProfileSummary: View {
var profile:Profile
var body: some View {
VStack(alignment: .leading) {
Text.init(profile.username)
.bold()
.font(.title)
Text("Notifications: \(profile.prefersNotifications ? "On" : "Off")")
Text("Seasonal Photots: \(profile.seasonalPhoto.rawValue)")
Text("Goal Date: ") + Text(profile.goalDate, style: .date)
}
}
}
struct ProfileSummary_Previews: PreviewProvider {
static var previews: some View {
ProfileSummary(profile: Profile.default)
}
}
创建ProfileHost页面:
swift
import SwiftUI
struct ProfileHost: View {
@State private var draftProfile = Profile.default
var body: some View {
VStack.init(alignment: .leading ,spacing:20) {
ProfileSummary(profile: draftProfile)
}.background(.blue)
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost()
}
}
import SwiftUI
struct ProfileHost: View {
@State private var draftProfile = Profile.default
var body: some View {
VStack.init(alignment: .leading ,spacing:20) {
ProfileSummary(profile: draftProfile)
}.background(.blue)
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost()
}
}
3). 创建HikeBadge页面,组合之前写的Badge页面
swift
import SwiftUI
struct HikeBadge: View {
var name: String
var body: some View {
VStack {
Badge()
// 徽章的绘制逻辑产生的结果取决于渲染框架的大小。为确保获得理想的外观,请在300 x 300点的框架中进行渲染。为获得最终图形所需的尺寸,可缩放渲染结果并将其放置在相应较小的框架中。
.frame(width: 300, height: 300)
.scaleEffect(1 / 3.0)
.frame(width: 100,height: 100)
Text(name)
.font(.caption)
// 徽章只是一个图形,因此 HikeBadge 中的文本以及 accessibilityLabel(_:) 修饰符可以让其他用户更清楚地了解徽章的含义。
.accessibilityLabel("Badge for \(name).")
}
}
}
struct HikeBadge_Previews: PreviewProvider {
static var previews: some View {
HikeBadge(name: "Preivew Testing")
}
}
import SwiftUI
struct HikeBadge: View {
var name: String
var body: some View {
VStack {
Badge()
// 徽章的绘制逻辑产生的结果取决于渲染框架的大小。为确保获得理想的外观,请在300 x 300点的框架中进行渲染。为获得最终图形所需的尺寸,可缩放渲染结果并将其放置在相应较小的框架中。
.frame(width: 300, height: 300)
.scaleEffect(1 / 3.0)
.frame(width: 100,height: 100)
Text(name)
.font(.caption)
// 徽章只是一个图形,因此 HikeBadge 中的文本以及 accessibilityLabel(_:) 修饰符可以让其他用户更清楚地了解徽章的含义。
.accessibilityLabel("Badge for \(name).")
}
}
}
struct HikeBadge_Previews: PreviewProvider {
static var previews: some View {
HikeBadge(name: "Preivew Testing")
}
}
- 完善 ProfileSummary 页面
swift
import SwiftUI
// 存储展示用户信息
struct ProfileSummary: View {
var profile: Profile
@EnvironmentObject var modelData: ModelData
var body: some View {
ScrollView.init {
VStack(alignment: .leading) {
Text(profile.username)
.bold()
.font(.title)
Text("Notifications: \(profile.prefersNotifications ? "On" : "Off")")
Text("Seasonal Photots: \(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(alignment: .leading) {
Text("Recent Hikes")
.font(.headline)
HikeView(hike: modelData.hikes[0])
}
}
}
}
}
struct ProfileSummary_Previews: PreviewProvider {
static var previews: some View {
ProfileSummary(profile: Profile.default)
.environmentObject(ModelData())
}
}
import SwiftUI
// 存储展示用户信息
struct ProfileSummary: View {
var profile: Profile
@EnvironmentObject var modelData: ModelData
var body: some View {
ScrollView.init {
VStack(alignment: .leading) {
Text(profile.username)
.bold()
.font(.title)
Text("Notifications: \(profile.prefersNotifications ? "On" : "Off")")
Text("Seasonal Photots: \(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(alignment: .leading) {
Text("Recent Hikes")
.font(.headline)
HikeView(hike: modelData.hikes[0])
}
}
}
}
}
struct ProfileSummary_Previews: PreviewProvider {
static var previews: some View {
ProfileSummary(profile: Profile.default)
.environmentObject(ModelData())
}
}
5). 调整首页,支持弹出详情
swift
struct CategoryHome: View {
@EnvironmentObject var modelData: ModelData
@State private var showingProfile = false
var body: some View {
NavigationView {
List(content: {
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]!)
}
})
// 调整样式
.listStyle(.inset)
.navigationTitle("Featured")
// 增加顶部按钮 (类似navgationItem)
.toolbar {
Button {
showingProfile.toggle()
} label: {
Label("User Profile", systemImage: "person.crop.circle")
}
}
// 弹出模态视图
.sheet(isPresented: $showingProfile) {
ProfileHost()
.environmentObject(modelData)
}
}
}
}
struct CategoryHome: View {
@EnvironmentObject var modelData: ModelData
@State private var showingProfile = false
var body: some View {
NavigationView {
List(content: {
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]!)
}
})
// 调整样式
.listStyle(.inset)
.navigationTitle("Featured")
// 增加顶部按钮 (类似navgationItem)
.toolbar {
Button {
showingProfile.toggle()
} label: {
Label("User Profile", systemImage: "person.crop.circle")
}
}
// 弹出模态视图
.sheet(isPresented: $showingProfile) {
ProfileHost()
.environmentObject(modelData)
}
}
}
}
给ProfileHost增加编辑功能
1). 修改数据源
swift
final class ModelData: ObservableObject {
// 可观察对象需要发布对其数据的任何更改,以便其订阅者能够接收更改。
@Published var landmarks: [Landmark] = load("landmarkData.json")
@Published var profile = Profile.default
var hikes: [Hike] = load("hikeData.json")
var features: [Landmark] {
landmarks.filter { $0.isFeatured }
}
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarks,
by: { $0.category.rawValue }
)
}
}
final class ModelData: ObservableObject {
// 可观察对象需要发布对其数据的任何更改,以便其订阅者能够接收更改。
@Published var landmarks: [Landmark] = load("landmarkData.json")
@Published var profile = Profile.default
var hikes: [Hike] = load("hikeData.json")
var features: [Landmark] {
landmarks.filter { $0.isFeatured }
}
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarks,
by: { $0.category.rawValue }
)
}
}
2). 修改ProfileHost页面
swift
import SwiftUI
struct ProfileHost: View {
// 添加一个 Environment 视图属性,该属性以环境的 \.editMode 为键。
// SwiftUI 在环境中为您可以使用 @Environment 属性包装器访问的值提供存储。访问 editMode 值可读取或写入编辑范围。
@Environment(\.editMode) var editMode
@EnvironmentObject var modelData: ModelData
//@State private var draftProfile = Profile.default
var body: some View {
VStack(alignment: .leading, spacing: 20) {
// VStack.init(alignment: .leading ,spacing:20) {
// ProfileSummary(profile: draftProfile)
// }
HStack {
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
} else {
Text("On Editing")
Spacer()
}
}.padding()
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost().environmentObject(ModelData())
}
}
import SwiftUI
struct ProfileHost: View {
// 添加一个 Environment 视图属性,该属性以环境的 \.editMode 为键。
// SwiftUI 在环境中为您可以使用 @Environment 属性包装器访问的值提供存储。访问 editMode 值可读取或写入编辑范围。
@Environment(\.editMode) var editMode
@EnvironmentObject var modelData: ModelData
//@State private var draftProfile = Profile.default
var body: some View {
VStack(alignment: .leading, spacing: 20) {
// VStack.init(alignment: .leading ,spacing:20) {
// ProfileSummary(profile: draftProfile)
// }
HStack {
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
} else {
Text("On Editing")
Spacer()
}
}.padding()
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost().environmentObject(ModelData())
}
}
3). 编辑页面:绑定各个字段. 这里使用了TextField / Trogle/Picker/DatePicker
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 控制显示、编辑字符串
TextField("Username", text: $profile.username)
}
// Toggles
Toggle(isOn: $profile.prefersNotifications) {
Text("Enable Notifications").bold()
}
// Picker segmented style
VStack(alignment: .leading, spacing: 20) {
Text("Seasonal Photo").bold()
Picker("Seasonal Photo", selection: $profile.seasonalPhoto) {
ForEach(Profile.Season.allCases) { season in
Text(season.rawValue).tag(season)
}
}
.pickerStyle(.segmented)
}
// date picker
DatePicker(selection: $profile.goalDate, in: dateRange, displayedComponents: .date) {
Text("Goal Date").bold()
}
}
}
}
struct ProfileEditor_Previews: PreviewProvider {
static var previews: some View {
ProfileEditor(profile: .constant(.default))
}
}
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 控制显示、编辑字符串
TextField("Username", text: $profile.username)
}
// Toggles
Toggle(isOn: $profile.prefersNotifications) {
Text("Enable Notifications").bold()
}
// Picker segmented style
VStack(alignment: .leading, spacing: 20) {
Text("Seasonal Photo").bold()
Picker("Seasonal Photo", selection: $profile.seasonalPhoto) {
ForEach(Profile.Season.allCases) { season in
Text(season.rawValue).tag(season)
}
}
.pickerStyle(.segmented)
}
// date picker
DatePicker(selection: $profile.goalDate, in: dateRange, displayedComponents: .date) {
Text("Goal Date").bold()
}
}
}
}
struct ProfileEditor_Previews: PreviewProvider {
static var previews: some View {
ProfileEditor(profile: .constant(.default))
}
}
4). 调整ProfileHost页面
swift
import SwiftUI
struct ProfileHost: View {
// 添加一个 Environment 视图属性,该属性以环境的 \.editMode 为键。
// SwiftUI 在环境中为您可以使用 @Environment 属性包装器访问的值提供存储。访问 editMode 值可读取或写入编辑范围。
@Environment(\.editMode) var editMode
@EnvironmentObject var modelData: ModelData
var body: some View {
VStack(alignment: .leading, spacing: 20) {
HStack {
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
} else {
// Text("On Editing")
ProfileEditor(profile: $modelData.profile)
Spacer()
}
}.padding()
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost().environmentObject(ModelData())
}
}
import SwiftUI
struct ProfileHost: View {
// 添加一个 Environment 视图属性,该属性以环境的 \.editMode 为键。
// SwiftUI 在环境中为您可以使用 @Environment 属性包装器访问的值提供存储。访问 editMode 值可读取或写入编辑范围。
@Environment(\.editMode) var editMode
@EnvironmentObject var modelData: ModelData
var body: some View {
VStack(alignment: .leading, spacing: 20) {
HStack {
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
} else {
// Text("On Editing")
ProfileEditor(profile: $modelData.profile)
Spacer()
}
}.padding()
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost().environmentObject(ModelData())
}
}
5). 官方示例中边界方法稍有不同,是根据onAppear、onDisappear来控制数据赋值绑定的
swift
import SwiftUI
struct ProfileHost: View {
// 添加一个 Environment 视图属性,该属性以环境的 \.editMode 为键。
// SwiftUI 在环境中为您可以使用 @Environment 属性包装器访问的值提供存储。访问 editMode 值可读取或写入编辑范围。
@Environment(\.editMode) var editMode
@EnvironmentObject var modelData: ModelData
@State private var draftProfile = Profile.default
var body: some View {
VStack(alignment: .leading, spacing: 20) {
HStack {
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
} else {
// Text("On Editing")
ProfileEditor(profile: $modelData.profile)
.onAppear{
draftProfile = modelData.profile
}
.onDisappear {
modelData.profile = draftProfile
}
Spacer()
}
}.padding()
}
}
import SwiftUI
struct ProfileHost: View {
// 添加一个 Environment 视图属性,该属性以环境的 \.editMode 为键。
// SwiftUI 在环境中为您可以使用 @Environment 属性包装器访问的值提供存储。访问 editMode 值可读取或写入编辑范围。
@Environment(\.editMode) var editMode
@EnvironmentObject var modelData: ModelData
@State private var draftProfile = Profile.default
var body: some View {
VStack(alignment: .leading, spacing: 20) {
HStack {
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
} else {
// Text("On Editing")
ProfileEditor(profile: $modelData.profile)
.onAppear{
draftProfile = modelData.profile
}
.onDisappear {
modelData.profile = draftProfile
}
Spacer()
}
}.padding()
}
}