Require a dictionary to contain certain values (Swift) - ios

I am creating a model for saving user data to a Firestore database and am initializing it with a dictionary. Depending on what fields I want to update, I put those values in the dictionary so that the user model only contains certain fields and will therefore only update those fields in my database. However, I want to somehow require that certain fields are provided in certain use cases.
* Below example is a very simplified version of what I am trying to do *
For example: if I am saving a new user, I want to make sure that I include a name, a profile image, and a description. But if I simply want to update a field, then I don't want to require that all those fields are included
I am 99% certain I am attacking this the wrong way, so any help is appreciated.
My Current User Model:
struct FirestoreUser {
var id: String
var name: String?
var profileImage: String?
var dictionary: [String: Any]
init(dictionary: [String: Any]) {
self.id = dictionary["id"] as! String
self.name = dictionary["name"] as? String
self.profileImage = dictionary["profileImage"] as? String
self.dictionary = dictionary
}
}
// MARK: Firestore functions
extension FirestoreUser {
typealias Handler = ((Error?) -> Void)?
func saveNewFirestoreUser(then handler: Handler = nil) {
// make sure all necessary variables are set
// if they aren't all set, something went wrong
guard let _ = name, let _ = profileImage else { return }
let firestore = Firestore.firestore()
let ref = firestore.collection("users").document(id)
ref.setData(dictionary) { (error) in
if let error = error {
handler?(error)
}
handler?(nil)
}
}
}

Construct struct with optional value and pass nil as the parameter which you don't want to update.
You can use the below extension to convert struct to dictionary later.
struct FirestoreUser: Codable {
var id: String
var name: String?
var profileImage: String?
}
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
Use -
let test = FirestoreUser(id: "01", name: "Abhinav", profileImage: nil)
print(test.dictionary)
Output -
["name": Abhinav, "id": 01]

In realtime database use updateChildValues instead of setValue if you just want to update the child. I believe there's something equivalent in firestore.
Answer for realtime database:
Update
self.ref.child("users/\(user.uid)/username").setValue(username)
Set new data
let key = ref.child("posts").childByAutoId().key
let post = ["uid": userID,
"author": username,
"title": title,
"body": body]
let childUpdates = ["/posts/\(key)": post,
"/user-posts/\(userID)/\(key)/": post]
ref.updateChildValues(childUpdates)
Answer for firestore:
Just read the documentation for firestore, use updateData instead of addDocument:
let washingtonRef = db.collection("cities").document("DC")
// Set the "capital" field of the city 'DC'
washingtonRef.updateData([
"capital": true
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
Set/Add new data
// Add a new document with a generated id.
var ref: DocumentReference? = nil
ref = db.collection("cities").addDocument(data: [
"name": "Tokyo",
"country": "Japan"
]) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID: \(ref!.documentID)")
}
}
So rule of thumb is to only pass in the field that you need to update instead of the whole collection/dictionary.

Related

Having Trouble Pulling Data From Firebase RT Database

