Implement variable attribute property observer in Swift - 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'

Related

RxSwift - make one UI element hidden/not hidden according to other element

Im using RxSwift and RxCocoa in my project.
I have some UITextField named "lastNameTF", and there is a UILabel name "lastNameTitle".
I wanna know if there is any way to set the isHidden value of lastNameTitle always be equal to isHidden value of lastNameTF using RxSwift.
I believe you can use KVO as described here -
https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#kvo
It is super easy to use KVO. Here is an example of exactly what you are trying to do, just without using RxSwift (don't know what that is...)
Here is the gist of it
class ViewController: UIViewController {
private var lastNameTextFieldHiddenContext = 0
private var lastNameObservingView:UIView? = nil
#IBOutlet weak var lastNameLabel: UILabel!
#IBOutlet weak var lastNameTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// add the observer
lastNameTextField.addObserver(
self,
forKeyPath: "hidden",
options: [.new],
context: &self.lastNameTextFieldHiddenContext
)
}
/// function will be called whenever an added observer is triggered
override func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?
) {
// make sure it is our text field isHidden observer
if context == &self.lastNameTextFieldHiddenContext {
// get the new value that was set
if let newValue = change?[NSKeyValueChangeKey.newKey] as? Bool {
// do what needs to be done when the observer is triggered
self.lastNameLabel.isHidden = newValue
}
}
}
deinit {
// remove the observer
if let view = self.lastNameObservingView {
view.removeObserver(self, forKeyPath: "hidden")
self.lastNameObservingView = nil
}
}
#IBAction func showHideButtonAction(_ sender: Any) {
self.lastNameTextField.isHidden = !self.lastNameTextField.isHidden
}
}
If you still need a simple RxSwift approach please try this:
// Controls are visible by default (isHidden = false)
let isControlHidden = BehaviorRelay<Bool>(value: false)
override func viewDidLoad() {
super.viewDidLoad()
let isHiddenDriver = self.isControlHidden.asDriver()
isHiddenDriver
.drive(self.lastNameTitle.rx.isHidden)
.disposed(by: disposeBag)
isHiddenDriver
.drive(self.lastNameTF.rx.isHidden)
.disposed(by: disposeBag)
}
Since you need both control visibilities bound to each other, you can use a Subject or Relay to achieve that, in this case isControlHidden. So, if you want to show/hide the, you just emit a new signal:
#IBAction func hide(_ sender: Any) {
self.isControlHidden.accept(true)
}
#IBAction func show(_ sender: Any) {
self.isControlHidden.accept(false)
}

Once set up, how are NSSpeechSynthesizerDelegate protocol methods called automatically?

