Swift property wrapper not compiling any more in Swift 5.4+? - ios

The following code used to work for Swift 5.2, and possibly Swift 5.3.
(Last build was November 2020)
#propertyWrapper
class ActionBindable<Button> where Button : UIControl {
var target: Any? {
didSet { setTargetAction() }
}
weak var wrappedValue: Button! {
didSet { setTargetAction() }
}
private let action: Selector
private let event: UIControl.Event
init(action: Selector, event: UIControl.Event = .touchUpInside) {
self.action = action
self.event = event
}
private func setTargetAction() {
guard target != nil && wrappedValue != nil else { return }
wrappedValue.addTarget(target, action: action, for: event)
}
}
However, I'm getting an error now:
Property type 'UIKit.UIControl?' does not match 'wrappedValue' type 'UIKit.UIControl?'
Haven't been following property wrappers for some time, so I'm wondering what changed.
Here is the code where the property wrapper is being used:
#ActionBindable(action: #selector(addAction))
var addButton: UIControl!

The bug we have to deal with right now is:
When wrappedValue is weak, there is no mechanism to explicitly set a type for a wrapped variable that uses the relevant property wrapper, after its name.
Your workaround is:
// ActionBindable<UIControl>
#ActionBindable(action: #selector(addAction)) var addButton
// Any subclasses:
#ActionBindable<UIButton>(action: #selector(addAction)) var addButton
However, I bet you'll have other problems, because implicitly-unwrapped optionality doesn't propagate outside of property wrappers. I bet it used to, given your addButton definition. You'll probably need a computed variable to wrap addButton then, to avoid future optional checking, which may negate the point of having a property wrapper.

Related

(Swift) How to get rid of "Left side of mutating operator isn't mutable"

