在SwiftUI中使用参数初始化@StateObject

153

我想知道当前(在提问时,第一个 Xcode 12.0 Beta)是否有一种方法可以使用来自初始化程序的参数初始化@StateObject

更具体地说,这段代码片段运行良好:

struct MyView: View {
  @StateObject var myObject = MyObject(id: 1)
}
但是这并不:
struct MyView: View {
  @StateObject var myObject: MyObject

  init(id: Int) {
    self.myObject = MyObject(id: id)
  }
}

据我所了解,@StateObject 的作用是使视图成为对象的所有者。我目前使用的解决方法是像这样传递已经初始化的 MyObject 实例:

从我的理解来看,@StateObject 的作用是将对象所有权交给视图。我目前采用的解决办法是通过以下方式传递已初始化的 MyObject 实例:

struct MyView: View {
  @ObservedObject var myObject: MyObject

  init(myObject: MyObject) {
    self.myObject = myObject
  }
}

但据我了解,现在创建对象的视图拥有该对象,而非当前视图。

谢谢。

12个回答

146

这是一个解决方案的演示。已经使用Xcode 12+进行了测试。

class MyObject: ObservableObject {
    @Published var id: Int
    init(id: Int) {
        self.id = id
    }
}

struct MyView: View {
    @StateObject private var object: MyObject
    init(id: Int = 1) {
        _object = StateObject(wrappedValue: MyObject(id: id))
    }

    var body: some View {
        Text("Test: \(object.id)")
    }
}

来自苹果公司(针对所有像 @user832 这样的用户):

confirmation


37
在 @StateObject 的文档中,它表示“不直接调用此初始化器”。 - user832
4
每次重新构建视图时都会调用init()函数。但是,State和StateObject仅在第一次初始化它们的对象。 - Brett
1
是的,这是一个矛盾的帖子。对于某些人有效,而对于其他人则无效,根据投票结果是2比1...所以如果你在寻找通用的复制粘贴解决方案 - 这不是那种情况 - 请谨慎尝试。 - Asperi
21
这是在WWDC21的SwiftUI数字休息室中提出的,并在那种情况下得到了SwiftUI团队的认可。SwiftUI-Lab.com对数字休息室的精彩总结记录在这里:https://www.swiftui-lab.com/random-lessons#data-10 当然,是否采用这种软性认可取决于您和您的项目,您是否足够放心地按照这种方法进行。 - Alex Fringes
21
这里有一个非常重要的警告:wrappedValue 是一个闭包!如果你在一行上创建了你的视图模型,然后在另一行上将其传递到 wrappedValue 中,你将会悄悄地破坏 StateObject,并在每次更新时得到一个新的视图模型实例。这绝对会让我措手不及。 - Selali Adobor
显示剩余5条评论

55

@Asperi所提供的答案应该被避免,因为苹果在他们的StateObject文档中这样说。

不要直接调用此初始化程序。而是在View、App或Scene中声明一个带有@StateObject属性的属性,并提供初始值。

苹果试图在幕后进行优化,请不要与系统斗争。

只需创建一个具有“Published”值的ObservableObject来代替您想要使用的参数。然后使用`.onAppear()`设置它的值,SwiftUI将完成剩下的工作。

代码:

class SampleObject: ObservableObject {
    @Published var id: Int = 0
}

struct MainView: View {
    @StateObject private var sampleObject = SampleObject()
    
    var body: some View {
        Text("Identifier: \(sampleObject.id)")
            .onAppear() {
                sampleObject.id = 9000
            }
    }
}

