江戸一番のジャスタウェイ職人のブログ

江戸一番のジャスタウェイ職人

UIAppearanceのリアルタイム反映は容易ではない

テーマの切り替え機能を実装しようとUIAppearanceについて調べたが、UIAppearanceはUIコンポーネントの属性(色やフォント)の初期値を設定するもので、既に表示が終わったUIコンポーネントの属性を動的に変えてくれるものではなかった。

それを踏まえ調べた限りではみな以下のスニペットで対応しているようだ。

class func refreshAppearance() {
    let windows = UIApplication.sharedApplication().windows as [UIWindow]
    for window in windows {
        let subviews = window.subviews as [UIView]
        for v in subviews {
            v.removeFromSuperview()
            window.addSubview(v)
        }
    }
}

たしかに、これで表示は切り替わるのだがいくつか副作用が存在する。

問題1 ... 重い

まず重い、これが気になった、しかしローディングを表示すればいいという意見もありそうだ。

  1. 解決案1 ... ローディングを表示する => 選びづらさは解決しない
  2. 解決案2 ... プレビューや適用イメージを見せてテーマを選択してもらう => 実装が面倒

問題2 ... viewWillAppear が呼ばれまくる

removeFromSuperview / addSubview すると ViewController の viewWillAppear が呼ばれ、不必要なイベント割当処理などが走ってしまう、テーマ変更中は何もしないとか工夫は可能だがテーマの有無によって本来テーマと関係ないViewControllerの実装が汚染されるのは許し難い。

根本的解決

で、根本的な解決を目指して試したのは「すべてのviewを再帰的に走査し弄る」という「それUIAppearance使う意味あんの?」という解決策だった...

UIAppearanceを使えばそのUIViewが生成された時点でテーマが適応されているので「すべてをCustomView化してイニシャライザでテーマ適応」とか「ViewController側でテーマ適応」とか、とにかくViewやViewController側にテーマの存在による影響を出さないで済む、これだけでもUIAppearanceを使うメリットはある。

class func apply(theme: Theme, refresh: Bool = true) {
    
    // for UIKit
    
    // Note: Adding "View controller-based status bar appearance" to info.plist and setting it to "NO"
    UIApplication.sharedApplication().statusBarStyle = theme.statusBarStyle()
    UITableViewCell.appearance().backgroundColor = theme.mainBackgroundColor()
    UITableView.appearance().backgroundColor = theme.mainBackgroundColor()
    
    // for CustomView
    TextLable.appearance().textColor = theme.bodyTextColor()
    BackgroundView.appearance().backgroundColor = theme.mainBackgroundColor()
    MenuView.appearance().backgroundColor = theme.menuBackgroundColor()
    MenuButton.appearance().setTitleColor(theme.menuTextColor(), forState: .Normal)
    
    if refresh {
        refreshAppearance(theme)
    }
}

class func refreshAppearance(theme: Theme) {
    let windows = UIApplication.sharedApplication().windows as [UIWindow]
    for window in windows {
        refreshWindow(window, theme: theme)
    }
    // Change status bar's background color.
    if let rootView = windows.first?.subviews.first? as? UIView {
        rootView.backgroundColor = theme.mainBackgroundColor()
    }
    windows.first?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
}

class func refreshWindow(window: UIWindow, theme: Theme) {
    // NSLog("+ \(NSStringFromClass(window.dynamicType))")
    for subview in window.subviews as [UIView] {
        refreshView(subview, theme: theme)
    }
}

class func refreshView(view: UIView, theme: Theme, indent: String = "  ") {
    // NSLog("\(indent)- \(NSStringFromClass(view.dynamicType))")
    for subview in view.subviews as [UIView] {
        refreshView(subview, theme: theme, indent: indent + "  ")
        switch subview {
        case let v as UITableViewCell:
            v.backgroundColor = theme.mainBackgroundColor()
        case let v as UITableView:
            v.backgroundColor = theme.mainBackgroundColor()
        case let v as ReplyButton:
            v.setTitleColor(theme.buttonNormal(), forState: .Normal)
            v.setTitleColor(theme.buttonNormal(), forState: .Selected)
        default:
            break
        }
    }
}