1. 需求分析
作为一个开发者,平时肯定在各个平台,网站注册了各种账号;由于太多,很多时候都是注册之后就抛之脑后,等到用到的时候,已经记不起是否已经注册过,或者账号及密码是多少也很模糊了。大部分的做法是找回密码,但是,使用哪个邮箱注册的,这又是一个问题。。。等等的问题,虽然不大,但却也带来一点烦扰。
该项目主要就是解决这个最基本的需求而设计的,整体功能包含以下几部分:
- 账号信息录入、保存、修改
- 账号信息检索
- 安全保护:手势/数字/Touch ID
- 其他:iCloud共享、iTunes导入/导出
2. 使用的技术
- 该项目整体使用 Swift 编写,混合使用了部分OC的工具类;
- 数据存储使用了Coredata,主要内容见LQCoreData封装类,该类封装了常见的数据增删改查;
- 安全设置:安全方面提供了手势密码、数字密码、Touch ID三种方式进行锁屏设置
- 手势密码,提供了锁屏/设置密码/重置密码/修改密码/验证密码等模式,详见 GesturePasswordSetting 文件夹内文件
- 数字密码:数字密码目前使用的是4位密码,文件在NumberPasswordSetting 文件夹中,其中的封装中还有6位密码和自由输入两种模式可供选择;
- Touch ID:封装了系统的API,详见 LQTouchID.swift文件
- iCloud共享:使用了iCloud在设备间进行共享数据,在登录同一Apple ID的设备间可进行数据的同步;
- iTunes导出/导入:对数据的备份可使用iTunes进行导入/导出,不过需要一定的数据格式,具体可参见代码中的‘设置 - 分享设置 - iTunes分享’
- 其他:增加了随机选择卡片样式的背景色及内容文字颜色,使保存的信息丰富多彩。
3. 代码实现
3.1. 数据存储
数据存储封装了系统的Coredata,主要有以下几个方法:
/// 增
///
/// - Parameters:
/// - name: 实体(Entity)名称
/// - handler: 配置待保存数据回调
/// - rs: 保存结果回调
class func insert(entity name: String, configHandler handler: ((_ obj: NSManagedObject) -> Void), resulteHandler rs: ((_ error: NSError? ) -> Void)? = nil)
/// 删
///
/// - Parameters:
/// - name: 实体(Entity)名称
/// - predicate: 删除条件
/// - result: 删除结果回调
class func delete(entity name: String, predicate: NSPredicate? = nil, resultHandler rs: ((_ error: NSError?, _ deletedObjs: Array<Any>?) -> Void)? = nil)
/// 改
///
/// - Parameters:
/// - name: 实体(Entity)名称
/// - predicate: 查询条件
/// - handler: 更新数据的回调
/// - result: 更新结果的回调
class func update(withEntityName name: String, predicate: NSPredicate, configNewValues handler: ((_ objs: Array<Any>) -> Void), result: ((_ error: NSError?) -> Void)? = nil)
/// 查
///
/// - Parameters:
/// - name: 实体(Entity)名称
/// - predicate: 查询条件-谓词
/// - propertiesToFetch: 查询的属性, 默认所有属性
/// - key: 查询结果排序的依据属性, 升序
/// - result: 查询结果回调
class func fetch(entity name: String, predicate: NSPredicate? = nil, propertiesToFetch: Array<Any>? = nil, resultSortKey key: String? = nil, resultHandler rs: @escaping ((_ info: Array<Any>?) -> Void))
在需要保存/获取数据的地方调用相应的方法即可;
查询所有分组:
func loadData() {
if dataSource.count > 0 {
dataSource.removeAll()
}
LQCoreData.fetch(entity: LQGroupModelID, predicate: nil, propertiesToFetch: nil, resultSortKey: "createDate") { (arr) in
for obj in arr ?? [] {
if let model = obj as? LQGroupModel {
if let uid = model.uid {
model.count = Int32(LQCoreData.count(ofEntity: LQAccountModelID, predicate: LQCoreData.predicate(.match(string: uid, forProperty: "groupid"))))
}
self.dataSource.append(model)
}
}
self.table.reloadData()
}
if let handler = self.loadSearchData {
handler(nil)
}
}
查询某个分组内的数据:
func loadData() {
if self.dataSource.count > 0 {
self.dataSource.removeAll()
}
guard let group = self.groupModel, let uid = group.uid else { return }
LQCoreData.fetch(entity: LQAccountModelID, predicate: LQCoreData.predicate(.match(string: uid, forProperty: "groupid")), resultSortKey: "createDate") { (objs) in
if let accounts = objs as? [LQAccountModel] {
self.dataSource.append(contentsOf: accounts)
self.table.reloadData()
}
}
}
新增数据:
guard let groupID = defaultGroup?.uid else { return }
LQCoreData.insert(entity: LQAccountModelID, configHandler: { (obj) in
if let model = obj as? LQAccountModel {
self.updateAccount(model)
model.groupName = defaultGroup?.title
model.groupid = groupID
model.uid = String.randomMD5
model.createDate = Date()
model.backgroundColor = backgroundColorStr
model.contentColor = contentColorStr
}
}) { (error) in
self.alert("保存成功", message: "再添加一个?", commitTitle: "继续添加", cancelTitle: "返回", destrucTitle: nil, commitHandler: {
self.dataSource[0] = ""
self.dataSource[1] = ""
self.dataSource[2] = ""
self.dataSource[3] = ""
self.dataSource[4] = ""
self.dataSource[5] = self.defaultGroup?.title
self.perform(#selector(self.raloadTable), with: nil, afterDelay: 1.0)
}, cancelHandler: {
self.navigationController?.popViewController(animated: true)
}, destrucHandler: nil)
}
更新数据:
if let uid = model.uid {
let pred = LQCoreData.predicate(.match(string: uid, forProperty: "uid"))
LQCoreData.update(withEntityName: LQAccountModelID, predicate: pred, configNewValues: { (objs) in
if let model = objs.first as? LQAccountModel {
self.updateAccount(model)
model.groupName = self.defaultGroup?.title
model.groupid = self.defaultGroup?.uid
}
}, result: { (error) in
self.alert("更新成功", message: nil, commitTitle: "我知道了", cancelTitle: nil, destrucTitle: nil, commitHandler: {
self.navigationController?.popViewController(animated: true)
}, cancelHandler: nil, destrucHandler: nil)
})
return
3.2. 数据搜索
数据的搜索使用了系统的 UISearchController ,结合 UITableView 进行数据的显示:
获取所有数据:
func loadData () {
if self.dataSource.count > 0 {
self.dataSource.removeAll()
self.indexTitles.removeAll()
}
LQCoreData.fetch(entity: LQAccountModelID) { (objs) in
if let accounts = objs as? [LQAccountModel] {
let results = LQSortTool.sortObjs(accounts)
for dic in results {
let group = LQSearchGroup()
group.title = dic.0
group.objs = dic.1
self.indexTitles.append(group.title ?? "")
self.dataSource.append(group)
}
self.table.reloadData()
}
}
}
匹配搜索结果:
func updateSearchResults(for searchController: UISearchController) {
guard let input = searchController.searchBar.text else { return }
if self.results.count > 0 {
self.results.removeAll()
}
for obj in self.dataSource {
if let name = obj.nickName {
if name.contains(input.lowercased()) {
self.results.append(obj)
} else {
let str = name.lq_first.toPinyin.lq_first
let metch = input.toPinyin
if metch.contains(str) {
self.results.append(obj)
}
}
}
if let name = obj.userName {
if name.contains(input.lowercased()) {
if self.results.contains(obj) == false {
self.results.append(obj)
}
} else {
let str = name.lq_first.toPinyin.lq_first
let metch = input.toPinyin
if metch.contains(str) {
self.results.append(obj)
}
}
}
}
if self.results.count > 0 {
self.tableFooterLabel.text = "匹配到 \(self.results.count) 个结果"
} else {
self.tableFooterLabel.text = "无结果"
}
self.table.reloadData()
}
3.3. 安全设置
针对该项目,主题功能简单,就是信息的增删改查,但是同时,需要保护信息的安全,所以这里我设计了三种方式来保护信息安全,包括每次打开应用都会进行验证。
- 手势密码
手势密码定义了以下几种验证方式
enum LQGestureSettingType {
case setting, verify, update, screen
}
图形绘制封装在了 LQGestureCircleView 文件中,使用代理的方式进行绘制结果的回调:
@objc protocol LQGestureCircleViewDelegate {
@objc optional
// 绘制无效时的绘制,例如: 少于最低连线个数
func circleView(_ view: LQGestureCircleView, invalidConnect result: String)
@objc optional
// 绘制成功
func circleView(_ view: LQGestureCircleView, commitConnect result: String)
@objc optional
/// 返回每次绘制完成后的截图
/// - parameter view: LZCircleView
///
/// - returns: 绘制完成的图像
func circleView(_ view: LQGestureCircleView, shotImage img: UIImage)
}
- 数字密码
数字密码的UI是仿写的系统数字密码设置,提供了4位、6位、自定义三种输入密码方式:
enum LQInputType {
case four, six, custom
}
同样,输入结果通过相应的代理进行回调:
@objc protocol LQInputViewDelegate {
@objc optional
func inputView(_ view: LQInputView, didInput input: String)
@objc optional
func inputView(_ view: LQInputView, shouldInput input: String)
}
- Touch ID
作为最方便的解锁方式,这个功能是不会少的,这个封装的代码比较少,主要是判断当前是否可用Touch ID,以及发起验证:
static var isTouchIdEnable: Bool
// 开始验证指纹
static func startVerify( _ completion: @escaping (_ success: Bool, _ msg: String, _ error: LAError? ) -> Void)
3.4. iCloud 同步
为了在设备间进行数据同步,使用了iCloud功能,源文件在 LQiCloud 文件夹内,目前还是使用OC写的,晚些时候会改成swift,主要封装了上传与下载等接口:
/**
上传到iCloud方法
@param name 保存在iCloud的名称
@param file 需要保存的文件, 可为数组, 字典,或已保存在本地的文件路径或名称
@param block 上传结果回调
*/
+ (void)uploadToiCloud:(NSString *)name file:(id)file resultBlock:(uploadBlock)block;
/**
从iCloud获取保存的文件
@param name 保存在iCloud的文件名称
@param 保存的文件,可能为数组,字典或NSData
*/
+ (void)downloadFromiCloud:(NSString *)name responsBlock:(downloadBlock)block;
3.5. 其他
在保存数据的基础上,设计了卡片式展示保存的内容,同时提供了自定义卡片背景色和内容颜色的功能,时保存的内容形式更丰富多彩:
颜色的选中封装了 LQColorPicker.swift 文件,外部使用相关的方法即可:
colorPicker.colorInfo {[weak self] (color, r, g, b, a) in
print(color)
print(r)
self?.configStyle(color, r: r, g: g, b: b, a: a)
}
4. 项目结构
5. 效果图
分组列表:
账号列表
信息详情
自定义色彩
写在最后
[iOS] 完整源码, Swift语言 - 账号保存工具
注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权