Artisan

Artisan 是一个 Swift 的 MVVM 框架,它使用了来自 Pharos 的绑定特性,来自 Draftsman 的约束构建器,以及来自 Builder 的构建器模式。

codebeat badge build test SwiftPM Compatible Version License Platform

示例

要运行示例项目,请克隆仓库,并首先从 Example 目录运行 pod install

要求

安装

Cocoapods

Artisan 可以通过 CocoaPods 获得。 要安装它,只需将以下行添加到您的 Podfile 中

pod 'Artisan', '~> 5.1.0'

通过 XCode 使用 Swift Package Manager

通过 Package.swift 使用 Swift Package Manager

Package.swift 中添加它作为您的 target 依赖

dependencies: [
    .package(url: "https://github.com/hainayanda/Artisan.git", .upToNextMajor(from: "5.1.0"))
]

在您的 target 中使用它,名称为 Artisan

 .target(
    name: "MyModule",
    dependencies: ["Artisan"]
)

作者

Nayanda Haberty, hainayanda@outlook.com

许可证

Artisan 在 MIT 许可证下可用。 有关更多信息,请参见 LICENSE 文件。

用法

阅读 wiki 以获取更详细的信息。

基本用法

使用 Artisan 创建 MVVM 模式很容易。绑定由 Pharos 支持,视图构建由 Draftsman 支持,Artisan 使两者可以完美地协同工作。比如,你想创建一个简单的搜索屏幕

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
    ...
    ...
    ...
    
}

使用像这样的ViewModel协议

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

它将使用 Draftsman 创建一个 View,并使用 PharosModel 绑定到 View。 正如您从上面的代码中看到的那样,它会将 searchBar.bindables.textModel 中的 searchPhraseBindable 绑定,并将 resultsObservable 中的更改转发到 allResults。 这将确保来自 searchBar 的每个更改都将转发到 Model,然后来自 Model 的每个结果更改都将转发回 View。 然后,这些结果将被 UITableView 内置数据源(由 Artisan 提供并由 DiffableDataSource 提供支持)观察,用于更新 UITableView 中的单元格。

您可以像这样创建您的 ViewModel

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
        }
    }
}

然后绑定 ViewModel 将像这样容易

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

不要忘记,每次 BindableView 与新的 ViewModel 绑定时,其所有旧的保留的 Pharos relay 都将被释放。

您可以克隆并查看 Example folder,或者要获得更多 wiki,请访问 这里

贡献

你知道怎么做,只需克隆并提交 pull request