某些app用户只需要登录一次,退出app再次打开后还是保持登录状态。实现这个需求,用脚趾头都能想到用户账号数据已经做了持久化处理,那么怎么做的呢?今天我就写一篇笔记,给没有经验的朋友参考。
iOS的存储方式
NSKeyedArchiver:使用归档保存数据需要遵守NSCoding协议,并且该对象对应的类必须提供encodeWithCoder:和initWithCoder:方法。前一个方法告诉系统怎么对对象进行编码,而后一个方法则是告诉系统怎么对对象进行解码。
NSUserDefaults:偏好设置用于保存一些简单数据,比如app里一些设置数据、状态。NSUserDefaults可以存储的数据类型包括:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary。如果要存储其他类型,则需要转换为前面的类型,才能用NSUserDefaults存储。
Write:直接将对象永久写入到本地磁盘中,一些类自带写入属性。比如UIImage、NSString等,或者使用NSFileManager进行文件操作。
SQLite:采用数据库的形式存储数据,iOS中使用SQLite来存储一些数据量比较大的数据。比如新闻资讯类的app实现本地数据缓存,大都是采用数据库形式存储。数据库存储相比前面三种存储方式来说,相对比较复杂一些,但对大量数据进行缓存管理还是得需要数据库的。
需求分析
首先我们要明确一点,我们实现用户数据缓存的目的是保持用户的登录状态和数据(由于本人垃圾,所以这里不考虑用户数据隐私)。
一般app里我们都会定义一个用户账号类来管理用户数据,然后让这个类的对象能够一直保存在内存中,被整个app共享,并且修改了用户数据也要及时更新(更新本地数据、异步上传更新服务端的用户数据)。然后提供登录后的初始化操作方法和注销登录后的清理方法,在登录后就将数据缓存,退出后清除用户数据。
并且,一般后台会提供一个token字段和token有效期字段,用于判断用户登录状态是否有效。如果token失效,则清理用户数据。
代码实现
这里我就以最近写的一个 六阿哥客户端 app为例,创建账号类JFAccountModel,并声明一堆用户相关的属性,并提供一个转模型的构造方法:
/// 令牌 var token: String? /// 用户id var id: Int = 0 /// 用户名 var username: String? /// 昵称 var nickname: String? /// 注册时间 var registerTime: String? /// 邮箱 var email: String? /// 头像路径 var avatarUrl: String? /// 用户组 var groupName: String? /// 积分 var points: String? /// 个性签名 var saytext: String? /// 电话号码 var phone: String? /// qq号码 var qq: String? // KVC 字典转模型 init(dict: [String: AnyObject]) { super.init() setValuesForKeysWithDictionary(dict) } override func setValue(value: AnyObject?, forUndefinedKey key: String) {}
遵守协议实现归档和解档方法:
// MARK: - 归档和解档 func encodeWithCoder(aCoder: NSCoder) { aCoder.encodeObject(token, forKey: "token") aCoder.encodeInt(Int32(id), forKey: "id") aCoder.encodeObject(username, forKey: "username") aCoder.encodeObject(registerTime, forKey: "registerTime") aCoder.encodeObject(email, forKey: "email") aCoder.encodeObject(avatarUrl, forKey: "avatarUrl") aCoder.encodeObject(groupName, forKey: "groupName") aCoder.encodeObject(points, forKey: "points") aCoder.encodeObject(saytext, forKey: "saytext") aCoder.encodeObject(phone, forKey: "phone") aCoder.encodeObject(qq, forKey: "qq") aCoder.encodeObject(nickname, forKey: "nickname") } required init?(coder aDecoder: NSCoder) { token = aDecoder.decodeObjectForKey("token") as? String id = Int(aDecoder.decodeIntForKey("id")) username = aDecoder.decodeObjectForKey("username") as? String registerTime = aDecoder.decodeObjectForKey("registerTime") as? String email = aDecoder.decodeObjectForKey("email") as? String avatarUrl = aDecoder.decodeObjectForKey("avatarUrl") as? String groupName = aDecoder.decodeObjectForKey("groupName") as? String points = aDecoder.decodeObjectForKey("points") as? String saytext = aDecoder.decodeObjectForKey("saytext") as? String phone = aDecoder.decodeObjectForKey("phone") as? String qq = aDecoder.decodeObjectForKey("qq") as? String nickname = aDecoder.decodeObjectForKey("nickname") as? String }
定义一个私有的静态属性用于持久保存JFAccountModel对象在内存中:
// 持久保存到内存中 private static var userAccount: JFAccountModel?
创建归档JFAccountModel对象的路径,一般都是保存在沙盒中的Document目录下:
/// 归档账号的路径 static let accountPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last! + "/Account.plist"
归档JFAccountModel的方法:
func saveAccount() { NSKeyedArchiver.archiveRootObject(self, toFile: JFAccountModel.accountPath) }
登录成功后保存JFAccountModel的方法:
func updateUserInfo() { // 保存到内存中 JFAccountModel.userAccount = self // 归档用户信息 saveAccount() }
这里我们来模拟登录,发送登录请求:
var parameters: [String : AnyObject] parameters = [ "username" : self.usernameField.text!, "password" : self.passwordField.text! ] // 发送登录请求 JFNetworkTool.shareNetworkTool.post(LOGIN, parameters: parameters) { (success, result, error) in if success { if let successResult = result { let account = JFAccountModel(dict: successResult["data"].dictionaryObject!) // 更新用户本地数据 account.updateUserInfo() } } else if result != nil { JFProgressHUD.showInfoWithStatus(result!["data"]["info"].string!) } }
在登录成功后调用上面的 updateUserinfo() 方法进行更新本地缓存数据。
退出登录后的清理工作,因为我这里也使用了第三方登录,所以在退出登录的同时取消授权:
class func logout() { ShareSDK.cancelAuthorize(SSDKPlatformType.TypeQQ) ShareSDK.cancelAuthorize(SSDKPlatformType.TypeSinaWeibo) // 清除内存中的账号对象和归档 JFAccountModel.userAccount = nil do { try NSFileManager.defaultManager().removeItemAtPath(JFAccountModel.accountPath) } catch { print("退出异常") } }
至此,从登录成功并归档账号数据,到退出后清理归档和内存中的账号数据就做完了。
补充完善
上面只是实现了基本的用户数据归档和清理,但是这样的还不够。比如,开头我们说到的让JFAccountModel对象能够给整个app共享,我们什么时候去登录有效期处理,在进行一些用户操作的时候判断当前用户是否是登录状态。这些我们都可以封装在JFAccountModel用户类里面的。
我这里的做法是,在app启动的时候去判断是否已经登录,首先我们需要提供一个方法来让JFAccountModel对象能够被app共享:
static func shareAccount() -> JFAccountModel? { if userAccount == nil { userAccount = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? JFAccountModel } if userAccount == nil { // 没有则说明没有登录 return nil } else { // 有则说明已经登录 return userAccount } }
调用上面的方法,如果返回不为nil,则说明已经登录,并获取到了JFAccountModel对象。我们再提供一个便捷的方法直接判断用户是否已经登录:
class func isLogin() -> Bool { return JFAccountModel.shareAccount() != nil }
app启动时在appDelegate的didFinishLaunchingWithOptions方法中去判断当前用户信息是否有效:
class func checkUserInfo(finished: () -> ()) { if isLogin() { // 已经登录并保存过信息,验证信息是否有效 let parameters: [String : AnyObject] = [ "username" : JFAccountModel.shareAccount()!.username!, "userid" : JFAccountModel.shareAccount()!.id, "token" : JFAccountModel.shareAccount()!.token! ] JFNetworkTool.shareNetworkTool.post(GET_USERINFO, parameters: parameters, finished: { (success, result, error) in guard let successResult = result where success == true else { JFAccountModel.logout() print("登录信息无效") return } print("登录信息有效") let account = JFAccountModel(dict: successResult["data"].dictionaryObject!) // 更新用户信息 account.updateUserInfo() // 更新完成回调 finished() }) } }
这个方法也可以作为更新本地用户信息的方法,比如用户在其他手机上的app登录过后的一些数据改变,我们需要去更新同步这些数据。当然,还有一种情况,也就是没有网络或者网络差的情况,直接去调用这个方法会导致清空本地用户数据,这个可以根据自己的需求去变通。比如判断请求结果,根据不同情况去处理,或者判断当前网络状态,无网络情况不去更新用户信息。
这只是举一个栗子,真正做项目的时候还是得根据后端api返回的具体数据去具体分析的,比如我们这里将用户登录有效期和更新用户信息给写成一个方法了,因为我对用户是否是真的登录,要求并不是很高。就算登录失效,本地用户的数据也是可以用的。只是在调用一些接口的时候,需要传递token,这个时候可能传递错误的token,后台就会返回对应错误码给你,你再去针对处理就行。一些对用户安全性要求比较低的app,可能根本就没有token字段,或者有token字段但没有token有效期。
下一篇继续讲解缓存,使用SQLite缓存本地数据。