Skip to content

3.2 SwiftUI Working with UI Controls 使用UI控件

在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")
    }
}

  1. 完善 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())
    }
}

|300

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())
    }
}

|300

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()
    }
}