iOS
Swift

使うと手放せなくなるSwift Extension集 (Swift2版)

More than 1 year has passed since last update.

2016/8/15更新

汎用性高めのExtension集です。後半にライブラリもまとめました。
Swift2.2で確認済みです。

紹介している全てのextensionを導入したリポジトリを公開しています(紹介していないものも含みます)。
詳しくは下記をご覧ください。
iOSアプリをハッカソンで作るときに便利な初期プロジェクトを作ってみた

Swift3版はこちらです。

クラス名の取得

extension NSObject {
    class var className: String {
        return String(self)
    }

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

MyClass.className   //=> "MyClass"
MyClass().className //=> "MyClass"

XIBの登録・取り出し

XIBファイルとクラスを同じ名前にして利用してください。
上記の「クラス名の取得」を利用しています。

UITableView

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 registerCells<T: UITableViewCell>(types: [T.Type]) {
        types.forEach { registerCell($0) }
    }

    func dequeueCell<T: UITableViewCell>(type: T.Type, indexPath: NSIndexPath) -> T {
        return self.dequeueReusableCellWithIdentifier(type.className, forIndexPath: indexPath) as! T
    }
}
tableView.registerCell(MyCell.self)
tableView.registerCells([MyCell1.self, MyCell2.self])

let cell = tableView.dequeueCell(MyCell.self)

UICollectionView

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

    func registerCells<T: UICollectionViewCell>(types: [T.Type]) {
        types.forEach { registerCell($0) }
    }

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

    func registerReusableViews<T: UICollectionReusableView>(types: [T.Type], kind: String = UICollectionElementKindSectionHeader) {
        types.forEach { registerReusableView($0, kind: kind) }
    }

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

    func dequeueReusableView<T: UICollectionReusableView>(type: T.Type, indexPath: NSIndexPath, kind: String = UICollectionElementKindSectionHeader) -> T {
        return dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: type.className, forIndexPath: indexPath) as! T
    }
}
collectionView.registerCell(MyCell.self)
collectionView.registerCells([MyCell1.self, MyCell2.self])
let cell = collectionView.dequeueCell(MyCell.self)

collectionView.registerReusableView(MyReusableView.self)
collectionView.registerReusableViews([MyReusableView1.self, MyReusableView2.self])
let view = collectionView.dequeueReusableView(type: MyReusableView.self, indexPath: indexPath)

16進数でUIColorの作成

extension UIColor {
    convenience init(hex: Int, alpha: Double = 1.0) {
        let r = CGFloat((hex & 0xFF0000) >> 16) / 255.0
        let g = CGFloat((hex & 0x00FF00) >> 8) / 255.0
        let b = CGFloat(hex & 0x0000FF) / 255.0
        self.init(red: r, green: g, blue: b, alpha: CGFloat(alpha))
    }
}
let color = UIColor.color(0xAABBCC)

最前面のUIViewControllerを取得

extension UIApplication {
    var topViewController: UIViewController? {
        guard var topViewController = UIApplication.sharedApplication().keyWindow?.rootViewController else { return nil }

        while let presentedViewController = topViewController.presentedViewController {
            topViewController = presentedViewController
        }
        return topViewController
    }

    var topNavigationController: UINavigationController? {
        return topViewController as? UINavigationController
    }
}
UIApplication.sharedApplication().topViewController

StoryboardのViewControllerを生成

protocol StoryBoardHelper {}

extension StoryBoardHelper where Self: UIViewController {
    static func instantiate() -> Self {
        let storyboard = UIStoryboard(name: self.className, bundle: nil)
        return storyboard.instantiateViewControllerWithIdentifier(self.className) as! Self
    }

    static func instantiate(storyboard: String) -> Self {
        let storyboard = UIStoryboard(name: storyboard, bundle: nil)
        return storyboard.instantiateViewControllerWithIdentifier(self.className) as! Self
    }
}

extension UIViewController: StoryBoardHelper {}
MyViewController.instantiate() // Storyboardファイルとクラスが同じ名前の場合
MyViewController.instantiate("MyStoryboard")

※ 上記の「クラス名の取得」を利用しています。

XIBのViewを生成

protocol NibHelper {}

extension NibHelper where Self: UIView {
    static func instantiate() -> Self {
        let nib = UINib(nibName: self.className, bundle: nil)
        return nib.instantiateWithOwner(nil, options: nil)[0] as! Self
    }
}

extension UIView: NibHelper {}
MyView.instantiate()

※ XIBファイルとクラスは同じ名前にしてください。
※ 上記の「クラス名の取得」を利用しています。

子Viewを全て削除

extension UIView {
    func removeAllSubviews() {
        for subview in self.subviews {
            subview.removeFromSuperview()
        }
    }
}
view.removeAllSubViews()

Selectorを集約化

private extension Selector {
    static let buttonTapped = #selector(MyViewController.buttonTapped(_:))
}
let button = UIButton()
button.addTarget(self, action: .buttonTapped, forControlEvents: .TouchUpInside)

