Plistのキー名のメンバを持ったエンティティにPlistの値をマッピングするということを、NSObjectのsetValue:forKeyを敢えて利用せずに(思いつかなかった)、ポインタを利用することでも出来たので紹介します。
環境
Xcode8.1
Swift3.0
ベースとなるエンティティを用意する
BaseEntityにはInt、String、Bool、Array、BaseEntityを継承したエンティティにパースするためのメソッドを用意して、継承したエンティティで呼び出します。(メンバにBaseEntityを継承したエンティティを含めない場合はNSObjectを継承する必要はありません)
import UIKit
class BaseEntity : NSObject {
func parse(_ data : NSDictionary) {
for child in Mirror(reflecting: self).children {
guard let key = child.label else {continue}
let value = child.value
let mirror = Mirror(reflecting: value)
let type = String(describing: mirror.subjectType)
let ivar: Ivar = class_getInstanceVariable(type(of: self), key)
let fieldOffset = ivar_getOffset(ivar)
let pointerToInstance = Unmanaged.passUnretained(self).toOpaque().advanced(by: fieldOffset)
let opaquePointer = OpaquePointer(pointerToInstance)
switch type {
case "Int", "Optional<Int>":
//Int型を取得する
let pointerToField = UnsafeMutablePointer<Int?>(opaquePointer)
pointerToField.pointee = data[key] as? Int
continue
case "String", "Optional<String>":
//String型を取得する
let pointerToField = UnsafeMutablePointer<String?>(opaquePointer)
pointerToField.pointee = data[key] as? String
continue
case "Bool", "Optional<Bool>":
//Bool型を取得する
let pointerToField = UnsafeMutablePointer<Bool?>(opaquePointer)
pointerToField.pointee = data[key] as? Bool
continue
default:
break
}
//Arrayを取得する
if type.substring(to: type.index(type.startIndex, offsetBy: 5)) == "Array" ||
type.substring(to: type.index(type.startIndex, offsetBy: 14)) == "Optional<Array" {
if let items = data[key] as? NSArray {
let pointerToField = UnsafeMutablePointer<[AnyObject]>(opaquePointer)
pointerToField.pointee = items as [AnyObject]
continue
}
}
//上記以外でBasePlistEntityを継承している場合
let pattern = "Optional<.*?>"
do {
let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive])
regex.enumerateMatches(in: type, options: [], range: NSRange(location: 0, length: type.characters.count), using: {(result, flag, stop) in
guard let result = result else {return}
for i in 0..<result.numberOfRanges {
let range = result.rangeAt(i)
var className = (type as NSString).substring(with: range)
className = className.substring(with: className.index(className.startIndex, offsetBy: 9)..<className.index(className.endIndex, offsetBy: -1))
//アプリ名を取得
guard let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String else {continue}
let fAppName = appName.replacingOccurrences(of: " ", with: "_", options: .literal, range: nil)
//クラス名を取得して初期化
let objClass = NSClassFromString("\(fAppName).\(className)") as! NSObject.Type
let obj = objClass.init()
guard let items = data[key] as? NSDictionary else {continue}
(obj as! BaseEntity).parse(items)
//AnyObject?でいいのか怪しい。でも意図したとおりに動く。。。
let pointerToField = UnsafeMutablePointer<AnyObject?>(opaquePointer)
pointerToField.pointee = obj as AnyObject
}
})
}catch {
continue
}
}
}
}
Plistを用意する
今回は車を2台所有していて、息子・娘がいる40歳の山口さんを用意しました。
子供のエンティティを用意する
子供のエンティティには名前と年齢を持っています。
import UIKit
class ChildEntity: BaseEntity {
var name : String? //名前
var age : Int? //年齢
}
親のエンティティを用意する
親のエンティティには名前、年齢、所持している車、息子、娘を持っています。
import UIKit
class ParentEntity: BaseEntity {
var name : String? //名前
var age : Int? //年齢
var cars : [AnyObject]? //所有している車
var son : ChildEntity? //息子
var daughter : ChildEntity? //娘
}
使い方
//PlistからNSDictionaryのデータを取得する
guard let path = Bundle.main.path(forResource: "Test", ofType: "plist"), let data = NSDictionary(contentsOfFile: path) else {return}
let parent = ParentEntity()
parent.parse(data)
参考
StackOverflow
Using reflection to set object properties without using setValue forKey