SpikeThea
SpikeThea

Reputation: 360

WebImage not updating the View - SwiftUI (Xcode 12)

My problem is that when I change my observed objects string property to another value, it updates the JSON Image values printing out the updated values(Using the Unsplash API), but the WebImage (from SDWebImageSwiftUI) doesn't change.

The struct that Result applies to:

struct Results : Codable {
    var total : Int
    var results : [Result]
}

struct Result : Codable {
    var id : String
    var description : String?
    var urls : URLs
}

struct URLs : Codable {
    var small : String
}

Here is the view which includes the webImage thats supposed to update:

struct MoodboardView: View {
    @ObservedObject var searchObjectController = SearchObjectController.shared
    
    var body: some View {
        
        List {
            VStack {
                Text("Mood Board: \(searchObjectController.searchText)")
                    .fontWeight(.bold)
                    .padding(6)
                ForEach(searchObjectController.results, id: \.id, content: { result in
                    Text(result.description ?? "Empty")
                    WebImage(url: URL(string: result.urls.small) )
                        .resizable()
                        .frame(height:300)
                    })
                }.onAppear() {
                searchObjectController.search()
            }
            
        }
    }
}   

Here is the class which does the API Request:

class SearchObjectController : ObservableObject {
    static let shared = SearchObjectController()
    private init() {}
    
    var token = "gQR-YsX0OpwkYpbjhPVi3b4kSR-DtWrR5phwDm2kPMM"
    @Published var results = [Result]()
    @Published var searchText : String = "forest"
    
    func search () {
        let url = URL(string: "https://api.unsplash.com/search/photos?query=\(searchText)")
        var request = URLRequest(url: url!)
        request.httpMethod = "GET"
        request.setValue("Client-ID \(token)", forHTTPHeaderField: "Authorization")
        print("request: \(request)")
        
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            guard let data = data else {return}
            print(String(data: data, encoding: .utf8)!)

            do {
                let res = try JSONDecoder().decode(Results.self, from: data)
                DispatchQueue.main.async {
                    self.results.append(contentsOf: res.results)
                }
                //print(self.results)
            } catch {
                print("catch: \(error)")
            }
        }
        task.resume()
    }
}

Here is how I change the value of the searchText in a Button, if you would like to see:

struct GenerateView: View {
    
    @ObservedObject var searchObjectController = SearchObjectController.shared
    
    @State private var celsius: Double = 0
    
    var body: some View {
        ZStack {
            Color.purple
                .ignoresSafeArea()
            VStack{
                
                Text("Generate a Random Idea")
                    .padding()
                    .foregroundColor(.white)
                    .font(.largeTitle)
                    .frame(maxWidth: .infinity, alignment: .center)
                    
                Image("placeholder")
                Slider(value: $celsius, in: -100...100)
                    .padding()
                Button("Generate") {
                    print("topic changed to\(searchObjectController.searchText)")
                    
                    searchObjectController.searchText.self = "tables"
                }
                Spacer()
            }
        }
    }
}

Upvotes: 0

Views: 723

Answers (1)

Simon
Simon

Reputation: 1870

It turns out you update the search text but don't search again. See this does the trick:

Button("Generate") {
    print("topic changed to\(searchObjectController.searchText)")

    searchObjectController.searchText.self = "tables" // you changed the search text but didnt search again
    self.searchObjectController.search() // adding this does the trick
}

Also. I updated your code to use an EnvironmentObject. If you use one instance of an object throughout your app. Consider making it an EnvironmentObject to not have to pass it around all the time.

Adding it is easy. Add it to your @main Scene

import SwiftUI

@main
struct StackoverflowApp: App {
    
    @ObservedObject var searchObjectController = SearchObjectController()
    
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(self.searchObjectController)
        }
    }
}

And using it even simpler:

struct MoodboardView: View {
    
    // Env Obj. so we reference only one object
    @EnvironmentObject var searchObjectController: SearchObjectController

    var body: some View {
        
        Text("")
    }
}

Here is your code with the changes and working as expected: I added comments to the changes I made


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

struct GenerateView: View {

    @EnvironmentObject var searchObjectController: SearchObjectController

    @State private var celsius: Double = 0

    var body: some View {
        ZStack {
            Color.purple
                .ignoresSafeArea()
            VStack{
                
                Text("Generate a Random Idea")
                    .padding()
                    .foregroundColor(.white)
                    .font(.largeTitle)
                    .frame(maxWidth: .infinity, alignment: .center)
                    
                Image("placeholder")
                Slider(value: $celsius, in: -100...100)
                    .padding()
                Button("Generate") {
                    print("topic changed to\(searchObjectController.searchText)")
                    
                    searchObjectController.searchText.self = "tables" // you changed the search text but didnt search again
                    self.searchObjectController.search() // adding this does the trick
                    
                }
                Spacer()
            }
        }
    }

}

class SearchObjectController : ObservableObject {
    
    //static let shared = SearchObjectController() // Delete this. We want one Object of this class in the entire app.
     
    //private init() {} // Delete this. Empty Init is not needed

    var token = "gQR-YsX0OpwkYpbjhPVi3b4kSR-DtWrR5phwDm2kPMM"
    @Published var results = [Result]()
    @Published var searchText : String = "forest"

    func search () {
        let url = URL(string: "https://api.unsplash.com/search/photos?query=\(searchText)")
        var request = URLRequest(url: url!)
        request.httpMethod = "GET"
        request.setValue("Client-ID \(token)", forHTTPHeaderField: "Authorization")
        print("request: \(request)")
        
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            guard let data = data else {return}
            print(String(data: data, encoding: .utf8)!)

            do {
                let res = try JSONDecoder().decode(Results.self, from: data)
                DispatchQueue.main.async {
                    self.results.append(contentsOf: res.results)
                }
                //print(self.results)
            } catch {
                print("catch: \(error)")
            }
        }
        task.resume()
    }
}

struct MoodboardView: View {
    
    // Env Obj. so we reference only one object
    @EnvironmentObject var searchObjectController: SearchObjectController

    var body: some View {
        
        List {
            VStack {
                Text("Mood Board: \(searchObjectController.searchText)")
                    .fontWeight(.bold)
                    .padding(6)
                ForEach(searchObjectController.results, id: \.id, content: { result in
                    Text(result.description ?? "Empty")
                    WebImage(url: URL(string: result.urls.small) )
                        .resizable()
                        .frame(height:300)
                    })
                }.onAppear() {
                searchObjectController.search()
            }
            
            // I added your update button here so I can use it.
            GenerateView()
        }
    }
}

struct Results : Codable {
    var total : Int
    var results : [Result]
}

struct Result : Codable {
    var id : String
    var description : String?
    var urls : URLs
}

struct URLs : Codable {
    var small : String
}

Ps. please not that it appears when you search you just append the results to the array and don't delete the old ones. Thats the reason why you still see the first images after updating. The new ones just get appended at the bottom. Scroll down to see them. If you don't want that just empty the array with results upon search

Upvotes: 1

Related Questions