7

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

1
  • Can you show us in the context of your code? ie, these lines clearly don't exist next to each other in the actual files, as you can't declare properties and logic next to each other. Might help diagnose it. Commented Feb 8, 2021 at 21:52

1 Answer 1

7

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)
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks so much. It would have taken me forever to figure that one out. Trying to build a board game similar to chess. Any suggestions for tutorials or samples?
@DouglasCurran -- if this worked for you and answered your question, please consider accepting the answer. I don't have any specific tutorials to suggest for this particular scenario. In general, I'd always recommend reading as much hackingwithswift.com as you can
Thanks, I've been digging my way through the hackingwithswift.
option 1 & 2 produce a compile error: "Escaping closure captures mutating 'self' parameter", this is an architectural issue, do not mutate a view state during view render cycle, rather change the view's data model outside of the render cycle and let the re-render of the view reflect that change, that is why - options 3 & 4 are preferred either way it means that on first view render the data will be missing / nil / or partially and you have to deal with it (add some if statements to check if the array is empty or add some empty data that will be run over once the object is initialized)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.