Chaining swift combine publishers and receiving each result - swift

In the example below, I’m making a network request to load different movie genres, then using that to load all the movies. The sink only returns the movie results. How could I receive both the genres and movies?
struct Genre: Codable, Identifiable{
let id: Int
let name: String
var movies: [Movie]?
}
struct Movie: Codable, Hashable, Identifiable {
let title: String
let id: Int
let posterPath: String?
let backdropPath : String?
var tagline: String?
}
loadGenres() is AnyPublisher<[Genre], Error>
fetchMoviesIn() is AnyPublisher<[Movie], Error>
class GenresViewModel: ObservableObject{
#Published var genres = [Genre]()
#Published var movies = [Movie]()
var requests = Set<AnyCancellable>()
init(){
NetworkManager.shared.loadGenres()
.flatMap{ genres in
genres.publisher.flatMap{ genre in
NetworkManager.shared.fetchMoviesIn(genre)
}
}
.collect()
.retry(1)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion{
case .finished:
print("Finished loading all movies in every genre")
case .failure(let error):
print("Error: \(error)")
}
}, receiveValue: { [self] values in
let allMovies = values.joined()
self.movies = allMovies.map{$0}
})
.store(in: &self.requests)
}
}

Depends on how you want to collect genres and movies.
For example, do you want a genre and a list of movies in that genre? The result could be an array of (Genre, [Movies]).
NetworkManager.shared.loadGenres()
.flatMap { genres in
genres.publisher.setFailureType(to: Error.self)
}
.flatMap { genre in
NetworkManager.shared.fetchMoviesIn(genre)
.map { movies in (genre, movies) }
}
.collect()
Or, if you want an array of (Genre, Movie) tuples, then it's a similar approach, but with an additional level of .flatMap to get individual movies
NetworkManager.shared.loadGenres()
.flatMap { genres in
genres.publisher.setFailureType(to: Error.self)
}
.flatMap { genre in
NetworkManager.shared.fetchMoviesIn(genre)
.flatMap { movies in
movies.publisher.setFailureType(to: Error.self)
}
.map { movie in (genre, movie) }
}
.collect()
To answer your comment question, you want to return the updated Genre, you could return that instead of returning a tuple. Bear in mind that since Genre is a struct, you'd need to create a variable copy of the object (the genre available in the flatMap closure is a constant), update the copy, and return that:
NetworkManager.shared.loadGenres()
.flatMap { genres in
genres.publisher.setFailureType(to: Error.self)
}
.flatMap { genre in
NetworkManager.shared.fetchMoviesIn(genre)
.map { movies -> Genre in
var genreCopy = genre
genreCopy.movies = movies
return genreCopy
}
}
.collect()

Related

Using Combines zip with two api calls that return Result<T, Error>

