如何构造Swift框架(3)数据模块

[中文教程]
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看215 回复回复1 收藏收藏2 分享淘帖 转播转播 分享分享 微信
查看: 215|回复: 1
收起左侧

[中文教程] 如何构造Swift框架(3)数据模块

[复制链接]
Caesar 发表于 2016-10-14 10:22:01 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑
本帖最后由 Caesar 于 2016-10-14 11:07 编辑

为什么将数据模块与网络模块一起编写?紧密相关的2者,也是应用程序最常见的组合:请求数据-存储数据-展示数据。




1、数据模块(JSON、Model、DB)

数据持久层其实有很多杂乱的选择,文件做为持久层的基础类型,又被分为sql、xml等等类型,我们可选的范围也很广阔,也可以自己动手来完善整个持久层模块,但这似乎对于很懒的程序员来说太浪费玩耍的时间了。所以我们索性在一些完善的开源库中挑选一类供我们做上层的封装。我们姑且确定选择FMDB作为我们的数据库支撑,但是联想到一般情况下,把JSON转换为Model直接存向数据库是常用需求,所以在数据模块这一层貌似扩展的很大了。我们首先引入SwiftyJSON作为JSON解析,不仅供框架使用也供外层使用,但是SwiftJSON解析后的数据仅是JSON对象,看来我们要亲自把JSON对象转换为Model了,写好之后,就是数据的存储。

通常情况下,iOS App中常用的存储:UserDefault、WriteToFile、SQL、钥匙串等。为了框架的通用性,只选择完善SQL,即Model->DB,至于别的存储让App在框架外层随意。如果你的团队不是足够成熟的话, 并不建议在项目中使用CoreData。

想想这个模块比较大,所以单开一个文件夹:Model吧。

首先引入SwiftJSON作为JSON解析的模块。

[Bash shell] 纯文本查看 复制代码
pod 'SwiftyJSON'


明确目标1:

  • JSON对象转换为模型
  • 模型转换为JSON对象


要实现我们这2个功能,必须遵守协议,在协议的方法中返回Mapping供我们赋值,类似:[属性名:JSON对象的Key]。

[Swift] 纯文本查看 复制代码
public protocol ModelMap {
  /// 提供属性的对应Map
  ///
  /// - returns: 字典,为:属性名称:(JSON对象中名称,类型)
  static func keyMapping() ->Dictionary<String, (String, String)>
}


首先我们要了解一些关于Swift runtime的东西,在纯Swift类中的属性在未添加dynamic修饰之前是不可以被运行时获取到属性的,之前我写过一篇有关Swift运行时的文章,可以细阅。

在这里有一个缺陷,就是我们这个库不允许你的模型是继承来的,因为static关键字代替了class final,在子类中不可以被覆盖,使用Extension又不能很好的解决方便程度上的问题而且需要写基类。所以在考虑了很多很多之后,还是决定写扩展!并且不对纯Swift类进行处理,只处理NSObject类。

[Swift] 纯文本查看 复制代码
extension NSObject {
  /// 提供属性的对应Map
  ///
  /// - returns: 字典,为:属性名称:JSON对象中名称
  open class func keyMapping() ->Dictionary<String, String>? {
    return nil
  }
  
  /// 提供的类对应Map
  open class func classMapping() ->Dictionary<String, AnyClass>? {
    return nil
  }
  
  /// 忽略的属性的名称
  ///
  /// - returns: 数组,忽略的属性名称数组
  open class func ignoredKey() ->[String]? {
    return nil
  }
}


这样所有需要被转换的类都需要继承Model类。但是使用的时候就比较尴尬了,必须要提供一个初始值,下面是我们的测试类:

[Swift] 纯文本查看 复制代码
import UIKit
import INSSwift
class Person: NSObject {
  var name: String = ""
  var gender : Bool = false
  var array: Array = [""]
  var dictionary: Dictionary<String, Any> = ["": ""]
  
  var student: Student = Student()
  var teachers: [Teacher] = []
  
  var ignored: String = ""
  
  override class func keyMapping() ->Dictionary<String, String>? {
    return [
      "gender": "dictionary, gender",
      "teachers": "student, t"
    ]
  }
  
