SwiftUI @State变量的初始化问题

203

我想要通过Structinit()方法在SwiftUI中初始化一个@State变量的值,以便从准备好的字典中获取正确的文本来用于TextField的操作。

源代码如下:

struct StateFromOutside: View {
    let list = [
        "a": "Letter A",
        "b": "Letter B",
        // ...
    ]
    @State var fullText: String = ""

    init(letter: String) {
        self.fullText = list[letter]!
    }

    var body: some View {
        TextField($fullText)
    }
}

不幸的是,执行过程中出现错误 Thread 1: Fatal error: Accessing State<String> outside View.body

我该如何解决这个问题?非常感谢您的帮助!


13
使用 State(initialValue:) - onmyway133
11
请将第二个答案的点赞数增加到150以上,并将其设为被采纳的答案。像我一样,许多人错过了第二个答案,花费了很长时间无法解决问题。 - Tul
1
得票最高的答案 可能比被采纳的答案更符合你的需求。 - Benjohn
8个回答

674

SwiftUI 不允许您在初始化程序中更改 @State,但您可以进行初始化。

移除默认值并使用 _fullText 直接设置 @State,而不是通过属性包装器访问器。

@State var fullText: String // No default value of ""

init(letter: String) {
    _fullText = State(initialValue: list[letter]!)
}

124
这应该是被接受的答案,因为在某些情况下 onAppear 可能太晚了。 - Yonat
8
起初我犹豫不决是否使用它,因为我以为它会访问一些内部API,但事实证明它是Swift语言的一部分。有关属性包装器在底层如何工作的简洁信息,包括合成下划线(_),请参见https://docs.swift.org/swift-book/ReferenceManual/Attributes.html下的**propertyWrapper**一节。 - bcause
17
请注意,在所有存储属性初始化完成后,您在init内部初始化的状态很可能会立即被覆盖。请参阅此评论,其中一位苹果工程师明确表示不要使用init来初始化@State,而是应该在内联中完成初始化。 - Damiaan Dufaux
10
答案并不是问题的解决方案。它不会像梦想一样工作,而更像是噩梦般的存在。如果我们想在视图中改变值,我们需要使用可以在初始化器中初始化的绑定。在 init 中初始化 @State 只会在创建该视图的初始时刻进行一次。后续的视图将不会看到传递给 init 的值,而是看到 SwiftUI 系统缓存的先前值。 - CouchDeveloper
5
“init不是更新@State变量值的好地方。” - shim
显示剩余13条评论

45

我会尝试在onAppear中初始化它。

struct StateFromOutside: View {
    let list = [
        "a": "Letter A",
        "b": "Letter B",
        // ...
    ]
    @State var fullText: String = ""

    var body: some View {
        TextField($fullText)
             .onAppear {
                 self.fullText = list[letter]!
             }
    }
}

或者更好的方法是,使用一个模型对象(与您的视图相关联的BindableObject),并在那里进行所有的初始化和业务逻辑。您的视图将自动更新以反映这些更改。


更新:现在BindableObject称为ObservableObject


谢谢,这个完美地运作了(尽管我将.onAppear应用于包围TextField的VStack上)。 - Daniel Messner
在继续探索一下之后,我注意到你的解决方案是为这种情况找到的变通方法,但我遇到了更多需要初始化 @State 变量的情况。虽然也可以在那里应用这种方法,但我并不认为这是处理它的最好方法。 - Daniel Messner
@DanielMessner:是的,我同意。对于更复杂的情况,只需使用模型对象(BindableObject),并在该对象中进行初始化,无论是在运行init()时还是通过视图中的.onAppear事件触发。 - Bogdan Farca
2
这并不是针对问题的最佳解决方案,但它是另一种类型问题的最佳解决方案。我有一个@State,其初始值取决于我传入的另一个对象,因此每次显示视图时,我希望它具有不同的初始值。另一个答案无法解决这个问题,但在onAppear中设置状态可以解决。 - Richard Venable

17
顶部的答案是一个反模式,当依赖关系改变(`letter`)时,会在后续过程中带来困扰,因为您的状态不会相应地更新。
在`View`的`init`中,永远不应该使用`State(initialValue:)`或`State(wrappedValue:)`来初始化状态。事实上,`State`只能内联初始化,如下所示:
@State private var fullText: String = "The value"

如果这不可行,可以使用@Binding@ObservedObject@Binding@State的组合,甚至是自定义的DynamicProperty
关于此更多信息,请点击这里

2

现在,在init方法中设置@State变量的默认值已经不是问题了。但是,你必须将给状态赋予的默认值去掉,这样它才能按照期望的方式工作:

,,,
    @State var fullText: String // <- No default value here

    init(letter: String) {
        self.fullText = list[letter]!
    }

    var body: some View {
        TextField("", text: $fullText)
    }
}

1

根据情况,您可以以不同的方式初始化状态:

// With default value

@State var fullText: String = "XXX"

// Not optional value and without default value

@State var fullText: String

init(x: String) {
    fullText = x
}

// Optional value and without default value

@State var fullText: String

init(x: String) {
    _fullText = State(initialValue: x)
}

0
你可以创建一个视图模型并初始化它:
 class LetterViewModel: ObservableObject {

     var fullText: String
     let listTemp = [
         "a": "Letter A",
         "b": "Letter B",
         // ...
     ]

     init(initialLetter: String) {
         fullText = listTemp[initialLetter] ?? ""
     }
 }

 struct LetterView: View {

     @State var viewmodel: LetterViewModel

     var body: some View {
    
         TextField("Enter text", text: $viewmodel.fullText)
     }
 }

然后像这样调用视图:

 struct ContentView: View {

     var body: some View {

           LetterView(viewmodel: LetterViewModel(initialLetter: "a"))
     }
 }

通过这种方式,您也不必调用State实例化方法。

0
Bogdan Farca的答案对于这种情况是正确的,但我们不能说这是所问问题的解决方案,因为我发现所问问题中存在Textfield的问题。尽管如此,我们仍然可以使用init来编写相同的代码,因此请查看下面的代码,它展示了所问问题的确切解决方案。
struct StateFromOutside: View {
    let list = [
        "a": "Letter A",
        "b": "Letter B",
        // ...
    ]
    @State var fullText: String = ""

    init(letter: String) {
        self.fullText = list[letter]!
    }

    var body: some View {
        VStack {
            Text("\(self.fullText)")
            TextField("Enter some text", text: $fullText)
        }
    }
}

只需在视图中调用即可使用

struct ContentView: View {
    var body: some View {
        StateFromOutside(letter: "a")
    }
}

-3
在下面的示例代码中,查看.id(count)
import SwiftUI
import MapKit

struct ContentView: View {
    @State private var count = 0
    
    var body: some View {
        Button("Tap me") {
            self.count += 1
            print(count)
        }
        Spacer()
        testView(count: count).id(count) // <------ THIS IS IMPORTANT. Without this "id" the initializer setting affects the testView only once and calling testView again won't change it (not desirable, of course)
    }
}



struct testView: View {
    var count2: Int
    @State private var region: MKCoordinateRegion
    
    init(count: Int) {
        count2 = 2*count
        print("in testView: \(count)")
        
        let lon =  -0.1246402 + Double(count) / 100.0
        let lat =  51.50007773 + Double(count) / 100.0
        let myRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: lon) , span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
        _region = State(initialValue: myRegion)
    }

    var body: some View {
        Map(coordinateRegion: $region, interactionModes: MapInteractionModes.all)
        Text("\(count2)")
    }
}

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