Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Working with UI Controls

Willie edited this page Jun 9, 2019 · 5 revisions

使用 UI 控件

Landmarks app 中,用户可以创建个人资料来展示个性。为了让用户能修改个人资料,我们需要添加一个编辑模式,并设计一个偏好设置界面。

我们将使用多种常用的 UI 控件来处理数据,并在用户保存修改时更新 Landmarks model。

  • 预计完成时间:25 分钟
  • 项目文件:下载

1. 显示用户资料

Landmarks app 在本地保存一些详细配置和偏好设置。在用户编辑他们的详情前,会在一个没有修改控件的摘要 view 中显示出来。

1.1 在 Landmark 文件夹里创建一个新文件夹 Profile ,然后在里面创建一个新文件 ProfileHost.swift

ProfileHost view 负责用户信息的静态摘要 view 以及编辑模式。

ProfileHost.swift

import SwiftUI
struct ProfileHost: View {
 @State var profile = Profile.default
 var body: some View {
 Text("Profile for: \(profile.username)")
 }
}
#if DEBUG
struct ProfileHost_Previews: PreviewProvider {
 static var previews: some View {
 ProfileHost()
 }
}
#endif

1.2 在 Home.swift 中,把静态的 Text 换成上一步中创建的 ProfileHost

现在 home screen 中的 profile 按钮会显示一个带有用户信息的模态。

Home.swift

import SwiftUI
struct CategoryHome: View {
 var categories: [String: [Landmark]] {
 .init(
 grouping: landmarkData,
 by: { 0ドル.category.rawValue }
 )
 }
 
 var featured: [Landmark] {
 landmarkData.filter { 0ドル.isFeatured }
 }
 
 var body: some View {
 NavigationView {
 List {
 FeaturedLandmarks(landmarks: featured)
 .scaledToFill()
 .frame(height: 200)
 .clipped()
 .listRowInsets(EdgeInsets())
 
 ForEach(categories.keys.sorted().identified(by: \.self)) { key in
 CategoryRow(categoryName: key, items: self.categories[key]!)
 }
 .listRowInsets(EdgeInsets())
 
 NavigationButton(destination: LandmarkList()) {
 Text("See All")
 }
 }
 .navigationBarTitle(Text("Featured"))
 .navigationBarItems(trailing:
 PresentationButton(
 Image(systemName: "person.crop.circle")
 .imageScale(.large)
 .accessibility(label: Text("User Profile"))
 .padding(),
 destination: ProfileHost()
 )
 )
 }
 }
}
struct FeaturedLandmarks: View {
 var landmarks: [Landmark]
 var body: some View {
 landmarks[0].image(forSize: 250).resizable()
 }
}
#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

1.3 创建一个新 view ProfileSummary ,它持有一个 Profile 实例并显示一些基本用户信息。

ProfileSummary 持有一个 Profile 值要比一个 profile 的 binding 更合适,因为它的父 view ProfileHost 负责管理它的 state

ProfileSummary.swift

import SwiftUI
struct ProfileSummary: View {
 var profile: Profile
 
 static let goalFormat: DateFormatter = {
 let formatter = DateFormatter()
 formatter.dateFormat = "MMMM d, yyyy"
 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)")
 }
 }
}
#if DEBUG
struct ProfileSummary_Previews: PreviewProvider {
 static var previews: some View {
 ProfileSummary(profile: Profile.default)
 }
}
#endif

1.4 更新 ProfileHost 来显示摘要 view 。

ProfileHost.swift

import SwiftUI
struct ProfileHost: View {
 @State var profile = Profile.default
 var body: some View {
 VStack(alignment: .leading, spacing: 20) {
 ProfileSummary(profile: self.profile)
 }
 .padding()
 }
}
#if DEBUG
struct ProfileHost_Previews: PreviewProvider {
 static var previews: some View {
 ProfileHost()
 }
}
#endif

