swift和swiftui
Quite a few people have written articles on SwiftUI, SwiftUI state management, and on SwiftUI application architecture. And quite a few of those articles were written by people eager to take their favorite iOS application architecture and port it over to SwiftUI.
很少有人写过有关SwiftUI,SwiftUI状态管理和SwiftUI应用程序体系结构的文章。 这些文章中有很多是由渴望采用自己喜欢的iOS应用程序体系结构并将其移植到SwiftUI的人们撰写的。
Hence we’ve had articles telling us how to use MVVM with SwiftUI. Or VIPER. Or Clean. Or those that insist that we need to impose a React/Redux architecture on top of it.
因此,我们有文章告诉我们如何在SwiftUI中使用MVVM。 或VIPER。 或清洁。 或者那些坚持认为我们需要在其之上强加React / Redux架构的人。
But most of them have a problem.
但是大多数都存在问题。
You see, many of the architectures and methodologies that we’re attempting to inflict on SwiftUI were constructed and honed on applications built with UIKit and that relied on UIKit behaviors. Or worse, they’re architectures designed for other platforms and languages, like JavaScript and React.
您会看到,我们试图在SwiftUI上施加的许多架构和方法都是在使用UIKit构建的应用程序上构建和完善的,这些应用程序依赖于UIKit的行为。 更糟糕的是,它们是为其他平台和语言(例如JavaScript和React)设计的架构。
And the problem is… SwiftUI is not UIKit.
问题是…SwiftUI 不是 UIKit。
It doesn’t work the same, and it most definitely doesn’t behave the same.
它的工作原理不尽相同,并且绝对具有不同的表现。
This article will explore some of the mechanisms behind SwiftUI, and why even the words that we’re using to describe our application and its structure don’t mean what we think they mean.
本文将探讨SwiftUI背后的一些机制,以及为什么连我们用来描述应用程序及其结构的词也并不意味着我们认为它们意味着什么。
And why those misunderstandings can lead to performance problems, unexpected behavior, and even fatal errors in our applications.
以及这些误解为何会导致我们的应用程序出现性能问题,意外行为甚至致命错误的原因。
代码 (The Code)
Before we get started, I want to make it clear that the following conclusions are based on facts obtained from debugging, instrumenting, profiling, and logging actual SwiftUI applications.
在开始之前,我想明确指出以下结论是基于从实际SwiftUI应用程序的调试,检测,配置和日志记录中获得的事实。
The demonstration code in this article comes from a basic SwiftUI Master/Detail app to which I added a common Model-View-ViewModel architecture. I strongly suggest you download and run the code, interact with the program, and watch the logs as you do so.
本文中的演示代码来自基本的SwiftUI Master / Detail应用程序 ,我在其中添加了通用的Model-View-ViewModel体系结构。 我强烈建议您下载并运行代码,与程序进行交互,并在执行操作时查看日志。
This article is long, but a good portion of that length comes from the inclusion of some of that demonstration code and the resulting logs so you can better track the events unfolding as they occur and how those events support the article’s conclusions.
本文很长,但其中的很大一部分来自一些演示代码和生成的日志,因此您可以更好地跟踪事件发生时发生的事件以及这些事件如何支持本文的结论。
Let’s get started.
让我们开始吧。
没有视图 (There is no View)
If you’ve been an iOS developer for any length of time you have UIView and UIViewController baked into your brain.
如果您已经成为iOS开发人员有一段时间,那么UIView和UIViewController就会成为您的大脑。
You understand that views and view controllers are reference types and you understand UIKit’s rather extensive class heirarchy, from NSObject on up.
您了解视图和视图控制器是引用类型,并且从NSObject开始,您还了解UIKit相当广泛的类层次结构。
You understand how view controllers manage views, how they persist in the navigation stack, and all of the UIViewController lifecycle events. And you understand how views are constructed and added to subviews which are added to subviews and how they persist and are managed throughout the window-view-tree.
您将了解视图控制器如何管理视图,它们如何在导航堆栈中持久保存以及所有UIViewController生命周期事件。 并且您了解如何构造视图并将视图添加到添加到子视图的子视图中,以及它们如何持久化并在整个窗口视图树中进行管理。
Unfortunately, it’s this baked-in, ingrained knowledge of how UIKit-based development works that’s actually detrimental to understanding SwiftUI.
不幸的是,这种基于UIKit的开发工作方式的扎根,根深蒂固的知识实际上不利于理解SwiftUI。
Apple didn’t help much in this regard, as SwiftUI’s developers decided in their infinite wisdom to tell you that everything was a “View”. You make views of type View. Your view body returns some View. Even view modifiers return views.
Apple在这方面没有太大帮助,因为SwiftUI的开发人员以他们无穷的智慧决定告诉您一切都是“视图”。 您可以创建View类型的视图 。 您的视图主体返回一些 View 。 甚至视图修饰符也会返回视图。
But View is a term that makes long-time iOS developers think one thing when in actuality it’s something very, very, very different.
但是View是一个术语,它使长期的iOS开发人员实际上想到的是一件非常非常不同的事情。
UIKit is an imperative framework. We build our interface elements piece by piece, one element at a time, and then we wire everything together as best we can and shuffle data back and forth as needed.
UIKit是命令式框架。 我们一步一步地构建界面元素,然后一次将所有元素连接在一起,并根据需要来回移动数据。
SwiftUI, on the other hand, is a declarative framework. We use it to describe the user interface we want and then let SwiftUI build that interface for us based on our description.
另一方面,SwiftUI是声明性框架。 我们使用它来描述所需的用户界面,然后让SwiftUI根据我们的描述为我们构建该界面。
So please keep in mind that a SwiftUI View is not the resulting user interface view. It may not even end up being a view at all. A SwiftUI View is just a struct that contains a simple description of what we want our user interface to look like when it’s created and when it’s rendered.
因此,请记住,SwiftUI 视图 不是最终的用户界面视图。 它甚至可能根本就不是视图。 SwiftUI 视图只是一个结构,其中包含对我们希望用户界面在创建和呈现时的外观的简单描述。
A view may also have the ability to describe how we’re going to want that interface to eventually behave when the user interacts with it. Like when they tap a button.
视图还可以描述用户在与之交互时最终希望该界面如何表现。 就像当他们点击一个按钮时一样。
And that’s it. SwiftUI views are just definitions.
就是这样。 SwiftUI视图只是定义。
But old habits die hard. We look at a SwiftUI View, see that it’s a View, and in our heads we start thinking that our view definition is going to persist just like it would if it were a UIView. It won’t.
但是旧习惯很难消亡。 我们来看一个SwiftUI 视图 ,看到它是一个视图 ,并且在我们脑海中,我们开始认为我们的视图定义将像UIView一样持久化。 不会的
We think that it’s safe to hang view models and other classes off them like we do with UIViewController. It’s not.
我们认为,像使用UIViewController一样,将视图模型和其他类挂起是安全的。 不是。
We think we know when our “view” is instantiated, when its body variable is called, and how the interface is constructed from that definition. But we don’t.
我们认为我们知道何时实例化“视图”,何时调用其主体变量以及如何根据该定义构造接口。 但是我们没有。
And we may even think a view modifier modifies its view… I mean, that’s what it’s called, right? But again, the problem is that in many cases it doesn’t really do that either.
我们甚至可能认为视图修饰符会修改其视图……我的意思是,这就是所谓的,对吧? 但是,问题又来了,在很多情况下,它也没有做到这一点。
There’s a lot to unpack here, so let’s get started.
这里有很多要解压的东西,所以让我们开始吧。
那么,如果视图不是视图,那么它是什么? (So if a View is not a View, then what is it?)
SwiftUI is a struct-based, protocol-oriented, Domain-Specific-Language developed to describe user interfaces on Apple devices ranging from iPhones to iPads to Apple Watches to the Apple TV and even the Mac.
SwiftUI是一种基于结构的,面向协议的,特定于域的语言,旨在描述Apple设备上的用户界面,包括iPhone,iPad,Apple Watch,Apple TV甚至Mac。
In that sense, it’s platform agnostic. We give SwiftUI an idea of what we want, and by and large we let it do what’s appropriate when that code’s run on a specific platform. A Toggle view says we want a switch, but how that switch looks and even how it functions differs from iOS to macOS to the Apple TV.
从这个意义上讲,它与平台无关。 我们给SwiftUI一个我们想要的想法,总的来说,当代码在特定平台上运行时,我们让它做适当的事情。 切换视图表示我们需要一个开关,但是从iOS到macOS再到Apple TV,该开关的外观甚至功能如何都不同。
From an implementation standpoint, it’s important to remember that SwiftUI Views are lightweight value types that exist solely to define our interface.
从实现的角度来看,重要的是要记住,SwiftUI 视图是仅用于定义我们的接口的轻量值类型。
Every custom view we create has a body variable that, when called, returns another view. The returned view might be a primitive view type native to SwiftUI like Text or Image.
我们创建的每个自定义视图都有一个body变量,该变量在调用时会返回另一个视图。 返回的视图可能是SwiftUI固有的原始视图类型,例如Text或Image 。
struct ItemDateView: View {
var date: Date
var body: some View {
Text("\(date, formatter: dateFormatter)")
}
}
Here, the description of ItemDateView’s interface is exceedingly simple. We just want to show the user a date formatted as text. The returned view could also be a container view like a List or VStack that tracks and manages a list of views.
在这里, ItemDateView界面的描述非常简单。 我们只想向用户显示一个格式化为文本的日期。 返回的视图也可以是容器视图,例如List或VStack ,用于跟踪和管理视图列表。
struct DetailContentView: View {
var model: DetailViewModel
var body: some View {
tracker {
VStack(spacing: 16) {
ItemDateView(item: model.item)
DetailStateView()
DetailBoilerplateView(text: model.boilerplate)
Spacer()
}
}
}
}
Here, our DetailContentView is also simple. We just want a vertical stack in which we’ll show the contents of our ItemDateView, a DetailStateView, and a DetailBoilerplateView, plus another primitive Spacer view.
在这里,我们的DetailContentView也很简单。 我们只需要一个垂直堆栈,即可在其中显示ItemDateView, DetailStateView和DetailBoilerplateView以及另一个原始的Spacer视图的内容。
Each one of those three custom views has its own body variable, which when called will again return some primitive view, a container view, or a custom view.
这三个自定义视图中的每一个都有其自己的body变量,该变量在被调用时将再次返回一些原始视图,容器视图或自定义视图。
In SwiftUI, we rinse and repeat and combine all of these views as needed until we’ve finally defined our entire application.
在SwiftUI中,我们将根据需要冲洗并重复并合并所有这些视图,直到最终定义了整个应用程序。
But note that nothing we’ve defined so far has generated a single user interface element.
但是请注意,到目前为止,我们定义的任何内容都没有生成单个用户界面元素。
Key Point #1: A SwiftUI View is not a view. It’s a description of a view.
关键点1:SwiftUI视图不是视图。 这是对视图的描述。
状态和视图图 (State and the View Graph)
You can’t do much with SwiftUI without also learning that it’s designed from the ground up to be state-driven.
在不了解SwiftUI是完全由状态驱动而设计的情况下,SwiftUI不能做太多事情。
That state can be as simple as a view property. Or it could be a @State variable, or an @Environment variable, or any of several other types provided for us by SwiftUI.
该状态可以与view属性一样简单。 也可以是@State变量或@Environment变量,或者是SwiftUI为我们提供的其他几种类型中的任何一种。
Each piece of state is the Source of Truth for some part of our application.
每个状态都是我们应用程序某些部分的真理之源 。
When our application is launched, the views we’ve defined are combined with the initial state of the application and together will eventually form a tree-like structure known as the view graph.
启动我们的应用程序时,我们定义的视图将与应用程序的初始状态结合在一起,并最终形成一个称为视图 图的树状结构。
This graph defines how our application appears at that specific point in time.
此图定义了我们的应用程序在该特定时间点的显示方式。
应用启动 (Application Launch)
So let’s run our app and examine the process in detail. Most SwiftUI apps start with a ContentView that’s assigned as the root view of our application in our SceneDelegate. Or, in SwiftUI 2.0, in the main App view.
因此,让我们运行我们的应用程序并详细检查该过程。 大多数SwiftUI应用程序都以ContentView开头,该ContentView在SceneDelegate中被分配为应用程序的根视图。 或者,在SwiftUI 2.0中,在主应用程序视图中。
@main
struct AppStateDemo: App {
var body: some Scene {
return WindowGroup {
ContentView()
}
}
}
The struct defining ContentView will be instantiated when our application was launched and that particular instance of the view will exist for the lifetime of the entire application (or the lifetime of the scene, in this case).
启动我们的应用程序时,将实例化定义ContentView的结构,并且该视图的特定实例将在整个应用程序的生命周期(在这种情况下为场景的生命周期)中存在。
So let’s say that it’s finally time for our interface to be rendered and presented to the user. To accomplish this, SwiftUI will ask ContentView’s body variable to provide a set of views that describe the interface we want.
因此,可以说现在是时候将我们的界面呈现并呈现给用户了。 为此,SwiftUI将要求ContentView的 body变量提供一组描述所需接口的视图。
Here’s our ContentView.
这是我们的ContentView 。
struct ContentView: View {
@ObservedObject var master = MasterViewModel()
var body: some View {
NavigationView {
MasterView()
.environmentObject(master)
}
}
}
This looks straightforward enough. But let’s walk thought it.
这看起来很简单。 但是,让我们漫步吧。
First it’s obvious that a MasterViewModel was instantiated when the ContentView was created. We’ve also wrapped it in an ObservedObject property wrapper since it’s an ObservableObject. Seems fine so far.
首先,很明显MasterViewModel是 创建ContentView时实例化。 由于它是ObservableObject,因此我们也将其包装在ObservedObject属性包装器中。 到目前为止似乎还不错。
Next, we’ve been told that SwiftUI will call our body variable to determine what we want to build, so, working inside out in order of evaluation, ContentView’s body variable will first instantiate a custom MasterView struct. We then indicate that an instance of our view model should be inserted into the environment for later use downstream using the environmentObject modifier available on MasterView.
接下来,我们被告知,SwiftUI将调用我们的body变量来确定我们要构建的内容,因此,按照评估顺序由内而外进行操作, ContentView的 body变量将首先实例化自定义MasterView结构。 然后,我们指示应使用MasterView上的environmentObject修饰符将视图模型的实例插入环境中,以供以后在下游使用。
The “view” returned by the modifier is handed to the ViewBuilder needed by NavigationView’s initialization function. Our ContentView’s description is now complete, and the body function will return our completely constructed NavigationView.
修饰符返回的“ 视图”将传递给NavigationView的初始化功能所需的ViewBuilder 。 现在,我们对ContentView的描述已经完成,并且body函数将返回我们完整构建的NavigationView 。
And that that builds the initial nodes in our view graph.
这将在我们的视图图中构建初始节点。
Easy as pie.
非常简单。
Except this is SwiftUI, and in SwiftUI, nothing is that simple.
除了这是SwiftUI,在SwiftUI中, 没有那么简单。
First, and I’m going to keep pounding on this, let me again point out that the NavigationView that’s returned by the ContentView body is not a UINavigationView.
首先,我将继续对此进行说明,让我再次指出ContentView 主体返回的NavigationView 不是 UINavigationView 。
It will indeed cause a UINavigationView to be created (at least on iOS), but here it’s just a struct that tells SwiftUI that some form of navigation will be needed when our application is rendered.
确实会导致创建UINavigationView (至少在iOS上是这样),但这只是一个结构,它告诉SwiftUI在呈现我们的应用程序时将需要某种形式的导航。
And in point of fact, SwiftUI will begin construction of our UINavigationView hierarchy before it even calls ContentView’s body variable. Which means that it can’t use our definition’s NavigationView as our UINavigationView.
实际上,SwiftUI甚至会在调用ContentView的 body变量之前开始构建UINavigationView层次结构。 这意味着它不能使用我们定义的NavigationView作为UINavigationView。
We haven’t even constructed one yet.
我们甚至还没有建造一个。
In effect, SwiftUI knows that we need a UINavigationView before we even have a chance to tell it what we want a NavigationView! But how is that possible?
实际上,SwiftUI知道我们甚至需要有一个UINavigationView才有机会告诉我们我们想要一个NavigationView ! 但是那怎么可能呢?
类型推断 (Type Inference)
The short answer is simple: Type Inference.
简短的答案很简单: 类型推断。
A somewhat longer answer is that in SwiftUI a View is a protocol with an associated Body type that’s defined as the result of the body variable.
更长一点的答案是,在SwiftUI中, 视图是一种协议,具有关联的主体类型,该主体类型定义为主体变量的结果。
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
And if we get print the type of ContentView.Body in the debugger we see…
如果我们在调试器中打印出ContentView.Body的类型 我们看…
NavigationView _EnvironmentKeyWritingModifier>>>
As such, SwiftUI can use our return types to tell what sort of things we’re going to need even before we ask for them. But how do we know this?
这样,SwiftUI可以使用我们的返回类型来告诉我们甚至在我们需要它们之前需要什么。 但是我们怎么知道呢?
记录ContentView (Logging ContentView)
Let’s look at a slightly customized version of ContentView.
让我们看一下ContentView的一个定制版本。
struct ContentView: View {
@ObservedObject var master = MasterViewModel()
let tracker = InstanceTracker("ContentView")
var body: some View {
tracker {
NavigationView {
MasterView()
.environmentObject(master)
}
}
}
}
This version is identical to the one before, except that we added an InstanceTracker class to our view that lets us know when one is instantiated and which also adds a pass-through function that tells us when our view’s body variable is called. Each time a tracker is instantiated it’s given a new tracking number.
该版本与之前的版本相同,除了我们向视图添加了InstanceTracker类,该类使我们知道实例化的时间,并且还添加了传递函数,该函数可告诉我们何时调用视图的body变量。 每次实例化跟踪器时,都会为其指定一个新的跟踪号。
And since tracker is deallocated when our view goes out of scope, we can also track the lifecycle of that view.
并且由于在视图超出范围时会重新分配跟踪器 ,因此我们也可以跟踪该视图的生命周期。
AppStateDemo.init() #1
AppStateDemo.body #1 {
MasterViewModel.init() #2
ContentView.init() #3
}UINavigationController.initWithRootViewController
ContentView.body #3 {
MasterView.init() #4
}
If we look at the debugging log with our tracker’s added, we can see when our MasterViewModel was initialized, when our ContentView was initialized, and then, thanks to a symbolic breakpoint in the debugger, we can see that a UINavigationController was constructed using initWithRootViewController.
如果查看添加了跟踪器的调试日志,就可以看到何时初始化MasterViewModel ,何时初始化ContentView ,然后,由于调试器中有符号断点,我们可以看到使用initWithRootViewController构造了UINavigationController 。
And that all of that occurred before ContentView.body was called and had a chance to return our NavigationView/MasterView definition.
而且所有这些都是在调用ContentView.body 之前发生的,并且有机会返回我们的NavigationView / MasterView定义。
Now, you could be forgiven for thinking that this is simply how SwiftUI works, and that it will always build a UINavigationController as part of its app launch process, but we can disprove that in a couple of ways. Simply remove the NavigationView from the code, and SwiftUI won’t construct one.
现在,您可能会以为这只是SwiftUI的工作原理而被原谅,并且它将始终在其应用程序启动过程中构建UINavigationController ,但我们可以通过两种方式对此进行反驳。 只需从代码中删除NavigationView ,SwiftUI就不会构造一个。
Or we could erase the result of the Body type using AnyView. If we do that we’ll still get a UINavigationController, but it will be built later in the process, after SwiftUI has had a chance to retrieve and inspect ContentView’s set of view definitions.
否则我们可以删除Body类型的结果 使用AnyView。 如果这样做,我们仍然会得到一个UINavigationController,但是它将在该过程的稍后部分构建, 因为 SwiftUI有机会检索并检查ContentView的视图集 定义。
AppStateDemo.init() #1
AppStateDemo.body #1 {
MasterViewModel.init() #2
ContentView.init() #3
}
ContentView.body #3 {
MasterView.init() #4
}UINavigationController.initWithRootViewController
The initial root view controller is our old friend UIHostingController, which indicates that SwiftUI builds its view controller hierarchy almost exactly as we did manually in iOS 12 using SceneDelegate.
最初的根视图控制器是我们的老朋友UIHostingController ,它指示SwiftUI几乎像我们在iOS 12中使用SceneDelegate手动完成的那样构建其视图控制器层次结构。
Keep in mind that SwiftUI has yet to even peek inside MasterView and yet its already begun construction of our navigation stack and user interface.
请记住,SwiftUI尚未进入MasterView内部,但它已经开始构建我们的导航堆栈和用户界面。
主视图 (MasterView)
Moving on, our ContentView said we want to show a MasterView, and to do that we now need to call MasterView’s body variable to request its definition.
继续,我们的ContentView说我们要显示一个MasterView ,为此,我们现在需要调用MasterView的 body变量以请求其定义。
Here’s MasterView, also fitted out with a tracker and some more debugging code so we can track our progress.
这是MasterView,还配备了跟踪器和更多调试代码,以便我们可以跟踪进度。
struct MasterView: View { @EnvironmentObject var master: MasterViewModel let tracker = InstanceTracker("MasterView")
var body: some View {
tracker("List(\(master.items.count) items)") {
List {
ForEach(master.items) { item in
NavigationLink(
destination: DetailView(item: item)
) {
ItemDateView(date: item.date)
}
}
}
.onAppear {
tracker("MasterView.onAppear")
}
.navigationBarTitle(Text("Master"))
.navigationBarItems(
trailing: Button(action: { self.master.add() }) {
Image(systemName: "plus")
}
)
}
}
}
Note that MasterView contains a List. Lists in SwiftUI on iOS will generate table views to manage them. Particularly, an UpdateCoalescingTableView with our view and its ForEach loop as its data source.
请注意, MasterView包含一个List 。 iOS上的SwiftUI中的列表将生成表视图以对其进行管理。 特别是,将视图和其ForEach循环作为数据源的UpdateCoalescingTableView 。
If we continue our debugging sequence, we’ll see when MasterView’s body is called.
如果继续调试,我们将看到何时调用MasterView的主体 。
MasterView.body #4 {
List(0 items)
}
It’s quite apparent from the logs how SwiftUI instantiates a view inside a body variable, then calls its body variable to get another view, and then calls its body variable when it’s time to construct the interface for that view.
这是一个从SwiftUI如何实例化一个体变量里面查看日志很明显, 然后调用它的身体变量获得另一种观点, 然后调用它的身体变化时,它的时间来构建该视图的接口。
In this particular instance we’re done, but in a more complex application the process of instantiating views, evaluating body variables, building nodes, and constructing user interface elements would repeat until we get to the eventual end of our view tree.
在这个特定的例子中,我们已经完成了,但是在更复杂的应用程序中,实例化视图,评估主体变量,构建节点以及构建用户界面元素的过程将重复进行,直到到达视图树的最后。
And we finally see…
我们终于看到了……
MasterView.onAppear
Key point #2: Interface construction in SwiftUI is an incremental process.
关键点2:SwiftUI中的接口构造是一个增量过程。
This is especially true in regard to navigation stacks with UIHostingController and lists that have tableviews at their core.
对于带有UIHostingController的导航堆栈以及以表格视图为核心的列表而言,尤其如此。
查看层次结构 (View Hierarchy)
Let’s take a quick look at our view hierarchy.
让我们快速看一下我们的视图层次结构。
The gray view in the middle is our table view. The view with the “0 items” text is a ListHostingView, which is rendering the contents of that particular cell.
中间的灰色视图是我们的表格视图。 带有“ 0个项目”文本的视图是ListHostingView ,它正在呈现该特定单元格的内容。
Note that “0 items” shown in this particular example is not a UILabelView and that’s an important point.
请注意,此特定示例中显示的“ 0个项目” 不是 UILabelView ,这很重要。
Up to now we might have been slowly lulled back into thinking that there’s a direct correspondence between SwiftUI views and UIKit views. After all, our NavigationView generated a UINavigationController, and our List view generated a table view.
到现在为止,我们可能已经慢慢淡忘了,认为SwiftUI视图和UIKit视图之间存在直接对应关系。 毕竟,我们的NavigationView生成了一个UINavigationController ,而我们的List视图生成了一个表视图。
So wouldn’t a Text view also be an UILabelView and an Image view be an UIImageView? Again, that would be simple, easy, and… that’s not how SwiftUI rolls.
因此, 文本视图也不会是UILabelView , 图像视图也不会是UIImageView吗? 同样,那将是简单,容易的,而且……这不是SwiftUI滚动的方式。
In SwiftUI, text and images and shapes are composed and rendered directly on one of a hosting view’s UILayers. HStacks and VStacks, similarly, are not UIStackViews, but fundamentally boil down to positioning calculations for the nested “subviews”.
在SwiftUI中,文本,图像和形状直接在托管视图的UILayers之一上组成和呈现。 同样, HStack和VStack也不是UIStackViews ,而是从根本上归结为嵌套“子视图”的定位计算。
ZStacks are just slightly more complex, in that each layer in a ZStack generates a new layer on the hosting view. Same for the background and overlay modifiers. Just more layers.
ZStack稍微复杂一点,因为ZStack中的每个层都会在托管视图上生成一个新层。 背景和叠加层修改器也是如此。 只是更多层。
Which brings up back around to our first key point: A SwiftUI View is not a view.
这回到了我们的第一个关键点: SwiftUI视图不是视图。
And which also reinforces one of Apple’s original key points: SwiftUI views, in many cases, are not views. Unlike in UIKit, adding another view to a view hierarchy doesn’t generate and add another heavy, clunky NSObject-based UIView to the view rendering and responding chain.
而且这也强化了Apple最初的关键要点之一:在许多情况下,SwiftUI视图不是视图。 与在UIKit中不同,在视图层次结构中添加另一个视图不会生成另一个笨重的, 基于NSObject的笨拙的UIView ,也不会将其添加到视图呈现和响应链。
Nor does adding additional HStacks and VStacks to a layout add more UIStackViews and tons of more NSLayoutConstraints.
向布局中添加其他HStack和VStack也不会添加更多UIStackViews和更多NSLayoutConstraints 。
Key Point #3: There isn’t a direct one-to-one correspondence between SwiftUI view definitions and UIKit view instances.
关键点3:SwiftUI视图定义和UIKit视图实例之间没有直接的一对一对应关系。
查看修改器 (View Modifiers)
In SwiftUI Views can be modified in various ways by view modifiers.
在SwiftUI中,可以通过视图修改器以多种方式修改视图。
Text("Hello World")
.forgroundColor(.red)
.font(.title)
But in actuality, a view modifier is just another struct that wraps the parent struct, which wraps the parent struct, etc..
但是实际上,视图修饰符只是包裹父结构的另一个结构,包裹父结构的等等。
But as I mentioned above, most view modifiers don’t actually modify the view. In fact, and as we’ve seen, there is often no view to modify. So what’s happening?
但是如上所述,大多数视图修饰符实际上并不修改视图。 实际上,正如我们所看到的, 通常没有修改视图。 那是怎么回事?
The short answer is that quite a few of the available view modifiers modify not the view but the environment.
简短的答案是,相当多的可用视图修饰符不是修改视图,而是修改环境。
Key Point #4: The current state of the environment is used when it’s time to render our interface.
关键点4:当需要渲染界面时,将使用环境的当前状态。
We want to render of piece of text, so we grab the current font from the environment, and the current foreground color, and we use those values to do our rendering. That’s how, for example, setting the current foregroundColor on a VStack or Group impacts each of the Text values contained within it.
我们要渲染一段文本,因此我们从环境中获取当前字体和当前前景色,然后使用这些值进行渲染。 例如,这就是在VStack或组上设置当前的前台颜色如何影响其中包含的每个Text值的方式。
VStack {
Text("A")
Text("B")
}
.foregroundColor(.red)
Each text element shown in the above example will be red.
上例中显示的每个文本元素将为红色。
状态变更 (State Change)
Now for the fun part. Our application is up and running and the user taps a button that causes our application’s state to change. Or maybe a timer fires or an API finally returned a result. How it occurs doesn’t really matter. The fact that our state changed does.
现在是有趣的部分。 我们的应用程序已启动并正在运行,用户点击一个按钮即可更改我们的应用程序状态。 也许计时器触发了,或者API最终返回了结果。 它如何发生并不重要。 我们状态改变的事实确实如此。
Key Point #5: When state changes, any view with a direct dependency on that state will be regenerated.
关键点5:状态更改时 , 将重新生成任何直接依赖于该状态的视图 。
By that I meant the dependent view’s body variable will be called and asked to provide a new definition of what the user interface should now look like based on the new state of the application. In short, it returns a new View.
我的意思是,将调用从属视图的主体变量,并要求其根据应用程序的新状态提供新的定义,以定义用户界面现在的外观。 简而言之,它返回一个新的View 。
Note that this one single request can trigger a cascade down that entire branch of the view graph.
请注意,这个单个请求可以触发级联向下遍历视图图的整个分支。
In our definition of MasterView, we included MasterViewModel as an @EnvironmentObject, which makes MasterView directly dependent on MasterViewModel. If MasterViewModel changes, then MasterView needs to be rebuilt in order to see what might have changed.
在对MasterView的定义中,我们将MasterViewModel作为@EnvironmentObject包含在内,这使MasterView直接依赖于MasterViewModel。 如果MasterViewModel发生更改,则需要重建MasterView以便查看可能发生了什么更改。
struct MasterView: View {
@EnvironmentObject var master: MasterViewModel
var body: some View {
...
}
}
So let’s hit the “plus” button to add a new entry to our list and observe the results.
因此,让我们点击“加号”按钮以将新条目添加到列表中,并观察结果。
ContentView.body #3 {
MasterView.init() #6
}
MasterView.body #6 {
List(1 items)
}
...
ContentView is also dependent on our MasterViewModel via ObservableObject. So when MasterViewModel changes SwiftUI will once again call view ContentView’s body variable to get a new view definition.
ContentView也通过ObservableObject依赖于我们的MasterViewModel 。 因此,当MasterViewModel更改时,SwiftUI将再次调用视图ContentView的 body变量以获取新的视图定义。
In the body variable ContentView instantiates a new, modified MasterView struct. That will, in turn, eventually cause SwiftUI to call view MasterView’s body once more get that view’s definition.
在主体变量中, ContentView实例化一个新的,经过修改的MasterView结构。 这将反过来,最终导致SwiftUI调用视图马西德威的身体再一次得到这种观点的 定义。
That would have occurred anyway since, as we’ve mentioned several times, MasterView also has a direct dependency on MasterViewModel.
无论如何,这都会发生,因为正如我们多次提到的那样, MasterView也直接依赖于MasterViewModel。
TableView更新 (TableView Updates)
MasterView uses a List, and a List is a tableview, and we now have some data to display… so now it’s time for the tableview to reload its data and build some cells from its datasource.
MasterView使用一个List ,一个List是一个表视图,现在我们要显示一些数据……所以现在该该表视图重新加载其数据并从其数据源构建一些单元格了。
We only have one cell at this point, so let’s see what else has been logged.
此时我们只有一个单元格,所以让我们看看还有什么记录的。
...
DetailView.init() #7
DetailViewModel.init() #8
ItemDateView.init() #9
ItemDateView.body #9 {
2020-08-23 22:36:14 +0000
}
ItemDateView.body #9 {
2020-08-23 22:36:14 +0000
}
ItemDateView.body #9 {
2020-08-23 22:36:14 +0000
}
MasterView.deinit() #4
Say what? Note that in constructing our cell an instance of our DetailView was instantiated… along with its associated view model!
说什么? 请注意,在构造单元格时,实例化了DetailView实例及其关联的视图模型!
I wrote about this at some length in SwiftUI and How NOT to Initialize Bindable Objects. (The title of which should tell you just how long this particular behavior has been around).
我在SwiftUI和“如何不初始化可绑定对象”中对此进行了详细介绍 。 (标题应该告诉您这种特定行为发生了多长时间)。
The bottom line is that every view in SwiftUI is a struct. Structs must be initialized when created. Our detail view is immediately instantiated and passed to NavigationLink as a parameter when that view is displayed and as such anything we construct during view initialization or as a view property will also be instantiated at the same time.
最重要的是,SwiftUI中的每个视图都是一个结构。 创建结构时必须对其进行初始化。 当显示该视图时,我们的详细信息视图将立即实例化并作为参数传递给NavigationLink ,因此,我们在视图初始化期间构造的任何内容或作为视图属性的对象也将同时被实例化。
struct DetailView: View { @EnvironmentObject var master: MasterViewModel
@ObservedObject var model: DetailViewModel init(item: Item) {self.model = DetailViewModel(item: item)
} let tracker = InstanceTracker("DetailView")
var body: some View {
tracker {
DetailContentView(model: model)
.padding()
.navigationBarTitle(Text("Detail"))
.navigationBarItems( ... )
.onAppear {
print("- DetailView onAppear")
}
}
}
}
As shown above, I instantiated a view model with the item passed in the ForEach loop, but due to the way NavigationLink works, our “lightweight” DetailView will now do a DetailViewModel heap allocation the instant we make a DetailView.
如上图所示,我实例化在foreach循环中通过项目视图模型,但由于方式NavigationLink的工作,我们的“轻量级” 的DetailView现在会做DetailViewModel堆分配我们做的DetailView瞬间。
That’s not good.
这不好。
More to the point, what happens if our list grew to where we had, say, a 1,000 items in it? Well, then we’re going to have a bunch of DetailViewModel objects continually being created and disposed of while we scroll around.
更重要的是,如果我们的清单增加到我们其中有1,000项的位置会发生什么? 好吧,那我们将拥有一堆DetailViewModel对象 在我们滚动时不断被创建和处理。
And whether we actually visit that page or not.
以及我们是否实际访问该页面。
What if those models were doing some sort of processing on init? Also not good.
如果这些模型在init上进行某种处理怎么办? 也不好。
And what happens if this view is regenerated and our view model ends up being replaced when we’re not expecting it? That’s definitely not good. We’ll come back to this point a bit later.
如果重新生成此视图,并且在我们不期望它时最终替换了我们的视图模型,会发生什么? 那绝对不好。 我们稍后再回到这一点。
下游变化 (Downstream Changes)
Key Point #6: When state changes, any view downstream of that direct dependency will be reinstantiated and regenerated.
关键点6:状态更改时 , 将重新实例化并重新生成该直接依赖关系下游的任何视图 。
Changing our state will ultimately result in new nodes for our view graph, which now represents the new state of the application. SwiftUI will then compare the new definitions against the definitions contained in the previous view graph.
更改状态最终将为视图图产生新的节点,该节点现在表示应用程序的新状态。 然后,SwiftUI会将新定义与上一个视图图中包含的定义进行比较。
Key Point #7: At any point where the graphs diverge, SwiftUI will update the user interface and the view graph accordingly.
关键点7:在图形发生分歧的任何时候,SwiftUI都会相应地更新用户界面和视图图形。
So if a font or color or some text property has changed, SwiftUI will update the appropriate font or color or text and redraw the relevant sections of the interface.
因此,如果字体,颜色或某些文本属性已更改,SwiftUI将更新相应的字体,颜色或文本并重绘界面的相关部分。
SwiftUI may even animate certain changes to the interface for us. If, say, an item is removed from a List or a VStack, then SwiftUI may animate the items beneath it upwards, closing the gap. Or it may do the opposite if another view is added.
SwiftUI甚至可以为我们的界面设置某些动画。 例如,如果从List或VStack中删除了一个项目,则SwiftUI可能会向上动画其下方的项目,从而缩小间隙。 或者,如果添加了另一个视图,则可能相反。
The change might be even more drastic, if, say, a presented view should be shown or dismissed.
如果说应该显示或取消呈现的视图,则更改可能会更加剧烈。
Once that’s done our user interface will once again accurately reflect our state, and the nodes in new view graph will replace those in the old view graph, ready for the next event.
一旦完成,我们的用户界面将再次准确地反映我们的状态,新视图图中的节点将替换旧视图图中的节点,为下一个事件做好准备。
The same exact thing happens all over again when we have another state change. And the next change. And the next. The view graph in a SwiftUI-based application is in a constant, never-ending state of flux.
当我们有另一个状态改变时,同样的事情又发生了。 然后下一个变化。 接下来。 基于SwiftUI的应用程序中的视图图处于不断变化的恒定状态。
观点是短暂的 (Views are Ephemeral)
This brings us to another key point that can not be stressed enough.
这给我们带来了另一个压力,这个压力不能被足够强调。
Key Point #8: SwiftUI Views are constantly being constructed and evaluated during the lifetime of our application.
关键点8: 在我们的应用程序生命周期中, 不断 在构建和评估 SwiftUI视图 。
Each and every state change will result in part or all of the view graph being reconstructed. Translated, that means your old views are going to get tossed and replaced with new views.
每个状态变化都会导致重构部分或全部视图。 翻译后,这意味着您的旧视图将被替换为新视图。
This means that anything attached to one of those views will also go away.
这意味着附加到这些视图之一的所有内容也将消失。
Think about the consequences of that for a moment.
暂时考虑一下其后果。
Or better yet, let’s hit our plus button one more time and observe the result.
或者更好的是,让我们再按一次加号按钮并观察结果。
ContentView.body #3 {
MasterView.init() #11
}
MasterView.body #11 {
List 2 items
}
DetailView.init() #12
DetailViewModel.init() #13
ItemDateView.init() #14
ItemDateView.body #14 {
2020-09-09 00:06:24 +0000
}
ItemDateView.deinit() #9
DetailViewModel.deinit() #8
DetailView.deinit() #7
DetailView.init() #15
DetailViewModel.init() #16
ItemDateView.init() #17
ItemDateView.body #17 {
2020-09-09 00:06:31 +0000
}
MasterView.deinit() #6
Note our reconstructed MasterView was once again thrown away and rebuilt. Worse, note that our original DetailView and our carefully constructed DetailViewModel for our first data element was also unceremoniously tossed aside in favor of a new one.
请注意,我们重新构建的MasterView再次被丢弃并重新构建。 更糟糕的是,请注意,我们最初的第一个数据元素的原始 DetailView和精心构造的DetailViewModel也被毫不客气地抛在一边,以支持一个新的。
轻量级视图 (Lightweight Views)
This leads us to a new pair of rules.
这导致我们有了新的一对规则。
Key Point #9: Don’t do computationally heavy work on view initialization.
关键点9:不要在视图初始化上做大量的计算工作。
SwiftUI views are lightweight structs for a reason. Do NOT burden the view initialization process with heap allocations or do a lot of processing at that point in time.
SwiftUI视图是轻量级结构,这是有原因的。 不要在视图初始化过程中增加堆分配的负担,也不要在该时间点进行大量处理。
Further, note that each of our ItemDateView’s body variables were called three times apiece during the construction and display of each List cell.
此外,请注意,在构造和显示每个List单元时,每个ItemDateView的 body变量都被调用了3次。
As I said at the beginning, you don’t really know just when SwiftUI is going to need to access your view’s definition. Or how often it’s going to do so.
正如我在开始时所说的,您并不真正知道SwiftUI何时需要访问视图的定义。 或这样做的频率。
So, if you don’t know when a block of code is going to be called, and if you don’t know how often it’s going to be called, should you put critical or data intensive code there?
因此,如果您不知道何时调用某个代码块,并且不知道将调用该代码的频率 ,您是否应该在其中放置关键或数据密集型代码?
Especially when doing so can have a significant impact on your application’s performance?
特别是这样做会对您的应用程序性能产生重大影响吗?
I don’t think so. There’s a time and a place for everything, but view initialization and inside your view’s body variable isn’t that time nor is it the place.
我不这么认为。 一切都有时间和地点,但是视图初始化和视图的body变量内部不是时间,也不是地点。
Key Point #10: Don’t do computationally heavy work in your view body either.
关键点10:在视图主体中也不要做繁重的工作。
根依赖 (Root Dependencies)
Note that several of the points we’ve made warrant further consideration.
请注意,我们提出的几点要进一步考虑。
As should be obvious by now, state change can have a major impact on the view graph, and just how much impact it has largely depends on just where the dependent view exists in the view hierarchy.
由于现在应该是显而易见的,状态变化可能对视图图形产生重大影响,而且它刚刚多大的影响在很大程度上取决于流向何方视图层次结构中存在依赖视角。
A dependency low or near the edge of the hierarchy will have a small impact on the number of views that need to be regenerated. As a small example, a button that changes its background color when tapped is a small, extremely localized change within the view hierarchy.
层次结构边缘较低或接近边缘的依赖关系将对需要重新生成的视图数量产生很小的影响。 作为一个小示例,在点击时更改其背景颜色的按钮是视图层次结构中的一个很小的,非常局部的更改。
Conversely, a dependency located at the root or high in our view hierarchy can cause the entire downstream view graph to be regenerated and the results compared each and every time the dependent state changes. As we just demonstrated in the section on Rebuilding the View Graph.
相反,位于我们视图层次结构根部或较高位置的依赖项可以导致重新生成整个下游视图图,并且每当依赖状态发生变化时,都会对结果进行比较。 正如我们在“ 重建视图图 ”一节中所演示的。
For best performance, such “root” dependencies should be avoided or minimized if at all possible.
为了获得最佳性能,应尽可能避免或最小化此类“根”依赖性。
As such, a React/Redux “there can be only one” style of global state management should probably be avoided, as any state change will cause everything dependent on the central data store to be regenerated. Again, basically the entire application view graph.
这样,应该避免使用React / Redux的“只有一个”全局状态管理风格,因为任何状态更改都将导致重新生成所有依赖于中央数据存储的内容。 同样,基本上是整个应用程序视图图。
Key Point #11: Avoid large collections of state data. Keep state localized where possible.
关键点11:避免大量收集状态数据。 尽可能保持状态本地化。
In fact, I wrote about this at some length in a previous article, SwiftUI Microservices.
实际上,我在上一篇文章SwiftUI Microservices中写了很多篇文章。
Note that the last point ends with the phrase “where possible”. While root dependencies should be avoided, they also shouldn’t be feared either. Provided, of course, that you’ve begun following my advice to keep your view initializations and view bodies as lightweight and performant as you can.
请注意,最后一点以短语“在可能的情况下”结尾。 尽管应该避免根依赖,但也不要担心它们。 当然,前提是您已经开始遵循我的建议,以保持视图初始化和视图主体尽可能轻巧和高效。
It is, after all, how SwiftUI was designed to work.
毕竟,这就是SwiftUI设计的工作方式。
异步更新 (Asynchronous Updates)
Earlier I mentioned that changing state will ultimately result in a new view graph. I choose those words with care, as the key word in that sentence is ultimately.
之前我提到过,改变状态最终将导致新的视图图。 我小心翼翼地选择那些词,因为该句子中的关键词是最终的。
Key Point #12: User interface updates happen after the state change has occurred during an asynchronous update scheduled for the next run cycle.
关键点12: 在为下一个运行周期安排的异步更新过程中,状态更改 发生 后 ,才进行 用户界面更新 。
I alluded to this earlier, but changing several @Published variables on an ObservableObject will fire several .objectWillChange() events. Those events are aggregated and will eventually — not immediately — trigger a view update cycle.
我之前提到过,但是在ObservableObject上更改几个@Published变量将触发几个。 objectWillChange ()事件。 这些事件已汇总,最终将(而不是立即)触发视图更新周期。
That may be hard to parse without an example, so let’s look at one.
没有示例可能很难解析,所以让我们来看一个示例。
Let’s examine a portion of our MasterView model, which has a few Combine sinks defined in our init function solely for tracking purposes.
让我们检查一下MasterView模型的一部分,该模型在我们的init函数中定义了一些Combine 接收器 ,仅用于跟踪目的。
class MasterViewModel: ObservableObject { @Published var update1 = 0
@Published var update2 = 0 var cancellable0: AnyCancellable!
var cancellable1: AnyCancellable!
var cancellable2: AnyCancellable! init() {
cancellable0 = objectWillChange.sink { (value) in
self.tracker.log("Sink 0 recived change notification")
}
cancellable1 = $update1.sink { (value) in
self.tracker.log("Sink 1 recived value = \(value)")
}
cancellable2 = $update2.sink { (value) in
self.tracker.log("Sink 2 recived value = \(value)")
}
} func update() {
update1 += 1
update2 += 1
update1 += 1
update2 += 1
}
...
Note the two @Published items: update1 and update2, that we’ll trigger later on from the DetailView by calling the update function from within a Button press. We saw a version of DetailView earlier, now here’s the collapsed code from inside of the navigation bar button.
请注意两个@Published项: update1和update2,稍后我们将通过在Button按键内调用update函数从DetailView中触发它们。 我们之前看到了DetailView的版本,现在这是导航栏按钮内部的折叠代码。
.navigationBarItems(
trailing: Button(action: {
DispatchQueue.main.async {
self.tracker.log("\n### Begin Update Cycle\n")
}
self.tracker.log("\n### Begin Update")
self.master.update()
self.tracker.log("### End Update\n")
}) {
Text("Update")
}
)
Notice the async event logging function we throw onto the stack at the beginning of the button action.
请注意,我们在按钮操作开始时将异步事件日志记录功能放到了堆栈上。
We’ll now subscribe to the same view model changes in our MasterView using onReceive.
现在,我们将使用onReceive在我们的MasterView中订阅相同的视图模型更改。
var body: some View {
tracker("List \(master.items.count) items") {
List {
...
}
.onReceive(master.objectWillChange) { () in
tracker.log("MasterView Changed")
}
.onReceive(master.$update1) { count in
tracker.log("MasterView Update 1 Received \(count)")
}
.onReceive(master.$update2) { count in
tracker.log("MasterView Update 2 Received \(count)")
}
...
}
}
Now we’re set. Let’s tap on the first item in our list and go to the DetailView.
现在开始了。 让我们点击列表中的第一项,然后进入DetailView。
DetailView.body #12 {
DetailContentView.init() #18
}
DetailContentView.body #18 {
ItemDateView.init() #19
DetailStateView.init() #20
DetailBoilerplateView.init() #21
}
ItemDateView.body #19 {
2020-09-09 00:06:24 +0000
}
DetailStateView.body #20 {
C3901189-FC7F-444C-A17F-C4C1781954CE
}
DetailBoilerplateView.body #21 {
You're viewing model #13.
}
DetailView onAppear
Straightforward enough. Our DetailView displayed a DetailContentView, which displayed the three elements we illustrated back in the beginning of this article. Just for the record, note that we’re displaying information from DetailViewModel instance #13.
直截了当。 我们的DetailView显示了DetailContentView ,其中显示了我们在本文开头介绍的三个元素。 仅作记录,请注意,我们正在显示来自DetailViewModel实例#13的信息。
Now let’s press that Update button and look at the log file generated when we actually call our update function. Here’s the first part…
现在,让我们按一下“更新”按钮,查看我们实际调用更新功能时生成的日志文件。 这是第一部分
### Begin Update
Sink 0 recived change notification
Sink 1 recived value = 1
Sink 0 recived change notification
Sink 2 recived value = 1
Sink 0 recived change notification
Sink 1 recived value = 2
Sink 0 recived change notification
Sink 2 recived value = 2
### End UpdateMasterView Changed
MasterView Update 1 Received 1
MasterView Changed
MasterView Update 2 Received 1
MasterView Changed
MasterView Update 1 Received 2
MasterView Changed
MasterView Update 2 Received 2### Begin Update Cycle...
Note that we updated each of our published items twice in the update function, and that we see our sink update notifications received immediately as we’d expect. (If you’ve used other reactive libraries like RxSwift, then this behavior is expected.) We also see our objectWillChange events sent prior to each, as we’d also expect from Apple’s formal definition of that value. The object will change. Then the object did change.
请注意,我们在更新功能中对每个已发布的项目进行了两次更新 ,并且我们看到接收器更新通知如预期般立即收到。 (如果您使用了RxSwift之类的其他React式库,那么这种行为是可以预期的。)我们还看到了objectWillChange事件在每个事件之前发送,正如我们对Apple对该值的正式定义所期望的那样。 对象将改变。 然后对象确实改变了。
One minor point I’d like to interject here is that you need to be careful in subscribing to an ObservableObject’s objectWillChange publisher. As the log shows, if you do this you’re going to be receiving events for each and every change to each and every published value.
我想在此指出的一个小问题是,您在订阅ObservableObject的 objectWillChange发布者时需要谨慎。 如日志所示,如果您这样做,您将收到每个发布值的每次更改的事件。
Moving on, note that our view’s onReceive change and data update notifications are received well after we’ve exited the update function (message logged at the end of our button handler).
继续,请注意, 退出更新功能(在按钮处理程序末尾记录的消息) 之后,将很好地接收到视图的onReceive更改和数据更新通知。
If we examine the call stack, we’d see that the same button gesture handling event that called our button press handler will in turn forward the appropriate events to our view receivers. This happens before the next update cycle occurs.
如果我们检查调用堆栈,我们将看到调用按钮按下处理程序的相同按钮手势处理事件将相应的事件转发给我们的视图接收器。 出现这种情况的发生在下一个更新周期之前 。
Speaking of which, we next see the update cycle message we placed on the main event queue. Note that the following update cycle occurs after our asynchronous event is logged, which in turn indicates that update cycles are asynchronous events that are kicked off the main run loop. (Which debugging confirms.)
说到这,我们接下来将看到放置在主事件队列上的更新周期消息。 请注意,在记录了异步事件之后 ,将发生以下更新周期,这又表明更新周期是从主运行循环开始的异步事件。 (经过调试确认。)
So lets examine that update cycle…
因此,让我们检查一下更新周期…
重建视图图 (Rebuilding the View Graph)
I’m including the remainder of the entire log here, just so you can see what cataclysmic forces our update cycle triggered.
我将整个日志的其余部分包括在这里,以便您可以看到是什么大灾变迫使我们的更新周期触发了。
...// BUILDING THE VISIBLE DETAIL VIEWDetailView.body #12 {
DetailContentView.init() #22
}
DetailContentView.deinit() #18
DetailContentView.body #22 {
ItemDateView.init() #23
DetailStateView.init() #24
DetailBoilerplateView.init() #25
}
ItemDateView.deinit() #19
DetailStateView.deinit() #20
DetailBoilerplateView.deinit() #21
ItemDateView.body #23 {
2020-09-09 00:35:14 +0000
}
DetailStateView.body #24 {
09E1696F-9C5B-441E-8CDA-7C499439E78B
}
DetailBoilerplateView.body #25 {
You're viewing model #13.
}// REBUILDING MASTER VIEWContentView.body #3 {
MasterView.init() #26
}
MasterView.body #26 {
List 2 items
}/// REBUILDING MASTER VIEW LIST / TABLEVIEW CELLSDetailView.init() #27
DetailViewModel.init() #28
ItemDateView.init() #29
ItemDateView.body #29 {
2020-09-09 00:35:15 +0000
}DetailView.init() #30DetailViewModel.init() #31
ItemDateView.init() #32
ItemDateView.body #32 {
2020-09-09 00:35:14 +0000
}
MasterView.onAppear // BUG!!!
MasterView.deinit() #11
ItemDateView.deinit() #14
ItemDateView.deinit() #17
DetailViewModel.deinit() #16
DetailView.deinit() #15// REBUILDING OUR VISIBLE DETAIL VIEWDetailView.body #30 {
DetailContentView.init() #33
}
DetailContentView.deinit() #22
DetailContentView.body #33 {
ItemDateView.init() #34
DetailStateView.init() #35
DetailBoilerplateView.init() #36
}
ItemDateView.deinit() #23
DetailStateView.deinit() #24
DetailBoilerplateView.deinit() #25
ItemDateView.body #34 {
2020-09-09 00:35:14 +0000
}
DetailStateView.body #35 {
09E1696F-9C5B-441E-8CDA-7C499439E78B
}
DetailBoilerplateView.body #36 {
You're viewing model #31.
}
First, our currently visible DetailView was regenerated. Then, and as we’ve seen before, anything dependent on MasterViewModel was rebuilt, including the visible elements of our tableview.
首先,重新生成了我们当前可见的DetailView 。 然后,正如我们之前所见,所有依赖MasterViewModel的东西都被重建, 包括 表格视图的可见元素。
And then our DetailView was called again to display our new information.
然后再次调用我们的DetailView来显示我们的新信息。
Only now it’s a different DetailView. With a different DetailViewModel!
只是现在它是一个不同的 DetailView。 使用 不同的 DetailViewModel!
Remember back when I mentioned that we were displaying data from DetailViewModel instance #13? Well, now we’re displaying data from DetailView #30 and DetailViewModel instance #31.
回想一下,当我提到我们正在显示DetailViewModel实例中的数据时 #13 ? 好了,现在我们显示的是DetailView #30和DetailViewModel实例中的数据 #31 。
SwiftUI, in rebuilding the tableview, rebuilt our first tableview cell, which instantiated a new DetailView, which instantiated a new DetailViewModel. It then handed off the new view to the hosting controller for the detail screen, and we’re now seeing results from that view and that view model.
SwiftUI在重建表视图时,重建了我们的第一个表视图单元,该单元实例化了一个新的DetailView ,该实例化了一个新的DetailViewModel 。 然后,它将新视图交给详细信息屏幕的托管控制器,现在我们可以看到该视图和该视图模型的结果。
Our “view” that was defining and managing our DetailView screen has been replaced.
定义和管理DetailView屏幕的“视图”已被替换。
And that’s the one of the reasons why hanging class-based view models off your views using @ObservedObject is problematic.
这就是为什么使用@ObservedObject将基于类的视图模型挂起视图的原因之一。
If you’re not exceedingly careful, they can and will be killed when you least expect them to be killed. Along with any internal state or data they might have contained.
如果您不太谨慎,当您最不希望它们被杀死时,它们可能会被杀死。 以及它们可能包含的任何内部状态或数据。
Your views are not your views.
您的意见不是您的意见。
地方政府 (Local State)
Let’s cherry pick something else interesting from the last log.
让我们从上一个日志中挑选出一些有趣的东西。
...
DetailStateView.body #24 {
09E1696F-9C5B-441E-8CDA-7C499439E78B
}
...
DetailStateView.body #35 {
09E1696F-9C5B-441E-8CDA-7C499439E78B
}
...
And here’s the corresponding DetailStateView.
这是对应的DetailStateView 。
struct DetailStateView: View {
@State var uuid = UUID()
let tracker = InstanceTracker("DetailStateView")
var body: some View {
tracker("\(uuid)") {
Text("\(uuid)")
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
Note that our state variable persisted across view instances when the view graph was updated, keeping the same 09E1xxx UUID number.
请注意,在更新视图图形时,我们的状态变量在视图实例之间持久存在,并保持相同的09E1xxx UUID号。
So why did @State persist but our ObservedObject did not?
那么为什么@State仍然存在,而我们的ObservedObject却没有?
Primarily because SwiftUI is managing the storage for @State. As long as it can make sense of the view graph when diffing the new state against the old state, it will do its best to track state variables and make sure they’re constant across instantiations.
主要是因为SwiftUI正在管理@State的存储。 只要在区分新状态和旧状态时可以理解视图图,它就会尽力跟踪状态变量,并确保它们在实例化过程中保持不变。
On the other hand, objects attached to @ObservedObject are NOT managed by SwiftUI and it’s up to the application to manage their storage and references to them.
另一方面,附加到@ObservedObject的对象是 通过SwiftUI 不管理,它是由应用程序来管理他们的存储和对它们的引用。
All of which makes using @ObservedObject problematic. When do you assign values to them? How do you manage references? How do you keep update cycles from taking down your entire world?
所有这些使使用@ObservedObject成为问题。 什么时候给它们赋值? 您如何管理参考? 您如何避免更新周期破坏整个世界?
欢迎StateObject (Welcome StateObject)
In SwiftUI 2.0 Apple provided a new property wrapper designed to address some of these issues: @StateObject. Let’s look at an example and see how it solves some of our problems with a slightly redesigned DetailView.
Apple在SwiftUI 2.0中提供了一个新的属性包装器,旨在解决其中的一些问题: @StateObject。 让我们看一个示例,看看它如何通过稍微重新设计的DetailView解决了我们的一些问题。
struct DetailView: View { @StateObject var model: DetailViewModel
@EnvironmentObject var master: MasterViewModel// init(item: Item) {
// self.model = DetailViewModel(item: item)
// }
We replace our @ObservedObject with @StateObject and we now pass our view model directly into it via our navigation link.
我们将@ObservedObject替换为@StateObject ,然后通过导航链接将视图模型直接传递给它。
NavigationLink(
destination: DetailView(model: DetailViewModel(item: item))
) {
ItemDateView(item: item)
}
Looking at the code, one would certainly be justified in asking just how this solves our performance issues. Are we not once more simply instantiating an instance of a view model each and every time we display a new cell in our list?
看一下代码,一定会问这是如何解决我们的性能问题的。 我们是否每次在列表中显示一个新的单元格时都不再简单地实例化视图模型的实例?
Actually, we’re not. Let’s look at the initializer for @StateObject.
实际上,我们不是。 让我们看一下@StateObject的初始化程序 。
@inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
The wrappedValue is actually encased in an @autoclosure and that closure isn’t called until something tries to access the wrapped value. At that point in time our DetailViewModel will be instantiated using the captured item value.
wrapdValue实际上被包装在@autoclosure中 ,直到某些东西试图访问包装的值时才调用该闭包。 到那时,我们的DetailViewModel将使用捕获的项目值实例化。
Here’s a log after adding one item.
这是添加一项后的日志。
MasterView Changed
MasterView.body #3 {
List 1 items
}
DetailView.init() #6
ItemDateView.init() #7
ItemDateView.body #7 {
2020-09-09 22:23:48 +0000
}
Note that we instantiated a DetailView, but not a DetailViewModel. Let’s now navigate to that page.
请注意,我们实例化了一个DetailView ,但没有实例化一个DetailViewModel 。 现在让我们导航到该页面。
DetailViewModel.init() #8
DetailView.body #6 {
DetailContentView.init() #9
}
DetailContentView.body #9 {
ItemDateView.init() #10
DetailStateView.init() #11
DetailBoilerplateView.init() #12
}
ItemDateView.body #10 {
2020-09-09 22:23:48 +0000
}
DetailStateView.body #11 {
4C61F292-8A47-480C-BFCF-92591AD49928
}
DetailBoilerplateView.body #12 {You're viewing model #8.
}
DetailView.onAppear
Now we see our DetailViewModel instantiated, along with our familiar update cycle. Interestingly, if we navigate back to the main list, and then once again tap on our single list item, SwiftUI will create a brand new DetailViewModel.
现在,我们看到了实例化的DetailViewModel以及我们熟悉的更新周期。 有趣的是,如果我们导航回到主列表,然后再次点击单个列表项,SwiftUI将创建一个全新的DetailViewModel。
That makes sense if you stop to think about it, as that entire screen went out of scope when we left it, which eliminated any managed storage associated with that portion of the view graph.
That makes sense if you stop to think about it, as that entire screen went out of scope when we left it, which eliminated any managed storage associated with that portion of the view graph.
There are still several drawbacks to @StateObject, one being that we’ve now exposed our view model to our caller, and the second that while we’re not creating an entire object instance for each and every item in our list, we’re still creating a closure for each, and that’s still a reference type.
There are still several drawbacks to @StateObject, one being that we've now exposed our view model to our caller, and the second that while we're not creating an entire object instance for each and every item in our list, we're still creating a closure for each, and that's still a reference type.
If either of those issues is a concern, one could simply wrap the destination view as follows:
If either of those issues is a concern, one could simply wrap the destination view as follows:
struct WrappedDetailView: View {
let item: Item
var body: some View {
return DetailView(model: DetailViewModel(item: item))
}
}
And of course, using @StateObject means that your minimum deployment target on iOS is now iOS 14.
And of course, using @StateObject means that your minimum deployment target on iOS is now iOS 14.
Completion Block (Completion Block)
It took awhile to get here and, in a way, we’ve only just started on our SwiftUI journey.
It took awhile to get here and, in a way, we've only just started on our SwiftUI journey.
But now you should have a much better understanding of just what’s going on in your SwiftUI application, where performance bottlenecks could occur, and where potential bugs may lie in wait, ready to take down your entire application.
But now you should have a much better understanding of just what's going on in your SwiftUI application, where performance bottlenecks could occur, and where potential bugs may lie in wait, ready to take down your entire application.
Perhaps more to the point, you’ve seen first hand how SwiftUI can and will manipulate the views on which your application is based. And what happens if you assume that they persist like our old friend, the UIView.
Perhaps more to the point, you've seen first hand how SwiftUI can and will manipulate the views on which your application is based. And what happens if you assume that they persist like our old friend, the UIView .
I’ve spent a good portion of this article telling you what not to do.
I've spent a good portion of this article telling you what not to do.
- Don’t assume views persist. Don't assume views persist.
- Don’t do heavy lifting during view initialization and in your view bodies. Don't do heavy lifting during view initialization and in your view bodies.
- Don’t assume you know when your views will be created. Don't assume you know when your views will be created.
- Don’t assume you know just when your view bodies will be called. Don't assume you know just when your view bodies will be called.
- Don’t assume you know just how often your view bodies will be called. Don't assume you know just how often your view bodies will be called.
Avoid @ObservedObject.
Avoid @ObservedObject.
Then again, you can’t build a great application on what not to do. And I’ve tried to help there as well.
Then again, you can't build a great application on what not to do. And I've tried to help there as well.
- Keep views lightweight and performant. Keep views lightweight and performant.
- Keep state as low in the hierarchy as possible. Keep state as low in the hierarchy as possible.
- Use distributed state and micro services where possible. Use distributed state and micro services where possible.
Use @StateObject when needed (and if possible).
Use @StateObject when needed (and if possible).
More articles are coming on how to properly architect SwiftUI applications that keep all of these principles in mind.
More articles are coming on how to properly architect SwiftUI applications that keep all of these principles in mind.
But for now, and if you remember nothing else, please remember this...
But for now, and if you remember nothing else, please remember this...
Don’t treat SwiftUI like UIKit.
Don't treat SwiftUI like UIKit.
It’s not.
不是。
翻译自: https://medium.com/@michaellong/deep-inside-views-state-and-performance-in-swiftui-d23a3a44b79
swift和swiftui