NSPredicate NSUnknownKeyException - Swift 4.0 - swift

I have my current code as listed below:
class ViewController:UIViewController{
ovveride func viewDidLoad(){
filterData()
}
func filterData(){
var usersArray = [User(name:"John",userId:1),User(name:"Ed",userId:2),User(name:"Ron",userId:3)]
let filteredData = (userArray as NSArray).filtered(using:NSPredicate(format: "userId=1"))
}
}
The above code throws throws the following Exception (NSUnknownKeyException):
The Object Type '[<ViewController.User 0x6000000afa20> valueForUndefinedKey:]':
this class is not key value coding-compliant for the key userId.
The document of Apple for filter(using:) does not specify any changes that could be causing this issue.
How can I use NSPredicate in Swift 4.0?
Also, as requested, I tried using #objc. But, still the same error.
#objc
class User:NSObject{
var name:String!
var userId:Int!
init(name:String,userId:Int){
self.name = name
self.userId = userId
}
}
With Further Comments received for this question, on adding #objc attribute to the userId I receive the below message.
property cannot be marked #objc because its type cannot be represented in Objective-C
#objc
class User:NSObject{
#objc var name:String!
var userId:Int! //#objc here results in error
init(name:String,userId:Int){
self.name = name
self.userId = userId
}
}
String property NSPredicate it works completely fine.
- Add #objc to class
- Also, add #objc to property

Why not just use filter? It's much more "Swift-y" and doesn't involve converting your array to an NSArray.
let filteredData = usersArray.filter { $0.userId == 1 }

Related

objc_getAssociatedObject and Runtime attributes

I have the following extension which was used to automatically save/retrieve runtime attributes unique to a UIImageView:
import UIKit
var imgAttributeKey:String? = nil
extension UIImageView {
var imgAttribute: String? {
get { return objc_getAssociatedObject(self, &imgAttributeKey) as? String }
set { objc_setAssociatedObject(self, &imgAttributeKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) }
}
}
This was working fine but after trying the code again recently, the getters were always returning nil. Did something change in Swift 5 version that could be breaking this implementation? Any suggestions on how to go about it?
Thanks to Tarun Tyagi for pointing out the correct fix.
#objc needs to be added to the property reference in the extension. Incorrectly marking the outside property results in a objc can only be used with members of classes, #objc protocols, and concrete extensions of classes error.
Working code is as follows:
import UIKit
var imgAttributeKey:String? = nil
extension UIImageView {
#objc var imgAttribute: String? {
get { return objc_getAssociatedObject(self, &imgAttributeKey) as? String }
set { objc_setAssociatedObject(self, &imgAttributeKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) }
}
}
The reason why this was needed it that this project was originally written in Swift 3 but starting from Swift 4, an explicit annotation #objc for dynamic Objective-C features (such as User Defined Runtime Attributes) is required.

Primary key can't be changed after an object is inserted