I have two api calls using Combine and Result that I need to merge so that i can create a new objects from the two.
public func getPosts() -> AnyPublisher<Result<[Post], Error>, Never> {
let client = ForumClient()
return client.execute(.posts)
}
public func getUsers() -> AnyPublisher<Result<[User], Error>, Never> {
let client = ForumClient()
return client.execute(.users)
}
I have working code, but I feel like I'm missing some syntactic sugar that can avoid the two switches.
Publishers.Zip(getPosts(), getUsers())
.sink(
receiveValue: { [weak self] postsResult, usersReult in
guard let self = self else {
return
}
// Get Posts
var posts: [Post] = []
switch postsResult {
case .failure(let error):
print("Error: \(error.localizedDescription)")
case .success(let postsArray):
posts = postsArray
}
// Get Users
var users: [User] = []
switch usersReult {
case .failure(let error):
print("Error: \(error.localizedDescription)")
case .success(let usersArray):
users = usersArray
}
//Combine posts and users
var forumPosts: [ForumPost] = []
posts.forEach { post in
users.forEach { user in
if user.id == post.userId {
let forumPost = ForumPost(username: user.username, title: post.title)
forumPosts.append(forumPost)
}
}
}
print(forumPosts)
})
.store(in: &publishers)
Is there a better way to do this that avoids using two switches etc?
Answering whether another way is "better" is hard.
At some point, you're going to have to deal with the fact that one of your requests for posts or users might fail. That's going to require looking at a Result. So far as I know, that can be done in one of two ways. Either using a switch or using the get() throws function in Result. The advantage of the switch is that you can look at the error and do something.
What I might choose to do, is put the error handling a bit closer to where the error might occur. I would want to have getPosts return a publisher of posts. If an error occurs I might emit the error inside that but still return all the posts I can get. Similarly for users.
Consider the following code that I put together in a Playground:
import UIKit
import Combine
struct Post {
let userId: Int
let title : String
}
struct User {
let userId: Int
let username : String
}
struct ForumPost {
let username: String
let title: String
}
func clientGetPosts() -> AnyPublisher<Result<[Post], Error>, Never> {
let results =
[ Result<[Post], Error>.success([
Post( userId: 1, title: "The World According to Alice"),
Post( userId: 2, title: "The World Accoording to Bob")
]) ]
return results.publisher.eraseToAnyPublisher()
}
func clientGetUsers() -> AnyPublisher<Result<[User], Error>, Never> {
let results =
[ Result<[User], Error>.success([
User(userId: 1, username: "Alice"),
User(userId: 2, username: "Bob")
]) ]
return results.publisher.eraseToAnyPublisher()
}
func getPosts() -> AnyPublisher<[Post], Never> {
clientGetPosts().reduce([]) {
(allPosts : [Post], postResult: Result<[Post], Error>) -> [Post] in
var newCollection = allPosts
switch postResult {
case .success(let newPosts) :
newCollection.append(contentsOf: newPosts)
case .failure(let error) :
print("Retrieving posts errored: \(error.localizedDescription)")
}
return newCollection
}.eraseToAnyPublisher()
}
func getUsers() -> AnyPublisher<[User], Never> {
clientGetUsers().reduce([]) {
(allUsers : [User], usersResult: Result<[User], Error>) -> [User] in
var newCollection = allUsers
switch usersResult {
case .success(let newUsers) :
newCollection.append(contentsOf: newUsers)
case .failure(let error) :
print("Retrieving users errored: \(error.localizedDescription)")
}
return newCollection
}.eraseToAnyPublisher()
}
var users : [User] = []
getUsers().sink { users = $0 }
let forumPosts = getPosts().flatMap {
(posts: [Post]) -> AnyPublisher<ForumPost, Error> in
return posts.publisher.tryMap {
(post:Post) throws -> ForumPost in
if let user = users.first(where: { $0.userId == post.userId }) {
return ForumPost(username: user.username, title: post.title)
} else {
print("Post with title \"\(post.title)\" has an unknown userId")
throw NSError(domain: "posters", code: -1, userInfo: nil)
}
}.eraseToAnyPublisher()
}.eraseToAnyPublisher()
forumPosts.sink(receiveCompletion: { _ in () },
receiveValue: {
(forumPost: ForumPost) in
debugPrint(forumPost)
})
the function clientGetPosts stands in for client.execute(.posts). I've changes getPosts so that it collects all the posts and emits errors, but returns all the posts it can find. There's still a switch statement, but it's closer to where the error is discovered.
Users are handled similarly.
At the bottom, I just grab the list of users. Because of the way we've set up getUsers it will grab as many users as it can and print out errors if any of the clients requests fail.
It then grabs the sequence that returns all the posts and uses flatMap to convert that into a sequence of ForumPosts with an error emitted for any forum post where a corresponding user can't be found.
You can combine the results into a switch statement:
switch (usersResult, postsResult) {
case (.success(let users), .success(let posts)):
print(makeForumPostsFrom(users: users, posts: posts))
case (.failure(let userError), .failure(let postError)):
print("UserError: \(userError)")
print("PostError: \(postError)")
case (.failure(let userError), _):
print("UserError: \(userError)")
case (_, .failure(let postError)):
print("PostError: \(postError)")
}
I created a utility method to create a ForumPost array from the resulting User and Post arrays:
func makeForumPostsFrom(users: [User], posts: [Post]) -> [ForumPost] {
users.reduce(into: .init()) { forumPosts, user in
let usersForumPosts = posts
.filter { post in
post.userId == user.id
}
.map { post in
ForumPost(username: user.username, title: post.title)
}
forumPosts.append(contentsOf: usersForumPosts)
}
}
All I did here was create an empty array of ForumPost, then for each user filter for their posts, then map those posts to a corresponding ForumPost, then store the results in the initial array.

