Think Big Act Local

iPhone開発を軸にブレブレの記事を書いていきます。

iOSアプリの開発効率をあげるSwiftの便利Extension

Swift の Extension を使うと既存クラスや自作クラスを拡張することができます。
会社や個人のプロジェクトでいつも使っている便利な拡張コードをまとめてみます。

便利な Extension の紹介

クラス名の取得

そのクラス・インスタンスのクラス名を取得するextensionです。

extension NSObject {
    class var className: String {
        get {
            return NSStringFromClass(self).componentsSeparatedByString(".").last!
        }
    }

    var className: String {
        get {
            return self.dynamicType.className
        }
    }
}

以下のような感じで使います。

MyCustomClass.className   // "MyCustomClass"

let myCell = MyCustomClass()
print(myCell.className)   // "MyCustomClass"

TableViewCell の登録と利用

自作クラスを UITableView で扱う場合、

  • あらかじめ registerNibregisterClass で登録し、
  • dequeueReusableCellWithIdentifier して利用する

ことが多いですが、それを簡潔に書けるようにする拡張です。

extension UITableView {
    func registerCell<T: UITableViewCell>(type: T.Type) {
        let className = type.className
        let nib = UINib(nibName: className, bundle: nil)
        registerNib(nib, forCellReuseIdentifier: className)
    }

    func dequeueCell<T: UITableViewCell>(type: T.Type, indexPath: NSIndexPath) -> T {
        return self.dequeueReusableCellWithIdentifier(type.className, forIndexPath: indexPath) as! T
    }
}

自分は「カスタムのCellを扱う場合、ReuseIdentifier はそのカスタムCellのクラス名にする」というルールで統一していて、それに準じたコードとなっています。

以下のように使います。

tableView.registerCell(MyCustomCell.self)  // 登録
let cell = tableView.dequeueCell(MyCustomCell.self)  // 利用

CollectionView の登録と利用を簡単に

上述したものの CollectionView 版です。
UICollectionViewCell に加えて、ReusableView についても定義しています。

extension UICollectionView {
    func registerCell<T: UICollectionViewCell>(type: T.Type) {
        let className = type.className
        let nib = UINib(nibName: type.className, bundle: nil)
        registerNib(nib, forCellWithReuseIdentifier: className)
    }

    func registerReusableView<T: UICollectionReusableView>(type: T.Type, kind: String) {
        let className = type.className
        let nib = UINib(nibName: className, bundle: nil)
        registerNib(nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: className)
    }

    func dequeueCell<T: UICollectionViewCell>(type: T.Type, forIndexPath indexPath: NSIndexPath) -> T {
        return dequeueReusableCellWithReuseIdentifier(type.className, forIndexPath: indexPath) as! T
    }

    func dequeueReusableView<T: UICollectionReusableView>(kind: String, type: T.Type, indexPath: NSIndexPath) -> T {
        return dequeueReusableSupplementaryViewOfKind(kind,
            withReuseIdentifier: type.className,
            forIndexPath: indexPath) as! T
    }
}

使い方は以下の通り。

// Cell
collectionView.registerCell(MyCustomCollectionViewCell.self)
let cell = collectionView.dequeueCell(MyCustomCollectionViewCell.self)

// ReusableView
collectionView.registerReusableView(MyCustomReusableView.self, kind: UICollectionElementKindSectionHeader )
let view = collectionView.dequeueReusableView(UICollectionElementKindSectionHeader, type: MyCustomReusableView.self, indexPath: indexPath)

UIColor を使いやすく

Hexで色を指定したり、アプリでよく使うkeyColorを登録してます。

extension UIColor {
    class func color(hex: Int, alpha: Double = 1.0) -> UIColor {
        let red = Double((hex & 0xFF0000) >> 16) / 255.0
        let green = Double((hex & 0xFF00) >> 8) / 255.0
        let blue = Double((hex & 0xFF)) / 255.0

        return UIColor(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: CGFloat(alpha))
    }

    class func defaultColor(alpha: Double = 1.0) -> UIColor {
        return UIColor.color(0x00ACED, alpha: alpha)
    }
}
// 使い方
let color = UIColor.color(0xCCCCCC)
let defaultColor = UIColor.defaultColor()

現在表示中の ViewController を探す

presentedViewController をできるだけ探索し、表示中の ViewController を返します。

extension UIApplication {
    func topViewController() -> UIViewController? {
        if var topViewController = UIApplication.sharedApplication().keyWindow?.rootViewController {
            while (topViewController.presentedViewController != nil) {
                topViewController = topViewController.presentedViewController!
            }
            return topViewController
        } else {
            return nil
        }
    }
}

