Swift

Swiftで使えるExtensionの紹介

はじめに

僕がSwiftで重宝しているextensionを紹介します。
Swiftでアプリケーションを作っている人には役立つ記事だと思います。
(久しぶりの投稿だな・・・)

謝辞

自分で作ったものもありますが、他の人が作ったものも入っております。
もし「これは私が作ったコードなので取り下げてほしい。」「出典を記載して欲しい」などがありましたらご連絡ください。
(すいません。長い間使っているため、出典を忘れているものがあります・・・)

あると便利なExtension

え?もう、みんな知ってるって・・・?
あっ、そうか。

クラス名取得

王道でほぼ必須なExtensionです。
ほぼ全てのクラスで使えます。

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

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

ex)

print("\(UIView().className) : \(UIView.className)")

主にデバッグログで使えます。
他にもクラス同士の比較をする時にisの代わりに使えます。isの場合は継承関係があるとtrueが帰ってきてしまうので、本当に同じクラスなのかをチェックする時にも重宝します。

リスト内のオブジェクトの削除

Object-cの時代にあったremoveObjectがSwiftにはありません。
リスト内の同じオブジェクトを削除したい場合に使えます。
ついでにuniqueメソッドも追記しています。

これはスタックオーバーフローのどこかで見つけたコードです。

extension Array where Element: Equatable {

    func unique() -> [Element] {
        var r = [Element]()
        for i in self {
            r += !r.contains(i) ? [i] : []
        }
        return r
    }

    mutating func uniqueInPlace() {
        self = self.unique()
    }

    @discardableResult
    mutating func removeObject<T : Equatable>(_ obj : T) -> Array {
        self = self.filter({$0 as? T != obj})
        return self;
    }

    mutating func removeObjectsInArray(_ array: [Element]) {
        for object in array {
            _ = self.removeObject(object)
        }
    }
}

ex)

取り外したいオブジェクトを渡す事でリストからオブジェクトを外せます。
配列を渡したり、ユニークにすることもできるので重宝すると思います。

lat orgStr = "hoge"
let list:[String] = ["foo", orgStr, "hoge"]
list.removeObject(orgStr) // -> list内は["foo", "hoge"]になる。

日付を文字列に

RubyライクにDate型を使いたい時に便利です。
文字列から日付を生成することもできます。

extension Date {
    // 文字列から日付を作成します。
    static func dateFromString(string: String, format: String) -> NSDate {
        let formatter: DateFormatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .gregorian)
        formatter.dateFormat = format
        return formatter.date(from: string)! as NSDate
    }

    func toString() -> String {
        let formatter = DateFormatter()

        formatter.calendar = Calendar.current
        formatter.dateFormat = "MM/dd HH:mm"

        return formatter.string(from: self)
    }

    func toShortString() -> String {
        let formatter = DateFormatter()

        formatter.calendar = Calendar.current
        formatter.dateFormat = "HH:mm"

        return formatter.string(from: self)
    }

    // 文字列として出力します。
    func toDateString() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "M月d日"
        return formatter.string(from: self)
    }

    // 文字列として出力します。
    func toDateTimeString() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy/MM/dd HH:mm"
        return formatter.string(from: self)
    }

    // 文字列として出力します。
    func toShortDateTimeString() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "MM/dd HH:mm"
        return formatter.string(from: self)
    }

    // 文字列として出力します。
    func toTimeString() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "H:mm"
        return formatter.string(from: self)
    }

    // 年を取得する。
    func year() -> TimeInterval {
        let calendar = Calendar.current
        return TimeInterval(calendar.component(.year, from: self))
    }

    // 月を取得する。
    func month() -> TimeInterval {
        let calendar = Calendar.current
        return TimeInterval(calendar.component(.month, from: self))
    }

    // 日を取得する。
    func day() -> TimeInterval {
        let calendar = Calendar.current
        return TimeInterval(calendar.component(.day, from: self))
    }

    // 時間を取得する。
    func hour() -> TimeInterval {
        let calendar = Calendar.current
        return TimeInterval(calendar.component(.hour, from: self))
    }

    // 分を取得する。
    func minute() -> TimeInterval {
        let calendar = Calendar.current
        return TimeInterval(calendar.component(.minute, from: self))
    }

    // 秒を取得する。
    func second() -> TimeInterval {
        let calendar = Calendar.current
        return TimeInterval(calendar.component(.second, from: self))
    }

    // 曜日を取得する。
    func wday() -> Int {
        let calendar = Calendar.current
        return calendar.component(.weekday, from: self)
    }
}

