Swift - `self` in variable initialization closure - swift

How does button.addTarget(self, action: #selector(taptap), for: .touchUpInside)
work without a lazy keyword?
Dropped lazy by mistake, and have a closure to initialize a button like below:
class MyView: UIView {
let button: UIButton = {
let button = UIButton()
button.addTarget(self, action: #selector(taptap), for: .touchUpInside)
print(self) // (Function)
// button.frame = bounds <- Cannot assign here
return button
}()
lazy var button2: UIButton = {
let button = UIButton()
print(self) // <sample.MyView ...>
return button
}()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSubview(button2)
addSubview(button)
button.frame = bounds
print(self) // <sample.MyView ...>
}
#objc func taptap() {
print("taptap")
}
}
And the printed result is:
(Function)
<sample.MyView: 0x7f961dd09d80; frame = (67 269; 240 128); autoresize = RM+BM; layer = <CALayer: 0x6080000268a0>>
<sample.MyView: 0x7f961dd09d80; frame = (67 269; 240 128); autoresize = RM+BM; layer = <CALayer: 0x6080000268a0>>
taptap
What's the difference self in button closure and self in others? And why my button is works?

It works in NSObject subclasses because NSObject (or rather NSObjectProtocol) declares method self. That method is also available on metatypes (which are also NSObject instances) and therefore you can call it in static context.
The fact that it actually works on UIButton is probably a quirk of the compiler and the fact that UIButton accepts Any? as target.
Don't use it, it's not how it's intended to work.
See the bug SR-4559

In short, when you declare an instance property using closure initialization, that property will be created before self would be available (before the instance had been initalized properly), hence you cannot access self. A lazy instance property can only ever be accessed after the instance had been initalized, so you can access self from a lazy propery.
Longer version:
If you use closure initializiation by let button: UIButton = { return UIButton() }(), the button variable will be handled the exact same way in runtime as if you simply declared it like let button:UIButton = UIButton(). Pay attention to the () at the end of the closure. That essentially executes the closure right away when the instance property is being initialized, this is why it can actually be declared as immutable. Since instance properties are being initialized before the class initializer would be called, self is not available inside the closure for of a property.
When you declare a variable using the lazy keyword, it will only be evaluated when it is first accessed. Due to this, self is available inside the closure declaring a lazy property, since a property cannot be accessed before the class instance would be created, so in all cases, self is already initialized and available by the time you'd ever access the lazy property.

Related

Different behavior between addTarget and addGestureRecognizer

I have a function that creates a button with a selector function as a target. The address of a button gets passed to handleSelectPhoto.
lazy var image1Button = createButton(selector: #selector(handleSelectPhoto))
func createButton(selector: Selector) -> UIButton {
let button = UIButton(type: .system)
button.addTarget(self, action: selector, for: .touchUpInside)
return button
}
#objc func handleSelectPhoto(button: UIButton) {
// Do something with button, this works
}
Now, I am trying to change the class of the above from UIButton to UIImageView like the following,
lazy var image1Button = createButton(selector: #selector(handleSelectPhoto))
func createButton(selector: Selector) -> UIImageView {
let view = UIImageView()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: selector))
view.isUserInteractionEnabled = true
return view
}
#objc func handleSelectPhoto(button: UIImageView) {
// HERE, button does not get passed
}
With the above changes, in handleSelectPhoto, button instance is not correct. I can not read it as UIImageView type.
If I add a selector function using addGestureRecognizer, does it behave differently than adding a selector function using addTarget, in terms of how selector function is executed with parameters? Maybe I am not understanding how this selector function works...
Adding a target to something like UIGestureRecognizer or UIButton only passes one parameter to the selected function. This parameter depends on the type you are about to add the target on.
In your case the first code snippet works because you are adding a target to an UIButton, so your selected function gets passed this UIButton instance.
In your second scenario you add the target to an UITapGestureRecognizer, so the passed instance will be exactly this gesture recognizer, which cannot be of type UIImageView.
So the difference from the target parameter perspective between UIGestureRecognizer and UIButton is no difference. They both pass their instances to the selected function.
From the UIView subclass perspective there is the difference that UIGestureRecognizer is not a subclass of UIView, but UIButton is. That's why you can just use the passed UIButton instance in your first snippet. In the second snippet you need use the view property of UIGestureRecognizer.
guard let imageView = gestureRecognizer.view as? UIImageView else { return }
Besides your actual question it seems important to clarify how to write #selectors correctly. You're doing it correct already. No change necessary. Some may say you need to add (_:) or : to your selector like so: #selector(handleSelectPhoto(_:)) but this isn't true. In general, you only need to add these special characters when you are selecting a method which has an overload method with a different amount of parameters, but the same base name.
You should make your tell while setting the selection that your function will accept a parameter by adding : at the end of method name.
lazy var image1Button = createButton(selector: #selector(handleSelectPhoto:))
UIKit will automatically understand that the selector methods parameter will be of type UITapGestureRecognizer. Now rewrite the below method like this and you will be good to go.
#objc func handleSelectPhoto(gesture: UITapGestureRecognizer) {
if let buttonImageView = gesture.view as? UIImageView {
//Here you can make changes in imageview what ever you want.
}
}