37
如果SampleObject在初始化时需要一个参数怎么办?另外,在SwiftUI 2.0中,.onAppear有点混乱,可能会被调用多次。 - Brett
6
我需要找到我读过这个内容的地方,但我认为 @Asperi 的答案是正确的,因为 views init 可能会被多次调用,但 StateObject 不会被覆盖,它只在第一次设置。 - Brett
3
谢谢。我已经尝试过了,也看到正确的行为了。视图被重新创建了,但是StateObject没有被覆盖。我只是希望在未来的SwiftUI版本中这一点不会改变。 - user3847320
6
“onAppear”解决方案适用于简单的用例,但如果“SampleObject”更复杂并且其.init中需要参数(例如注入的服务),则不适用。@Brett,请分享若使用自定义.init是正确的方法,则应使用Apple文档。这将解决许多变通方法。 - Jan
11
这个问题是在WWDC21的休息室中提出的,答案似乎与文档中的警告相矛盾:“[传递给StateObject.init(wrappedValue:)的对象]只会在视图生命周期开始时创建并保持活动状态。 StateObject的包装值是一个自动闭包,在视图生命周期的开始只调用一次。这也意味着SwiftUI将在首次创建计划值时捕获其值;[...] 如果您的视图标识未更改但传递了不同的[对象],则SwiftUI将不会注意到。” - halleygen
显示剩余5条评论

47

简短回答

StateObject的下一个初始化方法是: init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)。这意味着StateObject将在正确的时间创建对象的实例-在第一次运行body之前。但这并不意味着您必须像在View中一样在一行中声明该实例,例如:@StateObject var viewModel = ContentViewModel()

我找到的解决方案是传递一个闭包,并允许 StateObject 创建一个对象的实例。这个解决方案非常好用。有关更多详细信息,请阅读下面的长篇回答

class ContentViewModel: ObservableObject {}

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel
    
    init(viewModel: @autoclosure @escaping () -> ContentViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel())
    }
}

struct RootView: View {
    var body: some View {
        ContentView(viewModel: ContentViewModel())
    }
}

无论 RootView 创建多少次它的 bodyContentViewModel 的实例只会有一个。
通过这种方式,您可以初始化具有参数的 @StateObject 视图模型。
长答案 @StateObject @StateObject 在第一次运行 body 之前创建一个值的实例。(SwiftUI 中的数据基础知识)。并且在整个视图生命周期中保持此值的单个实例。您可以在 body 之外的某个位置创建视图的实例,并且您将看到 ContentViewModelinit 不会被调用。请参见下面示例中的 onAppear
struct ContentView: View {
    @StateObject private var viewModel = ContentViewModel()
}

struct RootView: View {
    var body: some View {
        VStack(spacing: 20) {
        //...
        }
        .onAppear {
            // Instances of ContentViewModel will not be initialized
            _ = ContentView()
            _ = ContentView()
            _ = ContentView()

            // The next line of code
            // will create an instance of ContentViewModel.
            // Buy don't call body on your own in projects :)
            _ = ContentView().view
        }
    }
}

因此,将创建实例的任务委托给StateObject非常重要。

为什么不应该使用StateObject(wrappedValue:)与实例一起使用

让我们考虑一个例子,当我们通过传递viewModel实例来创建StateObject的实例_viewModel = StateObject(wrappedValue: viewModel)时。当根视图触发body的额外调用时,viewModel上的新实例将被创建。如果您的视图是整个屏幕视图,那么这可能很好地工作。尽管如此,最好不要使用这种解决方案。因为您永远不知道父视图何时以及如何重新绘制其子视图。

final class ContentViewModel: ObservableObject {
    @Published var text = "Hello @StateObject"
    
    init() { print("ViewModel init") }
    deinit { print("ViewModel deinit") }
}

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel
    
    init(viewModel: ContentViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel)
        print("ContentView init")
    }

    var body: some View { Text(viewModel.text) }
}

struct RootView: View {
    @State var isOn = false
    
    var body: some View {
        VStack(spacing: 20) {
            ContentView(viewModel: ContentViewModel())
            
            // This code is change the hierarchy of the root view.
            // Therefore all child views are created completely,
            // including 'ContentView'
            if isOn { Text("is on") }
            
            Button("Trigger") { isOn.toggle() }
        }
    }
}

我点击了“Trigger”按钮3次,这是在Xcode控制台中的输出:
ViewModel init
ContentView init
ViewModel init
ContentView init
ViewModel init
ContentView init
ViewModel deinit
ViewModel init
ContentView init
ViewModel deinit
如您所见,ContentViewModel的实例被创建了多次。这是因为当根视图层次结构发生更改时,其中的所有内容都会从头开始创建,包括ContentViewModel。无论您将其设置为子视图中的@StateObject,只要您在根视图中调用init与根视图更新body的次数一样多,就会创建多个实例。