Chaining with flatMap

I'm having difficulty trying to figure out how to loop TMDb results from my publisher 1 and use the value I'm getting from key id for my second publisher. It's not looping through each result, it does get the proper URL inside my func fetchVideos(_ id: Int) but it's not calling each URL from TMDb's results array. I'm not sure if it's because I'm also getting an Array of results from Videos Codable data?
I attempted to use Publishers.MergeMany in my first publisher's flatMap. I'm still definitely at a novice level for combine, any tips would help. I'm trying to get a list of movies, then from the movies get the id key then use that to fetch the movie trailer data for each movie.
print output
https://api.themoviedb.org/3/movie/602223/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/459151/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/385128/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/522478/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/637693/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/529203/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/578701/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/631843/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/645856/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/581644/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/436969/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/568620/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/522931/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/681260/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/630586/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/671/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/618416/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/646207/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/550988/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/482373/videos?api_key=API_KEY&language=en-US
Videos(id: 459151, results: [themoviedb_demo.Video(id: 3F24D610-4261-4AEB-8906-A9D0E5FE8E4D, iso639_1: "en", iso3166_1: "US", key: "CK6xdYIsaa0", name: "DreamWorks\' The Boss Baby: Family Business | Official Trailer #3 | Peacock", site: "YouTube", size: 1080, type: "Trailer"), themoviedb_demo.Video(id: 417EF49C-B983-4CC3-B435-7A902DECE917, iso639_1: "en", iso3166_1: "US", key: "-rF2j6K5FoM", name: "The Boss Baby 2: Family Business – Official Trailer 2 (Universal Pictures) HD", site: "YouTube", size: 1080, type: "Trailer"), themoviedb_demo.Video(id: C34A3F5F-9429-4267-86F0-5506EF3E8281, iso639_1: "en", iso3166_1: "US", key: "QPzy8Ckza08", name: "THE BOSS BABY: FAMILY BUSINESS | Official Trailer", site: "YouTube", size: 1080, type: "Trailer")])
Codable data
struct Videos: Codable {
let id: Int
let results: [Video]
}
struct Video: Codable {
let id = Int
let key: String
let name: String
enum CodingKeys: String, CodingKey {
case id
case key, name
}
}
struct TMDb: Codable {
let results: [Results]?
}
struct Results: Codable {
let id: Int
let releaseDate, title: String?
let name: String?
enum CodingKeys: String, CodingKey {
case id
case releaseDate = "release_date"
case title
case name
}
}
#Published var movies = TMDb(results: Array(repeating: Results(id: 1, releaseDate: "", title: "", name: "") , count: 5))
#Published var videos = Videos(id: 1, results: Array(repeating: Video(id: 1, key: "", name: "") , count: 5))
func getUpcoming() {
var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
request.httpMethod = "GET"
let publisher = URLSession.shared.dataTaskPublisher(for: request)
.map{ $0.data }
.decode(type: TMDb.self, decoder: JSONDecoder())
let publisher2 = publisher
.flatMap{
// loop results from TMDb for id for publisher 2, only one is called
Publishers.MergeMany($0.results!.map { item in
return self.fetchVideos(item.id)
.map { $0 as Videos }
.replaceError(with: nil)
})
}
// Publishers.CombineLatest
Publishers.Zip(publisher, publisher2)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {_ in
}, receiveValue: { movies, videos in
self.movies = movies
self.videos = videos
}).store(in: &cancellables)
}
func fetchVideos(_ id: Int) -> AnyPublisher<Videos, Error> {
let url = URL(string: "https://api.themoviedb.org/3/movie/\(id)/videos?api_key=API_KEY&language=en-US")!
return URLSession.shared.dataTaskPublisher(for: url)
.mapError { $0 as Error }
.map{ $0.data }
.decode(type: Videos.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
Hi #cole for this operation you don't need either the merge or the zip, because you are not subscribing to two publishers, you are attempting to do an action after your first publisher emitted an event.
For this you only need a map .handleEvents in my opinion.
So lets try to enhance your code, we want to update both movies and videos separately, but we still need videos to be dependent of movies
First we will create the publisher to request the movies:
var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
request.httpMethod = "GET"
let publisher = URLSession.shared.dataTaskPublisher(for: request)
.map{ $0.data }
.decode(type: TMDb.self, decoder: JSONDecoder())
Now we enhance this publisher handling by assigning movies:
var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
request.httpMethod = "GET"
let publisher = URLSession.shared.dataTaskPublisher(for: request)
.map{ $0.data }
.decode(type: TMDb.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print ($0) },
receiveValue: { self.movies = $0.results })
Now we will add .handleEvent in order to iterate through our movies to create all the publishers which emit videos events and append videos for the videos array:
var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
request.httpMethod = "GET"
let publisher = URLSession.shared.dataTaskPublisher(for: request)
.map{ $0.data }
.decode(type: TMDb.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print ($0) },
receiveValue: { self.movies = $0.results })
.handleEvents(receiveSubscription:nil, receiveOutput: { [weak self] movies in guard let self = self else {return}
self.videos = [Videos]()
for movie in movies.results {
self.fetchVideos(movie.id)
}, receiveCompletion:nil, receiveCancel:nil, receiveRequest:nil)
})
.store(in: &cancellables)
Now for the last step lets update the fetchVideos accordingly:
func fetchVideos(_ id: Int) {
let url = URL(string: "https://api.themoviedb.org/3/movie/\(id)/videos?api_key=API_KEY&language=en-US")!
return URLSession.shared.dataTaskPublisher(for: url)
.mapError { $0 as Error }
.map{ $0.data }
.decode(type: Videos.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print ($0) },
receiveValue: { [weak self] videos in guard let self = self else {return}
self.videos.append(videos)
})
.store(in: &cancellables)
}
Solved my own question. I needed to create an array for my #Published variable instead of single item. Then I needed to call .collect() and .append() in my publisher's flatMap which will append to my #Published variable videos.

