{"id":28511710,"url":"https://github.com/yalantis/appearancenavigationcontroller","last_synced_at":"2026-03-14T20:50:12.479Z","repository":{"id":46606094,"uuid":"45475417","full_name":"Yalantis/AppearanceNavigationController","owner":"Yalantis","description":"Example with advanced configuration of the navigation controller's appearance","archived":false,"fork":false,"pushed_at":"2020-09-16T13:04:14.000Z","size":79,"stargazers_count":100,"open_issues_count":1,"forks_count":6,"subscribers_count":30,"default_branch":"develop","last_synced_at":"2025-06-09T00:07:49.782Z","etag":null,"topics":["behaviour","ios","navigation","navigation-controller","swift","toolbar","uikit","yalantis"],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Yalantis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/contributing.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-11-03T15:22:49.000Z","updated_at":"2025-05-25T19:06:19.000Z","dependencies_parsed_at":"2022-08-27T06:51:57.759Z","dependency_job_id":null,"html_url":"https://github.com/Yalantis/AppearanceNavigationController","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Yalantis/AppearanceNavigationController","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Yalantis%2FAppearanceNavigationController","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Yalantis%2FAppearanceNavigationController/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Yalantis%2FAppearanceNavigationController/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Yalantis%2FAppearanceNavigationController/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Yalantis","download_url":"https://codeload.github.com/Yalantis/AppearanceNavigationController/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Yalantis%2FAppearanceNavigationController/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260224201,"owners_count":22977361,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["behaviour","ios","navigation","navigation-controller","swift","toolbar","uikit","yalantis"],"created_at":"2025-06-09T00:07:52.799Z","updated_at":"2025-09-20T21:38:42.059Z","avatar_url":"https://github.com/Yalantis.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AppearanceNavigationController\nSample of navigation controller appearance configuration from our [blog post](https://yalantis.com/blog/declarative-navigation-bar-appearance-configuration/).\n\n\n#### Declarative Navigation Bar Appearance Configuration\n\nThe last couple of month I’ve been working on an app that required implementation of color changing behavior. \n\nIn this app, all users can choose a color for their profile, and the color of the “User Details” navigation bar depends on the avatar. This created a lot of issues in the app development including the need to make a status bar readable on the navigation bar’s background. What’s more, a tool bar in the app can only be visible for users’ friends. \n\nHow do you implement color changing features?\n\nA naive approach was to perform the navigation controller appearance configuration in the `UIViewController.viewWillAppear:`\n\nBut with this implementation I shortly found my view controllers full of unnecessary and even odd knowledge, such as navigation bar’s background image names, toolbar’s tint color alpha, and so on. Moreover, this logic was duplicated in several independent classes. \n\nGiven this nuicance I decided to refactor `UIViewController.viewWillAppear` and turn it into a small and handy tool that could free view controllers from repeated imperative appearance configurations. I wanted my implementation to have a UIKit-like declarative style as `UIViewController.preferredStatusBarStyle`. Under this logic, view controllers will be asked the number of questions or requirements that tell them how to behave. \n\n#### The principle of work of AppearanceNavigationController \n\nTo achieve a declarative behaviour we need to become a UINavigationControllerDelegate to handle push and pop between the view controllers.\n\nIn the `navigationController(_:willShowViewController:animated:)` we need to ask view controller that is being shown the following questions: \n\n- Do we need to display a navigation bar?\n- What color should it be?\n- Do we need to display a toolbar?\n- What color should it be?\n- What style for the status bar is preferred?\n\nLet’s turn it into a Swift’s struct: \n\n```swift\npublic struct Appearance: Equatable {\n    \n    public struct Bar: Equatable {\n       \n        var style: UIBarStyle = .default\n        var backgroundColor = UIColor(red: 234 / 255, green: 46 / 255, blue: 73 / 255, alpha: 1)\n        var tintColor = UIColor.white\n        var barTintColor: UIColor?\n    }\n    \n    var statusBarStyle: UIStatusBarStyle = .default\n    var navigationBar = Bar()\n    var toolbar = Bar()\n}\n```\nYou may have noticed that the flags of the navigation bar and toolbar visibility are missing, and here is why: most of the time I didn’t care about the bars appearance. All I needed is to hide them. Therefore, I decided to keep them separate.\nAs you remember, we’re going to “ask” our view controller about the preferred apperance. Let’s implement this as follows:\n\n```swift\npublic protocol NavigationControllerAppearanceContext: class {\n    \n    func prefersBarHidden(for navigationController: UINavigationController) -\u003e Bool\n    func prefersToolbarHidden(for navigationController: UINavigationController) -\u003e Bool\n    func preferredAppearance(for navigationController: UINavigationController) -\u003e Appearance?\n}\n```\n\nSince not every `UIViewController` will configure appearance, we’re not going to extend `UIViewController` with `AppearanceNavigationControllerContext`. Instead, let’s provide a default implementation using protocol extension introduced in Swift 2.0 so that anyone that confroms to the `NavigationControllerAppearanceContext` can implement only the methods they are interested in:\n\n```swift\nextension NavigationControllerAppearanceContext {\n    \n    func prefersBarHidden(for navigationController: UINavigationController) -\u003e Bool {\n        return false\n    }\n    \n    func prefersToolbarHidden(for navigationController: UINavigationController) -\u003e Bool {\n        return true\n    }\n    \n    func preferredAppearance(for navigationController: UINavigationController) -\u003e Appearance? {\n        return nil\n    }\n}\n```\n\nAs you may have noticed `preferredNavigationControllerAppearance` allows us to return `nil` which is useful to interpret as “Ok, this controller doesn’t want to affect the current appearance”. \n\nNow let’s implement the basics of our Navigation Controller:\n```swift\npublic class AppearanceNavigationController: UINavigationController, UINavigationControllerDelegate {\n\n    public required init?(coder decoder: NSCoder) {\n        super.init(coder: decoder)\n        \n        delegate = self\n    }\n    \n    override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {\n        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)\n\n        delegate = self\n    }\n\n    override public init(rootViewController: UIViewController) {\n        super.init(rootViewController: rootViewController)\n\n        delegate = self\n    }\n    \n    // MARK: - UINavigationControllerDelegate\n    \n    public func navigationController(\n        _ navigationController: UINavigationController,\n        willShow viewController: UIViewController, animated: Bool\n    ) {\n        guard let appearanceContext = viewController as? NavigationControllerAppearanceContext else {\n            return\n        }\n        setNavigationBarHidden(appearanceContext.prefersBarHidden(for: self), animated: animated)\n        setNavigationBarHidden(appearanceContext.prefersBarHidden(for: self), animated: animated)\n        setToolbarHidden(appearanceContext.prefersToolbarHidden(for: self), animated: animated)\n        applyAppearance(appearance: appearanceContext.preferredAppearance(for: self), animated: animated)\n    }\n\n    // mark: - Appearance Applying\n        \n    private func applyAppearance(appearance: Appearance?, animated: Bool) {        \n        // apply\n    }\n}\n```\n\n#### Appearance Configuration\n\nNow it’s time to implement the appearance applying the details: \n\n```swift\nprivate func applyAppearance(appearance: Appearance?, animated: Bool) {\n    if let appearance = appearance {\n        if !navigationBarHidden {\n            let background = ImageRenderer.renderImageOfColor(color: appearance.navigationBar.backgroundColor)\n            navigationBar.setBackgroundImage(background, for: .default)\n            navigationBar.tintColor = appearance.navigationBar.tintColor\n            navigationBar.barTintColor = appearance.navigationBar.barTintColor\n            navigationBar.titleTextAttributes = [\n                NSAttributedString.Key.foregroundColor: appearance.navigationBar.tintColor\n            ]\n        }\n\n        if !toolbarHidden {\n            toolbar?.setBackgroundImage(\n                ImageRenderer.renderImageOfColor(color: appearance.toolbar.backgroundColor),\n                forToolbarPosition: .any,\n                barMetrics: .default\n            )\n            toolbar?.tintColor = appearance.toolbar.tintColor\n            toolbar?.barTintColor = appearance.toolbar.barTintColor\n        }\n    }\n}\n```\nIf the View Controller’s appearance isn’t nil, we need to apply the apprearance differently – just ignore it. Code, that applies the appearance is fairly simple, except `ImageRenderer.renderImageOfColor(color)` which returns a colored image with 1x1 pixel size.\n\n#### Status bar configuration\n\nNote, that status bar style comes in pair with the `Appearance` and not via `UIViewController.preferredStatusBarStyle()`. This is because a status bar visibility depends on the navigation bar color brightness, so I decided to keep this “knowledge” about colors in a single place instead of putting it in two separate places.\n\n```swift\nprivate var appliedAppearance: Appearance?\n\nprivate func applyAppearance(appearance: Appearance?, animated: Bool) {\n    if let appearance = appearance where appliedAppearance != appearance {\n        appliedAppearance = appearance\n\n        // rest of the code\n\n        setNeedsStatusBarAppearanceUpdate()\n    }\n}\n\n// mark: - Status Bar \n\npublic override var preferredStatusBarStyle: UIStatusBarStyle {\n    appliedAppearance?.statusBarStyle ?? super.preferredStatusBarStyle\n}\n\npublic override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {\n    appliedAppearance != nil ? .fade : super.preferredStatusBarUpdateAnimation\n}\n```\nSince we’re going to use UIKit’s preferred way of Status Bar appearance change, the applied `Appearance` needs to be preserved. Also, if there is no appearance applied we’re switching to the default super’s implementation. \n\n#### Appearance Update\n\nObvisouly, the view controller appearance may change during its lifecycle. In order to update the view controller let’s add a UIKit-like method `NavigationControllerAppearanceContext`:\n\n```swift\npublic protocol NavigationControllerAppearanceContext: class {    \n    // rest of the interface\n\n    func setNeedsUpdateNavigationControllerAppearance()\n}\n\nextension NavigationControllerAppearanceContext {\n    // rest of the defaul implementation\n\n    func setNeedsUpdateNavigationControllerAppearance() {\n        if let viewController = self as? UIViewController,\n            let navigationController = viewController.navigationController as? AppearanceNavigationController {\n            navigationController.updateAppearance(for: viewController)\n        }\n    }\n}\n\n```\nAnd a corresponding implementation in the `AppearanceNavigationController`:\n```swift\nfunc updateAppearance(for viewController: UIViewController) {\n    if let context = viewController as? NavigationControllerAppearanceContext,\n        viewController == topViewController \u0026\u0026 transitionCoordinator == nil {\n        setNavigationBarHidden(context.prefersBarHidden(for: self), animated: true)\n        setToolbarHidden(context.prefersToolbarHidden(for: self), animated: true)\n        applyAppearance(appearance: context.preferredAppearance(for: self), animated: true)\n    }\n}\n\npublic func updateAppearance() {\n    if let top = topViewController {\n        updateAppearance(for: top)\n    }\n}\n```\nFrom this point any `AppearanceNavigationControllerContext` can ask its container to re-run the appearance configuration in case something gets changed (editing mode, for example). By various checks like `viewController == topViewController` and `transitionCoordinator() == nil` we’re disallowing appearance change invoked by an invisible view controller or happened during the interactive pop gesture.\n\n#### Usage\nWe’re done with the implementation. Now any view controller can define an appearance context, change appearance in the middle of the lifecycle and so on:\n```swift\nclass ContentViewController: UIViewController, NavigationControllerAppearanceContext {\n    \n    required init?(coder aDecoder: NSCoder) {\n        super.init(coder: aDecoder)\n        \n        navigationItem.rightBarButtonItem = editButtonItem\n    }\n    \n    var appearance: Appearance? {\n        didSet {\n            setNeedsUpdateNavigationControllerAppearance()\n        }\n    }\n    \n    // mark: - Actions\n    \n    override func setEditing(_ editing: Bool, animated: Bool) {\n        super.setEditing(editing, animated: animated)\n        \n        setNeedsUpdateNavigationControllerAppearance()\n    }\n    \n    // mark: - AppearanceNavigationControllerContent\n\n    func prefersToolbarHidden(for navigationController: UINavigationController) -\u003e Bool {\n        // hide toolbar during editing\n        return isEditing\n    }\n    \n    func preferredAppearance(for navigationController: UINavigationController) -\u003e Appearance? {\n        // inverse navigation bar color and status bar during editing\n        return isEditing ? appearance?.inverse() : appearance\n    }\n}\n```\n#### Gathering the appearance together\n\nNow we can gather all the appearance configurations as a category with common configurations, thus eliminating code duplication as in the naive solution:\n```swift\nextension Appearance {\n    \n    static let lightAppearance: Appearance = {\n        var value = Appearance()\n        \n        value.navigationBar.backgroundColor = .lightGray\n        value.navigationBar.tintColor = .white\n        value.statusBarStyle = .lightContent\n        \n        return value\n    }()\n}\n```\n\n#### Customizing the appearance \n\nTo make the animation more customizeable let’s wrap it into the `AppearanceApplyingStrategy`, hence anyone can extend this behaviour by providing a custom strategy:\n```swift\npublic class AppearanceApplyingStrategy {\n    \n    public func apply(appearance: Appearance?, toNavigationController navigationController: UINavigationController, animated: Bool) {\n    }\n}\n\n```\n\nAnd connect the strategy to the `AppearanceNavigationController`:\n```swift\nprivate func applyAppearance(appearance: Appearance?, animated: Bool) {\n// we ignore nil appearance\n    if appearance != nil \u0026\u0026 appliedAppearance != appearance {\n        appliedAppearance = appearance\n        \n        appearanceApplyingStrategy.apply(appearance: appearance, toNavigationController: self, animated: animated)\n        setNeedsStatusBarAppearanceUpdate()\n    }\n}\n\npublic var appearanceApplyingStrategy = AppearanceApplyingStrategy() {\n    didSet {\n        applyAppearance(appearance: appliedAppearance, animated: false)\n    }\n}\n```\n\nFor sure, this solution doesn’t pretend to be a silver bullet, however with this short and simple implementation we have: \n\n- simplified navigation controller’s appearance configuration\n- reduced duplication in code by defining extensions to the `Appearance`\n- made our code more UIKit-like\n- reduced the number of small and annoying bugs. For example, an accidental status bar style change by MailComposer.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyalantis%2Fappearancenavigationcontroller","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyalantis%2Fappearancenavigationcontroller","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyalantis%2Fappearancenavigationcontroller/lists"}