How to use self in lazy initialization in Swift

Some websites recommend to initialize views using lazy initialization instead of using a storyboard.
It works when self is not used in lazy initialization.
But when self is used in it, a compile error occurs.
For example, in the following code, label1 can be compiled successfully, but label2 can't, because self is used in it.
How to use self in lazy initializations?
class A {
private let label1: UILabel = {
return UILabel()
}()
private let label2: UILabel = {
let view = UILabel()
self.addTextToLabel(view) // compile error !!!
return view
}()
private func addTextToLabel(label: UILabel) {
label.text = "test"
}
}
Your question is written under the misunderstanding that you are currently using lazy initialization. But you are not. Both label1 and label2 are not using lazy initialization. They are being initialized immediately when A is being initialized and this is the cause of the error since self isn't ready when these property initializers are called.
The solution is to actually make label2 a lazy property.
private lazy var label2: UILabel = {
let view = UILabel()
self.addTextToLabel(label: view)
return view
}()
But note that this label2 initialization will not happen until the first time you actually try to access the label2 property.
As per the docs:
A lazy stored property is a property whose initial value is not calculated until the first time it is used. You indicate a lazy stored property by writing the lazy modifier before its declaration.
Lazy properties are useful when the initial value for a property is dependent on outside factors whose values are not known until after an instance’s initialization is complete.
Reference:
https://docs.swift.org/swift-book/LanguageGuide/Properties.html

Property Initialization with closures

I was looking into ARC and strong reference cycles and ran into this code of mine:
class TestClass: UIView {
let button: UIButton = {
let view = UIButton()
view.frame = CGRect(x: 50, y: 50, width: 200, height: 200)
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
view.setTitle("Button", for: .normal)
view.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)
return view
}()
#objc private func buttonClicked() {
print("Clicked")
}
override init(frame: CGRect) {
super.init(frame: frame)
print("Object of TestClass initialized")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("Object of TestClass deinitialized")
}
}
reference to self in the addTarget method inside the closure doesn't seem to create a strong reference cycle.
Can someone explain why?
Also, I noticed that if I remove inheritance from UIView the compiler starts complaining: Use of unresolved identifier 'self'.
Can someone explain this as well, why does it happen in this case and doesn't in the first one?
This is not a retain cycle because self is not what you think it is :)
Properties with initial value are "executed" even before any initializer runs, and for those properties self points to a higher order function of this type:
(TestClass) -> () -> TestClass
So you don't really access the instance, but rather you access a static-like method that does the initialization of all properties that have a default value. This is why you don't have a retain cycle.
addTarget accepts an Any? value for it's first argument, so this violates no type rules so the compiler doesn't complain that you don't pass a NSObject instance there.
Checking the later behaviour - e.g. what happens if the button is added to the UI hierarchy and is tapped, reveals something interesting: the runtime sees that you passed a non-object as a target and sets null values for target and action:

