Search code examples
network-programmingswiftuiviewmodelfetch-api

After successful fetch request.. Unable to transform the data to ViewModel Published var using combine subscriber .sink


CURRENT SITUATION: Okay so the example code on here works 🥳 but if i do this same code 4x more times to make 4 rows under each other in my larger project.. it does not work. Maybe it is too much calls for the API at the same time because i call them all at the same time.. please let me know!

  • I am fetching data from free API to a @published var.. successfully
  • then in ViewModel I subscribe to the @published var and try to .sink into my ViewModel @published var... and it doesn't work! I think its bcs of the mistakes in timings when I call these 2 funcs
  • I am using the OMDb free API and it worked perfectly up until now

Shorter example code to display the problem:

DataManager with the @published var and fetch func

class DataManager {
    
    static let instance = DataManager()
    
    let apiKey: String = "c2e5cb16"
    
    @Published var fetchedSPIDERMAN: SearchModel = SearchModel(search: nil, totalResults: nil, response: nil)
    var cancelFetchAllSelections = Set<AnyCancellable>()
    
    private init() {
        fetchSpiderman()
    }
    
    func fetchSpiderman() {
        guard let url = URL(string: "https://www.omdbapi.com/?apikey=\(apiKey)&s=spider") else { return print("MY ERROR: BAD URL") }
        
        NetworkingManager.download(url: url)
            .decode(type: SearchModel.self, decoder: JSONDecoder())
            .sink(receiveCompletion: NetworkingManager.handleCompletion,
                  receiveValue: { [weak self] (moviesDownloaded) in
                self?.fetchedSPIDERMAN = moviesDownloaded
                print("fetchedSPIDER")
            })
            .store(in: &cancelFetchAllSelections)
    }
}

ViewModel with the other @published var and func .sink

class ViewModel: ObservableObject {
    
    let dataService = DataManager.instance
    
    @Published var selectionForSpiderman: [MovieModel] = []
    @Published var cancellables = Set<AnyCancellable>()
    
    
    init() {
    }
    
    func sinkToSelectionForSpiderman(){
        dataService.$fetchedSPIDERMAN
            .receive(on: DispatchQueue.main)
            .sink { [weak self] (fetchedMovieModel) in
                if let unwrappedFetchedMovieModel = fetchedMovieModel.search {
                    self?.selectionForSpiderman = unwrappedFetchedMovieModel
                }else {
                    print("MY ERROR: CANT SINK SPIDERMAN")
                }
            }
            .store(in: &cancellables)
    }
}

In console it shows the print("MY ERROR: CANT SINK SPIDERMAN")

HomeView with .onAppear calling the vm.sinkToSelectionSpiderman()

struct HomeView: View {
    
    @StateObject var vm: ViewModel = ViewModel()
    
    let randomUrl: String = "https://media.istockphoto.com/id/525982128/cs/fotografie/agresivita-koček.jpg?s=1024x1024&w=is&k=20&c=y632ynYYyc3wS5FuPBgnyXeBNBC7JmjQNwz5Vl_PvI8="
    
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: true) {
            HStack(spacing: 0) {
                ForEach(vm.selectionForSpiderman) { selection in
                    VStack(alignment: .leading, spacing: 12) {
                        AsyncImage(url: URL(string: selection.poster ?? randomUrl)) { returnedImage in
                            switch returnedImage {
                            case .empty:
                                ProgressView()
                            case .success (let image):
                                image
                                    .resizable()
                                    .frame(width: 200,
                                           height: 120,
                                           alignment: .leading)
                                    .background(Color.red)
                                    .scaledToFill()
                                    .cornerRadius(10)
                                    .shadow(color: Color.white.opacity(0.2), radius: 5, x: 0, y: 5)
                            case .failure (_):
                                Image(systemName: "xmark")
                            default:
                                Image(systemName: "xmark")
                            }
                        }
                        
                        Text(selection.title ?? "NA")
                            .foregroundColor(Color.white)
                            .font(.system(.headline, design: .rounded, weight: .medium))
                            .padding(.leading, 12)
                    }.frame(maxWidth: 210)
                }
                .accentColor(Color.white)
            }
        }
        .onAppear {
            vm.sinkToSelectionForSpiderman()
        }
    }
}

My models are normally working with other Fetch funcs.

The main question is: I am pretty sure I need to call the .sink func after the Fetch func... but idk if the .onAppear block of code executes before the Fetch func is finished and had time to sent the data to the subscriber .sink func?

