All Projects → Yalantis → Appearancenavigationcontroller

Yalantis / Appearancenavigationcontroller

Licence: mit
Example with advanced configuration of the navigation controller's appearance

Programming Languages

swift
15916 projects

Projects that are alternatives of or similar to Appearancenavigationcontroller

NavigationRouter
A router implementation designed for complex modular apps, written in Swift
Stars: ✭ 89 (-2.2%)
Mutual labels:  navigation, uikit
Customnavigationbarsample
Navigation Bar Customization in Xamarin Forms
Stars: ✭ 104 (+14.29%)
Mutual labels:  navigation, toolbar
Tlyshynavbar
Unlike all those arrogant UINavigationBar, this one is shy and humble! Easily create auto-scrolling navigation bars!
Stars: ✭ 3,780 (+4053.85%)
Mutual labels:  uikit, navigation
Dropdownmenukit
UIKit drop down menu, simple yet flexible and written in Swift
Stars: ✭ 246 (+170.33%)
Mutual labels:  uikit, navigation
NavigationHeader
Navigation Header library based on MotionLayout inspired by dribble menu design built with MotionLayout and ObjectAnimator.
Stars: ✭ 39 (-57.14%)
Mutual labels:  navigation, toolbar
Starwars.ios
This component implements transition animation to crumble view-controller into tiny pieces.
Stars: ✭ 3,685 (+3949.45%)
Mutual labels:  uikit, yalantis
Androidnavigation
A library managing navigation, nested Fragment, StatusBar, Toolbar for Android
Stars: ✭ 636 (+598.9%)
Mutual labels:  navigation, toolbar
Swiftassetspickercontroller
A simple assets picker controller based on iOS 8 Photos framework. Supports iCloud photos and videos. It's written in Swift.
Stars: ✭ 81 (-10.99%)
Mutual labels:  uikit
Modo
Navigation library based on UDF principles
Stars: ✭ 85 (-6.59%)
Mutual labels:  navigation
Promotion Menu
RubyMotion gem allowing you to easily setup a facebook or Path style hidden slide menu easily with the ProMotion gem.
Stars: ✭ 78 (-14.29%)
Mutual labels:  navigation
Sizes
View your app on different device and font sizes
Stars: ✭ 1,213 (+1232.97%)
Mutual labels:  uikit
Sonwan Ui
SonWan UI is a modular UI component library based on figma design to build your next React Web Application.
Stars: ✭ 75 (-17.58%)
Mutual labels:  uikit
Pixpic
PixPic, a Photo Editing App
Stars: ✭ 1,261 (+1285.71%)
Mutual labels:  yalantis
Catalyst Helpers
Unlock missing UIKit functionality with these AppKit helpers
Stars: ✭ 81 (-10.99%)
Mutual labels:  uikit
Gradientnavigation
颜色渐变的导航栏、位置可变的导航栏
Stars: ✭ 90 (-1.1%)
Mutual labels:  navigation
Rsband local planner
A ROS move_base local planner plugin for Car-Like robots with Ackermann or 4-Wheel-Steering.
Stars: ✭ 78 (-14.29%)
Mutual labels:  navigation
Mrpt navigation
ROS nodes wrapping core MRPT functionality: localization, autonomous navigation, rawlogs, etc.
Stars: ✭ 90 (-1.1%)
Mutual labels:  navigation
Uinavigation
A UE4 plugin designed to help easily make UMG menus navigable by mouse, keyboard and gamepad
Stars: ✭ 88 (-3.3%)
Mutual labels:  navigation
Navigation
ROS Navigation stack. Code for finding where the robot is and how it can get somewhere else.
Stars: ✭ 1,248 (+1271.43%)
Mutual labels:  navigation
Karte
🗺 Conveniently launch directions in other iOS apps
Stars: ✭ 83 (-8.79%)
Mutual labels:  navigation

AppearanceNavigationController

Sample of navigation controller appearance configuration from our blog post.

Declarative Navigation Bar Appearance Configuration

The last couple of month I’ve been working on an app that required implementation of color changing behavior.

In 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.

How do you implement color changing features?

A naive approach was to perform the navigation controller appearance configuration in the UIViewController.viewWillAppear:

