【ios學(xué)習(xí)】夜間模式的實現(xiàn)

隨著越來越多的人晚上用電子設(shè)備,夜間模式變得愈加重要。

夜間模式示范

我們的目標(biāo)是通過簡單辦法給你的UI組件添加主題公罕,并在主題間動態(tài)切換棺聊。為了達(dá)到這個目標(biāo),我們要建立一個協(xié)議淹接,稱為Themed十性,任何參與主題的要符合它。

extension MyView: Themed {

func applyTheme(_ theme: AppTheme) {

backgroundColor = theme.backgroundColor

titleLabel.textColor = theme.textColor

subtitleLabel.textColor = theme.textColor

}

}

extension AppTabBarController: Themed {

func applyTheme(_ theme: AppTheme) {

tabBar.barTintColor = theme.barBackgroundColor

tabBar.tintColor = theme.barForegroundColor

}

}

想象一下應(yīng)用的表現(xiàn)蹈集,來讓我們理出一些基本的需求:

用于存儲和改變當(dāng)前主題的核心地區(qū)

由有標(biāo)簽的顏色定義組成的主題類型

當(dāng)主題改變時候烁试,能夠通知我們應(yīng)用的相應(yīng)機制

讓任何東西都可以參與到主題的簡潔方法

通過自定視圖與視圖控制器改變應(yīng)用的狀態(tài)欄,標(biāo)簽欄拢肆,導(dǎo)航欄

通過精美的淡入淡出動畫來表現(xiàn)主題變化

如果一個應(yīng)用能支持夜間模式减响,顯然它也能支持更多其他模式

帶著這些想法,讓我們?nèi)ラ_始制作我們的主要內(nèi)容吧

定義主題協(xié)議

我們說過需要一些地方存儲當(dāng)前主題郭怪,并能夠訂閱通知來知曉主題是否改變支示。首先我們要定義這句話是什么意思。

/// Describes a type that holds a current `Theme` and allows

/// an object to be notified when the theme is changed.

protocol ThemeProvider {

/// Placeholder for the theme type that the app will actually use

associatedtype Theme

/// The current theme that is active

var currentTheme: Theme { get }

/// Subscribe to be notified when the theme changes. Handler will be

/// removed from subscription when `object` is deallocated.

func subscribeToChanges(_ object: AnyObject, handler: @escaping (Theme) -> Void)

}

ThemeProvider描述了我們通過什么來及時從單點(single point)取得當(dāng)前主題鄙才,還有我們在哪里訂閱關(guān)于主題改變的通知颂鸿。

注意我們把Theme做成了關(guān)聯(lián)類型,這里我們不想定義一個特定的類型攒庵,因為我們希望應(yīng)用能通過任何它們希望的方式表現(xiàn)主題嘴纺。

訂閱機制通過對對象的弱引用運行,當(dāng)對象被釋放時浓冒,它會從訂閱列表出移除栽渴。我們會用這種方法代替Notification和NotificationCenter,因為這樣我們可以用協(xié)議拓展來回避樣本/重復(fù)代碼稳懒,從而避免通知的使用變得更復(fù)雜闲擦。

現(xiàn)在我們定義了處理當(dāng)前主題的地方,我們來看看它是怎么被使用的吧场梆。一旦被實例化/配置墅冷,一個要被themed化的對象就需要知道當(dāng)前的主題,并且如果主題變化還可以通知到它或油。

/// Describes a type that can have a theme applied to it

protocol Themed {

/// A Themed type needs to know about what concrete type the

/// ThemeProvider is. So we don't clash with the protocol,

/// let's call this associated type _ThemeProvider

associatedtype _ThemeProvider: ThemeProvider

/// Will return the current app-wide theme provider

var themeProvider: _ThemeProvider { get }

/// This will be called whenever the current theme changes

func applyTheme(_ theme: _ThemeProvider.Theme)

}

