Custom UIControl subclass with RxSwift - swift

I am creating a custom subclass of UIControl (I need to override its draw method) and I want to add RxSwift to bind its isSelected property to my model.
So far so good. This works fine.
My problem is how can I do to change the value isSelected property in response of user touchUpInside event?.
My first try was to use the addTarget method of UIControl, but changing the value of isSelected programmatically is not reported by the ControlProperty (as stated in the doc). But I can figure another way to resolve this.
Any help appreciated.
Source code of the subclass:
class SYYesNoButton: UIControl {
override init(frame: CGRect) {
super.init(frame: frame)
// subscribe to touchUpInside event
addTarget(
self,
action: #selector(userDidTouchUpInside),
for: UIControlEvents.touchUpInside)
}
func userDidTouchUpInside() {
// change the value of the property
// this does not work,
// the change is not reported to the ControlProperty
// HOW CAN I CHANGE THIS ??
self.isSelected = !isSelected
}
}
Extensions to add reactive support:
extension SYYesNoButton {
var rx_isSelected: ControlProperty<Bool> {
return UIControl.valuePublic(
self,
getter: { (button) -> Bool in
return button.isSelected
},
setter: { (button, value) in
button.isSelected = value
})
}
}
extension UIControl {
static func valuePublic<T, ControlType: UIControl>(_ control: ControlType, getter: #escaping (ControlType) -> T, setter: #escaping (ControlType, T) -> ()) -> ControlProperty<T> {
let values: Observable<T> = Observable.deferred { [weak control] in
guard let existingSelf = control else {
return Observable.empty()
}
return (existingSelf as UIControl).rx.controlEvent([.allEditingEvents, .valueChanged])
.flatMap { _ in
return control.map { Observable.just(getter($0)) } ?? Observable.empty()
}
.startWith(getter(existingSelf))
}
return ControlProperty(values: values, valueSink: UIBindingObserver(UIElement: control) { control, value in
setter(control, value)
})
}
}
Thanks for all.

Once you have an actual UIControl, there's an even nicer way to a "native" RxCocoa extension called a ControlProperty using a helper method in RxCocoa.
For example:
extension Reactive where Base: someControl {
var someProperty: ControlProperty<Float> {
return controlProperty(editingEvents: .valueChanged,
getter: { $0.value },
setter: { $0.value = $1 })
}
}
This will expose the current value from the getter block whenever the specified UIControlEvent is fired, and will also set the value whenever some stream is bound to it.
It sort of acts like an Observable and Observer type together - you can observe its value, but can also subscribe to it.

If you are subclassing from UIControl, then you are making your own control class and you have to override one or more of beginTracking(_:with:), continueTracking(_:with:), endTracking(_:with:), or cancelTracking(with:) to make the control work the way you want. Then call sendActions(for:) with the correct event. The guts of a UIControl would not have Rx in it.
Taking a queue from UIButton, your control should not select itself, although it can highlight and unhighlight itself (when the user's finger is on it for example.)
Once you have properly created your UIControl, code outside the control can use Rx to observe it with no extra work on your part.
The following works (Updated for Swift 5/RxSwift 5):
class ViewController: UIViewController {
#IBOutlet weak var yesNoButton: SYYesNoButton!
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
yesNoButton.rx.controlEvent(.touchUpInside)
.scan(false) { v, _ in !v }
.bind(to: yesNoButton.rx.isSelected)
.disposed(by: bag)
}
}
#IBDesignable
class SYYesNoButton: UIControl {
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = isSelected ? .green : .red
}
override var isSelected: Bool {
didSet {
super.isSelected = isSelected
backgroundColor = isSelected ? .green : .red
}
}
}

Related

Swift constrained Protocol for a subclass and superclass