  override class func classMapping() ->Dictionary<String, AnyClass>? {
    return [
      "student": Student.self,
      "teachers": Teacher.self
    ]
  }
  
  override class func ignoredKey() ->[String]? {
    return ["ignored"]
  }
}
class Student: NSObject {
  var sname: String = ""
  var sage: Int = 0
}
class Teacher: NSObject {
  var tname: String = ""
}


如果dynamic var name: String?的话,后果自负…会提示错误不能转换为OC类型。这里我附上完整的Model处理的代码以及测试代码,在Model文件夹下新建文件Model.swift:

[Swift] 纯文本查看 复制代码
import Foundation
import SwiftyJSON
public enum PropertyType: String {
  case tString = "T@\"NSString\""
  case tNSArray = "T@\"NSArray\""
  case tNSDictionary = "T@\"NSDictionary\""
  case tBool = "TB"
  case tClass = "T@\"_TtC"
  case tDouble = "Td"
  case tFloat = "Tf"
  case tInt = "Tq"
  case tInt8 = "Tc"
  case tInt16 = "Ts"
  case tInt32 = "Ti"
  case tUInt = "TQ"
  case tUInt8 = "TC"
  case tUInt16 = "TS"
  case tUInt32 = "TI"
  case unknow = "UNKNOWTYPE"
}
extension NSObject {
  /// 提供属性的对应Map
  ///
  /// - returns: 字典,为:属性名称:JSON对象中名称
  open class func keyMapping() ->Dictionary<String, String>? {
    return nil
  }
  
  /// 提供的类对应Map
  open class func classMapping() ->Dictionary<String, AnyClass>? {
    return nil
  }
  
  /// 忽略的属性的名称
  ///
  /// - returns: 数组,忽略的属性名称数组
  open class func ignoredKey() ->[String]? {
    return nil
  }
}
/// 模型生成工厂
final public class ModelFactory {
  
  // MARK: - 对象生成
  
  /// 转换JSON->Model
  ///
  /// - parameter json: JSON对象
  /// - parameter cls: 类型
  /// - returns: AnyObject
  public class func Convert(JSON json: JSON?, to cls: AnyClass) ->AnyObject? {
    let model = cls.alloc()
    let keyMapping = cls.keyMapping()
    let classMapping = cls.classMapping()
    
    setValue(with: fullPropertyFor(class: cls), for: model, with: classMapping, with: keyMapping, data: json)
    return model
  }
  
  // MARK: - Private methods
  
  /// 赋值
  /// - parameter props: 属性对应字典
  /// - parameter object: 赋值的对象
  /// - parameter clsMap: 类型映射字典
  /// - parameter mapping: 属性映射字典
  /// - parameter data: JSON对象
  public class func setValue(with props: Dictionary<String, String>?,
                             for object: AnyObject,
                             with clsMap: Dictionary<String, AnyClass>?,
                             with mapping: Dictionary<String, String>?,
                             data data: JSON?) {
    guard let fullInfos = props, fullInfos.count != 0, let json = data else {
      return
    }
    /// 将所有的Info取出来
    for (key, typeString) in fullInfos {
      /// 首先从Mapping中检测是否有对应的Mapper
      let JSONKey = mapping?[key] ?? key
      var JSONValue = json[JSONKey]
      if JSONKey.contains(",") {
        var items: Array<String> = []
        for item in JSONKey.components(separatedBy: ",") {
          items.append(item.replacingOccurrences(of: " ", with: ""))
        }
        JSONValue = json[items]
      }
      
      let propType = PropertyType(rawValue: typeString)
      
      if let classMapping = clsMap, let pointedClass = classMapping[key] {
        // 该属性实现了类型Map
        if propType == .tClass {
          let sweetObject = Convert(JSON: JSONValue, to: pointedClass)
          object.setValue(sweetObject, forKey: key)
          continue
        }
        else if propType == .tNSArray {
          let list: Array<JSON> = JSONValue.arrayValue
          var objectArray: Array<AnyObject> = []
          for item in list {
            guard let arrayObject = Convert(JSON: item, to: pointedClass) else {
              continue
            }
            objectArray.append(arrayObject)
          }
          object.setValue(objectArray, forKey: key)
          continue
        }
      }
      
      var value: Any? = JSONValue
      if propType == .tString {
        value = JSONValue.stringValue
      }
      if propType == .tInt || propType == .tInt8 || propType == .tInt16 || propType == .tInt32 ||
         propType == .tUInt || propType == .tUInt8 || propType == .tUInt16 || propType == .tUInt32 {
        value = JSONValue.intValue
      }
      if propType == .tDouble {
        value = JSONValue.doubleValue
      }
      if propType == .tFloat {
        value = JSONValue.floatValue
      }
      if propType == .tBool {
        value = JSONValue.boolValue
      }
      if propType == .tNSArray {
        value = JSONValue.arrayObject
      }
      if propType == .tNSDictionary {
        value = JSONValue.dictionaryObject
      }
      
      object.setValue(value, forKey: key)
    }
  }
  
