user4500882
user4500882

Reputation: 391

Reload aync call on view state update

I have the following view:

struct SpriteView: View {
    @Binding var name: String
    @State var sprite: Image = Image(systemName: "exclamationmark")
    
    var body: some View {
        VStack{
            sprite
        }
        .onAppear(perform: loadSprite)
    }
    
    func loadSprite() {
        // async function
        getSpriteFromNetwork(self.name){ result in
            switch result {
            // async callback
            case .success(newSprite):
                self.sprite = newSprite
            }
    }
}

What I want to happen is pretty simple: a user modifies name in text field (from parent view), which reloads SpriteView with the new sprite. But the above view doesn't work since when the view is reloaded with the new name, loadSprite isn't called again (onAppear only fires when the view is first loaded). I also can't put loadSprite in the view itself (and have it return an image) since it'll lead to an infinite loop.

There is a beta function onChange that is exactly what I'm looking for, but it's only in the beta version of Xcode. Since Combine is all about async callbacks and SwiftUI and Combine are supposed to play well together, I thought this sort of behavior would be trivial to implement but I've been having a lot of trouble with it.

Upvotes: 0

Views: 90

Answers (1)

user4500882
user4500882

Reputation: 391

I don't particular like this solution since it requires creating a new ObservableObject but this how I ended up doing it:

class SpriteLoader: ObservableObject {
    @Published var sprite: Image = Image(systemName: "exclamationmark")
    
    func loadSprite(name: String) {
        // async function
        self.sprite = Image(systemName: "arrow.right")
    }
}

struct ParentView: View {
    @State var name: String

    @State var spriteLoader = SpriteLoader()

    var body: some View {
        SpriteView(spriteLoader: spriteLoader)
        TextField(name, text: $name, onCommit: {
            spriteLoader.loadSprite(name: name)
        })
    }
}

struct SpriteView: View {
    @ObservedObject var spriteLoader: SpriteLoader
    
    var body: some View {
        VStack{
            spriteLoader.sprite
        }
    }
}

Old answer:

I think the best way to do this is as follows:

Parent view:

struct ParentView: View {
    @State var name: String

    @State spriteView = SpriteView()

    var body: some View {
        spriteView
        TextField(value: $name, onCommit: {
            spriteView.loadSprite(name)
        })
    }

And then the sprite view won't even need the @Binding name member.

Upvotes: 0

Related Questions