02 Working with UI Controls - ly918/SwiftUI-Chinese-Documents GitHub Wiki
在地标应用程序中,用户可以创建一个个人资料页来表达他们的个性。为了让用户能够更改他们的个人简介,您将添加一个编辑模式,并设计一个偏好设置页面。
您将使用各种用于数据输入的通用用户界面控件,并在用户保存更改时更新地标数据模型。
学习时间:25分钟
Landmarks
应用程序在本地存储一些详情配置和偏好设置。在用户编辑其详情之前,它们将显示在没有任何编辑控件的摘要视图中。
要开始,请在Landmarks
目录下创建一个名为Profile
的新目录,然后将名为ProfileHost
的视图添加到该目录中。
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()
}
}
将Home.swift
中的静态文本替换为上一步中创建的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()
}
}
创建一个名为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)
}
}
更新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()
}
}
创建一个名为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")
}
}
更新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)
}
}
通过引入视图动画与转场的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
,然后创建一个带有控件的视图,用于编辑单个数据。
添加一个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()
}
}
创建一个编辑按钮,用于打开和关闭环境的编辑模式。
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()
}
}
更新UserData
类以包含用户简介的实例,该实例在用户关闭简介视图后仍然存在。
UserData.swift
import Combine
import SwiftUI
final class UserData: ObservableObject {
@Published var showFavoritesOnly = false
@Published var landmarks = landmarkData
@Published var profile = Profile.default
}
从Environment
中读取Profile
数据,将数据的控制权传递给ProfileHost
。
为了避免在确认编辑之前更新全局应用程序状态(例如当用户输入其名称时),编辑视图将对其自身的副本进行操作。
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()
}
}
添加条件视图,显示静态简介视图或编辑模式视图。
注意
目前,编辑模式只是一个静态文本字段。
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()
}
}
用户简介编辑器主要由不同的控件组成,这些控件更改用户简介中的各个详情信息。配置文件中的某些项目(如徽章)不可由用户编辑,因此它们不会显示在编辑器中。
为了与信息摘要保持一致,您将在编辑器中按相同的顺序添加概要文件详情信息。
创建一个名为ProfileEditor
的新视图,并包含对用户简介副本的绑定。
视图中的第一个控件是一个TextField
,它控制并更新一个字符串的绑定,是用户选择的显示名称。当创建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))
}
}
更新ProfileHost
中的条件内容,使其包含Profile Editor
,并传递简介信息的绑定。
现在,单击“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()
}
}
添加是否接收地标相关事件通知的开关。
Toggles
是只有打开或关闭的控件,因此它们非常适合布尔值Boolean
,如“yes”或“no”的设置。
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))
}
}
将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))
}
}
最后,在季节选择器下面添加一个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))
}
}
要使其编辑,直到用户退出编辑模式后才生效,在编辑过程中使用其Profile
的草稿副本,然后仅当用户确认编辑时才将草稿副本分配给真实副本。
向ProfileHost
添加取消按钮。
与EditButton
提供的Done
按钮不同,Cancel
按钮不会将编辑应用于其闭包中的真实Profile
数据。
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()
}
}
应用onAppear(perform:)
和onDisappear(perform:)
修饰符,将正确的用户简介数据填充给编辑器,并在用户点击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)
.onAppear {
self.draftProfile = self.userData.profile
}
.onDisappear {
self.userData.profile = self.draftProfile
}
}
}
.padding()
}
}
struct ProfileHost_Previews: PreviewProvider {
static var previews: some View {
ProfileHost()
}
}