Ariadne
Ariadne's thread, named for the legend of Ariadne, is the solving of a problem with multiple apparent means of proceeding - such as a physical maze, a logic puzzle, or an ethical dilemma - through an exhaustive application of logic to all available routes.
Ariadne is an extensible routing framework, built with composition and dependency injection principles in mind. It helps to create transitions and routes, that abstract away view controller building and presentation logic to make it reusable and compact.
Motivation
UIKit has a routing problem. All view controller presentation and dismissal methods happen in view controller, which a lot of times leads to bloated view controller, because all view controller building, dependency injection and transitions that also happen there.
This leads to massive view controllers, that cannot be easily tested, and code, that is hard to reuse across different view controller instances. One solution to those problems is to separate view controller building, and transition code in separate object, commonly called Router
. And even though only some architectures like VIPER promote Router
as required component, I would argue that app with any other architecture can be drastically improved by having at least some form of routing.
This is where Ariadne
comes in. It is a framework, that provides view controller building mechanisms, transition and routing classes to abstract all this logic from view controller in architecture-agnostic way.
Example
Let's say, for example, that you need to present user profile inside UINavigationController
. Usually, in MVC app without any libraries, you would do something like this:
let storyboard = UIStoryboard(named: "User", bundle: nil)
let userController = storyboard.instantiateViewController(withIdentifier: "UserViewController")
userController.user = user
let navigation = UINavigationController(rootViewController: userController)
present(navigation, animated: true)
With Ariadne, this code is no longer tied to current view controller and can look like this:
let route = Storyboards.User.userViewController.builder.embeddedInNavigation().presentRoute()
router.navigate(to: route, with: user)
Note: this specific example requires SwiftGen integration, described in SwiftGen integration guide. Without SwiftGen
Storyboards.User.userViewController.builder
could be replaced by any custom view controller builder.
Requirements
- iOS 10+
- macOS 10.12+
- tvOS 10+
- watchOS 3+
- Xcode 10 / Swift 4.2 and higher
Installation
Swift Package Manager(requires Xcode 11)
- Add package into Project settings -> Swift Packages
CocoaPods
pod 'Ariadne'
Overview
Ariadne
architecture fundamentally starts with ViewBuilder
. Because view controllers are so tightly coupled with their views on iOS, UIViewController
is considered to be a view and is typealiased to ViewController
.
Note: on watchOS
ViewController
is a typealias forWKInterfaceController
, and on macOS forNSViewController
for similar reasons.
Definition of ViewBuilder
is simple - it builds a ViewController
out of provided Context
:
protocol ViewBuilder {
associatedtype ViewType: ViewController
associatedtype Context
func build(with context: Context) throws -> ViewType
}
Out of the box, Ariadne
provides builders for:
- UINavigationController
- UITabBarController
- UISplitViewController
Second building block of the framework are ViewTransition
objects, that are needed to perform transition between views. Out of the box, following transitions are supported:
- UINavigationController transitions - push, pop, pop to root, pop to view controller, replace controllers in navigation stack
- UIViewController presentations - present, dismiss
- UIWindow root view controller transition to perform switch of the root view controller with animation.
ViewBuilder
and ViewTransition
object can be combined together to form a performable Route
. For example, given AlertBuilder
, here's how creating a route for an alert might look like with Ariadne
:
let alertRoute = AlertBuilder().presentRoute()
Notice how presentRoute
method is called identically for AlertBuilder
and any UIViewController
builders. By leveraging protocol extensions on ViewBuilder
any transitions and routes can be reused on ViewBuilder
instance. To see examples of how ViewBuilder
protocol can be implemented and extended, please refer to Implementing view builders guide.
Last, but not least, Router
object ties everything together and allows you to actually perform routes:
router.navigate(to: alertRoute, with: alertModel, completion: { _ in
// Route has completed
})
Router uses RootViewProvider
to find which view controller is a root one in a view hierarchy. On iOS and tvOS RootViewProvider
is an interface for UIWindow
and allows Router
to get root view controller of view hierarchy. But on other platforms as well as application extensions UIApplication shared window is not accessible, and in that cases RootViewProvider
may be different, for example in iMessage apps MSMessagesAppViewController
may play similar role.
ViewFinder
object traverses view hierarchy starting from root view to find view controller that is currently visible on screen. On iOS and tvOS Ariadne
provides implementation of CurrentlyVisibleViewFinder
class, that recursively searches UIViewController
, UINavigationController
and UITabBarController
to find which view controller is currently visible, but on other platforms and in other scenarios you might want to roll with your implementation or subclass of CurrentlyVisibleViewFinder
, if your view hierarchy contains other view controller containers.
SwiftGen integration
SwiftGen is a powerful code generator, that can be used to set you free from using String-based API, that is cumbersome and error-prone. For example with storyboards, SwiftGen is able to generate code required for instantiating view controllers and makes this code to guarantee on compile-time that storyboard and view controller exist. Ariadne
can build on top of that, producing a neat syntax for route building, like so:
let route = Storyboards.User.userViewController.builder.embeddedInNavigation().presentRoute()
To find out, how this can be achieved, refer to SwiftGen integration guide.
Dependency injection
Different applications can have completely different architectures and requirements. To see examples of simple dependency injection see SwiftGen integration and for more advanced dependency injection with dependency containers like Dip, head to Advanced dependency injection examples guide.
Vision
To find out more about project future goals and vision, please read the Vision document.
Documentation
You can find complete project documentation here.
Example project
iOS Example project can be found in Ariadne.xcodeproj and contains:
- Root view controller animated change
- Push/pop, present/dismiss
- Peek & Pop
- Custom transition and presentation
- Update currently visible view.
License
Ariadne is released under a MIT license. See LICENSE for more information.