我的MVP,一切的行为,事件起点是P,P接管了V的生命周期,V的响应,ViewController不具备任何逻辑的功能,
我的架构,具备了数据与视图的双向绑定功能,从P可以连接起V与M之前的联系,但是,我个人更喜欢数据的单向流动。
以下是一个简单的日常完善资料业务逻辑:
代码文件结构
Page
原先的ViewController,在我的设计中,它只是一个view
class EditInfoPage: BaseScrollPage {
// MARK: - ----------------------------------info
private lazy var c: EditInfoLogic = EditInfoLogic(self)
private var state: EditInfoState { c.state }
override var preferredStatusBarStyle: UIStatusBarStyle {
.default
}
// MARK: - ----------------------------------system
override func viewDidLoad() {
super.viewDidLoad()
title = "mine_edit_info_title".lan
c.onViewDidLoad()
setBackButton(isLight: false)
setRightItem(image: "user_edit_save") { [weak self] in
self?.c.submit()
}
}
override func setupScroll() {
super.setupScroll()
contentView.flex.column.alignItems(.stretch).define {
$0.addItem(vwHead)
$0.addItem(vwInfo)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
// MARK: - ----------------------------------action
func update() {
updateHead()
vwInfo.update(state)
// vwInfo.update(state)
}
func updateHead() {
vwHead.update(state)
}
func updateAlbum() {
vwHead.update(state)
}
// MARK: - ----------------------------------ui
private let vwHead: EditInfoHeadView = EditInfoHeadView()
private let vwInfo: EditInfoDetailView = EditInfoDetailView()
}
Logic
Presenter层,业务的逻辑处理层,通过Rx接管了ViewController的生命周期,并且接管了ViewController的响应链(子View或者cell的响应链会传递到该层)
下面是简单的接管逻辑(这是BaseLogic中的一小段逻辑)
public init(_ page: T? = nil, identify: String? = nil) {
super.init()
self.page = page
self._identify = identify
// controller才能触发
if let page = page as? UIViewController {
// let viewWillAppear = #selector(UIViewController.viewWillAppear(_:))
page.rx.methodInvoked(#selector(UIViewController.viewDidLoad)).subscribe { [weak self] _ in
self?.onViewDidLoad()
}.disposed(by: page.disposeBag)
page.rx.methodInvoked( #selector(UIViewController.viewWillAppear(_:))).subscribe { [weak self] _ in
self?.onViewWillAppear()
}.disposed(by: page.disposeBag)
page.rx.methodInvoked( #selector(UIViewController.viewDidAppear(_:))).subscribe { [weak self] _ in
self?.onViewDidAppear()
}.disposed(by: page.disposeBag)
page.rx.methodInvoked( #selector(UIViewController.viewWillDisappear(_:))).subscribe { [weak self] _ in
self?.onViewWillDisappear()
}.disposed(by: page.disposeBag)
page.rx.methodInvoked( #selector(UIViewController.viewDidDisappear(_:))).subscribe { [weak self] _ in
self?.onViewDidDisappear()
}.disposed(by: page.disposeBag)
}
if takeoverDesignatedResponder {
(page as? Responder)?.designatedResponder = self
// 自己的响应选择page的NextResponder, 直接跳过page响应
self.designatedResponder = (page as? Responder)?.nextResponder
}
onInit()
}
这些展示一小段逻辑
class EditInfoLogic: BaseLogic<EditInfoPage> {
// MARK: - ----------------------------------info
let state = EditInfoState()
override var logLifecycle: Bool { true }
override var takeoverDesignatedResponder: Bool {
true
}
// MARK: - ----------------------------------system
override func onViewDidLoad() {
super.onViewDidLoad()
requestData()
}
func requestData() {
Observable.merge([requestUserInfo(), requestAlbumInfo(), requestLanguageInfo()])
.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
self.page?.update()
}).disposed(by: self.disposeBag)
}
/// 请求个人信息
private func requestUserInfo() -> Observable<Void> {
NetWorking.request(api: UserInfoApi.checkUserInfo()).do(onNext: { [weak self] res in
guard let self = self else { return }
guard let userInfo = res.unwrap(type: UserInfoModel.self) else { return }
self.state.userInfo = userInfo
}).map({ _ in () })
}
/// 请求相册信息
private func requestAlbumInfo() -> Observable<Void> {
EditInfoAPI.getUserPhotos()
.make()
.do(onSuccess: { [weak self] res in
guard let self = self else { return }
var arr: [EditInfoViewModel] = (res.unwrapList(type: EditAlbumModel.self) ?? []).map({ EditInfoViewModel(model: $0) })
if arr.count < 6 {
// 补充到6个
arr += (arr.count..<6).map { _ in EditInfoViewModel() }
}
self.state.album = arr
}).map({ _ in () }).asObservable()
}
/// 请求语言信息
private func requestLanguageInfo() -> Observable<Void> {
NetWorking.request(api: UserInfoApi.getLanguage(userId: InfoDataManager.user_id)).do(onNext: { [weak self] res in
guard let self = self else { return }
guard let model = res.unwrap(type: LanguargeResultModel.self) else { return }
self.state.languages = model.zsa_UserMultiLanguage
}).map({ _ in () })
}
// MARK: - ----------------------------------action
// MARK: 头像修改
@Respondable private var iconTap = EditInfoLogic.iconTap
func iconTap(_ param: ResponderParam?) {
selectImgFromPhoto()
}
func selectImgFromPhoto() {
AlertHelper.showSheet()
.addAction(title: "take_camera".lan, style: .default, handler: { [weak self] _ in
guard let self = self else { return }
self._doShowCamera()
})
.addAction(title: "take_photo".lan, style: .default, handler: { [weak self] _ in
guard let self = self else { return }
self._doShowPhoto()
})
.addAction(title: "cancel".lan, style: .cancel)
.show(from: self.page)
}
private func _doShowCamera() {
ImagePicker.showCamera(
config: CameraConfig(),
from: self.page,
completion: { [weak self] imgs, _ in
guard let img = imgs?.first else { return }
self?.state.headAsset.image = img
self?.state.headAsset.uploadUrl = nil
self?.page?.updateHead()
})
}
private func _doShowPhoto() {
ImagePicker.showPhoto(
config: ImageConfig(allowTakePhoto: false,
allowPickVideo: false,
allowPickGif: false,
maxCount: 1,
singleSelected: true),
from: self.page,
completion: { [weak self] imgs, assets in
guard let img = imgs?.first else { return }
self?.state.headAsset.image = img
self?.state.headAsset.uploadUrl = nil
self?.page?.updateHead()
})
}
// MARK: 昵称修改
@Respondable private var nickNameChange = EditInfoLogic.nickNameChange
func nickNameChange(_ param: ResponderParam?) {
guard let text = param?["v"] as? String else { return }
PPLog("\(text)")
state.nickname = text
if state.nickname == "" {
Hud.showText("昵称不能为空")
}
}
// MARK: 相册修改
@Respondable private var addImageAction = EditInfoLogic.addImageAction
func addImageAction(_ param: ResponderParam?) {
ImagePicker.showPhoto(
config: ImageConfig(allowTakePhoto: false,
allowPickVideo: false,
allowPickGif: false,
maxCount: 1,
singleSelected: true),
from: UIViewController.currentViewController(),
completion: { [weak self] imgs, assets in
guard let self = self,
let img = imgs?.first else { return }
// 选中图片后走上传图片,上传图片成功后,展示图片
UploadFileManager.upload(
data: img.pngData(),
serverPath: .userPhoto,
fileType: .image
)
.asObservable()
.flatMap({ url in
EditInfoAPI.postUserPhotos(url: url).make()
})
.observe(on: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self] m in
guard let self = self else { return }
Hud.showSuccess("上传成功")
self.requestAlbumInfo().subscribe { [weak self] _ in
self?.page?.updateAlbum()
}.disposed(by: self.disposeBag)
})
.disposed(by: self.disposeBag)
})
}
// MARK: 相册删除
@Respondable private var deleteImageAction = EditInfoLogic.deleteImageAction
func deleteImageAction(_ param: ResponderParam?) {
guard let viewModel = param?["model"] as? EditInfoViewModel else { return }
EditInfoAPI
.deleteUserPhotos(photoId: viewModel.model?.zsa_Id)
.make()
.subscribe(onSuccess: { [weak self] m in
guard let self = self else { return }
self.requestAlbumInfo().subscribe { [weak self] _ in
self?.page?.updateAlbum()
}.disposed(by: self.disposeBag)
}).disposed(by: self.disposeBag)
}
// MARK: 跳转语言选择
@Respondable private var goLanguageSelect = EditInfoLogic.goLanguageSelect
func goLanguageSelect(_ param: ResponderParam?) {
Router.to(name: .user_language_select)
}
// MARK: 提交资料
func submit() {
// 判断权限
guard _permissCheck() else {
return
}
// 图片
var observable: Observable<Void>
// 头像上传
if let img = state.headAsset.image,
state.headAsset.uploadUrl == nil {
// 选择了图片,单么有上传
observable = UploadFileManager
.upload(
data: img.pngData(),
serverPath: .userHead,
fileType: .image
)
.do(onSuccess: { [weak self] path in
self?.state.headAsset.uploadUrl = path
}).asObservable().map({ _ in () })
} else {
observable = .just(())
}
observable
.flatMap ({ [weak self] _ -> Observable<MainModel> in
guard let self = self else { return .empty() }
var param: [String: Any] = [:]
if let nickname = self.state.nickname {
param["zsa_nickname"] = nickname
}
if let headUrl = self.state.headAsset.uploadUrl {
param["zsa_headUrl"] = headUrl
}
return NetWorking.request(api: UserAPI.postUserInfo(param: param))
})
.observe(on: MainScheduler.asyncInstance)
.do(onNext: { _ in
Hud.showSuccess("修改成功")
})
.subscribe(onNext: {res in
Router.popToRoot()
}).disposed(by: self.disposeBag)
}
func _permissCheck() -> Bool {
var canSubmit = true
if let nickname = state.nickname {
if nickname.count == 0 {
// 昵称为空时不允许提交
canSubmit = false
Hud.showError("昵称不能为空")
}
}
// 无修改时不允许提交
if state.nickname == nil && state.headAsset.image == nil {
canSubmit = false
}
return canSubmit
}
}
State
状态层,由Logic持有,这里是存储数据的区域,所有的数据都应该存储这里,起名为State,意思是动态监听的Rx属性也是放在这里的
class EditInfoState {
/// 个人信息
var userInfo: UserInfoModel?
/// 相册
var album: [EditInfoViewModel] = []
/// 语言
var languages: [LanguageItemModel] = []
/// 头像选择
var headAsset: UploadAsset = UploadAsset()
/// 昵称
var nickname: String?
}
子View
class EditInfoHeadView: BaseFlexView {
// MARK: - ----------------------------------info
// MARK: - ----------------------------------system
override func commonInit() {
super.commonInit()
rootFlex.alignItems(.center).padding(24, 20, 32, 20).define {
// 头像
$0.addItem(iconWrapper).size(MakeSize(80, 80)).define {
// 头像
$0.addItem(imgIcon).position(.absolute).all(0)
// 角标
$0.addItem(imgIconEdit).position(.absolute).right(0).bottom(0).size(MakeSize(24, 24))
}
// 相册
$0.addItem(vwAlbum).marginTop(24).width(325).height(274)
}
}
// MARK: - ----------------------------------action
override func allEvents() {
super.allEvents()
iconWrapper.rx.tapGesture().subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
self.dispatch(event: "iconTap")
}).disposed(by: self.disposeBag)
}
func update(_ state: EditInfoState) {
if let img = state.headAsset.image {
imgIcon.image = img
} else if let url = state.userInfo?.zsa_HeadUrl {
imgIcon.load(source: url)
}
vwAlbum.update(state)
}
// MARK: - ----------------------------------ui
private let iconWrapper: UIView = UIView()
private let imgIcon: UIImageView = UIImageView.image(name: "placeHolder").then {
$0.contentMode = .scaleAspectFill
$0.layer.cornerRadius = 40
$0.layer.masksToBounds = true
}
private let imgIconEdit: UIImageView = UIImageView.image(name: "user_camara")
private let vwAlbum: EditInfoAlbumView = EditInfoAlbumView(config: EditInfoConfig())
}
举例说明数据的流向
比如view的一个删除相册的点击事件
- 1 在某个子view中点击删除按钮
// 删除
btnDelete.rx.tap.subscribe { [weak self] _ in
guard let self = self else { return }
self.dispatch(event: "deleteImageAction", param: ["model": self.model])
}.disposed(by: self.disposeBag)
这里dispatch,以响应链的模式,寻找能够接收deleteImageAction的响应层,此处解耦view与Logic
- 2 Logic处理逻辑,响应链接受事件,调用接口数据,完成后,调用page的刷新页面(相册删除是后端的特殊逻辑,不需要保存数据,直接请求接口刷新),而page就是viewController,通过weak引用持有,我设计的page与logic之间绝大部份情况下是耦合的,如果真需要page与logic解耦,再加一层PageProtocol,让ViewController遵循即可,当然,一般只有Logic需要适配多种Page时才需要用到protocol层。
@Respondable private var deleteImageAction = EditInfoLogic.deleteImageAction
func deleteImageAction(_ param: ResponderParam?) {
guard let viewModel = param?["model"] as? EditInfoViewModel else { return }
EditInfoAPI
.deleteUserPhotos(photoId: viewModel.model?.zsa_Id)
.make()
.subscribe(onSuccess: { [weak self] m in
guard let self = self else { return }
self.requestAlbumInfo().subscribe { [weak self] _ in
self?.page?.updateAlbum()
}.disposed(by: self.disposeBag)
}).disposed(by: self.disposeBag)
}
- 3 page刷新页面,传递state的数据让子视图去刷新
func updateAlbum() {
vwHead.update(state)
}
所以,处理的逻辑是子视图->logic->page->子视图,数据是单向流动的,子视图不要去修改state的数据,处理逻辑永远都在Logic中,请相信,当你彻彻底底分离出Logic,State,View这三层,不要去抢占任何一方的权利和义务,你的代码逻辑会非常非常清晰,至于Logic的代码多起来后,可以使用Logic+Album,Logic+Language,Logic+Nickname等等,用文件分离即可,再如果,你需要的逻辑非常非常庞大,可以分化Logic,分离出多个Logic去管理同一个页面。比如CallLogic,GameLogic,SheetLogic等,根据需要拓展即可。