SwiftUI 与 Apple 之前的 UI 结构的区别不只在于怎么界说视图和其他 UI 组件,还在于怎么在运用它的应用程序中办理视图级状况。

SwiftUI 不运用委托、数据源或 UIKit 和 AppKit 等命令式结构中常见的任何其他状况办理模式,而是附带了一些[特点包装器],使咱们能够准确地声明怎么调查、出现和处理数据。因咱们的观念而改动。

本周,让咱们细心看看每个特点包装器,它们怎么相互相关,以及它们怎么构成 SwiftUI 全体状况办理系统的不同部分。

[状况特点]

由于 SwiftUI 首要是一个UI 结构(尽管它也开端获得用于界说更高等级构造的 API,例如[应用程序和场景]),因而它的声明式规划不一定需求影响应用程序的整个模型和数据层 – 但是而只是与咱们的各种观念直接相关的状况。

例如,假定咱们正在开发一个允许用户经过输入 a和一个地址SignupView在应用程序中注册新帐户的应用程序。然后,咱们将运用这两个值来构成一个模型,该模型被传递给一个闭包——为咱们供给三个状况:username``email``User``handler

struct SignupView: View {
var handler: (User) -> Void 
var username = "" 
var email = ""
var body: some View { ... } }

由于这三个特点中只有两个 –usernameemail– 实践上将由咱们的视图进行修正,而且由于这两个状况能够坚持私有,因而咱们将运用 SwiftUI 的State特点包装器来符号它们 – 像这样:

struct SignupView: View {
    var handler: (User) -> Void
    @State private var username = ""
    @State private var email = ""
    var body: some View {
        ...
    }
}

这样做会主动在这两个值和咱们的视图自身之间创立衔接 – 这意味着每次这两个值中的任何一个发生更改时,咱们的视图都会从头烘托。在咱们的 中body,咱们将这两个特点中的每一个绑定TextField到相应的特点,以使它们可供用户修正 – 为咱们供给以下完结:

struct SignupView: View {
    var handler: (User) -> Void
    @State private var username = ""
    @State private var email = ""
    var body: some View {
        VStack {
            TextField("Username", text: $username)
            TextField("Email", text: $email)
            Button(
                action: {
                    self.handler(User(
                        username: self.username,
                        email: self.email
                    ))
                },
                label: { Text("Sign up") }
            )
        }
        .padding()
    }
}

SoState用于表示 SwiftUI 视图的内部状况,并在该状况更改时主动更新视图。State因而,保留-wrapped 特点一般是一个好主意private,这保证它们只会在该视图的主体内发生变化(尝试在其他当地改动它们实践上会导致运行时崩溃)。

[双向绑定]

检查上面的代码示例,咱们将每个特点传递给它们的办法TextField是在这些特点称号前面加上$.这是由于咱们不只仅是将普通String值传递到这些文本字段中,而是绑定到咱们的State-wrapped 特点自身。

为了更详细地讨论这意味着什么,现在假定咱们想要创立一个视图,让咱们的用户修正他们开始在注册时输入的个人资料信息。由于咱们现在期望修正外部状况值,而不只仅是私有状况值,因而这次咱们将符号咱们的usernameemail特点:Binding

struct ProfileEditingView: View {
    @Binding var username: String
    @Binding var email: String
    var body: some View {
        VStack {
            TextField("Username", text: $username)
            TextField("Email", text: $email)
        }
        .padding()
    }
}

很酷的是,绑定不只限于单个内置值,例如字符串或整数,还能够用于将任何 Swift 值绑定到咱们的视图之一。例如,以下是咱们怎么将User模型自身传递给ProfileEditingView,而不是传递两个单独的usernameemail值:

struct ProfileEditingView: View {
    @Binding var user: User
    var body: some View {
        VStack {
            TextField("Username", text: $user.username)
            TextField("Email", text: $user.email)
        }
        .padding()
    }
}

就像咱们在将StateBinding-wrapped 特点$传递到各种实例时怎么为它们增加前缀相同,在将任何值衔接到咱们自己界说的特点TextField时,咱们也能够做完全相同的作业。State``Binding

