SwiftUI - How to structure relational data in state - swift

I'm new to Swift and I am having trouble understanding how to structure relational data in a larger app.
Consider this api json response
// posts
{
"entities": {
"posts": [
{
"id": "1",
"content": "I am a post!"
"user": {
"id": "1",
"username": "user1"
}
},
{
"id": "2",
"content": "I am another post!"
"user": {
"id": "1",
"username": "user1"
}
}
]
}
}
// posts/featured
{
"entities": {
"posts": [
{
"id": "2",
"content": "I am another post!"
"user": {
"id": "1",
// username is not needed in the featured posts UI
}
}
]
}
}
There are a couple of things to keep in mind:
posts and posts/featured represent two independent screens on the app, each displaying a separate set of posts. Overlaps are allowed, as in the response above.
The api is tied to the app views, i.e. it will return only the data that is directly used in the app. posts/featured does not show the username, hence it is not returned by the api.
If a post's content is changed in posts, that update should automatically be applied to the same post in posts/featured only if it is there.
My attempt
Here is how I attempted to model this.
The Post and User structs
struct Post {
let id: String
var content: String?
var user: String?
init(json: [String: Any]) {
let id = json["id"] as! String
let content = json["content"] as? String ?? nil
let user = json["user"] as? [String: Any] ?? nil
self.id = id
self.content = content
self.user = user != nil ? user!["id"] as? String : nil
}
}
struct User {
let id: String
var username: String?
init(json: [String: Any]) {
let id = json["id"] as! String
let username = json["username"] as? String ?? nil
self.id = id
self.username = username
}
}
My entityState
class EntityState: ObservableObject {
#Published var posts: [String: Post]
#Published var users: [String: User]
... more stuff
}
Updating data across screens
My only solution is to normalize the state, and have Post.user be a String representing the User.id. With that, I keep in EntityState the dictionaries of posts and users, and in my ViewModels, I keep a local array of strings representing the postIds of the specific screen.
To update the data, only one update is required in the EntityState model and it will propagate everywhere it is being referenced by id.
Using Post and User struct in the code
Since User.username will sometimes be empty depending on where it is requested, it forces me to deal with conditionals or worse, use User.username! everywhere, which I don't think is correct, yet I am unsure of a better way.
What is the best way to structure this? I also control the api, so I am flexible in returning different data.

let json = """
{
"entities": {
"posts": [
{
"id": "1",
"content": "I am a post!",
"user": {
"id": "1",
"username": "user1"
}
},
{
"id": "2",
"content": "I am another post!",
"user": {
"id": "1",
"username": "user1"
}
},
{
"id": "2",
"content": "I am another post!",
"user": {
"id": "1"
}
}
]
}
}
"""
// Simulate data received from a network call
let data = json.data(using: .utf8)!
struct Entity: Codable {
var entities: [String : [Post]]
}
struct Post: Codable {
let id: String
var content: String?
var user: User?
}
struct User: Codable {
let id: String
var username: String?
}
do {
let entity = try JSONDecoder().decode(Entity.self, from: data)
print(entity)
}catch {
print(error)
}
Inspecting in the console:
po entity
▿ Entity
▿ entities : 1 element
▿ 0 : 2 elements
- key : "posts"
▿ value : 3 elements
▿ 0 : Post
- id : "1"
▿ content : Optional<String>
- some : "I am a post!"
▿ user : Optional<User>
▿ some : User
- id : "1"
▿ username : Optional<String>
- some : "user1"
▿ 1 : Post
- id : "2"
▿ content : Optional<String>
- some : "I am another post!"
▿ user : Optional<User>
▿ some : User
- id : "1"
▿ username : Optional<String>
- some : "user1"
▿ 2 : Post
- id : "2"
▿ content : Optional<String>
- some : "I am another post!"
▿ user : Optional<User>
▿ some : User
- id : "1"
- username : nil
RE: Well, this structure doesn't provide any details/explanation regarding state normalisation and why you didn't use it, nor does it provide any explanation for dealing with optional properties of structs in the code. It also doesn't address the problem of updating one Post/User entity and having that update be reflected across the app in multiple states. I've listed in my question 3 "requirements" to keep in mind, and my attempted solution for each at the end. I was hoping for a more complete answer around those points of discussion. – Darius Mandres 3 hours ago
State management in SwiftUI does not happen in the data. These structs are used for bringing in json to a more useable format. A popular way to do that in swift is by using structs. This will likely happen async on a background thread depending how you are getting your json. If you want to lean about state management in SwiftUI I suggest looking here https://developer.apple.com/documentation/swiftui/state-and-data-flow
2)Optionals are a part of swift. They replaced nil pointers of objective-c which were also very popular. If you want to avoid them you should consider two struct one Posts one Featured. In most cases if I'm dealing with ui personally I like to use a default value. Such as Text(userName ?? "") or Text(userName ?? "User Name Unavailable"), but that's up to you as a programmer, If by mistake a featured finds its way into posts should your app crash so you can find it?
Structs in swift are copies they are passed by copy not reference. If you modify a copy it applies to that copy. You can use a class and copy a pointer throughout the app, or you can notify people using that same struct of changes.
I think you may want to consider breaking your question up into specifics and posting individual questions if you still need help.

