新しいアプリを作るときによく使うSwift Extension集

f:id:vasilyjp:20180927112657j:plain

iOSエンジニアの庄司 (@WorldDownTown) です。

最近、業務で新しいiOSアプリを立て続けにいくつか開発する機会に恵まれました。 そんな中、いくつもアプリを使っていると、どのアプリでもよく使う処理があぶり出されてきます。 そういう処理はSwiftのExtensionとして別ファイルに書き出し、他のアプリへも切り出しやすいように個別のFrameworkにして管理しています。 Frameworkの管理については過去のこちらの記事を参考にしてみてください。

今記事では、最近の開発でよく使ったExtension集をご紹介します。


Swift標準ライブラリ

Date

private let formatter: DateFormatter = {
    let formatter: DateFormatter = DateFormatter()
    formatter.timeZone = NSTimeZone.system
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.calendar = Calendar(identifier: .gregorian)
    return formatter
}()

public extension Date {

    // Date→String
    func string(format: String = "yyyy-MM-dd'T'HH:mm:ssZ") -> String {
        formatter.dateFormat = format
        return formatter.string(from: self)
    }

    // String → Date
    init?(dateString: String, dateFormat: String = "yyyy-MM-dd'T'HH:mm:ssZ") {
        formatter.dateFormat = dateFormat
        guard let date = formatter.date(from: dateString) else { return nil }
        self = date
    }
}

Date().string(format: "yyyy/MM/dd") // 2017/02/26
Date(dateString: "2016-02-26T10:17:30Z")  // Date

同じモデルクラスを使って日付を表示する場合でも、リストページでは日付だけ、詳細ページでは時間も表示したいというときに便利です。 またイニシャライザの方は、ユーザーが入力した日付文字列から Date インスタンスを作ることができます。


Dictionary

// Dictionary同士を`+`演算子でマージできるようにする
public func +<K, V>(lhs: [K: V], rhs: [K: V]) -> [K: V] {
    var lhs = lhs
    for (key, value) in rhs {
        lhs[key] = value
    }
    return lhs
}

["key1": 0] + ["key1": 1, "key2": 2]    // ["key2": 2, "key1": 1]

APIリクエスト時の動的パラメータと固定のパラメータのDictionaryをマージするときなどに使います。


Int

private let formatter: NumberFormatter = NumberFormatter()

public extension Int {

    private func formattedString(style: NumberFormatter.Style, localeIdentifier: String) -> String {
        formatter.numberStyle = style
        formatter.locale = Locale(identifier: localeIdentifier)
        return formatter.string(from: self as NSNumber) ?? ""
    }

    // カンマ区切りString
    var formattedJPString: String {
        return formattedString(style: .decimal, localeIdentifier: "ja_JP")
    }

    // 日本円表記のString
    var JPYString: String {
        return formattedString(style: .currency, localeIdentifier: "ja_JP")
    }

    // USドル表記のString
    var USDString: String {
        return formattedString(style: .currency, localeIdentifier: "en_US")
    }
}

let million: Int = 1_000_000
million.formattedJPString   // 1,000,000
million.JPYString           // ¥1,000,000
million.USDString           // $1,000,000.00

どのアプリも価格を表示するところは多数あると思いますが、計算型プロパティ一つで面倒なカンマ区切り処理を書けるので、コードがかなりスッキリします。


UIKit

UIColor

public extension UIColor {

    // RGBのイニシャライザ
    public convenience init(rgb: UInt, alpha: CGFloat = 1.0) {
        let red: CGFloat = CGFloat((rgb & 0xff0000) >> 16) / 255.0
        let green: CGFloat = CGFloat((rgb & 0x00ff00) >> 8) / 255.0
        let blue: CGFloat = CGFloat(rgb & 0x0000ff) / 255.0
        self.init(red: red, green: green, blue: blue, alpha: alpha)
    }

    public struct iq {  // プロジェクトに合わせた名前で良い
        public static let pink: UIColor = UIColor(rgb: 0xfa4664)
        public static let textBlack: UIColor = UIColor(rgb: 0x333333)
    }
}

UIColor.iq.pink         // #fa4664
UIColor.iq.textBlack    // #333333

一つのプロジェクトで使う色数は、数え切れる程度になることが多いので、UIColorのExtensionに名前を付けて管理します。 Extensionの中でstructを定義して、その中に色をまとめることで、UIColorの本来の名前空間を汚してしまうことがなくなりますし、開発者が拡張しているというのがコードの読み手にも伝わりやすくなります。


UIView

public extension UIView {

