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!
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!! :)
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.