16

I am trying to add a theme to my app (a dark theme). So when the user clicks an activity switch it would then make the whole app go into the dark mode. I have hard coded the dark mode just to see what it would look like; however now I would like to be able to enable and disable it through and UISwitch, but I am not sure how to do this?

class DarkModeTableViewCell: UITableViewCell {

var DarkisOn = Bool()
let userDefaults = UserDefaults.standard


@IBOutlet var darkModeSwitchOutlet: UISwitch!

override func awakeFromNib() {
    super.awakeFromNib()


}

override func setSelected(_ selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)

    // Configure the view for the selected state
}


@IBAction func darkModeSwitched(_ sender: Any) {

    if darkModeSwitchOutlet.isOn == true {

        //enable dark mode

        DarkisOn = true

        userDefaults.set(true, forKey: "DarkDefault")
        userDefaults.set(false, forKey: "LightDefault")



    } else {

        //enable light mode
        DarkisOn = false

        userDefaults.set(false, forKey: "DarkDefault")
        userDefaults.set(true, forKey: "LightDefault")
    }

}



}



class DarkModeViewController: UIViewController {



func set(for viewController: UIViewController) {



    viewController.view.backgroundColor = UIColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 1.0)
        viewController.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white]
    viewController.navigationController?.navigationBar.tintColor =     UIColor.white
    viewController.navigationController?.navigationBar.barStyle =     UIBarStyle.black
    viewController.tabBarController?.tabBar.barStyle = UIBarStyle.black






}
static let instance = DarkModeViewController()
}

and then what I do is call the function in each one of the view controllers to see what it looks like, but I need to be able to access the bool value on if the switch is on or off and if it is then have it do that function, otherwise to just keep things the same. If you have any further questions, please let me know, I know some of this might not make to much sense.

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Jaqueline
  • 465
  • 2
  • 8
  • 25

6 Answers6

18

UPDATE: This question (and therefore, this answer) was written before iOS 13 was announced, therefore it does not use iOS 13 specific APIs.


I'd solve this using Notifications (NSNotificationCenter APIs).

The idea is to notify your view controllers in real-time when the dark mode is enabled and when it is disabled, so they can also adapt to the change in real time. You don't need to check the status of the switch or anything like that.