I'm very new to programming.
I am trying to update an object in my Realm database but I get always an error.
I have tried to find the issue but I can't find anyone with a similar issue.
What I'm trying to do is:
I have a Game-Score-App.
It should display the names on Tab1 and on the Tab2 I want to give the user the ability, to change the names of the players. As soon as the ViewDidDisappear I want to write the changes to Realm.
I already figured out how to update the names in the database. And it works properly the first time.
But as soon as I go a second time on the Tab2 and go back to Tab1 again, I get the message "Primary key can't be changed after an object is inserted."
Any Ideas?
class Games: Object {
#objc dynamic var game_id = UUID().uuidString
#objc dynamic var gameName: String = ""
var playerNames = List<String>()
override class func primaryKey() -> String? {
return "game_id"
}
}
class FirstPageVC: UIViewController {
#IBOutlet var playerNameLabels: [UILabel]!
#IBOutlet weak var gameNameLabel: UILabel!
let realm = try! Realm()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
let games = realm.objects(Games.self)
gameNameLabel.text = games[0].gameName
for i in 0...playerNameLabels.count - 1 {
playerNameLabels[i].text = games[0].playerNames[i]
}
}
}
class SecondPageVC: UIViewController {
#IBOutlet var playerNameTextbox: [UITextField]!
#IBOutlet weak var gameNameTextbox: UITextField!
#IBOutlet weak var numberOfIndex: UITextField!
let realm = try! Realm()
var playerNames: [String] = []
var gameName: String = ""
var game = Games()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if realm.objects(Games.self).count != 0 {
let games = realm.objects(Games.self)
gameNameTextbox.text = games[0].gameName
for i in 0...playerNameTextbox.count - 1 {
playerNameTextbox[i].text = games[0].playerNames[i]
}
}
}
#IBAction func addButton(_ sender: UIButton) {
gameName = gameNameTextbox.text!
for i in 0...playerNameTextbox.count - 1 {
playerNames.append(playerNameTextbox[i].text!)
}
let items = realm.objects(Games.self)
let number = Int(numberOfIndex.text!)
game.game_id = items[number!].game_id
game.gameName = gameName
game.playerNames.append(objectsIn: playerNames)
try! realm.write {
realm.add(game, update: .modified)
}
}
}
The problem is your Realm object structure. Anything that could possibly ever be changed should not be used as a primary key.
Also note from the Realm Documentation
Once an object with a primary key is added to a Realm, the primary key
cannot be changed.
To expand on that, it's often best practice to disassociate an objects key (e.g. primary key) from the rest of the properties of an object.
Here's how to do that
class Games: Object{
#objc dynamic var game_id = UUID().uuidString
#objc dynamic var gameName: String = ""
var playerNames = List<String>()
override class func primaryKey() -> String? {
return "game_id"
}
}
UUID().uuidString will generate a unique string for every object that's created and will look something like this string
CDEA69EA-AC84-4465-ABE3-DDA29D31B925
Once the object is created, you can use it to load that specific object or update it's properties.
See Objects with Primary Keys
Here's how to change the game name
let item = realm.object(ofType: Game.self, forPrimaryKey: "CDEA69EA-AC84-4465-ABE3-DDA29D31B925")!
try! realm.write {
game.gameName = "Pwn You!"
}
Try this solution:
Replace the code in viewDidDisappear after the end of for loop with the following code:
if let gameInRealm = realm.objects(Game.self).first{
try! realm.write {
gameInRealm.gameName = gameName
gameInRealm.playerNames = playerNames
}
}else{
game.gameName = gameName
game.playerNames.append(objectsIn: playerNames)
realm.add(game)
}
Explanation (if needed):
The code changes the existing Game properties in case a Game object exists. Otherwise, it creates a new one with the new properties.
Therefore, the else statement should get executed the first time you leave SecondPageVC, and then the if statement will get triggered every other time you leave SecondPageVC.

iOS Populate Table View With Firebase Data Using MVC Pattern

In my Firebase project I'm trying to apply Model View Controller pattern, so I separated the controller and the class that handles firebase requests.
I get this exception
exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber length]: unrecognized selector sent to instance
I'm trying to get news items from the database. Here is the model
class NewsItem: NSObject{
var title: String?
var detail: String?
var photoUrl: String?
var timestamp: String?
}
Here is the firebase handler class
protocol NewsController: class {
func fetchedNews(_ newsItem: NewsItem)
}
class FirebaseHandler {
private static let _instance = FirebaseHandler()
static var Instance: FirebaseHandler{
return _instance
}
weak var newsControllerDelegate: NewsController?
func fetchNews() {
References.Instance.newsRef.observe(.childAdded) {
(snapshot: DataSnapshot) in
if let child = snapshot.value as? [String: AnyObject]{
let newsItem = NewsItem()
print("CHILD: \n\n\n\n\(child)\n\n\n")
newsItem.setValuesForKeys(child)
DispatchQueue.main.async {
self.newsControllerDelegate?.fetchedNews(newsItem)
}
}
}
}
}
I can get the child values printed fine, but the problem is when I call the protocol delegate method.
Here are some portions of the table view controller class where I adopt the NewsController protocol:
FirebaseHandler.Instance.newsControllerDelegate = self
FirebaseHandler.Instance.fetchNews()
Then I implement the method:
func fetchedNews(_ newsItem: NewsItem) {
print("Item:\n\n\n\(newsItem)\n\n\n")
self.newsItems.append(newsItem)
self.tableView.reloadData()
}
The newsItem isn't printed since the error occurs before this method is called I guess. Any suggestions are appreciated.
From the reported NSNumber related error, I would guess your timestamp property is actually stored as an integer in Firebase (and not as a string). If this is the case, try changing it to:
var timestamp: Int = 0
To understand why we can't use Int? (or even Int!) above please see this answer as well.
Also: you don't need that DispatchQueue.main.async wrapper in your observer code. All database callbacks are already called on the main thread by Firebase ;)

