Handle populating a tableview with asynchronous API calls - ios

I have 2 objects. Note and User.
public class Note {
var userId: Int
var createdAtDate: NSDate
var content: String?
init(userId: Int, createdAtDate: NSDate) {
self.userId = userId
self.createdAtDate = createdAtDate
}
}
public class User {
var id: Int
var firstName: String
var lastName: String
var email: String
var avatar: UIImage?
init(id: Int, firstName: String, lastName: String, email: String) {
self.id = id
self.firstName = firstName
self.lastName = lastName
self.email = email
}
}
And there are two API methods. One to list out all the notes. This is the JSON response you get.
{
"status": "success",
"data": [
{
"user_id": 2,
"note": "This is a test\r\n\r\nThis is only a test.\r\n\r\nbeep boop",
"created_at": "2015-07-29 04:39:25"
}
]
}
Note that you only get the user's ID in this response. So to get the user details (first name ,last name etc), there's another API call. When you pass the user ID, it gives that user's details. This is its JSON response.
{
"status": "success",
"data": {
"id": 2,
"email": "dev#example.com",
"first_name": "John",
"last_name": "Appleseed",
"avatar": "avt.png"
}
}
Note that both these API methods return results asynchronously. Which brings me to my issue.
I need to display all these notes in a section-ed tableview with each note's user's full name in the footer.
The problem is since the user detail API call is asynchronous, so it happens in a different thread and it takes time to receive the results. But loading notes to the tableview happens separately so I can't figure out how to map each note's user when the user's detail arrives.
func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
let note = notes[section]
APIClient.getUserDetails(id: id, success: { (data) -> Void in
}) { (error) -> Void in
}
return ""
}
For example, let's take the first note in the notes array. It gets the userId of that note and pass it to the getUserDetails method to retrieve the user details. But since this is async, the tableview won't wait until it receives that user's details. By the time you get that, it might have moved on to another note and spinning off async calls all over the place!
Anyone got an idea how to properly tackle this situation?

The approach to solve this might help you.
You could fire the second request (getUserDetails) in the success block of your first API call. (Or whichever method/delegate you would be using to handle receiving data from the server)
On success of the second request, you could simply reload your table to show the appropriate data
Since it takes time to show details you could add an activity indicator and hide the table till you receive all required data from the server.
I am assuming that you are using a separate block or delegate that would do the job of handling data response from the server.
If you strategically place your server requests on the success handling methods, one after the other, even though your data is interlinked and the calls are asynchronous - you get yourself a synchronous-like flow.
Hope that helps!

Related

Adding a custom object to an array in Firestore

I am making an ordering app for customers to order their specific specs. When the user logs in they can go to a tab that contains a tableview with all their specs, once they click on a cell it will take them to a new view controller that will display more information on the spec. Once on this view controller they will have the ability to add x amount of pallets/rolls/etc of that item. I am able to add the spec to Firestore, but I cannot get it to an array in Firestore which I need. My goal is that on anther tab the user can view all the current specs they are trying to order until they hit submit. I am currently using the user.uid to get to that specific customers orders inside Firestore.
Code:
#IBAction func addPallet(_ sender: Any) {
// Get the current user
let user = Auth.auth().currentUser
if let user = user {
_ = user.uid
}
if spec != nil {
// Get the qty ordered for that spec
let totalQty: Int? = Int(palletsToAddTextField.text!)
let qty = spec!.palletCount * totalQty!
let specToAdd = Spec(specNumber: spec!.specNumber,
specDescription: spec!.specDescription,
palletCount: spec!.palletCount,
palletsOrdered: qty)
orderedArray.append(specToAdd)
let specAdded: [String: Any] = [
"SpecDesc": spec!.specDescription,
"SpecNum": spec!.specNumber,
"PalletCount": spec!.palletCount,
"PalletsOrder": qty
]
db.collection("orders").document(user?.uid ?? "error").setData(specAdded) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
}
code for spec:
struct Spec: Codable {
// Properties
var specNumber: String
var specDescription: String
var palletCount: Int
var palletsOrdered = 0
enum CodingKeys: String, CodingKey {
case specNumber
case specDescription
case palletCount
case palletsOrdered
}
}
I need something added like the below picture. The user will add x amount of pallets, then going to the next spec they want and add it to the array in Firestore as well.
Ok i guess i get what you want to do. Try:
db.collection("orders").document(userID).setData(["allMyData" : myArray])
"allMyData" will be the name of the field in which you want to save your array and myArray would be your array (specAdded). Thats what you are looking for?
If the document already exists you will want to use .updateData instead of .setData to keep all other fields that might already exist in that specific doc.
Kind regards

