可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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).
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:
回答1:
If you are going to use themes in your app Apple offers the UIApperance
protocol that helps you change controls properties of a certain kind at the same time, using this you'll have an uniform appearance for your UI. The way to use is really simple, to change all UILabel
background color is like this:
UILabel.apperance().backgroundColor = .lightGray
If you want to manage everything in a single place like in your sample code you can create a struct the contains the characteristics for your UI, check this struct (I used the same name you did):
import UIKit
struct GEUserSettings {
enum Theme { case light, dark }
static public var theme: Theme = .light {
didSet {
guard theme != oldValue else { return }
apply()
}
}
static weak var window: UIWindow?
static public func toggleTheme() {
self.theme = theme == .light ? .dark : .light
}
static private func apply() {
setColors()
if let window = window {
window.subviews.forEach({ (view: UIView) in
view.removeFromSuperview()
window.addSubview(view)
})
}
}
static public func setColors() {
switch theme {
case .light:
UILabel.appearance().textColor = .black
UISegmentedControl.appearance().tintColor = .blue
UILabel.appearance(whenContainedInInstancesOf: [UISegmentedControl.self]).backgroundColor = .clear
UITableViewHeaderFooterView.appearance().backgroundColor = .lightGray
UITableView.appearance().backgroundColor = .white
case .dark:
UILabel.appearance().textColor = .red
UISegmentedControl.appearance().tintColor = .purple
UILabel.appearance(whenContainedInInstancesOf: [UISegmentedControl.self]).backgroundColor = .clear
UITableViewHeaderFooterView.appearance().backgroundColor = .black
UITableView.appearance().backgroundColor = .darkGray
}
}
}
In the AppDelegate, or as soon as possible, you should pass the UIWindow
reference to the theme manager struct. I did it in the AppDelegate
didFinishLaunchingWithOptions
. This is necessary in order to make the color changes immediately.
With this struct defined you can customize any UI control as you wish. For example, you may define a certain background color for UILabel
and have a different one if it is contain in a UISegmentedControl
.
The shake event you define can toggle between themes like this:
override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
GEUserSettings.toggleTheme()
}
If you shake the device the screens will toggle between this two (I only changed a few properties):
If you want to play with the sample project is available at Github
Hope I helps!
回答2:
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
})
}
回答3:
typealias Style = StyleManager
//MARK: - Style
final class StyleManager {
static func selectedThem()->Int?
{
return AppUtility?.getObject(forKey: "selectedTheme") as? Int // 1 for dark Theme ...... 2 for light Theme
}
static func BoldFont()->UIFont {
return UIFont(name: FontType.bold.fontName, size: FontType.bold.fontSize)!
}
// MARK: - Style
static func setUpTheme() {
Chameleon.setGlobalThemeUsingPrimaryColor(primaryTheme(), withSecondaryColor: theme(), usingFontName: font(), andContentStyle: content())
}
// MARK: - Theme
static func SetPagerViewsColor()->UIColor
{
return secondarythemeColor
}
static func primaryTheme() -> UIColor {
setCheckMarkBackground()
if selectedThem() == 1
{
return UIColor.white
}
else
{
return OddRowColorlight
}
}
static func theme() -> UIColor {
if selectedThem() == 1
{
EvenRowColor = EvenRowColordark
OddRowColor = OddRowColorlight
primaryThemeColor=EvenRowColor
secondarythemeColor=OddRowColor
return darkGrayThemeColor
}
else
{
EvenRowColor = lightWhiteThemeColor!
OddRowColor = UIColor.white
primaryThemeColor=EvenRowColor
secondarythemeColor=OddRowColor
return lightWhiteThemeColor!
}
// return FlatWhite()
}
static func toolBarTheme() -> UIColor {
if selectedThem() == 1
{
return UIColor.white
}
else
{
return FlatBlack()
}
}
static func tintTheme() -> UIColor {
if selectedThem() == 1
{
return UIColor.white
}
else
{
return FlatBlack()
}
}
static func titleTextTheme() -> UIColor {
if selectedThem() == 1
{
return UIColor.white
}
else
{
return UIColor.white
}
}
static func titleTheme() -> UIColor {
if selectedThem() == 1
{
return darkGrayThemeColor
}
else
{
return FlatWhite()
}
}
static func textTheme() -> UIColor {
if selectedThem() == 1
{
return UIColor.white
}
else
{
return FlatBlack()
}
//return FlatMint()
}
static func backgroudTheme() -> UIColor {
if selectedThem() == 1
{
return .darkGray
}
else
{
return .white
}
}
}
Now Create Some variables at Global scope
var primaryThemeColor:UIColor!
var secondarythemeColor:UIColor!
var themeColor:UIColor!
var toolBarThemeColor:UIColor!
var tintThemeColor:UIColor!
var titleTextThemeColor:UIColor!
var titleThemeColor:UIColor!
var textThemeColor:UIColor!
var backgroundThemeColor:UIColor!
var positiveThemeColor:UIColor!
var negativeThemeColor:UIColor!
var clearThemeColor:UIColor!
var setCheckMarkBackgroundColor:UIColor!
var menuSectioColor:UIColor!
var menuCellColor:UIColor!
var menuBackgroundColor:UIColor!
var menuTextTHeme:UIColor!
var themeName:String!
var btnIconColor:UIColor!
Now in AppDelegate create below function and call this function in didFinish Launch
func setCurrentThemeColors()
{
themeColor = Style.theme()
toolBarThemeColor = Style.toolBarTheme()
tintThemeColor = Style.tintTheme()
titleTextThemeColor = Style.titleTextTheme()
titleThemeColor = Style.titleTheme()
textThemeColor = Style.textTheme()
backgroundThemeColor = Style.backgroudTheme()
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.setCurrentThemeColors()
return true
}
Now you have all set with your theme you just need Create function of theme update in your baseController and override that method in every ViewController Put UI Updating logic in that function and when device shaked call the overrided method like below
override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
print("Shaken!")
updateTheme()
appDelegate.setCurrentThemeColors()
}
func setLightTheme(){
AppUtility?.saveObject(obj: 0 as AnyObject, forKey: "selectedTheme")
}
func setDarkTheme(){
AppUtility?.saveObject(obj: 1 as AnyObject, forKey: "selectedTheme")
}
func updateTheme()
{
let theme = AppUtility?.getObject(forKey: "selectedTheme") as? Int
if theme != nil
{
_ = theme == 1 ? setLightTheme() : setDarkTheme()
}
else
{
setDarkTheme()
}
appDelegate.setCurrentThemeColors()
ConfigureView()
}
func ConfigureView(){
btnDownLoadPdf.backgroundColor = .clear
btnRightSide.backgroundColor = .clear
btnRefreshPage.backgroundColor = .clear
self.View.backgroundColor = secondarythemeColor
PeriodicePastDatesPickerView.backgroundColor = secondarythemeColor
customDatePicker.backgroundColor = secondarythemeColor
UnitPicker.backgroundColor = secondarythemeColor
currencyPicker.backgroundColor = secondarythemeColor
}
Note: You have to Update Colors according to your need it contains some of color that will not be available in your case