SwiftUI Setting a State on init - swiftui

If i don't have an initial value for a #State and set it in the init I get a Fatal Error: EXC_BAD_ACCESS, but only in Release Mode
public struct TestSetStateView: View {
#State var item:Int
public init(item: Int) {
self.item = item
}
public var body: some View {
Text("Hello: \(item)")
}
}
If i add a default value
public struct TestSetStateView: View {
#State var item:Int = 0
public init(item: Int) {
self.item = item
}
public var body: some View {
Text("Hello: \(item)")
}
}
and call it with TestSetStateView(item: 8) it shows "Hello: 0"
If I move that to onAppear
public struct TestSetStateView: View {
#State var item:Int = 0
let firstItem: Int
public init(item: Int) {
firstItem = item
}
public var body: some View {
Text("Hello: \(item)")
.onAppear(perform: {
item = firstItem
})
}
}
and call it with TestSetStateView(item: 8) it shows "Hello: 8" which is great
or if i use the _ wrapper:
public struct TestSetStateView: View {
#State var item:Int
public init(item: Int) {
_item = State(initialValue: item)
}
public var body: some View {
Text("Hello: \(item)")
}
}
and call it with TestSetStateView(item: 8) it shows "Hello: 8", which is also great!
I'm just not sure what's happening, why can i use item = firstItem in onAppear and not in init. I sort of understand that the _ uses the #State wrapper but why don't we have to use the in onAppear. Also not sure why the error in the first example only happens in release.

This code:
public struct TestSetStateView: View {
#State var item:Int
public init(item: Int) {
_item = State(initialValue: item)
}
public var body: some View {
Text("Hello: \(item)")
}
}
is correct. Until a State variable is initialized, you can't set the underlying value directly. Remember, State is a property wrapper. The type of #State var item is not Int, it is a State that happens to contain an Int. It could just as easily contain a Double or a String. Trying to set a State with a value of Int makes as much sense as setting a String with an Int. You have a type mismatch. Frankly, the compiler should have screamed that you can't do that, but instead you get a crash.
Once the State var is initialized, it knows how to handle changing its underlying wrapped value directly. You can also initialize it directly elsewhere. Even the code: _item refers to the wrapped value, and not the state itself. Why they chose different semantics in the State property wrapper initializer, I don't know. But they did, and excluded us from using the convenience init() of the #State declaration in our view inits.

Related

SwiftUI re-initialize EnvironmentObject?

How can I refresh an environment var in SwiftUI? It is easy to update any object that's a part of an environment object, but it seems like there should be a way to re-initialize.
struct reinitenviron: View{
#EnvironmentObject private var globalObj: GlobalClass
var body: some View{
Text("refresh").onTapGesture {
globalObj = GlobalClass() //error here
}
}
}
The following gives an error that globalObj is get only. Is it possible to re-initialize?
A possible solution is to introduce explicit method in GlobalClass to reset it to initial state and use that method and in init and externally, like
class GlobalClass: ObservableObject {
#Published var value: Int = 1
init() {
self.reset()
}
func reset() {
self.value = 1
// do other activity if needed
}
}
struct reinitenviron: View{
#EnvironmentObject private var globalObj: GlobalClass
var body: some View{
Text("refresh").onTapGesture {
globalObj.reset() // << here
}
}
}

Binding Constant Alternative, but Mutable

I have a view like this:
struct View1: View {
#Binding var myVariable: Bool
init() {
_myVariable = Binding.constant(true) // It works but myVariable is immutable, so I can't edit myVariable
}
init(myVariable: Binding<Bool>) {
_myVariable = myVariable
}
var body: some View {
Button("Change") {
myVariable.toggle()
}
}
}
struct View2: View {
var body: some View {
View1()
}
}
struct View3: View {
#State var myVariable = false
var body: some View {
View1(myVariable: $myVariable)
}
}
And I want to make this: If there is a parameter provided, set this to myVariable like second init in View1. Else, set the first value of myVariable like in first init.
I tried to use Binding.constant(value) but it is immutable. And I can't edit the variable. So, I need a mutable Binding initializer like Binding.constant(value). But I can't find it.
How can I solve this problem?
To avoid over-complicating View1, you can create an intermediate view with that name, and then have a private 'internal' view which has the actual implementation.
Code:
private struct View1Internal: View {
#Binding var myVariable: Bool
var body: some View {
Button("Change") {
myVariable.toggle()
}
}
}
struct View1: View {
private enum Kind {
case state
case binding(Binding<Bool>)
}
#State private var state = true
private let kind: Kind
init() {
kind = .state
}
init(myVariable: Binding<Bool>) {
kind = .binding(myVariable)
}
var body: some View {
switch kind {
case .state: View1Internal(myVariable: $state)
case .binding(let binding): View1Internal(myVariable: binding)
}
}
}

Pass binding to child view in init SwiftUI