  /// 获得子类与父类叠加后的属性列表
  public class func fullPropertyFor(class cls: AnyClass) ->[String: String]? {
    var currentCls: AnyClass = cls
    var infoDict = [String: String]()
    
    while let parent: AnyClass = currentCls.superclass() {
      infoDict.merge(dict: propertyFor(class: currentCls))
      currentCls = parent
    }
    
    return infoDict
  }
  
  /// 获取类的全部属性
  public class func propertyFor(class cls: AnyClass) ->[String: String]? {
    var count: UInt32 = 0
    let properties = class_copyPropertyList(cls, &count)
    let ignoredKeys = cls.ignoredKey() ?? []
    
    var infoDict = [String: String]()
    for index in 0..<numericCast(count) {
      let prop = properties?[Int(index)]
      
      let pname = String(cString: property_getName(prop))
      let ptype = String(cString: property_getAttributes(prop))
      
      var propertyRealType: PropertyType
      let index = ptype.index(ptype.startIndex, offsetBy: 2)
      
      if let type = PropertyType.init(rawValue: ptype.substring(to: index)) {
        propertyRealType = type
      } else {
        if ptype.hasPrefix(PropertyType.tString.rawValue) {
          propertyRealType = PropertyType.tString
        }
        else if ptype.hasPrefix(PropertyType.tNSArray.rawValue) {
          propertyRealType = PropertyType.tNSArray
        }
        else if ptype.hasPrefix(PropertyType.tNSDictionary.rawValue) {
          propertyRealType = PropertyType.tNSDictionary
        }
        else if ptype.hasPrefix(PropertyType.tClass.rawValue) {
          propertyRealType = PropertyType.tClass
        } else {
          propertyRealType = PropertyType.unknow
        }
      }
      
      if !ignoredKeys.contains(pname) {
        infoDict[pname] = propertyRealType.rawValue
      }
    }
    
    free(properties)
    
    guard infoDict.count != 0 else {
      return nil
    }
    return infoDict
  }
}
extension Dictionary {
  mutating func merge<K, V>(dict: [K: V]?) {
    guard let d = dict, d != nil, d.count != 0 else {
      return
    }
    for (k, v) in d {
      self.updateValue(v as! Value, forKey: k as! Key)
    }
  }
}


测试代码(测试的类在上边给出了):

[Swift] 纯文本查看 复制代码
let json: JSON =  ["name": "Jack",
                       "ignored": "test ignored",
                       "array":
                        [
                          "1", "2",
                          "3", "4"
                        ],
                       "dictionary":
                                  [
                                    "2": true,
                                    "options": 666,
                                    "gender": true
                                  ],
                       "student": [
                                    "sname": "JACK",
                                    "sage": 10,
                                    "t": [
                                      ["tname": "teacher1"],
                                      ["tname": "teacher2"],
                                      ["tname": "teacher3"]
                                    ]
                                  ]
                      ]
let object: Person = ModelFactory.Convert(JSON: json, to: Person.self) as! Person
// 使用filter答应、、
let s = object.teachers.filter {
  let teacher: Teacher = $0
  print(teacher.tname)
  return true
}
// 在32行之前加一个断点查看对象属性!
ILog(.debug, object)


