Call func with various struct - ios

I want to create one func which i can used with various struct.
I have several struct and I want use one func with all my struct.
I work with Firestore and want use this one func to access the Firestore.
My first struct:
struct Profile {
var name = ""
var surname = ""
var email = ""
var dictionary: [String: Any] {
return [
"name": name,
"surname": surname,
"email": email
]
}
}
extension Profile: DocumentSerializable {
init?(dictionary: [String: Any], id: String) {
let name = dictionary["name"] as? String ?? ""
let surname = dictionary["surname"] as? String ?? ""
let email = dictionary["email"] as? String ?? ""
self.init(name: name,
surname: surname,
email: email)
}
}
My second struct:
struct FavoriteList {
var favoriteList: [String]
var id: String
var dictionary: [String: Any] {
return [
"favoriteList": favoriteList,
"id": id
]
}
}
extension FavoriteList: DocumentSerializable {
init?(dictionary: [String : Any], id: String) {
let favoriteList = dictionary["favorite"] as? [String] ?? [""]
let id = id
self.init(favoriteList: favoriteList, id: id)
}
}
And my func which I used now to load data from firestore:
func observeQuery() {
guard let query = query else { return }
let time = DispatchTime.now() + 0.5
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
if let snapshot = snapshot {
DispatchQueue.main.asyncAfter(deadline: time) {
let profileModels = snapshot.documents.map { (document) -> Profile in
if let profileModel = Profile(dictionary: document.data(), id: document.documentID) {
return profileModel
} else {
fatalError("Error!")
}
}
self.profile = profileModels
self.document = snapshot.documents
self.tableView.reloadData()
}
}
}
}
So how I can make func observeQuery to use my structs Profile or FavouriteList?

You can use Generic Functions :
func observeQuery<T>(someObject: T) {
if someObject is Profile {
//do something
} else if someObject is FavouriteList {
//do something
}
}

Related

Swift - How to write complex objects to Firebase realtime database

I'm new to the Firebase realtime database and relatively new to Swift in general. I am attempting to build a song request app in which users can create events for guests to request songs from the Spotify API. I'm trying to write an Event object to Firebase, which contains nested objects and arrays of different types. However, when it writes to the database, it only writes the strings and none of the arrays or objects. What is the best way to write all this information to the Firebase Database in a nested structure, so that whenever users add song requests, I can edit the array of requests for the given event in firebase.
Here is my code:
Event.swift
struct Event: Codable{
var code: String
var name: String
var host: String
var description: String
var hostUserId: String
var guestIds: [String]
var requests: [Request]
var queue: [Request]
var played: [Request]
//private var allowExplicit: Bool
//private var eventLocation
init(code: String, name: String, host: String, description: String, hostUserId: String){
self.code = code
self.name = name
self.host = host
self.description = description
self.hostUserId = hostUserId
self.guestIds = []
self.requests = []
self.queue = []
self.played = []
}
func toAnyObject() -> Any{
var guestIdsDict: [String:String] = [:]
for id in guestIds{
guestIdsDict[id] = id
}
var requestsDict: [String: Any] = [:]
for request in requests{
requestsDict[request.getId()] = request.toAnyObject()
}
var queueDict: [String: Any] = [:]
for request in queue{
queueDict[request.getId()] = request.toAnyObject()
}
var playedDict: [String: Any] = [:]
for request in played{
playedDict[request.getId()] = request.toAnyObject()
}
return [
"code": code,
"name": name,
"host": host,
"description": description,
"hostUserId": hostUserId,
"guestIds": guestIdsDict,
"requests": requestsDict,
"queue":queueDict,
"played":playedDict
]
}
}
Request.swift
struct Request: Codable{
private var name: String
private var id: String
private var explicit: Bool
private var album: Album
private var artists: [Artist]
private var likes: Int
init(name: String, id: String, explicit: Bool, album: Album, artists: [Artist]){
self.name = name
self.id = id
self.explicit = explicit
self.album = album
self.artists = artists
self.likes = 1
}
func toAnyObject() -> Any{
var artistsDict: [String:Any] = [:]
for artist in artists {
artistsDict[artist.id] = artist.toAnyObject()
}
return [
"name": name,
"id": id,
"explicit": explicit,
"album": album.toAnyObject(),
"artists": artistsDict,
"likes": likes
]
}
mutating func like(){
self.likes += 1
}
mutating func unlike(){
self.likes -= 1
if(self.likes < 0){
self.likes = 0
}
}
mutating func setLikes(count: Int){
self.likes = count
}
func getLikes() -> Int{
return self.likes
}
func getName() -> String{
return self.name
}
func getId() -> String{
return self.id
}
func getExplicit() -> Bool{
return self.explicit
}
func getAlbum() -> Album {
return self.album
}
func getImages() -> [Image] {
return self.album.images
}
func getArtists() -> [Artist] {
return self.artists
}
func getArtistString() -> String{
var artistString = ""
for (i, artist) in self.artists.enumerated(){
artistString += artist.name
if(i != self.artists.endIndex-1){
artistString += ", "
}
}
return artistString
}
}
Album.swift
struct Album: Codable{
let name: String
let images: [Image]
func toAnyObject() -> Any{
var imagesDict: [String: Any] = [:]
for image in images{
imagesDict[image.url] = image.toAnyObject()
}
return [
"name": name,
"images": imagesDict
]
}
}
Artist.swift
struct Artist: Codable{
let id: String
let name: String
func toAnyObject() -> Any{
return ["id": id, "name": name]
}
}
Image.swift
struct Image: Codable{
let height: Int
let url: String
let width: Int
func toAnyObject() -> Any{
return ["height": height, "url": url, "width": width]
}
}
As you are using Codable, you can create a dic out of it as follows:
Step 1: Add this extension to your code
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] }
}
}
Step 2: Write below code in your Struct (this you have to do in every struct or you can modify code as per your need).
func createDic() -> [String: Any]? {
guard let dic = self.dictionary else {
return nil
}
return dic
}
Now with the help of struct obj, call createDic() method and you will get a dictionary.
And you can send this dictionary to the firebase.
FULL CODE EXAMPLE:
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
}
struct LoginModel: Codable {
let email: String
let password: String
func createDic() -> [String: Any]? {
guard let dic = self.dictionary else {
return nil
}
return dic
}
}
Please comment if you have any questions.
Happy to help!