Alamofire: Issue with decimal type value response

I am facing one issue while fetching data from server using Alamofire. Could anyone please help me out.
Here is my WS response:
{
"id": "32321",
"name": "product name",
"brandName": "product brand name",
"ratings": 4.6364,
"totalReviews": 11,
}
And my model is
struct FALProductModel: Decodable, FALProductBasicInfo {
var id: String?
var name: String?
var brandName: String?
let ratings: Double?
let totalReviews: Int?
}
When I am fetching data using Alamofire Alamofire.responseData. ratings and totalReviews params are missing.
I tried with Alamofire.responseJson to check the response but here also the same issue that json is not returning above two params.
I tried with a simple URLSession and here I am getting the proper response.
does anyone have any idea what I am doing wrong.

How to handle failure cases of JSON response using Codable?

I have some JSON response, that I take from a server. In success case, it might be like:
{
"success": true,
"data": [
{
/// something here
}
]
}
If all server responses would be successful, it would be really easy to parse that JSON. But we have also failure cases like:
{
"success": false,
"msg": "Your session expired",
"end_session": true
}
That means we need to handle two cases. As you noticed, attributes like success, msg may occur in any response. In order to handle that, I created following struct:
struct RegularResponse<T: Codable>: Codable {
let success: Bool
let msg: String?
let endSession: Bool?
let data: T?
enum CodingKeys: String, CodingKey {
case success, msg, data
case endSession = "end_session"
}
}
It may contain some data if response is successfull or otherwise, it is possible to identify why the error occurred(using success attribute or msg). Parsing process would go like following:
let model = try JSONDecoder().decode(RegularResponse<MyModel>.self, from: data)
if model.success {
// do something with data
} else {
// handle error
}
Everything works fine, but what if following JSON comes as following:
{
"success": true,
"name": "Jon Snow",
"living_place": "Nights Watch",
//some other fields
}
Here, I don't have data attribute. It means, my RegularResponse cannot be parsed. So, the question is how to handle these kind of situations? My idea for solution is simple: always put data in success cases into data field on my API. By doing so, my RegularResponse will always work, no matter what is inside data. But, it requires changes on a server side. Can this be fixed in a client side, not changing a server side? In other words, how to handle above situation in Swift using Codable?
I'm not sure if this is the best solution but if you know that your error response is in that shape, i.e.:
{
"success": false,
"msg": "Some error",
"end_session": "true",
}
then you could make another Codable struct/class that follows this response.
struct ErrorResponse: Codable {
let success: Bool
let msg: String
let end_session: String
}
and then when you are responding to your JSON you could adjust your code to:
if let successResponse = try? JSONDecoder().decode(RegularResponse<MyModel>.self, from: data) {
//handle success
} else if let responseError = try? JSONDecoder().decode(ErrorResponse.self, from data) {
//handle your error
}

Vapor 3: transform array of Future object to an array of Future other objects