I wanted to implement my own HUD for a UIViewCntroller and a UIView, so I did this:
protocol ViewHudProtocol {
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
Now I can easily adopt ViewHudProtocol on any UIView to call showLoadingView and hideLoadingView. The problem is I want to use the same protocol for UIViewController, so I did this:
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.view.showLoadingView() //Error: UIView has no member showLoadingView
}
func hideLoadingView() {
self.view.hideLoadingView() //Error: UIView has no member hideLoadingView
}
}
I agree to the error that UIView has not adopted the protocol yet. So I did this:
extension UIView: ViewHudProtocol {}
And it works. Is there a better way to do this? I mean it feels wrong to extend every view with ViewHudProtocol, where not all of them will use it. If I could do something like, "only adopt ViewHudProtocol implicitly for a UIView, if its UIViewController demands for it. Else you could adopt ViewHUDProtocol manually on any UIView when required."
I would solve this with the following approach, using associatedtype, defined only for needed views and/or controllers (tested in Xcode 11.2 / swift 5.1):
protocol ViewHudProtocol {
associatedtype Content : ViewHudProtocol
var content: Self.Content { get }
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
var content: some ViewHudProtocol {
return self
}
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.content.showLoadingView() //NO Error
}
func hideLoadingView() {
self.content.hideLoadingView() //NO Error
}
}
//Usage
extension UITableView: ViewHudProtocol { // only for specific view
}
extension UITableViewController: ViewHudProtocol { // only for specific controller
var content: some ViewHudProtocol {
return self.tableView
}
}
The problem
So you want to constraint the conformance of a UIViewController to the protocol ViewHudProtocol only when the UIViewController.view property conforms to ViewHudProtocol.
I am afraid this is not possible.
Understanding the problem
Let's have a better look at your problem
You have 2 types (UIView and UIViewController) and you want to add to both the same functionalities
func showLoadingView()
func hideLoadingView()
What Mick West teaches us
This kind of scenario is somehow similar to what Mick West faced during the development of the Tony Hawks series Mick West and an elegant solution is described in its article Evolve your hierarchy.
Solution
We can apply that approach to your problem and here's the solution
struct HudViewComponent {
let view: UIView
private let hud: UIView
init(view: UIView) {
self.view = view
self.hud = UIView(frame: view.frame)
self.hud.isHidden = true
self.view.addSubview(hud)
}
func showLoadingView() {
self.hud.isHidden = false
}
func hideLoadingView() {
self.hud.isHidden = true
}
}
protocol HasHudViewComponent {
var hidViewComponent: HudViewComponent { get }
}
extension HasHudViewComponent {
func showLoadingView() {
hidViewComponent.showLoadingView()
}
func hideLoadingView() {
hidViewComponent.hideLoadingView()
}
}
That's it, now you can add the hud functionalities to any Type conforming to HasHudViewComponent.
class SomeView: UIView, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self) }()
}
or
class MyViewController: UIViewController, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self.view) }()
}
Considerations
As you can see the idea is to thinking in terms of components.
You build a component (HudViewComponent) with your hud functionalities. The component only asks for the minimum requirements: it needs a UIView.
Next you define the HasHudViewComponent which states that the current type has a HudViewComponent property.
Finally you can add your hud functionalities to any Type which has a view (UIView, UIViewController, ...) simply conforming your type to HasHudViewComponent.
Notes
You asked an interesting question and I know this does not answers 100% what you were looking for, but by a practical point of view it should provides you with a tool to achieve what you need.
I would have taken this approach:
Create a UIView Class,
setup the view
Declare a shared object.
A function to show the view
A function to remove the view. and then call it in view controllers as IndicatorView.shared.show() , IndicatorView.shared.hide()
import Foundation
import UIKit
import Lottie
class IndicatorView : UIView {
static let shared = IndicatorView()
var loadingAnimation : AnimationView = {
let lottieView = AnimationView()
lottieView.translatesAutoresizingMaskIntoConstraints = false
lottieView.layer.masksToBounds = true
return lottieView
}()
var loadingLabel : UILabel = {
let label = UILabel()
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont(name: "SegoeUI", size: 12)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func show() {
setupLoadingView()
self.alpha = 0
UIView.animate(withDuration: 0.5, animations: {
self.isHidden = false
self.alpha = 1
}, completion: nil)
applyLottieAnimation()
}
public func hide() {
self.alpha = 1
UIView.animate(withDuration: 0.5, animations: {
self.alpha = 0
}, completion: { _ in
self.isHidden = true
self.removeFromSuperview()
}) }
private func setupLoadingView() {
let controller = UIApplication.shared.keyWindow!.rootViewController!
controller.view.addSubview(self)
//setup your views here
self.setNeedsLayout()
self.reloadInputViews()
}
}
For this particular scenario, a Decorator would work better, and result in a better design:
final class HUDDecorator {
private let view: UIView
init(_ view: UIView) {
self.view = view
}
func showLoadingView() {
// add the spinner
}
func hideLoadingView() {
// remove the spinner
}
}
Using the Decorator would then be as easy as declaring a property for it:
class MyViewController: UIViewController {
lazy var hudDecorator = HUDDecorator(view)
}
This will allow any controller to decide if it wants support for showing/hiding a loading view by simply exposing this property.
Protocols are too invasive for simple tasks like enhancing the looks on a UI component, and they have the disadvantage of forcing all views of a certain class to expose the protocol functionality, while the Decorator approach allows you to decide which view instances to receive the functionality.

Creating a selector with variable of function type

I am working on two views that are subclassing subclass of UITableViewCell. In the base one (subclass of UITableViewCell) I am trying to setup gesture recognizer in a way that each of super class could change the behavior (eventually call didTapped method on it's delegate) of the tap.
I have written following code. I can use #selector(tap), however I think that using a variable instead of overriding a tap method in each super class is a much cleaner way. Is it even possible to use something like #selector(tapFunc)? If no what would be the cleanest and best from engineering point of view solution?
class BaseCell: UITableViewCell {
#objc var tapFunc: () -> () = { () in
print("Tapped")
}
#objc func tap() {
print("TEST")
}
func setupBasicViews(withContent: () -> ()) {
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(tapFunc))
contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(tapGestureRecoginzer)
}
}
And then two views that are building on top of this one:
class ViewA: BaseCell {
//don't want to do this
override func tap() {
//do stuff
}
func setup {
//setup everything else
}
class ViewB: BaseCell {
var delegate: ViewBProtocool?
func setup {
tapFunc = { () in
delegate?.didTapped(self)
}
//setup everything else
}
You're not too far off. Make the following changes:
class BaseCell: UITableViewCell {
var tapFunc: (() -> Void)? = nil
// Called by tap gesture
#objc func tap() {
tapFunc?()
}
func setupBasicViews(withContent: () -> ()) {
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(tap))
contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(tapGestureRecoginzer)
}
}
class ViewA: BaseCell {
func setup() {
//setup everything else
}
}
class ViewB: BaseCell {
var delegate: ViewBProtocol?
func setup() {
tapFunc = {
delegate?.didTapped(self)
}
//setup everything else
}
}
Now each subclass can optionally provide a closure for the tapFunc property.
I show above that tapFunc is optional with no default functionality in the base class. Feel free to change that to provide some default functionality if desired.

