继续创作,加速成长!这是我参加「日新计划 6 月更文挑战」的第26天,点击检查活动概况

前言

SwiftUI与苹果之前的UI结构的差异不仅仅在于怎么界说视图和其他UI组件,还在于怎么在整个运用它的使用程序中办理视图层级的状况。

SwiftUI没有运用托付、数据源或任何其他在UIKit和AppKit等指令式结构中常见的状况办理模式,而是配备了一些特点包装器,使咱们能够精确地声明咱们的数据怎么被咱们的视图调查、烘托和改动。

本周,让咱们仔细看看这些特点包装器中的每一个,它们之间的联系,以及它们怎么构成SwiftUI整体状况办理系统的不同部分。

特点状况

由于SwiftUI首要是一个UI结构(虽然它也开始获得用于界说更高层次结构(如使用程序和场景)的API),其声明式规划不一定需求影响使用程序的整个模型和数据层——而只是直接绑定到咱们各种视图的状况。

例如,假设咱们正在开发一个SignupView,运用户能够经过输入用户名和电子邮件地址在使用程序中注册一个新账户。咱们将运用这两个值形成一个用户模型,并将其传递给一个闭包:

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 {
        ...
    }
}

这样做将主动在这两个值和咱们的视图自身之间树立一个衔接——这意味着咱们的视图将在每次改动这两个值的时分被从头烘托。在咱们的主体中,咱们将把这两个特点分别绑定到一个相应的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()
    }
}

因而,State被用来表示SwiftUI视图的内部状况,并在该状况被改动时主动使视图更新。因而,最常见的做法是将State特点包装器坚持为私有,这能够保证它们只在该视图的主体内被改动(试图在其他当地改动它们实践上会导致运行时溃散)。

双向绑定

看一下上面的代码样本,咱们将每个特点传入其TextField的办法是在这些特点名称前加上$。这是由于咱们不只是将普通的String值传入这些文本字段,而是与咱们的State包装的特点自身绑定。

为了更详细地探讨这意味着什么,让咱们现在假设咱们想创立一个视图,让咱们的用户编辑他们最初在注册时输入的个人资料信息。由于咱们现在要修改外部状况值,而不仅仅是私家状况值,所以这次咱们将usernameemail特点标记为Bingding:

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值绑定到咱们的一个视图中。例如,咱们能够将用户模型自身传递给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包装的特点传入各种TextField实例时用$作为前缀相同,咱们在将任何State值衔接到咱们自己界说的Binding特点时也能够做相同的作业。

例如,这里有一个ProfileView的实现,它运用一个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包装的特点分配一个新的值来改动它——比方咱们在 “Done “按钮的动作处理程序中把isEditingViewShown设置为false

因而,一个Binding标记的特点在给定的视图和界说在该视图之外的状况特点之间供给了一个双向的衔接,而StatrBinding包装的特点都能够经过在其特点名前加上$来作为绑定物传递。

调查目标

StateBingding的共同点是,它们处理的是在SwiftUI视图层次结构自身中办理的值。但是,虽然树立一个将所有的状况都保存在其各种视图中的使用程序是肯定可行的,但从架构和关注点分离的视点来看,这通常不是一个好主意,并且很简单导致咱们的视图变得相当庞大和杂乱。

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

作为一个比如,让咱们更新上面界说的ProfileView——经过将办理User模型的责任从视图自身转移到一个新的、专门的目标中。现在,咱们能够用许多不同的办法来描述这样一个目标,但由于咱们正在寻找创立一个类型来操控咱们的一个模型的实例——让咱们把它变成一个契合SwiftUI的ObservableObject协议的模型操控器:

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

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

有了上面的类型,现在让咱们回到ProfileView,让它调查新的UserModelController的实例,作为一个ObservedObject,而不是用一个State特点包装器来盯梢咱们的用户模型。最重要的是,咱们依然能够很简单地将这个模型绑定到咱们的ProfileEditingView上,就像以前相同,由于ObservedObject特点包装器也能够转换为绑定:

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

但是,咱们的新实现与之前运用的根据状况的实现之间的一个重要差异是,咱们的UserModelController现在需求作为初始化器的一部分被注入到ProfileView中。

除了 “迫使 “咱们在代码库中树立一个更清晰的依赖联系图之外,原因是一个标有ObservedObject的特点并不意味着对这个特点所指向的目标有任何形式的所有权。

因而,虽然下面的内容在技术上或许会被编译,但最终会导致运行时的问题——由于当咱们的视图在更新时被从头创立,UserModelController实例或许会被删去(由于咱们的视图现在是它的首要所有者):

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

重要的是要记住: SwiftUI视图不是对正在屏幕上烘托的实践UI组件的引证,而是描述咱们的UI的轻量级值——因而它们没有像UIView实例那样的生命周期。

为了处理上述问题,苹果在iOS 14和macOS Big Sur中引入了一个新的特点包装器,名为StateObject。标记为StateObject的特点与ObservedObject的行为完全相同——此外,SwiftUI将保证存储在此类特点中的任何目标不会由于结构在从头烘托视图时从头创立新实例而被意外释放:

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

虽然从技术上来说,从现在开始能够只运用StateObject——我依然建议在调查外部目标时运用ObservedObject,而在处理视图自身具有的目标时只运用StateObject。把StateObjectObservedObject看作是StateBinding的参考类型,或许SwiftUI版本的强和弱特点。

调查和修改环境变量

最终,让咱们来看看SwiftUI的环境系统怎么被用来在两个互不直接衔接的视图之间传递各种状况。虽然在一个父视图和它的一个子视图之间创立绑定通常很简单,但在整个视图层次结构中传递某个目标或值或许相当费事——而这正是环境变量旨在处理的问题类型。

有两种首要的办法来运用SwiftUI的环境。一种是首先在想要检索给定目标的视图中界说一个EnvironmentObject包装的特点——例如像这个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会处理其余的作业。这是经过运用environmentalObject修饰符完结的,例如,像这样:

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

有了上述内容,咱们现在能够运用Enviroment特点包装器(而不是EnvironmentObject)来标记咱们视图的theme特点,并传入咱们期望检索的环境键的键值途径:

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办理状况的办法绝对是该结构最风趣的方面之一,它或许需求咱们略微从头考虑数据在使用中的传递办法——至少在涉及到将被咱们的UI直接消费和修改的数据时是这样。

我期望这篇攻略能成为一个很好的办法来概述SwiftUI的各种状况处理机制,虽然一些更详细的API被遗漏了,这篇文章中着重的概念应该涵盖了所有根据SwiftUI的状况处理的绝大多数用例。

感谢你的阅览!