Super new to coding so apologies if something is super obvious here.
I'm working on an app that I can use to keep track of my weight lifting split. I write the data like this:
public func writeNewExercise(splitName: String, day: Int, exerciseNum: Int, exerciseName: String, sets: String, repsSecs: String, isTimed: Bool, completion: #escaping (Bool) -> Void) {
let user = AuthManager.shared.user
var exerciseRef: DatabaseReference!
exerciseRef = Database.database().reference(withPath: "\(user.uid)/splits/\(splitName)/day \(day)/exercise \(exerciseNum)")
var dataDictionary: [String: Any] = [:]
dataDictionary["Exercise Name"] = exerciseName
dataDictionary["Sets"] = sets
dataDictionary["Reps or Secs"] = repsSecs
dataDictionary["Is Timed"] = isTimed
exerciseRef.setValue(dataDictionary) { error, _ in
if error == nil {
completion(true)
return
} else {
completion(false)
return
}
}
}
This gives me a JSON dictionary in Firebase that looks like this:
{
"8aIzPgurRLPPEYDpXWv54r5JjvH3" : {
"splits" : {
"Test Split" : {
"day 1" : {
"exercise 0" : {
"Exercise Name" : "Curls",
"Is Timed" : false,
"Reps or Secs" : "12",
"Sets" : "4"
}
}
}
}
},
What I want to do now is to pull this data so I can insert each exercise into a tableView cell. Don't want to do anything fancy with it -- just be able to view it so I can follow my split. I'm doing this more for practice than practicality. I've tried pulling the data about 15 different ways, and no matter what I do it just won't work. I'm totally stumped. Here is the code I have right now:
public func downloadPost(splitName: String, day: Int, completion: #escaping (Bool) -> Void){
let user = AuthManager.shared.user
var exerciseRef: DatabaseReference!
exerciseRef = Database.database().reference()
var exerciseArray = [Exercise]()
exerciseRef.child("Users").child(user.uid).child("splits").child(splitName).child("day \(day)").observe(.value) { snapshot in
if snapshot.exists(){
for x in 0...100{
let nameValue = snapshot.childSnapshot(forPath: "exercise \(x)/Exercise Name").value
let setsValue = snapshot.childSnapshot(forPath: "exercise \(x)/Sets").value
let repsOrSecsValue = snapshot.childSnapshot(forPath: "exercise \(x)//Sets/Reps or Secs").value
let isTimedValue = snapshot.childSnapshot(forPath: "exercise \(x)/Sets/Is Timed").value
let exercise = Exercise(name: "\(nameValue!)",
sets: "\(setsValue!)",
repsOrSecs: "\(repsOrSecsValue!)",
isTimed: isTimedValue as? Bool ?? false)
print(exercise.name)
print(exercise.sets)
print(exercise.repsOrSecs)
print(exercise.isTimed)
exerciseArray.append(exercise)
completion(true)
return
}
} else {
print("no snapshot exists")
}
print(exerciseArray)
}
}
Exercise is a custom class I've created that has a name, amount of sets, amount of reps, and a Bool "isTimed". This code prints:
no snapshot exists, []
Trying other things, I've got it to print something like:
null,
0,
0,
false
Some other stuff I've tried has been:
using slash navigation instead of chaining .childs in the .observe.value
using .getData instead of .observe
throwing DispatchQueue.main.async all over the place
making the exerciseRef be the whole database, then calling to the specific point when assigning the snapshot.value
Much else
I've probably put something like 15 hours into just this at this point, and I really cannot figure it out. Any help would be massively appreciated. I'll watch this post closely and post any info that I may have left out if it's needed.
Thanks!
UPDATE
Got everything working by using the code provided by Medo below. For others trying to do something like this, after pulling the array as Medo demonstrated, just set all the labels in your tableViewCell to ExportedArray[indexPath.row].theClassPropertyYouWant
Here is my solution:
public func downloadPost(splitName: String, day: Int, completion: #escaping (([Exercise]) -> ())){
let user = AuthManager.shared.user
var exerciseRef: DatabaseReference!
exerciseRef = Database.database().reference()
var exerciseArray = [Exercise]()
exerciseRef.child(user.uid).child("splits").child(splitName).child("day \(day)").observe(.value, with: { snapshot in
guard let exercises = snapshot.children.allObjects as? [DataSnapshot] else {
print("Error: No snapshot")
return
}
for exercise in exercises {
let exerciseData = exercise.value as? [String:Any]
let exerciseName = exerciseData["Exercise Name"] as? String
let isTimed = exerciseData["Is Timed"] as? Bool
let repsOrSecs = exerciseData["Reps or Secs"] as? String
let sets = exerciseData["Sets"] as? String
let exerciseIndex = Exercise(name: "\(exerciseName)",
sets: "\(sets)",
repsOrSecs: "\(repsOrSecs)",
isTimed: isTimed)
exerciseArray.append(exerciseIndex)
}
completion(exerciseArray)
}
}
You can call the function downloadPost and extract the array from it like this:
downloadPost(splitName: "", day: 0, completion: {
aNewArray in
// aNewArray is your extracted array [Exercise]
print("\(aNewArray)")
})
Few things to be aware of:
If you want to ensure that your storing your exercises in order (and extract the data in order) then instead of having exercises 0, 1, 2... (in your database), name it by an id called "childByAutoId". Firebase will auto order them for you as you add/push or extract that data. Replace your writeNewExercise function with:
let user = AuthManager.shared.user
var exerciseRef: DatabaseReference!
let key = Database.database().reference().childByAutoId().key ?? ""
exerciseRef = Database.database().reference(withPath: "\(user.uid)/splits/\(splitName)/day \(day)/\(key)")
var dataDictionary: [String: Any] = [:]
dataDictionary["Exercise Name"] = exerciseName
dataDictionary["Sets"] = sets
dataDictionary["Reps or Secs"] = repsSecs
dataDictionary["Is Timed"] = isTimed
exerciseRef.setValue(dataDictionary) { error, _ in
if error == nil {
completion(true)
return
} else {
completion(false)
return
}
}
Firebase Realtime Database is a breadth first search and download. So you should probably flatten out your database structure as much as possible. This means observing on exerciseRef.child("Users").child(user.uid).child("splits").child(splitName).child("day \(day)") would still download all the exercise days.

how to add Json value into model Array to display into tableview in swift

I'm using the tableview to display the Two Json value but the problem is I cant add value into model struct to displaying into tableview using two Api's. i want to show percentage value in one of the cell label and
here is my json
[
{
"Percentage": 99.792098999,
}
]
my second json value
{
"Categories": [
"Developer",
"ios "
],
"Tags": [
{
"Value": "kishore",
"Key": "Name"
},
{
"Value": "2",
"Key": "office"
},
]
}
and i need show the Categories value in Categories label in tableview
value and key on tableview
here is my Struct
struct info: Decodable {
let Categories: String?
let Tags: String?
let Value: String?
let Key: String?
var Name: String?
let percentage: Double?
}
here its my code
var List = [info]()
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers)
print(json as Any)
guard let jsonArray = json as? [[String: Any]] else {
return
}
print(jsonArray)
for dic in jsonArray{
guard let per = dic["percentage"] as? Double else { return }
print(per)
}
and second json
if let array = json["Tags"] as? [[String: String]] {
for dict in array {
let key = dict["Key"]
let value = dict["Value"]
switch key {
case "office":
case "Name":
default:
break;
}
}
here is my cell for row indexpath
cell.Categories.text = list[indexpath.row].percentage
cell.Name.text = list[indexpath.row].name
cell.office.text = list[indexpath.row].office
Please use Swift 4 Codable protocol to decode the value from JSON.
//1.0 Create your structures and make it conform to Codable Protocol
struct Tags: Codable{
var Key: String
var Value: String
}
struct Sample: Codable{
var Categories: [String]
var Tags: [Tags]
}
In your method, perform below steps:
//2.0 Get your json data from your API. In example below, i am reading from a JSON file named "Sample.json"
if let path = Bundle.main.path(forResource: "Sample", ofType: "json") {
do {
let jsonData = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
do {
//3.0 use JSONDecoder's decode method to decode JSON to your model.
let sample = try JSONDecoder().decode(Sample.self, from: jsonData)
//4.0 User the "sample" model to do your stuff. Example, printing some values
print("Sample.Category = \(sample.Categories)")
print("Sample.Name = \(sample.Tags[0].Value)")
print("Sample.Office = \(sample.Tags[1].Value)")
} catch let error {
print("Error = \(error)")
}
} catch {
// handle error
}
}
I prefer to use Codable all the time with JSON even for simpler types so for percentage I would do
struct ItemElement: Decodable {
let percentage: Double
enum CodingKeys: String, CodingKey {
case percentage = "Percentage"
}
}
and we need to keep these values in a separate array, declared as a class property
let percentageList: [Double]()
and json encoding would then be
let decoder = JSONDecoder()
do {
let result = try decoder.decode([ItemElement].self, from: data)
percentageList = result.map { item.percentage }
} catch {
print(error)
}
Similar for the second part
struct Item: Decodable {
let categories: [String]
let tags: [Tag]
enum CodingKeys: String, CodingKey {
case categories = "Categories"
case tags = "Tags"
}
}
struct Tag: Decodable {
let value, key: String
enum CodingKeys: String, CodingKey {
case value = "Value"
case key = "Key"
}
}
use a dictionary for the result, again as a class property
var values = [String: String]()
and the decoding
let decoder = JSONDecoder()
do {
let result = try decoder.decode(Item.self, from: data)
for item in result.tags {
values[item.key] = values.item.value
}
} catch {
print(error)
}
and then in the cell for row code
cell.Categories.text = percentageList[indexpath.row].percentage
cell.Name.text = values["name"]
cell.office.text = values["office"]
Note that this last code looks very strange since you don't have an array of name/office values judging by your json. Maybe you have simplified it some way but the code above is the best I can do with the information given even if it possibly wrong

Swift JSON response two field values append into single array

I am trying to validated students == null or values avilable, If values avilable I need to get grade and store grade into table data array and subject null also I need to store in same array For example: [10, null, 11] from below JSON. how to append like this in single array from JSON response.
{
"students":[
{
"id":0,
"subject":[
{
"grade":10
}
]
},
{
"id":1,
"subject":null
},
{
"id":2,
"subject":[
{
"grade":11
}
]
}
]
}
Expected output: [10,null,11,......] //This array I am going to use Tableview cell
I am validating based on null and not null array values within cell for row. I can use var array = [String?] for accepting null values but how to append two different field result into same array?
You should take a look into the 'Codable' protocol.
By simply defining a struct like:
struct Student: Codable
you can decode it from JSON into these objects.
See for example: hackernoon or grokswift
This looks like a trivial scenario. Best solution is Decodable. Your payload loaded from network or whatever will be parsed into structure. Now you can easily make any manipulations.
Setup: Open a new project. Add "payload.json" file with json payload you provided in question.
Add the following to your project.
import UIKit
struct StudentData: Decodable {
var students: [Student]
}
struct Student: Decodable {
var id: Int
var subject: [Subject]?
}
struct Subject: Decodable {
var grade: Int
}
class ViewController: UIViewController {
var data: Data? {
guard let path = Bundle(for: type(of: self)).path(forResource: "payload", ofType: "json") else { return nil }
return try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
}
override func viewDidLoad() {
super.viewDidLoad()
if let data = data {
do {
let studentData = try JSONDecoder().decode(StudentData.self, from: data)
print(studentData)
// manipulate the structure in any way you want
let subjects: [Subject?] = studentData.students.map { $0.subject?.first }
print(subjects)
let nonNilValues = subjects.compactMap { $0 }
print(subjects)
// ... etc
} catch let error {
print(error.localizedDescription)
}
}
}
}
Sorry for not coding in playgrounds. It's way too buggy.
Try this
let students = [["id": 0,"subject": [["grade": 10]]],
["id": 0,"subject": nil],
["id": 0,"subject": [["grade": 10]]]] as! [Dictionary<String,Any>]
let array = students.map({(($0["subject"] as? [Any])?.first as? Dictionary<String,Int>)?["grade"]})
print(array)

If a value in a json array returns null, how to return the next non-null json value?

Currently I'm trying to return values from JSON data but am running into an issue where one of the values is returning null thereby causing my app to crash.
This is what the json looks like:
"results": [
{
"genre_ids": [
99
],
"id": 440108,
"poster_path": null, //The value that's causing incredible headaches!
},
{
"genre_ids": [
99,
10402
],
"id": 391698,
"poster_path": "/uv7syi4vRyjvWoB8qExbqnbuCu5.jpg",//The value trying to get!
},
]
I'm using the json object mapper Gloss which has been good to this point. Here is how I have my objects set up:
public struct ResultsGenrePosters: Decodable {
public let results : [GenrePosters]?
public init?(json: JSON) {
results = "results" <~~ json
}
}
public struct GenrePosters: Decodable, Equatable{
public let poster : String
public init? (json: JSON) {
guard let poster: String = "poster_path" <~~ json
else {return nil}
self.poster = poster
}
public static func ==(lhs: GenrePosters, rhs: GenrePosters) -> Bool {
return lhs.poster == rhs.poster
}
static func updateGenrePoster(genreID: NSNumber, urlExtension: String, completionHandler:#escaping (_ details: [String]) -> Void){
let nm = NetworkManager.sharedManager
nm.getJSONData(type:"genre/\(genreID)", urlExtension: urlExtension, completion: {
data in
if let jsonDictionary = nm.parseJSONData(data)
{
guard let genrePosters = ResultsGenrePosters(json: jsonDictionary)
else {
print("Error initializing object")
return
}
guard let posters = genrePosters.results
else {
print("No poster exists for genre: \(genreID)")// This keeps on triggering when the null object is hit, this is where it would be good to move to the next array to get that value
return
}
let postersArray = posters.map {$0.poster}// converts custom object "GenrePosters" to String(poster value)
completionHandler(postersArray)
}
})
}
}
guard let poster: String = "poster_path" <~~ json
should be
guard let poster = ("poster_path" <~~ json) as? String
(Of course you are using a library that I know nothing about, so the library could be crashing. JSONSerialization is your friend. It works. Everyone knows what it does).
for data in results as! [Dictionary<String,AnyObjects>]{
var value1 = data["key"] as? String
if value1 == nil{value1 = ""}else{}
}
}
That was just a simple logic but completely work for me all the time. :)