Passing selector via IBInspectable in Swift 3

I created a custom control and I want to pass the action in an #IBInspectable property to achieve the same effect of setting up an #IBAction using UIButton. How should I go about doing this?
class MyControl: UIButton {
// Problem with this string approach is
//I have no way to know which instance to perform the selector on.
#IBInspectable var tapAction: String?
// set up tap gesture
...
func labelPressed(_ sender: UIGestureRecognizer) {
if let tapAction = tapAction {
// How should I convert the string in tapAction into a selector here?
//I also want to pass an argument to this selector.
}
}
}
I really don't know why do you want it, but... Here is my solution:
Create a MyActions class, with actions that MyControl can to call:
class MyActions: NSObject {
func foo() {
print("foo")
}
func bar() {
print("bar")
}
func baz() {
print("baz")
}
}
Replace your MyControl class to
class MyControl: UIButton {
#IBInspectable var actionClass: String?
#IBInspectable var tapAction: String?
private var actions: NSObject?
override func awakeFromNib() {
// initialize actions class
let bundleName = Bundle.main.bundleIdentifier!.components(separatedBy: ".").last!
let className = "\(bundleName).\(actionClass!)"
guard let targetClass = NSClassFromString(className) as? NSObject.Type else {
print("Class \(className) not found!")
return
}
self.actions = targetClass.init()
// create tap gesture
let tap = UITapGestureRecognizer(target: self, action: #selector(pressed(_:)))
self.addGestureRecognizer(tap)
}
func pressed(_ sender: UIGestureRecognizer) {
actions!.perform(Selector(tapAction!))
}
}
And, set attributes of your button:
You can change the Tap Action in run time, for example:
#IBAction func buttonChangeAction(_ sender: Any) {
buttonMyControl.tapAction = textField.text!
}
Maybe you can change my code to pass parameters, but... it's you want?

