Artisan
Artisan is an MVVM framework for Swift using the bonding features from Pharos, constraints builder from Draftsman and builder pattern from Builder.
Example
To run the example project, clone the repo, and run pod install from the Example directory first.
Requirements
- Swift 5.3 or higher
 - iOS 10.0 or higher
 - XCode 12.5 or higher
 
Installation
Cocoapods
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 https://github.com/hainayanda/Artisan.git 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: "https://github.com/hainayanda/Artisan.git", .upToNextMajor(from: "5.1.0"))
]Use it in your target as Artisan
 .target(
    name: "MyModule",
    dependencies: ["Artisan"]
)Author
Nayanda Haberty, [email protected]
License
Artisan is available under the MIT license. See the LICENSE file for more info.
Usage
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!")
        .sizeToFit()
        .tintColor(.text)
        .barTintColor(.background)
        .delegate(self)
        .build()
    
    lazy var tableView: UITableView = builder(UITableView.self)
        .backgroundColor(.clear)
        .separatorStyle(.none)
        .allowsSelection(true)
        .delegate(self)
        .build()
    
    @LayoutPlan
    var viewPlan: ViewPlan {
        tableView.drf
            .edges.equal(with: .parent)
            .cells(from: $allResults) { _, result in
                Cell(from: ResultCell.self) { cell, _ in
                    cell.apply(result)
                }
            }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .background
        tableView.keyboardDismissMode = .onDrag
        applyPlan()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.navigationBar.tintColor = .main
        navigationItem.titleView = searchBar
    }
    
    // MARK: This is where View Model bind with View
    
    @BindBuilder
    func autoBinding(with model: Model) -> BindRetainables {
        model.searchPhraseBindable
            .bind(with: searchBar.bindables.text)
    }
    
    @BindBuilder
    func autoFireBinding(with model: Model) -> BindRetainables {
        model.resultsObservable
            .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 & SearchScreenDataBindingIt 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
        $searchPhrase
            .whenDidSet(thenDo: method(of: self, SearchScreenVM.search(for:)))
            .multipleSetDelayed(by: 1)
            .retained(by: self)
            .fire()
        
    }
}
// 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: changes.new ?? "") { [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
Contribute
You know how, just clone and do pull request