extension Themed where Self: AnyObject {

/// This is to be called once when Self wants to start listening for

/// theme changes. This immediately triggers `applyTheme()` with the

/// current theme.

func setUpTheming() {

applyTheme(themeProvider.currentTheme)

themeProvider.subscribeToChanges(self) { [weak self] newTheme in

self?.applyTheme(newTheme)

}

}

}

如果符合的類型是AnyObject寞忿,就使用一個便利的協(xié)議擴展,我們這樣就避免了每一個一致性都需要做的“應(yīng)用最初主題装哆,訂閱罐脊,當(dāng)主題改變時候再應(yīng)用下一個主題”步驟定嗓。這些都被放入了setUpTheming()方法中,每個對象都可以調(diào)用萍桌。

為了做到這個宵溅,Themed對象需要知道當(dāng)前ThemeProvider是什么。當(dāng)我們知道app的ThemeProvider的具體類型(無論什么類型都會最終符合ThemeProvider)上炎,我們就可以提供在Themed上提供一個擴展來返回應(yīng)用的ThemeProvider恃逻,我們馬上就要做這些。

這些都意味著符合的對象只需要調(diào)用setUpTheming()一次藕施,并提供applyTheme()的一個實現(xiàn)去給它配置這個主題寇损。

App的實現(xiàn)

現(xiàn)在我們已經(jīng)定義了帶主題的API,我們可以用它做點有趣的事情裳食,然后把它應(yīng)用到我們的app上矛市。讓我們定義我們app的主題類型,并聲明我們的白天與夜間主題诲祸。

struct AppTheme {

var statusBarStyle: UIStatusBarStyle

var barBackgroundColor: UIColor

var barForegroundColor: UIColor

var backgroundColor: UIColor

var textColor: UIColor

}

extension AppTheme {

static let light = AppTheme(

statusBarStyle: .`default`,

barBackgroundColor: .white,

barForegroundColor: .black,

backgroundColor: UIColor(white: 0.9, alpha: 1),

textColor: .darkText

)

static let dark = AppTheme(

statusBarStyle: .lightContent,

barBackgroundColor: UIColor(white: 0, alpha: 1),

barForegroundColor: .white,

backgroundColor: UIColor(white: 0.2, alpha: 1),

textColor: .lightText

)

}

這里我們定義我們的AppTheme類型是一個啞結(jié)構(gòu)(dumb struct)浊吏,包含用于設(shè)計我們app的標(biāo)簽化的顏色和值。我們之后為每一個可用的主題聲明一些靜態(tài)特性-對于本文的情況救氯,就是白天和夜間主題找田。

現(xiàn)在是時候建立我們app的ThemeProvider了

final class AppThemeProvider: ThemeProvider {

static let shared: AppThemeProvider = .init()

private var theme: SubscribableValue

var currentTheme: AppTheme {

get {

return theme.value

}

set {

theme.value = newTheme

}

}

init() {

// We'll default to the light theme to start with, but

// this could read directly from UserDefaults to get

// the user's last theme choice.

theme = SubscribableValue(value: .light)

}

func subscribeToChanges(_ object: AnyObject, handler: @escaping (AppTheme) -> Void) {

theme.subscribe(object, using: handler)

}

}

現(xiàn)在我們要面對2件事情:第一,使用一個靜態(tài)共享的單體(singleton)着憨,第二墩衙,SubscribableValue到底是什么

單體?真的甲抖?

我們?yōu)槲覀兊腡hemeProvider建立了一個app范圍共享的單體實例漆改,這通常是個需要警惕的地方。

我們的ThemeProvider很適合單元測試准谚,考慮到這種主題化是表示層上的工作籽懦,這是一個可接受的考慮。

在現(xiàn)實世界氛魁,app的UI是由多屏幕組成,每個都有內(nèi)嵌視圖組成的龐大層級厅篓。為一個視圖模式或視圖控制器使用依賴注入(dependency injection)非常容易秀存,但是為屏幕上的每個視圖進(jìn)行依賴注入會是件大工作,需要很多行代碼去完成羽氮。