ex)

特筆して書くことはないけれど一応。
日付を画面に表示したい時や日付計算をする時に便利だったりします。

print(Date().toString())

UIViewの親コントローラー知りたくないですか?

自分が付いている親コントローラーを把握していると便利だったりします。
え?どこで使うかって?さぁ。
でも、僕は結構使います。

おまけに角丸もつけられるExtensionです。
これはXCodeのストーリーボードで角丸の角度や外線の指定ができます。
ちょっとオシャレに丸い部品を作りたい時に便利!

extension UIView {
    // 自分の所属する親UIViewControllerを取得する。
    func parentViewController() -> UIViewController? {
        var parentResponder: UIResponder? = self
        while true {
            guard let nextResponder = parentResponder?.next else { return nil }
            if let viewController = nextResponder as? UIViewController {
                return viewController
            }
            parentResponder = nextResponder
        }
    }

    // 枠線の色
    @IBInspectable var borderColor: UIColor? {
        get {
            return layer.borderColor.map { UIColor(cgColor: $0) }
        }
        set {
            layer.borderColor = newValue?.cgColor
        }
    }

    // 枠線のWidth
    @IBInspectable var borderWidth: CGFloat {
        get {
            return layer.borderWidth
        }
        set {
            layer.borderWidth = newValue
        }
    }

    // 角丸設定
    @IBInspectable var cornerRadius: CGFloat {
        get {
            return layer.cornerRadius
        }
        set {
            layer.cornerRadius = newValue
            layer.masksToBounds = newValue > 0
        }
    }
}

ex)

自前のダイアログボックスなどをこのViewの親コントローラーに表示させたい時などに重宝。
僕はこのメソッドをさらにラップして使ってます。

otherView.parentViewController()?.addSubview(UIView())

Cellの付与とかってみんなどうしています?

StoryBoardとかに貼り付けたTableやCollectionのCell管理って面倒じゃないですか?
僕は自作したCellのIdentifierにはクラス名を入れています。

例えばHogeTableViewCellを作った場合はxibファイルのIdentifierにはHogeTableViewCellを入れてます。

extension UITableViewCell {
    // Xibファイルを生成して返します。
    class func createXib() -> UINib {
        return UINib.init(nibName: self.className, bundle: nil)
    }
    func createXib() -> UINib {
        return UINib.init(nibName: self.className, bundle: nil)
    }
}

extension UICollectionViewCell {
    // Xibファイルを生成して返します。
    class func createXib() -> UINib {
        return UINib.init(nibName: self.className, bundle: nil)
    }
    func createXib() -> UINib {
        return UINib.init(nibName: self.className, bundle: nil)
    }
}

ex)

HogeTableViewCellxibに定義した場合はこれでCellの登録が完了。
文字列で管理するとスペルミスをしやすい自分は必ずミスって謎のエラーに時間を取られてしまいます。

tableView.register(HogeTableViewCell.createXib(), forCellWithReuseIdentifier: HogeTableViewCell.className)

テキストボックスなどにDoneボタンを表示する。

テキストフィールドやテキストボックスを選択された時に表示されたキーボードを隠す処理って面倒じゃないですか?
ユーザーが"Done"をタップすることで隠せます。

※iPadや横向きで使う場合はサイズの調整が必要です。
(僕はiPhone縦でしか使わないので以下のコードになっています。)

extension UITextField {
    // キーボード表示時にDoneボタンを付与する。
    func addDoneButtonWithKeybord() {
        // 仮のサイズでツールバー生成
        let kbToolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 40))
        kbToolBar.barStyle = .default  // スタイルを設定

        kbToolBar.sizeToFit()  // 画面幅に合わせてサイズを変更

        // スペーサー
        let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)

        // 閉じるボタン
        let commitButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(UITextField.commitButtonTapped))

        kbToolBar.items = [spacer, commitButton]

        self.inputAccessoryView = kbToolBar
    }

    // 閉じるボタンを付与
    @objc func commitButtonTapped (){
        self.resignFirstResponder()
    }
}

