ポインタを利用してPlistをクラスにマッピングしてみる

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歳の山口さんを用意しました。

![スクリーンショット 2016-12-13 0.40.12.png](https://qiita-image-store.s3.amazonaws.com/0/33300/005d5c9a-0c5d-2847-ae1f-2fbbd4174dbe.png)

# 子供のエンティティを用意する
子供のエンティティには名前と年齢を持っています。

```
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](http://stackoverflow.com/questions/31589405/using-reflection-to-set-object-properties-without-using-setvalue-forkey)