Combine using optional struct variable

I'm trying to load an array of SCEvents into an array of EventModels using Combine. The variable imagePath is optional, which I'd like to translate it to an empty Data() in its corresponding EventModel.imageData variable.
struct SCEvent {
let name: String
let imagePath: String?
}
struct EventModel {
let name: String
let imageData: Data
}
The following code seems to work, but I can't help but wonder if it is the most optimal way of doing it:
func loadEvents(_ events: [SCEvent]) -> AnyPublisher<[EventModel], Error> {
events.publisher
.flatMap(loadEvent(_:))
.collect()
.eraseToAnyPublisher()
}
func loadEvent(_ event: SCEvent) -> AnyPublisher<EventModel, Error> {
if let imagePath = event.imagePath {
return DataDownloader.downloadData(fromPath: imagePath)
.map { EventModel(name: event.name, imageData: $0) }
.eraseToAnyPublisher()
}
return Just(EventModel(name: event.name, imageData: Data())
.eraseToAnyPublisher()
}
Ideally, I'd like to use a single publisher in the loadEvent function. Maybe something like this (doesn't work, but serves as an example of what I expect):
func loadEvent(_ event: SCEvent) -> AnyPublisher<[EventModel], Error> {
event.imagePath
.flatMap(DataDownloader.downloadData(_:))
.replaceNil(with: Data())
.map {
EventModel(name: event.name, imageData: $0)
}
.eraseToAnyPublisher()
}
Which doesn't work because .replaceNil should be used after event.imagePath to replace a nil string. Another possible approach would be:
func loadEvent(_ event: SCEvent) -> AnyPublisher<[EventModel], Error> {
event.imagePath
.replaceNil(with: "")
.flatMap(
DataDownloader.downloadData(_:)
.replaceError(with: Data())
)
.map {
EventModel(name: event.name, imageData: $0)
}
.eraseToAnyPublisher()
}
But it seems forced. Is it even possible with Combine? Is my initial approach the only valid solution?
You can use the publisher of Optional, which gives you a publisher that publishes one element if the optional is not nil, and an empty publisher otherwise.
You can then replaceEmpty(with: Data()), and map to an EventModel.
func loadEvent(_ event: SCEvent) -> AnyPublisher<EventModel, Error> {
event.imagePath.publisher
.flatMap(DataDownloader.downloadData(fromPath:))
.replaceEmpty(with: Data())
.map {
EventModel(name: event.name, imageData: $0)
}
.eraseToAnyPublisher()
}
However, I don't think replacing with a Data() is a good idea. A better design would be to replace with nil, in which case you'll have to map to an optional first:
struct EventModel {
let name: String
let imageData: Data?
}
...
.flatMap(DataDownloader.downloadData(fromPath:))
.map { $0 as Data? } // here
.replaceEmpty(with: nil)

