CharacterSetを定義する

CharacterSetとは

言葉通り文字の集合である。Appleのドキュメントにも書いてあるが、文字列の中から特定の文字を検索したいときなどに使う構造体。自分も以前そのような用途で使ったことがある。
CharacterSet - Foundation | Apple Developer Documentation

CharacterSetを定義する

自分は以前CharacterSetを使用したことがあるが、static varで既に定義されているdecimalDigitsalphanumericsしか使ったことがない。しかし、先日ドキュメントを眺めていたら、コンストラクタinit(charactersIn:)を呼べば、自分でも定義できることを知った。

// 文字a, b, cを含んだ文字の集合
let abcSet = CharacterSet(charactersIn: "abc")

CharacterSetを使用する例

自分でCharacterSetを定義できれば、そのCharacterSetを使い文字の検索ができる。例えばこんな感じ。

extension CharacterSet {
    // 16進数で使用する文字の集合を定義
    static var hexDigits = CharacterSet(charactersIn: "0123456789ABCDEFabcdef")
}

func checkHexDigits(from string: String) {
    // 文字列に16進数表す文字以外が含まれていないかチェック
    if string.rangeOfCharacter(from: CharacterSet.hexDigits.inverted) != nil {
        print("target string(\(string)) contains non hex digits")
    }
}

var colorCode = "ffFFff"
checkHexDigits(from: colorCode)

colorCode = "ffFFfg"
checkHexDigits(from: colorCode)

実際にPlaygroundで実行すると以下となる。 "ffFFfg"の'g'はhexDigitsに含まれてないので出力される。 f:id:culumn:20180531230628p:plain

アプリを開発する際も、このようにCharacterSetを定義して、バリデーションなどに使えると思う。

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かつそのRawValueStringになるように制約をつけている。

ちなみに実行はこんな感じ。

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

型推論を使えるようにする

上の例ではenumstatic 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)

これをenumstatic 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桁入力するごとに次のテキストフィールドに移動し、デリートキーを入力すれば前のテキストフィールドに戻るといったものを実装しようとしたところ問題を見つけた。
今フォーカスされているテキストフィールドには文字がないので、デリートキーを検出できなかった。

f:id:culumn:20180223012201p:plain

f:id:culumn:20180223012156p:plain

テキストが空の状態からデリートキーを検出する

初めに紹介した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からの機能だが使っておらず、最近使い始めたので紹介する。 f:id:culumn:20180211221647p:plain

画像リテラル

アセットに登録した画像名をタイプすると、その画像をプレビューしたリテラルが補完に出てくる。これを選択することで画像リテラルとして使えるようになる。 f:id:culumn:20180211214722p:plain

メリット

  • コード上から使う画像を確認することができる。今までだと以下のようなコードで画像名を指定するだけだったので、当然画像をコード上から確認できなかった。また、画像名のタイポも防ぐことができる。
let image = UIImage(named: "MyLogo")
  • 正直これを一番推したくて、実は画像リテラルUIImage型でnilチェックする必要がない。上の画像名を指定するコードのimageUIImage?でオプショナル型である。

デメリット

  • 画像によってはリテラルとして見えにくい。画像の色合いやサイズがあるので、これはしかたない。

リテラル

リテラルColorまでタイプすると補完で出てくるのでそれを選択する。 f:id:culumn:20180211221824p:plain それからそのリテラルをダブルクリックで展開し、色を指定する。 f:id:culumn:20180211222145p:plain

メリット

  • こちらも同様だがコード上から使う色を確認することができる。また、下のようなコードに比べればタイプ数が減る。
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/

github.com

Carthageについて

こちらも有名どころのライブラリ管理ツール。プロジェクトのビルドが早くなることから好む人も多いと思う。フレームワークを配布するには、まず プロジェクト名.xcodeprojを置き、そのなかでフレームワークSchemeのSharedにチェックを入れる。そして、それをgithubにあげる。 公式README

f:id:culumn:20171207205410p:plain

github.com

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

CocoaPods.org

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 属性を付けるのは少し面倒くさいと思ってしまった。