complete error:
Left side of mutating operator isn't mutable: 'abc' is a 'let' constant
Happens because I am trying to change value of a variable sent by parameter to function.
Can I get rid of this, or find some other solution?
Code(My code is much complex, but in effect doing the same as this):
func generateABC() {
var abc = "this"
abc += "is"
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(abc)) )
tapGesture.delegate = self
webView.addGestureRecognizer(tapGesture)
abc += "function"
}
handleTap function :
#objc
func handleTap(_ someString: String) {
someString += "my"
}
Short story: It's impossible to add custom parameters to (any) target/action
Either there is no parameter
#objc
func handleTap() { ...
or the affected recognizer is the parameter
#objc
func handleTap(_ recognizer : UITapGestureRecognizer) { ...
That's it. In both cases the corresponding declaration is
UITapGestureRecognizer(target: self, action: #selector(handleTap))
You have to use a temporary variable to handle the string.
For passing parameters using UITapGestureRecognizer, One approach would be to subClass UITapGestureRecognizer and then set a property as example below:
class SampleGesture: UITapGestureRecognizer {
var someString = String()
}
class ViewController: UIViewController {
let tapGesture = SampleGesture(target: self, action: #selector(self.handleTap))
tapGesture.delegate = self
webView.addGestureRecognizer(tapGesture)
tapGesture.someString = //your text
}
And as for error others already said in answers that Parameters of a function are 'constants' by default
#objc func handleTap(sender: SampleGesture) {
var newTitle: String = sender.someString // you can declare as globally
newTitle += "my"
}
You can not change a Passed variable's value within function. If you want to change value of someString then you have to store it into another variable and use it further like this.
#objc
func handleTap(_ someString: String) {
var newString: String = someString
newString += "my"
}
All parameters passed into a Swift function are constants, so you can’t change them. If you want, you can pass in one or more parameters as inout, which means they can be changed inside your function, and those changes reflect in the original value outside the function.
func doubleInPlace(number: inout Int) {
number *= 2
}
Credit: Paul Hudson https://www.hackingwithswift.com/sixty/5/10/inout-parameters
Be careful if you are going with this approach, because you can end up with unexpected sideffects. You can also simply use a temp variable inside your function, and assign the parameter to it
#objc
func handleTap(_ someString: String)-> String {
var tempString = someString
tempString += "Whatever"
return tempString
}

Selectors in Swift 4.0

I was wondering if there is a more 'swift 4' way of creating a selector and calling a function? I am wanting to have the click of the Status Bar Button to call a simple print command from a function, but is this outdated or is there a more efficient 'swift' way of doing this?
button.action = #selector(myFunction)
#objc func myFunction (sender: NSStatusBarButton) {
print("Hi")
}
I am not sure if there's any good way to avoid using the target/action pattern under the hood, but you can definitely try to hide it.
Personally I use ReactiveSwift for all callbacks so I never have to use this awkward objc syntax. Another way to do it would be to hide this inside an extension. For instance, you can try something like:
extension UIButton {
private struct AssociatedKeys {
static var TouchUpClosure = "touchUpClosure"
}
internal var onTouchUpInside: ((UIButton) -> ())? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.TouchUpClosure) as? (UIButton) -> ()
}
set {
objc_setAssociatedObject(
self,
&AssociatedKeys.TouchUpClosure,
newValue as? (UIButton) -> (), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
button.action = #selector(executeTouchUpInside:)
}
}
#objc func executeTouchUpInside(sender: UIButton) {
self.touchUpInside(sender)
}
}
Which allows you to use a "more swift" syntax (no #objc or #selector):
button.onTouchUpInside = { _ in print("Hi") }
Disclaimer - I haven't checked if this exact code compiles, this post is more about sharing an idea.

Passing arguments to selector in Swift

I'm programmatically adding a UITapGestureRecognizer to one of my views:
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(modelObj:myModelObj)))
self.imageView.addGestureRecognizer(gesture)
func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
The first problem I encountered was "Argument of '#selector' does not refer to an '#Objc' method, property, or initializer.
Cool, so I added #objc to the handleTap signature:
#objc func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
Now I'm getting the error "Method cannot be marked #objc because the type of the parameter cannot be represented in Objective-C.
It's just an image of the map of a building, with some pin images indicating the location of points of interest. When the user taps one of these pins I'd like to know which point of interest they tapped, and I have a model object which describes these points of interest. I use this model object to give the pin image it's coordinates on the map so I thought it would have been easy for me to just send the object to the gesture handler.
It looks like you're misunderstanding a couple of things.
When using target/action, the function signature has to have a certain form…
func doSomething()
or
func doSomething(sender: Any)
or
func doSomething(sender: Any, forEvent event: UIEvent)
where…
The sender parameter is the control object sending the action message.
In your case, the sender is the UITapGestureRecognizer
Also, #selector() should contain the func signature, and does NOT include passed parameters. So for…
func handleTap(sender: UIGestureRecognizer) {
}
you should have…
let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
Assuming the func and the gesture are within a view controller, of which modelObj is a property / ivar, there's no need to pass it with the gesture recogniser, you can just refer to it in handleTap
Step 1: create the custom object of the sender.
step 2: add properties you want to change in that a custom object of the sender
step 3: typecast the sender in receiving function to a custom object and access those properties
For eg:
on click of the button if you want to send the string or any custom object then
step 1: create
class CustomButton : UIButton {
var name : String = ""
var customObject : Any? = nil
var customObject2 : Any? = nil
convenience init(name: String, object: Any) {
self.init()
self.name = name
self.customObject = object
}
}
step 2-a: set the custom class in the storyboard as well
step 2-b: Create IBOutlet of that button with a custom class as follows
#IBOutlet weak var btnFullRemote: CustomButton!
step 3: add properties you want to change in that a custom object of the sender
btnFullRemote.name = "Nik"
btnFullRemote.customObject = customObject
btnFullRemote.customObject2 = customObject2
btnFullRemote.addTarget(self, action: #selector(self.btnFullRemote(_:)), for: .touchUpInside)
step 4: typecast the sender in receiving function to a custom object and access those properties
#objc public func btnFullRemote(_ sender: Any) {
var name : String = (sender as! CustomButton).name as? String
var customObject : customObject = (sender as! CustomButton).customObject as? customObject
var customObject2 : customObject2 = (sender as! CustomButton).customObject2 as? customObject2
}
Swift 5.0 iOS 13
I concur a great answer by Ninad. Here is my 2 cents, the same and yet different technique; a minimal version.
Create a custom class, throw a enum to keep/make the code as maintainable as possible.
enum Vs: String {
case pulse = "pulse"
case precision = "precision"
}
class customTap: UITapGestureRecognizer {
var cutomTag: String?
}
Use it, making sure you set the custom variable into the bargin. Using a simple label here, note the last line, important labels are not normally interactive.
let precisionTap = customTap(target: self, action: #selector(VC.actionB(sender:)))
precisionTap.customTag = Vs.precision.rawValue
precisionLabel.addGestureRecognizer(precisionTap)
precisionLabel.isUserInteractionEnabled = true
And setup the action using it, note I wanted to use the pure enum, but it isn't supported by Objective C, so we go with a basic type, String in this case.
#objc func actionB(sender: Any) {
// important to cast your sender to your cuatom class so you can extract your special setting.
let tag = customTag as? customTap
switch tag?.sender {
case Vs.pulse.rawValue:
// code
case Vs.precision.rawValue:
// code
default:
break
}
}
And there you have it.
cell.btn.tag = indexPath.row //setting tag
cell.btn.addTarget(self, action: #selector(showAlert(_ :)), for: .touchUpInside)
#objc func showAlert(_ sender: UIButton){
print("sender.tag is : \(sender.tag)")// getting tag's value
}
Just create a custom class of UITapGestureRecognizer =>
import UIKit
class OtherUserProfileTapGestureRecognizer: UITapGestureRecognizer {
let userModel: OtherUserModel
init(target: AnyObject, action: Selector, userModel: OtherUserModel) {
self.userModel = userModel
super.init(target: target, action: action)
}
}
And then create UIImageView extension =>
import UIKit
extension UIImageView {
func gotoOtherUserProfile(otherUserModel: OtherUserModel) {
isUserInteractionEnabled = true
let gestureRecognizer = OtherUserProfileTapGestureRecognizer(target: self, action: #selector(self.didTapOtherUserImage(_:)), otherUserModel: otherUserModel)
addGestureRecognizer(gestureRecognizer)
}
#objc internal func didTapOtherUserImage(_ recognizer: OtherUserProfileTapGestureRecognizer) {
Router.shared.gotoOtherUserProfile(otherUserModel: recognizer.otherUserModel)
}
}
Now use it like =>
self.userImageView.gotoOtherUserProfile(otherUserModel: OtherUserModel)
You can use an UIAction instead:
self.imageView.addAction(UIAction(identifier: UIAction.Identifier("imageClick")) { [weak self] action in
self?.handleTap(modelObj)
}, for: .touchUpInside)
that may be a terrible practice but I simply add whatever I want to restore to
button.restorationIdentifier = urlString
and
#objc func openRelatedFact(_ sender: Any) {
if let button = sender as? UIButton, let stringURL = factButton.restorationIdentifier, let url = URL(string: stringURL) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
}
}
}

