Edited at
SwiftDay 18

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

More than 1 year has passed since last update.

便利で汎用性高めのExtension集です。Protocolやオペレータなども含まれています。

今後も便利なExtensionができ次第、本記事を更新していきます。

手放せなくなるSwift Extension集 (Swift2版)使うと手放せなくなるSwift Extension集 (Swift3版)のSwift4版です。

記法の最適化や拡張の取捨選択で段々と良くなっています。

今回紹介したExtensionは全て下記のリポジトリに入っています。利用したい方はコピペやCarthageで導入してみてください。

https://github.com/tattn/SwiftExtensions

Swift4.2 で動作確認をしています。

※ コード片ごとに紹介していますが、別のコード片のExtensionに依存している場合がありますので、ご注意ください。


クラス名の取得


extension

public protocol ClassNameProtocol {

static var className: String { get }
var className: String { get }
}

public extension ClassNameProtocol {
public static var className: String {
return String(describing: self)
}

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

extension NSObject: ClassNameProtocol {}



usage

UIView.className   //=> "UIView"

UILabel().className //=> "UILabel"


XIBの登録・取り出し


UITableView


extension

public extension UITableView {

public func register(cellType: UITableViewCell.Type, bundle: Bundle? = nil) {
let className = cellType.className
let nib = UINib(nibName: className, bundle: bundle)
register(nib, forCellReuseIdentifier: className)
}

public func register(cellTypes: [UITableViewCell.Type], bundle: Bundle? = nil) {
cellTypes.forEach { register(cellType: $0, bundle: bundle) }
}

public func dequeueReusableCell<T: UITableViewCell>(with 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

public extension UICollectionView {

public func register(cellType: UICollectionViewCell.Type, bundle: Bundle? = nil) {
let className = cellType.className
let nib = UINib(nibName: className, bundle: bundle)
register(nib, forCellWithReuseIdentifier: className)
}

public func register(cellTypes: [UICollectionViewCell.Type], bundle: Bundle? = nil) {
cellTypes.forEach { register(cellType: $0, bundle: bundle) }
}

public func register(reusableViewType: UICollectionReusableView.Type,
ofKind kind: String = UICollectionElementKindSectionHeader,
bundle: Bundle? = nil) {
let className = reusableViewType.className
let nib = UINib(nibName: className, bundle: bundle)
register(nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: className)
}

public func register(reusableViewTypes: [UICollectionReusableView.Type],
ofKind kind: String = UICollectionElementKindSectionHeader,
bundle: Bundle? = nil) {
reusableViewTypes.forEach { register(reusableViewType: $0, ofKind: kind, bundle: bundle) }
}

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

public func dequeueReusableView<T: UICollectionReusableView>(with type: T.Type,
for indexPath: IndexPath,
ofKind 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)



StoryboardのUIViewControllerの生成


protocol

public enum StoryboardInstantiateType {

case initial
case identifier(String)
}

public protocol StoryboardInstantiatable {
static var storyboardName: String { get }
static var storyboardBundle: Bundle { get }
static var instantiateType: StoryboardInstantiateType { get }
}

public extension StoryboardInstantiatable where Self: NSObject {
public static var storyboardName: String {
return className
}

public static var storyboardBundle: Bundle {
return Bundle(for: self)
}

private static var storyboard: UIStoryboard {
return UIStoryboard(name: storyboardName, bundle: storyboardBundle)
}

public static var instantiateType: StoryboardInstantiateType {
return .identifier(className)
}
}

public extension StoryboardInstantiatable where Self: UIViewController {
public static func instantiate() -> Self {
switch instantiateType {
case .initial:
return storyboard.instantiateInitialViewController() as! Self
case .identifier(let identifier):
return storyboard.instantiateViewController(withIdentifier: identifier) as! Self
}
}
}



usage

// クラス名とStoryboard名、Storyboard IDが同じ

final class ViewController: UIViewController, StoryboardInstantiatable {
}

ViewController.instantiate()

// Is Initial View Controllerにチェックを入れている & クラス名とStoryboard名が同じ
final class InitialViewController: UIViewController, StoryboardInstantiatable {
static var instantiateType: StoryboardInstantiateType {
return .initial
}
}

InitialViewController.instantiate()



XIBのViewの生成


protocol

public protocol NibInstantiatable {

static var nibName: String { get }
static var nibBundle: Bundle { get }
static var nibOwner: Any? { get }
static var nibOptions: [AnyHashable: Any]? { get }
static var instantiateIndex: Int { get }
}

public extension NibInstantiatable where Self: NSObject {
public static var nibName: String { return className }
public static var nibBundle: Bundle { return Bundle(for: self) }
public static var nibOwner: Any? { return self }
public static var nibOptions: [AnyHashable: Any]? { return nil }
public static var instantiateIndex: Int { return 0 }
}

public extension NibInstantiatable where Self: UIView {
public static func instantiate() -> Self {
let nib = UINib(nibName: nibName, bundle: nibBundle)
return nib.instantiate(withOwner: nibOwner, options: nibOptions)[instantiateIndex] as! Self
}
}



usage

// XIB名とクラス名が同じ & 0番目のView

final class View: UIView, NibInstantiatable {
}

View.instantiate()

// XIB名とクラス名が異なる & 2番目のView
final class View2: UIView, NibInstantiatable {
static var nibName: String { return "Foo" } // Foo.xib
static var instantiateIndex: Int { return 2 }
}

View2.instantiate()



Interface Builder内にXibで作ったカスタムViewを置く


protocol

public protocol EmbeddedNibInstantiatable {

associatedtype Embedded: NibInstantiatable
}

public extension EmbeddedNibInstantiatable where Self: UIView, Embedded: UIView {
public var embedded: Embedded { return subviews[0] as! Embedded }

public func configureEmbededView() {
let view = Embedded.instantiate()
insertSubview(view, at: 0)
view.fillSuperview() // 後述
}
}



usage

final class EmbeddedView: UIView, NibInstantiatable {

}

@IBDesignable
final class IBEmbeddedView: UIView, EmbeddedNibInstantiatable {
typealias Embedded = EmbeddedView

#if TARGET_INTERFACE_BUILDER
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
configureEmbededView()
}
#endif

override func awakeFromNib() {
super.awakeFromNib()
configureEmbededView()
}
}


(例) EmbeddedView.xibに置いたUIViewのCustom Classを設定 (File's Ownerの方ではない)

(例) Storyboard上のViewControllerにUIViewを載せて、カスタムクラスを設定


ViewをSuperviewと同じ大きさにする


extension

public extension UIView {

public func fillSuperview() {
guard let superview = self.superview else { return }
translatesAutoresizingMaskIntoConstraints = superview.translatesAutoresizingMaskIntoConstraints
if translatesAutoresizingMaskIntoConstraints {
autoresizingMask = [.flexibleWidth, .flexibleHeight]
frame = superview.bounds
} else {
topAnchor.constraint(equalTo: superview.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true
leftAnchor.constraint(equalTo: superview.leftAnchor).isActive = true
rightAnchor.constraint(equalTo: superview.rightAnchor).isActive = true
}
}
}


usage

superView.addSubview(view)

view.fillSuperview()


最前面のUIViewController/UINavigationControllerの取得


extension

public extension UIApplication {

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

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

public var topNavigationController: UINavigationController? {
return topViewController as? UINavigationController
}
}



usage

UIApplication.shared.topViewController



そのViewを持つViewControllerを取得


extension

public extension UIView {

public var viewController: UIViewController? {
var parent: UIResponder? = self
while parent != nil {
parent = parent?.next
if let viewController = parent as? UIViewController {
return viewController
}
}
return nil
}
}


usage

view.viewController



16進数でUIColorの作成


extension

public extension UIColor {

public 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)



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


extension

public extension Array where Element: Equatable {

@discardableResult
public mutating func remove(element: Element) -> Index? {
guard let index = index(of: element) else { return nil }
remove(at: index)
return index
}

@discardableResult
public mutating func remove(elements: [Element]) -> [Index] {
return elements.flatMap { remove(element: $0) }
}
}



usage

let array = ["foo", "bar"]

array.remove(element: "foo")
array //=> ["bar"]


配列から同一の値を削除


extension

public extension Array where Element: Hashable {

public mutating func unify() {
self = unified()
}
}

public extension Collection where Element: Hashable {
public func unified() -> [Element] {
return reduce(into: []) {
if !$0.contains($1) {
$0.append($1)
}
}
}
}



usage

let array = [1, 2, 3, 3, 2, 1, 4]

array.unify() // [1, 2, 3, 4]


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


extension

public extension Collection {

public subscript(safe index: Index) -> Element? {
return startIndex <= index && index < endIndex ? self[index] : nil
}
}


usage

let array = [0, 1, 2]

if let item = array[safe: 5] {
print("unreachable")
}


NSLocalizedStringを使いやすくする


extension

public extension String {

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

public 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



様々なRangeで部分文字列を取得


extension

public extension String {

subscript (bounds: CountableClosedRange<Int>) -> String {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return String(self[start...end])
}

subscript (bounds: CountableRange<Int>) -> String {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return String(self[start..<end])
}

subscript (bounds: PartialRangeUpTo<Int>) -> String {
let end = index(startIndex, offsetBy: bounds.upperBound)
return String(self[startIndex..<end])
}

subscript (bounds: PartialRangeThrough<Int>) -> String {
let end = index(startIndex, offsetBy: bounds.upperBound)
return String(self[startIndex...end])
}

subscript (bounds: CountablePartialRangeFrom<Int>) -> String {
let start = index(startIndex, offsetBy: bounds.lowerBound)
return String(self[start..<endIndex])
}
}



usage

let string = "0123456789"

string[0...5] //=> "012345"
string[1...3] //=> "123"
string[3..<7] //=> "3456"
string[...4] //=> "01234
string[..<4] //=> "0123"
string[4...] //=> "456789"


全角/半角文字列の変換


extension

public extension String {

public var halfWidth: String {
return transformFullWidthToHalfWidth(reverse: false)
}

public var fullWidth: String {
return transformFullWidthToHalfWidth(reverse: true)
}

private func transformFullWidthToHalfWidth(reverse: Bool) -> String {
let string = NSMutableString(string: self) as CFMutableString
CFStringTransform(string, nil, kCFStringTransformFullwidthHalfwidth, reverse)
return string as String
}
}



usage

let string = "123ABcdeあいう"

string.halfWidth //=> "123ABcdeあいう"
string.fullWidth //=> "123ABcdeあいう"


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


extension

public extension NSObjectProtocol {

public var describedProperty: 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を作成/文字列リテラルからURLを生成


extension

public extension String {

public var url: URL? {
return URL(string: self)
}
}

extension URL: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
guard let url = URL(string: value) else {
fatalError("\(value) is an invalid url")
}
self = url
}

public init(extendedGraphemeClusterLiteral value: String) {
self.init(stringLiteral: value)
}

public init(unicodeScalarLiteral value: String) {
self.init(stringLiteral: value)
}
}



usage

if let url = "https://example.com".url {

}

let url: URL = "https://example.com"



特定の色で塗りつぶされたUIImageを生成


extension

public extension UIImage {

public convenience init(color: UIColor, size: CGSize) {
UIGraphicsBeginImageContext(size)
guard let context: CGContext = UIGraphicsGetCurrentContext() else {
self.init()
return
}

context.setFillColor(color.cgColor)
context.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))

guard let image: UIImage = UIGraphicsGetImageFromCurrentImageContext() else {
self.init()
return
}
UIGraphicsEndImageContext()

if let cgImage = image.cgImage {
self.init(cgImage: cgImage)
} else {
self.init()
}
}
}



usage

UIImage(color: .red, size: .init(width: 100, height: 100))



画像を別の色で塗りつぶす (透明色は塗りつぶさない)


extension

public extension UIImage {

func image(withTint color: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)

guard let context: CGContext = UIGraphicsGetCurrentContext(), let cgImage = cgImage else {
return UIImage()
}
context.scaleBy(x: 1, y: -1)
context.translateBy(x: 0, y: -self.size.height)
context.clip(to: rect, mask: cgImage)
context.setFillColor(color.cgColor)
context.fill(rect)
guard let image = UIGraphicsGetImageFromCurrentImageContext() else {
return UIImage()
}
UIGraphicsEndImageContext()

return image
}
}



usage

fooImage.image(withTint: .red)



Kotlinっぽいスコープ関数を使う


protocol

public protocol Appliable {}

public extension Appliable {
@discardableResult
public func apply(closure: (Self) -> Void) -> Self {
closure(self)
return self
}
}

public protocol Runnable {}

public extension Runnable {
@discardableResult
public func run<T>(closure: (Self) -> T) -> T {
return closure(self)
}
}

extension NSObject: Appliable {}
extension NSObject: Runnable {}



usage

let view = UIView().apply {

$0.backgroundColor = .red
$0.frame = .init(x: 0, y: 0, width: 200, height: 200)
}


???でOptionalをErrorとしてThrowできるようにする


operator

infix operator ???

public func ???<T>(lhs: T?,
error: @autoclosure () -> Error) throws -> T {
guard let value = lhs else { throw error() }
return value
}



usage

let value: String? = nil

struct OptionalError: Error {}

do {
let v = try value ??? OptionalError()
print(v) // unreachable
} catch {
print(error) //=> OptionalError
}



Dictionaryの値取得時にkeyがなければErrorをThrowする


extension

public struct DictionaryTryValueError: Error {

public init() {}
}

public extension Dictionary {
func tryValue(forKey key: Key, error: Error = DictionaryTryValueError()) throws -> Value {
guard let value = self[key] else { throw error }
return value
}
}



usage

var dictionary: [String: Int] = [:]

do {
let value = try dictionary.tryValue(forKey: "foo")
print(value) // unreachable
} catch {
print(error) //=> DictionaryTryValueError
}


.exでアクセスできるプロパティやメソッドを作る


protocol

public struct TargetedExtension<Base> {

let base: Base
init (_ base: Base) {
self.base = base
}
}

public protocol TargetedExtensionCompatible {
associatedtype Compatible
static var ex: TargetedExtension<Compatible>.Type { get }
var ex: TargetedExtension<Compatible> { get }
}

public extension TargetedExtensionCompatible {
public static var ex: TargetedExtension<Self>.Type {
return TargetedExtension<Self>.self
}

public var ex: TargetedExtension<Self> {
return TargetedExtension(self)
}
}



usage

// UIViewに.exを増やす場合

extension UIView: TargetedExtensionCompatible {}

private extension TargetedExtension where Base: UIView {
func foo() {}
}

UIView().ex.foo()



Codableで値がdecodeできないことを許容する


struct

public struct Safe<Wrapped: Decodable>: Codable {

public let value: Wrapped?

public init(from decoder: Decoder) throws {
do {
let container = try decoder.singleValueContainer()
self.value = try container.decode(Wrapped.self)
} catch {
self.value = nil
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}



usage

let json = """

[
{"
name": "Taro"},
{"
name": 123}
]
"""
.data(using: .utf8)!

struct User: Decodable {
let name: String
}

let users = try! JSONDecoder().decode([Safe<User>].self,
from: json)
users[0].value?.name //=> "Taro"
users[1].value //=> nil

let json2 = """
{"
url": "https://foo.com", "url2": "invalid url string"}
""".data(using: .utf8)!

struct Model: Decodable {
let url: Safe<URL>
let url2: Safe<URL>
}

let model = try! JSONDecoder().decode(Model.self,
from: json)
model.url.value?.absoluteString //=> "https://foo.com"
model.url2.value //=> nil



CodableでStringを任意の型に変換して受け取る


struct

public struct StringTo<T: LosslessStringConvertible>: Codable {

public let value: T

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let stringValue = try container.decode(String.self)

guard let value = T(stringValue) else {
throw DecodingError.dataCorrupted(
.init(codingPath: decoder.codingPath,
debugDescription: "The string cannot cast to \(T.self).")
)
}

self.value = value
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.description)
}
}



usage

let json = """

{
"
number": "100",
}
"""
.data(using: .utf8)!

struct Model: Codable {
let number: StringTo<Int>
}
let model = try! JSONDecoder().decode(Model.self, from: data)
model.number.value //=> 100 (Int型)



参考文献

https://speakerdeck.com/tattn/codable-tipsji

https://gist.github.com/erica/5a26d523f3d6ffb74e34d179740596f7

https://stackoverflow.com/questions/45562662/how-can-i-use-string-slicing-subscripts-in-swift-4

https://github.com/tarunon/Instantiate

http://tech.vasily.jp/entry/swift_modern_extensions