1.5 创建一个新 view HikeBadge ,这个 view 组合了 绘制路径和形状 中的徽章以及一些远足的描述文本。

徽章只是一个图形,因此 HikeBadge 中的文本以及 accessibility(label:) 方法让徽章对其他用户来说含义更加清晰。

两个调用 frame(width:height:) 的方法让徽章以 ×ばつ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)."))
 }
 }
}
#if DEBUG
struct HikeBadge_Previews : PreviewProvider {
 static var previews: some View {
 HikeBadge(name: "Preview Testing")
 }
}
#endif

1.6 更新 ProfileSummary ,给它添加几个具有不同色调的徽章以及获得徽章的原因。

ProfileSummary.swift

import SwiftUI
struct ProfileSummary: View {
 var profile: Profile
 
 static let goalFormat: DateFormatter = {
 let formatter = DateFormatter()
 formatter.dateFormat = "MMMM d, yyyy"
 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)
 }
 }
 }
}
#if DEBUG
struct ProfileSummary_Previews: PreviewProvider {
 static var previews: some View {
 ProfileSummary(profile: Profile.default)
 }
}
#endif

1.7 引入 动画视图与转场 中的 HikeView 来完成 ProfileSummary

ProfileSummary.swift

import SwiftUI
struct ProfileSummary: View {
 var profile: Profile
 
 static var goalFormat: DateFormatter {
 let formatter = DateFormatter()
 formatter.dateFormat = "MMMM d, yyyy"
 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])
 }
 }
 }
}
#if DEBUG
struct ProfileSummary_Previews: PreviewProvider {
 static var previews: some View {
 ProfileSummary(profile: Profile.default)
 }
}
#endif

2. 加入编辑模式

用户需要在个人详情中切换浏览和编辑模式。我们会通过在现有的 ProfileHost 中添加一个 EditButton 来实现编辑模式,并且创建一个带有编辑单个数据控件的 view 。

2.1 创建一个 Environment view 属性,并输入 \.editMode

我们可以使用此属性来读取和写入当前编辑范围。

ProfileHost.swift

import SwiftUI
struct ProfileHost: View {
 @Environment(\.editMode) var mode
 @State var profile = Profile.default
 var body: some View {
 VStack(alignment: .leading, spacing: 20) {
 ProfileSummary(profile: profile)
 }
 .padding()
 }
}
#if DEBUG
struct ProfileHost_Previews: PreviewProvider {
 static var previews: some View {
 ProfileHost()
 }
}
#endif

2.2 创建一个可以切换环境中编辑模式开关的 Edit 按钮。

ProfileHost.swift

import SwiftUI
struct ProfileHost: View {
 @Environment(\.editMode) var mode
 @State var profile = Profile.default
 var body: some View {
 VStack(alignment: .leading, spacing: 20) {
 HStack {
 Spacer()
 
 EditButton()
 }
 ProfileSummary(profile: profile)
 }
 .padding()
 }
}
#if DEBUG
struct ProfileHost_Previews: PreviewProvider {
 static var previews: some View {
 ProfileHost()
 }
}
#endif

2.3 添加一个用户信息的草稿副本来传递给编辑控件。

为了避免在任何编辑确认之前更新 app 的全局状态,例如在用户输入其名称时,编辑 view 只会对其自身的副本进行操作。

ProfileHost.swift

import SwiftUI
struct ProfileHost: View {
 @Environment(\.editMode) var mode
 @State var profile = Profile.default
 @State var draftProfile = Profile.default
 
 var body: some View {
 VStack(alignment: .leading, spacing: 20) {
 HStack {
 Spacer()
 
 EditButton()
 }
 ProfileSummary(profile: self.profile)
 }
 .padding()
 }
}
#if DEBUG
struct ProfileHost_Previews: PreviewProvider {
 static var previews: some View {
 ProfileHost()
 }
}
#endif

2.4 添加条件 view,显示静态信息或编辑模式的 view。

