color theme
Với việc iOS 13 đã có sẵn trên hơn 70% thiết bị iOS, chế độ tối ngày càng được hỗ trợ tốt hơn từ các nhà phát triển iOS mỗi ngày.

Tuy nhiên, đôi khi bạn muốn vượt ra ngoài lược đồ đơn màu và làm cho ứng dụng của bạn sáng sủa hơn với một gam màu sống động.

Cách làm đơn giản nhất

Hãy tưởng tượng bạn phải tạo một ứng dụng mới với chức năng cho thuê xe thương hiệu của bạn. Hiện tại, chỉ có ba mẫu xe có sẵn cho thuê: X, Y và Z. Ứng dụng nên có thể hiển thị thông tin cơ bản về mỗi chiếc xe, số lượng tồn kho và khả năng thuê nó.

UI đó có thể nhìn giống như thế này.
car
Sẽ khá dễ dàng để sửa đổi UI để hỗ trợ chế độ tối, nhưng bây giờ phòng thiết kế đã gửi cho chúng ta một đề xuất cho một tính năng khác - mỗi chiếc xe nên có màu sắc riêng của nó.

Làm thế nào để làm cho ứng dụng có màu sắc động?

Việc triển khai này nên hỗ trợ nhiều chủ đề như phòng thiết kế muốn, cũng như bất kỳ số lượng màn hình và thành phần nào có thể được thay đổi chủ đề do đề xuất của bên thiết kế.
cars
Có một vài cách tiếp cận khả thi để thêm chức năng màu sắc, một số cách sẽ tốt hơn so với những cách khác.

1.ViewController cụ thể cho từng chủ đề?‍

Cách làm này quen thuộc với mọi người khi gặp phải vấn đề như vậy. Đơn giản là, bên trong viewWillAppear có thể gán màu, font chữ, hình ảnh, v.v. cho các yếu tố giao diện bằng cách kiểm tra xem chủ đề nào đã được chọn lần cuối. Lựa chọn chủ đề có thể được lưu trữ trong UserDefaults.

Nhược điểm của phương pháp này là gì? Vâng, vấn đề xảy ra lần đầu tiên khi thêm một phần tử mới, ví dụ: lần sau khi thêm một view controller mới. Không có cách nào để theo dõi mọi phần tử được thêm vào viewWillAppear để đặt màu hoặc font chính xác.

Code sẽ trở nên rối rắm nhanh chóng theo cách này.

Ngoài ra, đôi khi thậm chí nó còn không kết nối UI với code theo cách mà chúng ta mong muốn, (ví dụ: static labels không cần biên dịch hoặc thay đổi trong thời gian chạy). Khi thêm một view controller mới, hãy cân nhắc thêm thời gian cần thiết để triển khai logic cho việc thay đổi màu sắc (code lặp lại).

Tuy nhiên, vấn đề lớn nhất của cách tiếp cận này là việc tải lại chủ đề, điều này sẽ dẫn đến nhiều lỗi. Ví dụ, khi thay đổi chủ đề trên màn hình đang hoạt động hiện tại, nó sẽ không thay đổi mà không tải lại toàn bộ màn hình, điều này sẽ dẫn đến trải nghiệm tệ cho người dùng.

2. Tải lại ứng dụng?‍

Cách làm thứ hai có thể sửa một số vấn đề từ cách làm đầu tiên.

Nếu bạn bỏ qua vấn đề kết nối mọi thành phần của UI và code lặp lại một lát, bạn sẽ có thể tải lại toàn bộ ứng dụng, tức là thay đổi ngăn xếp điều hướng để người dùng luôn ở trang đầu tiên trong ứng dụng mỗi khi chủ đề thay đổi.

Ngay cả khi điều này sửa chữa vấn đề ban đầu với việc tải lại UI, sẽ có một vấn đề mới — trạng thái ứng dụng.

Người dùng sẽ mất phiên đăng nhập hiện tại, cần phải đăng nhập mới và có thể cài đặt lại ứng dụng cũng cần phải được thực hiện lại.
Điều này có thể được sửa bằng cách cố gắng lưu trạng thái ứng dụng cuối cùng được biết đến, nhưng khi ứng dụng trở nên lớn hơn và lớn hơn, điều này sẽ tăng tính phức tạp tuyến tính cho một tính năng nhỏ như vậy. Vì vậy, điều này cũng không thể thực hiện được.

3. Thông báo và đăng ký

Cũng ổn nhưng vẫn chưa đủ.

Bạn có thể tạo một đăng ký cho NotificationCenter trong mỗi Viewcontroller và gửi một thông báo sẽ kích hoạt việc tải lại giao diện người dùng mỗi khi thay đổi chủ đề xảy ra. Vấn đề về việc tải lại và trạng thái ứng dụng được giải quyết trong cách tiếp cận này.

Vẫn còn vấn đề về code lộn xộn, dễ gặp lỗi trong đó một thành phần có thể dễ dàng bị quên.

4. Protocol-oriented theming

Đây là cách chúng ta sẽ làm.