Start by creating two notifications (you can also do it with one only and pass in the desired theme in the userInfo dictionary, but in this case it's easier to create two notifications, since you need to cast and what-not with Swift).

NotificationsName+Extensions.swift:

import Foundation

extension Notification.Name {
    static let darkModeEnabled = Notification.Name("com.yourApp.notifications.darkModeEnabled")
    static let darkModeDisabled = Notification.Name("com.yourApp.notifications.darkModeDisabled")
}

On all your "themable" view controllers, listen to these notifications:

    override func viewDidLoad() {
        super.viewDidLoad()

        // Add Observers
        NotificationCenter.default.addObserver(self, selector: #selector(darkModeEnabled(_:)), name: .darkModeEnabled, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(darkModeDisabled(_:)), name: .darkModeDisabled, object: nil)
    }

Don't forget to remove them in deinit, since sending notifications to invalid objects raises an exception:

deinit {
    NotificationCenter.default.removeObserver(self, name: .darkModeEnabled, object: nil)
    NotificationCenter.default.removeObserver(self, name: .darkModeDisabled, object: nil)
}

In your "themable" view controllers, implement darkModeEnabled(_:) and darkModeDisabled(_:):

@objc private func darkModeEnabled(_ notification: Notification) {
    // Write your dark mode code here
}

@objc private func darkModeDisabled(_ notification: Notification) {
    // Write your non-dark mode code here
}

Finally, toggling your switch will trigger either notification:

@IBAction func darkModeSwitched(_ sender: Any) {

    if darkModeSwitchOutlet.isOn == true {
        userDefaults.set(true, forKey: "darkModeEnabled")

        // Post the notification to let all current view controllers that the app has changed to dark mode, and they should theme themselves to reflect this change.
        NotificationCenter.default.post(name: .darkModeEnabled, object: nil)

    } else {

        userDefaults.set(false, forKey: "darkModeEnabled")

        // Post the notification to let all current view controllers that the app has changed to non-dark mode, and they should theme themselves to reflect this change.
        NotificationCenter.default.post(name: .darkModeDisabled, object: nil)
    }

}

With this, all your view controllers will be notified in real time when the "theme" changes and they will react accordingly. Do note that you need to take measures to show the right mode when the app launches, but I'm sure you are doing that since you are using UserDefaults and presumably checking them. Also worth mentioning NSNotificationCenter is not thread-safe, although it shouldn't matter since this all UI code that should go in the main thread anyway.

For more information, you can check the NSNotificationCenter documentation.

Note: This code is built upon what OP had. It can be simplified (you don't need to keep track of both "light" and "dark" states for example, just one).

Andy Ibanez
  • 12,104
  • 9
  • 65
  • 100
  • Thank you so much for that! It looks like it will work perfectly! I will try this out later when I get a chance and get back to you if I have any more questions. – Jaqueline Nov 26 '17 at 21:30
  • @Jaqueline no problem! If it helps you please don't forget to mark the answer as accepted. Thanks! – Andy Ibanez Nov 26 '17 at 21:31
  • I am sorry, but where do I put the deinit? – Jaqueline Nov 26 '17 at 22:32
  • 1
    It’s a “special” function that doesn’t need the “func” keyword. So at the same scope as all your other methods in a class. – Andy Ibanez Nov 26 '17 at 22:34
  • So I just put it in the "Theamable" controllers? – Jaqueline Nov 26 '17 at 22:35
  • I know I am probably putting it in the wrong place but I just put it inside of the class (Not inside a function or viewDidLoad or anything) and I get an error: Cannot convert value of type '(Notification) -> ()' to expected argument type 'NSNotification.Name?' – Jaqueline Nov 26 '17 at 22:39
  • That does not sound like an issue with deinit. You might have a syntax error someplace else. – Andy Ibanez Nov 26 '17 at 22:40
  • Yes it was, another question: How do I get the UserDefaults to work? I don't think I did it right since it isn't working. I've never worked with UserDefautls before – Jaqueline Nov 26 '17 at 23:28
  • is it also possible that you could update your answer for also updating the dark mode to a table view? I am having a lot of bugs with trying to do it and I can't figure out what is wrong. – Jaqueline Nov 26 '17 at 23:45
  • We are getting too off-topic here. It's better to ask a new question at this point. – Andy Ibanez Nov 27 '17 at 01:05
  • Thank you very much for your help! I've figured out everything with the Table views, it was just a matter of making custom cells. However I am still having trouble with the user defaults, if you could check out my other question that I've made, I would really appreciate it! https://stackoverflow.com/questions/47622052/adding-user-defaults-to-dark-mode – Jaqueline Dec 03 '17 at 19:01
  • Sorry, it did not show up as such until a few seconds ago. I deleted the other comment. – Andy Ibanez Dec 03 '17 at 19:03
  • Hi Andy, I haven't been able to find anyone to answer my question on user defaults, would you mind checking it out. https://stackoverflow.com/questions/47622052/adding-user-defaults-to-dark-mode – Jaqueline Dec 28 '17 at 06:48
  • Wondering if it might be nice to define a protocol for those dark mode methods. – Woodstock Feb 18 '18 at 13:27
  • @Andy Ibanez, for viewcontrollers than instatiate later in app lifecycle, how do you recommend to handle? Best to pass Boolean on instatiation to tell the VC if dark mode is enabled or instead post notification when creating any new VC? – Woodstock Feb 21 '18 at 13:55
  • @Woodstock you can check on `viewDidLoad` itself to see if the dark mode is enabled or not. – Andy Ibanez Feb 21 '18 at 16:53
  • @Andy, yes indeed sir, but I don’t want to animate these changes after view loads of night mode was already applied previously, you know what I mean? – Woodstock Feb 21 '18 at 17:01
  • 2
    @Woodstock `viewDidLoad` gets called before your view is actually visible. The dark mode would be applied before the view is shown to the user. – Andy Ibanez Feb 21 '18 at 17:05
  • @Jaqueline did you ever figure out the deinit problem? – juelizabeth May 27 '18 at 03:52
  • @juelizabeth yes – Jaqueline May 27 '18 at 05:44
  • @Jaqueline how did you fix the issue> – juelizabeth May 27 '18 at 05:45
  • @juelizabeth follow the answer that I marked best. I did some tweaking on it but it couldn’t work better. It also depends on what kind of application you are building wether it uses tableviews or search bars, etc. – Jaqueline May 27 '18 at 05:52
  • @DaniSpringer yes. – Andy Ibanez Oct 23 '18 at 17:28
  • @DaniSpringer it’s not an iOS convention. It’s just do something I do to namespace my notifications since it’s possible other components could post notifications with the same names. It doesn’t matter if you do just instagram or con.example.instagram, as long as you do anything to ensure it’s unique. – Andy Ibanez Oct 23 '18 at 17:38
  • I think you have. I don’t understand your question. – Andy Ibanez Oct 23 '18 at 19:59
  • Never mind then (gonna clear up some comments). By the way, I think `deinit` needs to have periods before "darkModeEnabled", like so: ".dark..." –  Oct 24 '18 at 05:39
  • What is `DarkisOn = true` for? –  Oct 24 '18 at 06:02
  • 1
    That’s just stuff specific to the op’s code. You don’t need it to implement the dark mode changing feature. Just understand the notifications system and UserDefaults and you are good to go. – Andy Ibanez Oct 24 '18 at 06:04
  • It seems like `#selector(darkModeEnabled)` (belonging in `viewDidLoad`) compiles without the `(_:)`. Should I still include it? Or it's indifferent? –  Oct 24 '18 at 06:17
  • Does it make sense that I must check the value in UserDefaults, then call either one of the theming functions, manually, from viewDidLoad? As of now, changes only take place if: app is launched, toggle switched, view "changed" (moved to a different view). So for example: in the view in which the switch is, the changes aren't applied until I leave and come back to that view. Is that expected? –  Oct 24 '18 at 07:14
  • You need to get the current theme from somewhere when you loss a new view controller, otherwise it may not have the right theme applied to it. So yes, reading from UserDefaults is a good idea. – Andy Ibanez Oct 24 '18 at 14:33
  • How do you update the theme without leaving a View Controller? –  Nov 11 '18 at 21:38
  • Just update the colors of your components. For example if the color of a label is supposed to be white, do `myLabel.textColor = .white`. – Andy Ibanez Nov 11 '18 at 22:40
  • Have you tried using this code in a sample app? If not, please do. Thanks. –  Nov 22 '18 at 04:41
  • My theme engine, while not using this exact same code, does use this idea and has `NotificationCenter` at its core. It works like a charm. – Andy Ibanez Nov 22 '18 at 15:18
6

From iOS 13 apple launched dark theme, If you want to add dark theme in your iOS app you can apply following lines of code on viewDidLoad() like:

        if #available(iOS 13.0, *) {
            overrideUserInterfaceStyle = .dark
        } else {
            // Fallback on earlier versions
        }

So you can change theme like there is 2 options light or dark theme. But If you are writing above mentioned code it will take always dark theme only on iOS 13 running devices.

overrideUserInterfaceStyle = .light

Or if your device already running on iOS 13, you can change theme like:

enter image description here

you can even check the current set theme like:

if self.traitCollection.userInterfaceStyle == .dark{
    print("Dark theme")
 }else{
    print("Light theme")
}

Let's see the example:

override func viewDidLoad() {
       super.viewDidLoad()

       if self.traitCollection.userInterfaceStyle == .dark{
           self.view.backgroundColor = UIColor.black
       }else{
            self.view.backgroundColor = UIColor.white
  }

}

Results:

enter image description here

enter image description here

Here is the video for the same: https://youtu.be/_k6YHMFCpas

Mr.Javed Multani
  • 12,549
  • 4
  • 53
  • 52
5

There are basically two ways to theme your App. Way number one: use Apple's UIAppearance proxy. This works very well if your app is very consistent about color usuage across all your views and controls, and not so well if you have a bunch of exceptions. In that case I recommend using a third party pod like SwiftTheme

Josh Homann
  • 15,933
  • 3
  • 30
  • 33
  • Thank you. I will look into this when I am at my computer. And my app stays consistent with colors. Everything is mainly white and black except for the user profile images, etc. – Jaqueline Nov 27 '17 at 01:04
  • I think, this is the perfect answer – Hardik Shah Jun 30 '19 at 17:02
  • Or option 3, just do it yourself with notifications the simplest and safest possible way. 3rd-party libraries should always be a last resort. – trndjc Nov 06 '19 at 02:50
4

Note that this approach has been superseded by Apple globally introducing "dark mode" into (almost) all platforms. The way to go now are "named colors" with appearance variants.

DrMickeyLauer
  • 4,455
  • 3
  • 31
  • 67
3

To support dark mode in iOS 13 and above you can use the unicolor Closure.

    @objc open class DynamicColor : NSObject{
        public   var light :  UIColor
        public   var dark :  UIColor
        public  init(light : UIColor,dark : UIColor) {
            self.light = light
            self.dark = dark
        }
    }
    extension DynamicColor{
      public  func resolve() -> UIColor{
           return UIColor.DynamicResolved(color: self)

        }
    }
    extension UIColor{
       class func DynamicResolved(color: DynamicColor) -> UIColor{
        if #available(iOS 13.0, *) {
            let dynamicColor = UIColor { (traitCollection: UITraitCollection) -> UIColor in
                if traitCollection.userInterfaceStyle == .dark {
                    return color.dark
                } else {
                    return color.light
                }
            }
            return dynamicColor
        } else {
            // Fallback on earlier versions
            return color.light
        }
        }

    }

While using it in view

UIView().backgroundColor =  DynamicColor(light: .white, dark: .black).resolve()
//OR
UIView().backgroundColor =  UIColor.DynamicColor(light: .white, dark: .black)
Manikandan
  • 1,195
  • 8
  • 26
0

In SwiftUI this was made far easier. Simply include the environment variable colorScheme In the view and check it for dark mode like so:

struct DarkModeView: View {
    @Environment(\.colorScheme) var colorScheme: ColorScheme

    var body: some View {
        Text("Hi")
            .foregroundColor(colorScheme == .dark ? .white : .black)
    }
}

There’s a great article on how this all works here

Jake
  • 2,126
  • 1
  • 10
  • 23