Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/avito-tech/Marshroute

Marshroute is an iOS Library for making your Routers simple but extremely powerful
https://github.com/avito-tech/Marshroute

hacktoberfest hacktoberfest2021 ios navigation router swift transition viper

Last synced: about 1 month ago
JSON representation

Marshroute is an iOS Library for making your Routers simple but extremely powerful

Awesome Lists containing this project

README

        

# Marshroute
[![GitHub license](https://img.shields.io/badge/License-MIT-lightgrey.svg)](https://github.com/avito-tech/Marshroute/blob/master/LICENSE) [![GitHub release](https://img.shields.io/badge/Version-1.0.0-brightgreen.svg)](https://github.com/avito-tech/Marshroute/releases) [![Swift 5.0 support](https://img.shields.io/badge/Swift%205.0-supported-brightgreen.svg)](https://github.com/avito-tech/Marshroute/commit/278beaa9b53476315906fee1ba6555caf2e1db86) [![cocoapods compatible](https://img.shields.io/badge/Cocoapods-compatible-blue.svg)](https://cocoapods.org)

## Contents

* [Overview](#overview)
* [Detailes](#overview-details)
* [Tuning the transition animation](#tuning-the-transition-animation)
* [3d touch support](#3d-touch-support)
* [PeekAndPopUtility](#peek-and-pop-utility)
* [Peek and pop state observing](#peek-and-pop-state-observing)
* [Demo](#demo)
* [Requirements](#requirements)
* [Installation](#installation)
* [Cocoapods](#cocoapods)
* [Carthage](#carthage)
* [Customization](#plugin-customization)
* [Licence](#licence)
* [Objective-c support](#objective-c-support)
* [Useful links](#useful-links)
* [Authors](#authors)

## Overview

`Marshroute` is a library that will encourage you to locate all the navigation logic in the `Router` layer, no matter which architecture you prefer.
`Marshroute` helps make your `Router`s syntactically compact and clear.

Key features:
- Every `Router`-driven transition is always forwarded to the topmost `UIViewController`. This means you can ask `Marshroute` to present a view controller from any point in your program and it will simply work!
- No matter how you module was presented, you can simply ask your module's `Router` to dismiss this module via calling `router.dismissCurrentModule()` and it will simply work! Your parent module can change presentation style in the future (e.g. present modally instead of pushing), but `router.dismissCurrentModule()` will work anyway!
- No matter how your module presents subsequent modules, you can simply ask your module's `Router` to return to this module via calling `router.focusOnCurrentModule()` and it will also simply work!
- `Marshroute` allows changing transition animations in just 1 line of code (see [Tuning the transition animation](#tuning-the-transition-animation) for more details)
- `Marshroute` supports 3d touch transitions
- `Marshroute` detects view controller retain cycles and notifies you about them via assertions API. You can override default assertions with your implementation: e.g. print assertions to the output or do some advanced analytics (see [plugin-customization](#plugin-customization) for more details)
- `Marshroute` features a detailed [demo](#demo) project describing key navigation principles on both iPhone and iPad

### Details

Every `Router`-driven transition is always forwarded to the topmost `UIViewController`
to make it super easy to support `DeepLink`s and for example present `Authorization` module from any point of your application.
I prefer doing this right from my root application's `Router`.

This repo allows you to drive your transitions in a super clean, descriptive and flexible fashion.
For example pretend the following code is taken from your root application's `Router`:

```swift
func showAuthorization() {
pushViewControllerDerivedFrom { routerSeed -> UIViewController in
let authorizationAssembly = assemblyFactory.authorizationAssembly()

let viewController = authorizationAssembly.module(
routerSeed: routerSeed
)

return viewController
}
}
```

This code pushes an `Authorization` view controller to the top `UINavigationController`'s stack.
The `routerSeed` parameter is only used to create a `Router` for the `Authorization` module.

The magic here is in this line of code:
```swift
pushViewControllerDerivedFrom { routerSeed -> UIViewController in
```

You can easily change the presentation style in favor of a modal transition by simply changing it to:
```swift
presentModalNavigationControllerWithRootViewControllerDerivedFrom { routerSeed -> UIViewController in
```

If for some reason you do not need a `UINavigationController` for your `Authorization` module, you may accomplish this by:
```swift
presentModalViewControllerDerivedFrom { routerSeed -> UIViewController in
```

Once again, the transition will be forwarded to the top, keeping the `Router` very plain and straightforward.
So that, the `Router` keeps being responsible for only one thing: selecting the style of a transition.

### Tuning the transition animation

You may add an animator to customize the way your transition looks like. For example

```swift
func showCategories() {
presentModalNavigationControllerWithRootViewControllerDerivedFrom( { routerSeed -> UIViewController in
let categoriesAssembly = assemblyFactory.categoriesAssembly()

let viewController = categoriesAssembly.module(
routerSeed: routerSeed
)

return viewController
}, animator: RecursionAnimator())
}
```

The key line here is
```swift
}, animator: RecursionAnimator())
```

So the syntax remains clean and it is super easy to switch back to the original animation style.

## 3d touch support

### PeekAndPopUtility

Want to add fancy peek and pop previews? Easy peasy! Just use `PeekAndPopUtility` from the `MarshrouteStack` and register your view controller as capable of previewing other controllers!

```swift
peekAndPopUtility.register(
viewController: self,
forPreviewingInSourceView: peekSourceView,
onPeek: { [weak self] (previewingContext, location) in
self?.startPeekWith(
previewingContext: previewingContext,
location: location
)
},
onPreviewingContextChange: nil
)
```

`peekSourceView` is used by `UIKit` during preview animations to take screenshots from. You can register single view controller for previewing in many source views (e.g.: in a table view and in a navigation bar).

`onPeek` closure will get called every time a force touch gesture occurs in a `peekSourceView`. In your `startPeekWith(previewingContext:location:)` method you should do the following:

1. Find a view which a user interacts with (interactable view). You should use a specified `location` in `previewingContext.sourceView`'s coordinate system.

1. Adjust `sourceRect` of a `previewingContext`. You should convert a frame of that interactable view to `previewingContext.sourceView`'s coordinate system. `UIKit` uses `sourceRect` to keep it visually sharp when a user presses it, while blurring all surrounding content.

1. Invoke the transition, that will normally occur if a user simply taps at a same `location`.
For example, it a user presses a `UIControl`, you may call `sendActions(for: .touchUpInside)` to invoke that `UIControl`'s an action handler.

Lets pretend the above-mentioned action handler ends up with some router calling `pushViewControllerDerivedFrom(_:)` to push a new view controller.
In this case no pushing will actually occur. Instead of this, `Marshroute` will freeze a transition and present a target view controller in a preview mode.
The transition will eventually get performed only if a user commits the preview (i.e. pops).

The above described behavior takes place only during active `peek` requests (when `UIViewControllerPreviewingDelegate` requests a view controller to be previewed).
In all other situations, `pushViewControllerDerivedFrom(_:)` will push immediately as expected.

Important note: if you invoke no transition within `onPeek` closure, or invoke an asynchronous transition, no `peek` will occur.
This behavior is a result of `UIKit` Api restrictions: `UIViewControllerPreviewingDelegate` is required to return a previewing view controller synchronously.

You can also use `onPreviewingContextChange` closure to set up your gesture recognizer failure relationships.

### Peek and pop state observing

You can use `PeekAndPopStateObservable` from the `MarshrouteStack` to observe any view controller's `peek and pop` state changes.
This may be useful for analytics purposes.

```swift
peekAndPopStateObservable.addObserver(
disposable: self,
onPeekAndPopStateChange: { viewController, peekAndPopState in
debugPrint("viewController: \(viewController) changed `peek and pop` state: \(peekAndPopState)")
}
)
```

You can also use `PeekAndPopStateViewControllerObservable` to observe your particular view controller's `peek and pop` state changes.
This may be useful for adjusting view controller's appearance in `peek` and `popped` modes.

```swift
peekAndPopStateViewControllerObservable.addObserver(
disposableViewController: self,
onPeekAndPopStateChange: { [weak self] peekAndPopState in
switch peekAndPopState {
case .inPeek:
self?.onPeek?()
case .popped:
self?.onPop?()
case .interrupted:
break
}
}
)
```

Here in `onPeek` and `onPop` closures your `Presenter` may force a view to update its UI accordingly

```swift
view?.onPeek = { [weak self] in
self?.view?.setSimilarSearchResultsHidden(true)
}

view?.onPop = { [weak self] in
self?.view?.setSimilarSearchResultsHidden(false)
}
```

## Demo

Check out the [demo](https://github.com/avito-tech/Marshroute/tree/master/Example) project.
This demo is written in `Swift` using `VIPER` architecture and shows all the capabilities which `Router`s are now full of.

Run this demo on a simulator and check out what happens if you simulate a memory warning or a device shake.
You will see several types of transitions driven by the root module's `Router` (i.e. a `UITabBarController`'s `Router`).

The demo project targets both `iPhone` and `iPad` and adds some minor differences to their navigation behaviors by creating distinct `Router` implementations for every supported device idiom, thus highlighting the value of moving the navigation logic from the `View` layer in favor of a `Router` layer.

When you tap a blue timer tile, you schedule a reverse transition to the module that tile belongs to.
To see this effect taking place, you should make several transitions deeper into the navigation stack.

Starting with 0.4.0 the demo project was updated to show `PeekAndPopUtility` in action: you can press on any table view cell and navigation bar button to get a preview of an underlying transition.
You can also learn how to use `PeekAndPopStateViewControllerObservable` to adjust `AdvertisementViewController`'s appearance in `peek` and `popped` modes: in a `peek` mode you will see only a fullscreen colored image pattern, while in a `popped` mode you will also see a similar advertisements section.

## Requirements

- iOS 9.0+
- Xcode 14.0+

Note: peek and pop is supported only for iOS 9.0+

## Installation
### Cocoapods

To install Marshroute using CocoaPods, add the following lines to your `Podfile`:

```ruby
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

pod 'Marshroute'
```

Then run `pod install` command. For details of the installation and usage of CocoaPods, visit [its official website](https://cocoapods.org).

### Carthage

To install Marshroute using Carthage, add the following lines to your `Cartfile`:
```ruby
github "avito-tech/Marshroute" ~> 1.0.0
```

Then run `carthage update --platform iOS` command. For details of the installation and usage of Carthage, visit [its repo website](https://github.com/Carthage/Carthage).

## Customization

You can provide custom print and assert realization using `MarshroutePrintPlugin` and `MarshrouteAssertionPlugin`.
This is as easy as:
```
MarshroutePrintManager.setUpPrintPlugin(YourPrintPlugin())
MarshrouteAssertionManager.setUpAssertionPlugin(YourAssertionPlugin())
```

## Licence
MIT

## Objective-c support
The framework is written in pure `Swift` using its latest features, so if you want to use `Marshroute` in your `Objective-c` application you will have to write your `Router`s in `Swift`.

## Useful links
You can watch [this video](https://www.youtube.com/watch?v=kyOm_dC038s&t=1s) to get a closer look at the reasons and ideas which formed the basis of `Marshroute` (in Russian).

You can also read [this guide](https://github.com/avito-tech/Marshroute/wiki/Examples) of using `Marshroute` when implementing `DeepLink`s support in your application.

## Authors
Timur Yusipov ([email protected], [email protected], https://twitter.com/Fizmatchel, https://stackoverflow.com/users/2982854/tim).