How can I store a Dictionary with RealmSwift?

Considering the following model:
class Person: Object {
dynamic var name = ""
let hobbies = Dictionary<String, String>()
}
I'm trying to stock in Realm an object of type [String:String] that I got from an Alamofire request but can't since hobbies has to to be defined through let according to RealmSwift Documentation since it is a List<T>/Dictionary<T,U> kind of type.
let hobbiesToStore: [String:String]
// populate hobbiestoStore
let person = Person()
person.hobbies = hobbiesToStore
I also tried to redefine init() but always ended up with a fatal error or else.
How can I simply copy or initialize a Dictionary in RealSwift?
Am I missing something trivial here?
Dictionary is not supported as property type in Realm.
You'd need to introduce a new class, whose objects describe each a key-value-pair and to-many relationship to that as seen below:
class Person: Object {
dynamic var name = ""
let hobbies = List<Hobby>()
}
class Hobby: Object {
dynamic var name = ""
dynamic var descriptionText = ""
}
For deserialization, you'd need to map your dictionary structure in your JSON to Hobby objects and assign the key and value to the appropriate property.
I am currently emulating this by exposing an ignored Dictionary property on my model, backed by a private, persisted NSData which encapsulates a JSON representation of the dictionary:
class Model: Object {
private dynamic var dictionaryData: NSData?
var dictionary: [String: String] {
get {
guard let dictionaryData = dictionaryData else {
return [String: String]()
}
do {
let dict = try NSJSONSerialization.JSONObjectWithData(dictionaryData, options: []) as? [String: String]
return dict!
} catch {
return [String: String]()
}
}
set {
do {
let data = try NSJSONSerialization.dataWithJSONObject(newValue, options: [])
dictionaryData = data
} catch {
dictionaryData = nil
}
}
}
override static func ignoredProperties() -> [String] {
return ["dictionary"]
}
}
It might not be the most efficient way but it allows me to keep using Unbox to quickly and easily map the incoming JSON data to my local Realm model.
I would save the dictionary as JSON string in Realm. Then retrive the JSON and convert to dictionary. Use below extensions.
extension String{
func dictionaryValue() -> [String: AnyObject]
{
if let data = self.data(using: String.Encoding.utf8) {
do {
let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: AnyObject]
return json!
} catch {
print("Error converting to JSON")
}
}
return NSDictionary() as! [String : AnyObject]
} }
and
extension NSDictionary{
func JsonString() -> String
{
do{
let jsonData: Data = try JSONSerialization.data(withJSONObject: self, options: .prettyPrinted)
return String.init(data: jsonData, encoding: .utf8)!
}
catch
{
return "error converting"
}
}
}
UPDATE 2021
Since Realm 10.8.0, it is possible to store a dictionary in a Realm object using the Map type.
Example from the official documentation:
class Dog: Object {
#objc dynamic var name = ""
#objc dynamic var currentCity = ""
// Map of city name -> favorite park in that city
let favoriteParksByCity = Map<String, String>()
}
Perhaps a little inefficient, but works for me (example dictionary from Int->String, analogous for your example):
class DictObj: Object {
var dict : [Int:String] {
get {
if _keys.isEmpty {return [:]} // Empty dict = default; change to other if desired
else {
var ret : [Int:String] = [:];
Array(0..<(_keys.count)).map{ ret[_keys[$0].val] = _values[$0].val };
return ret;
}
}
set {
_keys.removeAll()
_values.removeAll()
_keys.appendContentsOf(newValue.keys.map({ IntObj(value: [$0]) }))
_values.appendContentsOf(newValue.values.map({ StringObj(value: [$0]) }))
}
}
var _keys = List<IntObj>();
var _values = List<StringObj>();
override static func ignoredProperties() -> [String] {
return ["dict"];
}
}
Realm can't store a List of Strings/Ints because these aren't objects, so make "fake objects":
class IntObj: Object {
dynamic var val : Int = 0;
}
class StringObj: Object {
dynamic var val : String = "";
}
Inspired by another answer here on stack overflow for storing arrays similarly (post is eluding me currently)...

Resources