这是我参与更文挑战的第28天,活动详情查看: 更文挑战
接受意见
很多朋友都说我是标题党,其实我本人也想这样,可能是自己语文不好导致,于是向掘金群的大佬讨教一个标题,准备都改了。以《Swift 开发 wanandroid 客户端——XXXX》开头。
状态管理
其实说到状态管理的时候,这个概念在前端特别的熟悉,比如Vue之于Vuex,React之于Redux,Flutter之于Provider,这些都是在通过在全局配置一个Store,对于全局可能需要使用的数据进行配置,并且由于是MVVM的设计模式,当Store中的数据改变时,会主动去通知相关绑定页面,数据改变啦,UI也随之改变。
上面都是说的非iOS开发的状态管理,当然随着SwiftUI的崛起,这种思维模式会做到大统一,目前SwiftUI中就有SwiftUIFlux这个框架在做这个事情。
其实伴随着使用RxSwift,在RxSwift中也有介绍其常用架构,这里是中文官方文档中的截图:
除了MVVM,还有RxFeedback和ReactorKit可以使用,基本上都是前端那一套思想。
虽然我也会Vuex中的那一套思路。不过考虑我是一个iOS开发者,我还是用自己熟悉的那一套来吧。
熟悉方式——使用单例编写账户管理
就我以前的写代码的经验,我一般会写一个用户管理的单例,然后里面存着用户信息到处走,当用用户管理单例中的用户信息有改变的时候,我会考虑发通知,去告诉相关页面去做刷新等操作。
所以我们写来写一个单例:
import Foundation
import RxSwift
import RxCocoa
import NSObject_Rx
import MBProgressHUD
final class AccountManager {
/// 单例
static let shared = AccountManager()
/// 对外只读是否登录属性
private(set) var isLogin = BehaviorRelay(value: false)
/// 对外只读用户信息属性
private(set) var accountInfo: AccountInfo?
/// 私有化初始化方法
private init() {}
}
extension AccountManager {
/// 已登录请求头处理
var cookieHeaderValue: String {
if let username = accountInfo?.username, let password = accountInfo?.password {
return "loginUserName=\(username);loginUserPassword=\(password)";
} else {
return ""
}
}
}
extension AccountManager {
/// 登录成功,保存登录信息
func saveLoginUsernameAndPassword(info: AccountInfo?, username: String, password: String) {
accountInfo = info
accountInfo?.username = username
accountInfo?.password = password
UserDefaults.standard.setValue(username, forKey: kUsername)
UserDefaults.standard.setValue(password, forKey: kPassword)
/// 需要注意赋值顺序,将info赋值给单例后,再改变isLogin的状态才能获取正确的请求头
isLogin.accept(true)
}
/// 登出成功,清理登录信息
func clearAccountInfo() {
isLogin.accept(false)
accountInfo = nil
}
}
extension AccountManager {
/// 更新收藏夹
func updateCollectIds(_ collectIds: [Int]) {
AccountManager.shared.accountInfo?.collectIds = collectIds
}
}
extension AccountManager {
/// 获取本地保存用户名
func getUsername() -> String? {
return UserDefaults.standard.value(forKey: kUsername) as? String
}
/// 获取本地保存密码
func getPassword() -> String? {
return UserDefaults.standard.value(forKey: kPassword) as? String
}
/// 自动登录
func autoLogin() {
if !isLogin.value {
guard let username = getUsername(), let password = getPassword() else {
return
}
login(username: username, password: password)
}
}
/// 调用登录接口
func login(username: String, password: String) {
accountProvider.rx.request(AccountService.login(username, password))
.map(BaseModel<AccountInfo>.self)
/// 转为Observable
.subscribe { baseModel in
if baseModel.isSuccess {
AccountManager.shared.saveLoginUsernameAndPassword(info: baseModel.data, username: username, password: password)
DispatchQueue.main.async {
MBProgressHUD.showText("登录成功")
}
}
} onError: { _ in
}.disposed(by: disposeBag)
}
}
extension AccountManager: HasDisposeBag {}
复制代码
以上代码都不是特别复杂,我会对每一段都进行分析。
Swift的单例写法
Swift中单例书写非常的简洁,static let shared = AccountManager()
,调用的时候直接AccountManager.shared
即可。
但是同时需要注意2点:
-
class前使用final修饰,如果不使用final修饰,那么这个类是可以继承的,一旦继承了,那么即便上就可以为所欲为了。
-
私有化初始化方法
init()
,保证只能通过.shared
来过去实例。
是否登录与用户信息保存
这里我使用了两个变量来进行保存,具体是下面这样
private(set) var isLogin = BehaviorRelay(value: false)
private(set) var accountInfo: AccountInfo?
复制代码
先说明private(set)
这个修饰符,这个修饰符用来表示这个属性对外只读,对内可读可写,保证只能通过这类的方法来修改变量,从一定程度上保证其安全性。
然后,让我们看看这个AccountInfo模型。
AccountInfo模型
struct AccountInfo : Codable {
let admin : Bool?
let chapterTops : [Int]?
var collectIds : [Int]?
let email : String?
let icon : String?
let id : Int?
let nickname : String?
var password : String?
let publicName : String?
let token : String?
let type : Int?
var username : String?
}
复制代码
有几个变量我是通过var来修饰的,比如collectIds
,用来表示收藏夹,这个数组变量,会随着用户的操作增加或者变少,在这个代码中有所体现:
func updateCollectIds(_ collectIds: [Int]) {
AccountManager.shared.accountInfo?.collectIds = collectIds
}
复制代码
而username
与password
保存下来主要是针对有些接口必须登录后方可请求,并且需要在请求头添加数据
!通过对accountInfo进行解包,从而生成cookies,具体如下:
var cookieHeaderValue: String {
if let username = accountInfo?.username, let password = accountInfo?.password {
return "loginUserName=\(username);loginUserPassword=\(password)";
} else {
return ""
}
}
复制代码
数据保存和清空
- 每次登录成功,都会更新更新用户信息:
func saveLoginUsernameAndPassword(info: AccountInfo?, username: String, password: String) {
/// 赋值个人信息
accountInfo = info
/// 赋值用户名
accountInfo?.username = username
/// 赋值密码
accountInfo?.password = password
/// 将关键信息保存到本地,用于每次进入App自动登录使用
UserDefaults.standard.setValue(username, forKey: kUsername)
UserDefaults.standard.setValue(password, forKey: kPassword)
/// 改变isLogin状态为true,需要注意赋值顺序,将info赋值给单例后,再改变isLogin的状态才能获取正确的请求头
isLogin.accept(true)
}
复制代码
- 每次登出成功,都会清除用户信息:
func clearAccountInfo() {
isLogin.accept(false)
accountInfo = nil
}
复制代码
自动登录功能:
之前的代码中我保存的username和password到本地,还可以用来通过App的生命周期去触发。
AccountManager.shared.autoLogin()
复制代码
账户管理模块的使用
在上篇的文章中,我编写了登录、注册模块,其中我的页面也和登录状态有关联。
如下图所示:
通过AccountManager中的isLogin属性,来进行数据的绑定,使用了两套不同的数据源来驱动页面:
class MyViewModel: BaseViewModel {
let logoutDataSource: [My] = [.ranking, .openSource, .login]
let loginDataSource: [My] = [.ranking, .myCoin, .myCollect, .openSource, .logout]
let currentDataSource = BehaviorRelay<[My]>(value: [])
let myCoin = BehaviorRelay<CoinRank?>(value: nil)
private let disposeBag: DisposeBag
init(disposeBag: DisposeBag) {
self.disposeBag = disposeBag
super.init()
AccountManager.shared.isLogin.subscribe(onNext: { [weak self] isLogin in
guard let self = self else { return }
print("\(self.className)收到了关于登录状态的值")
self.currentDataSource.accept(isLogin ? self.loginDataSource : self.logoutDataSource)
if isLogin {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
let result = self.getMyCoin()
result.map{ $0.data }
/// 去掉其中为nil的值
.compactMap{ $0 }
.subscribe(onSuccess: { data in
self.myCoin.accept(data)
}, onError: { error in
guard let _ = error as? MoyaError else { return }
self.networkError.onNext(())
})
.disposed(by: disposeBag)
}
} else {
self.myCoin.accept(nil)
}
}).disposed(by: disposeBag)
}
}
extension MyViewModel {
func getMyCoin() -> Single<BaseModel<CoinRank>> {
return myProvider.rx.request(MyService.userCoinInfo)
.map(BaseModel<CoinRank>.self)
}
func logout() -> Single<BaseModel<String>> {
return accountProvider.rx.request(AccountService.logout)
.map(BaseModel<String>.self)
}
}
复制代码
在MyViewModel的初始化方法中,我对AccountManager.shared.isLogin
进行了订阅,分别判断了在非登录和登录上不同的逻辑:
-
两套数据源
logoutDataSource
和loginDataSource
的切换。 -
登录过后,进行个人积分的接口的请求。
-
登出过后,对于个人积分的清空。
大家可以回想一下,如果不是RxSwift编写,这种登录操作前后应该怎么去通知相关页面进行操作呢?
没错,在iOS中是使用通知,在Android中使用的EventBus,其实两者的设计思路基本相同。
总结:
本篇文章我们聊了一下几点:
-
App端开发的状态管理目前正在向前端靠拢,其基本思路就是Redux模式,其中RxSwift中也提供了响应的解决方案。
-
本篇我还是用了最熟悉的单例模式进行了账户管理模块的实现,主要是我RxSwift都是在边学习边研究,RxFeedback和ReactorKit我确实还没有接触。
-
举例说明AccountManager在我的页面的使用。
明日继续
其实持续了大半个月的Swift开发wanandroid客户端已经接近尾声了,进度上项目、公众号页面基本一致,而体系页面略有不同,还有加载信息的WebView页面等。
不管怎么样,我都会尽力都讲解一下。
有很多朋友问:这个项目开源吗?
我的回答是,已经开源了,只是没放上来,主要是因为写代码和写文章同步进行,很多代码都还没有整理好,后面必定会放上链接,大家到时候给一个star我就最开心啦!
大家加油。