Artisan is an MVVM framework for Swift using the bonding features from Pharos and constraints builder from Draftsman.

What's New


  • Since some projects will have different Quick and Nimble versions, which shouldn't include in the distribution, it is now removed from dependency in the Swift Package Manager version.
  • Separate binding retaining with a regular one
  • Add bind to plan to simplified binding to applyPlan
  • Add auto binding to simplified binding
  • Removed and modified some UIControl action because its already provided by Pharos


Artisan is an MVVM framework for Swift using the bonding features from Pharos, constraints builder from Draftsman and builder pattern from Builder.

codebeat badge build test SwiftPM Compatible Version License Platform


To run the example project, clone the repo, and run pod install from the Example directory first.


  • Swift 5.3 or higher
  • iOS 10.0 or higher
  • XCode 12.5 or higher



Artisan is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'Artisan', '~> 5.1.0'

Swift Package Manager from XCode

  • Set rules at version, with Up to Next Major option and put 5.1.0 as its version
  • Add it using XCode menu File > Swift Package > Add Package Dependency
  • Add as Swift Package URL
  • Click next and wait

Swift Package Manager from Package.swift

Add as your target dependency in Package.swift

dependencies: [
    .package(url: "", .upToNextMajor(from: "5.1.0"))

Use it in your target as Artisan

    name: "MyModule",
    dependencies: ["Artisan"]


Nayanda Haberty,


Artisan is available under the MIT license. See the LICENSE file for more info.


Read wiki for more detailed information.

Basic Usage

Creating an MVVM Pattern using Artisan is easy. Binding is supported by Pharos and View building is supported by Draftsman, Artisan is the one that make both can work with each other perfectly. Like if you want to create simple Search Screen:

import UIKit
import Artisan
import Draftsman
import Builder
import Pharos

class SearchScreen: UIPlannedController, ViewBindable {
    typealias Model = SearchScreenViewModel
    @Subject var allResults: [Result] = []
    // MARK: View
    lazy var searchBar: UISearchBar = builder(UISearchBar.self)
        .placeholder("Search here!")
    lazy var tableView: UITableView = builder(UITableView.self)
    var viewPlan: ViewPlan {
            .edges.equal(with: .parent)
            .cells(from: $allResults) { _, result in
                Cell(from: ResultCell.self) { cell, _ in
    override func viewDidLoad() {
        view.backgroundColor = .background
        tableView.keyboardDismissMode = .onDrag
    override func viewWillAppear(_ animated: Bool) {
        navigationController?.navigationBar.tintColor = .main
        navigationItem.titleView = searchBar
    // MARK: This is where View Model bind with View
    func autoBinding(with model: Model) -> BindRetainables {
            .bind(with: searchBar.bindables.text)
    func autoFireBinding(with model: Model) -> BindRetainables {
            .relayChanges(to: $allResults)
    // more code for UITableViewDelegate and UISearchbarDelegate below

with ViewModel protocol like this:

protocol SearchScreenDataBinding {
    var searchPhraseBindable: BindableObservable<String?> { get }
    var resultsObservable: Observable<[Result]> { get }

protocol SearchScreenSubscriber {
    func didTap(_ event: Result, at indexPath: IndexPath)

typealias SearchScreenViewModel = ViewModel & SearchScreenSubscriber & SearchScreenDataBinding

It will create a View using Draftsman and bind Model to View using Pharos. As you can see from the code above, it will bind searchBar.bindables.text with searchPhraseBindable from Model relay changes from resultsObservable to allResults. This then will make sure that every changes coming from searchBar will be relayed to the Model and then every results changes from Model will be relayed back to the View. The results then will be observed by UITableView built-in datasource (provided by Artisan and powered by DiffableDataSource) for then used to update a cells in the UITableView.

You can create your ViewModel like this:

import UIKit
import Artisan
import Pharos
import Impose

// MARK: ViewModel

class SearchScreenVM: SearchScreenViewModel, ObjectRetainer {
    @Injected var service: EventService
    let router: SearchRouting
    @Subject var searchPhrase: String?
    @Subject var results: [Result] = []
    // MARK: Data Binding
    var searchPhraseBindable: BindableObservable<String?> { $searchPhrase }
    var resultsObservable: Observable<[Result]> { $results }
    init(router: SearchRouting) {
        self.router = router
            .whenDidSet(thenDo: method(of: self,
            .multipleSetDelayed(by: 1)
            .retained(by: self)

// MARK: Subscriber

extension EventSearchScreenVM {
    func didTap(_ history: HistoryResult, at indexPath: IndexPath) {
        searchPhrase = history.distinctifier as? String
    func didTap(_ event: EventResult, at indexPath: IndexPath) {
        guard let tappedEvent = event.distinctifier as? Event else { return }
        router.routeToDetails(of: tappedEvent)

// MARK: Extensions

extension EventSearchScreenVM {
    func search(for changes: Changes<String?>) {
        service.doSearch(withSearchPhrase: ?? "") { [weak self] results in
            self?.results = results

Then binding the View and Model will be as easy like this:

let searchScreen = SearchScreen()
let searchRouter = SearchRouter(screen: searchScreen)
let searchScreenVM = SearchScreenVM(router: searchRouter)
searchScreen.bind(with: searchScreenVM)

Don't forget that everytime BindableView is bind with new ViewModel, all of its old retained Pharos relay will be released.

You can clone and check the Example folder or for more wiki, go to here


You know how, just clone and do pull request


  • Swift Tools 5.3.0
View More Packages from this Author


Last updated: Thu Apr 18 2024 20:44:09 GMT-0900 (Hawaii-Aleutian Daylight Time)