Hai thành phần chính ở đây là UIAppearanceProtocols. Hãy xem làm thế nào hai thứ nhỏ bé nhưng mạnh mẽ này có thể giúp chúng ta triển khai bất kỳ màu sắc nào.

UIAppearance

Như tài liệu chính thức nói, UIAppearance là một tập hợp các phương thức cấp quyền truy cập đến bản proxy xuất hiện cho một lớp. Đó là một protocol trả về một bản proxy chuyển tiếp bất kỳ cấu hình nào đến các phiên bản của một lớp cụ thể.

Một khi cấu hình được nhận, nó được áp dụng khi một lớp được thêm vào cấu trúc thư viện cửa sổ và có thể được áp dụng cho tất cả các phiên bản của một lớp, hoặc các lớp được chứa trong một cấu trúc hệ thống phân cấp nhất định.

UILabel.appearance().textColor = .red
UINavigationBar.appearance().barStyle = blackTranslucent

Có vấn đề là mọi UILabel được chứa trong cấu trúc thư viện của chúng ta đều có màu văn bản màu đỏ. Để giải quyết điều này, sử dụng whenContainedInInstancesOf :

UILabel.appearance(whenContainedInInstancesOf: [CustomViewController.self]).textColor = .red

Bạn có thể sử dụng proxy xuất hiện trên hầu hết các thuộc tính của phần tử, nhưng nếu bạn muốn làm điều gì đó như thế này thì sao:

AppButton.appearance().cornerRadius = 12

Vấn đề bạn sẽ gặp là UIButton không có một thuộc tính được đặt tên là cornerRadius phù hợp với protocol UIAppearance. Hãy sửa chữa điều đó luôn!

Để thêm hỗ trợ đó, tạo thuộc tính tính toán @objc dynamic :

class AppButton: UIButton {

    @objc dynamic var cornerRadius: CGFloat {
        get { return layer.cornerRadius }
        set (newValue) { layer.cornerRadius = newValue }
    }

}

Vậy đó ! Với tính năng của OS mạnh mẽ này, bạn sẽ tiến một bước gần hơn đến một ứng dụng đầy màu sắc.
Trước khi tiếp tục đến phần Protocol, có một điều nhỏ cần xem xét về UIAppearance. Như bạn có thể thấy, không có tiện ích mở rộng nào được tạo trên UIButton mà thay vào đó là lớp con tùy chỉnh AppButton
Ngay cả khi có thể sử dụng trực tiếp UIAppearance trên UIButton, bạn vẫn nên phân lớp mọi phần tử có thể sử dụng theo chủ đề.

Lý do làm điều đó là để sau này bạn có thể sử dụng UIButton làm thành phần có giao diện mặc định, cũng như dễ dàng xem thành phần nào có thể sử dụng được trong code và đảm bảo rằng màu sắc của bạn sẽ không ảnh hưởng đến các thành phần hệ thống khác được iOS sử dụng.
Protocol, enum và người manager
Phần thứ hai của tính năng này chứa một enum đơn giản, hai protocol và một trình quản lý để gắn kết mọi thứ lại với nhau.

  • Enum

Enum có hai trách nhiệm – liệt kê tất cả các chủ đề có sẵn và cung cấp cấu hình cho chủ đề đã chọn.

var appTheme: ThemeProtocol {
    switch self {
        case .red: return RedTheme()
        case .green: return GreenTheme()
        case .blue: return BlueTheme()
    }
}

  • Protocols
    Nói chung, chỉ có một protocol là đủ để nó hoạt động, nhưng hãy tách trách nhiệm thành hai protocol. Main ThemeProtocolThemeable làm protocol trợ giúp.
    ThemeProtocol được sử dụng làm protocol chính chịu trách nhiệm xác định hành vi của nội dung chính và tiện ích mở rộng:
protocol ThemeProtocol {
    var assets: Themeable { get }
    var `extension`: (() -> Void)? { get }
}

protocol này được sử dụng sau này trong thành phần cuối cùng - manager. Nhưng bên trong nó, chúng ta có thể thấy một protocol Themeable khác chịu trách nhiệm mô tả sản phẩm trông như thế nào.
Nếu có một theme phù hợp với Themeable và bạn chỉ muốn mở rộng chức năng cho một chủ đề đó, hãy sử dụng block extension để chỉ mở rộng các tính năng cho một chủ đề đó.
Themeable là protocol chính trong trường hợp này vì mô tả các mục được định nghĩa trong đó.

protocol Themeable {
    var labelAssets: LabelAssets { get }
    var buttonAssets: ButtonAssets { get }
    var switchAssets: SwitchAssets { get }
    // ...
}

Ví dụ, mô tả của một mục có thể trông như sau:

struct LabelAssets {
    var textColor: UIColor { get }
}
  • Manager

Mảnh ghép cuối cùng ở đây là ThemeManager, nó liên kết tất cả mọi thứ lại với nhau. Tất cả logic cần thiết để áp dụng theme được chọn có thể được viết trong chỉ vài code:

struct ThemeManager {