例如,下面是一个运用-wrapped 特点ProfileView盯梢模型的完结,然后在将上述实例出现为作业表时将绑定传递给该模型- 这将主动同步用户所做的任何更改该原始特点的值:User``State``ProfileEditingView``State

struct ProfileView: View {
    @State private var user = User.load()
    @State private var isEditingViewShown = false
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Username: ")
                .foregroundColor(.secondary)
                + Text(user.username)
            Text("Email: ")
                .foregroundColor(.secondary)
                + Text(user.email)
            Button(
                action: { self.isEditingViewShown = true },
                label: { Text("Edit") }
            )
        }
        .padding()
        .sheet(isPresented: $isEditingViewShown) {
            VStack {
                ProfileEditingView(user: self.$user)
                Button(
                    action: { self.isEditingViewShown = false },
                    label: { Text("Done") }
                )
            }
        }
    }
}

请注意,咱们还能够State经过为包装特点分配一个新值来改动它,就像咱们在“完结”按钮的操作处理程序中isEditingViewShown设置的那样。false

因而, –Binding符号的特点在给定视图和在该视图外部界说的状况特点之间供给了双向衔接,而且 –State包装Binding的特点能够经过在特点称号前加上 . 前缀作为绑定传递$

[调查物体]

两者StateBinding共同点是它们处理在 SwiftUI 视图层次结构自身内办理的值。但是,尽管当然能够构建一个将其一切状况保留在其各种视图中的应用程序,但就架构和关注点别离而言,这一般不是一个好主意,而且很简单导致咱们的视图变得适当庞大和杂乱。

值得幸亏的是,SwiftUI 还供给了许多机制,使咱们能够将外部模型目标衔接到咱们的各种视图。其中一种机制是ObservableObject协议,当与ObservedObject特点包装器结合运用时,咱们能够设置对在视图层外部办理的引证类型的绑定。

作为一个比如,让咱们更新ProfileView上面界说的——经过将办理模型的职责User从视图自身转移到一个新的专用目标中。现在,咱们能够运用许多不同的隐喻来描绘这样的目标,但由于咱们期望创立一种类型来操控咱们模型之一的实例 – 让咱们将其设为符合以下条件的模型操控器 SwiftUI 的ObservableObject协议:

class UserModelController: ObservableObject {
    @Published var user: User
    ...
}

特点Published包装器用于界说目标的哪些特点在修正时应触发调查告诉。

有了上面的类型,现在让咱们回到咱们的ProfileView并让它调查咱们的 new 实例UserModelController作为 anObservedObject,而不是运用State-wrapped 特点来盯梢咱们的User模型。真实奇妙的是,咱们依然能够轻松地将模型绑定到咱们的ProfileEditingView,就像曾经相同,由于ObservedObject-wrapped 特点也能够转换为绑定 – 像这样:

struct ProfileView: View {
    @ObservedObject var userController: UserModelController
    @State private var isEditingViewShown = false
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Username: ")
                .foregroundColor(.secondary)
                + Text(userController.user.username)
            Text("Email: ")
                .foregroundColor(.secondary)
                + Text(userController.user.email)
            Button(
                action: { self.isEditingViewShown = true },
                label: { Text("Edit") }
            )
        }
        .padding()
        .sheet(isPresented: $isEditingViewShown) {
            VStack {
                ProfileEditingView(user: self.$userController.user)
                Button(
                    action: { self.isEditingViewShown = false },
                    label: { Text("Done") }
                )
            }
        }
    }
}

State但是,咱们的新完结与之前运用的根据 – 的完结之间的一个重要区别是,咱们的UserModelControllernow 需求作为其初始化程序的一部分注入到 our 中。ProfileView

这样做的原因,除了它“迫使”咱们在代码库中树立一个更明确界说的依靠关系图之外,还在于符号为 的特点并不意味着对该特点所指向的目标具有任何形式的ObservedObject一切权。

因而,尽管像下面这样的东西或许在技术上能够编译,但它终究或许会导致运行时问题 – 由于当咱们的视图UserModelController在更新期间从头创立时,存储在咱们的视图中的实例终究或许会被开释(由于咱们的视图现在是它的首要一切者) ):