Interesting fact: Once it actually showed the data in the scrollView in the Canvas :DD unfortunately just once and never again.. maybe the API is slow

I have tried to look up how the timings work in SwiftUI and when to call which function but it is too specific and I didn't find an answer to it. If you know any resources where I can study this kind of stuff, please let me know!! :)


Solution

  • You do not need 2 class for dataManager and ViewModel. It can be made simpler :

    // Data structures defined from the json data
    struct MovieModel: Codable, Identifiable {
        var id: String { // Identifiable for ForEach
            imdbID
        }
        let title: String
        let year: String?
        let imdbID: String
        let type: String?
        let poster: String?
        
        enum CodingKeys: String, CodingKey {
            case title = "Title"
            case year = "Year"
            case imdbID = "imdbID"
            case type = "Type"
            case poster = "Poster"
        }
    }
    
    struct SearchModel: Codable {
        var search: [MovieModel] = []
        var totalResults: String = "0"
        var response: String = "False"
        var responseOK: Bool {
            response == "True"
        }
        
        enum CodingKeys: String, CodingKey {
            case search = "Search"
            case totalResults = "totalResults"
            case response = "Response"
        }
    }
    
    // The data manager handle the data receiving and put these in its published var movies
    class DataManager: ObservableObject {
        static let instance = DataManager()
        
        let apiKey: String = "c2e5cb16"
        @Published var movies: [MovieModel] = []
        var cancelFetchAllSelections = Set<AnyCancellable>()
        
        private init() {
            // No need to call fetch here as it is done in onAppear
        }
        
        func fetchSpiderman() {
            // https://www.omdbapi.com/?apikey=c2e5cb16&s=spider
            guard let url = URL(string: "https://www.omdbapi.com/?apikey=\(apiKey)&s=spider") else { return print("MY ERROR: BAD URL") }
            print("start request on \(url.absoluteString)")
            let request = URLRequest(url: url)
            // URLSession.shared.dataTaskPublisher is handy to retrieve data from network 
            URLSession.shared.dataTaskPublisher(for: request)
                .map(\.data) // Publish the received data
                .decode(type: SearchModel.self, decoder: JSONDecoder()) // decode dara
                .receive(on: DispatchQueue.main) // on main queue to refresh display
                .map({ searchModel in
                    if searchModel.responseOK {
                        print("Response ok : \(searchModel.totalResults) results")
                        return searchModel.search
                    } else {
                        print("Response error")
                        return []
                    }
                }) // Return the received movies
                .catch() {error -> Just<[MovieModel]> in
                    print("Exception : \(error)")
                    return Just(self.movies)
                }
                .assign(to: &$movies) // save into items
        }
    }
    
    struct HomeView: View {
        // This is the model
        @StateObject var dataManager = DataManager.instance
        
        // Shortcut to get movies from the model
        var movies: [MovieModel] {
            dataManager.movies
        }
        
        let randomUrl: String = "https://media.istockphoto.com/id/525982128/cs/fotografie/agresivita-koček.jpg?s=1024x1024&w=is&k=20&c=y632ynYYyc3wS5FuPBgnyXeBNBC7JmjQNwz5Vl_PvI8="
        
        
        var body: some View {
            ScrollView(.horizontal, showsIndicators: true) {
                HStack(spacing: 0) {
                    // Loop on the movies
                    ForEach(movies) { selection in
                        VStack(alignment: .leading, spacing: 12) {
                            AsyncImage(url: URL(string: selection.poster ?? randomUrl)) { returnedImage in
                                switch returnedImage {
                                    case .empty:
                                        ProgressView()
                                    case .success (let image):
                                        image
                                            .resizable()
                                            .frame(width: 200,
                                                   height: 120,
                                                   alignment: .leading)
                                            .background(Color.red)
                                            .scaledToFill()
                                            .cornerRadius(10)
                                            .shadow(color: Color.white.opacity(0.2), radius: 5, x: 0, y: 5)
                                    case .failure (_):
                                        Image(systemName: "xmark")
                                    default:
                                        Image(systemName: "xmark")
                                }
                            }
                            
                            Text(selection.title)
                                .foregroundColor(Color.white)
                                .font(.system(.headline, design: .rounded, weight: .medium))
                                .padding(.leading, 12)
                        }.frame(maxWidth: 210)
                    }
                    .accentColor(Color.white)
                }
            }
            .onAppear {
                // starting the fetch
                dataManager.fetchSpiderman()
            }
        }
    }
    

    Hope this can help you.