Open16

Swift: Extension

KyomeKyome

最強の標準出力

func logput(_ items: Any...,
            file: String = #file,
            line: Int = #line,
            function: String = #function) {
    #if DEBUG
    let fileName = URL(fileURLWithPath: file).lastPathComponent
    var array: [Any] = ["💫Log: \(fileName)", "Line:\(line)", function]
    array.append(contentsOf: items)
    Swift.print(array)
    #endif
}
KyomeKyome

他言語対応

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

URLからファイル名を取得

extension URL {
    var fileName: String {
        return self.deletingPathExtension().lastPathComponent
    }
}
KyomeKyome

画像の正しいピクセルサイズでNSImageを初期化

extension NSImage {
    convenience init(name: String) {
        self.init(imageLiteralResourceName: name)
        let rep = representations[0]
        size = CGSize(width: rep.pixelsWide, height: rep.pixelsHigh)
    }
}
KyomeKyome

NSControl.StateValue と Bool の相互変換

extension Bool {
    var state: NSControl.StateValue {
        return self ? .on : .off
    }
}

extension NSControl.StateValue {
    var isOn: Bool {
        return self == .on
    }
}
KyomeKyome

ダークモードかどうかの取得(macOS)

extension NSAppearance {
    var isDark: Bool {
        if self.name == .vibrantDark { return true }
        guard #available(macOS 10.14, *) else { return false }
        return self.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
    }
}
KyomeKyome

Int の桁数を取得する

extension Int {
    var digit: Int {
        assert(0 < self, "The number must be a natural number.")
        return Int(floor(log10(Double(self)))) + 1
    }
}
KyomeKyome

NSMenuItem に target と selector を渡す

extension NSMenuItem {
    func setAction(target: AnyObject, selector: Selector) {
        self.target = target
        self.action = selector
    }
}
KyomeKyome

NSScreen から DisplayID を取得する

extension NSScreen {
    var displayID: CGDirectDisplayID {
        let key = NSDeviceDescriptionKey(rawValue: "NSScreenNumber")
        return deviceDescription[key] as! CGDirectDisplayID
    }
}

DisplayID と WindowID から任意のウィンドウの背景画像を取得する

extension CGImage {
    static func background(displayID: CGDirectDisplayID, windowID: CGWindowID) -> CGImage? {
        let bounds = CGDisplayBounds(displayID)
        let windowOptions: CGWindowListOption = [
            .optionOnScreenOnly,
            .optionOnScreenBelowWindow
        ]
        let imageOptions: CGWindowImageOption = [
            .bestResolution,
            .boundsIgnoreFraming
        ]
        return CGWindowListCreateImage(bounds, windowOptions, windowID, imageOptions)
    }
}
KyomeKyome

NSView のサムネイルを作成

extension NSView {
    var thumbnail: NSImage? {
        guard let rep = bitmapImageRepForCachingDisplay(in: bounds) else { return nil }
        rep.size = bounds.size
        cacheDisplay(in: bounds, to: rep)
        guard let data = rep.representation(using: .png, properties: [:]) else { return nil }
        return NSImage(data: data)
    }
}
KyomeKyome

NSColor の色見本を取得する・NSColorの成分を出力する

extension NSColor {
    var swatch: NSImage {
        let size = NSSize(width: 18.0, height: 18.0)
        let rect = NSRect(origin: .zero, size: size)
        let image = NSImage(size: size)
        image.lockFocus()
        self.drawSwatch(in: rect)
        image.unlockFocus()
        let swatch = NSImage(size: size)
        swatch.lockFocus()
        let path = NSBezierPath(roundedRect: rect, xRadius: 3, yRadius: 3)
        path.windingRule = .evenOdd
        path.addClip()
        image.draw(at: .zero, from: rect, operation: .sourceOver, fraction: 1)
        swatch.unlockFocus()
        return swatch
    }

    func printComponents() {
        print("🌈", self.colorSpace.localizedName ?? "unknown")
        print("Number of Components:", self.colorSpace.numberOfColorComponents)
        switch self.colorSpace {
        case .genericGray, .deviceGray, .genericGamma22Gray, .extendedGenericGamma22Gray:
            print("- white", self.whiteComponent)
        case .genericRGB, .deviceRGB, .sRGB, .extendedSRGB, .displayP3, .adobeRGB1998:
            print("- red", self.redComponent)
            print("- green", self.greenComponent)
            print("- blue", self.blueComponent)
            print("- hue", self.hueComponent)
            print("- saturation:", self.saturationComponent)
            print("- brightness:", self.brightnessComponent)
        case .genericCMYK, .deviceCMYK:
            print("- cyan", self.cyanComponent)
            print("- magenta", self.magentaComponent)
            print("- yellow", self.yellowComponent)
            print("- black", self.blackComponent)
        default:
            break
        }
        print("- alpha:", self.alphaComponent)
        print()
    }
}
KyomeKyome

