Swift Keypath with UIButtons - ios

I'm trying to get a keypath to the selected property of an IBOutlet within a class. But get:
Type 'UIButton?' has no member 'isSelected'
directly accessing the UIButton.isSelected keypath works, but doesn't fulfill my usecase.
#objc class Demo: UIViewController{
#IBOutlet #objc dynamic weak var button: UIButton!
}
var demo = Demo()
print(#keyPath(UIButton.isSelected)) // no error
print(#keyPath(Demo.button.isSelected)) // error
print(#keyPath(demo.button.isSelected)) // error
what am I missing?

#keyPath is just syntactic sugar that creates a string value, While ensuring that the keyPath is valid for the object you specify; It helps to prevent crashes when using KVO, since it validates, at compile time, that your keyPath is valid, rather than crashing at runtime if it isn't.
Accordingly, you don't specify a keyPath on a particular instance, you specify it on the object type. This is why your first line works and the second two don't.
You specify the specific object instance on which you want to observe the keyPath when you call addObserver:
demo.addObserver(someObserver, forKeyPath: #keyPath(UIButton.isSelected), options: [], context: nil)
You could also say
demo.addObserver(someObserver, forKeyPath: "selected", options: [], context: nil)
with the same result
But if you accidentally typed "slected" instead of "selected" you wouldn't find out until your app crashed at runtime, while #keyPath(UIButton.isSlected) will give you a compiler error straight away.

Related

Referencing UIView subclass .center with Swift 3 #keyPath

In a view controller in my app, I'm reacting to changes to a view's positioning by key-value observing its center property, like this:
class CoordinatingViewController: UIViewController {
#IBOutlet weak var cardContainerView: CardView!
override func viewDidLoad() {
super.viewDidLoad()
addObserver(self, forKeyPath: "cardContainerView.center", options: [.new], context: nil)
}
}
This works just fine for now, but since that key path is a string, it can't be checked by the compiler. To mitigate this, I'd like to use Swift 3's #keyPath() syntax, which takes a compiler-checked key path and returns the proper corresponding string. However, when I change that line to use it like this:
addObserver(self, forKeyPath: #keyPath(cardContainerView.center), options: [.new], context: nil)
The compiler gives me 2 errors:
Type 'CardView!' has no member 'center'
Cannot refer to type member 'center' within instance of type 'CardView!'
I don't understand why I'm getting these errors, as center is a documented property of UIView, which CardView inherits from directly, which I can both read and write to outside the #keyPath() statement there. Furthermore, everything appears to work ok when I pass the key path directly as a string, as in my first example, which makes this even more confusing. How can I have the compiler check that key path for me?
It seems like Swift is not happy about the weak modifier. Remove it and the code will compile. From this, we can see that outlets are recommended to be strong, unless you actually have a retain cycle. See my answer here for how to find retain cycles.
In Swift 4, you will be able to do this, using the new \ key path syntax:
// no problems with "weak"!
let observation = observe(\.cardContainerView.center) { (object, change) in
...
}

Failed to set () user defined inspected property on (UIButton)

In a simple ViewController, I added one UIButton.
I would like to use "User Defined Runtime Attributes". For now I added the default Bool attribute.
The button is an #IBOutlet:
#IBOutlet var button:UIButton!
The link in the storyboard is done.
I have nothing else in my app.
I got this error:
2017-03-26 20:31:44.935319+0200 ISAMG[357:47616] Failed to set (keyPath) user defined inspected property on (UIButton):
[<UIButton 0x15e3a780> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key keyPath.
I don't understand what I'm missing.
EDIT 1
Following the advices #timaktimak, I created a custom UIButton class:
#IBDesignable
class ChoiceButton: UIButton {
#IBInspectable var keyPath: Bool! {
didSet {
print("didSet viewLabel, viewLabel = \(self.keyPath)")
}
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
}
The error is the same:
2017-03-26 22:32:27.389126+0200 ISAMG[429:71565] Failed to set (keyPath) user defined inspected property on (ISAMG.ChoiceButton):
[<ISAMG.ChoiceButton 0x14e8f040> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key keyPath.
Well, your button doesn't have a property called keyPath to be setting it to something.
User Defined Runtime Attributes are used to simply set a property value in the Interface Builder.
You can use a standard property that every UIButton has, for example backgroundColor:
Or, you can create a custom UIButton subclass, add a property to it, then set the button's class to the created custom subclass and set the property value in the User Defined Runtime Attributes section.
You can check out, for example, this answer, it contains an example of a custom class with User Defined Runtime Attributes: https://stackoverflow.com/a/24433125/3445458
Edit: make sure you are not using optional (or implicitly unwrapped optional) for the type, it doesn't work with User Defined Runtime Attributes (I guess because the Interface Builder doesn't know whether the value can be set to nil, and so show it in the options or not).
So you can't do
var keyPath: Bool!
instead you can only do
var keyPath: Bool = false
or don't use a default value and set it in the constuctor instead. For some reason it works with an optional String (and in the example they use an optional String), but it doesn't with optional Bool. To conclude, don't use or worry about User Defined Runtime Attributes too much, it is clearer to set the default values in code!
Hope this helps! Good luck!
Since this thread came up about 5 times in my search for a solution, I wanted to post my fix here for future frustrated souls. The reason I kept getting this error is because Interface Builder doesn't clean up old IBInspectable properties if you're experimenting with them.
In my case, I started with an Int property called type and set a few custom buttons on my screen to use a value of 3 as a test. Later, I changed it to a more meaningful property name and made it a string, then set a few of those. But when I ran the app, I was getting this message.
I didn't discover the error until I went to the Identity Inspector and noticed that I still had an old keypath and value in use for some of the buttons:
Removing the old keypath fixed the problem.
In User Defined Runtime Attributes use layer.cornerRadius instead of cornerRadius.
Looks like failed to set keyPath bool value to true of false.
setKeyPath to true or false!
Compare #IBInspectable(in sub class) and Key Path(identity inspector). Remove key value which is not in #IBInspectable.
Another way to set the set a property value in the Interface Builder is that create an IBOutlet of your view on your view controller.
#IBOutlet weak var yourView: UIView!
yourView.layer.shadowOffset = CGSize(width: 0, height: 1)
yourView.layer.shadowColor = UIColor.lightGray.cgColor
yourView.layer.shadowOpacity = 1
yourView.layer.shadowRadius = 5
yourView.layer.masksToBounds = false
yourView.layer.cornerRadius = 20

[SearchStockCell retain]: message sent to deallocated instance

I'm getting the following error:
SearchStockCell retain]: message sent to deallocated instance 0x7f9fa1922c00
but I am having a hard time tracing the issue because whenever I profile with zombies, it stops without any warning or error(2-3 secs).
I'm using realm for this project and the data parsing is performed at background.Not sure if this information is relevant.
Is there other way to track this? or is possible I use weak for tableview cell?
Updated
class SearchStockCell: SSBaseTableCell {
#IBOutlet var symbolLabel: UILabel!
#IBOutlet var marketLabel: UILabel!
#IBOutlet var priceLabel: UILabel!
var stock: StockInfo? {
willSet{ "About to step to \(newValue)"
if let aStock = newValue {
// add KVO on newValue
aStock.addObserver(self,
forKeyPath: "price",
options: NSKeyValueObservingOptions.New,
context: nil)
aStock.addObserver(self,
forKeyPath: "change",
options: NSKeyValueObservingOptions.New,
context: nil)
}
}
didSet { "Just stepped from \(oldValue)"
if let aStock = oldValue {
// remove KVO on old value
aStock.removeObserver(self, forKeyPath: "price")
}
if let aStock = oldValue {
// remove KVO on old value
aStock.removeObserver(self, forKeyPath: "change")
}
self.configureCell()
}
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "price" {
self.updatePrice()
}
if keyPath == "change" {
self.updateChange()
}
}
....
...
..
.
Here is the code happened in SearchStockCell.
I will fire an API to update my visible cells(it will update my realm) which later will prompt the changes on the SearchStockCell by KVO. Note that I can't really reload the table again because I need to maintain the position of tableview and there's thousands rows of data in it.
It is really hard to guess from code but would try my best to look answer
Please look for places
have used SearchStockCell as a property somewhere if yes check for attributes strong/weak. There is possible cycle of holding each other.
Check if you are using SearchStockCell object in block, if yes try using it as weak object. Also check for other things done inside the block.
you are using KVO, check if at any point of time is observer going out of memory.
Most likely issue which I can think of right is at some place you are assigning/using SearchStockCell object as weak/strong due to which ARC is handling retain count wrongly.
It looks like you're vastly overcomplicating this situation by adding and balancing KVO on these table cells.
You mentioned that you don't want to reload the table since you'll lose your position in the scroll view. Have you considered simply saving the scroll position of the table view before reloading and then re-setting it afterwards?
As a side note, Realm will soon introduce a feature to track insertions/updates/deletions on a table view data source, so hopefully once that's out, you could use that here instead (Disclaimer: I work for Realm).

IF Statement Incorrectly Evaluating to True when Using Optional Values in Swift

I have set up my view controllers so that they send a notification once their -viewDidLoad method is about to return. For example:
class MyViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
//Do Stuff
var notificationCenter = NSNotificationCenter.defaultCenter();
notificationCenter.postNotificationName("AViewControllerDidLoadNotification", object: self);
}
}
My AppDelegate class is listening for this notification and implementing the method shown in this picture.
In case the picture isn't loading, the method takes the notification sent by the view controllers as it's only argument and then tests whether the UIViewController's title property has a non-nil value. If the title property is non-nil it logs the title.
However, as you can see in the debugger panel, the title property of the view controller is nil and the if statement is still evaluating to true.
I am admittedly new to optional values. But I have recreated this situation in a swift playground and the if statement evaluates to false. Any ideas?
You've gotten yourself into rather an odd situation with your very peculiar use of the expression notification.object?.title, because notification.object is not, of itself, a UIViewController. It is an AnyObject.
Now, an AnyObject has no known properties, so it has no title and your expression, it would seem, should not even compile. But, by a special dispensation coming from certain oddities of Objective-C, you are in fact allowed to ask about an AnyObject's properties anyway. But when you do, the result is itself an Optional, because no such property might exist.
Thus, you are actually testing, not the value of a view controller's title property, but whether this unknown object has a title property in the first place; and if in fact it does have a title property at all, the value of that title property is double-wrapped inside that Optional.
To see this clearly, just test this (silly) code:
let n = NSNotification(name: "Howdy", object: "Hi")
let t = n.object?.title
Look at what type t is. It is not a String?; it is a String??. That's your double-wrapped Optional. This means that it would be an Optional-wrapping-a-String in case this object turns out to have a title property, but just in case, that value has itself been wrapped in an Optional.
Thus, your test doesn't do what you want it to do. To do what you want to do, just speak much more plainly and simply. You need to cast the object to a UIViewController first, and then examine its title. Like this:
func aViewControllerDidLoad(notification:NSNotification) {
if let vc = notification.object as? UIViewController {
if vc.title != nil {
// ...
}
}
}

Strange behaviour when naming variable in lowerCamelCase

I came across a strange behaviour in Swift while programming a Master-Detail application.
Here's the scenario:
It's a simple Task Manager application. I have two text controls (TaskName, TaskDescription) on the TaskDetailView and two string variables with the same name but in lowerCamelCase (taskName, taskDescription) declared in the TaskDetailViewController.
#IBOutlet var TaskName:UITextField! //UpperCamelCase
#IBOutlet var TaskDescription:UITextView! //UpperCamelCase
var taskName:String? //lowerCamelCase
var taskDescription:String? //lowerCamelCase
I am setting the values of Text controls on ViewDidLoad() as usual:
override func viewDidLoad() {
super.viewDidLoad()
TaskName.text = taskName
TaskDescription.text = taskDescription
}
And I am passing the data in prepareForSegue (from TaskListViewController) as usual:
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if(segue.identifier == "TaskListSegue"){
let detailViewController = segue.destinationViewController as ToDoTaskViewController
let (task, desc) = m_ToDoListManager.GetTask(TaskListView.indexPathForSelectedRow().row)
println("selected \(task) \(desc)")
detailViewController.taskName = task
detailViewController.taskDescription = desc
}
}
The way everything is implemented is correct.
But now when you run the application, the values of text controls are not set.
In fact, the values of the variables also are not set.
What must be happening here?
I have already investigated this problem and also came up with a solution (see my answer below). Please also see Martin R's answer below for a detailed explanation. I just wanted to share this with everyone. I am not sure if anyone has come across this issue.
Update:
Here's the actual code:https://github.com/Abbyjeet/Swift-ToDoList
Here is an explanation:
Your Swift class is (ultimately) a subclass of NSObject.
Therefore the properties are Objective-C properties with getter and setter method.
The name of the setter method for a property is built by capitalizing the first
letter of the property name, e.g. property "foo" has the setter method setFoo:
As a consequence, the setter method for both properties TaskName and taskName is called setTaskName:.
In an Objective-C file, you would get a compiler error
synthesized properties 'taskName' and 'TaskName' both claim setter 'setTaskName:' - use of this setter will cause unexpected behavior
but the Swift compiler does not notice the conflict.
A small demo of the problem:
class MyClass : NSObject {
var prop : String?
var Prop : String?
}
let mc = MyClass()
mc.prop = "foo"
mc.Prop = "bar"
println(mc.prop) // bar
println(mc.Prop) // nil
In your case
TaskName.text = ...
sets the "taskName" property, not the "TaskName". The properties have different type,
so that the behavior is undefined.
Note that the problem does only occur for "Objective-C compatible" properties. If you remove the
NSObject superclass in above example, the output is as expected.
Conclusion: You cannot have two Objective-C properties that differ only in the
case of the first letter. The Swift compiler should fail with an error here (as the
Objective-C compiler does).
The problem you were facing with was not connected to the swift language. Method prepareForSegue is called before loadView. That mean UITextField and UITextView are not initialized yet. That's why fields were not initialized.
You also asked: Why compiler doesn't show any error? That's because any selector performed on nil object doesn't throw an exception. So for example (sorry for obj-c):
UITextField *tf = nil;
[tf setText:#"NewText"];
Will not show any error.
As you said on your own answer to solve your problem you need to add additional fields to your destination controller (copy-paste):
var tAskName:String? //cUstomCamelCase
var tAskDescription:String? //cUstomCamelCase
Why is it happening?
I believe that internally Swift is using lowerCamelCase for text controls names which are not yet initialized and thus failing to set the values. But it is also strange that I didn't get any kind of error.
How did I solve it?
I know that the Swift is case-sensitive. So that was not the issue. So I just changed the case of one letter and named the variables as (tAskName, tAskDescription) and the values were set as expected.
#IBOutlet var TaskName:UITextField! //UpperCamelCase
#IBOutlet var TaskDescription:UITextView! //UpperCamelCase
var tAskName:String? //cUstomCamelCase
var tAskDescription:String? //cUstomCamelCase
So the conclusion is that if I have a control named TaskName, I cannot have a variable named as taskName

Resources