使用闭包

因为StateObject在init中使用闭包 - init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType),我们也可以使用这个方法并传递闭包。代码与上一节完全相同(ContentViewModelRootView),唯一的区别是在将闭包作为ContentView的init参数时。
struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel
    
    init(viewModel: @autoclosure @escaping () -> ContentViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel())
        print("ContentView init")
    }

    var body: some View { Text(viewModel.text) }
}

在点击“触发器”按钮3次后,输出结果如下:

ContentView init
ViewModel init
ContentView init
ContentView init
ContentView init

你可以看到只创建了一个ContentViewModel实例。此外,ContentViewModel是在ContentView之后创建的。

顺便说一句,实现同样效果最简单的方法是将属性设置为internal/public并删除init:

struct ContentView: View {
    @StateObject var viewModel: ContentViewModel
}

结果是一样的。但在这种情况下,viewModel 不能作为私有属性。

尽管这个解决方案可行,但当你向后滑动时它会创建一个问题,因为它重新实例化了视图和视图模型。当我用 NavigationStack 替换 NavigationView 时,问题得到了解决。但是 NavigationStack 只在 iOS16.0 中可用。 - Christos Chadjikyriacou
如果我的根视图是“@main”作为入口点怎么办?它符合“app”的要求,因此我没有“body”来应用init。 - David.C
@David.C 对于应用程序根视图,可能只需使用 @StateObject var viewModel = AppRootViewModel(),不需要任何 init 和闭包。 - Andrew Bogaevskyi

6

我想我找到了一个方法来控制被@StateObject包装的视图模型的实例化。如果您不把视图模型设置为私有,您可以使用合成的成员初始化器,并且在那里您将能够轻松地控制它的实例化。如果您需要一种公共的实例化视图的方式,您可以创建一个工厂方法,该方法接收您的视图模型依赖项并使用内部合成的初始化器。

import SwiftUI

class MyViewModel: ObservableObject {
    @Published var message: String

    init(message: String) {
        self.message = message
    }
}

struct MyView: View {
    @StateObject var viewModel: MyViewModel

    var body: some View {
        Text(viewModel.message)
    }
}

public func myViewFactory(message: String) -> some View {
    MyView(viewModel: .init(message: message))
}

我没有看到在上面的例子中初始化viewModel,你能否再详细解释一下,或者添加缺失的代码(如果有的话)? - Brett
查看 func myViewFactory 内部 - cicerocamargo
是的,我看到了,但是我的ViewFactory在哪里被调用了? - Brett
2
无论何时需要调用MyView(params),您都可以调用myViewFactory(params) - cicerocamargo

4

就像@Mark指出的那样,您不应该在初始化期间处理@StateObject。这是因为@StateObject在View.init()之后略微在body被调用之前/之后初始化。

我尝试了许多不同的方法来从一个视图传递数据到另一个视图,并提出了一种适用于简单和复杂视图/视图模型的解决方案。

版本

Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)

这个解决方案可以在iOS14.0及以上版本中使用,因为你需要使用 .onChange() 视图修饰符。这个例子是用Swift Playgrounds编写的。如果您需要一个类似于 onChange 的修饰符来支持较低版本的iOS系统,则需要自己编写一个修饰符。

主视图

主视图包含一个@StateObject viewModel,处理所有视图逻辑,例如按钮点击和“数据”(testingID: String)->请查看ViewModel。

struct TestMainView: View {
    
    @StateObject var viewModel: ViewModel = .init()
    
    var body: some View {
        VStack {
            Button(action: { self.viewModel.didTapButton() }) {
                Text("TAP")
            }
            Spacer()
            SubView(text: $viewModel.testingID)
        }.frame(width: 300, height: 400)
    }
    
}

主视图模型 (ViewModel)

视图模型发布一个 testID: String?。这个testID可以是任何类型的对象(例如配置对象等),对于本例子,它只是一个在子视图中需要使用的字符串。

final class ViewModel: ObservableObject {
    
    @Published var testingID: String?
    
    func didTapButton() {
        self.testingID = UUID().uuidString
    }
    
}

