A comprehensive SwiftUI coordinator pattern library that provides powerful navigation management and tab-based coordination for iOS applications. SUICoordinator enables clean separation of concerns by decoupling navigation logic from view presentation, making your SwiftUI apps more maintainable and scalable.
- Pure SwiftUI: No UIKit dependencies - built entirely with SwiftUI
- Coordinator Pattern: Clean separation of navigation logic from views
- Tab Coordination: Advanced tab-based navigation with
TabCoordinator
, custom views, and badges - Flexible Presentations: Support for push, sheet, fullscreen, detents, and custom presentations
- Deep Linking: Force presentation capabilities for push notifications and external triggers
- Type-Safe Routes: Strongly typed navigation routes with compile-time safety
- Async Navigation: Full async/await support for smooth navigation flows
- Custom Tab Bars: Create completely custom tab interfaces with
TabCoordinator
- Badge Support: Dynamic badge management for tab items in
TabCoordinator
- Memory Management: Automatic cleanup and resource management
- Open Xcode and your project
- Go to
File
→Swift Packages
→Add Package Dependency...
- Enter the repository URL:
https://github.com/felilo/SUICoordinator
- Click
Next
twice and thenFinish
- Download the source files from the
Sources/SUICoordinator
directory. - Drag and drop the
SUICoordinator
folder into your Xcode project. - Make sure to add it to your target.
Create an enum that conforms to RouteType
to define your navigation paths and their associated views.
import SwiftUI
import SUICoordinator
enum HomeRoute: RouteType {
case push(dependencies: DependenciesPushView)
case sheet(viewModel: DependenciesSheetView)
var presentationStyle: TransitionPresentationStyle {
switch self {
case .push: .push
case .sheet: .sheet
}
}
@ViewBuilder
var view: some View {
switch self {
case .push(let dependencies): PushView(viewModel: .init(dependencies: dependencies))
case .sheet(let dependencies): SheetView(viewModel: .init(dependencies: dependencies))
}
}
}
For a deeper dive into route protocol, take a look at RouteType
Subclass Coordinator<YourRouteType>
and implement the mandatory start()
method. This method defines the initial view or flow for the coordinator.
import SUICoordinator
class HomeCoordinator: Coordinator<HomeRoute> {
override func start() async {
// This is the first view the coordinator will show.
// 'startFlow' clears any existing navigation stack and presents this route.
let viewModel = ActionListViewModel(coordinator: self)
await startFlow(route: .actionListView(viewModel: viewModel), animated: animated)
}
// Example: Navigating to another view within this coordinator's flow
func navigateToPushView() async {
let viewModel = PushViewModel(coordinator: self)
// Use the 'router' to navigate to other routes defined in HomeRoute.
await router.navigate(toRoute: .push(viewModel: viewModel))
}
// Example: Presenting a sheet
func presentSheet() async {
let viewModel = SheetViewModel(coordinator: self)
// Use 'presentSheet(route:)' for modal presentations.
// The route's presentationStyle (e.g., .sheet, .fullScreenCover) will be used.
await presentSheet(route: .sheet(viewModel: viewModel))
}
// Example: Navigating to another Coordinator (e.g., a TabCoordinator)
func presentCustomTabs() async {
let tabCoordinator = CustomAppTabCoordinator() // Assuming this is your TabCoordinator
// The 'navigate(to:presentationStyle:)' method on a coordinator
// is used to present another coordinator.
await navigate(to: tabCoordinator, presentationStyle: .sheet)
}
func close() async {
// 'router.close()' will either dismiss a presented sheet or pop a pushed view.
await router.close(animated: true)
}
func endThisCoordinator() async {
// 'finishFlow()' will dismiss/pop all views of this coordinator
// and remove it from its parent coordinator.
await finishFlow(animated: true)
}
func restart(animated: Bool = true) async {
// Resets the coordinator's navigation state by calling `router.restart()`.
// This clears all navigation stacks and modal presentations managed by this coordinator.
// All navigation history will be lost, modal presentations dismissed, and the coordinator returns to its initial state as if `start()` was just called (depending on how `start()` and `startFlow()` are implemented).
// Useful for logout scenarios or major state changes.
await router.restart(animated: animated)
}
}
Your SwiftUI views will typically be initialized with a ViewModel. The ViewModel can hold a reference to its coordinator to trigger navigation actions.
// ActionListViewModel.swift
import Foundation
class ActionListViewModel: ObservableObject {
let coordinator: HomeCoordinator // Or a protocol if you prefer
init(coordinator: HomeCoordinator) {
self.coordinator = coordinator
}
@MainActor func userTappedPush() async {
await coordinator.navigateToPushView()
}
@MainActor func userTappedShowSheet() async {
await coordinator.presentSheet()
}
@MainActor func userTappedShowTabs() async {
await coordinator.presentCustomTabs()
}
}
// NavigationActionListView.swift
import SwiftUI
struct NavigationActionListView: View {
@StateObject var viewModel: ActionListViewModel
var body: some View {
List {
Button("Push Example View") { Task { await viewModel.userTappedPush() } }
Button("Present Sheet Example") { Task { await viewModel.userTappedShowSheet() } }
Button("Present Tab Coordinator") { Task { await viewModel.userTappedShowTabs() } }
}
.navigationTitle("Coordinator Actions")
}
}
In your main App
struct, instantiate your root coordinator and use its getView()
method.
import SwiftUI
import SUICoordinator // Import the library
@main
struct SUICoordinatorExampleApp: App {
// Instantiate your main/root coordinator
var mainCoordinator = HomeCoordinator() // Or your primary app coordinator
var body: some Scene {
WindowGroup {
mainCoordinator.getView()
}
}
}
The TabCoordinator<Page: TabPage>
is a specialized coordinator for managing a collection of child coordinators, where each child represents a distinct tab in a tab-based interface.
First, create an enum that conforms to TabPage
. TabPage
is a typealias for PageDataSource & TabNavigationRouter & SCEquatable
.
PageDataSource
: Requires you to define:position: Int
: The order of the tab (0-indexed).dataSource: YourPageDataSourceType
: An object/struct that provides the tab's visual elements (e.g., icon, title).
TabNavigationRouter
: Requires you to implement:coordinator() -> any CoordinatorType
: A function that returns the specificCoordinator
instance for this tab's navigation flow.
SCEquatable
: Enums conform toEquatable
automatically if all their raw values/associated values do. This is usually satisfied.
import SwiftUI
import SUICoordinator
// Step 1.1: Define the data source for your tab items' appearance
// This struct will hold the data for icons and titles for each tab.
public struct AppTabPageDataSource {
let page: AppTabPage // A reference to the AppTabPage enum case
@ViewBuilder public var icon: some View {
switch page {
case .home: Image(systemName: "house.fill")
case .settings: Image(systemName: "gearshape.fill")
}
}
@ViewBuilder public var title: some View {
switch page {
case .home: Text("Home")
case .settings: Text("Settings")
}
}
}
// Step 1.2: Define your TabPage enum
enum AppTabPage: TabPage, CaseIterable { // CaseIterable is useful for providing .allCases
case home
case settings
// PageDataSource conformance
var position: Int {
switch self {
case .home: return 0
case .settings: return 1
}
}
var dataSource: AppTabPageDataSource {
// Return an instance of your data source for this page
AppTabPageDataSource(page: self)
}
// TabNavigationRouter conformance
func coordinator() -> any CoordinatorType {
// Return the specific coordinator instance for this tab's flow
switch self {
case .home: return HomeCoordinator() // Create and return a new instance
case .settings: return SettingsCoordinator() // Create and return a new instance
}
}
}
Note: HomeCoordinator
and SettingsCoordinator
in the example above are regular Coordinator
subclasses, each managing their own RouteType
and views.
Subclass TabCoordinator<YourTabPageEnum>
. In its initializer, you'll typically call super.init()
providing:
pages
: An array of yourTabPage
enum cases (e.g.,AppTabPage.allCases.sorted(by: { $0.position < $1.position })
).currentPage
: TheTabPage
case that should be selected initially.presentationStyle
(optional): How thisTabCoordinator
itself is presented by its parent (default is.sheet
).viewContainer
: A closure that returns the SwiftUI view responsible for rendering the tab bar interface.- For SwiftUI's standard
TabView
, useTabViewCoordinator(dataSource: $0, currentPage: $0.currentPage)
. - For a completely custom tab bar UI, provide your own SwiftUI view that takes the
TabCoordinator
instance (asviewModel
ordataSource
). SeeCustomTabView.swift
in the example project.
- For SwiftUI's standard
// Example: TabCoordinator using SwiftUI's default TabView
import SUICoordinator
// Example: TabCoordinator using a custom TabView UI
// (See CustomTabView.swift and CustomTabCoordinator.swift in the example project for a full implementation)
class CustomAppTabCoordinator: TabCoordinator<AppTabPage> {
init(initialPage: AppTabPage = .home) {
super.init(
pages: AppTabPage.allCases,
currentPage: initialPage,
viewContainer: { dataSource in
// Provide your custom tab bar view.
CustomTabView(dataSource: dataSource)
}
)
}
}
For a detailed example, you can take a look at the CustomTabView.swift implementation.
Instantiate and start your TabCoordinator
from a parent coordinator, just like any other coordinator.
// In a parent coordinator (e.g., your main AppRootCoordinator)
func showMainApplicationTabs() async {
let tabCoordinator = CustomAppTabCoordinator()
// Present the TabCoordinator. '.fullScreenCover' is common for main tab interfaces.
await navigate(to: tabCoordinator, presentationStyle: .fullScreenCover)
}
SUICoordinator
facilitates deep linking (e.g., from push notifications) by allowing you to programmatically navigate to specific parts of your application. The primary method for this is forcePresentation(mainCoordinator:)
on a target coordinator. For tab-based applications, you'll combine this with TabCoordinator
methods like setCurrentPage(with:)
(or direct assignment to currentPage
) and then use the child coordinator's router
for further navigation.
- Identify the Target: Determine the ultimate destination:
- If it's within a
TabCoordinator
, identify theTabCoordinator
itself, the targetTabPage
, and the specificRoute
within that tab's child coordinator. - If it's a standalone flow, identify the
Coordinator
and its initialRoute
.
- If it's within a
- Instantiate Coordinators: Create instances of the necessary coordinators. For a deep link into a tab, this usually means instantiating the relevant
TabCoordinator
. - Force Present the Entry Coordinator: Call
yourTargetCoordinator.forcePresentation(presentationStyle: mainCoordinator:)
.yourTargetCoordinator
is the coordinator that directly leads to the deep link's entry point (e.g., aTabCoordinator
or a specific featureCoordinator
).mainCoordinator
should be your application's root/main coordinator to establish the correct presentation context. This step ensures the target coordinator's view hierarchy becomes active, potentially dismissing or covering other views.
- Navigate to the Specific Tab (if applicable): If
yourTargetCoordinator
is aTabCoordinator
:- Set its
currentPage
to the desiredTabPage
. For example:yourTabCoordinator.currentPage = .settingsTab
. - A brief
Task.sleep
(e.g.,try? await Task.sleep(nanoseconds: 100_000_000)
for 0.1s) after settingcurrentPage
can sometimes help ensure UI updates complete before further navigation.
- Set its
- Navigate Within the Active Coordinator:
- If it's a
TabCoordinator
, get the active child coordinator usingtry await yourTabCoordinator.getCoordinatorSelected()
. - Cast this child coordinator to its concrete type (e.g.,
SettingsCoordinator
). - Use this coordinator's
router
to navigate to the finalRoute
(e.g.,await settingsCoordinator.router.navigate(toRoute: SettingsRoute.itemDetails(id: "itemID123"))
).
- If it's a
@main
struct SUICoordinatorExampleApp: App {
/// The main coordinator for the application, responsible for managing the primary tab-based navigation.
/// It's an instance of `CustomTabCoordinator` which uses the standard SwiftUI `TabView`.
var mainCoordinator = CustomTabCoordinator()
/// The body of the app, defining the main scene.
/// It sets up a `WindowGroup` containing the view provided by the `mainCoordinator`.
/// - It includes `onReceive` for handling custom notifications that might trigger deep links.
/// - It includes `onOpenURL` for handling URL-based deep links.
/// - An `onAppear` modifier simulates an automatic deep link handling scenario after a 3-second delay
/// on application launch, demonstrating programmatic navigation.
var body: some Scene {
WindowGroup {
mainCoordinator.getView()
.onReceive(NotificationCenter.default.publisher(for: Notification.Name.PushNotification)) { object in
// Assumes `incomingURL` is accessible or passed via notification's object/userInfo
// For demonstration, let's assume `object.object` contains the URL string
guard let urlString = object.object as? String,
let path = DeepLinkPath(rawValue: urlString) else { return }
Task {
await try? handlePushNotificationDeepLink(path: path, rootCoordinator: mainCoordinator)
}
}
.onOpenURL { incomingURL in
guard let host = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true)?.host,
let path = DeepLinkPath(rawValue: host)
else { return }
Task { @MainActor in
try? await handlePushNotificationDeepLink(path: path, rootCoordinator: mainCoordinator)
}
}
}
}
/// Defines possible deep link paths for the application.
/// These raw string values would typically match URL schemes or notification payloads.
/// - `home`: Represents a path to a home-like view, potentially within a tab, to present a detents sheet.
/// - `tabCoordinator`: Represents a path to present a `CustomTabCoordinator` modally.
enum DeepLinkPath: String {
case home = "home" // Example: "yourapp://home" or a notification payload "home"
case tabCoordinator = "tabs-coordinator" // Example: "yourapp://tabs/coordinator"
}
/// Handles deep link navigation based on the provided path.
///
/// This function demonstrates how to programmatically navigate to specific parts of the app
/// by interacting with the coordinator hierarchy. It's designed to be called from
/// `onOpenURL`, `onReceive` (for notifications), or other app events.
///
/// - Parameters:
/// - path: The `DeepLinkPath` indicating the destination within the app.
/// - rootCoordinator: The root `AnyCoordinatorType` instance of the application (e.g., `mainCoordinator`),
/// used as a starting point to traverse and manipulate the coordinator tree.
/// - Throws: Can throw errors from coordinator operations, such as `topCoordinator()` or `getCoordinatorSelected()`,
/// if the navigation path is invalid or a coordinator is not in the expected state.
func handlePushNotificationDeepLink(
path: DeepLinkPath,
rootCoordinator: AnyCoordinatorType
) async throws {
switch path {
case .tabCoordinator:
// This case demonstrates deep linking to a view (with detents) within a specific tab.
// It ensures that the action is performed on a HomeCoordinator if it's managing the currently selected tab.
//
// 1. `rootCoordinator.topCoordinator()`: Gets the topmost coordinator. This could be a modal's coordinator
// or the selected tab's coordinator if `rootCoordinator` is a TabCoordinator without a modal presented directly by it.
// 2. `?.parent as? CustomTabCoordinator`: Checks if the parent of the topmost coordinator is our
// main `CustomTabCoordinator`. This confirms we are operating within the main tab structure.
// If true, `tabCoordinator` becomes this `CustomTabCoordinator` (which is `mainCoordinator`).
// 3. `tabCoordinator.getCoordinatorSelected()`: Retrieves the coordinator for the *currently selected tab*
// from the (now confirmed) `CustomTabCoordinator`.
// 4. `as? HomeCoordinator`: Checks if this selected tab's coordinator is an instance of `HomeCoordinator`.
// 5. `await coordinatorSelected.presentDetents()`: If all checks pass, calls `presentDetents()` on the
// `HomeCoordinator` of the active tab, typically showing a sheet with detents.
// This pattern allows for deep linking into a specific state (like showing a detents view) of a specific tab.
if let tabCoordinator = try rootCoordinator.topCoordinator()?.parent as? CustomTabCoordinator {
if let coordinatorSelected = try tabCoordinator.getCoordinatorSelected() as? HomeCoordinator {
await coordinatorSelected.presentDetents()
}
}
case .home:
// This case demonstrates presenting a different Coordinator modally (HomeCoordinator in this example).
// It creates a new `HomeCoordinator` instance and uses `forcePresentation`
// to display it as a sheet over the current context, managed by the `mainCoordinator`.
let coordinator = HomeCoordinator()
try? await coordinator.forcePresentation(
presentationStyle: .sheet,
mainCoordinator: mainCoordinator
)
}
}
}
For a comprehensive understanding and more advanced use cases, including TabCoordinator
examples (both default SwiftUI TabView
and custom tab views), please explore the example project located in the Examples folder.
These are the most important features and actions that you can perform:
To create any route in SUICoordinator
, your route definition (typically an enum
) must conform to the RouteType
protocol. This protocol is fundamental for defining navigable destinations within your application.
Protocol Requirements:
Conforming to RouteType
(which also implies SCHashable
) requires you to implement:
-
presentationStyle: TransitionPresentationStyle
:- A computed property that returns a
TransitionPresentationStyle
. - This determines how the view associated with the route will be presented. Possible values are:
.push
: For navigation stack presentation (e.g., within aNavigationStack
)..sheet
: Standard modal sheet presentation..fullScreenCover
: A modal presentation that covers the entire screen..detents(Set<PresentationDetent>)
: A sheet presentation that can rest at specific heights (detents like.medium
,.large
, or custom heights). Requires iOS 16+.- Example:
.detents([.medium, .large])
- Example:
.detents([.height(100), .fraction(0.75)])
- Example:
.custom(transition: AnyTransition, animation: Animation?, fullScreen: Bool = false)
: Allows for custom SwiftUI transitions.transition
: TheAnyTransition
to use (e.g.,.slide
,.opacity
, custom).animation
: An optionalAnimation
to apply to the transition.fullScreen
: ABool
indicating if the custom transition should behave like a full-screen presentation (defaultfalse
).
- A computed property that returns a
-
view: Body
:- A computed property, annotated with
@ViewBuilder
- It must return a type conforming to
any View
(SwiftUI'sView
protocol).Body
is a typealias forany View
withinRouteType
. - This property provides the actual SwiftUI view that will be displayed for this route.
- A computed property, annotated with
Example Implementation:
import SwiftUI
import SUICoordinator
enum AppRoute: RouteType { // AppRoute now conforms to RouteType
case login
case dashboard(userId: String)
case settings
case itemDetails(itemId: String)
case helpSheet
case customTransitionView
// 1. presentationStyle
var presentationStyle: TransitionPresentationStyle {
switch self {
case .login:
return .fullScreenCover // Login as a full screen cover
case .dashboard, .itemDetails:
return .push // Dashboard and item details are pushed
case .settings:
return .sheet // Settings are presented as a standard sheet
case .helpSheet:
return .detents([.medium, .large]) // Help sheet with detents
case .customTransitionView:
return .custom( // Example of a custom transition
transition: .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)),
animation: .easeInOut(duration: 0.5),
fullScreen: true
)
}
}
// 2. view
@ViewBuilder
var view: Body { // Body is 'any View'
switch self {
case .login:
LoginView() // Assuming LoginView exists
case .dashboard(let userId):
DashboardView(userId: userId) // Assuming DashboardView exists
case .settings:
SettingsView() // Assuming SettingsView exists
case .itemDetails(let itemId):
ItemDetailView(itemId: itemId) // Assuming ItemDetailView exists
case .helpSheet:
HelpView() // Assuming HelpView exists
case .customTransitionView:
MyCustomAnimatedView() // Assuming MyCustomAnimatedView exists
}
}
}
By defining routes this way, SUICoordinator
can manage the presentation and lifecycle of your views in a type-safe and structured manner. The SCHashable
conformance allows routes to be used in navigation stacks and for SwiftUI to differentiate between them.
You can also use DefaultRoute
for generic views if you don't need a specific enum for routes, as demonstrated in the TabFlowCoordinator
example.
The Router
(a property on every Coordinator
instance, e.g., coordinator.router
) is responsible for managing the navigation stack and modal presentations for that specific coordinator. It abstracts navigation details, allowing views and ViewModels to request navigation changes without knowing the underlying SwiftUI mechanisms.
Name | Parameters | Description |
---|---|---|
navigate(toRoute:presentationStyle:animated:) |
|
Navigates to the given route. If the effective presentation style is .push , it pushes the view onto this router's navigation stack. Otherwise, it presents the view modally using this router's sheetCoordinator . |
present(_:presentationStyle:animated:) |
|
Presents a view modally (e.g., sheet, fullScreenCover, detents) using this router's sheetCoordinator . If the presentation style is .push , it delegates to navigate(toRoute:) . |
pop(animated:) |
|
Pops the top view from this router's navigation stack. |
popToRoot(animated:) |
|
Pops all views on this router's navigation stack except its root view. |
dismiss(animated:) |
|
Dismisses the top-most modally presented view (sheet, fullScreenCover, etc.) managed by this router's sheetCoordinator . |
close(animated:finishFlow:) |
|
Intelligently closes the top-most view: dismisses a sheet if one is presented by this router's sheetCoordinator , otherwise pops a view from this router's navigation stack. |
The Coordinator
is the brain for a specific navigation flow or feature. You subclass Coordinator<YourRouteType>
to define navigation methods specific to that flow.
Name | Parameters | Description |
---|---|---|
router |
N/A (Property) | Instance of Router specific to this coordinator. Use this for all navigation operations within this coordinator's flow (e.g., router.navigate(toRoute:) , router.pop() , router.dismiss() ). |
start(animated:) |
|
**Must be overridden by subclasses.** This is where you define the initial view or flow for the coordinator, typically by calling await startFlow(route:animated:) . |
startFlow(route:transitionStyle:animated:) |
|
Clears this coordinator's current navigation stack and any sheets it presented, then starts a new flow with the given route. This is essential for initializing or resetting the coordinator's view hierarchy. |
finishFlow(animated:) |
|
Dismisses all views (pushed or presented) managed by *this* coordinator and removes *this* coordinator from its parent coordinator's children list. Effectively ends this coordinator's lifecycle and its associated UI. |
forcePresentation(presentationStyle:animated:mainCoordinator:) |
|
Forcefully presents *this* coordinator, even if other coordinators or views are active. Useful for handling deep links or push notifications. It will call this coordinator's start() method. |
navigate(to:presentationStyle:animated:) |
|
Navigates from *this* coordinator to *another* coordinator. It adds the target coordinator as a child, sets its parent, and calls the target coordinator's start() method. The presentation style determines how the new coordinator's view is shown (e.g., pushed onto this coordinator's stack, or presented as a sheet by this coordinator). |
restart(animated:) |
|
Resets the coordinator's navigation state by calling router.restart() . This clears all navigation stacks and modal presentations managed by this coordinator. All navigation history will be lost, modal presentations dismissed, and the coordinator returns to its initial state as if start() was just called (depending on how start() and startFlow() are implemented). Useful for logout scenarios or major state changes. |
Name | Parameters / Type | Description |
---|---|---|
router |
Router (Property) |
The router for the TabCoordinator itself (e.g., for how it's presented or if it needs to present something over the tabs). Not for navigation within individual tabs. |
pages |
@Published var pages: [Page] (Property) |
The array of TabPage enums that define the tabs. Modifying this will update the tab bar. |
currentPage |
@Published var currentPage: Page (Property) |
Get or set the currently selected TabPage . Changing this programmatically will switch the active tab. |
setPages(_:currentPage:) |
|
Asynchronously updates the set of pages (tabs) dynamically. Child coordinators for new pages are initialized, and old ones are cleaned up. |
getCoordinatorSelected() |
Returns any CoordinatorType (Throws) |
Returns the child CoordinatorType instance that is currently active/selected based on currentPage . Throws TabCoordinatorError.coordinatorSelected if not found. |
getCoordinator(with:) |
AnyCoordinatorType?
|
Returns the child CoordinatorType instance at the given numerical position (index) in the tabs. |
setBadge |
PassthroughSubject<(String?, Page), Never> (Property) |
A Combine subject to set or remove a badge on a tab. Send a tuple: ("badgeText", .yourTabPage) to set, or (nil, .yourTabPage) to remove the badge. The TabViewCoordinator and example CustomTabView handle displaying these. |
viewContainer |
(TabCoordinator) -> Page.View (Property) |
A closure that you provide during initialization. It returns the SwiftUI view for the tab bar interface itself (e.g., `TabViewCoordinator` or your `CustomTabView`). |
Key Steps:
- Instantiate Target Coordinator: Create an instance of the coordinator you want to present (e.g.,
MainTabCoordinator
). forcePresentation
: Call this on the instantiated coordinator, passing your app's current root/main coordinator. This makes the target coordinator active.- Set State (if needed): For a
TabCoordinator
, updatecurrentPage
to the desired tab. - Get Child Coordinator: Use
getCoordinatorSelected()
if navigating within a tab. - Navigate in Child: Call relevant navigation methods on the child coordinator.
This approach ensures that the navigation hierarchy is correctly established before attempting to navigate to the final destination screen.
Contributions to the SUICoordinator library are welcome! To contribute, simply fork this repository and make your changes in a new branch. When your changes are ready, submit a pull request to this repository for review.