I tried to make the most basic example that I could think of for my problem. I have a Course model and a many-to-many table to User that also stores some extra properties (the progress in the example below).
import FluentPostgreSQL
import Vapor
final class Course: Codable, PostgreSQLModel {
var id: Int?
var name: String
var teacherId: User.ID
var teacher: Parent<Course, User> {
return parent(\.teacherId)
}
init(name: String, teacherId: User.ID) {
self.name = name
self.teacherId = teacherId
}
}
struct CourseUser: Pivot, PostgreSQLModel {
typealias Left = Course
typealias Right = User
static var leftIDKey: LeftIDKey = \.courseID
static var rightIDKey: RightIDKey = \.userID
var id: Int?
var courseID: Int
var userID: UUID
var progress: Int
var user: Parent<CourseUser, User> {
return parent(\.userID)
}
}
Now, when I return a Course object, I want the JSON output to be something like this:
{
"id": 1,
"name": "Course 1",
"teacher": {"name": "Mr. Teacher"},
"students": [
{"user": {"name": "Student 1"}, progress: 10},
{"user": {"name": "Student 2"}, progress: 60},
]
}
Instead of what I would normally get, which is this:
{
"id": 1,
"name": "Course 1",
"teacherID": 1,
}
So I created some extra models and a function to translate between them:
struct PublicCourseData: Content {
var id: Int?
let name: String
let teacher: User
let students: [Student]?
}
struct Student: Content {
let user: User
let progress: Int
}
extension Course {
func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
let teacherQuery = self.teacher.get(on: req)
let studentsQuery = try CourseUser.query(on: req).filter(\.courseID == self.requireID()).all()
return map(to: PublicCourseData.self, teacherQuery, studentsQuery) { (teacher, students) in
return try PublicCourseData(id: self.requireID(),
name: self.name,
teacher: teacher,
students: nil) // <- students is the wrong type!
}
}
}
Now, I am almost there, but I am not able to convert studentsQuery from EventLoopFuture<[CourseUser]> to EventLoopFuture<[Student]>. I tried multiple combinations of map and flatMap, but I can't figure out how to translate an array of Futures to an array of different Futures.
The logic you're looking for will look like this
extension Course {
func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
return teacher.get(on: req).flatMap { teacher in
return try CourseUser.query(on: req)
.filter(\.courseID == self.requireID())
.all().flatMap { courseUsers in
// here we should query a user for each courseUser
// and only then convert all of them into PublicCourseData
// but it will execute a lot of queries and it's not a good idea
}
}
}
}
I suggest you to use the SwifQL lib instead to build a custom query to get needed data in one request 🙂
You could mix Fluent's queries with SwifQL's in case if you want to get only one course, so you'll get it in 2 requests:
struct Student: Content {
let name: String
let progress: Int
}
extension Course {
func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
return teacher.get(on: req).flatMap { teacher in
// we could use SwifQL here to query students in one request
return SwifQL.select(\CourseUser.progress, \User.name)
.from(CourseUser.table)
.join(.inner, User.table, on: \CourseUser.userID == \User.id)
.execute(on: req, as: .psql)
.all(decoding: Student.self).map { students in
return try PublicCourseData(id: self.requireID(),
name: self.name,
teacher: teacher,
students: students)
}
}
}
}
If you want to get a list of courses in one request you could use pure SwifQL query.
I simplified desired JSON a little bit
{
"id": 1,
"name": "Course 1",
"teacher": {"name": "Mr. Teacher"},
"students": [
{"name": "Student 1", progress: 10},
{"name": "Student 2", progress: 60},
]
}
first of all let's create a model to be able to decode query result into it
struct CoursePublic: Content {
let id: Int
let name: String
struct Teacher:: Codable {
let name: String
}
let teacher: Teacher
struct Student:: Codable {
let name: String
let progress: Int
}
let students: [Student]
}
Ok now we are ready to build a custom query. Let's build it in some request handler function
func getCourses(_ req: Request) throws -> Future<[CoursePublic]> {
/// create an alias for student
let s = User.as("student")
/// build a PostgreSQL's json object for student
let studentObject = PgJsonObject()
.field(key: "name", value: s~\.name)
.field(key: "progress", value: \CourseUser.progress)
/// Build students subquery
let studentsSubQuery = SwifQL
.select(Fn.coalesce(Fn.jsonb_agg(studentObject),
PgArray(emptyMode: .dollar) => .jsonb))
.from(s.table)
.where(s~\.id == \CourseUser.userID)
/// Finally build the whole query
let query = SwifQLSelectBuilder()
.select(\Course.id, \Course.name)
.select(Fn.to_jsonb(User.table) => "teacher")
.select(|studentsSubQuery| => "students")
.from(User.table)
.join(.inner, User.table, on: \Course.teacherId == \User.id)
.join(.leftOuter, CourseUser.table, on: \CourseUser.teacherId == \User.id)
.build()
/// this way you could print raw query
/// to execute it in postgres manually
/// for debugging purposes (e.g. in Postico app)
print("raw query: " + query.prepare(.psql).plain)
/// executes query with postgres dialect
return query.execute(on: req, as: .psql)
/// requests an array of results (or use .first if you need only one first row)
/// You also could decode query results into the custom struct
.all(decoding: CoursePublic.self)
}
Hope it will help you. There may be some mistakes in the query cause I wrote it without checking 🙂 You can try to print a raw query to copy it and execute in e.g. Postico app in postgres directly to understand what's wrong.

