0

I have custom classes for labels, buttons, views etc.

Example class:

class PopupButton: UIButton {
    override func awakeFromNib() {
        self.layer.borderWidth = 1.3
        self.layer.borderColor = fadedTextColor.cgColor
        self.layer.cornerRadius = 10
        self.layer.backgroundColor = whiteColor.cgColor
        self.setTitleColor(textHeaderColor, for: .normal)
    }
}

When I change the colors ie.: fadedTextColor I want this PopupButton class to reflect that change immediately.

How can I achieve this?

Thank you for your time.

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
Vetuka
  • 1,523
  • 1
  • 24
  • 40
  • 3
    Not related to your question but you must call `super.awakeFromNib()` when overriding awakeFromNib method – Leo Dabus Jan 10 '18 at 01:38
  • Make a `didSet` for `fadedTextColor`, which updates other things appropriately – Alexander Jan 10 '18 at 01:39
  • Where is fadedTextColor declared? is it a property of your PopupButton? – Leo Dabus Jan 10 '18 at 01:45
  • @LeoDabus fadedTextColor is a global variable. – Vetuka Jan 10 '18 at 01:56
  • 1
    There's an argument for moving thing into either a `struct` or `enum` at https://stackoverflow.com/questions/38585344/swift-constants-struct-or-enum and another about colors (extension or struct) at https://stackoverflow.com/questions/42050283/swift-extension-and-enum-for-color-schemes#42050438 Depending on what you are doing, these may give you a good direction. –  Jan 10 '18 at 01:59
  • What do you mean by global? You shouldn't declare any variable at global scope. You should take a look at Singleton in Apple's AdoptingCocoaDesignPatterns https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-ID177 – Leo Dabus Jan 10 '18 at 02:00
  • @LeoDabus I created a Swift file, and put all my global variables inside. That way I am able to access them throughout the app. Should I put those into struct? Even if I do, how it is going to solve my question? – Vetuka Jan 10 '18 at 02:03
  • 2
    I never said it would solve your issue. You should create a singleton class instead of just dumping your variables inside a file. – Leo Dabus Jan 10 '18 at 02:05
  • That sounds like a job for Key Value Observing. – Alexander Jan 10 '18 at 02:08
  • @Alexander is this mean I need to post notification on change and observe it from custom classes? – Vetuka Jan 10 '18 at 02:10
  • @LeoDabus, I guess I didn't glean where the OP is looking to change it during runtime. If the desire is to catch this change during build time, those links could help. –  Jan 10 '18 at 02:19
  • sc13 Notifications work, but KVO is preferable (cleaner, faster). https://cocoacasts.com/key-value-observing-kvo-and-swift-3 – Alexander Jan 10 '18 at 03:00
  • @sc13 basically, it boils down to makign your buttons call [`addObserver(_:forKeyPath:options:context:)`](https://developer.apple.com/documentation/objectivec/nsobject/1412787-addobserver) to register themselves as observers of a keypath. For the keypath, you'll provide the `fadedTextColor` property of a singleton, much like LeoDabus' `Shared.instance.fadedTextColor` – Alexander Jan 10 '18 at 03:02

3 Answers3

1

First you create a singleton class to hold all your "global" variables. Then add a didSet to the fadedTextColor property to post a notification when its value changes. Next add an observer and a selector at your custom class to change the buttons border color using the color from your singleton class:

class Shared {
    private init() {}
    static let instance = Shared()
    var fadedTextColor: UIColor = .red {
        didSet {
            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "fadedTextColorChanged"), object: nil)
        }
    }
    var textHeaderColor: UIColor = .blue
}