配列でオブジェクトのインスタンスを検索して削除

extension
extension Array where Element: Equatable {
    mutating func remove(element element: Element) -> Bool {
        guard let index = indexOf(element) else { return false }
        removeAtIndex(index)
        return true
    }

    mutating func remove(elements elements: [Element]) {
        for element in elements {
            remove(element: element)
        }
    }
}
let array = ["foo", "bar"]
array.remove(element: "foo")

Out of Rangeを防いで、要素を取得

extension
extension CollectionType {
    subscript (safe index: Index) -> Generator.Element? {
        get {
            return indices.contains(index) ? self[index] : nil
        }
    }
}
let array = [0, 1, 2]
if let item = array[safe: 5] {
    // unreachable
}

2つのDictionaryを結合

extension
extension Dictionary {
    mutating func merge<S: SequenceType where S.Generator.Element == (Key, Value)>(sequence: S) {
        for (key, value) in sequence {
            self[key] = value
        }
    }
}
var dic1 = ["key1": 1]
let dic2 = ["key2": 2]
dic1.merge(dic2) // => ["key1": 1, "key2": 2]

指定範囲内に値を収める

Viewの配置をコードで実装するときに便利です。

extension
extension Comparable {
    func clamped(min min: Self, max: Self) -> Self {
        if self < min {
            return min
        }

        if self > max {
            return max
        }

        return self
    }
}
let x: CGFloat = 20
x.clamped(min: 0, max: 10) // => 10

NSLocalizedStringを使いやすくする

extension
extension String {
    var localized: String {
        return NSLocalizedString(self, comment: self)
    }

    func localizedWithOption(tableName tableName: String? = nil, bundle: NSBundle = NSBundle.mainBundle(), value: String = "") -> String {
        return NSLocalizedString(self, tableName: tableName, bundle: bundle, value: value, comment: self)
    }
}
let message = "Hello".localized

クラスのプロパティを全て出力

extension
extension NSObjectProtocol where Self: NSObject {
    var description: String {
        let mirror = Mirror(reflecting: self)
        return mirror.children.map { element -> String in
            let key = element.label ?? "Unknown"
            let value = element.value
            return "\(key): \(value)"
        }
        .joinWithSeparator("\n")
    }
}
class Hoge: NSObject {
    var foo = 1
    let bar = "bar"
   }
}

Hoge().description // => "foo: 1\nbar: bar"

文字列からNSURLを取得

extension
extension String {
    var url: NSURL? {
        return NSURL(string: self)
    }
}
if let url = "https://example.com".url {
}

UIAlertControllerをBuilderパターンっぽく扱う

Androidアプリも書いている方は欲しくなるはずです。。。

extension
extension UIAlertController {

    func addAction(title title: String, style: UIAlertActionStyle = .Default, handler: (UIAlertAction -> Void)? = nil) -> Self {
        let okAction = UIAlertAction(title: title, style: style, handler: handler)
        addAction(okAction)
        return self
    }

    func addActionWithTextFields(title title: String, style: UIAlertActionStyle = .Default, handler: ((UIAlertAction, [UITextField]) -> Void)? = nil) -> Self {
        let okAction = UIAlertAction(title: title, style: style) { [weak self] action in
            handler?(action, self?.textFields ?? [])
        }
        addAction(okAction)
        return self
    }

    func configureForIPad(sourceRect: CGRect, sourceView: UIView? = nil) -> Self {
        popoverPresentationController?.sourceRect = sourceRect
        if let sourceView = UIApplication.sharedApplication().topViewController?.view {
            popoverPresentationController?.sourceView = sourceView
        }
        return self
    }

    func configureForIPad(barButtonItem: UIBarButtonItem) -> Self {
        popoverPresentationController?.barButtonItem = barButtonItem
        return self
    }

    func addTextField(handler: UITextField -> Void) -> Self {
        addTextFieldWithConfigurationHandler(handler)
        return self
    }

    func show() {
        UIApplication.sharedApplication().topViewController?.presentViewController(self, animated: true, completion: nil)
    }
}
usage
UIAlertController(title: "ログイン", message: "IDを入力してください", preferredStyle: .Alert)
.addTextField { textField in
    textField.placeholder = "ID"
}
.addActionWithTextFields(title: "OK") { action, textFields in
    // validation
}
.addAction(title: "キャンセル", style: .Cancel)
.show()

UIViewのスクショを撮る

extension
extension UIView {
    func screenShot(width width: CGFloat) -> UIImage? {
        let imageBounds = CGRect(x: 0, y: 0, width: width, height: bounds.size.height * (width / bounds.size.width))

        UIGraphicsBeginImageContextWithOptions(imageBounds.size, true, 0)

        drawViewHierarchyInRect(imageBounds, afterScreenUpdates: true)

        var image: UIImage?
        let contextImage = UIGraphicsGetImageFromCurrentImageContext()

        if let cgImage = contextImage.CGImage {
            image = UIImage(
                CGImage: cgImage,
                scale: UIScreen.mainScreen().scale,
                orientation: contextImage.imageOrientation
            )
        }

        UIGraphicsEndImageContext()

        return image
    }
}
usage
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
label.text = "Hello"
label.textAlignment = .Center
label.backgroundColor = .whiteColor()

