この記事はVASILY DEVELOPERS BLOGにも同じ内容で投稿しています。よろしければ他の記事もご覧ください。
最近、業務で新しいiOSアプリを立て続けにいくつか開発する機会に恵まれました。
そんな中、いくつもアプリを使っていると、どのアプリでもよく使う処理があぶり出されてきます。
そういう処理はSwiftのExtensionとして別ファイルに書き出し、他のアプリへも切り出しやすいように個別の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にまとまっていると、新しいアプリを作る時にも共通処理を持ってくることができます。
新規開発時に試してみてください。