-
- 1.1. Recomputed properties
- 1.2. Going back to the source
- 1.3. Initialization logic
- 1.4. Dedicated model logic
- 1.5. Conclusion
1. 避免在SwiftUI视图中重新计算值
通常,使用计算属性是建模我们想在需要时按需创建的特定于视图的数据的一种好方法——特别是在SwiftUI视图中,因为每个这样的视图都已经使用计算属性(body)来实现其实际UI。
例如,这里我们使用两个计算属性来确定UserSessionView应该基于应用的当前LoginState呈现什么标题和按钮文本:
struct UserSessionView: View {
var buttonAction: () -> Void
@Environment(\.loginState) private var state
var body: some View {
VStack {
Text(title)
.font(.headline)
.foregroundColor(.white)
Button(buttonText, action: buttonAction)
.padding()
.background(Color.white)
.cornerRadius(10)
}
.padding()
.background(Color.blue)
.cornerRadius(15)
}
private var title: String {
switch state {
case .loggedIn(let user):
return "Welcome back, \(user.name)!"
case .loggedOut:
return "Not logged in"
}
}
private var buttonText: String {
switch state {
case .loggedIn:
return "Log out"
case .loggedOut:
return "Log in"
}
}
}
要了解上面使用的Environment属性包装器的更多信息,请查看我的SwiftUI状态管理系统指南。
实现SwiftUI视图,使用多个计算属性,往往是一个很好的方法,因为它可以让我们保持我们的身体实现尽可能的简单,因为它给了我们清晰的了解我们如何计算给定的视图将显示不同的内容。
1.1. Recomputed properties
但是,我们也必须记住,计算属性只是——计算——没有缓存形式或其他类型的内存存储涉及,这意味着每个这样的值将总是在每次被访问时重新计算。
这在我们的第一个例子中不是问题,因为我们的每个属性都可以用常数(或O(1))的时间复杂度快速计算出来。然而,现在让我们看另一个例子,它在性能特征方面是完全不同的,因为我们现在是通过对集合排序来计算属性:
struct RemindersList: View {
var items: [Reminder.ID: Reminder]
var body: some View {
List(sortedItems) { reminder in
...
}
}
private var sortedItems: [Reminder] {
items.values.sorted(by: {
$0.dueDate < $1.dueDate
})
}
}
一方面,如果传递的items字典最终包含大量的项,那么上述实现可能会有很大的问题,因为每次我们访问sortedItems属性时,该集合都会被重新排序。 但另一方面,它是一个只能从视图body内部访问的私有属性,因为视图没有任何可变状态,我们的属性不太可能经常被访问。
然而,如果我们要在视图中添加任何类型的本地状态,这种情况可能很快就会改变——例如,允许用户使用内联TextField添加一个新的提醒:
struct RemindersList: View {
var items: [Reminder.ID: Reminder]
var newItemHandler: (Reminder) -> Void
@State private var newItemName = ""
var body: some View {
VStack {
List(sortedItems) { reminder in
...
}
HStack {
TextField("Add a new reminder", text: $newItemName)
Button("Add") {
newItemHandler(Reminder(name: newItemName))
}
.disabled(newItemName.isEmpty)
}
.padding()
}
}
private var sortedItems: [Reminder] {
items.values.sorted(by: {
$0.dueDate < $1.dueDate
})
}
}
现在,每当用户在文本字段中输入一个新字符时,sortedItems属性将被调用,items字典将被重新排序。虽然最初这可能不会导致任何明显的性能下降,但这是一种非常低效的实现,而且很可能在某些方面导致问题,特别是对于有大量提醒的用户。
重要的是要记住,尽管SwiftUI并使用基于类型dif算法来确定潜在的观点重新划分为每个状态的变化,并以确保我们的UI仍是高性能,这不是魔术——如果我们编写的代码非常低效,没有什么SwiftUI能做。
1.2. Going back to the source
那么我们如何解决这个问题呢?一种方法是回到项目数据的根源,并对其进行更新,以便在最前面执行必要的排序。 这样做有两个主要的好处——一,它允许我们执行一次操作,而不是在每次视图更新期间,二,它将本质上是模型级操作的内容移动到实际的模型层。大赢!如果我们使用Combine通过某种形式的模型控制器来加载我们的项目,就会看到这样的效果:
func loadItems() -> AnyPublisher<[Item], Error> {
controller
.loadReminders()
.map { items in
items.values.sorted(by: {
$0.dueDate < $1.dueDate
})
}
...
.eraseToAnyPublisher()
}
有了上面的改变,我们现在可以简单地让我们的RemindersList视图接受一个预先排序的提醒数组,我们可以按原样渲染:
struct RemindersList: View {
var items: [Reminder]
...
var body: some View {
VStack {
List(items) { reminder in
...
}
...
}
}
}
然而,尽管上述方法在许多情况下肯定更可取,但我们将核心模型集合建模为字典而不是使用数组可能有很好的理由,更改它可能不那么简单,甚至根本不可行。所以,让我们也探讨一些其他的选择。
1.3. Initialization logic
我们可以在视图级本身解决问题的一种方法是仍然让我们的RemindersList视图接受一个字典,就像以前一样,但是在视图的初始化器中执行排序,而不是使用一个计算属性-例如:
struct RemindersList: View {
var items: [Reminder]
var newItemHandler: (Reminder) -> Void
init(items: [Reminder.ID: Reminder],
newItemHandler: @escaping (Reminder) -> Void) {
self.items = items.values.sorted(by: {
$0.dueDate < $1.dueDate
})
self.newItemHandler = newItemHandler
}
@State private var newItemName = ""
...
}
有了上面的改变,我们现在只对每个RemindersList实例排序一次我们的集合,而不是在每次击键之后,而不需要改变我们的视图创建的方式或我们的应用程序的数据管理方式。 因此,虽然我的一般建议是让初始化器专注于简单的设置工作,而不是执行数据突变,但在这种情况下,这是我们可能愿意做出的一种权衡。
不过,值得记住的是,如果RemindersList视图的父视图进行了更新(或者更具体地说,如果父视图的body属性被重新求值),那么很可能会创建视图的一个新实例,这意味着我们将再次执行item排序操作。
基本上,在SwiftUI视图中编写代码时,几乎不可能完全控制代码何时以及如何执行。毕竟,像SwiftUI这样的声明式UI框架设计的一个核心部分是,该框架负责为我们编排所有的UI更新。
因此,如果我们想改善对实际模型逻辑生命周期的控制,那么更好的方法可能是将该逻辑从视图实现中移出,放到我们完全控制的对象中。
1.4. Dedicated model logic
一种方法是使用视图模型之类的东西来封装与处理项目数组相关的逻辑。如果我们把那个视图模型变成一个ObservableObject,那么我们就可以很容易地观察它,并在SwiftUI视图中连接到它:
class RemindersListViewModel: ObservableObject {
@Published private(set) var items: [Reminder]
init(items: [Reminder.ID: Reminder]) {
self.items = items.values.sorted(by: {
$0.dueDate < $1.dueDate
})
}
func addItem(named name: String) {
...
}
}
有了上面的功能,我们现在可以大大简化我们的视图,我们也可以自由选择我们想要管理上面的RemindersListViewModel的方式,而不必考虑视图更新和其他SwiftUI实现细节:
struct RemindersList: View {
@ObservedObject var viewModel: RemindersListViewModel
@State private var newItemName = ""
var body: some View {
VStack {
List(viewModel.items) { reminder in
...
}
HStack {
TextField("Add a new reminder", text: $newItemName)
Button("Add") {
viewModel.addItem(named: newItemName)
}
.disabled(newItemName.isEmpty)
}
.padding()
}
}
}
非常好!当然,这并不意味着我们应用程序中的每个视图现在都需要一个视图模型。在这个特殊的情况下,视图模型被证明是我们问题的一个很好的解决方案,因为它使我们能够将与视图相关的模型逻辑从视图层次结构本身中移出(这也极大地提高了代码的可测试性)。
1.5. Conclusion
次更新SwiftUI视图时重新计算一些与视图相关的值通常不是问题。毕竟,这是每个视图的body属性的工作方式,只要这些计算能够快速发生(理想情况下,具有恒定的时间复杂度),那么我们就不太可能遇到任何类型的主要性能问题。
然而,情况并非总是如此,有时我们可能需要特别小心在视图中使用模型数据的方式,特别是当这样做涉及到任何可能会降低整体UI性能的繁重操作时。