But 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.

Given 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.

The principle of work of AppearanceNavigationController

To achieve a declarative behaviour we need to become a UINavigationControllerDelegate to handle push and pop between the view controllers.

In the navigationController(_:willShowViewController:animated:) we need to ask view controller that is being shown the following questions:

  • Do we need to display a navigation bar?
  • What color should it be?
  • Do we need to display a toolbar?
  • What color should it be?
  • What style for the status bar is preferred?

Let’s turn it into a Swift’s struct:

public struct Appearance: Equatable {
    
    public struct Bar: Equatable {
       
        var style: UIBarStyle = .default
        var backgroundColor = UIColor(red: 234 / 255, green: 46 / 255, blue: 73 / 255, alpha: 1)
        var tintColor = UIColor.white
        var barTintColor: UIColor?
    }
    
    var statusBarStyle: UIStatusBarStyle = .default
    var navigationBar = Bar()
    var toolbar = Bar()
}

You 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. As you remember, we’re going to “ask” our view controller about the preferred apperance. Let’s implement this as follows:

public protocol NavigationControllerAppearanceContext: class {
    
    func prefersBarHidden(for navigationController: UINavigationController) -> Bool
    func prefersToolbarHidden(for navigationController: UINavigationController) -> Bool
    func preferredAppearance(for navigationController: UINavigationController) -> Appearance?
}

Since 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:

extension NavigationControllerAppearanceContext {
    
    func prefersBarHidden(for navigationController: UINavigationController) -> Bool {
        return false
    }
    
    func prefersToolbarHidden(for navigationController: UINavigationController) -> Bool {
        return true
    }
    
    func preferredAppearance(for navigationController: UINavigationController) -> Appearance? {
        return nil
    }
}

As 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”.

Now let’s implement the basics of our Navigation Controller:

public class AppearanceNavigationController: UINavigationController, UINavigationControllerDelegate {

    public required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)
        
        delegate = self
    }
    
    override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

        delegate = self
    }

    override public init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)

        delegate = self
    }
    
    // MARK: - UINavigationControllerDelegate
    
    public func navigationController(
        _ navigationController: UINavigationController,
        willShow viewController: UIViewController, animated: Bool
    ) {
        guard let appearanceContext = viewController as? NavigationControllerAppearanceContext else {
            return
        }
        setNavigationBarHidden(appearanceContext.prefersBarHidden(for: self), animated: animated)
        setNavigationBarHidden(appearanceContext.prefersBarHidden(for: self), animated: animated)
        setToolbarHidden(appearanceContext.prefersToolbarHidden(for: self), animated: animated)
        applyAppearance(appearance: appearanceContext.preferredAppearance(for: self), animated: animated)
    }

    // mark: - Appearance Applying
        
    private func applyAppearance(appearance: Appearance?, animated: Bool) {        
        // apply
    }
}

Appearance Configuration

Now it’s time to implement the appearance applying the details:

private func applyAppearance(appearance: Appearance?, animated: Bool) {
    if let appearance = appearance {
        if !navigationBarHidden {
            let background = ImageRenderer.renderImageOfColor(color: appearance.navigationBar.backgroundColor)
            navigationBar.setBackgroundImage(background, for: .default)
            navigationBar.tintColor = appearance.navigationBar.tintColor
            navigationBar.barTintColor = appearance.navigationBar.barTintColor
            navigationBar.titleTextAttributes = [
                NSAttributedString.Key.foregroundColor: appearance.navigationBar.tintColor
            ]
        }

        if !toolbarHidden {
            toolbar?.setBackgroundImage(
                ImageRenderer.renderImageOfColor(color: appearance.toolbar.backgroundColor),
                forToolbarPosition: .any,
                barMetrics: .default
            )
            toolbar?.tintColor = appearance.toolbar.tintColor
            toolbar?.barTintColor = appearance.toolbar.barTintColor
        }
    }
}

If 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.

Status bar configuration

Note, 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.

private var appliedAppearance: Appearance?

private func applyAppearance(appearance: Appearance?, animated: Bool) {
    if let appearance = appearance where appliedAppearance != appearance {
        appliedAppearance = appearance

        // rest of the code

        setNeedsStatusBarAppearanceUpdate()
    }
}