因此,通过点击按钮,我们的ViewModel将更新testID。我们还希望在我们的SubView中使用此testID,如果它发生更改,我们也希望我们的SubView能够识别并处理这些更改。通过ViewModel @Published var testingID,我们能够向我们的视图发布更改。现在让我们来看一下我们的SubViewSubViewModelSubView 因此,SubView具有自己的@StateObject来处理其自己的逻辑。它与其他视图和ViewModel完全分开。在此示例中,SubView仅显示来自其MainViewtestID。但请记住,它可以是任何类型的对象,例如数据库请求的预设和配置。
struct SubView: View {
    
    @StateObject var viewModel: SubviewModel = .init()
    
    @Binding var test: String?
    init(text: Binding<String?>) {
        self._test = text
    }
    
    var body: some View {
        Text(self.viewModel.subViewText ?? "no text")
            .onChange(of: self.test) { (text) in
                self.viewModel.updateText(text: text)
            }
            .onAppear(perform: { self.viewModel.updateText(text: test) })
    }
}

为了“连接”我们的MainViewModel发布的testingID,我们用@Binding初始化我们的SubView。现在我们在SubView中有相同的testingID。但是我们不想直接在视图中使用它,而是需要将数据传递到我们的SubViewModel中,记住我们的SubViewModel是一个@StateObject,用于处理所有的逻辑。并且我们不能在视图初始化期间将值传递给@StateObject,就像一开始我写的那样。此外,如果我们MainViewModel中的数据 (testingID: String) 发生更改,我们的SubViewModel应该识别和处理这些更改。

因此,我们使用两个ViewModifiers

onChange

.onChange(of: self.test) { (text) in
                self.viewModel.updateText(text: text)
            }

onChange修改器订阅我们@Binding属性的更改。因此,如果它发生了更改,这些更改将传递给我们的SubViewModel。请注意,您的属性需要是Equatable的。如果您传递了一个更复杂的对象,比如一个Struct,请确保在您的Struct实现此协议。

onAppear

我们需要onAppear来处理“第一次初始数据”,因为onChange不会在视图第一次初始化时触发。它仅用于更改

.onAppear(perform: { self.viewModel.updateText(text: test) })

好的,这里是SubViewModel,我想没有更多需要解释的了。

class SubviewModel: ObservableObject {
    
    @Published var subViewText: String?
    
    func updateText(text: String?) {
        self.subViewText = text
    }
}

现在,您的数据同步在MainViewModelSubViewModel之间。这种方法适用于具有许多子视图及其子视图等的大型视图。它还可以使您的视图和相应的viewModel具有高重用性。

工作示例

Github上的Playground: https://github.com/luca251117/PassingDataBetweenViewModels

附加说明

为什么我使用onAppearonChange而不是只有onReceive: 似乎用这两个修饰符替换onReceive会导致连续的数据流多次触发SubViewModel updateText。如果需要将数据流传递给表示层,这可能是可以接受的,但如果要处理网络调用等操作,则可能会导致问题。这就是为什么我更喜欢“两个修饰符法”的原因。

个人建议:请不要在对应视图范围之外修改stateObject。即使某种方式可能是可行的,但这并不是其设计初衷。


3
@cicerocamargo的回答是一个很好的建议。我在我的应用程序中也遇到了相同的问题,试图找出如何在@StateObject视图模型中注入依赖项,并在经过多次测试后得出了相同的答案。这样,无论在哪种情况下,视图模型只会被实例化一次。
class MyViewModel: ObservableObject {
   @Published var dependency: Any

   init(injectedDependency: Any) {
       self.dependency = injectedDependency
   }
}

struct  MyView: View {
    @StateObject var viewModel: MyViewModel
    
    var body: some View {
       // ...
    } 
}

struct MyCallingView: View {
    var body: some View {
        NavigationLink("Go to MyView",
            destination: MyView(viewModel: MyViewModel(injectedDependency: dependencyValue)))
    }
}


唯一需要记住的是,视图模型的实例化应与视图的实例化相一致。如果我们将调用视图的代码更改为以下内容:
struct MyCallingView: View {
    var body: some View {
        let viewModel = MyViewModel(injectedDependency: dependencyValue)
        NavigationLink("Go to MyView",
            destination: MyView(viewModel: viewModel))
    }
}