pass custom parameter to uibutton #selector swift 3

I have a 2 classes where I am passing uistackviews from one class to other. I want the controls to be created in same stackview. Hence I am passing the view in all the render function parameters. I also want that view to be passed with action #selector of uibutton
class 1:
class ViewController: UIViewController {
func createbutton(parentview: UIStackView) {
let buttn = UIButton()
buttn.backgroundColor = .gray
buttn.setTitle("testttt", for: .normal)
buttn.frame.size.height = 30
buttn.frame.size.width = 40
buttn.addTarget(self, action: #selector(anotherbutton(parentview:)), for: .touchUpInside)
parentview.addArrangedSubview(buttn)
}
func anotherbutton(parentview: UIStackView) {
//another button here
}
func loadpage() {
print("loadpage")
}
}
Class 2:
class plugin : UIViewController {
let vw = ViewController()
override func viewDidLoad() {
super.viewDidLoad()
let parentview = getparentnode()
vw.createbutton(parentview: parentview)
}
func getparentnode() -> UIStackView {
let parentnode = UIStackView()
parentnode.axis = UILayoutConstraintAxis.vertical
parentnode.distribution = UIStackViewDistribution.equalSpacing
parentnode.alignment = UIStackViewAlignment.center
parentnode.spacing = 16.0
parentnode.tag = 50
parentnode.translatesAutoresizingMaskIntoConstraints = false;
self.view.addSubview(parentnode)
//Constraints
parentnode.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
parentnode.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
return parentnode
}
}
but this throws an error unrecognized selector sent to instance 0x7b25e010'
How to pass the UIView in action selector parameter ? Thank you for any help
You can't. The only things that you can pass through a selector is:
Nothing
The object itself (in this case the button)
These scenarios would look like this:
button.addTarget(self, action: #selector(myFunc), ...) //no parameters
or
button.addTarget(self, action: #selector(myFunc(_:)) //passes itself (the button)
If you want to pass the value of a view to another ViewController I recommend using the prepareForSegue method. That is how you are supposed to pass data from ViewController to ViewController.
In terms of the rest of your code, I believe you are breaking the MVC design pattern by creating an instance of your class in another class (this line: let vw = ViewController()). First of all, this will create an entirely new instance if your ViewController, which isn't the same as the one running on your device. Second of all, this is bad practice. You should be allowing each viewController to manage itself and not have outwards interference from other viewControllers. Using prepareForSegue is an example of using the MVC design pattern effectively.
Hope this helped.

What am I doing when I initialize a property in the form let var : ClassName = {...}()

I have this in my view controller
let inputsContainerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.whiteColor()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 5
view.layer.masksToBounds = true
return view
}()
I am not exactly sure how the initialization within the curly braces works. Is this shorthand? If so, what would be the long form?
Note, I am just trying to understand this syntax more than anything, it works just fine.
You are defining a function and calling that function to get its result. Then you assign that result (view, the value returned from the function) as the initial value of your variable inputsContainerView.
I call this idiom define-and-call, and it's very useful in exactly this situation, namely, you want to initialize a variable but your initializer needs to consist of several lines of code.
You are defining a closure that takes no parameters and returns a UIView and then you are calling that closure with () to get back a UIView which is assigned to inputsContainerView.
Is this shorthand? If so, what would be the long form?
Yes, the full form of a closure specifies the inputs and outputs. The long form would add () -> UIView in. Swift is able to infer the inputs and outputs from the values passed in () and the fact that the result is assigned to a UIView constant which is why you were able to leave them off.
let inputsContainerView: UIView = { () -> UIView in
let view = UIView()
view.backgroundColor = UIColor.whiteColor()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 5
view.layer.masksToBounds = true
return view
}()
You can read more about closures here.