I need to save an instance of a child view into a variable, so I can call a method on it afterward.
However, I need to pass a binding into this child view when its initialized. How do I do that?
struct EditImageView: View {
#State private var currentSelectedText:String
#State private var currentSelectedFilter:Filter
var imageCanvasView: ImageCanvasView
init() {
currentSelectedText = "Hello"
currentSelectedFilter = Filter.noFilter
imageCanvasView = ImageCanvasView(imageText: $currentSelectedText, filter: $currentSelectedFilter)
//Error: 'self' used before all stored properties are initialized
}
var body: some View {
imageCanvasview
Button("Take screenshot") {
imageCanvasview.takeScreenshot()
}
}
}
One way is to declare imageCanvasView in body, like:
struct EditImageView: View {
#State private var currentSelectedText = "Hello"
#State private var currentSelectedFilter = Filter.noFilter
var body: some View {
let imageCanvasView = ImageCanvasView(imageText: $currentSelectedText, filter: $currentSelectedFilter)
VStack {
imageCanvasView
Button("Take screenshot") {
imageCanvasView.takeScreenshot()
}
}
}
}
All you need to do is to change the property wrapper prefix. For example, if you wanted to pass your currentSelectedText you would pass it like so.
var currentSelectedText: Binding<String>
// Effectively is the equivalent of `#State`
The same can be done in your init()
init(someString: Binding<String>) { ....
Probably a better way is to use a view model which both EditImageView and ImageCanvasView use, something like:
class EditImageViewModel: ObservableObject {
#Published var currentSelectedText: String = "Hello"
#Published var currentSelectedFilter = Filter.noFilter
func takeScreenshot() {
}
}
struct ImageCanvasView: View {
#EnvironmentObject var editImage: EditImageViewModel
var body: some View {}
}
struct EditImageView: View {
#StateObject var editImage = EditImageViewModel()
var body: some View {
VStack {
ImageCanvasView()
Button("Take screenshot") {
editImage.takeScreenshot()
}
}
.environmentObject(editImage)
}
}

#State variable cannot be changed if initialized with a constant

I need to start an object during the init of another one.
But this object uses a #Binding variable to a #State variable, like this:
struct MainView:View {
#State var myVar = false
private var myObject:MyObject
init()
let myObject($myVar)
self.myObject = myObject
}
but I cannot pass $myVar to myObject because self was not initialized alreay, so I tried to
struct MainView:View {
#State var myVar = false
private var myObject:MyObject
init()
let myObject(.constant(false))
self.myObject = myObject
}
This is MyObject
class MyObject {
#Binding var myVar:Bool
init(_ myVar:Binding<Bool>) }
self._myvar = myVar
}
but by doing so, myVar inside MyObject is permanently locked in the false state and cannot be changed.
How do I pass a dummy value to MyObject during initialization that does not lock the variable forever?
You can make the compiler accept it. But it will not work (here the Toggle will not move) :
struct MainView: View {
#State private var myVar = false
var myObject: MyObject!
init() {
myObject = MyObject($myVar)
}
var body: some View {
VStack {
Text(myVar.description)
Toggle("", isOn: myObject.$myVar)
}
}
}
Because it doesn't make much sense to have myVar (the State) and myObject (with the Binding) in the same view, I made two different views, a MainView and a SubView:
import SwiftUI
struct MainView: View {
#State private var myVar = false
var body: some View {
VStack {
Text(myVar.description)
Toggle("MainView :", isOn: $myVar)
SubView($myVar)
}
.background(Color.blue.opacity(0.3))
}
}
struct SubView: View {
var myObject: MyObject
init(_ myVar: Binding<Bool>) {
myObject = MyObject(myVar)
}
var body: some View {
VStack {
Text(myObject.myVar.description)
Toggle("SubView :", isOn: myObject.$myVar)
}.background(Color.yellow)
}
}
class MyObject: ObservableObject {
#Binding var myVar: Bool
init(_ myVar: Binding<Bool>) {
_myVar = myVar
}
}
It is indeed a two-way Binding: the Toggle of the SubView acts on that of the MainView and vice versa.
Now you will have to find a use for this object. Because you could have exposed the property myVar (with the propertyWrapper #Binding) directly in the SubView.

How do I pass a subset of data to a view that has its own view model?

Given the following...
import SwiftUI
class ViewModel: ObservableObject {
var value: Bool
init(value: Bool) {
self.value = value
}
func update() {
value = !value
}
}
struct A: View {
#ObservedObject let viewModel: ViewModel
init(value: Bool) {
viewModel = ViewModel(value: value)
}
var body: some View {
Text("\(String(viewModel.value))")
.onTapGesture {
viewModel.update()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: val[0])
}
}
How do I get viewModel to update B's val? It looks like I should be able to use #Binding inside of A but I can't use #Binding inside ViewModel, which is where I want the modification code to run. Then, I don't think I'd need #ObservedObject because the renders would flow from B.
You either need Binding, or an equivalent that does the same thing, in ViewModel. Why do you say you can't use it?
struct A: View {
#ObservedObject var model: Model
init(value: Binding<Bool>) {
model = .init(value: value)
}
var body: some View {
Text(String(model.value))
.onTapGesture(perform: model.update)
}
}
extension A {
final class Model: ObservableObject {
#Binding private(set) var value: Bool
init(value: Binding<Bool>) {
_value = value
}
func update() {
value.toggle()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: $val[0])
}
}
If you want to update the value owned by a parent, you need to pass a Binding from the parent to the child. The child changes the Binding, which updates the value for the parent.
Then you'd need to update that Binding when the child's own view model updates. You can do this by subscribing to a #Published property:
struct A: View {
#ObservedObject var viewModel: ViewModel
#Binding var value: Bool // add a binding
init(value: Binding<Bool>) {
_value = value
viewModel = ViewModel(value: _value.wrappedValue)
}
var body: some View {
Button("\(String(viewModel.value))") {
viewModel.update()
}
// subscribe to changes in view model
.onReceive(viewModel.$value, perform: {
value = $0 // update the binding
})
}
}
Also, don't forget to actually make the view model's property #Published:
class ViewModel: ObservableObject {
#Published var value: Bool
// ...
}