set value of user rating to Firebase Database

I try to send the voting from users to firebase and save them under the specific user.
class User: NSObject {
var id: String?
init(dictionary: [String: AnyObject]) {
self.id = dictionary["id"] as? String
}
var ref: DatabaseReference!
var numberOfGood = 0
init(id: String? = nil) {
self.id = id
ref = Database.database().reference().child("users").childByAutoId()
}
init(snapshot: DataSnapshot){
ref = snapshot.ref
if let value = snapshot.value as? [String : Any] {
numberOfGood = value["numberOfGood"] as! Int
}
}
func save() {
let postDictionary = [
"id" : self.id,
"numberOfGood" : self.numberOfGood,
] as [String : Any]
self.ref.setValue(postDictionary)
}
}
Inside the viewController where to vote I handle the voting itself like this:
class UserRatingClass {
var numberOfGood = 0
var ref = Database.database().reference().child("users").childByAutoId()
func good() {
numberOfGood += 1
ref.child("numberOfGood").setValue(numberOfGood)
}
}
var userRating: UserRatingClass! {
didSet {
let x = userRating.numberOfGood
self.good.setTitle("\(x) 👍", for: [])
}
}
#IBAction func goodReview(_ sender: UIButton) {
userRating.good()
let x = userRating.numberOfGood
self.good.setTitle("\(x) 👍", for: [])
}
I tried different ways like
var StringGood = String(user?.numberOfGood)
self.ref.child("users").child(StringGood).setValue(x)
inside the buttonActionFunction but by this I'm always getting Cannot invoke initializer for type 'String' with an argument list of type '(Int?)' as an error...
Edit: I call the User.swift class like this:
var user: User?

Realm filter for both parent and child classes