目前,编辑模式只是一个静态文本。

ProfileHost.swift

import SwiftUI
struct ProfileHost: View {
 @Environment(\.editMode) var mode
 @State var profile = Profile.default
 @State var draftProfile = Profile.default
 
 var body: some View {
 VStack(alignment: .leading, spacing: 20) {
 HStack {
 Spacer()
 
 EditButton()
 }
 if self.mode?.value == .inactive {
 ProfileSummary(profile: profile)
 } else {
 Text("Profile Editor")
 }
 }
 .padding()
 }
}
#if DEBUG
struct ProfileHost_Previews: PreviewProvider {
 static var previews: some View {
 ProfileHost()
 }
}
#endif

3. 定义信息编辑器

用户信息编辑器主要包含了更改详情时的不同控件。信息中徽章之类某些项目是用户编辑不了的,因此它们不会出现在编辑器中。

为了与信息摘要保持一致,我们会在编辑器中以相同的顺序添加信息详情。

3.1 创建一个新 view ProfileEditor ,然后给用户信息的草稿副本引入一个 binding

view 中第一个控件是一个 TextField ,它控制并更新一个字符串的 binding,在这里例子中则是用户选择的显示名称。

ProfileEditor.swift

import SwiftUI
struct ProfileEditor: View {
 @Binding var profile: Profile
 
 var body: some View {
 List {
 HStack {
 Text("Username").bold()
 Divider()
 TextField($profile.username)
 }
 }
 }
}
#if DEBUG
struct ProfileEditor_Previews: PreviewProvider {
 static var previews: some View {
 ProfileEditor(profile: .constant(.default))
 }
}
#endif

3.2 更新 ProfileHost 中的条件内容,引入 ProfileEditor 并给它传递一个信息的 binding

现在当你点击 Edit 后,信息编辑 view 就会显示。

ProfileHost.swift

import SwiftUI
struct ProfileHost: View {
 @Environment(\.editMode) var mode
 @State var profile = Profile.default
 @State var draftProfile = Profile.default
 
 var body: some View {
 VStack(alignment: .leading, spacing: 20) {
 HStack {
 Spacer()
 
 EditButton()
 }
 if self.mode?.value == .inactive {
 ProfileSummary(profile: profile)
 } else {
 ProfileEditor(profile: $draftProfile)
 }
 }
 .padding()
 }
}
#if DEBUG
struct ProfileHost_Previews: PreviewProvider {
 static var previews: some View {
 ProfileHost()
 }
}
#endif

3.3 添加接收地标相关事件通知的 toggle ,它与用户偏好相对应。

Toggle 是只有 onoff 的控件,所以它很适合像 yesno 之类的 Boolean 值。

ProfileEditor.swift

import SwiftUI
struct ProfileEditor: View {
 @Binding var profile: Profile
 
 var body: some View {
 List {
 HStack {
 Text("Username").bold()
 Divider()
 TextField($profile.username)
 }
 
 Toggle(isOn: $profile.prefersNotifications) {
 Text("Enable Notifications")
 }
 }
 }
}
#if DEBUG
struct ProfileEditor_Previews: PreviewProvider {
 static var previews: some View {
 ProfileEditor(profile: .constant(.default))
 }
}
#endif

3.4 将一个 SegmentedControl 和它的 label 放在一个 VStack 中,使地标照片具有可选择的季节。

ProfileEditor.swift

import SwiftUI
struct ProfileEditor: View {
 @Binding var profile: Profile
 
 var body: some View {
 List {
 HStack {
 Text("Username").bold()
 Divider()
 TextField($profile.username)
 }
 
 Toggle(isOn: $profile.prefersNotifications) {
 Text("Enable Notifications")
 }
 
 VStack(alignment: .leading, spacing: 20) {
 Text("Seasonal Photo").bold()
 
 SegmentedControl(selection: $profile.seasonalPhoto) {
 ForEach(Profile.Season.allCases.identified(by: \.self)) { season in
 Text(season.rawValue).tag(season)
 }
 }
 }
 .padding(.top)
 }
 }
}
#if DEBUG
struct ProfileEditor_Previews: PreviewProvider {
 static var previews: some View {
 ProfileEditor(profile: .constant(.default))
 }
}
#endif