Binding dose not seem to be working from ReactiveCocoa Swift

var viewModel = CTCViewModel()
var mainView: CTCMainView {
return self.view as! CTCMainView
}
override func viewDidLoad() {
super.viewDidLoad()
let callButtonEnabledSignal = self.viewModel.rac_valuesForKeyPath("callButtonEnabled", observer: self.viewModel)
callButtonEnabledSignal.setKeyPath("enabled", onObject: self.mainView.callButton, nilValue: false)
self.mainView.callButton.rac_signalForControlEvents(UIControlEvents.TouchUpInside).subscribeNext {
(Void) -> Void in
self.viewModel.callButtonEnabled = !self.viewModel.callButtonEnabled
}
}
When I press the button, self.viewModel.callButtonEnabled did get updated but the enabled property of the button. It does not seem they are bound.
Add dynamic on your callButtonEnabled property:
class CTCViewModel: NSObject {
dynamic var callButtonEnabled = false
}
Because the implementation of rac_valuesForKeyPath is using Objective-C runtime, and the compiler can omit it when access Swift properties. You mark a property with dynamic to let the compiler always use Objective-C runtime.

Execute a method when a variable value changes in Swift

I need to execute a function when a variable value changes.
I have a singleton class containing a shared variable called labelChange. Values of this variable are taken from another class called Model. I have two VC classes, one of them has a button and a label and the second only a button.
When the button in the first VC class is pressed I am updating the label with this func:
func updateLabel(){
self.label.text = SharingManager.sharedInstance.labelChange
}
But I want to call the same method whenever the value of the labelChange is changed. So in button click I will only update the labelChange value and when this thing happen I want to update the label with the new value of the labelChange. Also in the second VC I am able to update the labelChange value but I am not able to update the label when this value is changed.
Maybe properties are the solution but can anyone show me how to do so.
Edited second time:
Singleton Class:
class SharingManager {
func updateLabel() {
println(labelChange)
ViewController().label.text = SharingManager.sharedInstance.labelChange
}
var labelChange: String = Model().callElements() {
willSet {
updateLabel()
}
}
static let sharedInstance = SharingManager()
}
First VC:
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
#IBAction func Button(sender: UIButton) {
SViewController().updateMessageAndDismiss()
}
}
Second VC:
func updateMessageAndDismiss() {
SharingManager.sharedInstance.labelChange = modelFromS.callElements()
self.dismissViewControllerAnimated(true, completion: nil)
}
#IBAction func b2(sender: UIButton) {
updateMessageAndDismiss()
}
I made some improvements but I need to reference a label from the first VC class in singleton. Therefore I will update that label of VC in singleton.
When I print the value of labelChange the value is being updated and everything is fine. But when I try to update that value on label from singleton I receive an error:
unexpectedly found nil while unwrapping an Optional value
and the error is pointing in 4th line of singleton class.
You can simply use a property observer for the variable, labelChange, and call the function that you want to call inside didSet (or willSet if you want to call it before it has been set):
class SharingManager {
var labelChange: String = Model().callElements() {
didSet {
updateLabel()
}
}
static let sharedInstance = SharingManager()
}
This is explained in Property Observers.
I'm not sure why this didn't work when you tried it, but if you are having trouble because the function you are trying to call (updateLabel) is in a different class, you could add a variable in the SharingManager class to store the function to call when didSet has been called, which you would set to updateLabel in this case.
Edited:
So if you want to edit a label from the ViewController, you would want to have that updateLabel() function in the ViewController class to update the label, but store that function in the singleton class so it can know which function to call:
class SharingManager {
static let sharedInstance = SharingManager()
var updateLabel: (() -> Void)?
var labelChange: String = Model().callElements() {
didSet {
updateLabel?()
}
}
}
and then set it in whichever class that you have the function that you want to be called, like (assuming updateLabel is the function that you want to call):
SharingManager.sharedInstance.updateLabel = updateLabel
Of course, you will want to make sure that the view controller that is responsible for that function still exists, so the singleton class can call the function.
If you need to call different functions depending on which view controller is visible, you might want to consider Key-Value Observing to get notifications whenever the value for certain variables change.
Also, you never want to initialize a view controller like that and then immediately set the IBOutlets of the view controller, since IBOutlets don't get initialized until the its view actually get loaded. You need to use an existing view controller object in some way.
Hope this helps.
In Swift 4 you can use Key-Value Observation.
label.observe(\.text, changeHandler: { (label, change) in
// text has changed
})
This is basically it, but there is a little catch. "observe" returns an NSKeyValueObservation object that you need to hold! - when it is deallocated, you’ll receive no more notifications. To avoid that we can assign it to a property which will be retained.
var observer:NSKeyValueObservation?
// then assign the return value of "observe" to it
observer = label.observe(\.text, changeHandler: { (label, change) in
// text has changed,
})
You can also observe if the the value has changed or has been set for the first time
observer = label.observe(\.text, changeHandler: { (label, change) in
// just check for the old value in "change" is not Nil
if let oldValue = change.oldValue {
print("\(label.text) has changed from \(oldValue) to \(label.text)")
} else {
print("\(label.text) is now set")
}
})
For More Information please consult Apples documentation here
Apple provide these property declaration type :-
1. Computed Properties:-
In addition to stored properties, classes, structures, and enumerations can define computed properties, which do not actually store a value. Instead, they provide a getter and an optional setter to retrieve and set other properties and values indirectly.
var otherBool:Bool = false
public var enable:Bool {
get{
print("i can do editional work when setter set value ")
return self.enable
}
set(newValue){
print("i can do editional work when setter set value ")
self.otherBool = newValue
}
}
2. Read-Only Computed Properties:-
A computed property with a getter but no setter is known as a read-only computed property. A read-only computed property always returns a value, and can be accessed through dot syntax, but cannot be set to a different value.
var volume: Double {
return volume
}
3. Property Observers:-
You have the option to define either or both of these observers on a property:
willSet is called just before the value is stored.
didSet is called immediately after the new value is stored.
public var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
NOTE:- For More Information go on professional link
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html
There is another way of doing so, by using RxSwift:
Add RxSwift and RxCocoa pods into your project
Modify your SharingManager:
import RxSwift
class SharingManager {
static let sharedInstance = SharingManager()
private let _labelUpdate = PublishSubject<String>()
let onUpdateLabel: Observable<String>? // any object can subscribe to text change using this observable
// call this method whenever you need to change text
func triggerLabelUpdate(newValue: String) {
_labelUpdate.onNext(newValue)
}
init() {
onUpdateLabel = _labelUpdate.shareReplay(1)
}
}
In your ViewController you can subscribe to value update in two ways:
a. subscribe to updates, and change label text manually
// add this ivar somewhere in ViewController
let disposeBag = DisposeBag()
// put this somewhere in viewDidLoad
SharingManager.sharedInstance.onUpdateLabel?
.observeOn(MainScheduler.instance) // make sure we're on main thread
.subscribeNext { [weak self] newValue in
// do whatever you need with this string here, like:
// self?.myLabel.text = newValue
}
.addDisposableTo(disposeBag) // for resource management
b. bind updates directly to UILabel
// add this ivar somewhere in ViewController
let disposeBag = DisposeBag()
// put this somewhere in viewDidLoad
SharingManager.sharedInstance.onUpdateLabel?
.distinctUntilChanged() // only if value has been changed since previous value
.observeOn(MainScheduler.instance) // do in main thread
.bindTo(myLabel.rx_text) // will setText: for that label when value changed
.addDisposableTo(disposeBag) // for resource management
And don't forget to import RxCocoa in ViewController.
For triggering event just call
SharingManager.sharedInstance.triggerLabelUpdate("whatever string here")
HERE you can find example project. Just do pod update and run workspace file.
var item = "initial value" {
didSet { //called when item changes
print("changed")
}
willSet {
print("about to change")
}
}
item = "p"
override var isHighlighted: Bool {
get { super.isHighlighted }
set {
super.isHighlighted = newValue
if newValue {
label.textColor = highlightedTextColor
contentView.backgroundColor = highlightedBackgroundColor
} else {
label.textColor = normalTextColor
contentView.backgroundColor = normalBackgroundColor
}
}
}

Resources