Artisan 5.1.0

Artisan 5.1.0

Maintained by nayanda1.



 
Depends on:
Draftsman~> 3.0.6
Pharos~> 2.3.5
DiffableDataSources~> 0.5.0
 

Artisan 5.1.0

  • By
  • nayanda

Artisan

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

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 & 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
        $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