便利で汎用性高めのExtension集です。Protocolやオペレータなども含まれています。
今後も便利なExtensionができ次第、本記事を更新していきます。
手放せなくなるSwift Extension集 (Swift2版)と使うと手放せなくなるSwift Extension集 (Swift3版)のSwift4版です。
記法の最適化や拡張の取捨選択で段々と良くなっています。
今回紹介したExtensionは全て下記のリポジトリに入っています。利用したい方はコピペやCarthageで導入してみてください。
https://github.com/tattn/SwiftExtensions
※ Swift4.2 で動作確認をしています。
※ コード片ごとに紹介していますが、別のコード片の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 {}
UIView.className //=> "UIView"
UILabel().className //=> "UILabel"
XIBの登録・取り出し
UITableView
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
}
}
tableView.register(cellType: MyCell.self)
tableView.register(cellTypes: [MyCell1.self, MyCell2.self])
let cell = tableView.dequeueReusableCell(with: MyCell.self, for: indexPath)
UICollectionView
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
}
}
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の生成
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
}
}
}
// クラス名と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の生成
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
}
}
// 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を置く
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() // 後述
}
}
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と同じ大きさにする
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
}
}
}
superView.addSubview(view)
view.fillSuperview()
最前面のUIViewController/UINavigationControllerの取得
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
}
}
UIApplication.shared.topViewController
そのViewを持つViewControllerを取得
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
}
}
view.viewController
16進数でUIColorの作成
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))
}
}
let color = UIColor(hex: 0xAABBCC)
配列でオブジェクトのインスタンスを検索して削除
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) }
}
}
let array = ["foo", "bar"]
array.remove(element: "foo")
array //=> ["bar"]
配列から同一の値を削除
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)
}
}
}
}
let array = [1, 2, 3, 3, 2, 1, 4]
array.unify() // [1, 2, 3, 4]
Out of Rangeを防いで、要素を取得
public extension Collection {
public subscript(safe index: Index) -> Element? {
return startIndex <= index && index < endIndex ? self[index] : nil
}
}
let array = [0, 1, 2]
if let item = array[safe: 5] {
print("unreachable")
}
NSLocalizedStringを使いやすくする
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)
}
}
let message = "Hello".localized
様々なRangeで部分文字列を取得
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])
}
}
let string = "0123456789"
string[0...5] //=> "012345"
string[1...3] //=> "123"
string[3..<7] //=> "3456"
string[...4] //=> "01234
string[..<4] //=> "0123"
string[4...] //=> "456789"
全角/半角文字列の変換
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
}
}
let string = "123ABcdeあいう"
string.halfWidth //=> "123ABcdeあいう"
string.fullWidth //=> "123ABcdeあいう"
クラスのプロパティを全て出力
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")
}
}
class Hoge: NSObject {
var foo = 1
let bar = "bar"
}
}
Hoge().described // => "foo: 1\nbar: bar"
文字列からURLを作成/文字列リテラルからURLを生成
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)
}
}
if let url = "https://example.com".url {
}
let url: URL = "https://example.com"
特定の色で塗りつぶされたUIImageを生成
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()
}
}
}
UIImage(color: .red, size: .init(width: 100, height: 100))
画像を別の色で塗りつぶす (透明色は塗りつぶさない)
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
}
}
fooImage.image(withTint: .red)
Kotlinっぽいスコープ関数を使う
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 {}
let view = UIView().apply {
$0.backgroundColor = .red
$0.frame = .init(x: 0, y: 0, width: 200, height: 200)
}
???でOptionalをErrorとしてThrowできるようにする
infix operator ???
public func ???<T>(lhs: T?,
error: @autoclosure () -> Error) throws -> T {
guard let value = lhs else { throw error() }
return value
}
let value: String? = nil
struct OptionalError: Error {}
do {
let v = try value ??? OptionalError()
print(v) // unreachable
} catch {
print(error) //=> OptionalError
}
Dictionaryの値取得時にkeyがなければErrorをThrowする
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
}
}
var dictionary: [String: Int] = [:]
do {
let value = try dictionary.tryValue(forKey: "foo")
print(value) // unreachable
} catch {
print(error) //=> DictionaryTryValueError
}
.exでアクセスできるプロパティやメソッドを作る
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)
}
}
// UIViewに.exを増やす場合
extension UIView: TargetedExtensionCompatible {}
private extension TargetedExtension where Base: UIView {
func foo() {}
}
UIView().ex.foo()
Codableで値がdecodeできないことを許容する
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()
}
}
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を任意の型に変換して受け取る
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)
}
}
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