iOS(Swift) 我的MVP

我的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等,根据需要拓展即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352