struct ProfileView: View {
    @ObservedObject var userController = UserModelController.load()
    ...
}

重要的是要记住,SwiftUI 视图不是对屏幕上出现的实践 UI 组件的引证,而是描绘咱们的 UI 的轻量级值 – 因而它们不具有与实例等相同类型的生命周期UIView

为了处理上述问题,Apple 在 iOS 14 和 macOS Big Sur 中引入了一个新的特点包装器,称为StateObject.符号为 的特点的StateObject行为办法与 完全相同ObservedObject– 此外,SwiftUI 将保证存储在此类特点中的任何目标不会意外开释,由于结构在从头烘托视图时从头创立视图的新实例:

struct ProfileView: View {
    @StateObject var userController = UserModelController.load()
    ...
}

尽管从技术上讲,从现在开端能够运用- 我依然主张在调查外部目标时运用,而且仅在处理视图自身具有的目标时运用。将and视为适当于and的引证类型,或许强特点和弱特点的 SwiftUI 版别。StateObject``ObservedObject``StateObject``StateObject``ObservedObject``State``Binding

[调查和改动环境]

最终,让咱们看一下怎么运用 SwiftUI 的环境系统在两个不直接衔接的视图之间传递各种状况。尽管在父视图与其子视图之一之间创立绑定一般很简单,但在整个视图层次结构中传递某个目标或值或许适当麻烦——而这正是环境旨在处理的问题类型。

运用 SwiftUI 环境的首要办法有两种。一种是首先在想要检索给EnvironmentObject定目标的视图中界说一个-wrapped 特点- 例如,怎么检索包含色彩信息的目标:**ArticleView``Theme

struct ArticleView: View {
    @EnvironmentObject var theme: Theme
    var article: Article
    var body: some View {
        VStack(alignment: .leading) {
            Text(article.title)
                .foregroundColor(theme.titleTextColor)
            Text(article.body)
                .foregroundColor(theme.bodyTextColor)
        }
    }
}

然后,咱们有必要保证Theme在视图的父级之一中供给环境目标(本例中是一个实例),SwiftUI 将处理其余的作业。这是运用environmentObject修饰符完结的,例如如下所示:

struct RootView: View {
    @ObservedObject var theme: Theme
    @ObservedObject var articleLibrary: ArticleLibrary
    var body: some View {
        ArticleListView(articles: articleLibrary.articles)
            .environmentObject(theme)
    }
}

请注意,咱们不需求将上述修饰符应用于将运用环境目标的切当视图 – 咱们能够将其应用于层次结构中坐落其上方的任何视图。

运用 SwiftUI 环境系统的第二种办法是界说一个自界说EnvironmentKey– 然后能够运用它向内置类型分配值或从内置EnvironmentValues类型检索值:

struct ThemeEnvironmentKey: EnvironmentKey {
    static var defaultValue = Theme.default
}
extension EnvironmentValues {
    var theme: Theme {
        get { self[ThemeEnvironmentKey.self] }
        set { self[ThemeEnvironmentKey.self] = newValue }
    }
}

完结上述操作后,咱们现在能够运用特点包装器(而不是)来符号视图的theme特点,并传入咱们期望检索其值的环境键的键途径:Environment``EnvironmentObject

struct ArticleView: View {
    @Environment(.theme) var theme: Theme
    var article: Article
    var body: some View {
        VStack(alignment: .leading) {
            Text(article.title)
                .foregroundColor(theme.titleTextColor)
            Text(article.body)
                .foregroundColor(theme.bodyTextColor)
        }
    }
}

上述两种办法之间的一个明显区别是,根据键的办法要求咱们在编译时界说一个默认值,而根据EnvironmentObject– 的办法假定将在运行时供给这样的值(如果不这样做将导致碰撞)。

[定论]

SwiftUI 办理状况的办法绝对是该结构最风趣的方面之一,而且或许需求咱们稍微从头考虑数据在应用程序内传递的办法 – 至少在触及将直接运用和变异的数据时经过咱们的用户界面。