注:其实在实现Model提供Mapping的时候,期望还是使用Protocol的方式来做,但是不能解决static fun这个问题,static 代表着final class,所以会出现子类无法覆盖的问题。如果有解决方法还请告知,当然,这上边是我们自己手写的代码,在正真环境中,我还是选择了YYModel,前边仅是为了让大家了解一下转换代码。记得去掉SwiftJSON,等下我们的操作是Alamofire->YYModel->LKDBHelper,是不是很懒。

[Bash shell] 纯文本查看 复制代码
pod 'YYModel'


明确目标2:

  • 模型-数据库中的操作(数据库的基本语法在这里不做介绍)


说到模型-数据库中的操作,其实特别想把LKDBHelper拿来直接使用,这里比较懒,因为代码其实和上边差不多,多个数据库操作而已,所以不写了。LKDBHelper是一套OC的代码而且在Swift上测试没有问题。但是他的Demo是4个月前更新的,应该是Swift2.3版本,所以我们决定,fork一份代码并切转换到Swift3.0做测试,如果没问题,直接拿来用,以减少我们框架模块的编写时间。

好了,测试没有问题,并且提交了PR。我们现在拉过来使用,作者目前还没有合并到master,大家可以先来我的仓库查看使用的Demo。

[Bash shell] 纯文本查看 复制代码
pod 'LKDBHelper'


这样我们JSON-模型-数据库的代码基本完成了30%。剩下的都是后期的针对业务的修改。




2、网络请求模块

使用Alamofire,写到这里,读者肯定说我这个框架没有什么技术含量;要知道,这些库的成长经历了多少人的洗礼,而我只有3个人的小团队,创业公司的项目压力各位应该知道。所以合理的使用第三方库也是一个好的选择!如果你在一个大的团队,那么就可以为了晋升去写一点东西了。知道原理就好了,知道原理不写的原因是没有人家那么考虑的全面、构思的缜密。

新建文件Request.swift,Request对象作为在网络层流动的对象,Alamofire作为支撑。

[Bash shell] 纯文本查看 复制代码
pod 'Alamofire'


但是你真的以为我会这么做么?这么做太麻烦了,我很懒的。我们使用Moya+Alamofire。直接上使用流程:可以选择继承INSRequest来实现自己的Request类,也可以直接下一个枚举统一管理,但是Moya这里有个问题是:实现Moya的协议必须要实现所有的方法,该协议中所有的属性全部是required(而且让我不理解的是,sampleData这种非必须数据竟然不是?,已经提了一个issue提问,因为文档没有很明确),对其中的一些属性做解释:

[Bash shell] 纯文本查看 复制代码
pod 'Moya'


记得把开发环境调整到iOS 9.0

baseURL: 根地址

path:网络请求路径

method:网络请求方法

parameters: 参数

sampleData:默认的Response data

task:当前任务的形态,一般为request,还有upload和download

[Swift] 纯文本查看 复制代码
import Foundation
import Moya
/// 在这里完成API的名称定义
enum MMAPI {
  case launch
  case signin(mobilePhoneNumber: String, password: String)
}
/// 在这里完成API所需信息补全
extension MMAPI: TargetType {
  var baseURL: URL {
    return URL(string: "http://leaf.leanapp.cn/api/")!
  }
  var path: String {
    switch self {
    case .launch:
      return "launch.json"
    case .signin:
      return "login"
    }
  }
  var method: Moya.Method {
    switch self {
    case .signin:
      return .POST
    default:
      return .GET
    }
  }
  var parameters: [String: Any]? {
    switch self {
    case .signin(let number, let password):
      return ["phone": number, "passwd": password]
    default:
      return nil
    }
  }
  var sampleData: Data {
    return Data(base64Encoded: "")!
  }
  
  var task: Task {
    return .request
  }
}


在使用时候(顺便贴上官方Demo的地址):

[Swift] 纯文本查看 复制代码
let provider = MoyaProvider<MMAPI>()
provider.request(.launch) { result in
  switch result {
    case let .success(response):
      do {
        if let json = try response.mapJSON() as? NSArray {
          ILog(.debug, "\(json) \n \(response.statusCode)")
        } else {}
        } catch {}
    case let .failure(error):
      ILog(.debug, error.localizedDescription)
    }
  }


如何构造Swift框架(3)数据模块