Handling documentID in FirebaseFirestoreSwift is really confusing

I have to query a slew of collections and the models are always defined something like this:
struct Order : Identifiable, Codable {
#DocumentID var id : String?
let fieldOne : String?
let fieldTwo : Int?
enum CodingKeys : String, CodingKey {
case id // (had to comment this out)
case fieldOne
case fieldTwo
}
}
Today, I spent all day trying to figure out why I couldn't load documents from for one particular collection. I was getting a snapshot with documents but could not convert and populate them into an array. After hours of trial and error I commented out the "case id" in the enum and got it to work.
Any idea why this is happening?
Here's a query which works WITH the case id:
listener = db.collection("Meal_Plans").whereField("userId", isEqualTo: userEmail).order(by: "timeOfCreation", descending: true).addSnapshotListener({ (querySnapshot, error) in
if let error = error {
print("error in mp query: \(error.localizedDescription)")
} else {
guard let documents = querySnapshot?.documents else {
print("No mealplans")
return
}
let mealplanArray: [Mealplan] = documents.compactMap { queryDocumentSnapshot -> Mealplan? in
return try? queryDocumentSnapshot.data(as: Mealplan.self)
}
let planViewModel = mealplanArray.map({return PlanViewModel(mealplan: $0)})
DispatchQueue.main.async {
if mealplanArray.count > 0 {
self.planViewModelDelegate?.plansFetched(self.updateHour(sourcePlans: planViewModel))
}
}
}
})
And this is the one WITHTOUT:
listener = db.collection("Placed_Orders").whereField("userId", isEqualTo: userId).whereField("status", isLessThan: 410).order(by: "status", descending: false).order(by: "nextOrderDate", descending: false).addSnapshotListener({ (documentSnapshot, error) in
if let error = error {
print("error")
self.orderCallback?(error.localizedDescription, nil, .error)
} else {
print("empty")
guard let documents = documentSnapshot?.documents else {
return }
if documents.isEmpty {
self.orderCallback?("No orders found", nil, .error)
return
} else {
print("snapshot count: \(documents.count)")
let orderArray: [Order] = documents.compactMap { queryDocumentSnapshot -> Order? in
return try? queryDocumentSnapshot.data(as: Order.self)
}
let orderViewModel = orderArray.map({ return OrderViewModel(order: $0)})
print("array count: \(orderArray.count)")
DispatchQueue.main.async {
print("status: \(orderViewModel)")
self.orderCallback?(nil, orderViewModel, .success)
}
}
}
})
The differences are rather subtle. In both cases I am using a snapshot listener to query the snapshot and populate it into an array and then map that array into a view model.
Yet, in the latter case, I have to comment out the case id for the identifiable field. I need the ID so need to see if it's working but would like to know why I have to comment out the case id.

