SwifUI @State初始化报错原因与修复方法
SwifUI @State初始化报错原因与修复方法

SwifUI @State初始化报错原因与修复方法

问题描述

为了让代码遵循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 确保它的生命周期完整,因此直接赋值跳过了它的状态管理机制

所以,这个写法有问题,且不推荐使用。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注