如何构造Swift框架(3)数据模块 - 敏捷大拇指 - 如何构造Swift框架(3)数据模块


也是看到成功了,这个时候,我们把开始写的->Model->DB顺便测试一下:

[Swift] 纯文本查看 复制代码
if let json = try response.mapJSON() as? NSDictionary {
  // 转换成对象
  let launchModel = LaunchModel.yy_model(withJSON: json)
  ILog(.debug, "\(launchModel?.imageUri) \n \(response.statusCode)")
  
  // 存储到数据库
  ILog(.debug, "Save to DB result \(launchModel?.saveToDB())")
  
  // 查询
  let searchedObject = LaunchModel.searchSingle(withWhere: nil, orderBy: nil) as! LaunchModel
  ILog(.debug, "Searched result \(searchedObject.imageUri)")
            
}


[Swift] 纯文本查看 复制代码
[DEBUG] [ViewController.swift: viewDidLoad(): 33] 
 Save to DB result Optional(true)
[DEBUG] [ViewController.swift: viewDidLoad(): 36] 
 Searched result Optional("http://static.zhaogeshi.com")


这里有一个坑大家注意: 在Swift上使用LKDB的时候,必须重写返回类名的方法:

[Swift] 纯文本查看 复制代码
class LaunchModel: Model {
  var bizUri: String?
  var domains: NSArray?
  var hotFixJS: String?
  var imageUri:String?
  var learningURL: String?
  var status: String?
  var version: String?
  
  override static func getPrimaryKey() -> String {
    return "version"
  }
  
  override static func getTableName() -> String {
    return "LaunchModel"
  }
}


否则,Swift这边读出的表名称会有问题,导致数据存储失败。

这样,借助第三方之手,我们美美的解决了领导给的2大块任务,数据模块与网络模块。依旧只能说,这2大功能模块只是提供了基础的功能,上层的封装还需要我们在接触业务的时候定制。所以这里只说,2大模块只完成了30%。

下一篇文章开始写日志上报、异常捕获与处理、推送、位置、数据打点等。




相关内容

如何构造Swift框架(1)知识储备

如何构造Swift框架(2)功能编写

如何构造Swift框架(3)数据模块

如何构造Swift框架(4)推送服务

作者:dylan

都看到这里了,就把这篇资料推荐给您的好朋友吧,让他们也感受一下。

回帖是一种美德,也是对楼主发帖的尊重和支持。

*声明:敏捷大拇指是全球最大的Swift开发者社区、苹果粉丝家园、智能移动门户,所载内容仅限于传递更多最新信息,并不意味赞同其观点或证实其描述;内容仅供参考,并非绝对正确的建议。本站不对上述信息的真实性、合法性、完整性做出保证;转载请注明来源并加上本站链接,敏捷大拇指将保留所有法律权益。如有疑问或建议,邮件至marketing@swifthumb.com

*联系:微信公众平台:“swifthumb” / 腾讯微博:@swifthumb / 新浪微博:@swifthumb / 官方QQ一群:343549891(满) / 官方QQ二群:245285613 ,需要报上用户名才会被同意进群,请先注册敏捷大拇指

嗯,不错!期待更多好内容,支持一把:
支持敏捷大拇指,用支付宝支付10.24元 支持敏捷大拇指,用微信支付10.24元

评分

参与人数 1金钱 +10 贡献 +10 专家分 +10 收起 理由
Anewczs + 10 + 10 + 10 32个赞!专家给力!

查看全部评分

本帖被以下淘专辑推荐:

女汉子 发表于 2016-10-15 22:41:58 | 显示全部楼层
Mark.
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

做任务,领红包。
我要发帖

分享扩散

都看到这里了,就把这资料推荐给您的好朋友吧,让他们也感受一下。
您的每一位朋友访问此永久链接后,您都将获得相应的金钱积分奖励
关闭

站长推荐 上一条 /3 下一条

热门推荐

合作伙伴

Swift小苹果

  • 北京治世天下科技有限公司
  • ©2014-2016 敏捷大拇指
  • 京ICP备14029482号
  • Powered by Discuz! X3.1 Licensed
  • swifthumb Wechat Code
  •   
快速回复 返回顶部 返回列表