what is difference of self in lazy var and var(let) block - ios

when I use let UISwitch replace lazy var UISwitch in UITableViewCell like this:
let trigger: UISwitch = {
let sw = UISwitch()
sw.addTarget(self, action: #selector(onTriggerChanged(sender:)), for: .valueChanged)
return sw
}()
I found the function onTriggerChanged never called, so I do some test, here is my test code:
class TestCell: UITableViewCell {
lazy var trigger: UISwitch = {
let sw = UISwitch()
print("trigger \(self)")
sw.addTarget(self, action: #selector(onTriggerChanged(sender:)), for: .valueChanged)
return sw
}()
var trigger2: UISwitch = {
let sw = UISwitch()
print("trigger2 \(self)")
sw.addTarget(self, action: #selector(onTriggerChanged(sender:)), for: .valueChanged)
return sw
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let trigger3: UISwitch = {
let sw = UISwitch()
print("trigger3 \(self)")
sw.addTarget(self, action: #selector(onTriggerChanged(sender:)), for: .valueChanged)
return sw
}()
contentView.addSubview(trigger)
contentView.addSubview(trigger2)
contentView.addSubview(trigger3)
}
}
print message is:
trigger <TestCell: 0x......>
trigger2 (Function)
trigger3 <TestCell: 0x......>
and trigger2 valueChanged event not called.
so what difference of self in trigger/trigger2/trigger3

I'm sure you're aware, self (in the usual sense) is not available in property initialisers of non-lazy properties, which is why you use lazy as a workaround.
The self that you are printing in trigger2 can't refer to the self instance. It actually refers to an instance method called self, declared in NSObject, which is inherited by TestCell.
This is actually an instance method. In Swift, you are allowed to refer to instance methods without having an instance (remember that you don't have access to the actual self instance here). It's just that their type becomes (T) -> F, where T is the type in which the instance method is declared, and F is the function type for the method. Essentially, they become static methods that take an instance as parameter, and give you back the original instance method.
For example, the self method, when referenced without an instance, would have the type:
(TestCell) -> (() -> TestCell)
You can see that is indeed what it is by printing out type(of: self).
Clearly, adding the self method as the target doesn't work. It can't respond to the selector onTriggerChangedWithSender.
For trigger1 and trigger3, the self instance is available, and self mean the usual thing - the current instance.

Lazy var is computed when the variable is required somewhere and only runs the initializing code once while non lazy var and let compute the value when the variable is initialized. and where is the onTriggerChanged function.

Related

Xcode 13.3 warning: "'self' refers to the method '{object}.self', which may be unexpected

I just updated to Xcode 13.3 and I'm seeing several instances of a new warning that I've not seen with previous versions of Xcode. As an example, I have a simple table view cell named LabelAndSwitchTableViewCell that looks like this:
import UIKit
class LabelAndSwitchTableViewCell: UITableViewCell {
private let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let _switch: UISwitch = {
let _switch = UISwitch()
_switch.translatesAutoresizingMaskIntoConstraints = false
_switch.addTarget(self, action: #selector(didToggleSwitch), for: .valueChanged)
return _switch
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(label)
contentView.addSubview(_switch)
// layout constraints removed for brevity
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc private func didToggleSwitch() {
print("Switch was toggled...")
}
}
As you can see, I'm adding a target to the switch that I want to be called when the value of the switches changes:
_switch.addTarget(self, action: #selector(didToggleSwitch), for: .valueChanged)
After updating to Xcode 13.3, I'm now seeing a new warning on this line:
'self' refers to the method 'LabelAndSwitchTableViewCell.self', which may be unexpected
Xcode's suggestion to silence this warning is to replace:
_switch.addTarget(self, action: #selector(didToggleSwitch), for: .valueChanged)
...with...
_switch.addTarget(LabelAndSwitchTableViewCell.self, action: #selector(didToggleSwitch), for: .valueChanged)
Making this change does silence the warning but it also causes the app to crash (unrecognized selector) when I toggle the switch. Here's the dump from that crash:
[app_mockup.LabelAndSwitchTableViewCell didToggleSwitch]: unrecognized selector sent to class 0x1043d86e8
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[app_mockup.LabelAndSwitchTableViewCell didToggleSwitch]: unrecognized selector sent to class 0x1043d86e8'
Making the didToggleSwitch() method static will prevent the crash but I'm not sure why I'd want to do that. I can obviously revert the change (from LabelAndSwitchTableViewCell.self back to just self) but I'm wondering if there's something else that I should be doing to address this?
You can fix by changing the lets to lazy var's
private lazy var _switch2: UISwitch = {
let _switch = UISwitch()
_switch.translatesAutoresizingMaskIntoConstraints = false
_switch.addTarget(self, action: #selector(didToggleSwitch), for: .valueChanged)
return _switch
}()
The Xcode fix-it suggestion is just wrong.
The reason is self is not ready yet in phase 1 of object initialisation. Phase 1 is to set all stored properties, and only in phase 2, you can access to self.
To fix your code, you can use lazy property, where the initialisation phase 1 is completed.
Here is the reference:
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html
For this warning:
If there is no super class, or the super class is not NSObject, self is an error when to use in a stored property directly.
If the super class is NSObject, self gets compiled to an auto closure (ClassXXX) -> () -> ClassXXX. So in runtime, self will be used as current instance. But, since Swift 5.6 is warning us on that, I think the reference of self in stored property of a NSObject subclass may not be allowed in future swift versions.
Example 1: error when there is no super class
Example 2: the compiled self in AST
For this code:
import Foundation
class MyTest: NSObject {
var myself = self
}
Here is part of the compiled AST:
LabelAndSwitchTableViewCell.self as suggested, will not work in most cases. Use nil and search the responder chain.
addTarget(_:action:for:)

UICollectionViewListCell and custom cell accessory in iOS 14

My issues with new collection view list cells is that I'm not able to add action handlers to a custom accessory view.
I've been trying to do the following:
protocol TappableStar: class {
func onStarTapped(_ cell: UICollectionViewCell)
}
class TranslationListCell: UICollectionViewListCell {
let starButton: UIButton = {
let starButton = UIButton()
let starImage = UIImage(systemName: "star")!.withRenderingMode(.alwaysTemplate)
starButton.setImage(starImage, for: .normal)
starButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
starButton.addTarget(self, action: #selector(starButtonPressed(_:)), for: .touchUpInside)
starButton.tintColor = .systemOrange
return starButton
}()
var translation: TranslationModel?
weak var link: TappableStar?
override init(frame: CGRect) {
super.init(frame: frame)
accessories = [.customView(configuration: .init(customView: starButton, placement: .trailing(displayed: .always)))]
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc private func starButtonPressed(_ sender: UIButton) {
link?.onStarTapped(self)
}
override func updateConfiguration(using state: UICellConfigurationState) {
// Create new configuration object and update it base on state
var newConfiguration = TranslationContentConfiguration().updated(for: state)
// Update any configuration parameters related to data item
newConfiguration.inputText = translation?.inputText
newConfiguration.outputText = translation?.outputText
contentConfiguration = newConfiguration
}
}
I subclass UICollectionViewListCell, create a button with target-action handler and add it to accessories array. I also have my own implementation of cell configuration.
Now, I create a protocol where I delegate action handling to my view controller (I also implemented new cell registration API and set cell.link = self).
My problem here is that my accessory button doesn't call starButtonPressed although this accessory view is responsive (it changes color when highlighted).
My idea is that there might be something wrong with the way I implement my action handling with a custom accessory but there seems to be little to none information about this new api.
Moreover, when choosing between predefined accessories, some of them have actionHandler closures of type UICellAccessory.ActionHandler but I don't seem to understand how to properly implement that.
Any ideas would be much appreciated.
iOS 14, using UIActions
Since iOS 14 we can initialise UIButton and other UIControls with primary actions. It becomes similar to handlers of native accessories. With this we can use any parametrized method we want. And parametrising is important, because usually we want to do some action with specific cell. #selector's cannot be parametrised, so we can't pass any information to method about which cell is to be updated.
But this solution works only for iOS 14+.
Creating UIAction:
let favoriteAction = UIAction(image: UIImage(systemName: "star"),
handler: { [weak self] _ in
guard let self = self else { return }
self.handleFavoriteAction(for: your_Entity)
})
Creating UIButton:
let favoriteButton = UIButton(primaryAction: favoriteAction)
Creating accessory:
let favoriteAccessory = UICellAccessory.CustomViewConfiguration(
customView: favoriteButton,
placement: .leading(displayed: .whenEditing)
)
Using
cell.accessories = [.customView(configuration: favoriteAccessory)]
I solved my issue by adding tap gesture recognizer to my accessory's custom view. So it works like this:
let customAccessory = UICellAccessory.CustomViewConfiguration(
customView: starButton,
placement: .trailing(displayed: .always))
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(starButtonPressed(_:)))
customAccessory.customView.addGestureRecognizer(tapGesture)
accessories = [.customView(configuration: customAccessory)]
Haven't seen it documented anywhere so hope it helps somebody.
I followed a similar approach to yours, but instead of a UITapGestureRecognizer, I added a target to the button.
var starButton = UIButton(type: .contactAdd)
starButton.addTarget(self, action: #selector(self.starButtonTapped), for: .touchUpInside)
let customAccessory = UICellAccessory.CustomViewConfiguration(customView: starButton, placement: .trailing(displayed: .always))
cell.accessories = [.customView(configuration: customAccessory)]
I first tried the tap gesture recognizer and it didn't work for me.

Swift Variable used within its own initial value while using #selector

I'm trying to invoke my method from selector (action) but getting error Variable used within its own initial value Below is my code snippet
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sendRequest(tapGestureRecognizer, userID)))
Below is the method which I'm calling
#objc func sendRequest(tapGestureRecognizer: UITapGestureRecognizer, identifer: String) {
print("hello world")
}
My method accepts 2 paramters. I don't know how to call it. The way I'm currently calling the sendRequestMethod is throwing error.
Please help me to get it resolved.
when those functions are inside an UIView, as GestureRecognizers usually are, then you can make a hittest and find what UIView was under your touch.
Consider this is not the ideal way to catch touched views. As every UIView also offers you a .tag you can set a numbering and use it to make assumtions to whom the hit UIView fits. See Example
class checkCheckView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sendRequest(recognizer:identifer:)) )
#objc func sendRequest(recognizer:UITapGestureRecognizer, identifer:String) {
print("hello world")
// your problem will be, how is identifier passed in here?
}
let PanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panning(gesture:)))
#objc func panning(gesture:UIPanGestureRecognizer) {
//print("pan-translation %#", gesture.translation(in: self).debugDescription)
let location = gesture.location(ofTouch: 0, in: self)
//print("pan-location %#",location.debugDescription);
let hitview = self.hitTest(location, with: nil)
if hitview != nil {
let UserIDTag = hitview?.tag ?? 0
print("userId by UIView tag",UserIDTag)
}
}
}
Hint: Much easier is to subclass UIView and prepare a property that holds UserID given when allocation is done. addtarget: action:#selector() to each of them. The action/selector method gets invoked and the sender is passed in and because you know the type of the sender (YourUserUIView) you find the UserID.
With GestureRecognizer's you tend to write recognition code that can become very complex the more complex your UI will be, instead of just passing objects to actions that tell you what you are looking for.

store #selector in a variable

I'm adding a target to a button but instead of having the target action reference a predefined function I want it to reference a closure.
typealias AlertAction = (title: String, handler: () -> ())
class ErrorView: UIView {
func addAction(_ action: AlertAction) {
let button = UIButton()
...
let selector = #selector(action.handler) //error happens here
button.addTarget(self, action: selector, for: .touchUpInside)
}
}
I'm getting an error on this line:
let selector = #selector(action.handler)
which is "Argument of '#selector' does not refer to an '#objc' method, property, or initializer"
This makes sense because usually you have to add #objc to your func declaration, but I'm wondering if there's a way to make my closure refer to an #objc method after the fact perhaps by wrapping it in another function.
Is this possible? I don't know how to define an #objc marked closure so I'm not sure.
#selector() is based on Objective-C bridging, since swift closures are non-objective-c, you can't use them.
One alternative solution is wrap your code inside an Objective-C function.
class AlertAction:NSObject {
var title:String?
#objc
func getHandler(sender:Any){
print("hi")
}
}
and use it like:
// Instance of a class
let alertAction = AlertAction()
// Usage
let button = UIButton()
let selector1 = #selector(AlertAction.getHandler)
button.addTarget(alertAction, action: selector1, for: .touchUpInside)

Adding an observer to a UISwitch within a custom tableview cell

This is a follow on from a previous question I have asked but I feel I am missing something very simple and its driving me up the wall!
I have a custom tableview cell which contains a switch and I need to trigger a function each time it's value is changed. I've tried using .addTarget but it never seems to trigger the function so maybe my selector syntax is incorrect.
I create the switch programatically within the tableview cell like this:
let thisSwitch: UISwitch = {
let thisSwitch = UISwitch()
thisSwitch.isOn = false
thisSwitch.translatesAutoresizingMaskIntoConstraints = false
thisSwitch.addTarget(self, action: Selector("switchTriggered:"), for: .valueChanged)
return thisSwitch
}()
Then directly below that I have my function:
func switchTriggered(sender: AnyObject) {
print("SWITCH TRIGGERED")
let sentSwitch = sender as! UISwitch
privateExercise.switchState = sentSwitch.isOn
}
It shows an error message stating " No method declared with Objective-C selector 'switchTriggered:' ". What am I missing here? Any help would be much appreciated!
The selector syntax should be
thisSwitch.addTarget(self, action: #selector(switchTriggered), for: .valueChanged)
Also keep the parameter as UISwitch type itself in order to avoid casting in function
func switchTriggered(sentSwitch: UISwitch) {
print("SWITCH TRIGGERED")
privateExercise.switchState = sentSwitch.isOn
}

Resources