    static func apply(_ theme: Theme, application: UIApplication) {
        // 1
        let appTheme = theme.appTheme
        // 2
        updateLabel(using: appTheme.assets.labelAssets)
        // 3
        appTheme.extension?()
        // 4
        application.keyWindow?.reload()
    }
}

Hãy đi qua từng dòng mã đó một cách từng bước, để làm cho mọi thứ rõ ràng hơn. Ở //1, dòng đầu tiên, bạn chỉ đang truy xuất mô tả các mục cho theme đã chọn.
Dòng thứ hai ở //2 là trách nhiệm để áp dụng theme đó bằng cách sử dụng giao thức UIAppearance mô tả trong phần đầu tiên. Điều này chỉ đặt giá trị mới cho nhãn của chúng ta, nhưng nó sẽ không thay đổi nó ngay lập tức. Việc triển khai của updateLabel cũng đơn giản như vậy:

func updateLabel(using themeAssets: LabelAssets) {
    AppLabel.appearance().textColor = themeAssets.textColor
}

Ở dòng tiếp theo //3 áp dụng một số thay đổi bổ sung cụ thể cho theme mà thực hiện extension.
Hãy giả sử rằng Blue theme đang thực hiện một số câu lệnh mà Red theme và Green theme không làm (ở đây chỉ hiển thị triển khai của một Blue theme, nhưng cùng một nguyên tắc cũng được áp dụng cho các theme khác):

class BlueTheme: ThemeProtocol {

    var assets: Themeable {
        return ThemeAssets(
            labelAssets: AppLabelAssets(
                color: UIColor.blue.primary,
                font: .systemFont(ofSize: 18)
            ),
            buttonAssets: AppButtonAssets(
                normalBackgroundColor: UIColor.blue.primary,
                selectedBackgroundColor: UIColor.blue.secondary,
                disabledBackgroundColor: UIColor.blue.tertiary
            ),
            switchAssets: AppSwitchAssets(
                isOnColor: UIColor.blue.primary,
                isOnDefault: true
            ),
            cellAssets: AppTableViewCellAssets(selectedColor: UIColor.blue.tertiary),
            segmentedControlAssets: AppSegmentControllAssets(activeColor: UIColor.blue.primary),
            pinAssets: AppAnnotationViewAssets(color: UIColor.blue.primary)
        )
    }

    override var `extension`: (() -> Void)? {
        return {
            let proxy = AppButton.appearance(whenContainedInInstancesOf: [AppView.self])
            proxy.cornerRadius = 12.0
            proxy.setBackgroundColor(color: blue, forState: .normal)
        }
    }
}

Bước cuối cùng //4 là buộc UI áp dụng các giá trị này cho tất cả các thành phần và cách dễ nhất là chỉ cần tải lại tất cả mọi thứ trong một cấu trúc cửa sổ:

// application.keyWindow?.reload()

// Implementation
public extension UIWindow {

    /// Unload all views and add them back
    /// Used for applying `UIAppearance` changes to existing views
    func reload() {
        subviews.forEach { view in
            view.removeFromSuperview()
            addSubview(view)
        }
    }   
}

Khi chạy thử, phương pháp này không gây ảnh hưởng lớn đến CPU/GPU và hoạt động tốt với bố cục tự động (tất cả các ràng buộc đều được giữ nguyên).

Áp dụng chủ đề

Để thay đổi chủ đề hãy gọi 1 dòng code duy nhất và điều kì diệu sẽ diễn ra.

ThemeManager.apply(.red)

Theme màu đỏ sẽ được áp dụng trên tất cả các màn hình trong ứng dụng, mà không cần thêm bất kỳ loại mã nào để xử lý theme bên trong bất kỳ view controller nào hoặc kết nối giao diện người dùng của bạn với mã.
Bây giờ, nếu bạn đặt nhãn của mình là AppLabel, ngay cả khi thêm từ Storyboard, một màu mới sẽ luôn được áp dụng đúng cách.

Hãy làm cho ứng dụng của bạn sinh động

Đó là bốn cách khác nhau để triển khai tùy chỉnh ứng dụng và làm cho ứng dụng trở nên đầy màu sắc động. Để công việc của bạn dễ dàng hơn, tránh sử dụng hai phương pháp đầu tiên và làm cho ứng dụng trở nên đa màu sắc với hai phương pháp còn lại.
Sử dụng sức mạnh của UIAppearance của Apple để thiết lập giao diện mới cho một phần tử kết hợp với các Protocols để mô tả giao diện đó và áp dụng nó. Bằng cách này, bạn chỉ cần tạo các mục có thể tùy chỉnh một lần duy nhất.
Bằng cách sử dụng chúng bất cứ đâu trong ứng dụng, bạn có hỗ trợ để thay đổi theme mà không cần thêm bất kỳ logic bổ sung nào để thay đổi giao diện khi theme thay đổi.
Bây giờ khi bạn đã hiểu rõ, ứng dụng của bạn cuối cùng có thể thể hiện màu sắc thực sự của nó.
nguồn tham khảo : Implementing Dynamic Color Themes in Your iOS App