因爲 API 變更,此文章部份內容已失效,最新完整中文教程及代碼請查看 github.com/WillieWangW…git
SwiftUI
表明將來構建 App 的方向,歡迎加羣一塊兒交流技術,解決問題。github
在
Landmarks
app 中,用戶能夠建立我的資料來展現個性。爲了讓用戶能修改我的資料,咱們須要添加一個編輯模式,並設計一個偏好設置界面。swift咱們將使用多種經常使用的 UI 控件來處理數據,並在用戶保存修改時更新
Landmarks
model。bash
- 預計完成時間:25 分鐘
- 項目文件:下載
Landmarks
app 在本地保存一些詳細配置和偏好設置。在用戶編輯他們的詳情前,會在一個沒有修改控件的摘要 view 中顯示出來。微信
1.1 在 Landmark
文件夾裏建立一個新文件夾 Profile
,而後在裏面建立一個新文件 ProfileHost.swift
。閉包
ProfileHost
view 負責用戶信息的靜態摘要 view 以及編輯模式。app
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
。ide
如今 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×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
複製代碼
用戶須要在我的詳情中切換瀏覽和編輯模式。咱們會經過在現有的 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.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
是隻有 on
或 off
的控件,因此它很適合像 yes
或 no
之類的 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.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
複製代碼