Search code examples
iosswiftuiviewcontrollerxcode-storyboard

Trying to reload view controller to update the current theme


What I am looking for

I am trying to reload all the views in my view controller, to change between themes (similar to what Twitter or Apple Maps does).

Twitter Apple Maps


How I have setup my different themes

I have themed views setup like so:

@IBDesignable
extension UIView {

    @IBInspectable
    var lightBackgroundColor: UIColor? {
        set {
            switch GEUserSettings.theme {
            case .light:    backgroundColor = newValue
            case .dark:     break
            }
        }
        get {
            return self.lightBackgroundColor
        }
    }

    @IBInspectable
    var darkBackgroundColor: UIColor? {
        set {
            switch GEUserSettings.theme {
            case .light:    break
            case .dark:     backgroundColor = newValue
            }
        }
        get {
            return self.darkBackgroundColor
        }
    }
}

This allows me in my Main.storyboard to set a light and dark theme background colour, depending on the current theme. My background blur effect is excluded from this, as I couldn't find a way to update the style in code, so it is created in viewDidLoad.


Triggering the theme from shaking the device

However, when I want to change the theme, I'm not sure how to do it. I want to trigger it from shaking the device, like so:

override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
    print("Shaken!")
    let oppositeTheme: GEUserSettings.Theme = {
        switch GEUserSettings.theme {
        case .light:    return .dark
        case .dark:     return .light
        }
    }()

    GEUserSettings.theme = oppositeTheme

    // My attempt to update the view controller to
    // update the theme, which doesn't do anything.
    dismiss(animated: true) {
        UIApplication.shared.keyWindow?.rootViewController?.present(self, animated: true, completion: nil)
        // Yes, the presenting is working, but the views don't change.
    }
}

What are the possible solutions?

The settings take effect if the app is quit and relaunched. I could either force the app to quit (not using exit(0) or anything that counts as a crash), or reload it whilst using the app.

I tried to dismiss and then reload the view controller, as shown in the code above. The one I am reloading is presented on top of the base view controller.

How can I make this work, as I am using storyboards?

Edit - Added an image of my light/dark modes to make my question clearer:

Light/dark modes


Solution

  • I finally figured it out, using NotificationCenter!

    GEUserSettings

    GEUserSettings now looks like the following:

    enum GEUserSettings {
    
        enum Theme: String {
            case light
            case dark
        }
        /// The current theme for the user.
        static var theme: Theme = .dark
        #warning("Store theme in UserDefaults")
    
        /// Toggles the theme.
        static func toggleTheme() {
            switch GEUserSettings.theme {
            case .light:    theme = .dark
            case .dark:     theme = .light
            }
    
            NotificationCenter.default.post(name: Notification.Name("UpdateThemeNotification"), object: nil)
        }
    }
    

    GEView

    GEView is my custom subclass of UIView. This is a replacement instead of my extension to UIView. It now looks similar to this:

    /// UIView subclass to allow creating corners, shadows, and borders in storyboards.
    @IBDesignable
    final class GEView: UIView {
    
        // MARK: - Initializers
        override init(frame: CGRect) {
            super.init(frame: frame)
        }
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            // Triggers when the theme is changed
            NotificationCenter.default.addObserver(self, selector: #selector(updateBackgroundColorNotification), name: Notification.Name("UpdateThemeNotification"), object: nil)
        }
        @objc
        private func updateBackgroundColorNotification() {
            updateBackgroundColor()
        }
    
    
        /* ... */
    
    
        // MARK: - Background
        @IBInspectable
        var lightBackgroundColor: UIColor? {
            didSet {
                updateBackgroundColor()
            }
        }
        @IBInspectable
        var darkBackgroundColor: UIColor? {
            didSet {
                updateBackgroundColor()
            }
        }
    
        /// Updates the background color depending on the theme.
        private func updateBackgroundColor() {
            switch GEUserSettings.theme {
            case .light:    backgroundColor = self.lightBackgroundColor
            case .dark:     backgroundColor = self.darkBackgroundColor
            }
        }
    }
    

    Updating through motionBegan(_:with:)

    override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
        super.motionBegan(motion, with: event)
    
        // Toggles the theme and update the views
        GEUserSettings.toggleTheme()
        drawerViewModel.updateBlurEffect(drawerView: drawerView)
    }
    

    And the blur is removed and recreated, as such:

    /// Creates the blur effect behind the collection view.
    func updateBlurEffect(drawerView: GEView) {
        if let blurView = drawerView.subviews[0] as? UIVisualEffectView {
            blurView.removeFromSuperview()
        }
    
        let blurEffect: UIBlurEffect = {
            switch GEUserSettings.theme {
            case .light:    return UIBlurEffect(style: .light)
            case .dark:     return UIBlurEffect(style: .dark)
            }
        }()
        let blurView = UIVisualEffectView(effect: blurEffect)
    
        drawerView.addSubview(blurView)
        drawerView.sendSubviewToBack(blurView)
        GEConstraints.fillView(with: blurView, for: drawerView)
    }
    

    This doesn't even require quitting the app or reloading the view controller, it happens instantly!

    Extra (animations)

    If you wish, you can also animate the color change by changing the updateBackgroundColor() function:

    /// Updates the background color depending on the theme.
    private func updateBackgroundColor() {
        UIView.animate(withDuration: 0.25) {
            switch GEUserSettings.theme {
            case .light:    self.backgroundColor = self.lightBackgroundColor
            case .dark:     self.backgroundColor = self.darkBackgroundColor
            }
        }
    }
    

    You can also animate the blur as well:

    /// Creates the blur effect behind the collection view.
    func updateBlurEffect(drawerView: GEView) {
        if let blurView = drawerView.subviews[0] as? UIVisualEffectView {
            UIView.animate(withDuration: 0.25, animations: {
                blurView.alpha = 0
            }, completion: { _ in
                blurView.removeFromSuperview()
            })
        }
    
        let blurEffect: UIBlurEffect = {
            switch GEUserSettings.theme {
            case .light:    return UIBlurEffect(style: .light)
            case .dark:     return UIBlurEffect(style: .dark)
            }
        }()
        let blurView = UIVisualEffectView(effect: blurEffect)
        blurView.alpha = 0
    
        drawerView.addSubview(blurView)
        drawerView.sendSubviewToBack(blurView)
        GEConstraints.fillView(with: blurView, for: drawerView)
    
        UIView.animate(withDuration: 0.25, animations: {
            blurView.alpha = 1
        })
    }