I'm working through a tutorial that uses NSSpeechSynthesizer and two of its NSSpeechSynthesizerDelegate protocol methods. In my ViewController, I don't explicitly call the protocol methods so I'm curious as to what do I need to research in order to understand how these methods are called during runtime? The delegate methods are working as expected but I'm wondering how are they being called which makes this possible?
import Cocoa
class MainWindowController: NSWindowController, NSSpeechSynthesizerDelegate, NSWindowDelegate {
//Now MainWindowController is more powerful by having its own KITT being able to delegate powerful functionality and do less work. The delegate will do all the heavy lifting and return the results to MainWindowController instances.
// MARK: - Properties
#IBOutlet weak var textField: NSTextField!
#IBOutlet weak var speakButton: NSButton!
#IBOutlet weak var stopButton: NSButton!
let speechSynth = NSSpeechSynthesizer.init(voice: NSSpeechSynthesizer.VoiceName.init(rawValue: "com.apple.speech.synthesis.voice.Victoria"))
var isSpeaking: Bool = false {
didSet {
updateButtons()
}
}
// MARK: - Overriden Properties
override var windowNibName: NSNib.Name? {
return NSNib.Name("MainWindowController")
}
// MARK: - Overidden Methods
override func windowDidLoad() {
super.windowDidLoad()
updateButtons()
speechSynth?.delegate = self
}
// MARK: - UI methods
#IBAction func speakIt(sender: NSButton) {
//Get tuype-in text as a string
let string = textField.stringValue
if string.isEmpty {
print("string from \(textField) is empty")
} else {
speechSynth?.startSpeaking(string)
isSpeaking = true
}
}
#IBAction func stopIt(sender: NSButton) {
speechSynth?.stopSpeaking()
}
func updateButtons(){
if isSpeaking {
speakButton.isEnabled = false
stopButton.isEnabled = true
} else {
speakButton.isEnabled = true
stopButton.isEnabled = false
}
}
// MARK: - NSSpeechSynthesizerDelegate Methods
//this functionality is considered more powerful and is made possible due to the speechSynthesizer.delegate = self
//the delegate is doing the work and reporting that completed work to the MainWindowController instance
//so kinda like the delegate is providing the signature and its up to us as the developers based on what we do with those parameters inside the function in order for us to add our own creativity.
func speechSynthesizer(_ sender: NSSpeechSynthesizer, didFinishSpeaking finishedSpeaking: Bool) {
//by setting this variable to FALSE, it will fire off the didSet computed property which this variable has both storage and behavior.
isSpeaking = false
}
// MARK: - NSWindowDelegate Methods
func windowShouldClose(_ sender: NSWindow) -> Bool {
return !isSpeaking
}
}
Your windowDidLoad method contains this line:
speechSynth?.delegate = self
This means the speech synthesizer object has a reference back to your MainWindowController, so the speech synthesizer object can send messages to your MainWindowController.
A simplified implementation inside NSSpeechSynthesizer could look something like this in Swift:
class NSSpeechSynthesizer: NSSoundDelegate {
weak var delegate: NSSpeechSynthesizerDelegate?
func startSpeaking(_ string: String) {
guard
let audioData = audioData(for: string),
let sound = NSSound(data: audioData)
else { return }
sound.delegate = self
sound.play()
}
// Part of NSSoundDelegate
func sound(_ sound: NSSound, didFinishPlaying finished: Bool) {
// The first ? means Swift only sends the message if
// delegate is not nil.
// The second ? means Swift only sends the message if delegate
// implements speechSynthesizer(_:didFinishSpeaking:).
delegate?.speechSynthesizer?(self, didFinishSpeaking: finished)
}
}
But it's actually implemented in Objective-C, where you have to be more verbose about checking whether the delegate handles the message:
- (void)sound:(NSSound *)sound didFinishPlaying:(BOOL)finished {
if ([delegate respondsToSelector:#selector(speechSynthesizer:didFinishSpeaking:)]) {
[delegate speechSynthesizer:self didFinishSpeaking:finished];
}
}

Simple MVVM with KVO

I'm trying to create a simple mvvm model using kvo
My goal is when the UITextField text changes, automatically change the UILabel text.
But for some reason the observeValue function is not called
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
var viewModel : TestViewModel?
#IBOutlet weak var LBLABEL: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = TestViewModel()
addObserver(self, forKeyPath: #keyPath(viewModel.infoText), options: [.old, .new], context: nil)
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
viewModel?.infoText = textField.text
return true
}
// MARK: - Key-Value Observing
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "info" {
// Update Time Label
LBLABEL.text = viewModel?.infoText
}
}
}
class TestViewModel : NSObject{
var model : TestModel
var infoText:String? {
didSet{
model.info = self.infoText
}
}
override init() {
model = TestModel()
}
}
class TestModel {
var info:String?
}
I already tried to change the observer's declaration or even the ViewModel gets and sets and never succeeded
UPDATE
According to Apple docs, creating an observer for the key path will be much simpler in Swift 4.
class MyObserver: NSObject {
#objc var objectToObserve: MyObjectToObserve
var observation: NSKeyValueObservation?
init(object: MyObjectToObserve) {
objectToObserve = object
super.init()
observation = observe(\.objectToObserve.myDate) { object, change in
print("Observed a change to \(object.objectToObserve).myDate, updated to: \(object.objectToObserve.myDate)")
}
}
}
let observed = MyObjectToObserve()
let observer = MyObserver(object: observed)
observed.updateDate()
You need to add dynamic to the properties of NSObject subclass you want to observe. In your case:
#objc dynamic var infoText:String? {
didSet{
model.info = self.infoText
}
}
Btw, I don't know why you want to you textField:shouldChangeCharactersIn as it gets the textfield value before updated. Also, keyPath == "info" won't never be true. Shouldn't it be something else. E.g., keyPath == "viewModel.infoText"?

Custom UIControl subclass with RxSwift

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
}
}
}

Is it a way to observe global variable in Swift?

I want to add an observer to check changes of a global variable, but it not seems possible.
var selectedItem: NSManagedObject?
class LCCC: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
// need to set up an "observer" that trigger selectedItemDidChange method, if selectedItem changed
}
func selectedItemDidChange {
}
}
I think the most idiomatic way to do this in swift would be using property observers:
var test:NSString = "hi" {
willSet {
}
didSet {
}
}
(Although you need to provide an initializer)
Have you tried something like:
NSNotificationCenter.defaultCenter().addObserver(selectedItem, selector: Selector("selectedItemDidChange"), name: "selectedItemObserver", object: self)