3.5 最后,在季节选择器的下面添加一个 DatePicker ,用来修改到达地标的日期。

ProfileEditor.swift

import SwiftUI
struct ProfileEditor: View {
 @Binding var profile: Profile
 
 var body: some View {
 List {
 HStack {
 Text("Username").bold()
 Divider()
 TextField($profile.username)
 }
 
 Toggle(isOn: $profile.prefersNotifications) {
 Text("Enable Notifications")
 }
 
 VStack(alignment: .leading, spacing: 20) {
 Text("Seasonal Photo").bold()
 
 SegmentedControl(selection: $profile.seasonalPhoto) {
 ForEach(Profile.Season.allCases.identified(by: \.self)) { season in
 Text(season.rawValue).tag(season)
 }
 }
 }
 .padding(.top)
 
 VStack(alignment: .leading, spacing: 20) {
 Text("Goal Date").bold()
 DatePicker(
 $profile.goalDate,
 minimumDate: Calendar.current.date(byAdding: .year, value: -1, to: profile.goalDate),
 maximumDate: Calendar.current.date(byAdding: .year, value: 1, to: profile.goalDate),
 displayedComponents: .date
 )
 }
 .padding(.top)
 }
 }
}

4. 延迟编辑的传递

要使编辑在用户退出编辑模式之后才生效,我们需要在编辑期间使用信息的草稿副本,然后仅在用户确认编辑时将草稿副本分配给真实副本。

4.1 给 ProfileHost 添加一个确认按钮。

EditButton 提供的 Cancel 按钮不同, Done 按钮会在其操作闭包中将编辑应用于实际的数据。

ProfileHost.swift

import SwiftUI
struct ProfileHost: View {
 @Environment(\.editMode) var mode
 @State var profile = Profile.default
 @State var draftProfile = Profile.default
 
 var body: some View {
 VStack(alignment: .leading, spacing: 20) {
 HStack {
 if self.mode?.value == .active {
 Button(action: {
 self.profile = self.draftProfile
 self.mode?.animation().value = .inactive
 }) {
 Text("Done")
 }
 }
 
 Spacer()
 
 EditButton()
 }
 if self.mode?.value == .inactive {
 ProfileSummary(profile: profile)
 } else {
 ProfileEditor(profile: $draftProfile)
 }
 }
 .padding()
 }
}
#if DEBUG
struct ProfileHost_Previews: PreviewProvider {
 static var previews: some View {
 ProfileHost()
 }
}
#endif

4.2 使用 onDisappear(perform:) 方法来清空用户点击 Cancel 按钮时选择丢弃的值。

否则,下次编辑模式激活时会显示旧值。

ProfileHost.swift

import SwiftUI
struct ProfileHost: View {
 @Environment(\.editMode) var mode
 @State var profile = Profile.default
 @State var draftProfile = Profile.default
 
 var body: some View {
 VStack(alignment: .leading, spacing: 20) {
 HStack {
 if self.mode?.value == .active {
 Button(action: {
 self.profile = self.draftProfile
 self.mode?.animation().value = .inactive
 }) {
 Text("Done")
 }
 }
 
 Spacer()
 
 EditButton()
 }
 if self.mode?.value == .inactive {
 ProfileSummary(profile: profile)
 } else {
 ProfileEditor(profile: $draftProfile)
 .onDisappear {
 self.draftProfile = self.profile
 }
 }
 }
 .padding()
 }
}
#if DEBUG
struct ProfileHost_Previews: PreviewProvider {
 static var previews: some View {
 ProfileHost()
 }
}
#endif

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /