I'm using SharkORM on iOS Swift project and I'm having problem with a specific object. I have other objects in the project that works, but this one.
My class is like this:
import Foundation
import SharkORM
class Exam: SRKObject {
dynamic var serverId: NSNumber?
dynamic var type: String?
dynamic var when: String?
dynamic var file: String?
dynamic var filename: String?
dynamic var url: String?
func toJson() -> [String:Any?] {
return [
"name" : type,
"date" : when,
"serverId" : serverId,
"file" : file,
"filename" : filename,
"url" : url,
"id" : id
]
}
static func fromJson(_ json: [String:Any?]) -> Exam {
let exam = Exam()
exam.id = json["id"] as? NSNumber ?? NSNumber(value: 0)
exam.type = json["name"] as? String ?? ""
exam.file = json["file"] as? String ?? ""
exam.filename = json["filename"] as? String ?? ""
exam.url = json["url"] as? String ?? ""
exam.serverId = json["serverId"] as? NSNumber ?? NSNumber(value: 0)
exam.when = json["date"] as? String ?? ""
return exam
}
}
I add to an array objects that needs to be saved and after user press save button, the app starts committing it.
// save exams
for exam in self.examsToSave {
if !exam.commit() {
print("Error commiting exam.")
}
}
if let rs = Exam.query().fetch() {
print("exams: \(rs.count)")
}
The commit method returns true and I added a print right after it finishes committing and result is zero.
Any idea?
I found out the problem right after post it. In my text here, my variable "when" was colored like a keyword. I just changed the name to "whenDate" and it started committing. Weird it didn't show up any error or a crash. Anyway, a variable named "when" is not allowed inside a SRKObject.
Given same Commit problem, figured best to keep to topic here. And I've spent number of hours trying to debug this so thought I'd try this:
I have a simple class (and overly simplified but tested as provided here):
class user: SRKObject {
#objc dynamic var name: String = ""
}
(No, no odd syntax coloring on the object property names.)
And I do the following (simplified test case), first defining
public var currUser = user()
Then in a function:
let users = user.query().fetch() as! [user]
if users.count > 0 {
currUser = users[0]
NSLog("Num users \(users.count) name \(currUser.name)")
} else {
self.currUser.name = "T1 User"
if !self.currUser.commit() {
print ("Failed to commit")
}
else {
let u = user.query().fetch()
print("Num users \(u.count)")
}
}
The commit() call succeeds -- at least I don't get the "Failed to commit" message. However, I do get zero count in the last fetch().
Viewing the DB file (in Simulator) from a "DB Browser for SQLite" shows the DB is created fine but the "user" record is not in there, and neither is the "committed" data.
BTW when I had this code in SRKTransaction.transaction, it DID fall into the failure (rollback) block, so yes, did get a transaction error, but tracking that down will be next.
In the meantime, appreciate in advance any help given this case as presented should work.
#retd111, I copied and pasted your code and got the same error.
Then, I moved the currUser to a local var, like this:
override func viewDidLoad() {
super.viewDidLoad()
var currUser: user? = nil
let users = user.query().fetch() as! [user]
if users.count > 0 {
currUser = users[0]
NSLog("Num users \(users.count) name \(currUser!.name)")
} else {
currUser = user()
currUser!.name = "T1 User"
if !currUser!.commit() {
print ("Failed to commit")
}
else {
let u = user.query().fetch()
print("Num users \(u?.count ?? 0)")
}
}
}
It works without problems.
For some reason, if you instantiate the currUser as a class member variable, as your example:
public var currUser = user()
it won't work.
Related
I am on Swift 4. The goal is to load all the data in an address book, before render the address book in view. In a different language such as js, I may use await in each item in the loop, before telling the view to render the rows. I am looking for the canonical way to solve this issue in Swift 4 with UITableViewController.
Right now the address book is stored in backend with Amplify and GraphQL. I have a User model of form
type User #Model {
id: ID!
name: String!
bio : String!
}
and Contact of form
type Contact #model {
ownerId: ID!
userId: ID!
lastOpened: String
}
In ContactController: UITableViewController.viewDidLoad I fetch all Contact in database where the ownerId is my user's id-token, I then create an object using this contact information. And then for each Contact object instance, I get its corresponding User in database when the object is initialized. Per this post: Wait until swift for loop with asynchronous network requests finishes executing, I am using Dispatch group, and then reload the UITableView after the loop completes and the Dispatch group has ended. But when I print to console, I see that the loop completes before the Contact object has loaded its User information.
Code snippets:
class ContactsController: UITableViewController, UISearchResultsUpdating {
var dataSource : [Contact] = []
override func viewDidLoad() {
super.viewDidLoad()
let fetchContactGrp = DispatchGroup()
fetchContactGrp.enter()
self.getMyContacts(){ contacts in
for contact in contacts {
let _contactData = Contact(
userId : contact.userId
, contactId : contact.id
, timeStamp : contact.timeStamp
, lastOpened : contact.lastOpened
, haveAccount: true
)
_contactData.loadData()
self.dataSource.append(_contactData)
}
}
fetchContactGrp.leave()
DispatchQueue.main.async{
self.tableView.reloadData()
}
}
}
The function self.getMyContacts is just a standard GraphQL query:
func getMyContacts( callBack: #escaping ([Contact]) -> Void ){
let my_token = AWSMobileClient.default().username
let contact = Contact.keys
let predicate = contact.ownerId == my_token!
_ = Amplify.API.query(from: Contact.self, where: predicate) { (event) in
switch event {
case .completed(let result):
switch result {
case .success(let cts):
/// #On success, output a user list
callBack(cts)
case .failure(let error):
break
}
case .failed(let error):
break
default:
break
}
}
}
And the Contact object loads the User data from database:
class Contact {
let userId: String!
let contactId: String!
var name : String
var bio : String
var website: String
let timeStamp: String
let lastOpened: String
init( userId: String, contactId: String, timeStamp: String, lastOpened: String, haveAccount: Bool){
self.userId = userId
self.contactId = contactId
self.timeStamp = timeStamp
self.lastOpened = lastOpened
self.haveAccount = haveAccount
self.name = ""
self.bio = ""
self.website = ""
}
func loadData(){
/// #use: fetch user data from db and populate field on initation
let _ = Amplify.API.query(from: User.self, byId: self.userId) { (event) in
switch event {
case .completed(let res):
switch res{
case .success (let musr):
if (musr != nil){
let userData = musr!
let em = genEmptyString()
self.name = (userData.name == em) ? "" : userData.name
self.bio = (userData.bio == em) ? "" : userData.bio
self.website = (userData.website == em) ? "" : userData.website
print(">> amplify.query: \(self.name)")
} else {
break
}
default:
break
}
default:
print("failed")
}
}
}
}
It's because the function getMyContacts() is performing an Async task and the control goes over that and execute the leave statement. You need to call the leave statement inside the getMyContacts() function outside the for loop.
Try the following code:
override func viewDidLoad() {
super.viewDidLoad()
let fetchContactGrp = DispatchGroup()
fetchContactGrp.enter()
self.getMyContacts(){ contacts in
for contact in contacts {
let _contactData = Contact(
userId : contact.userId
, contactId : contact.id
, timeStamp : contact.timeStamp
, lastOpened : contact.lastOpened
, haveAccount: true
)
_contactData.loadData()
self.dataSource.append(_contactData)
}
fetchContactGrp.leave()
}
fetchContactGrp.wait()
DispatchQueue.main.async{
self.tableView.reloadData()
}
}
I posted a more general version of this question here: Using `DispatchGroup` or some concurency construct to load data and populate cells in `UITableViewController` sequentially
And it has been resolved.
I am trying to read from Firestore into a Dictionary[Any] type using Struct. I can get the values loaded into variable "data" dictionary with Any type.
However I cannot loop thru it to access normal nested Dictionary variable.
I cannot get Key, values printed.
Following is my code:
class PullQuestions {
//shared instance variable
**public var data = [Any]()**
private var qdb = Firestore.firestore()
public struct questionid
{
let qid : String
var questions : [basequestion]
var answers: [baseans]
}
public struct basequestion {
let category : String
let question : String
}
public struct baseans {
let answer : String
}
class var sharedManager: PullQuestions {
struct Static {
static let instance = PullQuestions()
}
return Static.instance
}
static func getData(completion: #escaping (_ result: [Any]) -> Void) {
let rootCollection = PullQuestions.sharedManager.qdb.collection("questions")
//var data = [Any]()
rootCollection.order(by: "upvote", descending: false).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
guard let topSnapshot = querySnapshot?.documents else { return }
// var questiondoc = [basequestion]()
for questioncollection in topSnapshot {
rootCollection.document(questioncollection.documentID).collection("answers").getDocuments(completion: {
(snapshot, err) in
guard let snapshot = snapshot?.documents else { return }
var answers = [baseans]()
for document in snapshot { //There should be only one Document for each answer collection
//Read thru all fields
for i in 0..<document.data().count
{
let newAns = baseans(answer: answer)
print("Answer Docs=>", (answer))
answers.append(newAns)
}
}
let qid = questioncollection.documentID
let category = questioncollection.data()["category"] as! String
let question = questioncollection.data()["question"] as! String
let newQuestions = basequestion(category: category ,question: question)
let newQuestionDict = questionid(qid: qid, questions: [newQuestions], answers: answers)
PullQuestions.sharedManager.data.append(newQuestionDict)
//Return data on completion
completion(PullQuestions.sharedManager.data)
})
}
}
})
}
}
I can print like this
print("Count =>", (PullQuestions.sharedManager.data.count))
// print(PullQuestions.sharedManager.data.first ?? "Nil")
print(PullQuestions.sharedManager.data[0])
for element in PullQuestions.sharedManager.data
{
print("Elements in data:=>", (element))
}
I could access only the key.. how do i go and get the nested values ?
First of all, consider using Swift code conventions (e.g. your structs are named with small letters, but you should start with capital), this will make your code more readable.
Returning to your question. You use an array instead of dictionary (this piece of code: public var data = [Any]()). And here you are trying to print values:
for element in PullQuestions.sharedManager.data
{
print("Elements in data:=>", (element))
}
In this context element is an Any object, thus you cannot access any underlying properties. In order to do this you have two options:
1. You should specify the type of array's objects in it's declaration like this:
public var data = [questionid]()
or you can user this:
public var data: [questionid] = []
These two are equals, use the one you prefer.
2. If for any reasons you don't want to specify the type in declaration, you can cast it in your loop. Like this:
for element in PullQuestions.sharedManager.data
{
if let element = element as? quetionid {
print("Elements in data:=>", (element))
// you can also print element.qid, element.questions, element.answers
} else {
print("Element is not questionid")
}
}
You could of course use the force cast:
let element = element as! questionid
and avoid if let syntax (or guard let if you prefer), but I wouldn't recommend this, because it (potentially) can crash your app if element will be nil or any other type.
I added a list to my realm object class, and i need to migrate but i keep getting this error:
"Migration is required due to the following errors:
- Property 'UserIdCard.idImages' has been added."
these are my classes:
class UserIdCard: Object {
dynamic var idAccountId: Int = 0
dynamic var idName: String = ""
dynamic var idType: String = ""
let idImages = List<UserImage>()
override static func primaryKey() -> String? {
return "idAccountId"
}
}
class UserImage: Object {
dynamic var image: Data? = nil
}
And this is my migration code:
if (oldSchemaVersion < 4) {
migration.enumerateObjects(ofType: UserImage.className()) { oldObject, newObject in
newObject!["image"] = nil
}
migration.enumerateObjects(ofType: UserIdCard.className()) { oldObject, newObject in
let image = migration.create(UserImage.className(), value: Data())
let images = newObject?["idImages"] as? List<MigrationObject>
images?.append(image)
}
}
I did exactly like the example Realm provided : Link
Also i tried this : Link
,but it's not working, i tried to pass different values in "value" field but nothing worked, what is the right way to migrate a list in realm?
Thanks,
Below is my custom object class.
class UserGroups: NSObject {
let groupName: String
let users: [CheckIn]?
init(json:JSON) {
self.groupName = json[Constants.Models.UserGroups.groupName].stringValue
self.users = UserGroups.getUserGroupsList(jsonArray: json[Constants.Models.UserGroups.users].arrayValue)
}
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn]{
return jsonArray.flatMap({ (jsonItem: JSON) -> CheckIn in
return CheckIn(json: jsonItem)
})
}
}
I've an array of above custom objects. How can I combine 2 or more custom objects into a single object by merging users of every object having same groupName.
Below is my CheckIn Model:
class CheckIn: NSObject {
let id: String
let firstName: String
let lastName: String
let latitude: String
let longitude: String
let hint: String
init(json: JSON) {
self.id = json[Constants.Models.CheckIn.id].stringValue
self.firstName = json[Constants.Models.CheckIn.firstName].stringValue
self.lastName = json[Constants.Models.CheckIn.lastName].stringValue
self.hint = json[Constants.Models.CheckIn.hint].stringValue
self.latitude = json["location"][Constants.Models.CheckIn.latitude].stringValue
self.longitude = json["location"][Constants.Models.CheckIn.longitude].stringValue
}
}
id field is not unique in CheckIn.
Here's a slightly simplified example that shows how to combine groups that have the same group name.
Here is the UserGroup class. users is now a variable (var) because we will be adding elements to groups to combine them.
class UserGroups: NSObject {
let groupName: String
var users: [String]?
init(groupName: String, users: [String]?) {
self.groupName = groupName
self.users = users
}
}
Here are three groups, two of the share the same group name, Blues.
let group1 = UserGroups(groupName: "Blues", users: ["Tom", "Huck", "Jim"])
let group2 = UserGroups(groupName: "Reds", users: ["Jo", "Ben", "Tommy"])
let group3 = UserGroups(groupName: "Blues", users: ["Polly", "Watson", "Douglas"])
Next, we'll put all the groups in an array.
let allGroups = [group1, group2, group3]
Here, we use Swift's reduce function to allow us to reduce the array to only groups with unique group names.
let compacted = allGroups.reduce([UserGroups](), { partialResult, group in
var dupe = partialResult.filter {$0.groupName == group.groupName }.first
if let dupeGroup = dupe {
dupeGroup.users?.append(contentsOf: group.users ?? [])
return partialResult
} else {
var newPartialResult = partialResult
newPartialResult.append(group)
return newPartialResult
}
})
The array is now reduced to unique groups, we print out all the groups and their users with the help of Swift's map function.
print(compacted.map { $0.users })
// Prints [
Optional(["Tom", "Huck", "Jim", "Polly", "Watson", "Douglas"]),
Optional(["Jo", "Ben", "Tommy"])
]
The Solution
You did not include the CheckIn model, but I will assume that it has some sort of an id field unique to each user. We will use this to make the object Hashable:
// Add this to your file outside of the UserGroups class
extension CheckIn: Hashable {
var hashValue: Int { return self.id }
}
Making it Hashable allows you to convert the Array to a Set, which does not allow duplicates and will remove them in a very efficient way.
// Change getUserGroupsList as follows
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
return Array(Set(jsonArray.flatMap({ (jsonItem: JSON) -> CheckIn in
return CheckIn(json: jsonItem)
})))
}
Optional Considerations
As an aside, in case you're coming from another language, Swift gives you nice type inference and default names for closure arguments ($0 is the first argument). You can probably make the code a little less verbose, but it's a matter of taste which is preferred.
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
return Array(Set(jsonArray.flatMap { CheckIn(json: $0) }))
}
Also consider whether you really want the return value to be an array. If you want the list to always have unique users, it is a bit more efficient to use a Set as your return type and forgo the conversion back to an Array like this:
class func getUserGroupsList(jsonArray: [JSON]) -> Set<CheckIn> {
return Set(jsonArray.flatMap { CheckIn(json: $0) })
}
Finally, consider whether you really need the users property to be optional. With sequence types, it is often sufficient to use an empty sequence to denote absence of a value. Depending on your situation, this may simplify your code. The final version looks like this:
class UserGroups: NSObject {
let groupName: String
let users: Set<CheckIn>
init(json:JSON) {
self.groupName = json[Constants.Models.UserGroups.groupName].stringValue
self.users = UserGroups.getUserGroupsList(jsonArray: json[Constants.Models.UserGroups.users].arrayValue)
}
class func getUserGroupsList(jsonArray: [JSON]) -> Set<CheckIn> {
return Set(jsonArray.flatMap { CheckIn(json: $0) })
}
}
Maintaining Order
The caveat is that Set does not maintain the order of the items. If the order of the groups does matter, we can use this solution instead:
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
var encountered: Set<CheckIn> = []
return jsonArray.flatMap { CheckIn(json: $0) }.filter { encountered.update(with: $0) == nil }
}
In this version, we still use a set, but only to maintain a set of items we've encountered already. The update method on a set returns the same value if it's already in the set or returns nil if it's being inserted for the first time. We use that to filter our array to those items being encountered for the first time while adding them to the set of encountered items to filter them out when they are subsequently encountered again.
I have a class with init a build method that i want to use to create instances of class.
Code is the following
class Article {
let id:Int
let title:String
let subtitle:String
let editor1:String
let mainImage:NSData
init(id:Int, title:String, subtitle:String, editor1:String, mainImage:NSData) {
self.id = id
self.title = title
self.subtitle = subtitle
self.editor1 = editor1
self.mainImage = mainImage
}
class func build(json:JSON) -> Article {
id = Int(json["id"].string),
title = json["title"].string,
subtitle = json["subtitle"].string,
editor1 = json["editor1"].string,
mainImage = json["images"]["main"].rawData() {
return Article(
id: id,
title: title,
subtitle: subtitle,
editor1: editor1,
mainImage: mainImage)
}
}
}
But i have errors
What am I doing wrong ?
SwiftyJSON's .rawData() is an Optional getter.
So I guess what you wanted to do is use if let:
class func build(json:JSON) -> Article? {
id = Int(json["id"].string)
title = json["title"].string
subtitle = json["subtitle"].string
editor1 = json["editor1"].string
if let mainImage = json["images"]["main"].rawData() {
return Article(
id: id,
title: title,
subtitle: subtitle,
editor1: editor1,
mainImage: mainImage)
} else {
// ...
return nil
}
}
Also it looks like that you copied/pasted the parameters from your Article initializer to declare them earlier in the function but you forgot to get rid of the commas at the end of the lines.
Update
Your problem is that your class properties are immutable (declared with let) but inside this function you are trying to change their values:
id = Int(json["id"].string)
This is interpreted as
self.id = Int(json["id"].string)
And you can't change the value of self.id because it is immutable.
Solutions:
1- Make the properties mutable by using var instead of let. Example:
var id:Int
var title:String
var subtitle:String
var editor1:String
var mainImage:NSData
or
2- Do not replace the properties in the function since you're going to init with your new object anyway. Example:
class func build(json:JSON) -> Article? {
if let img = json["images"]["main"].rawData() {
return Article(
id: Int(json["id"].string),
title: json["title"].string,
subtitle: json["subtitle"].string,
editor1: json["editor1"].string,
mainImage: img)
} else {
// ...
return nil
}
}
Update 2
If the compiler complains about "not marked with try", do the "if let" with "try?":
if let img = try? json["images"]["main"].rawData() {
Explanation: SwiftyJSON may have changed this method without updating the documentation yet (or I didn't find it). It previously returned an Optional and now seems to "throw" instead. Using "try?" lets you make it an Optional again.
Couple things look off.
You should be returning Article? since there is a chance you're going to be returning nil.
You've got a number of extraneous commas after things like id = Int(json["id.... Get rid of them.
That closure seems unnecessary. Simply check for the valid JSON first, if its bad return nil, otherwise build it up and return the article.
You have ,s where they shouldn't be. Try using this:
class func build(json:JSON) -> Article {
id = Int(json["id"].string)
title = json["title"].string
subtitle = json["subtitle"].string
editor1 = json["editor1"].string
mainImage = json["images"]["main"].rawData() {
return Article(
id: id,
title: title,
subtitle: subtitle,
editor1: editor1,
mainImage: mainImage)
}
}