Listing Firebase data and relationships

I have just started working with Firebase database and I am a bit confused how to structure the database. In the following example I have a users object and a groups object. Each user can be part of multiple groups and every group can have multiple users. The proposed database structure is the following, according to "Structure Your Database".
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
"groups": {
"techpioneers": true,
"womentechmakers": true
}
}
},
"groups": {
"techpioneers": {
"name": "Historical Tech Pioneers",
"startDate": "24-04-1820",
"members": {
"alovelace": true,
"ghopper": true,
"eclarke": true
}
}
}
}
Let's say I want to display all groups in a list in my app, with the group name and start date. How would I make that database call? Since the user object only contains the id of the groups, would I then have to make a separate call to the database for every group just to find out the name and start date of the group? If there are many groups in the list, then that becomes a lot of calls. My group might contain a lot of other information as well so this doesn't seem good for performance. Can I get all the groups in the groups list of the user, in one call?
One though I had was to include the name and start date in the groups object under the user:
"users": {
"alovelace": {
"name": "Ada Lovelace",
"groups": {
"techpioneers":{
"name": "Historical Tech Pioneers",
"startDate": "24-04-1820"
},
"womentechmakers":{
"name": "Women in Technology",
"startDate": "13-10-1823"
}
}
}
}
}
but this solution seems to add a lot of duplicate data. Also if I want to update the name I would have to do that in multiple locations. And maybe I want to add a sponsor organization object, that also contains group, and then want to list them. Then there would be 3 places to update the information on. How would I solve this?
You would then have two possibilities, one would be to store the data you need (duplicating it) in the groups node of each user.
The other, which is the one that I would recommend the most, would be to add an observeSingleEvent(of: .value) inside your first observer (that could be an observe(.value), observe(.childAdded) or whatever).
Say you have an array of all your group members, and an object named AppUser that represents a user in your app :
var groupMembers = [AppUser]()
To detect whenever a new member is added to a group for example, you could use a .childAdded observer for example :
func groupWasAddedObserver(completion: #escaping () -> ()) {
// Add a .childAdded observer to the group's members node (groupId should be defined somewhere) :
groupsRef.child(groupId).child("members").observe(.childAdded, with: { [weak self] (snapshot) in
// When you get the snapshot, store its key, which represents the member id :
let memberId = snapshot.key
// fetch this member's profile information using its id :
self?.getUser(memberId, completion: { (groupMember) in
// Once you got the profile information of this member, add it to an array of all your members for example :
self?.groupMembers.append(groupMember)
// Call the completion handler so that you can update the UI or reload a table view somewhere maybe depending on your needs :
completion()
})
})
}
And the second method to fetch a user data knowing his or her id :
func getUser(_ userId: String, completion: #escaping (AppUser) -> ()) {
// Add the observerSingleEvent observer :
usersRef.child(userId).observeSingleEvent(of: .value, with: { (snapshot) in
// Get the data you need using the snapshot you get :
guard let email = snapshot.childSnapshot(forPath: "email").value as? String else { return }
guard let name = snapshot.childSnapshot(forPath: "name").value as? String else { return }
guard let picUrl = snapshot.childSnapshot(forPath: "picUrl").value as? String else { return }
// Call the completion handler to return your user/member :
completion(AppUser(id: snapshot.key, email: email, name: name, picUrl: picUrl))
})
}
As you can see you get the memberId of each user using the snapshot key, and you use this memberId to fetch this specific user data.

Resources