便利で汎用性高めのExtension集です。後半にライブラリもまとめました。
Swift3.0で動作確認をしています。
追記: Swift4版はこちらです。
以前投稿した使うと手放せなくなるSwift Extension集 (Swift2版)のSwift3版で、命名規則などに変更があります。
※強制アンラップを使用しているので、状況に合わせて変更して利用してください。
クラス名の取得
extension NSObject {
class var className: String {
return String(describing: self)
}
var className: String {
return type(of: self).className
}
}
MyClass.className //=> "MyClass"
MyClass().className //=> "MyClass"
XIBの登録・取り出し
XIBファイルとクラス名、identifierを同じ名前にして利用してください。
上記の「クラス名の取得」を利用しています。
UITableView
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: T.Type, for indexPath: IndexPath) -> T {
return self.dequeueReusableCell(withIdentifier: type.className, for: indexPath) as! T
}
}
tableView.register(cellType: MyCell.self)
tableView.register(cellTypes: [MyCell1.self, MyCell2.self])
let cell = tableView.dequeueReusableCell(with: MyCell.self, for: indexPath)
UICollectionView
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
}
}
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 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(hex: 0xAABBCC)
最前面のUIViewControllerを取得
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
}
}
UIApplication.shared.topViewController
StoryboardのViewControllerを生成
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
}
}
MyViewController.instantiate() // Storyboardファイルとクラスが同じ名前の場合
MyViewController.instantiate(withStoryboard: "MyStoryboard")
※ 上記の「クラス名の取得」を利用しています。
XIBのViewを生成
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
}
}
MyView.instantiate()
※ XIBファイルとクラスは同じ名前にしてください。
※ 上記の「クラス名の取得」を利用しています。
子Viewを全て削除
extension UIView {
func removeAllSubviews() {
subviews.forEach {
$0.removeFromSuperview()
}
}
}
view.removeAllSubViews()
配列でオブジェクトのインスタンスを検索して削除
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)
}
}
}
let array = ["foo", "bar"]
array.remove(element: "foo")
Out of Rangeを防いで、要素を取得
extension Collection {
subscript(safe index: Index) -> _Element? {
return index >= startIndex && index < endIndex ? self[index] : nil
}
}
let array = [0, 1, 2]
if let item = array[safe: 5] {
print("unreachable")
}
2つのDictionaryを結合
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
}
}
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
追記: Swift 4で同等の機能が入りました。Swift 4のextensionに関しては、こちらをご参照ください。
指定範囲内に値を収める
Viewの配置をコードで実装するときに便利です。
extension Comparable {
func clamped(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 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)
}
}
let message = "Hello".localized
クラスのプロパティを全て出力
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")
}
}
class Hoge: NSObject {
var foo = 1
let bar = "bar"
}
}
Hoge().described // => "foo: 1\nbar: bar"
文字列からURLを作成
extension String {
var url: URL? {
return URL(string: self)
}
}
if let url = "https://example.com".url {
}
UIAlertControllerをBuilderパターンっぽく扱う
Androidアプリも書いている方は欲しくなるはずです。。。
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)
}
}
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 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
}
}
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
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
いい感じのフラットカラーを用意してくれるライブラリ。
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
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
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
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
NSAttributedStringを簡単に設定できるライブラリ。
let attrs = TextAttributes()
.font(name: "HelveticaNeue", size: 16)
.foregroundColor(white: 0.2, alpha: 1)
.lineHeightMultiple(1.5)
NSAttributedString(string: "ほげ", attributes: attrs)
Async
Grand Central Dispatch (GCD) を使いやすくするライブラリ。
Before:
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:
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"])
}
```