Extract data after network call in Swift - ios

This is a question about the objc.io video about networking.
Note: I have seen similar questions, but none of the suggestions worked
for me.
In the sample code, the main call is:
Webservice().load(resource: Episode.all) { result in
print(result!)
}
and the load function looks like this:
func load<A>(resource: Resource<A>, completion: #escaping (A?) -> ()) {
URLSession.shared.dataTask(with: resource.url as URL) { data, _, _ in
guard let data = data else {
completion(nil)
return
}
completion(resource.parse(data as NSData))
}.resume()
}
For simplicity they just print the result (an array of dictionaries), but I don't understand how I can further use it, because it is inside the closure. This way I can eg show the array in a UITableView or something like that.
So what I would like to do is something like:
var episodes = [Episode]()
Webservice().load(resource: Episode) { result in
episodes = result!
}
print(episodes)
But this results in an empty array being printed.
I also tried using
DispatchQueue.main.async {
episodes = result!
}
But again, nothing is printed.
How can I accomplish that?

Related

Firebase function not getting called inside a forEach loop with a DispatchGroup

I apologize if this question is simple or the problem is obvious as I am still a beginner in programming.
I am looping over an array and trying to make an async Firestore call. I am using a DispatchGroup in order to wait for all iterations to complete before calling the completion.
However, the Firestore function is not even getting called. I tested with print statements and the result is the loop iterations over the array have gone through with an enter into the DispatchGroup each time and the wait is stuck.
func getUserGlobalPlays(username: String, fixtureIDs: [Int], completion: #escaping (Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { ids in
group.enter()
print("entered")
DispatchQueue.global().async { [weak self] in
self?.db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: ids).getDocuments { snapshot, error in
guard let snapshot = snapshot, error == nil else {
completion(.failure(error!))
return
}
for document in snapshot.documents {
let fixtureDoc = document.data()
let fixtureIDx = fixtureDoc["fixtureID"] as! Int
let choice = fixtureDoc["userChoice"] as! Int
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
group.leave()
print("leaving")
}
}
}
group.wait()
print(plays.count)
completion(.success(plays))
}
There are a few things going on with your code I think you should fix. You were dangerously force-unwrapping document data which you should never do. You were spinning up a bunch of Dispatch queues to make the database calls in the background, which is unnecessary and potentially problematic. The database call itself is insignificant and doesn't need to be done in the background. The snapshot return, however, can be done in the background (which this code doesn't do, so you can add that if you wish). And I don't know how you want to handle errors here. If one document gets back an error, your code sends back an error. Is that how you want to handle it?
func getUserGlobalPlays(username: String,
fixtureIDs: [Int],
completion: #escaping (_result: Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { id in
group.enter()
db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: id).getDocuments { snapshot, error in
if let snapshot = snapshot {
for doc in snapshot.documents {
if let fixtureIDx = doc.get("fixtureIDx") as? Int,
let choice = doc.get("choice") as? Int {
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
}
} else if let error = error {
print(error)
// There was an error getting this one document. Do you want to terminate
// the entire function and pass back an error (through the completion
// handler)? Or do you want to keep going and parse whatever data you can
// parse?
}
group.leave()
}
}
// This is the completion handler of the Dispatch Group.
group.notify(queue: .main) {
completion(.success(plays))
}
}

Swift combine Convert Publisher type

