老习惯了的先展示下结果吧:
原版 | 仿写1 | 仿写2 |
---|---|---|
# 补充文章
薅了这么久沸点的羊毛,是时候结束了🥶
# 技术路线
# 三方库选择:
Texture
Moya
RxSwift
Kingfisher
Lantern
: 照片浏览器Toast-Swift
: toast 提示- etc...
# 整体架构
使用 Texture
进行的界面元素搭建,RxSwift
实现的 VM
数据流驱动 View
更新,Moya + Rx
的网络请求。
# Texture
对于 Texture
的使用其实上手难度不如我预想的大,官方的大量例子,和一些优秀的技术博客都能让我们很好地接入 Texture
。
布局:可以通过在网页(ASDK🐸)上以玩乐的形式上手 FlexBox
布局。
原理与使用:
这里看一个布局点赞用户头像列表的布局吧:
// Title
private lazy var titleNode: ASTextNode = {
let node = ASTextNode()
let attr: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 12, weight: .regular),
.foregroundColor: UIColor.XiTu.digCount
]
node.attributedText = NSAttributedString(string: "等人赞过", attributes: attr)
return node
}()
// 布局
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let imageHeihgt = constrainedSize.min.height - 1
imageNode1.style.preferredSize = CGSize(width: imageHeihgt, height: imageHeihgt)
imageNode2.style.preferredSize = CGSize(width: imageHeihgt, height: imageHeihgt)
imageNode3.style.preferredSize = CGSize(width: imageHeihgt, height: imageHeihgt)
imageNode1.cornerRadius = imageHeihgt / 2
imageNode2.cornerRadius = imageHeihgt / 2
imageNode3.cornerRadius = imageHeihgt / 2
let overlayWidth: CGFloat = 5
let inset1 = ASInsetLayoutSpec(insets: .only(.right, value: 2 * (imageHeihgt - overlayWidth)), child: imageNode1)
let inset2 = ASInsetLayoutSpec(insets: .only(.horizontal, value: imageHeihgt - overlayWidth), child: imageNode2)
let inset3 = ASInsetLayoutSpec(insets: .only(.left, value:2 * (imageHeihgt - overlayWidth) ), child: imageNode3)
let overlay1 = ASOverlayLayoutSpec(child: inset1, overlay: inset2)
let overlay = ASOverlayLayoutSpec(child: overlay1, overlay: inset3)
let hStack = ASStackLayoutSpec.horizontal()
hStack.spacing = 5
hStack.alignItems = .center
hStack.children = [overlay, titleNode]
return ASInsetLayoutSpec(insets: .only(.left, value: 7), child: hStack)
}
# MVVM
与 RxSwift
Demo 中并没有使用 双向数据绑定,未对 RxSwift
进行运算符重载,也未使用上次的 RxTableViewDataSource
。整体代码可读性应该会有所提高吧,适合 RxSwift
入门学习。
ViewModel: VM
层使用协议区分了 input
和 output
,接口隔离:
protocol DynamicListViewModelInputs {
func viewDidLoad()
func refreshDate()
func moreData(with cursor: String)
// FIXED: - 以下 view 层接口需要根据产品的逻辑而定. 例如: 是否要处理一部分埋点之类的额外操作
/// 查看详情
func showDetail()
/// 查看点赞用户
func diggUserClick()
}
protocol DynamicListViewModelOutputs {
//var willRefreshData: Observable<Void> { get }
var refreshData: Observable<XTListResultModel> { get }
var moreData: Observable<XTListResultModel> { get }
var endRefresh: Observable<Void> { get }
var hasMoreData: Observable<Bool> { get }
var showError: Observable<String> { get }
}
protocol DynamicListViewModelType {
var input: DynamicListViewModelInputs { get }
var output: DynamicListViewModelOutputs { get }
}
final class DynamicListViewModel: DynamicListViewModelType, DynamicListViewModelInputs, DynamicListViewModelOutputs {
var input: DynamicListViewModelInputs { self }
var output: DynamicListViewModelOutputs { self }
// ect...
}
View
(VC
) 中的使用:
extension DynamicListViewController {
func eventListen() {
self.mjHeader.refreshingBlock = { [unowned self] in
self.viewModel.input.refreshDate()
}
self.mjFooter.refreshingBlock = { [unowned self] in
self.viewModel.input.moreData(with: self.dataSource.nextCursor)
}
}
}
// MARK: - 绑定 viewModel
extension DynamicListViewController {
func bindViewModel() {
viewModel.output.refreshData.subscribe(onNext: { [weak self] wrappedModel in
self?.dataSource.newData(from: wrappedModel)
self?.tableNode.reloadData()
}).disposed(by: disposeBag)
viewModel.output.moreData.subscribe(onNext: { [weak self] wrappedModel in
if let insertIndexPath = self?.dataSource.moreData(from: wrappedModel), !insertIndexPath.isEmpty {
self?.tableNode.insertRows(at: insertIndexPath, with: UITableView.RowAnimation.automatic)
}
}).disposed(by: disposeBag)
// etc...
}
}
DataSource:对 ASTableNode
(UITableView
) 拆分出了 DataSource
来存储和处理 Model
、遵守 ASTableDataSource
协议,将 VM
层的职责进一步的细分,便于维护。
这里就说这么多吧,更多的是在 cellNode
与 cellNodeModel
里面的逻辑处理,和此类似。
备注:最近被大佬教育过不敢自称是 MVVM
架构,但数据处理层仍使用了 XxxNodeMode
/ XxxViewModel
的名称。
Model: 直接使用 struct
和 Codable
进行 JSON
解析。注意,如果使用 UITableView
实现类似的界面效果可能需要在 model
中补充额外的存储属性和 Decoder
逻辑代码。
# Moya
掘金里相关技术文章也不少,这里不做说明了。
# 相关知识点
# ASImageNode + Kingfrisher
对于 ASImageNode
使用第三方图片库其实并不困难,特别是对于面向协议封装的 Kingfirsh
,这里简单的扩展一下吧:
import Kingfisher
import AsyncDisplayKit
extension ASImageNode: KingfisherCompatible {}
public extension KingfisherWrapper where Base: ASImageNode {
func setImage(
with source: Resource?,
placeholder: UIImage? = nil,
failureImage: UIImage? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: DownloadProgressBlock? = nil)
{
guard let source = source else {
self.base.image = placeholder ?? failureImage
return
}
KingfisherManager.shared.retrieveImage(with: source, options: options, progressBlock: progressBlock, downloadTaskUpdated: nil) { result in
switch result {
case .success(let retrieveResult):
self.base.image = retrieveResult.image
case .failure(_):
self.base.image = failureImage ?? placeholder
}
}
}
}
备注:老久没看 SDWebImage
的源码了,最近才发现 SDWebImage
内部也开始使用 协议 加 实现协议的默认类 重构了,是我太久没关注的原因吗😅?
# 其他
- 使
ASControl
支持RxSwift
- 正则(
regx
)表达式在Swift
中的使用 - 命名空间的创建
- etc...
具体见源码吧。
# 关于源码
源码中已经将接口进行了移除,如果您有接口抓取能力(太简单的操作了)请在
-
SceneDelegate.swift
中如下代码将rootVC
更换为HomeTabBarController
:func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let _ = (scene as? UIWindowScene) else { return } guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow.init(frame: windowScene.coordinateSpace.bounds) window?.windowScene = windowScene //let rootVC = HomeTabBarController() //let rootVC = UINavigationController(rootViewController: SimpleRegxViewController()) let rootVC = UINavigationController(rootViewController: TextureDemoViewController()) window?.rootViewController = rootVC window?.makeKeyAndVisible() UIWindow.setupLayoutFitInfo() }
-
XTNetworkService.swift
中将jjBaseUrl
和var path{xxx}
替换到对应的接口。
如果仅是学习 Texture
布局等,源码中也附带了 xxx.json
文件供您本地使用(直接运行就好了)。
# 絮叨
2020年掘金的iOS客户端界面还在使用 Texture
,其中的沸点页面令初见 Texture
的我大吃一惊,也是从此开始接触并使用 Texture
,其和 CSS
相似的 FlexBox
布局思想令人着迷。如今掘金的iOS版本经历了大的重构,去除了很多第三方库,这其中就包括 Texture
,常用App中使用 Texture
的厂家又少了一个😞。这里也希望掘金iOS团队能够分享一下 Texture
使用过程中的利弊吧。