Swift: Unable To Set Description on Subclass of NSObject

I have built a custom class that looks something like this:
import UIKit
class Device: NSObject {
var id = String()
var name = String()
var type = String()
//var description = String() //Cannot override with a stored property 'description'
}
Very simple class, and I am inheriting NSObject so I can use "setValue(value, forKey: keyName)". However, when I do the following:
device.setValue("My Description", forKey: "description")
I am getting the following error:
'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key description.'
So insummary, I can't override the NSObject.description, but when I try to set it I am getting an error. Anyone run into this before?
Look at where description is defined. It is listed in the NSObjectProtocol as
public var description: String { get }
You can only get the description property, you can't set it.
In Objective-C, description is implemented in most classes as a method; it has no underlined storage. The swift equivalent would be a computed property:
public override var description: String {
return "I'm an object"
}
tl;dr Use a computed property instead of a stored property
class CustomObject : NSObject {
private var des: String
override var description: String {
get {
return des
}
set(newValue) {
des = newValue
}
}
init(string: String) {
des = string
}
}
You can only get the description property, you can't set it.
In Objective-C string classes, the class description for NSMutableString specifies that the class inherits from NSString. description, when you try to set description it will be getting an error.
Method 1
While using setValue(value, forKey: keyName) Use can store property value by using.
class ObjName: NSObject{
var id: String?
var descriptions : String?
override var description: String {
get {
return self.description
}
set(newvalue) {
descriptions = newvalue
}
}
}
Using above code, method setValue for key description value store into the descriptions. while you get the value you can use descriptions. Also, it does not affect on description get.
Method 2
Overriding setValue function. like below.
class ObjName: NSObject{
var id: String?
var descriptions : String?
override func setValue(_ value: Any?, forKey key: String) {
if key == "description"{
if let desc = value as? String{
self.descriptions = String()
self.descriptions = desc
}
}else{
super.setValue(value, forKey: key)
}
}
}

Realm Cannot invoke 'add' with an argument list of type '(Person)'

This error is so frustrating. I'm just trying to add an object to my Realm database and I have gotten to the point of just copying and pasting example code and it will not work. So I have an add person method that does this:
func addPerson(person person:Person){
realm.beginWrite()
realm.add(person)
realm.commitWrite()
}
And the realm variable is stored like this in the class header:
private var realm:Realm
Being initialized in the init() method as such:
realm = Realm()
My actual person class looks like this:
import UIKit
class Person {
var name:String?
var relation: Relations?
var title: String?
var importance:Double?
var events:Array<Event>?
var image:UIImage?
init(name:String,relation:Relations?,events:Array<Event>?,image:UIImage?){
self.relation = relation
if relation != nil{
self.title = relation!.title
self.importance = relation!.importance
}
else{
self.title = nil
self.importance = nil
}
self.events = events
self.image = image
self.name = name
}
init() {
}
}
The error is so frustrating because it seems like the Person class does not conform to the Object superclass, but in reality, that's the only option
This is just a summary of what I said in the comments:
Use Object as a subclass as it is necessary for a class to be a model in Realm.