LoginSignup
1
2

More than 5 years have passed since last update.

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

Posted at

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

子供のエンティティを用意する

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

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2