extension UITextView {
    // キーボード表示時にDoneボタンを付与する。
    func addDoneButtonWithKeybord() {
        // 仮のサイズでツールバー生成
        let kbToolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 40))
        kbToolBar.barStyle = .default  // スタイルを設定

        kbToolBar.sizeToFit()  // 画面幅に合わせてサイズを変更

        // スペーサー
        let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)

        // 閉じるボタン
        let commitButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(UITextField.commitButtonTapped))

        kbToolBar.items = [spacer, commitButton]

        self.inputAccessoryView = kbToolBar
    }

    // 閉じるボタンを付与
    func commitButtonTapped (){
        self.resignFirstResponder()
    }
}

ex)

Doneボタンをつけたい部品でメソッドを呼ぶだけでDoneがつきます。

@IBOutlet private weak var textField:UITextField!

...

textField.addDoneButtonWithKeybord()

無くても良いけれどあると便利なExtension

そんなの使わないって・・・?
いやいや、使う人いるでしょ。

Int型を文字列に

警告回避で使ってます。Javaライクにコードがかけます。
僕は色々なクラスにtoString()をつけています。

extension Int {
    func toString() -> String {
        return "\(self)"
    }
}

ex)

print(1.toString())

文字列操作

大したものではないのですが、ユーザー入力時やAPIでデータ受信時にtrimするのは面倒じゃないですか?

extension String {
    // 先頭指定の文字数を返す。
    func truncate(_ maxLength:Int) -> String {
        if self.utf8.count > maxLength {
            return String(self.prefix(maxLength))
        } else {
            return self
        }
    }

    // 前後のスペースをトリムする。
    func trim() -> String {
        return self.trimmingCharacters(in: CharacterSet.whitespaces)
    }
}

ex)

print(" abc ".trim()) // -> "abc"と出力

いろいろな色を!!!

Swiftで文字列から色を作るのって面倒じゃないですか?
デザイナーとやり取りをする時ってRGBを文字列でもらったりしません?

extension UIColor {
    class func RGB(r: Int, g: Int, b: Int, alpha: CGFloat = 1) -> UIColor{
        return UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: alpha)
    }

    // HEX表記からUIColorを作成
    static func colorWithHexString (_ hex:String) -> UIColor {
        let cString = hex.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).uppercased()

        if ((cString as String).count != 6) {
            return UIColor.gray
        }

        let rString = (cString as NSString).substring(with: NSRange(location: 0, length: 2))
        let gString = (cString as NSString).substring(with: NSRange(location: 2, length: 2))
        let bString = (cString as NSString).substring(with: NSRange(location: 4, length: 2))

        var r:CUnsignedInt = 0, g:CUnsignedInt = 0, b:CUnsignedInt = 0;
        Scanner(string: rString).scanHexInt32(&r)
        Scanner(string: gString).scanHexInt32(&g)
        Scanner(string: bString).scanHexInt32(&b)

        return UIColor(
            red: CGFloat(Float(r) / 255.0),
            green: CGFloat(Float(g) / 255.0),
            blue: CGFloat(Float(b) / 255.0),
            alpha: CGFloat(Float(1.0))
        )
    }
}

ex)

UIColorこんなに簡単に作れるなんて!
素敵。
誰が作ったか忘れてしまったが、このコード重宝しています。ありがとうっ!

UIColor.colorWithHexString("87cefa")

あんまりつかないかな?現在のrootControllerが知りたい!

使う人は使う。今表示されている最前面のController。
スタックされまくっている前面のControllerに通知を出したい!・・・とか?

extension UIApplication {
    class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
        if let navigationController = controller as? UINavigationController {
            return topViewController(controller: navigationController.visibleViewController)
        }
        if let tabController = controller as? UITabBarController {
            if let selected = tabController.selectedViewController {
                return topViewController(controller: selected)
            }
        }
        if let presented = controller?.presentedViewController {
            return topViewController(controller: presented)
        }
        return controller
    }
}

ex)

割愛。

それ、ゲームでしか使わないんじゃない?

だよね。

画像の色を変えたいな。

え?デザイナーさん忙しいの?
じゃぁ、自分で作るか・・・