總體上說或链,你的商務(wù)邏輯應(yīng)該能進(jìn)行單元測試,你應(yīng)該不需要向下測試到表示層档押。這確實是一個有趣的話題澳盐,以后我們也許會再討論它祈纯。

SubscribableValue

你也許已經(jīng)很好奇SubscribableValue到底是什么!ThemeProvider需要對象去訂閱當(dāng)前主題的改變叼耙。這個邏輯上很簡單腕窥,可以很容易合并到ThemeProvider中,但是訂閱一個數(shù)值的習(xí)慣可以筛婉,也應(yīng)該變得更加通用簇爆。

一個分開的,通用的”可以訂閱的值”的實現(xiàn)爽撒,意味著它可以被孤立的測試和再使用入蛆。它也讓ThemeProvider變得更干凈,即允許它處理只屬于自己的特定職責(zé)硕勿。

當(dāng)然如果你在你的項目中用Rx(或有同樣功能的)哨毁,你可以用一些類似的代替它,比如Variable/BehaviorSubject

SubscribableValue的實現(xiàn)看起來像這樣:

/// A box that allows us to weakly hold on to an object

struct Weak {

weak var value: Object?

}

/// Stores a value of type T, and allows objects to subscribe to

/// be notified with this value is changed.

struct SubscribableValue {

private typealias Subscription = (object: Weak, handler: (T) -> Void)

private var subscriptions: [Subscription] = []

var value: T {

didSet {

for (object, handler) in subscriptions where object.value != nil {

handler(value)

}

}

}

init(value: T) {

self.value = value

}

mutating func subscribe(_ object: AnyObject, using handler: @escaping (T) -> Void) {

subscriptions.append((Weak(value: object), handler))

cleanupSubscriptions()

}

private mutating func cleanupSubscriptions() {

subscriptions = subscriptions.filter({ entry in

return entry.object.value != nil

})

}

}

SubscribableValue含有一個弱對象引用與閉包組成的數(shù)組源武。當(dāng)數(shù)值改變時扼褪,我們在didSet中迭代這些訂閱并調(diào)用閉包。當(dāng)對象被釋放時软能,它還會移除訂閱迎捺。

現(xiàn)在我們有了一個可以用的ThemeProvider,距離一切就緒就差一件事了查排。這就是為Themed添加一個擴展凳枝,用來返回我們app的單一AppThemeProvider實例。

extension Themed where Self: AnyObject {

var themeProvider: AppThemeProvider {

return AppThemeProvider.shared

}

}

如果你還從Themed協(xié)議與擴展中記得它跋核,對象需要這個特性來使用方便的setUpTheming()方法岖瑰,從而管理對ThemeProvider的訂閱。現(xiàn)在它意味著每個Themed對象需要做的事情就是實現(xiàn)applyTheme()砂代。完美蹋订!

獲得Themed

現(xiàn)在我們已經(jīng)準(zhǔn)備好,讓我們的視圖刻伊,視圖控制器和app欄目響應(yīng)主題的變化露戒,讓我們開始一致化吧!

UIView

如果你有一個很好的UIView子類捶箱,想要它響應(yīng)主題變化智什。你要做的就是讓它符合Themed,在init中調(diào)用setUpTheming()丁屎,保證所有主題相關(guān)設(shè)置都在applyTheme()中荠锭。

別忘了在準(zhǔn)備時也調(diào)用applyTheme()一次,這樣你所有的主題代碼就能放在一個適合的地方晨川。

class MyView: UIView {

var label = UILabel()

init() {

super.init(frame: .zero)

setUpTheming()

}

}

extension MyView: Themed {

func applyTheme(_ theme: AppTheme) {

backgroundColor = theme.backgroundColor

label.textColor = theme.textColor

}

}

UIStatusBar 和 UINavigationBar