How do I resolve: Unable to Cast due to 'Unknown Context'?

Scenario:
A function takes a generic parameter (CovidResource) to grab data from a server which returns a data in a particular format depending on the value of the function's parameter, CovidResource.
The intent is to use one (1) function to grab data from assorted endpoints in a format/endpoint.
The function returns a struct AppleSubRegions (in this example, one of any possible data types via generic value) in CovidResource format.
let url = URL(string: "https://disease.sh/v3/covid-19/apple/countries/CANADA")!
countryListViewModel.getList(urlDataModel: CovidResource<AppleSubRegions>(url: url))
struct AppleSubRegions: Codable {
let country, subregion: String
let data: [AppleDatum]
}
import Combine
import UIKit
protocol URLResource {
associatedtype DataModel: Decodable
var url: URL? { get }
}
struct CovidResource<T: Decodable>: URLResource {
typealias DataModel = T
var url = URL(string: "https://disease.sh/v3/covid-19/apple/countries/Canada")
}
// =====================================================================================================
class CountryRegionListModel: ObservableObject {
#Published var countryRegionList: [String] = []
// Data Persistence:
var cancellables: Set<AnyCancellable> = []
// ---------------------------------------------------------------------------
func getList<Resource>(urlDataModel: Resource) where Resource: URLResource {
let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: urlDataModel.url!)
.map(\.data)
.handleEvents(receiveOutput: { data in
print(String(data: data, encoding: .utf8)!)
})
.receive(on: DispatchQueue.main)
.decode(type: Resource.DataModel.self, decoder: JSONDecoder())
//.print("getList: ")
remoteDataPublisher
.eraseToAnyPublisher()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Publisher Finished")
case let .failure(anError):
Swift.print("\nReceived error: ", anError)
}
}, receiveValue: { someValue in
print("\n\n ---- Model: \(someValue)")
DataSource.shared.countryName = (someValue as! AppleSubRegions).country
}).store(in: &cancellables)
}
}
You can see the datatype returned is 'AppleSubRegions':
(Notice the 'unknown context...)?
(lldb) po type(of: someValue)
Covid19.TabNavView.(unknown context at $10bb96490).(unknown context at $10bb96504).AppleSubRegions
I can see the country, 'Canada' via the debugger:
---- Model: AppleSubRegions(country: "Canada", subregions: ["Alberta", "Calgary", "Edmonton", "British Columbia", "Vancouver", "Manitoba", "New Brunswick", "Newfoundland and Labrador", "Northwest Territories", "Halifax", "Nova Scotia", "Ontario", "Ottawa", "Toronto", "Prince Edward Island", "Montreal", "Quebec", "Saskatchewan", "All", "Yukon Territory"])
(lldb) po someValue.country
"Canada"
I tried repeating what I did via the debugger in code:
... but as you can see, the DataModel isn't aware of 'country':
Yet I can't access its members.
So I tried to cast 'someValue' to AppleSubRegions type:
Which crashed the compile.
Question: How do I convert the CovidResource into AppleSubRegions proper (Swift data item)?
Suggestions tried:
1. Qualifying closure with
...receiveValue { someValue: AppleSubRegions in
But I got a compiler syntax error:
Adding an additional constraint:
But this causes another compiler error:
This should work if you further constrain the type to AppleSubRegions:
func getList<R: URLResource>(urlDataModel: R) where R.DataModel == AppleSubRegions {
let remoteDataPublisher = Just(json)
.receive(on: DispatchQueue.main)
.decode(type: R.DataModel.self, decoder: JSONDecoder())
remoteDataPublisher
.sink(
receiveCompletion: {...},
receiveValue: { someValue in
print(someValue.country)
}).store(in: &c)
}