如果这样写,编译器将无法优化代码,每次 MyCallingView 被使无效并需要重新绘制时都会实例化viewModel。好处是,即使每次都进行实例化,也只有原始实例被使用。

1
如果MyView需要一个自定义的初始化方法,这将会出现问题。 - Morgz
2
@Morgz - 嗯,不完全是。首先,视图真的需要自定义初始化程序吗?大多数情况下,实际上是ViewModel需要自定义初始化程序,并且您想要注入到View中的任何内容都应该注入到ViewModel中。如果某些东西确实需要直接注入到View中,请使用包装技巧(这将保留功能,仅使用初始ViewModel实例,但确实会创建多个ViewModel实例)init(injected: Any, viewModel: MyViewModel) {_viewModel = StateObject(wrappedValue: viewModel)} - mike

2

根据Andrew Bogaevskyi的答案,我创建了一个扩展到StateObjectinit,它模拟了一个闭包并避免每次创建一个新的StateObject实例。

extension StateObject {
    @inlinable init(_ value: @escaping () -> ObjectType) where ObjectType : ObservableObject {
        self.init(wrappedValue: value())
    }
}

这是测试代码

final class ContentViewModel: ObservableObject {
    @Published var text = "Hello @StateObject"
    
    init() { print("ViewModel init")}
    deinit { print("ViewModel deinit") }
}

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel
    
    init() {
        _viewModel = StateObject {
            ContentViewModel()
        }
        print("ContentView init")
    }

    var body: some View { Text(viewModel.text) }
}

struct RootView: View {
    @State var isOn = false
    
    var body: some View {
        VStack(spacing: 20) {
            ContentView()
            
            // This code is change the hierarchy of the root view.
            // Therefore all child views are created completely,
            // including 'ContentView'
            if isOn { Text("is on") }
            
            Button("Trigger") { isOn.toggle() }
        }
    }
}

extension StateObject {
    @inlinable init(_ value: @escaping () -> ObjectType) where ObjectType : ObservableObject {
        self.init(wrappedValue: value())
    }
}

输出结果为:

ContentView init
ViewModel init
ContentView init
ContentView init
ContentView init
ContentView init
ContentView init

1

非常好的答案。

现在,我发现在某些情况下,正确使用@StateObject可能会很棘手,比如处理需要在用户浏览UI时惰性检索信息的网络请求。

这是我喜欢使用的一种模式,特别是当屏幕(或屏幕层次结构)由于其关联的检索成本而应该惰性地呈现数据时。

它的步骤如下:

  • 主屏幕保存子屏幕的模型。
  • 每个模型跟踪其显示状态以及是否已经加载了信息。这有助于避免重复昂贵的操作,比如网络调用。
  • 子屏幕依赖于模型并检查显示状态以显示加载视图或呈现最终信息/错误。

以下是屏幕分解:

enter image description here

匆忙之间? 这是项目:

https://github.com/tciuro/StateObjectDemo

主屏幕(ContentView):

import SwiftUI

struct ContentView: View {
    @StateObject private var aboutModel = AboutModel()
    
    var body: some View {
        NavigationView {
            List {
                Section {
                    NavigationLink(destination: AboutView(aboutModel: aboutModel)) {
                        Text("About...")
                    }
                } footer: {
                    Text("The 'About' info should be loaded once, no matter how many times it's visited.")
                }
                
                Section  {
                    Button {
                        aboutModel.displayMode = .loading
                    } label: {
                        Text("Reset Model")
                    }
                } footer: {
                    Text("Reset the model as if it had never been loaded before.")
                }
            }
            .listStyle(InsetGroupedListStyle())
        }
    }
}

支持的数据类型:

enum ViewDisplayState {
    case loading
    case readyToLoad
    case error
}

enum MyError: Error, CustomStringConvertible {
    case loadingError
    
    var description: String {
        switch self {
            case .loadingError:
                return "about info failed to load... don't ask."
        }
    }
}

关于屏幕(AboutView):

import SwiftUI

struct AboutView: View {
    @ObservedObject var aboutModel: AboutModel
    
