使用UI控件
在Landmarks应用程序中,用户可以创建个人资料来表达自己的个性。为了让用户能够更改他们的个人资料,您将添加一个编辑模式并设计偏好屏幕。
您将使用各种常见的用户界面控件进行数据输入,并在用户保存更改时更新Landmarks模型类型。
按照步骤构建这个项目,或者下载完成的项目来自己探索。
第一节 显示用户配置页面
Landmarks应用程序在本地存储一些配置详细信息和偏好。在用户编辑其详细信息之前,它们将显示在没有任何编辑控件的摘要视图中。
第一步 新建Profile.swift
在Models分组中新建一个配置模型Profile.swift
import Foundation
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}
}
}
第二步 新建ProfileHost.swift
在Views
分组下,新增一个Profiles
分组。在Profiles
分组下,新增一个视图ProfileHost.swift
,该视图展示了存储在profile模型中username。
struct ProfileHost: View {
@State private var draftProfile = Profile.default
var body: some View {
Text("Profile for: \(draftProfile.username)")
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost()
}
}
ProfileHost视图将同时承载配置文件信息的静态摘要视图和编辑模式。
第三步 新建ProfileSummary.swift
在Profiles
分组下,新建一个视图ProfileSummary.swift
,该视图使用Profile
实例展示一些用户的基本信息。
import SwiftUI
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 Photo: \(profile.seasonalPhoto.rawValue)")
Text("Goal Date: ") + Text(profile.goalDate, style: .date)
}
}
}
}
struct ProfileSummary_Previews: PreviewProvider {
static var previews: some View {
ProfileSummary(profile: Profile.default)
}
}
配置文件摘要采用profile值,而不是绑定到配置文件,因为父视图ProfileHost管理此视图的状态。
第四步 更新ProfileHost.swift
import SwiftUI
struct ProfileHost: View {
@State private var draftProfile = Profile.default
var body: some View {
VStack {
ProfileSummary(profile: draftProfile)
}
.padding()
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost()
}
}
第五步 创建HikeBadge.swift
在Hikes
分组下,新建视图HikeBadge.swift
,该视图根据“绘制路径和形状”以及有关远足的一些描述性文本组成徽章。
徽章只是一个图形,因此HikeBadge中的文本以及accessibilityLabel(_:)修饰符可以让其他用户更清楚地理解徽章的含义。
笔记:
徽章的绘制逻辑会产生一个结果,该结果取决于其渲染的帧的大小。若要确保所需的外观,请在300 x 300点的frame中进行渲染。要获得最终图形所需的大小,请缩放渲染结果,并将其放置在相对较小的frame中。
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)
.accessibilityLabel("Badge for \(name).")
}
}
}
struct HikeBadge_Previews: PreviewProvider {
static var previews: some View {
HikeBadge(name: "Preview Testing")
}
}
第六步 更新ProfileSummary.swift
在ProfileSummary.swift
中添加几个不同色调的徽章,以及获得徽章的原因
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)
}
}
第七步
通过包含“设置视图和过渡动画”中的HikeView来完成配置文件摘要。
若要使用Hike数据,还需要添加模型数据环境对象。
Divider()
VStack(alignment: .leading) {
Text("Recent Hikes")
.font(.headline)
HikeView(hike: modelData.hikes[0])
}
第八步 串联CategoryHome.swift
在CategoryHome.swift中,使用工具栏修改器将用户配置文件按钮添加到导航栏,并在用户点击它时显示ProfileHost视图。
.toolbar {
Button {
showProfile.toggle()
} label: {
Label("User Profile", systemImage: "person.crop.circle")
}
}
.sheet(isPresented: $showProfile) {
ProfileHost().environmentObject(modelData)
}
第九步 更改listStyle
添加listStyle修饰符以选择更适合内容的列表样式。
import SwiftUI
struct CategoryHome: View {
@EnvironmentObject var modelData: ModelData
@State private var showProfile = false
var body: some View {
NavigationView {
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(Text("Featured"))
.toolbar {
Button {
showProfile.toggle()
} label: {
Label("User Profile", systemImage: "person.crop.circle")
}
}
.sheet(isPresented: $showProfile) {
ProfileHost().environmentObject(modelData)
}
}
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome().environmentObject(ModelData())
}
}
第十步 切换到实时预览,然后尝试点击配置文件按钮以查看配置文件摘要。
第二节 添加编辑模式
在个人信息配置页面,用户既可以查看也可以编辑,因此您要提供一个编辑按钮,当用户点击的时候,创建一个视图供用户修改某些信息
第一步 修改ProfileHost.swift
在预览视图中添加环境对象ModelData
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost()
.environmentObject(ModelData())
}
}
尽管此视图没有使用带有@EnvironmentObject属性的属性,但此视图的子级ProfileSummary使用了。因此,如果没有修改器,预览将失败。
第二步 引用视图环境的编辑模式
添加一个环境视图属性,该属性可以打开或者关闭编辑模式 在环境中,swiftUI提供了值的存储,您可以通过@Environment包裹属性来访问这些值。访问editMode值以读取或写入编辑范围
@Environment(\.editMode) var editMode
第三步 添加编辑按钮
添加一个按钮来决定是否开启编辑,EditButton
同样控制着环境中editMode的值
HStack {
Spacer()
EditButton()
}
第四步 更新ModelData.swift
ModelData类添加用户配置文件的实例,该实例即使在用户解除配置文件视图后也会持续存在。
@Published var profile = Profile.default
第五步 ProfileHost控制环境中的用户配置数据
从环境中读取用户的配置文件数据,以将数据控制权传递给ProfileHost
。
为了避免在确认任何编辑之前更新全局应用程序状态,例如在用户输入姓名时,编辑视图会对其自身的副本进行操作。
@EnvironmentObject var modelData: ModelData
第六步 编辑和非编辑模式下内容展示
添加一个条件判断,在编辑模式下展示一段文案“Profile Editor”。非编辑模式下正常展示用户的配置数据
import SwiftUI
struct ProfileHost: View {
@Environment(\.editMode) var editMode
@EnvironmentObject var modelData: ModelData
@State private var draftProfile = Profile.default
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
}else {
Text("Profile Editor")
}
}
.padding()
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost()
.environmentObject(ModelData())
}
}
在预览模式下, 点击编辑按钮,可以看到在编辑模式下展示了一段静态文本
第三节 定义配置文件编辑器
用户配置文件编辑器主要由不同的控件组成,这些控件可更改配置文件中的各个细节。配置文件中的某些项目,如徽章,是不可由用户编辑的,因此它们不会出现在编辑器中。
为了与概要文件摘要保持一致,您将在编辑器中以相同的顺序添加概要文件详细信息。
第一步 创建ProfileEditor.swift
创建一个名为ProfileEditor的新视图,并包含到用户配置文件草稿副本的绑定。 视图中的第一个控件是TextField,它控制并更新字符串绑定——在本例中是用户选择的显示名称。创建文本字段时,可以为字符串提供标签和绑定。
import SwiftUI
struct ProfileEditor: View {
@Binding var profile: Profile
var body: some View {
List {
HStack {
Text("Username").bold()
Divider()
TextField("username", text: $profile.username)
}
}
.listStyle(.inset)
}
}
struct ProfileEditor_Previews: PreviewProvider {
static var previews: some View {
ProfileEditor(profile: .constant(.default))
}
}
第二步 更新ProfileHost
if editMode?.wrappedValue == .inactive {
ProfileSummary(profile: modelData.profile)
} else {
ProfileEditor(profile: $draftProfile)
}
第三步 通知开关
添加一个开关,该开关与用户配置文件中的prefersNotifications相对应。
Toggle(isOn: $profile.prefersNotifications) {
Text("Enable Notifications").bold()
}
第四步 季节图片选择器
将Picker控件及其标签放置在VStack中,使地标照片具有可选择的季节。
VStack(alignment: .leading, spacing: 20) {
Text("Seasonal Photo").bold()
Picker("Seasonl Photo", selection: $profile.seasonalPhoto) {
ForEach(Profile.Season.allCases) { season in
Text(season.rawValue).tag(season)
}
}
.pickerStyle(.segmented)
}
第五步 添加一个时间选择器
最后,在季节选择器下面添加一个DatePicker,使里程碑式的访问目标日期可以修改。
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").bold()
}
VStack(alignment: .leading, spacing: 20) {
Text("Seasonal Photo").bold()
Picker("Seasonl Photo", selection: $profile.seasonalPhoto) {
ForEach(Profile.Season.allCases) { season in
Text(season.rawValue).tag(season)
}
}
.pickerStyle(.segmented)
}
DatePicker(selection: $profile.goalDate, in: dateRange, displayedComponents: .date) {
Text("Goal Date").bold()
}
}
.listStyle(.inset)
}
}
struct ProfileEditor_Previews: PreviewProvider {
static var previews: some View {
ProfileEditor(profile: .constant(.default))
}
}
第四节 延迟编辑传播
为了使编辑在用户退出编辑模式后才会生效,您可以在编辑过程中使用其配置文件的草稿副本,然后只有在用户确认编辑时才将草稿副本分配给真实副本。
第一步 向ProfileHost添加一个取消按钮。
与EditButton提供的“完成”按钮不同,“取消”按钮不会将编辑应用于其闭包中的真实配置文件数据。
if editMode?.wrappedValue == .active {
Button("Cancel", role: .cancel) {
draftProfile = modelData.profile
editMode?.animation().wrappedValue = .inactive
}
}
第二步 编辑完成更新数据
当用户点击编辑按钮,编辑器显示时,将modelData中的配置数据的副本传递过去。当用户点击完成时,将副本赋值给modelData的profile。否则,在下次激活编辑模式时依旧显示的是旧值。
ProfileEditor(profile: $draftProfile)
.onAppear {
draftProfile = modelData.profile
}
.onDisappear {
modelData.profile = draftProfile
}
ProfileHost.swift
完整代码
import SwiftUI
struct ProfileHost: View {
@Environment(\.editMode) var editMode
@EnvironmentObject var modelData: ModelData
@State private var draftProfile = Profile.default
var body: some View {
VStack(alignment: .leading, spacing: 10) {
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 {
draftProfile = modelData.profile
}
.onDisappear {
modelData.profile = draftProfile
}
}
}
.padding()
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost()
.environmentObject(ModelData())
}
}