Related

URLSession POST request with json "_id" element with codable class in Swift

I'm trying to use URLSessions to get and post data. I don't have problems with get requests on Swift, just post. I followed this
I have a codable class that looks like this:
class Item: Codable {
var _id: String?
var name: String = ""
var color: String = ""
var rating: Int = 0
init(name: String, color: String, rating: Int){
self.name = name
self.color = color
self.rating = rating
}
}
And json data that looks like this:
[ {
"_id" : "5e50a10c4ea5d87f0001c9da",
"name" : "Pepper",
"color" : "blue",
"rating" : 4
},
{
"_id" : "5e50a10c4ea5d87f0001c9db",
"name" : "Pepper",
"color" : "blue",
"rating" : 2
},
{
"_id" : "5e50a10c4ea5d87f0001c9dc",
"name" : "Pepper"
"color" : "blue"
"rating" : 6
}
]
I'm currently using restdb.io for my database, and have tested my requests using Postman for all Get,Post,Put,...etc.
On Postman, when I create a POST request with Json body with just name, color and rating elements, it will generate a unique _id without me having to specify.
When I do this on Swift and send a post request with Item object using the init() method, where I left _id as an optional in the class, my code crashes with an "Unexpectedly found nil while implicitly unwrapping an Optional value". How do I work around that?
If you don't need _id in your swift code you can ignore it and not include it in your codable struct. In this way, the codable will ignore it and not process it

Unable to create an array of JSON's and assign it to a key which are to be send as parameters to Alamofire Post request?

I have a Post Request, in which I am trying to create an Array of json which the user types and then send to the server, I have used dictionary and it is working for a single request but not for multiple requests.
The JSON structure to be sent is
{
"id" : "u_101"
"data" : [
{ "name" : "Shubham"
"age" : "23"
},
{
"name" : "S"
"age" : "20"
}
]
}
Here is what I am using in swift for setting the parameters of alamofire request.
func setData (id: String, data: [Any]) {
request.httpMethod = post
var parameters = Parameters()
parameters["id"] = id
parameters["data"] = data
}
Then in the view controller I am doing this, (Items contain a dictionary of entered data through the view )
var allData : [Any] = []
for item in items {
var data: [String:String] = [:]
data["name"] = item.key
data["age"] = item.value
allData.append(data)
}
setData(id: "u_101", data: alldata)
This is not working and the server is throwing error.
If I send this to the Alamofire post request.
{
"id" : "u_101"
"data" : [
{ "name" : "Shubham"
"age" : "23"
}
]
}
The server responds with a success.

Type Mismatch with Decodable and Object

I've got a problem with parsing data from the server. I've got JSON which has an array of objects, something like this:
{
"items": [
{
"itemType": {
"id": 12,
"tagId": "FCHA78558D"
},
"parts": [
{
"partId": 52,
"manufacturer": "xxx"
},
{
"partId": 53,
"manufacturer": "xxx"
},
{
"partId": 54,
"manufacturer": "xxx"
}
],
"description": "yyy"
},
{
"itemType": {
"id": 13,
"tagId": "FCHA755158D"
},
"parts": [
{
"partId": 64,
"manufacturer": "xxx"
},
{
"partId": 65,
"manufacturer": "xxx"
}
],
"description": "zzz"
}
]
}
I only want to obtain this one array of objects so I implemented this class like this:
class User : Object, Decodable {
var items = List<Equipment>()
}
in the Alamofire I'm downloading the JSON, parsing it to data and then in do-catch block I receive an error:
let items = try JSONDecoder().decode(User.self, from: receivedValue)
error:
▿ DecodingError
▿ typeMismatch : 2 elements
- .0 : Swift.Array<Any>
▿ .1 : Context
▿ codingPath : 2 elements
- 0 : CodingKeys(stringValue: "items", intValue: nil)
▿ 1 : _JSONKey(stringValue: "Index 0", intValue: 0)
- stringValue : "Index 0"
▿ intValue : Optional<Int>
- some : 0
- debugDescription : "Expected to decode Array<Any> but found a dictionary instead."
- underlyingError : nil
That's weird because it is an array of objects for sure. I tried setting my items property to String to see the result and then I got:
- debugDescription : "Expected to decode String but found an array instead."
I had this error couple of times but I always managed to find the solution.
I suppose you were using the List conditional conformance to Decodable from my answer to your previous question. I don't fully understand why it doesn't work in this specific case, but I'll investigate.
Until then, you can make decoding work by manually implementing the init(from decoder:Decoder) function.
class User : Object, Decodable {
let items = List<Equipment>()
private enum CodingKeys: String, CodingKey {
case items
}
required convenience init(from decoder:Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
let itemsArray = try container.decode([Equipment].self, forKey: .items)
self.items.append(objectsIn: itemsArray)
}
}