你可能還想根據(jù)當(dāng)前主題更新app狀態(tài)欄與導(dǎo)航欄的外觀证九。假設(shè)你的app正在使用基于視圖控制器的狀態(tài)欄外觀(這是默認(rèn)設(shè)置)删豺,你可以把導(dǎo)航控制器劃入子類,并使它符合themed愧怜。

class AppNavigationController: UINavigationController {

private var themedStatusBarStyle: UIStatusBarStyle?

override var preferredStatusBarStyle: UIStatusBarStyle {

return themedStatusBarStyle ?? super.preferredStatusBarStyle

}

override func viewDidLoad() {

super.viewDidLoad()

setUpTheming()

}

}

extension AppNavigationController: Themed {

func applyTheme(_ theme: AppTheme) {

themedStatusBarStyle = theme.statusBarStyle

setNeedsStatusBarAppearanceUpdate()

navigationBar.barTintColor = theme.barBackgroundColor

navigationBar.tintColor = theme.barForegroundColor

navigationBar.titleTextAttributes = [

NSAttributedStringKey.foregroundColor: theme.barForegroundColor

]

}

}

類似的對你的UITabViewController子類

class AppTabBarController: UITabBarController {

override func viewDidLoad() {

super.viewDidLoad()

setUpTheming()

}

}

extension AppTabBarController: Themed {

func applyTheme(_ theme: AppTheme) {

tabBar.barTintColor = theme.barBackgroundColor

tabBar.tintColor = theme.barForegroundColor

}

}

現(xiàn)在在你的故事板(storyboard)(或代碼)中呀页,確保你app的標(biāo)簽欄與導(dǎo)航控制器是你新的子類類型。

這樣就可以了叫搁,你app的狀態(tài)與導(dǎo)航欄會響應(yīng)主題變化赔桌,非常巧妙!

隨著每一個組件和視圖都符合Themed渴逻,整個app就會響應(yīng)主題的變化了疾党。

讓主題變化的邏輯與每一個獨立組件緊密耦合,意味著每一部分都可以在自己的范圍內(nèi)做好自己工作惨奕,這樣每部分都做的很好雪位。

循環(huán)主題

我們需要一些功能來在可用的主題間循環(huán),我們可以通過添加下面的代碼來調(diào)整app的ThemeProvider的一些實現(xiàn)

final class AppThemeProvider: ThemeProvider {

// ...

private var availableThemes: [AppTheme] = [.light, .dark]

// ...

func nextTheme() {

guard let nextTheme = availableThemes.rotate() else {

return

}

currentTheme = nextTheme

}

}

extension Array {

/// Move the last element of the array to the beginning

///? - Returns: The element that was moved

mutating func rotate() -> Element? {

guard let lastElement = popLast() else {

return nil

}

insert(lastElement, at: 0)

return lastElement

}

}

我們列出了在ThemeProvider中的可用主題梨撞,并用了一個nextTheme()函數(shù)來讓它們循環(huán)雹洗。

要想實現(xiàn)在一組主題中循環(huán),而不需要一個記錄索引的變量卧波,一個簡單的方法是獲取主題組中的最后一個时肿,并把它移動到開頭。為了在所有數(shù)值間循環(huán)港粱,這個操作可以被重復(fù)進(jìn)行螃成。我們通過延伸主題組并寫一個名為rotate()的mutating方法做到。

現(xiàn)在當(dāng)我們想切換主題時就可以調(diào)用AppThemeProvider.shared.nextTheme()查坪,這樣就會更新了寸宏。

動畫化

我們想潤色一下,為主題改變添加一個同步淡入淡出的動畫偿曙。我們可以在每個applyTheme()方法中把每個屬性變化進(jìn)行動畫化氮凝,但考慮到整個窗口都要改變,使用UIKit來表現(xiàn)整個窗口的快照轉(zhuǎn)換會更加簡潔高效望忆,代碼更少罩阵。

讓我們再次調(diào)整app的ThemeProvider,讓它帶給我們這個功能:

final class AppThemeProvider: ThemeProvider {

// ...

var currentTheme: AppTheme {

// ...

set {

setNewTheme(newValue)

}

}

// ...

private func setNewTheme(_ newTheme: AppTheme) {

let window = UIApplication.shared.delegate!.window!! //

UIView.transition(

with: window,

duration: 0.3,

options: [.transitionCrossDissolve],

animations: {

self.theme.value = newTheme

},

completion: nil

)

}

}

你可以看到启摄,我們把主題數(shù)值的改變包裝到一個UIView同步淡入淡出轉(zhuǎn)換中永脓。所有applyTheme()方法會通過設(shè)定主題的新數(shù)值而被調(diào)用,所有的改變都在轉(zhuǎn)換的動畫區(qū)塊發(fā)生鞋仍。

為了這個操作,我們需要app的窗口搅吁,本例里比起整個app中應(yīng)該存在的數(shù)量威创,實際有著更多強制解包(在一條線中)落午。從現(xiàn)實考慮,這應(yīng)該是完全可以的肚豺。就面對它把溃斋,如果你的app沒有一個委托(delegate)和窗口,你就有更大的問題了-但是在你特定的實現(xiàn)中請隨意調(diào)整這個吸申,讓它變得更保守梗劫。

這樣我們就完成了,一個有效實現(xiàn)的夜間模式和對主題化的深入了解截碴。如果你想試試一個有效的實現(xiàn)梳侨,你可以用示例代碼玩玩。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末日丹,一起剝皮案震驚了整個濱河市走哺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哲虾,老刑警劉巖丙躏,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異束凑,居然都是意外死亡晒旅,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門汪诉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來废恋,“玉大人,你說我怎么就攤上這事摩瞎∷┣” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵旗们,是天一觀的道長蚓哩。 經(jīng)常有香客問我,道長上渴,這世上最難降的妖魔是什么岸梨? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮稠氮,結(jié)果婚禮上曹阔,老公的妹妹穿的比我還像新娘。我一直安慰自己隔披,他們只是感情好赃份,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般抓韩。 火紅的嫁衣襯著肌膚如雪纠永。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天谒拴,我揣著相機與錄音尝江,去河邊找鬼。 笑死英上,一個胖子當(dāng)著我的面吹牛炭序,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播苍日,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼惭聂,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了易遣?” 一聲冷哼從身側(cè)響起彼妻,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎豆茫,沒想到半個月后侨歉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡揩魂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年幽邓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片火脉。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡牵舵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出倦挂,到底是詐尸還是另有隱情畸颅,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布方援,位于F島的核電站没炒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏犯戏。R本人自食惡果不足惜送火,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望先匪。 院中可真熱鬧种吸,春花似錦、人聲如沸呀非。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至猖败,卻和暖如春形耗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辙浑。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拟糕,地道東北人判呕。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像送滞,于是被迫代替她去往敵國和親侠草。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Lua 5.1 參考手冊 by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,740評論 0 38
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理犁嗅,服務(wù)發(fā)現(xiàn)边涕,斷路器,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • Swift2.0 1.defer譯為延緩褂微、推遲之意類似棧 注意作用域功蜓,其次是調(diào)用順序——即一個作用域結(jié)束(注意),...
    zeqinjie閱讀 3,367評論 0 50
  • 當(dāng)你還是一個碼農(nóng)的時候宠蚂,每天都要編寫多少行代碼每次都要創(chuàng)建很多類的時候式撼,每建立一次給你一點經(jīng)驗值,累計到十級的時...
    暗夜精靈_NightElf閱讀 2,983評論 3 14
  • 秋意是涼爽的求厕!一天也是幸福美好的著隆!結(jié)束一天或許有些疲憊,有些幸福呀癣∶榔郑回家的路上滿滿的思念想快點飛奔回家!路上時常會遇...
    小朱繪本館閱讀 274評論 0 0