NSCodingをSwiftyにしてみる
NSCodingについて
Swiftにはクラスをシリアライズ, デシリアライズするためにNSCoding
が用意されている。
そのクラスをシリアライズ, デシリアライズするには、NSCoding
に準拠させ、2つのメソッド(func encode(with aCoder: NSCoder)
とinit?(coder aDecoder: NSCoder)
)を実装する必要がある。
例えばこのような感じ。
class User: NSObject, NSCoding { var id: String! var name: String? init(id: String, name: String) { self.id = id self.name = name } required init?(coder aDecoder: NSCoder) { id = aDecoder.decodeObject(forKey: "id") as! String name = aDecoder.decodeObject(forKey: "name") as? String } func encode(with aCoder: NSCoder) { aCoder.encode(id, forKey: "id") aCoder.encode(name, forKey: "name") } }
シリアライズするときは、func encode(with aCoder: NSCoder)
が呼ばれ、aCoder: NSCode
に対して値とそのキーを指定して、エンコードする。
デシリアライズするときは、init?(coder aDecoder: NSCoder)
が呼ばれ、aDecoder: NSCode
からキーを指定して値をデコードし、クラスのストアドプロパティを満たす形にする。
NSCodingをSwiftyでないポイント
自分は上の例でキーをString
リテラルで指定しているのがSwiftらしくないなと思います。String
リテラルだとタイポをしていてもコンパイルが通ってしまい、意図しない挙動をする可能性があります。なので、Swiftならばenum
でキーを指定したいです。
// デシリアライズ id = aDecoder.decodeObject(forKey: "id") as! String // シリアライズ aCoder.encode(id, forKey: "id")
ならばSwiftyに
// NSCoderのメソッドをラップ extension NSCoder { // RawRepresentableでそのRawValueがStringであるCodingKeyをジェネリクスで指定 func encode<CodingKey: RawRepresentable>(_ object: Any?, forKey key: CodingKey) where CodingKey.RawValue == String { // CodingKeyのRawValueがStringなので、 key.rawValueでキーを指定する encode(object, forKey: key.rawValue) } func decodeObject<CodingKey: RawRepresentable>(forKey key: CodingKey) -> Any? where CodingKey.RawValue == String { return decodeObject(forKey: key.rawValue) } } protocol NSCodingKeyHandleable: NSCoding { // NScodingを継承 // RawRepresentableでそのRawValueがStringであるNSCodingKeyを持たせる associatedtype NSCodingKey: RawRepresentable where NSCodingKey.RawValue == String } class User: NSObject, NSCodingKeyHandleable { var id: String! var name: String? // NSCodingKeyHandleableのNSCodingKeyをenumで定義 enum NSCodingKey: String { case id case name } init(id: String, name: String) { self.id = id self.name = name } required init?(coder aDecoder: NSCoder) { // NSCodingKeyでキーを指定 id = aDecoder.decodeObject(forKey: NSCodingKey.id) as! String name = aDecoder.decodeObject(forKey: NSCodingKey.name) as? String } func encode(with aCoder: NSCoder) { // NSCodingKeyでキーを指定 aCoder.encode(id, forKey: NSCodingKey.id) aCoder.encode(name, forKey: NSCodingKey.name) } }
classにenum NSCodingKey: String
をキーを定義させ、それを使用して、シリアライズ, デシリアライズできるようにした。キーをString
にさせるように、各箇所でRawRepresentable
かつそのRawValue
がString
になるように制約をつけている。
ちなみに実行はこんな感じ。
let user = User(id: "123", name: "culumn") let userData = NSKeyedArchiver.archivedData(withRootObject: user) let decodedUser = NSKeyedUnarchiver.unarchiveObject(with: userData) as! User print(decodedUser.id) // 123 print(decodedUser.name) // Optional("culumn")
まとめ
実はこれを考えてる途中でNSCoding
を使うのならSwift4で追加されたCodable
使ったほうがいいのでは気づいてしまった。また、調べてみるとCodable
を使ったほうがシリアライズしたときのバイナリサイズが小さいらしい。
qiita.com
そうは言うものの、どうしたらSwiftyにできるかを考えること自体は楽しかったので良しとしたい。今後NSCoding
を使う機会があればこのパターンを使ってみたいと思う。
Swiftで型推論を使っていきたい
省略について
Swiftには型名.varibaleHoge
と書くところを型がわかっている場合は.varibaleHoge
と書ける。
例えば以下のような感じ。
// textAlignmentはNSTextAlignment(enum)で、centerが定義されている UILabel().textAlignment = .center // centerはCGPointで、zeroがstatic varで定義されている UILabel().center = .zero
型推論を使えるようにする
上の例ではenum
やstatic var
で既に定義してあるものを省略して書いている。これを自分たちで定義することで便利に呼び出すことが出来る。
例えばアプリで使う色が決まっている場合は以下のようにできる。
// MARK: - App color scheme extension UIColor { /// #FFFA89 static let yellowBackground = #colorLiteral(red: 1, green: 0.9803921569, blue: 0.537254902, alpha: 1) /// #A66BFF static let purpleButton = #colorLiteral(red: 0.6509803922, green: 0.4196078431, blue: 1, alpha: 1) /// #FF1A1B static let redError = #colorLiteral(red: 1, green: 0.1019607843, blue: 0.1058823529, alpha: 1) } // static letで定義したので省略して書ける // また直接カラーリテラルを代入するのと違い、変数名で色の役割がわかる UILabel().backgroundColor = .yellowBackground UILabel().textColor = .redError
また自分が先日使えると思ったのを書いてみる。NotificationCenterで独自に通知名を指定し、処理する例である。通常であれば下のように書くと思う。.init("appHogeEvent")
で独自の通知名を指定しているが、"appHogeEvent"
でタイポしてしまうと処理されなくなってしまう。
NotificationCenter.default.post(name: .init("appHogeEvent"), object: nil)
これをenum
とstatic let
を使うことで以下のように書ける。
enum AppNotificationName: String { case hogeEvent } // MARK: - App custom notification extension Notification.Name { // AppNotificationNameを使ってイニシャライズできるようにする init(appNotificationName: AppNotificationName) { self.init(appNotificationName.rawValue) } static let appHogeEvent = Notification.Name(appNotificationName: .hogeEvent) } // static letで定義したappHogeEventを型推論を書く NotificationCenter.default.post(name: .appHogeEvent, object: nil)
まとめ
自分でextension
を使って拡張することで便利に型推論を使うことが出来る。タイプ数を減らすだけでなく、変数名で役割を明確にしタイポも減らすことが出来るので使っていきたいという話。
UITextFieldを空の状態からデリートキーを検出する
文字入力の検出
まずUITextField
において文字入力を検出するには主に2つの方法がある。
一つはUITextFieldDelegate
を準拠し、textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool
を用いる方法。もう一つはUIControl
にあるaddTarget(_:action:for:)
を以下のように書いて、テキストの内容が変更したタイミングで#selector
で指定した関数でハンドリングする。
let textField = UITextField() textField.addTarget(self, action: #selector(textFieldDidChangeText(_:)), for: .editingChanged)
自分もいずれかの方法で文字入力のハンドリングをしていたが、ある時一つの問題を発見した。それは、テキストが空の状態からデリートキーの入力を検出できないことだ。
自分が直面したケースとしては、複数のテキストフィールドを用いてPINの入力する画面である。1桁入力するごとに次のテキストフィールドに移動し、デリートキーを入力すれば前のテキストフィールドに戻るといったものを実装しようとしたところ問題を見つけた。
今フォーカスされているテキストフィールドには文字がないので、デリートキーを検出できなかった。
テキストが空の状態からデリートキーを検出する
初めに紹介した2つの方法では無理なので調べていたら解決法を見つけた。UITextField
においてデリートキーはUIKeyInput
にあるdeleteBackward()
を呼ぶことで入力しているらしい。つまりUITextField
を継承したクラスを作成し、deleteBackward()
をオーバライドして扱えるようにすればいい。そして、独自のDelegateを用意しそのタイミングで外のクラスに処理を委譲すればいい。具体的には以下のようなコードになる。
protocol CustomTextFieldDelegate: class { func didDeleteBackward(_ textField: CustomTextField) } class CustomTextField: UITextField { weak var deletionDelegate: CustomTextFieldDelegate? override func deleteBackward() { super.deleteBackward() deletionDelegate?.didDeleteBackward(self) } }
また、これを使いframeworkにしたものサンプル付きでgithubに上げた。(簡単とは言え初めてframeworkを作った) github.com
参考
Xcodeの画像、色リテラルを使う
画像、色リテラルとは
名前の通り画像や色をリテラルとして扱えるようにしたもの。Xocde8からの機能だが使っておらず、最近使い始めたので紹介する。
画像リテラル
アセットに登録した画像名をタイプすると、その画像をプレビューしたリテラルが補完に出てくる。これを選択することで画像リテラルとして使えるようになる。
メリット
- コード上から使う画像を確認することができる。今までだと以下のようなコードで画像名を指定するだけだったので、当然画像をコード上から確認できなかった。また、画像名のタイポも防ぐことができる。
let image = UIImage(named: "MyLogo")
デメリット
- 画像によってはリテラルとして見えにくい。画像の色合いやサイズがあるので、これはしかたない。
色リテラル
色リテラルもColor
までタイプすると補完で出てくるのでそれを選択する。
それからそのリテラルをダブルクリックで展開し、色を指定する。
メリット
- こちらも同様だがコード上から使う色を確認することができる。また、下のようなコードに比べればタイプ数が減る。
view.backgroundColor = UIColor(red: redColor, green: greenColor, blue: blueColor, alpha: 1.0)
デメリット
- 個人的にはないと思う。
まとめ
最近ではリソースマネージャーライブラリとしてはR.swiftが優秀で使っているが、色や画像に関してはリテラルを使ったほうが良いと感じた。個人的にはもちろんStoryboardから設定できるのであれば設定した方がコード数も減って良いと思う。
おまけ
もちろんこのリテラル機能はXcode独自のものであり、他のエディタからこのようには見えない。例えば、一番最初の画像にあるコードはこのような感じで記述されている。
view.backgroundColor = #colorLiteral(red: 0.1450980392, green: 0.5490196078, blue: 0.6980392157, alpha: 1) logoImageView.image = #imageLiteral(resourceName: "MyLogo")
Swift Package Manager対応フレームワークをCarthageにも対応させる
Swift Package Managerについて
Swiftが標準で提供しているライブラリ管理ツール。SPMとも略される。
フレームワークを配布するには、Package.swift
にフレームワークの依存関係などを書いてSources/
にソースコードを置く。さらにそのディレクトリをgithubにあげる。公式README
- Project Directory
- Package.swift
- Sources/
- Tests/
Carthageについて
こちらも有名どころのライブラリ管理ツール。プロジェクトのビルドが早くなることから好む人も多いと思う。フレームワークを配布するには、まず プロジェクト名.xcodeproj
を置き、そのなかでフレームワークのSchemeのSharedにチェックを入れる。そして、それをgithubにあげる。
公式README
SPM → Carthageの問題
SPMはSources/
にソースコード置くだけで.xcodeproj
が存在しないためCarthage対応させることができない。
SPM → Carthageの解決
つまりはSPMのソースに対して.xcodeproj
を作れば良い。
SPMにはソースから.xcodeproj
を生成するコマンドが標準であるのでそれを使う。
$ swift package generate-xcodeproj
それからSchemeの設定をしてgithubにあげれば終わりです。
まとめ
自分はよくCarthageを使っていて、githubで使いたいフレームワークを見つけてもCarthage対応してないものを見つけては残念に思っていました。解決できないかなーと公式のREADMEを読んで今回の方法を思いつきました。
また、今回の方法を用いて人生で初めてOSSにプルリク作ってみました。 IdleHandsApps/IHKeyboardAvoiding#45
ちなみにCocoapodsに関しては知識がないので今回触れてません。m(__)m
参考
GitHub - apple/swift-package-manager: The Package Manager for the Swift Programming Language
GitHub - Carthage/Carthage: A simple, decentralized dependency manager for Cocoa
Swiftのoptional funcでenumを使いたい時
protocolについて
まずswiftにはprotocolが用意されている。例えばUIKit
系のクラスではよくdelegateやdataSourceが用意されていて、他のクラスに処理を委譲する設計になっている。delegateやdataSourceを継承したクラスはその処理を書かなければならない。
class HogeViewController: UIViewController, UITableViewDataSource { // UITableViewDataSource内で定義された関数をこのクラス内で実装しなければいけない // 実装しなければコンパイルエラーが起こる func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {} func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {} }
optional funcを定義する
上でprotocolについて説明したが、継承しても実装しなくても大丈夫なようにもできる。optional
を修飾子としてつければ良い。
// optional funcを定義するにはそれとprotocolに@objc属性をつけなければいけない @objc protocol HogeDelegate { @objc optional func doSomething() }
optional funcにenumを使う
通常のprotocol(@objc
属性がついてない)ものであればenumを使ってもエラーにはならないが、@objc
属性がついているものはエラーになってしまう。
enum MyType { case A case B } @objc protocol HogeDelegate { @objc optional func didChange(type: MyType) // Method cannot be marked @objc because the type of the parameter cannot be represented in Objective-C }
これの解決策はenumにも@objc
属性をつける。ただし、注意点としてそのenumにはInt
を継承させなければいけない。
@objc enum MyType: Int { case A case B } @objc protocol HogeDelegate { @objc optional func didChange(type: MyType) }
まとめ
optional funcにenumを使いたければ、protocolとenumに@objc
属性をつければ良い。
コード例は抽象的だが、自分でカスタムUIクラスなど作るときに使えると思う。
最後にswiftにはenumやprotcolといった魅力的な機能があるが、それにわざわざ@objc
属性を付けるのは少し面倒くさいと思ってしまった。
UIColorからCIColorの変換で少しはまった話
エラー概要
先日iOSアプリの開発中にCIColor
型で黒色の値を取得しようとして次のようなコードを書いた。
let blackColor: CIColor = UIColor.black.ciColor
ビルドしてもエラーがないので、このままRun出来ると思ったらこのエラーでアプリが落ちた。
reason: '*** -CIColor not defined for the UIColor UIExtendedGrayColorSpace 0 1; need to first convert colorspace.'
解決策
上のエラーをググれば案外簡単に出てくる。どうやらUIColor
のciColor
のプロパティは使えないらしいので、CIColor(cgColor: CGColor)
を使う必要がある。
let blackColor: CIColor = CIColor(cgColor: UIColor.black.cgColor)
まとめ
このエラーのやっかいなところはビルドは通るが、Run時に初めて分かるところ。
単にCIColor
を使いたいのであれば
let blackColor: CIColor = CIColor.black
としたほうがよい。