Alamofire multi parameters dictionary

Hi i am trying to give to alamofire parameters called "addons" that are in array...array can contain 3 or X items. I am trying to use FOR cycle to ad dictionary to another another one set of items, but...it only shows the last one...that seems it override the previous one. I tried everything I know...Even try to use SwiftyJSON framework....but alamofire only take pure dictionary type.
let itemsArr = ["Skirts", "Coat", "Shirt"]
let priceArr = ["7.00", "7.00", "2.90"]
let quantityArr = ["2", "5", "1"]
let personalInfo: [String : Any] = [
"phone" : phone,
"notes" : descNote
]
var para: [String: Any] = [
"pieces" : pieces,
"personal_info" : personalInfo,
"payment_method" : paymentMethod
]
for i in 0..<itemsArr.count {
let addons: [String: Any] = [
"name":itemsArr[i],
"price":priceArr[i],
"quantity":quantityArr[i]
]
print(addons)
para["addons"] = addons
}
well I need something like this
{
"pieces": 12,
"personal_info": {
"phone": "+420783199102",
"notes": "Plz be fast, I need to play Game of War"
},
"payment_method": "cod",
"addons": [
{
"name": "Select day Tue",
"price": 3.5,
"quantity": 1
},
{
"name": "Select day Thu",
"price": 3.5,
"quantity": 1
}
]
}
Your problem is that in loop you are overwriting variable every single iteration with single result. That's why only last one is left for you.
What you should do is:
//create an array to store the addons outside of the loop
var addons: [[String: Any]] = []
for i in 0..<itemsArr.count {
let addon: [String: Any] = [
"name":itemsArr[i],
"price":priceArr[i],
"quantity":quantityArr[i]
]
//append a single addon to our array prepared before the loop
addons.append(addon)
}
//once we gathered all addons, append results to `para` dictionary
para["addons"] = addons

Dictionary wrong order - JSON

I am trying to create a dictionary that I can make into a JSON formatted object and send to the server.
Example:
var users = [
[
"First": "Albert",
"Last": "Einstein",
"Address":[
"Street": "112 Mercer Street",
"City": "Princeton"]
],
[
"First": "Marie",
"Last": "Curie",
"Address":[
"Street": "108 boulevard Kellermann",
"City": "Paris"]]
]
I use this function
func nsobjectToJSON(swiftObject: NSObject) -> NSString {
var jsonCreationError: NSError?
let jsonData: NSData = NSJSONSerialization.dataWithJSONObject(swiftObject, options: NSJSONWritingOptions.PrettyPrinted, error: &jsonCreationError)!
var strJSON = NSString()
if jsonCreationError != nil {
println("Errors: \(jsonCreationError)")
}
else {
// everything is fine and we have our json stored as an NSData object. We can convert into NSString
strJSON = NSString(data: jsonData, encoding: NSUTF8StringEncoding)!
println("\(strJSON)")
}
return strJSON
}
But my result is this:
[
{
"First" : "Albert",
"Address" : {
"Street" : "112 Mercer Street",
"City" : "Princeton"
},
"Last" : "Einstein"
},
{
"First" : "Marie",
"Address" : {
"Street" : "108 boulevard Kellermann",
"City" : "Paris"
},
"Last" : "Curie"
}
]
Problem: why is the last name last? I think it should be above address. Please let me know what I am doing wrong with the NSDictionary for this to come out wrong. Any help would be very much appreciated - thank you.
To post what has already been said in comments: Dictionaries are "unordered collections". They do not have any order at all to their key/value pairs. Period.
If you want an ordered collection, use something other than a dictionary. (an array of single-item dictionaries is one way to do it.) You can also write code that loads a dictionary's keys into a mutable array, sorts the array, then uses the sorted array of keys to fetch key/value pairs in the desired order.
You could also create your own collection type that uses strings as indexes and keeps the items in sorted order. Swift makes that straightforward, although it would be computationally expensive.
I did like this.
let stagesDict = NSDictionary()
if let strVal = sleepItemDict["stages"] as? NSDictionary {
stagesDict = strVal
let sortedKeys = (stagesDict.allKeys as! [String]).sorted(by: <)
var sortedValues : [Int] = []
for key in sortedKeys {
let value = stagesDict[key]!
print("\(key): \(value)")
sortedValues.append(value as! Int)
}
}