正規表現にマッチした箇所を指定した文字列で置換

extension String {
    func replace(pattern: String, expect: String) -> String {
        return self.replacingOccurrences(of: pattern,
                                         with: expect,
                                         options: .regularExpression,
                                         range: self.range(of: self))
    }
}

print("Hello :NAME:".replace(pattern: #":NAME:"#, expect: "Mike")) // Hello Mike
print("http://hoge.com/".replace(pattern: #"^https?://"#, expect: "")) // hoge.com/
print("AAA--BbB--CCC".replace(pattern: #"[ABC]{3}"#, expect: "🐷")) // 🐷--BbB--🐷

正規表現のパターンを記入するときは#""#で囲むとエスケープしなくていいので楽。

KyomeKyome

引数の正規表現にマッチしている文字列かどうか確認

完全一致
extension String {
    func match(pattern: String) -> Bool {
        let selfRange = self.startIndex ..< self.endIndex
        let matchRange = self.range(of: pattern, options: .regularExpression)
        return selfRange == matchRange
    }
}

let str = "abcdefg"
print(str.match(pattern: #"[a-z]+"#)) // true
print(str.match(pattern: #"[A-Z]+"#)) // false
部分一致
extension String {
    func match(pattern: String) -> Bool {
        let matchRange = self.range(of: pattern, options: .regularExpression)
        return matchRange != nil
    }
}
KyomeKyome

Retina 環境でも正確に NSImage をリサイズする

extension CGFloat {
    var roundedInt: Int {
        return Int(self.rounded())
    }
}

extension NSImage {
    var ppi: CGFloat {
        let rep = self.representations[0]
        return (72.0 * CGFloat(rep.pixelsWide) / self.size.width)
    }
    
    var pixelSize: CGSize {
        let rep = self.representations[0]
        return CGSize(width: rep.pixelsWide, height: rep.pixelsHigh)
    }
    
    func resized(with ratio: CGFloat) -> NSImage {
        let pixelSize = self.pixelSize
        let newPixelsWide: Int = (ratio * pixelSize.width).roundedInt
        let newPixelsHigh: Int = (ratio * pixelSize.height).roundedInt
        
        let sourceRep = NSBitmapImageRep(data: self.tiffRepresentation!)!
        let resizedRep = NSBitmapImageRep(bitmapDataPlanes: nil,
                                          pixelsWide: newPixelsWide,
                                          pixelsHigh: newPixelsHigh,
                                          bitsPerSample: sourceRep.bitsPerSample,
                                          samplesPerPixel: sourceRep.samplesPerPixel,
                                          hasAlpha: sourceRep.hasAlpha,
                                          isPlanar: sourceRep.isPlanar,
                                          colorSpaceName: sourceRep.colorSpaceName,
                                          bytesPerRow: sourceRep.bytesPerRow,
                                          bitsPerPixel: sourceRep.bitsPerPixel)!
        let ppi = self.ppi
        let newSize = NSSize(width: CGFloat(newPixelsWide) * 72.0 / ppi,
                             height: CGFloat(newPixelsHigh) * 72.0 / ppi)
        resizedRep.size = newSize
        
        NSGraphicsContext.saveGraphicsState()
        NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: resizedRep)
        self.draw(in: NSRect(origin: .zero, size: newSize))
        NSGraphicsContext.restoreGraphicsState()
        
        let image = NSImage(size: newSize)
        image.addRepresentation(resizedRep)
        return image
    }
}
KyomeKyome

画像を保存する

extension NSImage {
    func saveFile(at url: URL, fileName: String, fileType: NSBitmapImageRep.FileType) {
        guard let data = self.tiffRepresentation,
              let bitmapRep = NSBitmapImageRep(data: data),
              let imageData = bitmapRep.representation(using: fileType, properties: [:])
        else { return }
        let fileExtension: String
        switch fileType {
        case .tiff: fileExtension = "tiff"
        case .bmp: fileExtension = "bmp"
        case .gif: fileExtension = "gif"
        case .jpeg: fileExtension = "jpg"
        case .png: fileExtension = "png"
        case .jpeg2000: fileExtension = "jp2"
        @unknown default: fileExtension = ""
        }
        var saveURL = url.appendingPathComponent(fileName).appendingPathExtension(fileExtension)
        var cnt = 2
        while FileManager.default.fileExists(atPath: saveURL.path) {
            saveURL = url.appendingPathComponent("\(fileName) \(cnt)").appendingPathExtension(fileExtension)
            cnt += 1
        }
        try? imageData.write(to: saveURL, options: .atomic)
    }
}
KyomeKyome

Debug用のLog

func DebugLog(_ instance: AnyClass, _ message: String) {
#if DEBUG
    let type = String(describing: type(of: instance))
    NSLog("🛠 \(type): \(message)")
#endif
}