I want to implement filter on both the parent and child, as if search 'chicken2' result should return only lines with meal as 'chicken2' + meals with name 'chicken2', below are my model classes with query and result.
import Foundation
import RealmSwift
class Canteen: Object {
#objc dynamic var name: String?
let lines = List<Line>()
func initWithJSON(json: [String: Any]) {
self.name = json["name"] as? String
let lines = json["lines"] as! [[String: Any]]
for lineJSON in lines {
let line = Line()
line.initWithJSON(json: lineJSON)
self.lines.append(line)
}
}
override static func primaryKey() -> String? {
return "name"
}
}
class Line: Object {
#objc dynamic var name: String?
var meals = List<Meal>()
let canteens = LinkingObjects(fromType: Canteen.self, property: "lines")
func initWithJSON(json: [String: Any]) {
self.name = json["name"] as? String
let meals = json["meals"] as! [[String: Any]]
for mealJSON in meals {
let meal = Meal()
meal.initWithJSON(json: mealJSON)
self.meals.append(meal)
}
}
override static func primaryKey() -> String? {
return "name"
}
}
class Meal: Object {
#objc dynamic var name: String?
#objc dynamic var vegan: Bool = false
let lines = LinkingObjects(fromType: Line.self, property: "meals")
func initWithJSON(json: [String: Any]) {
self.name = json["name"] as? String
self.vegan = json["isVegan"] as! Bool
}
override static func primaryKey() -> String? {
return "name"
}
}
Below is my controller class's viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
let file = Bundle.main.path(forResource: "mealss", ofType: ".json")
let data = try! Data(contentsOf: URL(fileURLWithPath: file!))
let json = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
if let dict = json as? [String: Any] {
let canteen = Canteen()
canteen.initWithJSON(json: dict)
try! realm.write {
realm.add(canteen, update: true)
}
}
realm.objects(Line.self).filter("ANY meals.name contains 'chicken2'")
print(lines.description)
}
below is the output of my print statement.
Below is the json file which i have used.
{
"name": "canteen1",
"lines": [
{
"name": "line1",
"meals": [
{
"name": "chicken2",
"isVegan": false
},
{
"name": "egges",
"isVegan": false
}
]
},
{
"name": "line2",
"meals": [
{
"name": "chicken",
"isVegan": true
},
{
"name": "egges",
"isVegan": true
}
]
}
]
}
Below is my expected output.
[Line {
name = line1;
meals = List<Meal> <0x281301b90> (
[0] Meal {
name = chicken2;
vegan = 0;
}
);
}]
You can retrieve a Meal object and show its parent object's name if you change the Meal class like this.
class Meal: Object {
#objc dynamic var name: String?
#objc dynamic var vegan: Bool = false
let lines = LinkingObjects(fromType: Line.self, property: "meals")
var line: Line { return lines.first! } // <- added
func initWithJSON(json: [String: Any]) {
self.name = json["name"] as? String
self.vegan = json["isVegan"] as! Bool
}
override static func primaryKey() -> String? {
return "name"
}
}
Instead of Line objects, retrieve Meal objects and access their parent object's name.
let meals = realm.objects(Meal.self).filter("name contains 'chicken2'")
for meal in meals {
print("name = \(meal.line.name!)")
print(meal)
}
Here is the output:
name = line1
Meal {
name = chicken2;
vegan = 0;
}

Updating Firestore Database causes iOS crash