    var body: some View {
        Group {
            switch aboutModel.displayMode {
                case .loading:
                    VStack {
                        Text("Loading about info...")
                    }
                case .readyToLoad:
                    Text("About: \(aboutModel.info ?? "<about info should not be nil!>")")
                case .error:
                    Text("Error: \(aboutModel.error?.description ?? "<error hould not be nil!>")")
            }
        }
        .onAppear() {
            aboutModel.loadAboutInfo()
        }
    }
}

AboutView模型:

import SwiftUI

final class AboutModel: ObservableObject {
    private(set) var info: String?
    private(set) var error: MyError?
    
    @Published var displayMode: ViewDisplayState = .loading
    
    func loadAboutInfo() {
        /**
        If we have loaded the about info already, we're set.
        */
        
        if displayMode == .readyToLoad {
            return
        }
        
        /**
        Load the info (e.g. network call)
        */
        
        loadAbout() { result in
            /**
            Make sure we assign the 'displayMode' in the main queue
            (otherwise you'll see an Xcode warning about this.)
            */
            
            DispatchQueue.main.async {
                switch result {
                    case let .success(someAboutInfo):
                        self.info = someAboutInfo
                        self.displayMode = .readyToLoad
                    case let .failure(someError):
                        self.info = nil
                        self.error = someError
                        self.displayMode = .error
                }
            }
        }
    }
    
    /**
    Dummy function; for illustration purposes. It's just a placeholder function
    that demonstrates what the real app would do.
    */
    
    private func loadAbout(completion: @escaping (Result<String, MyError>) -> Void) {
        /**
        Gather the info somehow and return it.
        Wait a couple secs to make it feel a bit more 'real'...
        */
        
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            if Bool.random() {
                completion(.success("the info is ready"))
            } else {
                completion(.failure(MyError.loadingError))
            }
        }
    }
}

简而言之,我发现对于这种懒加载模式,将@StateObject放置在主屏幕而不是子屏幕中可以避免潜在的不必要的代码重新执行。
此外,使用ViewDisplayState可以控制是否显示加载视图,解决了常见的UI闪烁问题,当数据已经在本地缓存时,UI加载视图就没有必要展示了。
当然,这并非万能药。但根据您的工作流程,它可能会有用。
如果您想看到这个项目的实际效果并进行调试,请随意从这里下载。
祝好!

1

目前我没有一个好的解决方案来处理@StateObjects,但是我试图在@main App中使用它们作为@EnvironmentObjects的初始化点。我的解决方案是不使用它们。我把这个答案放在这里给那些和我一样尝试做同样事情的人参考。

在想了很长时间之后,我想到了以下方法:

这两个let声明位于文件级别。

private let keychainManager = KeychainManager(service: "com.serious.Auth0Playground")
private let authenticatedUser = AuthenticatedUser(keychainManager: keychainManager)

@main
struct Auth0PlaygroundApp: App {

    var body: some Scene {
    
        WindowGroup {
            ContentView()
                .environmentObject(authenticatedUser)
        }
    }
}

这是我找到的唯一一种使用参数初始化environmentObject的方法。如果没有keychainManager,我无法创建authenticatedUser对象,而且我不会改变整个应用程序的架构,使所有注入的对象都不需要参数。

0
我的解决方案如下。
我将触发状态刷新的ID放置在父级中,然后将ViewModel注入到子视图中。
为什么是ID?因为如果我不把它放进去,就无法触发视图模型内容的刷新。
if let vm = globalState.selected {
    ChildView(state: ChildViewModel(vm: vm))
       .id(vm.id)
}

struct ChildView: View {
  @StateObject var vm: ChildViewModel
  ...
}

我不知道那样做的影响,但在我的情况下它是有效的,因为我需要我所描述的行为。希望能对你有所帮助。


我建议不要采用这种方法。这将改变视图的标识符,因此如果视图具有任何动画效果或其他功能(如更改大小),SwiftUI系统将不会将其视为对现有视图的更改,而是将其替换为完全不同的视图。这只是一些思考的食粮。 - Mark A. Donohoe
我建议不要采用这种方法。这样做会改变视图的标识符,因此如果视图有任何动画效果,比如改变大小,SwiftUI系统将不会将其视为对现有视图的更改,而是将其视为完全不同的视图替换。只是一些值得思考的事情。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接