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

  • 398
    いいね
  • 2
    コメント

便利で汎用性高めのExtension集です。後半にライブラリもまとめました。
Swift3.0で動作確認をしています。

以前投稿した使うと手放せなくなるSwift Extension集 (Swift2版)のSwift3版で、命名規則などに変更があります。

※強制アンラップを使用しているので、状況に合わせて変更して利用してください。

クラス名の取得

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

    var className: String {
        return type(of: self).className
    }
}

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

XIBの登録・取り出し

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

UITableView

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

    func register<T: UITableViewCell>(cellTypes: [T.Type]) {
        cellTypes.forEach { register(cellType: $0) }
    }

    func dequeueReusableCell<T: UITableViewCell>(with type type: T.Type, for indexPath: IndexPath) -> T {
        return self.dequeueReusableCell(withIdentifier: type.className, for: indexPath) as! T
    }
}

usage
tableView.register(cellType: MyCell.self)
tableView.register(cellTypes: [MyCell1.self, MyCell2.self])

let cell = tableView.dequeueReusableCell(with: MyCell.self, for: indexPath)

UICollectionView

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

    func register<T: UICollectionViewCell>(cellTypes: [T.Type]) {
        cellTypes.forEach { register(cellType: $0) }
    }

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

    func register<T: UICollectionReusableView>(reusableViewTypes: [T.Type], kind: String = UICollectionElementKindSectionHeader) {
        reusableViewTypes.forEach { register(reusableViewType: $0, of: kind) }
    }

    func dequeueReusableCell<T: UICollectionViewCell>(with type: T.Type, for indexPath: IndexPath) -> T {
        return dequeueReusableCell(withReuseIdentifier: type.className, for: indexPath) as! T
    }

    func dequeueReusableView<T: UICollectionReusableView>(with type: T.Type, for indexPath: IndexPath, of kind: String = UICollectionElementKindSectionHeader) -> T {
        return dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: type.className, for: indexPath) as! T
    }
}
usage
collectionView.register(cellType: MyCell.self)
collectionView.register(cellTypes: [MyCell1.self, MyCell2.self])
let cell = collectionView.dequeueReusableCell(with: MyCell.self, for: indexPath)

collectionView.register(reusableViewType: MyReusableView.self)
collectionView.register(reusableViewTypes: [MyReusableView1.self, MyReusableView2.self])
let view = collectionView.dequeueReusableView(with: MyReusableView.self, for: indexPath)

16進数でUIColorの作成

extension
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))
    }
}
usage
let color = UIColor(hex: 0xAABBCC)

最前面のUIViewControllerを取得

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

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

    var topNavigationController: UINavigationController? {
        return topViewController as? UINavigationController
    }
}
usage
UIApplication.shared.topViewController

StoryboardのViewControllerを生成

extension
protocol StoryBoardInstantiatable {}
extension UIViewController: StoryBoardInstantiatable {}

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

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

usage
MyViewController.instantiate() // Storyboardファイルとクラスが同じ名前の場合
MyViewController.instantiate(withStoryboard: "MyStoryboard")

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

XIBのViewを生成

extension
protocol NibInstantiatable {}
extension UIView: NibInstantiatable {}

extension NibInstantiatable where Self: UIView {
    static func instantiate(withOwner ownerOrNil: Any? = nil) -> Self {
        let nib = UINib(nibName: self.className, bundle: nil)
        return nib.instantiate(withOwner: ownerOrNil, options: nil)[0] as! Self
    }
}
usage
MyView.instantiate()

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

子Viewを全て削除

extension
extension UIView {
    func removeAllSubviews() {
        subviews.forEach {
            $0.removeFromSuperview()
        }
    }
}
usage
view.removeAllSubViews()

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

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

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

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

extension
extension Collection {
    subscript(safe index: Index) -> _Element? {
        return index >= startIndex && index < endIndex ? self[index] : nil
    }
}
usage
let array = [0, 1, 2]
if let item = array[safe: 5] {
    print("unreachable")
}

2つのDictionaryを結合

extension
extension Dictionary {
    mutating func merge<S: Sequence>(contentsOf other: S) where S.Iterator.Element == (key: Key, value: Value) {
        for (key, value) in other {
            self[key] = value
        }
    }

    func merged<S: Sequence>(with other: S) -> [Key: Value] where S.Iterator.Element == (key: Key, value: Value) {
        var dic = self
        dic.merge(contentsOf: other)
        return dic
    }
}
usage
var dic1 = ["key1": 1]
let dic2 = ["key2": 2]
dic1.merge(contentsOf: dic2) // => ["key1": 1, "key2": 2]

標準になるかもしれません。
https://github.com/apple/swift-evolution/blob/master/proposals/0100-add-sequence-based-init-and-merge-to-dictionary.md

指定範囲内に値を収める

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

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

        if self > max {
            return max
        }

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

NSLocalizedStringを使いやすくする

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

    func localized(withTableName tableName: String? = nil, bundle: Bundle = Bundle.main, value: String = "") -> String {
        return NSLocalizedString(self, tableName: tableName, bundle: bundle, value: value, comment: self)
    }
}
usage
let message = "Hello".localized

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

extension
extension NSObjectProtocol where Self: NSObject {
    var described: 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)"
            }
            .joined(separator: "\n")
    }
}
usage
class Hoge: NSObject {
    var foo = 1
    let bar = "bar"
   }
}

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

文字列からURLを作成

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

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

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

extension
extension UIAlertController {

    func addAction(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: 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.shared.topViewController?.view {
            popoverPresentationController?.sourceView = sourceView
        }
        return self
    }

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

    func addTextField(handler: @escaping (UITextField) -> Void) -> Self {
        addTextField(configurationHandler: handler)
        return self
    }

    func show() {
        UIApplication.shared.topViewController?.present(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: CGFloat) -> UIImage? {
        let imageBounds = CGRect(x: 0, y: 0, width: width, height: bounds.size.height * (width / bounds.size.width))

        UIGraphicsBeginImageContextWithOptions(imageBounds.size, true, 0)

        drawHierarchy(in: imageBounds, afterScreenUpdates: true)

        var image: UIImage?
        let contextImage = UIGraphicsGetImageFromCurrentImageContext()

        if let contextImage = contextImage, let cgImage = contextImage.cgImage {
            image = UIImage(
                cgImage: cgImage,
                scale: UIScreen.main.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 = .white

let image = label.screenShot(width: 200)

ライブラリ

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

SwiftDate

https://github.com/malcommac/SwiftDate

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

example
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

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

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

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

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

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

R.swift

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

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

Before:

example
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:

example
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に便利なメソッドを追加してくれるライブラリ。

example
"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っぽく扱えるライブラリ。

example
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を簡単に設定できるライブラリ。

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

NSAttributedString(string: "ほげ", attributes: attrs)

Async

https://github.com/duemunk/Async

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

Before:

example
DispatchQueue.global(qos: .background).async {
    print("This is run on the background queue")

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

After:

example
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

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

example
let async = AsyncKit<String, NSError>()

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