Refresh remotely loaded image in SwiftUI

Viewed 127

It's a lot of code and looks daunting, but it's pretty simple--I'm trying to load remote image, and when the image is clicked, I'd like to switch to the next image:

struct TestView: View {
  @State var selectedIndex: Int = 0
  @State var arrayOfImages: [String] = ["https://s3-media3.fl.yelpcdn.com/bphoto/_0bkRz0wln3URHevWORCkA/o.jpg", "https://s3-media2.fl.yelpcdn.com/bphoto/MDZXc4pDt5xUfXF0Rw6rMw/o.jpg", "https://s3-media3.fl.yelpcdn.com/bphoto/feYg35an2MilNK3dCwwqTQ/o.jpg"]
  var body: some View {
    RemoteImage(url: arrayOfImages[selectedIndex])
    .scaledToFill()
    .frame(width: 200, height: 200)
    .clipped()
    .onTapGesture {
      selectedIndex += 1
    }
  }
}

struct RemoteImage: View {
    private enum LoadState {
        case loading, success, failure
    }
    
    private class Loader: ObservableObject {
        var data = Data()
        var state = LoadState.loading
        
        init(url: String) {
            guard let parsedURL = URL(string: url) else {
                fatalError("Invalid URL: \(url)")
            }
            
            URLSession.shared.dataTask(with: parsedURL) { data, response, error in
                if let data = data, data.count > 0 {
                    self.data = data
                    self.state = .success
                } else {
                    self.state = .failure
                }
                
                DispatchQueue.main.async {
                    self.objectWillChange.send()
                }
            }.resume()
        }
    }
    
    @StateObject private var loader: Loader
    var loading: Image
    var failure: Image
    
    var body: some View {
        selectImage()
            .resizable()
    }
    
    init(url: String, loading: Image = Image(""), failure: Image = Image(systemName: "multiply.circle")) {
        _loader = StateObject(wrappedValue: Loader(url: url))
        self.loading = loading
        self.failure = failure
    }
    
    private func selectImage() -> Image {
        switch loader.state {
        case .loading:
            return loading
        case .failure:
            return failure
        default:
            if let image = UIImage(data: loader.data) {
                return Image(uiImage: image)
            } else {
                return failure
            }
        }
    }
}

Here's the problem: the image doesn't go to the next one when you tap on it. I think it's because the RemoteImage view isn't being reloaded, but I'm not sure how to fix. Any help is appreciated!

1 Answers

I think you are trying to do too much inside RemoteImage, in particular declaring private @StateObject private var loader: Loader and all that derives from this.

Try this approach with @StateObject var loader = Loader() outside your RemoteImage. Works for me.

struct ContentView: View {
    var body: some View {
        TestView()
    }
}

struct TestView: View {
    @StateObject var loader = Loader() // <-- here
    
    @State var selectedIndex: Int = 0
    @State var arrayOfImages: [String] = ["https://s3-media3.fl.yelpcdn.com/bphoto/_0bkRz0wln3URHevWORCkA/o.jpg", "https://s3-media2.fl.yelpcdn.com/bphoto/MDZXc4pDt5xUfXF0Rw6rMw/o.jpg", "https://s3-media3.fl.yelpcdn.com/bphoto/feYg35an2MilNK3dCwwqTQ/o.jpg"]
    
    var body: some View {
        RemoteImage(loader: loader) // <--- here
            .scaledToFill()
            .frame(width: 200, height: 200)
            .clipped()
            .onTapGesture {
                selectedIndex += 1
                if selectedIndex < arrayOfImages.count {  
                    loader.load(url: arrayOfImages[selectedIndex])  // <-- here
                } else {
                    //...
                }
            }
            .onAppear {
                loader.load(url: arrayOfImages[selectedIndex]) // <--- here
            }
    }
}

class Loader: ObservableObject {
    var data = Data()
    var state = LoadState.loading
    
    func load(url: String) { // <--- here
        guard let parsedURL = URL(string: url) else {
            fatalError("Invalid URL: \(url)")
        }
        URLSession.shared.dataTask(with: parsedURL) { data, response, error in
            if let data = data, data.count > 0 {
                self.data = data
                self.state = .success
            } else {
                self.state = .failure
            }
            
            DispatchQueue.main.async {
                self.objectWillChange.send()
            }
        }.resume()
    }
}

enum LoadState {
    case loading, success, failure
}

struct RemoteImage: View {
    @ObservedObject var loader: Loader  // <--- here
    
    var loading: Image = Image("")
    var failure: Image = Image(systemName: "multiply.circle")
    
    var body: some View {
        selectImage().resizable()
    }
    
    private func selectImage() -> Image {
        switch loader.state {
        case .loading:
            return loading
        case .failure:
            return failure
        default:
            if let image = UIImage(data: loader.data) {
                return Image(uiImage: image)
            } else {
                return failure
            }
        }
    }
}
Related