extension UIImage {
    // 元の画像に色を加えて新しい画像を生成して返す。
    func blend(filterColor: UIColor) -> UIImage {
        // オフスクリーン描画の初期化設定
        UIGraphicsBeginImageContext(self.size)
        let ctx = UIGraphicsGetCurrentContext()!
        let rect = CGRect(origin: CGPoint.zero, size: self.size)

        // Core Graphics と UIImage は座標系違うので変換しておく
        ctx.translateBy(x: 0, y: self.size.height)
        ctx.scaleBy(x: 1.0, y: -1.0)

        // 描画モードを Multiply に設定しておく
        ctx.setBlendMode(.multiply)

        // 元画像をオフスクリーンキャンバスで描く
        ctx.draw(self.cgImage!, in: rect)

        // 元画像をクリッピングマスクとして描画領域を設定する
        ctx.clip(to: rect, mask: self.cgImage!)
        ctx.addRect(rect)

        // フィルターを設定する
        filterColor.setFill()
        ctx.drawPath(using: CGPathDrawingMode.fill)

        // オフスクリーン描画結果を UIImage として出力する
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image!
    }
}

ex)

まぁ、普通は使わないよね。

image.blend(filterColor: UIColor.colorWithHexString("FF0000"))

座標の計算をもっと楽に・・・

これ、説明いらないよね。
これが必要な人は見ればわかる。

CGPoint同士で足し算とかできるようになるよ。

func * (size: CGSize, scalar: CGFloat) -> CGSize {
    return CGSize(width: size.width * scalar, height: size.height * scalar)
}

func + (left: CGPoint, right: CGPoint) -> CGPoint {
    return CGPoint(x: left.x + right.x, y: left.y + right.y)
}

func - (left: CGPoint, right: CGPoint) -> CGPoint {
    return CGPoint(x: left.x - right.x, y: left.y - right.y)
}

func * (point: CGPoint, scalar: CGFloat) -> CGPoint {
    return CGPoint(x: point.x * scalar, y: point.y * scalar)
}

func / (point: CGPoint, scalar: CGFloat) -> CGPoint {
    return CGPoint(x: point.x / scalar, y: point.y / scalar)
}

#if !(arch(x86_64) || arch(arm64))
    func sqrt(a: CGFloat) -> CGFloat {
        return CGFloat(sqrtf(Float(a)))
    }
#endif

func ccp(_ x:CGFloat, _ y:CGFloat) -> CGPoint {
    return CGPoint(x: x, y: y)
}

extension CGPoint {
    func length() -> CGFloat {
        return sqrt(x*x + y*y)
    }

    func normalized() -> CGPoint {
        return self / length()
    }
}

おまけ

スクリーンサイズ知りたいよー

UIScreen.main.bounds.size

これ、iPhoneいくつ?
iPhoneXってなに?
この手のメソッドはどれが正しいのだろうか・・・?

    // iPhone5の高さ判定ロジック
    static let isIPhone5:Bool = {
        let nativeSize = UIScreen.main.nativeBounds.size
        return nativeSize.height <= 1136
    }()
    // iPhoneXの判定ロジック
    static let isIPhoneX:Bool = {
        guard #available(iOS 11.0, *),
            UIDevice().userInterfaceIdiom == .phone else {
                return false
        }
        let nativeSize = UIScreen.main.nativeBounds.size
        let (w, h) = (nativeSize.width, nativeSize.height)
        let (d1, d2): (CGFloat, CGFloat) = (1125.0, 2436.0)
        return (w == d1 && h == d2) || (w == d2 && h == d1)
    }()

iPhoneXの下の線消したくない?デザインにかぶるんだけれど?

# UIViewControllerにこのメソッドを書くと消えるよ。
    // iPhoneXのノッチ(HomeIndicator)を隠す。
    @available(iOS 11, *)
    override func prefersHomeIndicatorAutoHidden() -> Bool {
        return true
    }

どうでも良いが、iPhoneXのノッチ(上の出っ張り)の高さは30pxらしい。(きっと
そして、iPhoneXの下部には20pxのマージンを入れると下の線(ホームインジケーター)に被らなくなるよ。

最後に

僕の作っているゲームはUIViewとSKViewのハイブリッド(?)なので結構こういうExtensionが役に立ったりするけれど、みんなはどうだろうか・・・?
(SKViewについては需要がなさそうなので書きませんw)
もし役に立ったぞーという方はぜひ、僕が作っているゲームを遊んでやってください。

180.png
Vanitas -草原の冒険者たち-