class PopupButton: UIButton {
    override func awakeFromNib() {
        super.awakeFromNib()
        layer.borderWidth = 1.3
        layer.borderColor = Shared.instance.fadedTextColor.cgColor
        layer.cornerRadius = 10
        layer.backgroundColor = UIColor.white.cgColor
        setTitleColor(Shared.instance.textHeaderColor, for: .normal)
        NotificationCenter.default.addObserver(self, selector: #selector(colorChanged), name: NSNotification.Name(rawValue: "fadedTextColorChanged"), object: nil)
    }
    @objc func colorChanged(notification: Notification) {
        layer.borderColor = Shared.instance.fadedTextColor.cgColor
    }
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • I will implement this method and report to you back. Thank you @Leo Dabus. – Vetuka Jan 10 '18 at 02:31
  • it worked, thank you very much! Do you think, should I put all my global variables inside this Shared class? – Vetuka Jan 10 '18 at 22:39
  • I am trying add another instances into Shared class. For example I change the "instance" into "colors". Now I want to add "toggles", "userDefault" etc..How can I achieve this? – Vetuka Jan 14 '18 at 01:38
  • What do you mean by changed the "instance" to colors? You should use only one instance to all properties. `Shared.instance.fadedTextColor = .black` and `Shared.instance.textHeaderColor = .white` – Leo Dabus Jan 14 '18 at 01:41
  • Everything works fine. I just wanted to add and categorize my other variables as well. Lets say I have Bool variables 10ea lets call them Toggles. So, like instance here, I want to create toggles under Shared class? – Vetuka Jan 14 '18 at 01:48
  • You can add as many properties as you would like inside Shared class. If you would like You can create another Singleton but I think it would be easier for you to keep them all together. – Leo Dabus Jan 14 '18 at 01:58
  • I understand I can add as much as properties I want. I meant to use Shared.instance.fadedTextColor = .black - also -> Shared.toggles.userLogged = true. But, when I call Shared.toggles I also access to .fadedTextColor, .textHeaderColor etc. Is there a way to separate those? – Vetuka Jan 14 '18 at 02:06
  • 1
    Well in this case you need to create another singleton. Just call it `Toggles.sharedInstance.whatever` – Leo Dabus Jan 14 '18 at 02:13
  • 1
    I understand. I cannot put them under same singleton Shared. Need to create new singleton. Thank you so much! – Vetuka Jan 14 '18 at 02:15
0

Swift 3

Note: this answer is for Swift 3. My other answer takes advantage of the much nicer API in Swift 4.

I would use Key Value Observation for this purpose. It has the nice property of using using standardized #keyPath Strings to identify changes, rather than Notifications' arbitrary Strings.

The [.initial, .new] options make it so that observeValue(forKeyPath:of:change:context:) is called for you right away (to set the initial value), and after every change (to be notified of new values).

public final class AppConfig: NSObject {
    static let shared = AppConfig()
    override private init() {}

    @objc dynamic var fadedTextColor: UIColor = .red
    @objc dynamic var textHeaderColor: UIColor = .blue
    // ... add other global config state properties here
    // any pro properties you wish to observe must be `@objc` and ` dynamic`
}

class PopupButton: UIButton {
    override func awakeFromNib() {
        let appconfig = AppConfig.shared

        self.layer.borderWidth = 1.3
        
        appconfig.addObserver(self,
                              forKeyPath: #keyPath(AppConfig.fadedTextColor),
                              options: [.initial, .new], context: nil)
        self.layer.cornerRadius = 10
        self.layer.backgroundColor = UIColor.white.cgColor


        appconfig.addObserver(self,
                              forKeyPath: #keyPath(AppConfig.textHeaderColor),
                              options: [.initial, .new], context: nil)



    }

    override func observeValue(
        forKeyPath keyPath: String?,
        of object: Any?,
        change: [NSKeyValueChangeKey : Any]?,
        context: UnsafeMutableRawPointer?) {
        guard let keyPath = keyPath, let newValue = change?[.newKey] else { return }

        if object as? AppConfig == AppConfig.shared {
            switch (keyPath, newValue) {
            case (#keyPath(AppConfig.fadedTextColor), let fadedTextColor as UIColor):
                self.layer.borderColor = fadedTextColor.cgColor

            case (#keyPath(AppConfig.textHeaderColor), let textHeaderColor as UIColor):
                self.setTitleColor(textHeaderColor, for: .normal)

            // Handle other changes here

            default: break
            }
        }
    }
}
Community
  • 1
  • 1
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • I am sure your method also would work flawlessly. However, I implemented Leo's method and works. Since, I am not as good programmer as you are I do not want to play around jeopardize the working state. I do appreciate your time. Thank you. – Vetuka Jan 10 '18 at 22:44
  • @sc13 Not that I really mind which of the two approaches you take, your comment scares me. You are using source control... right? – Alexander Jan 10 '18 at 22:49
  • Could you please elaborate source control? – Vetuka Jan 10 '18 at 23:31
  • @sc13 Version control? Like Git or SVN? Oh boy, this is scaring me – Alexander Jan 10 '18 at 23:44
  • @sc13 It's a piece of software that lets you track changes to source code (or any files, for that matter) over time, with all kinds of benefits. It can serve as a backup as your code, it can help you investigate what version of the software a bug first started in, it makes it easier to collaborate on one software project with multiple people, and most importantly (in my opinion), is that you can make experimental changes safely knowing that a working state exists for you to roll back to at any time. – Alexander Jan 10 '18 at 23:47
  • The App I am building does not have any third party library. Also, it is not connected to GitHub repository. But, it is properly, archived and backups are being made. I will look into version control and its use. Really appreciated! – Vetuka Jan 11 '18 at 00:01
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/162941/discussion-between-alexander-and-sc13). – Alexander Jan 11 '18 at 00:08
  • I do understand version control allows you to clone your GitHub repo, so you can experiment with the App without effecting the repo. I am assuming it also allows you to push the cloned version into GitHub. I see its usage and its feasibility. I see myself using it while building next App. – Vetuka Jan 11 '18 at 00:08
  • I finally integrated my app with GitHub. I wish I did this in the very beginning, I would have save a lot of time. Anyway, liked the GitHub Desktop App which is so easy to use. Thank you! – Vetuka Jan 25 '18 at 11:39
  • @sc13 You don't need to clone it repeatedly. You can just switch which branch/commit you have checked out. Read the atlassian guide I linked in the chat. – Alexander Jan 25 '18 at 16:27
0

Swift 4

We have a really nice, but poorly documented API, in Swift 4, which uses KeyPath for concise, type safe Key-Value Observation. Gone is the need for a massive observeValue(forKeyPath:of:change:context:) that has to handle all the call backs. Instead, each observation takes a closure, which is called only when event pertaining to it take place. This breaks your observeValue(forKeyPath:of:change:context:) method apart very nicely.

For more information, see the What's New in Foundation video from WWDC 2017, starting around 19:30.

public final class AppConfig: NSObject {
    static let shared = AppConfig()
    override private init() {}

    @objc dynamic var fadedTextColor: UIColor = .red
    @objc dynamic var textHeaderColor: UIColor = .blue
    // ... add other global config state properties here
    // any pro properties you wish to observe must be `@objc` and ` dynamic`
}

import UIKit

class PopupButton: UIButton {
    var observers = [NSKeyValueObservation]()

    override func awakeFromNib() {
        let appconfig = AppConfig.shared

        self.layer.borderWidth = 1.3

        observers.append(appconfig.observe(\.fadedTextColor,  options: [.initial, .new]) { object, change in
            self.layer.borderColor = object.fadedTextColor.cgColor
        })
        self.layer.cornerRadius = 10
        self.layer.backgroundColor = UIColor.white.cgColor


        observers.append(appconfig.observe(\.textHeaderColor,  options: [.initial, .new]) { object, change in
            self.setTitleColor(object.textHeaderColor, for: .normal)
        })
    }
}
Community
  • 1
  • 1
Alexander
  • 59,041
  • 12
  • 98
  • 151