I'm exploring Combine Swift with this project https://github.com/sgl0v/TMDB
and I'm trying to replace its imageLoader with something that supports Combine: https://github.com/JanGorman/MapleBacon
The project has a function that returns the type AnyPublisher<UIImage?, Never>.
But the imageLoader MapleBacon library returns the type AnyPublisher<UIImage, Error>.
So I'm trying to convert types with this function:
func convert(_ loader: AnyPublisher<UIImage, Error>) -> AnyPublisher<UIImage?, Never> {
// here.
}
I actually found a question that is kinda similar to mine, but the answers weren't helpful:
https://stackoverflow.com/a/58234908/3231194
What I've tried to so far (Matt's answer to the linked question).
The sample project has this function:
func loadImage(for movie: Movie, size: ImageSize) -> AnyPublisher<UIImage?, Never> {
return Deferred { return Just(movie.poster) }
.flatMap({ poster -> AnyPublisher<UIImage?, Never> in
guard let poster = movie.poster else { return .just(nil) }
let url = size.url.appendingPathComponent(poster)
let a = MapleBacon.shared.image(with: url)
.replaceError(with: UIImage(named: "")!) // <----
})
.subscribe(on: Scheduler.backgroundWorkScheduler)
.receive(on: Scheduler.mainScheduler)
.share()
.eraseToAnyPublisher()
}
if I do replaceError,
I get the type Publishers.ReplaceError<AnyPublisher<UIImage, Error>>
BUT, I was able to solve this one, by extending the library.
extension MapleBacon {
public func image(with url: URL, imageTransformer: ImageTransforming? = nil) -> AnyPublisher<UIImage?, Never> {
Future { resolve in
self.image(with: url, imageTransformer: imageTransformer) { result in
switch result {
case .success(let image):
resolve(.success(image))
case .failure:
resolve(.success(UIImage(named: "")))
}
}
}
.eraseToAnyPublisher()
}
}
First, you need to map a UIImage to a UIImage?. The sensible way to do this is of course to wrap each element in an optional.
Then, you try to turn a publisher that sometimes produces errors to a publisher that Never produces errors. You replaceError(with:) an element of your choice. What element should you replace errors with? The natural answer, since your publisher now publishes optional images, is nil! Of course, assertNoFailure works syntactically too, but you might be downloading an image here, so errors are very likely to happen...
Finally, we need to turn this into an AnyPublisher by doing eraseToAnyPublisher
MapleBacon.shared.image(with: url)
.map(Optional.some)
.replaceError(with: nil)
.eraseToAnyPublisher()

Using parameters on async call in swift