// mark: - Status Bar 

public override var preferredStatusBarStyle: UIStatusBarStyle {
    appliedAppearance?.statusBarStyle ?? super.preferredStatusBarStyle
}

public override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
    appliedAppearance != nil ? .fade : super.preferredStatusBarUpdateAnimation
}

Since 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.

Appearance Update

Obvisouly, the view controller appearance may change during its lifecycle. In order to update the view controller let’s add a UIKit-like method NavigationControllerAppearanceContext:

public protocol NavigationControllerAppearanceContext: class {    
    // rest of the interface

    func setNeedsUpdateNavigationControllerAppearance()
}

extension NavigationControllerAppearanceContext {
    // rest of the defaul implementation

    func setNeedsUpdateNavigationControllerAppearance() {
        if let viewController = self as? UIViewController,
            let navigationController = viewController.navigationController as? AppearanceNavigationController {
            navigationController.updateAppearance(for: viewController)
        }
    }
}

And a corresponding implementation in the AppearanceNavigationController:

func updateAppearance(for viewController: UIViewController) {
    if let context = viewController as? NavigationControllerAppearanceContext,
        viewController == topViewController && transitionCoordinator == nil {
        setNavigationBarHidden(context.prefersBarHidden(for: self), animated: true)
        setToolbarHidden(context.prefersToolbarHidden(for: self), animated: true)
        applyAppearance(appearance: context.preferredAppearance(for: self), animated: true)
    }
}

public func updateAppearance() {
    if let top = topViewController {
        updateAppearance(for: top)
    }
}

From 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.

Usage

We’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:

class ContentViewController: UIViewController, NavigationControllerAppearanceContext {
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        navigationItem.rightBarButtonItem = editButtonItem
    }
    
    var appearance: Appearance? {
        didSet {
            setNeedsUpdateNavigationControllerAppearance()
        }
    }
    
    // mark: - Actions
    
    override func setEditing(_ editing: Bool, animated: Bool) {
        super.setEditing(editing, animated: animated)
        
        setNeedsUpdateNavigationControllerAppearance()
    }
    
    // mark: - AppearanceNavigationControllerContent

    func prefersToolbarHidden(for navigationController: UINavigationController) -> Bool {
        // hide toolbar during editing
        return isEditing
    }
    
    func preferredAppearance(for navigationController: UINavigationController) -> Appearance? {
        // inverse navigation bar color and status bar during editing
        return isEditing ? appearance?.inverse() : appearance
    }
}

Gathering the appearance together

Now we can gather all the appearance configurations as a category with common configurations, thus eliminating code duplication as in the naive solution:

extension Appearance {
    
    static let lightAppearance: Appearance = {
        var value = Appearance()
        
        value.navigationBar.backgroundColor = .lightGray
        value.navigationBar.tintColor = .white
        value.statusBarStyle = .lightContent
        
        return value
    }()
}

Customizing the appearance

To make the animation more customizeable let’s wrap it into the AppearanceApplyingStrategy, hence anyone can extend this behaviour by providing a custom strategy:

public class AppearanceApplyingStrategy {
    
    public func apply(appearance: Appearance?, toNavigationController navigationController: UINavigationController, animated: Bool) {
    }
}

And connect the strategy to the AppearanceNavigationController:

private func applyAppearance(appearance: Appearance?, animated: Bool) {
// we ignore nil appearance
    if appearance != nil && appliedAppearance != appearance {
        appliedAppearance = appearance
        
        appearanceApplyingStrategy.apply(appearance: appearance, toNavigationController: self, animated: animated)
        setNeedsStatusBarAppearanceUpdate()
    }
}

public var appearanceApplyingStrategy = AppearanceApplyingStrategy() {
    didSet {
        applyAppearance(appearance: appliedAppearance, animated: false)
    }
}

For sure, this solution doesn’t pretend to be a silver bullet, however with this short and simple implementation we have:

  • simplified navigation controller’s appearance configuration
  • reduced duplication in code by defining extensions to the Appearance
  • made our code more UIKit-like
  • reduced the number of small and annoying bugs. For example, an accidental status bar style change by MailComposer.
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].