SwiftyNote

主にSwiftな技術ブログ

【Swift3】enumでJSONデコーダを作ってみる

JSONデコーダenumで作成したらどうなるかふと思ったので作ってみることに。 最少限度の実装でまずはできるか試して、今度検証したりするとき用のメモ程度に。

個人的にはindirectJSONの階層構造を表現することでちょっとだけ高速になるかもしれないと考えた。 JSONではjson["user"]["name"]のように連想配列的に値にアクセスできる。また、このように同じ階層の値がさらに子を持つことがよくある。連想配列ではハッシュにより任意のキーの値へのアクセスのオーダーはO(1)である、しかし実装によっては(※1)階層が増えれば増えるほどキーアクセスはO(n)となる。なので階層が深いキーなどは通過時に一度パスを保持しておくことでアクセス数が減るのではないかと思ったが、多分、数MB以上のJSONなどでない限り効果が薄すぎてあまり意味ないかもしれない。 とりあえず、まず実用的かは考えずに実装してみる。(indirectを使うとは…言っていない)

// `T`型に変換または`invalidPropertyType`をthrow
func castOrFail<T>(_ any: Any) throws -> T {
    guard let result = any as? T else {
        throw JSONParseError.invalidPropertyType
    }
    return result
}

protocol Decodable {
    static func decode(_ json: JSON) throws -> Self
}

// JSONの値は`String`に変換可能
extension String: Decodable {
    static func decode(_ json: JSON) throws -> String {
        return try castOrFail(json)
    }
}

// JSONの値は`Int`に変換可能
extension Int: Decodable {
    static func decode(_ json: JSON) throws -> Int {
        return try castOrFail(json)
    }
}

// 適当なエラーを定義
enum JSONParseError: Error {
    case invalidJSON
    case invalidPropertyType
}

enum JSON {
    typealias RawValue = Any?

    case json([String: Any])
    
    init?(rawValue: RawValue) {
        guard let json = rawValue as? [String: Any] else { return nil }
        self = .json(json)
    }

    func decode<T: Decodable>(path: String) throws -> T {
        if case .json(let json) = self {
            for node in json {
                if path == node.key { return try castOrFail(node.value) }
                if let newJSON = JSON(rawValue: node.value) {
                    let _: T = try newJSON.decode(path: path)
                }
            }
        }
        throw JSONParseError.invalidJSON
    }

}

// カスタムオペレータの定義
infix operator <| : MultiplicationPrecedence

func <| <T: Decodable>(rhs: JSON, lhs: String) throws -> T {
    return try rhs.decode(path: lhs)
}

// モデルを定義
struct User: Decodable {
    let name: String
    let age: Int
    static func decode(_ j: JSON) throws -> User {
        return try User(
            name: j <| "name",
            age: j <| "age")
    }
}

// JSONデータを用意
let json: Any = [
    "name": "User500",
    "age": 18
]

// デコード
if let json = JSON(rawValue: json),
    let user = try? User.decode(json) {
        print(user.name) // -> User500
        print(user.age) // -> 18
}

とりあえずAny型のJSONデータをデコードできるenumが完成した。あとはネストを考慮したり再起部分を繰り返し処理に変えればもうちょっとましになるかもしれない。 さらに冒頭で考えてた実装をまだ実現できていないので、まだまだ改良の余地がありそう。(それがやりたいなら別にenum使わなくても良かった…)

※1…内部的にキーを連結したものからハッシュが計算可能であればこの限りではない

UITextViewでハッシュタグとメンションを色を変えて表示とタップ検知をする

UITextViewで複数のハイライトを表現したい

最近の実装でSNSなどの普及でか#swiftなどのハッシュタグに加えて@rinovなど個別にハイライトして、タップしたときの挙動も柔軟に指定したい時がありました。 基本的には標準のNS***AttributeNameなどで該当の単語ごとにNSRangeを求めて属性を追加するのですが、この実装方法はかなり良くないと感じました。

Attribute Stringの問題点

UITextViewを用いて文字を装飾したい場合にはAttributedStringで属性と値を指定することで実現します。

例: UITextViewのテキストを赤くする

textView.text = "Hello World"
let attributedText = textView.mutableCopy() as? NSMutableAttributedString
attributedText.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: NSMakeRange(0, 5))
textView.attributedText = attributedText

問題点

  • NS***AttributeNameというグローバルな値が属性のキーになっているため一覧性がない
  • UITextViewaddAttributeは値がAny型となっているため、例えばテキストカラーを設定するときにUIColorが設定されることが自明でありながら.redなどの省略が行えない。また、どんな値でも引数に渡せてしまい安全でない。
  • NSMutable~, NSRange, NS***AttributeNameなどObjective-Cの遺産を多く使用している。
  • ハイライトする単語のNSRangeを毎回算出するのが面倒である

上記の例では色を付けるだけでしたが多くの場合特定のキーワードを個別にハイライトしたりタップ時の挙動を柔軟に変更したいです。 UITextViewのプロパティとしてlinkTextAttributesというものがありますが、これはリンク属性の付いたテキストの見た目を一括で装飾の指定を行うことができますが、例えば、ハッシュタグは青色、メンションは赤色など個別に対応した属性の指定を行うことはできません。

// 単語ごとに個別の指定は行えない
textView.linkTextAttributes = [NSForegroundColorAttributeName: UIColor.red]

こういったObjective-CとSwiftの間でコーディングしている感があり、今後Swiftのバージョンアップした時に既存のコードを全て実装しなおすリスクも考えるとなかなか気が進みません。 そこで最近のリッチなテキストを柔軟にカスタマイズしたいと思い、UITextViewで実装したものをライブラリ化しました。

RegeributedTextView

https://github.com/rinov/RegeributedTextView

使い方はREADME.mdまたはhttp://qiita.com/rinov/items/da450d0ba9dffaaf8967にまとめました。

調べた限りでもTTTAttributedLabelなど有名なライブラリ(UILabel専用)がありますが、Swift製で正規表現 + 独自のTextAttributeプロパティを追加しているライブラリはありませんでした。複数単語のハイライトやタップした単語ごとに任意の処理をしたいなどがありましたら一度使用してみていただけると嬉しいです。 またGithubでIssueやPR等いただけるとさらに嬉しいです。

github.com