let image = label.image(width: 200)

ライブラリ

開発を楽にしてくれる汎用性の高いextension系のライブラリをまとめました。

SwiftDate

https://github.com/malcommac/SwiftDate

NSDateの扱いを簡単にしてくれるライブラリ。

let date1 = NSDate(year: 2016, month: 12, day: 25, hour: 14)
let date2 = "2016-01-05T22:10:55.200Z".toDate(DateFormat.ISO8601)
let date3 = "22/01/2016".toDate(DateFormat.Custom("dd/MM/yyyy"))
let date4 = (5.days + 2.hours - 15.minutes).fromNow
let date5 = date4 + 1.years + 2.months + 1.days + 2.hours

より詳しく知りたい方はこちら

Chameleon

https://github.com/ViccAlexander/Chameleon

Chameleon.png

いい感じのフラットカラーを用意してくれるライブラリ。

let color1 = UIColor.flatGreenColorDark()
let color2 = FlatGreenDark() // 上の短縮版
let color3 = RandomFlatColor()
let color4 = ComplementaryFlatColorOf(color1) // 補色

UIColor.pinkColor().flatten()
FlatGreen.hexValue //=> "2ecc71"
UIColor(averageColorFromImage: image)

コントロールの色を一括変更することもできます。

Chameleon.setGlobalThemeUsingPrimaryColor(FlatBlue(), withSecondaryColor: FlatMagenta(), andContentStyle: UIContentStyle.Contrast)

R.swift

https://github.com/mac-cain13/R.swift

AndroidのR.javaのようにファイル名などをプロパティ化してくれるライブラリ。
Typoがコンパイル時にわかるので、幸せになれます。

Before:

let icon = UIImage(named: "settings-icon")
let font = UIFont(name: "San Francisco", size: 42)
let viewController = CustomViewController(nibName: "CustomView", bundle: nil)
let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.currentLocale(), "Arthur Dent")

After:

let icon = R.image.settingsIcon()
let font = R.font.sanFrancisco(size: 42)
let viewController = CustomViewController(nib: R.nib.customView)
let string = R.string.localizable.welcomeWithName("Arthur Dent")

SwiftString

https://github.com/amayne/SwiftString

Stringに便利なメソッドを追加してくれるライブラリ。

"foobar".contains("foo")         //=> true
",".join([1,2,3])                //=> "1,2,3"
"hello world".split(" ")[1]      //=> "world"
"hello world"[0...1]             //=> "he"
"hi hi ho hey hihey".count("hi") //=> 3

SwiftyUserDefaults

https://github.com/radex/SwiftyUserDefaults

NSUserDefaultsをSwiftっぽく扱えるライブラリ。

extension DefaultsKeys {
    static let username = DefaultsKey<String?>("username")
    static let launchCount = DefaultsKey<Int>("launchCount")
}

// 取得と設定
let username = Defaults[.username]
Defaults[.hotkeyEnabled] = true

// 値の変更
Defaults[.launchCount]++
Defaults[.volume] += 0.1
Defaults[.strings] += "… can easily be extended!"

// 配列の操作
Defaults[.libraries].append("SwiftyUserDefaults")
Defaults[.libraries][0] += " 2.0"

// カスタム型もOK
Defaults[.color] = NSColor.whiteColor()
Defaults[.color]?.whiteComponent // => 1.0

TextAttributes

https://github.com/delba/TextAttributes

TextAttributes.png

NSAttributedStringを簡単に設定できるライブラリ。

let attrs = TextAttributes()
    .font(name: "HelveticaNeue", size: 16)
    .foregroundColor(white: 0.2, alpha: 1)
    .lineHeightMultiple(1.5)

NSAttributedString("ほげ", attributes: attrs)

Async

https://github.com/duemunk/Async

Grand Central Dispatch (GCD) を使いやすくするライブラリ。

Before:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {
    print("This is run on the background queue")

    dispatch_async(dispatch_get_main_queue(), {
        print("This is run on the main queue, after the previous block")
    })
})

After:

Async.background {
    print("This is run on the background queue")
}.main {
    print("This is run on the main queue, after the previous block")
}

AsyncKit

https://github.com/mishimay/AsyncKit
http://qiita.com/mishimay/items/7df447969a1c38d856d8

複数の非同期処理の終了後に次の処理を行うことができるライブラリ。

let async = AsyncKit<String, NSError>()

async.parallel([
    { done in done(.Success("one")) },
    { done in done(.Success("two")) }
]) { result in
    print(result) //=> Success(["one", "two"])
}
```