UIAlertController と組み合わせて、以下のように使っています。

let alertController = UIAlertController(title: "成功!",
    message: "正常に削除できました",
    preferredStyle: .Alert)
alertController.addAction(UIAlertAction(
    title: "OK",
    style: .Cancel, 
    handler: nil))

UIApplication.sharedApplication().topViewController()?
    .presentViewController(alertController, animated: true, completion: nil)

UIAlertController は必ず ViewController から presentViewController で呼ぶ必要があるため、topViewController() で取得できるようにしておくと昔のUIAlertViewのように使えて便利ですね。

ちなみに UIAlertView / UIActionSheet は iOS 8 から deprecated となっています。

NSUserDefaults で管理する値を拡張で

チュートリアルが完了したか」「初回起動のユーザーか」などの管理に NSUserDefaults を使っていますが、そのフラグを extension のプロパティとして定義してます。

private let isFirstSessionDoneKey = "IS_FIRST_SESSION_DONE"

extension NSUserDefaults {
    var isFirstSessionDone: Bool? {
        get {
            return self.objectForKey(isFirstSessionDoneKey) as? Bool
        }
        set {
            self.setObject(true, forKey: isFirstSessionDoneKey)
            self.synchronize()
        }
    }
}

フラグ系の処理がバラバラにならずまとめて書けるので気に入ってます。管理する数が増えてきたら専用のクラスを作った方が良いかと思います。

// 使い方
NSUserDefaults.standardUserDefaults.isFirstSessionDone = true

Storyboard からのインスタンス作成

開発時の規則として、

  • 基本的に1Storyboard - 1ViewController
  • Storyboardの名前と、initial な ViewController のクラス名を同じにする

というルールがあり、その上で以下の初期化メソッドを定義しています。 (これは extension ではなく大域関数になります)

func instantiate<T: UIViewController where T: NSObject>(_: T.Type) -> T {
    let storyboard = UIStoryboard(name: T.className, bundle: nil)
    return storyboard.instantiateInitialViewController() as! T
}

func instantiate<T: UIViewController where T: NSObject>(_: T.Type, storyboard: String) -> T {
    let storyboard = UIStoryboard(name: storyboard, bundle: nil)
    return storyboard.instantiateViewControllerWithIdentifier(T.className) as! T
}

使い方は以下の通り:

// initial な ViewController を取得
let topVC = instantiate(TopViewController.self)

// initial でない ViewController を取得
// (1Storyboard - 1ViewController でない特別な場合でのみ利用)
let topDetailVC = instantiate(TopDetailViewController.self, storyboard: "TopViewController")

詳細については以下のQiitaが詳しいです。

qiita.com

Nib からのインスタンス作成

カスタムのUIViewなどを扱う場合の記述です。
Storyboard と同じく、「MyCustomView」というクラス名の場合は「MyCustomView.xib」というファイル名にする規則を設けています。

func instantiateFromNib<T: UIView, U: AnyObject where T: NSObject>(_: T.Type, owner: U) -> T {
    return UINib(nibName: T.className, bundle: nil).instantiateWithOwner(owner, options: nil)[0] as! T
}

使い方は以下の通りです。

let myCustomView = instantiateFromNib(MyCustomView.self, owner: self)

追記: namespaceの衝突について

既存クラスを extension で拡張する場合、メソッド名の衝突を気にする必要があります。
自分のいまの環境だと小規模&1人プロジェクトが多いためそのまま使ってますが、規模が大きくなったら Embedded Framework で細かく分けようかと考えていました。

www.toyship.org

ただ、誤って同名で変数やメソッドを定義してしまった場合にはコンパイルエラーを吐いてくれてそこで気づけるので、そこまで気にしなくても良いのかもしれません。

f:id:himaratsu:20151102091512p:plain

まとめ

自分がいつも使う extension を紹介しました。 よく使う処理を簡潔に書けるほか、Storyboard の命名規則や初期化の方法など、extension そのものがコーディング規約となる側面もあり、非常に有用だと感じています。

紹介したすべてのコードは以下のGistからお使いいただけます。

ほかにも便利な extension があるぞ、という方はぜひ教えてください :)

参考資料

おすすめ書籍

基礎から上級へのレベルアップに。

ステップごとに丁寧でわかりやすく、入門の方におすすめです。

関連記事

himaratsu.hatenablog.com

himaratsu.hatenablog.com

himaratsu.hatenablog.com