问题描述
为了让代码遵循MVVM架构,将数据代码(属性、方法)迁移至ViewModel文件中,在视图文件中导入ViewModel类。因为视图中的属性需要初始化,所以初始化流程从视图变成了视图的VIewModel类。
这就导致视图中,原本的初始化代码失效:
struct EditView: View {
@Environment(\.dismiss) var dismiss
@State private var editViewModel = EditViewModel() // 引入的ViewModel报错
init(location: Location, onSave: @escaping (Location) -> Void) {
self.location = location
self.onSave = onSave
self.name = location.name
self.description = location.description
}
...
}
将视图中的初始化器改为初始化ViewModel的属性:
struct EditView: View {
@Environment(\.dismiss) var dismiss
@State private var editViewModel = EditViewModel() // 引入的ViewModel报错
init(location: Location, onSave: @escaping (Location) -> Void) {
editViewModel.location = location
editViewModel.onSave = onSave
editViewModel.name = location.name
editViewModel.description = location.description
}
...
}
视图依然会存在未初始化EditViewModel的报错:
Missing arguments for parameters 'location', 'onSave' in call
Insert 'location: <#Location#>, onSave: <#(Location) -> Void#>'
这一问题在于,原本视图的依赖初始化器的变量遵循MVVM架构,迁移到ViewModel中后,初始化器变成导入到View视图中的ViewModel类。
因为ViewModel类的初始化器需要两个参数:location 和 onSave,而在创建 editviewModel 的地方,并未提供这两个参数。这就导致编译器提示 “Missing arguments for parameters” 的错误。
解决方案
方法 1:使用延迟初始化
由于ViewModel需要在初始化时提供 location 和 onSave,可以使用View 视图的 init 初始化对 ViewModel进行初始化赋值。
struct EditView: View {
@Environment(\.dismiss) var dismiss
@State private var editViewModel: EditViewModel
init(location: Location, onSave: @escaping (Location) -> Void) {
self._editViewModel = State(wrappedValue: EditViewModel(location: location, onSave: onSave))
}
// ... 其余代码保持不变
}
在 SwiftUI 中,@State 是一个属性包装器,用于表示视图内的可变状态变量。
@State private var editViewModel: EditViewModel
由于 @State 的变量需要在视图生命周期内由 SwiftUI 管理,不能直接通过普通赋值操作修改,因此需要通过其包装器 _editViewModel(前缀 _ 的变量形式)来初始化。
_editViewModel = State(wrappedValue: EditViewModel(location: location, onSave: onSave))
State:表示使用 SwiftUI 的 @State 属性包装器。
wrappedValue:表示这个 State 包装器所管理的实际值。
EditViewModel(location: location, onSave: onSave):表示创建一个新的 EditViewModel 实例,并将其作为包装器的值。
注意:在上面的代码中,使用的是State(wrappedValue:),不是@State。
State(wrappedValue:)创建了一个State包装器,包装了一个EditViewModel对象,EditViewModel对象的初始化参数是通过View视图的初始化方法传递的。
最后将State包装器赋值给@State的底层包装器变量_editViewModel。
关于@State的相关逻辑,可以在《Swift案例分析<闭包在视图中的传递>》中查看相关知识点。
方法 2:为ViewModel提供默认值
这个方法仅适用于不进行动态传递的情况下,为视图提供一个便利构造器。
extension EditView.EditViewModel {
convenience init() {
self.init(location: Location(id: UUID(), name: "", description: "", latitude: 0, longitude: 0)) { _ in }
}
}
通过新增对应类的便利构造器,可以在不修改类的构造函数的情况下,提供默认参数。
但是,因为这个方法并不适用这里的报错,因为location和onSave都是需要传递适用的。
扩展知识
State(wrappedValue:)方法
State(wrappedValue:) 是 State 包装器的初始化方法,用于显式设置状态的初始值。通过 wrappedValue 参数,可以为 State 包装器所管理的值赋初始值。
let state = State(wrappedValue: 10)
wrappedValue 是这个 State 包装器实际管理的值(这里是 10)。
这样,state 实例就成为一个 State 包装器,它负责管理 10。
因为在文章代码中,editViewModel是一个@State管理的属性:
@State private var editViewModel: EditViewModel
当用 @State 修饰属性时,它的行为已经和普通属性不同。@State 属性的值实际上是由其包装器(State 类型)来管理的,不能直接赋值给它。
直接赋值会跳过 State 的管理逻辑,破坏 SwiftUI 的状态机制。
struct Name {
var name: String
func sayNmae() -> String{
return "Say my name:\(name)"
}
}
struct ContentView: View {
@State private var name: Name
init() {
// 错误:不能直接赋值
self.name = "fangjunyu"
}
var body: some View {
VStack {
Text("\(name)")
}
}
}
需要通过 _editViewModel(包装器)初始化
@State 的底层变量是 _editViewModel,它是 State<EditViewModel> 类型。
必须通过 State(wrappedValue:) 来初始化这个包装器,而不是直接修改它所管理的值。
struct Name {
var name: String
func sayNmae() -> String{
return "Say my name:\(name)"
}
}
struct ContentView: View {
@State private var name: Name
init() {
// 错误:不能直接赋值
_name = State(wrappedValue: Name(name: "fangjunyu"))
}
var body: some View {
VStack {
Text("\(name)")
}
}
}
其他错误用法的尝试
如果不对@State的底层变量_name赋值,而是直接赋值@State访问器:
init() {
// 错误:不能直接赋值
name = State(wrappedValue: Name(name: "fangjunyu"))
}
就会报错:
Cannot assign value of type 'State<Name>' to type 'Name'
提示:无法将“String”类型的值分配给“Name”类型。
如果尝试创建Name实例直接复制@State访问器name:
init() {
// 错误:不能直接赋值
name = Name(name: "fangjunyu")
}
@State访问器name的值就会变成“Name(name: “fangjunyu”)”,而不是fangjunyu。
这是因为name是一个访问器,当给访问器赋值时,实际调取的是:
// 赋值的代码
name = Name(name: "fangjunyu")
// 实际执行的代码
_name.wrappedValue = Name(name: "fangjunyu")
但是@State的底层变量是_name,它是 State<Name> 类型。必须通过 State(wrappedValue:) 来初始化这个包装器,而不是直接修改它所管理的值。
因此,SwiftUI可能是临时绕过某些严格的初始化规则:@State 包装器的初始化要求 SwiftUI 确保它的生命周期完整,因此直接赋值跳过了它的状态管理机制。
所以,这个写法有问题,且不推荐使用。