Protocol being reset in cell

I have a cell with a textField and a button. The button opens a page to collect data and has a protocol to pass that data back to the cell and fill the textField. That all works fine, however, when I come back the value is reset to zero. Print statements show that it is passing the data when it's set, but hitting Back clears it for some reason.
Protocol
protocol DistanceProtocol {
func distanceSet(distance: Double)
}
Call to protocol method
distanceProtocol?.distanceSet(totalDistance)
Cell class
class InputCell: CalculatorCell, DistanceProtocol {
#IBOutlet var textField: UITextField?
private var inputType = InputType.undefined
var viewController = UIViewController()
override func getHeight() -> CGFloat {
return 90
}
func distanceSet(distance: Double) {
print(distance)
textField?.text = "\(distance)"
}
func getInputType() -> InputType {
return inputType
}
func setInputType(inputType: InputType) {
self.inputType = inputType
}
#IBAction func walkTouched(sender: UIButton) {
let mapVc = viewController.storyboard!.instantiateViewControllerWithIdentifier("Map") as! MapLocationsViewController
mapVc.distanceProtocol = self
viewController.navigationController?.pushViewController(mapVc, animated: true)
}
}
As far as I can tell, everything is set up correctly. It's not reloading the cells in the tableView when I come back. Why is it resetting/how can I prevent it?
the text field is only set when you call the function. Unless you call this function in cellForRowAtIndex path it wont retain the value
I would likely implement it using a setter, do everytime the value is set, the label gets updated
protocol DistanceProtocol {
func distanceSet(distance: Double)
}
class CellWithText: UITableViewCell {
var cellText: String {
didSet {
textLabel?.text = cellText
}
}
}
extension CellWithText: DistanceProtocol {
func distanceSet(distance: Double) {
self.cellText = "\(distance)"
}
}
Then in your cellForRowAtIndexPath call you would call the function
cell.distanceSet(19.0)
i think you might try singleton design pattern or you can use struct for copying.

Implement variable attribute property observer in Swift

I want to implement a didSet over a "sub-attribute" of a variable.
Example:
#IBOutlet weak var myLabel: UILabel!
var myLabel.hidden { didSet{ "DO SOMETHING" } }
I want to hide/show some other views when myLabel.hidden attribute change.
How can I do it?
You can make a property like this
var hideLabel: Bool = false {
didSet {
myLabel.isHidden = hideLabel
//SHOW OR HIDE OTHER VIEWS
}
}
By doing this you don't have to use KVO at the same time you can add more controls to hide to show at didSet context.
I Believe this is a simpler way to do such a thing.
The standard process is to use KVO. Add observer when the view is loaded:
override func viewDidLoad() {
super.viewDidLoad()
label.addObserver(self, forKeyPath: "hidden", options: .New | .Old, context: nil)
}
When the view controller is deallocated, make sure to remove the observer.
deinit {
label.removeObserver(self, forKeyPath: "hidden")
}
And do whatever you want inside the observeValueForKeyPath method:
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
NSLog("\(change)")
// do whatever you want here
}
Property observers can only be added to a class, not an instance. In order to do this you need to subclass UILabel and then override hidden's property observers.
EDIT: If you want to set custom observers on instances of CustomLabel, you can do this easily by adding block function variables to your CustomLabel that trigger on didSet or willSet
class CustomLabel: UILabel {
var onDidSetHidden: ((Bool) -> ())?
var onWillSetHidden: ((Bool) -> ())?
override var hidden: Bool {
didSet {
if let block = onDidSetHidden {
block(self.hidden)
}
}
willSet (willBeHidden) {
if let block = onWillSetHidden {
block(willBeHidden)
}
}
}
}
var custom = CustomLabel()
custom.onDidSetHidden = { (isHidden) in
if isHidden {
println("IS HIDDEN")
} else {
println("IS NOT HIDDEN")
}
}
custom.hidden = true //prints 'IS HIDDEN'