    // 子Viewを親Viewのサイズいっぱいに表示するための制約を設定する
    func addConstraints(for childView: UIView, insets: UIEdgeInsets = .zero) {
        childView.translatesAutoresizingMaskIntoConstraints = false

        topAnchor.constraint(equalTo: childView.topAnchor, constant: insets.top).isActive = true
        bottomAnchor.constraint(equalTo: childView.bottomAnchor, constant: insets.bottom).isActive = true
        leadingAnchor.constraint(equalTo: childView.leadingAnchor, constant: insets.left).isActive = true
        trailingAnchor.constraint(equalTo: childView.trailingAnchor, constant: insets.right).isActive = true
    }
}

view.addSubview(childView)
view.addConstraints(for: childView)

let insets: UIEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
view.addConstraints(for: childView, insets: insets)

Interface Builderではなく、コードでviewをaddSubviewするとき、親Viewと同じサイズにしたいときに使います。 デフォルト引数は省略可能ですが、UIEdgeInsetsでマージンを指定することもできます。


UIViewController

public extension UIViewController {

    // ViewControllerのファクトリーメソッド
    static func create() -> Self {
        let name: String = "\(type(of: self))".components(separatedBy: ".").first!
        return instantiate(storyboardName: name)
    }

    private static func instantiate<T>(storyboardName: String) -> T {
        let storyboard: UIStoryboard = UIStoryboard(name: storyboardName, bundle: nil)
        let vc: UIViewController? = storyboard.instantiateInitialViewController()
        return vc as! T
    }
}

let vc = SomeViewController.create()

VASILYのiOS開発では、 1ViewController / 1Storyboard でViewControllerを管理し、ViewControllerのクラス名とStoryboardのファイル名を揃えるルールで運用しています。 そのため、文字列を使わずにViewControllerを初期化することができます。


サードパーティライブラリ

SVProgressHUD

import SVProgressHUD

public extension SVProgressHUD {

    public struct iq {  // プロジェクトに合わせた名前で良い

        // プロジェクト固有の初期設定
        public static func setup() {
            SVProgressHUD.setDefaultStyle(.custom)
            SVProgressHUD.setFont(UIFont.boldSystemFont(ofSize: 14.0))
            SVProgressHUD.setForegroundColor(UIColor.iq.pink)
            SVProgressHUD.setBackgroundColor(UIColor.white.withAlphaComponent(0.9))
            SVProgressHUD.setMinimumDismissTimeInterval(2.0)
        }

        public static func show(maskType: SVProgressHUDMaskType = .none) {
            SVProgressHUD.setDefaultMaskType(maskType)
            SVProgressHUD.show()
        }
    }
}

サードパーティライブラリは、アプリ全体で設定を有効にするために、AppDelegateに処理を書くことがよくあります。 複数のライブラリを使っていると、application(_:didFinishLaunchingWithOptions:) -> Boolが 各ライブラリの初期設定処理で膨れ上がってしまいます。

上記のExtensionは、複数行の処理をExtensionにまとめることによってこの問題を回避できるようになります。

ここでも、サードパーティライブラリの名前空間を汚さないように処理をstructの中に分けて書いています。

// AppDelegate

func application(_ application: UIApplication, didFinishLaunchingWithOptions options: [Hashale: Any]) -> Bool {
    SVProgressHUD.iq.setup()
    ...
    return true
}


また、現在のSVProgressHUDはMaskTypeがグローバルに設定されてしまうため、MaskTypeを変更したいときは、setDefaultMaskType(_:)を実行してから表示する必要があります。 このExtensionでは2行必要な処理を一つのメソッドでMaskTypeを引数に取れるように変更しています。 小さな変更ですが、SVProgressHUDはどのアプリでも頻繁に登場するため、冗長なコードを減らすことができます。

// 従来
SVProgressHUD.setDefaultMaskType(.clear)
SVProgressHUD.show()

SVProgressHUD.setDefaultMaskType(.none)
SVProgressHUD.show()

// Extension
SVProgressHUD.iq.show(maskType: .clear)
SVProgressHUD.iq.show()    // デフォルト値:.none


まとめ

iOSアプリ開発でよくある処理のExtensionの数々を紹介しました。 これらの処理が一つのFrameworkにまとまっていると、新しいアプリを作る時にも共通処理を持ってくることができます。 新規開発時に試してみてください。

現在VASILYでは、IQON以外にも新規でいくつかiOS/Androidアプリを開発しています。 ゼロからアプリを一緒に開発してくれるiOSエンジニアを募集しています。興味がある方は以下のリンクをご覧ください。

カテゴリー