I am trying to reload all the views in my view controller, to change between themes (similar to what Twitter
or Apple Maps
does).
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
.
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.
}
}
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:
I finally figured it out, using NotificationCenter
!
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
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
}
}
}
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!
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
})
}