Douglas Curran
Douglas Curran

Reputation: 95

SwiftUI @State array not updating a value appended during init()

I'm a beginning programmer so...

Using Xcode 12.4 on iMac Big Sur 11.2 the project is 'Multiplatform App'

I'm trying to build an array of images to be moved around in a grid. I copied "My Image.png" into my Xcode project and used it just fine but no matter how I try to put it in an array it returns a nil array. I tried this on another computer and it seemed to work. What in Xcode could affect this? (Or did I just imagine it worked on the other machine? :-p)

code:

@State var stringURL: URL = Bundle.main.url(forResource: "My Image", withExtension: "png")!
Image(nsImage: NSImage(byReferencing: stringURL))

@State var arrayURLs = [URL]()
arrayURLs.append(Bundle.main.url(forResource: "My Image", withExtension: "png")!)
Image(nsImage: NSImage(byReferencing: arrayURLs[0]))

first 2 lines work but the last 3 lines lines fail in the same app. I get runtime error 'Index out of Range' which I believe is because the bundle call returned nil.

In context is basically the same. I am at the very start of this project and never got past the first lines....

import SwiftUI

struct ContentView: View {
@State var pieceImageURLs = [URL]()


init() {
    self.pieceImageURLs.append(Bundle.main.url(forResource: "Red Warrior", withExtension: "png")!)
}

var body: some View {
    HStack {
         Image(nsImage: NSImage(byReferencing: pieceImageURLs[0]))
    }
}}



    import SwiftUI

struct ContentView: View {
    @State var stringURL: URL = Bundle.main.url(forResource: "My Image", withExtension: "png")!
    @State var arrayURLs = [URL]()
init() {
        stringURL = Bundle.main.url(forResource: "My Image", withExtension: "png")!
        arrayURLs.append(Bundle.main.url(forResource: "My Image", withExtension: "png")!)
    }

    var body: some View {
        HStack {
            Image(nsImage: NSImage(byReferencing: stringURL))
            Image(nsImage: NSImage(byReferencing: arrayURLs[0]))
        }
        Button(action: {
            arrayURLs.append(Bundle.main.url(forResource: "My Image", withExtension: "png")!)
            Image(nsImage: NSImage(byReferencing: arrayURLs[0]))
        }) {
            Text("add image")
        }
    }
}

'''

Or all together like this either image line with arrays fails

Upvotes: 7

Views: 4303

Answers (1)

jnpdx
jnpdx

Reputation: 52645

This is an interesting one and I wouldn't have predicted the behavior myself. Looks like SwiftUI really doesn't like you to set @State variables during the init phase in a View. There are a couple ways around that.

Option 1

You can't set a new value, but you can initialize it explicitly as State in your init:

struct ContentView: View {
    @State var pieceImageURLs = [URL]()
    
    init() {
        if let url = Bundle.main.url(forResource: "Frame 1-2", withExtension: "png") {
            _pieceImageURLs = State(initialValue: [url]) // <--- Here
        }
    }
    
    var body: some View {
        HStack {
            if let imgUrl = pieceImageURLs.first, let nsImage = NSImage(byReferencing: imgUrl) {
                Image(nsImage: nsImage)
            } else {
                Text("HI")
            }
        }
        .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
    }
}

Option 2

Don't use @State for this, since it's static anyway (at least in your example) -- you're just setting a value in init.

struct ContentView: View {
    var pieceImageURLs = [URL]()  // <--- Here
    
    init() {
        if let url = Bundle.main.url(forResource: "Frame 1-2", withExtension: "png") {
            pieceImageURLs.append(url)
        }
    }
    
    var body: some View {
        HStack {
            if let imgUrl = pieceImageURLs.first, let nsImage = NSImage(byReferencing: imgUrl) {
                Image(nsImage: nsImage)
            } else {
                Text("HI")
            }
        }
        .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
    }
}

Option 3

Use state, but set it in onAppear:

struct ContentView: View {
    @State var pieceImageURLs = [URL]()
    
    var body: some View {
        HStack {
            if let imgUrl = pieceImageURLs.first, let nsImage = NSImage(byReferencing: imgUrl) {
                Image(nsImage: nsImage)
            } else {
                Text("HI")
            }
        }
        .onAppear {  // <--- Here
            if let url = Bundle.main.url(forResource: "Frame 1-2", withExtension: "png") {
                pieceImageURLs.append(url)
            }
        }
        .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
    }
}

Option 4

Do the init in a view model (ObservableObject)

class ViewModel : ObservableObject {  // <--- Here
    @Published var pieceImageURLs = [URL]()
    
    init() {
        if let url = Bundle.main.url(forResource: "Frame 1-2", withExtension: "png") {
            pieceImageURLs.append(url)
        }
    }
}

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()  // <--- Here
    
    var body: some View {
        HStack {
            if let imgUrl = viewModel.pieceImageURLs.first, let nsImage = NSImage(byReferencing: imgUrl) {
                Image(nsImage: nsImage)
            } else {
                Text("HI")
            }
        }
        .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
    }
}

Upvotes: 7

Related Questions