I'm having an async call with a completionhandler that fetches data for me through a query. These queries can vary based upon the users action.
My data call looks like this;
class DataManager {
func requestVideoData(query: QueryOn<VideoModel>, completion: #escaping (([VideoModel]?, UInt?, Error?) -> Void)) {
client.fetchMappedEntries(matching: query) { (result: Result<MappedArrayResponse<FRVideoModel>>) in
completion(videos, arrayLenght, nil)
}
}
}
My ViewController looks like this;
DataManager().requestVideoData(query: /*One of the queries below*/) { videos, arrayLength, error in
//Use the data fetched based on the query that has been entered
}
My queries look like this;
let latestVideosQuery = QueryOn<FRVideoModel>().limit(to: 50)
try! latestVideosQuery.order(by: Ordering(sys: .createdAt, inReverse: true))
And this;
let countryQuery = QueryOn<FRVideoModel>()
.where(valueAtKeyPath: "fields.country.sys.contentType.sys.id", .equals("country"))
.where(valueAtKeyPath: "fields.country.fields.countryTitle", .includes(["France"]))
.limit(to: 50)
But I'm not completely sure how I would implement these queries the right way so they correspond with the MVC model.
I was thinking about a switch statement in the DataManager class, and pass a value into the query parameter on my ViewController that would result in the right call on fetchMappedEntries(). The only problem with this is that I still need to execute the correct function according to my query in my VC, so I would need a switch statement over there as well.
Or do I need to include all my queries inside my ViewController? This is something I think is incorrect because it seems something that should be in my model.
This is somewhat subjective. I think you are right to want to put the construction of the queries in your DataManager and not in your view controller.
One approach is to dumb down the request interface, so that the view controller only needs to pass a simple request, say:
struct QueryParams {
let limit: Int?
let country: String?
}
You would then need to change your DataManager query function to take this instead:
func requestVideoData(query: QueryParams, completion: #escaping (([VideoModel]?, UInt?, Error?) -> Void))
Again, this is subjective, so you have to determine the tradeoffs. Dumbing down the interface limits the flexibility of it, but it also simplifies what the view controller has to know.
In the end I went with a slightly modified networking layer and a router where the queries are stored in a public enum, which I can then use in my functions inside my ViewController. Looks something like this;
public enum Api {
case latestVideos(limit: UInt)
case countryVideos(countries: [String], limit: UInt)
}
extension Api:Query {
var query: QueryOn<VideoModel> {
switch self {
case .latestVideos(let limit):
let latestVideosQuery = QueryOn<VideoModel>().limit(to: limit)
try! latestVideosQuery.order(by: Ordering(sys: .createdAt, inReverse: true))
return latestVideosQuery
case .countryVideos(let countries, let limit):
let countryQuery = QueryOn<VideoModel>()
.where(valueAtKeyPath: "fields.country.sys.contentType.sys.id", .equals("country"))
.where(valueAtKeyPath: "fields.country.fields.countryTitle", .includes(countries))
.limit(to: limit)
return countryQuery
}
}
}
And the NetworkManager struct to fetch the data;
struct NetworkManager {
private let router = Router<Api>()
func fetchLatestVideos(limit: UInt, completion: #escaping(_ videos: [VideoModel]?, _ arrayLength: UInt?,_ error: Error?) -> Void) {
router.requestVideoData(.latestVideos(limit: limit)) { (videos, arrayLength, error) in
if error != nil {
completion(nil, nil, error)
} else {
completion(videos, arrayLength, nil)
}
}
}
}

Swift - design to only parse Json once (currently same data gets parsed for every user selection)

I'm Learning Swift development for IOS and encountered a design problem in my simple Project. I have a pickerView set up so that everytime user selects a value, different information from the Json is displayed and it works just fine.
However, in my current design the data gets parsed/fetched again everytime the user selects a new value from the pickerview, what I want to do is to collect the data once and then just loop through the same data based on the users selection. My guess is that i need to separate the function to load the data and the function/code to actually do the looping and populate the labels. But I can't seem to find any way to solve it, when i try to return something from my loadData function I get problems with the returns already used inside the closure statements inside the function.
Hopefully you guys understand my question!
The selectedName variable equals the users selected value from the pickerView.
The function loadData gets run inside the pickerView "didselectrow" function.
func loadData() {
let jsonUrlString = "Here I have my Json URL"
guard let url = URL(string: jsonUrlString) else
{ return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do { let myPlayerInfos = try
JSONDecoder().decode(Stats.self, from: data)
DispatchQueue.main.async {
for item in myPlayerInfos.elements! {
if item.web_name == self.selectedName{
self.nameLabel.text = "Name:\t \t \(item.first_name!) \(item.second_name!)"
} else {}
}
}
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}//end function loaddata
And for reference, the Stats struct:
struct Stats: Decodable {
let phases: [playerPhases]?
let elements: [playerElements]?
}
struct playerPhases: Decodable{
let id: Int?
}
struct playerElements: Decodable {
let id: Int?
let photo: String?
let first_name: String?
let second_name: String?
}

Filtering an API Request Response by an object array?

I am making an API call for muscles relating to an exercise, the call looks like this:
func loadPrimaryMuscleGroups(primaryMuscleIDs: [Int]) {
print(primaryMuscleIDs)
let url = "https://wger.de/api/v2/muscle"
Alamofire.request(url).responseJSON { response in
let jsonData = JSON(response.result.value!)
if let resData = jsonData["results"].arrayObject {
let resData1 = resData as! [[String:AnyObject]]
if resData1.count == 0 {
print("no primary muscle groups")
self.musclesLabel.isHidden = true
} else {
print("primary muscles used for this exercise are")
print(resData)
self.getMuscleData(muscleUrl: resData1[0]["name"] as! String)
}
}
}
}
This returns me a whole list of all the muscles available, I need it to just return the muscles the exercise requires. This is presented in exercises as an array of muscle id's, which I feed in via the viewDidLoad below
self.loadPrimaryMuscleGroups(primaryMuscleIDs: (exercise?.muscles)!)
So I am feeding the exercises muscle array into the func as an [Int] but at this point im stumped on how to filter the request so that the resulting muscle data are only the ones needed for the exercise.
I was thinking it would be something like using primaryMuscleIDs to filter the id property of a muscle in the jsonData response, but im not sure how to go about that?
Thanks for any clarify here, hopefully I have explained it clearly enough to come across well
You'd want to do something like this in your else block:
var filteredArray = resData1.filter { item in
//I'm not exactly sure of the structure of your json object,
//but you'll need to get the id of the item as an Int
if let itemId = item["id"] as? Int {
return primaryMuscleIDs.contains(itemId)
}
return false
}
//Here with the filtered array
And since the Alamofire request is asynchronous, you won't be able to return a result synchronously. Your method will need to take a callback that gets executed in the Alamofire response callback with the filtered response.
Something like (but hopefully with something more descriptive than an array of Any:
func loadPrimaryMuscleGroups(primaryMuscleIDs: [Int], callback: ([Any]) -> ()) {
//...
Alamofire.request(url).responseJSON { response in
//...
//once you get the filtered response:
callback(filteredArray)
}
}
Finally, check out How to parse JSON response from Alamofire API in Swift? for the proper way to handle a JSON response from Alamofire.

Resources