When I update the firebase firestore database with any new field, it instantly kills any app running that uses the data with the fatal error in the code below.
The error I get says "fatalError: "Unable to initialize type Restaurant with dictionary [(name: "test", availability: "test", category: "test")]
I'd like to be able to update it without having the apps crash. If that happens, they have to delete and reinstall the app to get it to work again, so I think it's storing the data locally somehow, but I can't find where.
What can I do to make this reset the data or reload without crashing?
The file where the error is thrown (when loading the table data):
fileprivate func observeQuery() {
stopObserving()
guard let query = query else { return }
stopObserving()
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> Restaurant in
if let model = Restaurant(dictionary: document.data()) {
return model
} else {
// Don't use fatalError here in a real app.
fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
}
}
self.restaurants = models
self.documents = snapshot.documents
if self.documents.count > 0 {
self.tableView.backgroundView = nil
} else {
self.tableView.backgroundView = self.backgroundView
}
self.tableView.reloadData()
}
}
And the Restaurant.swift file:
import Foundation
struct Restaurant {
var name: String
var category: String // Could become an enum
var availability: String // from 1-3; could also be an enum
var description: String
var dictionary: [String: Any] {
return [
"name": name,
"category": category,
"availability": availability,
"description": description
]
}
}
extension Restaurant: DocumentSerializable {
//Cities is now availability
static let cities = [
"In Stock",
"Back Order",
"Out of Stock"
]
static let categories = [
"Rock", "Boulder", "Grass", "Trees", "Shrub", "Barrier"
]
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let category = dictionary["category"] as? String,
let availability = dictionary["availability"] as? String,
let description = dictionary["description"] as? String
else { return nil }
self.init(name: name,
category: category,
availability: availability,
description: description
)
}
}
The Local Collection File with the Document.Serializable code:
import FirebaseFirestore
// A type that can be initialized from a Firestore document.
protocol DocumentSerializable {
init?(dictionary: [String: Any])
}
final class LocalCollection<T: DocumentSerializable> {
private(set) var items: [T]
private(set) var documents: [DocumentSnapshot] = []
let query: Query
private let updateHandler: ([DocumentChange]) -> ()
private var listener: ListenerRegistration? {
didSet {
oldValue?.remove()
}
}
var count: Int {
return self.items.count
}
subscript(index: Int) -> T {
return self.items[index]
}
init(query: Query, updateHandler: #escaping ([DocumentChange]) -> ()) {
self.items = []
self.query = query
self.updateHandler = updateHandler
}
func index(of document: DocumentSnapshot) -> Int? {
for i in 0 ..< documents.count {
if documents[i].documentID == document.documentID {
return i
}
}
return nil
}
func listen() {
guard listener == nil else { return }
listener = query.addSnapshotListener { [unowned self] querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> T in
if let model = T(dictionary: document.data()) {
return model
} else {
// handle error
fatalError("Unable to initialize type \(T.self) with local dictionary \(document.data())")
}
}
self.items = models
self.documents = snapshot.documents
self.updateHandler(snapshot.documentChanges)
}
}
func stopListening() {
listener = nil
}
deinit {
stopListening()
}
}
fatalError: "Unable to initialize type Restaurant with dictionary [(name: "test", availability: "test", category: "test")]
Seems pretty straightforward - that dictionary does not contain enough information to create a Restaurant object.
The error is from
if let model = Restaurant(dictionary: document.data()) {
return model
} else {
// Don't use fatalError here in a real app.
fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
}
because your initializer returns a nil value, from:
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let category = dictionary["category"] as? String,
let availability = dictionary["availability"] as? String,
let description = dictionary["description"] as? String
else { return nil }
self.init(name: name,
category: category,
availability: availability,
description: description
)
}
because your guard is returning nil because you do not have a description key in the dictionary.
To fix, either put a description key in the dictionary OR change your initializer to use a default description when the key is missing.
For example, here is your initializer, rewritten to use a default description, for when the description entry is missing
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let category = dictionary["category"] as? String,
let availability = dictionary["availability"] as? String
else { return nil }
let description = dictionary["description"] as? String
let defaultDescription: String = description ?? "No Description"
self.init(name: name,
category: category,
availability: availability,
description: defaultDescription
)
}

how to use updateValue to add an object in Swift

I have a User Struct that I'm casting to Json to be able to get into NSUserDefaults...
import Foundation
struct User {
var name = ""
var stores: [Store] = []
init?(json: [String: AnyObject]) {
if let name = json["name"] as? String,
storesJSON = json["stores"] as? [[String: AnyObject]]
{
self.name = name
self.stores = storesJSON.map { Store(json: $0)! }
} else {
return nil
}
}
init() { }
func toJSON() -> [String: AnyObject] {
return [
"name": name,
"stores": stores.map { $0.toJSON() }
]
}
}
and I am using a Data Manager class (Singleton) to add a new User. But I can't figure out what to pass into updateValue in my addPerson function below? Alternatively is there another way to get this object into NSUserDefaults?
import Foundation
class DataManager {
static let sharedInstance = DataManager()
var users = [String : User]()
init() {
let userDefaults = NSUserDefaults.standardUserDefaults()
if let var userFromDefaults = userDefaults.objectForKey("users") as? [String : User] {
users = userFromDefaults
}
else {
// add default values later
}
}
var userList: [String] {
var list: [String] = []
for userName in users.keys {
list.append(userName)
}
list.sort(<)
return list
}
func addPerson(newUserName: String) {
users.updateValue(User(), forKey: newUserName)
// saveData()
}
You should change your interface of the addPerson function, use addPerson(newUser: User) instead of using addPerson(newUserName: String) as #iosDev82 said:
// Because your addPerson function needs two parameters: a name and a user object
func addPerson(newUser: User) {
users.updateValue(newUser, forKey: newUser.name)
// saveData()
}
so you can:
let newName = textField.text.capitalizedString
let newUser = User(["name": newName, "stores" : []])
DataManager.sharedInstance.addPerson(newUser)
I think you already know how to create a User object. And that is what you should pass as an argument to your following function. Something like this.
var aUser = User(["name": textField.text. capitalizedString])
DataManager.sharedInstance.addPerson(aUser)
func addPerson(newUser: User) {
users[newUser.name] = newUser
// saveData()
}

Resources