Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
CKeyView.swift
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | import CKeyScanKit |
| 2 | import CoreGraphics |
| 3 | import Foundation |
| 4 | #if canImport(SwiftUI) |
| 5 | import SwiftUI |
| 6 | #endif |
| 7 | #if canImport(UIKit) &&os(watchOS) |
| 8 | import UIKit |
| 9 | #elseif os(macOS) |
| 10 | import AppKit |
| 11 | #endif |
| 12 | |
| 13 | /// What a view draws: a payload to encode, or an encoded symbol. |
| 14 | enum CKeyViewContent: Hashable { |
| 15 | case payload(Data, version: Int, level: CKeyErrorCorrection, layout: CKeyLayout) |
| 16 | case symbol(CKeySymbol) |
| 17 | |
| 18 | static func == (a: CKeyViewContent, b: CKeyViewContent) -> Bool { |
| 19 | switch (a, b) { |
| 20 | case let (.payload(d1, v1, l1, y1), .payload(d2, v2, l2, y2)): |
| 21 | return d1 == d2 && v1 == v2 && l1 == l2 && y1 == y2 |
| 22 | case let (.symbol(s1), .symbol(s2)): |
| 23 | return s1 === s2 |
| 24 | default: |
| 25 | return false |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | func hash(into hasher: inout Hasher) { |
| 30 | switch self { |
| 31 | case let .payload(data, version, level, layout): |
| 32 | hasher.combine(0) |
| 33 | hasher.combine(data) |
| 34 | hasher.combine(version) |
| 35 | hasher.combine(level) |
| 36 | hasher.combine(layout) |
| 37 | case let .symbol(symbol): |
| 38 | hasher.combine(1) |
| 39 | hasher.combine(ObjectIdentifier(symbol)) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | func makeSymbol() throws -> CKeySymbol { |
| 44 | switch self { |
| 45 | case let .payload(data, version, level, layout): |
| 46 | return try CKeySymbol.encode(data, version: version, level: level, layout: layout) |
| 47 | case let .symbol(symbol): |
| 48 | return symbol |
| 49 | } |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | /// Renders and caches the native images the views display, keyed by |
| 54 | /// content, pixel size and appearance. |
| 55 | final class CKeyImageCache { |
| 56 | static let shared = CKeyImageCache() |
| 57 | |
| 58 | private struct Key: Hashable { |
| 59 | let content: CKeyViewContent |
| 60 | let pixels: Int |
| 61 | let style: CKeyStyle |
| 62 | let policy: CKeyRasterPolicy |
| 63 | let transparent: Bool |
| 64 | } |
| 65 | |
| 66 | private final class Box: NSObject { |
| 67 | let key: Key |
| 68 | init(_ key: Key) { self.key = key } |
| 69 | override var hash: Int { key.hashValue } |
| 70 | override func isEqual(_ object: Any?) -> Bool { (object as? Box)?.key == key } |
| 71 | } |
| 72 | |
| 73 | private let cache: NSCache<Box, CGImage> = { |
| 74 | let cache = NSCache<Box, CGImage>() |
| 75 | cache.countLimit = 32 |
| 76 | return cache |
| 77 | }() |
| 78 | |
| 79 | func image(_ content: CKeyViewContent, pixels: Int, style: CKeyStyle, policy: CKeyRasterPolicy, |
| 80 | transparent: Bool) -> CGImage? { |
| 81 | guard pixels >= 16 else { return nil } |
| 82 | let box = Box(Key(content: content, pixels: min(pixels, 16384), style: style, policy: policy, transparent: transparent)) |
| 83 | if let hit = cache.object(forKey: box) { return hit } |
| 84 | guard let symbol = try? content.makeSymbol(), |
| 85 | let image = try? symbol.cgImage(size: box.key.pixels, policy: policy, transparent: transparent, style: style) |
| 86 | else { return nil } |
| 87 | cache.setObject(image, forKey: box) |
| 88 | return image |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | #if canImport(SwiftUI) |
| 93 | /// A SwiftUI view that draws a CKey as native pixels: the core rasterizes |
| 94 | /// the symbol at the view's exact pixel size (points x display scale) into a |
| 95 | /// `CGImage`; no SVG, file or web view is involved. The view is square |
| 96 | /// (aspect ratio 1) and transparent by default: no background, border or |
| 97 | /// chrome. With `transparent: true` the host must place it on a background |
| 98 | /// that contrasts with the style's ink (black for `.plain`). |
| 99 | /// |
| 100 | /// CKeyView(data: "ABC123") |
| 101 | /// .frame(width: 160, height: 160) |
| 102 | public struct CKeyView: View { |
| 103 | private let content: CKeyViewContent |
| 104 | private let style: CKeyStyle |
| 105 | private let transparent: Bool |
| 106 | private let policy: CKeyRasterPolicy |
| 107 | private let label: String |
| 108 | @Environment(displayScale) private var displayScale |
| 109 | |
| 110 | /// Encodes UTF-8 text as a Profile 0 symbol. |
| 111 | public init(data: String, version: Int = 0, level: CKeyErrorCorrection = .m, layout: CKeyLayout = .standard, |
| 112 | style: CKeyStyle = .plain, transparent: Bool = true, policy: CKeyRasterPolicy = .coverage, |
| 113 | accessibilityLabel: String = "CKey code") { |
| 114 | self.init(bytes: Data(data.utf8), version: version, level: level, layout: layout, style: style, |
| 115 | transparent: transparent, policy: policy, accessibilityLabel: accessibilityLabel) |
| 116 | } |
| 117 | |
| 118 | /// Encodes bytes as a Profile 0 symbol. |
| 119 | public init(bytes: Data, version: Int = 0, level: CKeyErrorCorrection = .m, layout: CKeyLayout = .standard, |
| 120 | style: CKeyStyle = .plain, transparent: Bool = true, policy: CKeyRasterPolicy = .coverage, |
| 121 | accessibilityLabel: String = "CKey code") { |
| 122 | content = .payload(bytes, version: version, level: level, layout: layout) |
| 123 | self.style = style |
| 124 | self.transparent = transparent |
| 125 | self.policy = policy |
| 126 | label = accessibilityLabel |
| 127 | } |
| 128 | |
| 129 | /// Draws an already encoded symbol. |
| 130 | public init(symbol: CKeySymbol, style: CKeyStyle = .plain, transparent: Bool = true, |
| 131 | policy: CKeyRasterPolicy = .coverage, accessibilityLabel: String = "CKey code") { |
| 132 | content = .symbol(symbol) |
| 133 | self.style = style |
| 134 | self.transparent = transparent |
| 135 | self.policy = policy |
| 136 | label = accessibilityLabel |
| 137 | } |
| 138 | |
| 139 | public var body: some View { |
| 140 | GeometryReader { proxy in |
| 141 | let side = max(0, min(proxy.size.width, proxy.size.height)) |
| 142 | let scale = max(displayScale, 1) |
| 143 | let pixels = Int((side * scale).rounded(.down)) |
| 144 | ZStack { |
| 145 | if let image = CKeyImageCache.shared.image(content, pixels: pixels, style: style, |
| 146 | policy: policy, transparent: transparent) { |
| 147 | Image(decorative: image, scale: scale, orientation: .up) |
| 148 | .interpolation(.none) |
| 149 | .antialiased(false) |
| 150 | } |
| 151 | } |
| 152 | .frame(width: proxy.size.width, height: proxy.size.height) |
| 153 | } |
| 154 | .aspectRatio(1, contentMode: .fit) |
| 155 | .accessibilityElement(children: .ignore) |
| 156 | .accessibilityLabel(Text(label)) |
| 157 | .accessibilityAddTraits(.isImage) |
| 158 | } |
| 159 | } |
| 160 | #endif |
| 161 | |
| 162 | #if canImport(UIKit) &&os(watchOS) |
| 163 | /// A UIKit view that draws a CKey as native pixels at its exact pixel size |
| 164 | /// (bounds x `traitCollection.displayScale`). Transparent by default. |
| 165 | open class CKeyUIView: UIView { |
| 166 | /// The symbol to draw; `nil` draws nothing. |
| 167 | public var symbol: CKeySymbol? { didSet { setNeedsLayout() } } |
| 168 | public var style: CKeyStyle = .plain { didSet { setNeedsLayout() } } |
| 169 | public var transparent: Bool = true { didSet { setNeedsLayout() } } |
| 170 | public var policy: CKeyRasterPolicy = .coverage { didSet { setNeedsLayout() } } |
| 171 | |
| 172 | public override init(frame: CGRect) { |
| 173 | super.init(frame: frame) |
| 174 | setUp() |
| 175 | } |
| 176 | |
| 177 | public required init?(coder: NSCoder) { |
| 178 | super.init(coder: coder) |
| 179 | setUp() |
| 180 | } |
| 181 | |
| 182 | /// Encodes `data` (UTF-8). A payload that cannot be encoded draws nothing. |
| 183 | public convenience init(data: String, version: Int = 0, level: CKeyErrorCorrection = .m, |
| 184 | layout: CKeyLayout = .standard) { |
| 185 | self.init(frame: .zero) |
| 186 | symbol = try? CKeySymbol.encode(data, version: version, level: level, layout: layout) |
| 187 | } |
| 188 | |
| 189 | private func setUp() { |
| 190 | isOpaque = false |
| 191 | backgroundColor = .clear |
| 192 | layer.contentsGravity = .center |
| 193 | layer.magnificationFilter = .nearest |
| 194 | isAccessibilityElement = true |
| 195 | accessibilityLabel = "CKey code" |
| 196 | accessibilityTraits = .image |
| 197 | } |
| 198 | |
| 199 | open override func layoutSubviews() { |
| 200 | super.layoutSubviews() |
| 201 | let scale = max(traitCollection.displayScale, 1) |
| 202 | let side = min(bounds.width, bounds.height) |
| 203 | let pixels = Int((side * scale).rounded(.down)) |
| 204 | layer.contentsScale = scale |
| 205 | guard let symbol else { layer.contents = nil; return } |
| 206 | layer.contents = CKeyImageCache.shared.image(.symbol(symbol), pixels: pixels, style: style, |
| 207 | policy: policy, transparent: transparent) |
| 208 | } |
| 209 | |
| 210 | open override func traitCollectionDidChange(_ previous: UITraitCollection?) { |
| 211 | super.traitCollectionDidChange(previous) |
| 212 | if previous?.displayScale != traitCollection.displayScale { setNeedsLayout() } |
| 213 | } |
| 214 | } |
| 215 | #elseif os(macOS) |
| 216 | /// An AppKit view that draws a CKey as native pixels at its exact pixel size |
| 217 | /// (bounds x the window's backing scale factor). Transparent by default. |
| 218 | open class CKeyNSView: NSView { |
| 219 | /// The symbol to draw; `nil` draws nothing. |
| 220 | public var symbol: CKeySymbol? { didSet { needsLayout = true } } |
| 221 | public var style: CKeyStyle = .plain { didSet { needsLayout = true } } |
| 222 | public var transparent: Bool = true { didSet { needsLayout = true } } |
| 223 | public var policy: CKeyRasterPolicy = .coverage { didSet { needsLayout = true } } |
| 224 | |
| 225 | public override init(frame frameRect: NSRect) { |
| 226 | super.init(frame: frameRect) |
| 227 | setUp() |
| 228 | } |
| 229 | |
| 230 | public required init?(coder: NSCoder) { |
| 231 | super.init(coder: coder) |
| 232 | setUp() |
| 233 | } |
| 234 | |
| 235 | /// Encodes `data` (UTF-8). A payload that cannot be encoded draws nothing. |
| 236 | public convenience init(data: String, version: Int = 0, level: CKeyErrorCorrection = .m, |
| 237 | layout: CKeyLayout = .standard) { |
| 238 | self.init(frame: .zero) |
| 239 | symbol = try? CKeySymbol.encode(data, version: version, level: level, layout: layout) |
| 240 | } |
| 241 | |
| 242 | private func setUp() { |
| 243 | wantsLayer = true |
| 244 | layer?.contentsGravity = .center |
| 245 | layer?.magnificationFilter = .nearest |
| 246 | setAccessibilityElement(true) |
| 247 | setAccessibilityRole(.image) |
| 248 | setAccessibilityLabel("CKey code") |
| 249 | } |
| 250 | |
| 251 | open override func layout() { |
| 252 | super.layout() |
| 253 | let scale = max(window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 1, 1) |
| 254 | let side = min(bounds.width, bounds.height) |
| 255 | let pixels = Int((side * scale).rounded(.down)) |
| 256 | layer?.contentsScale = scale |
| 257 | guard let symbol else { layer?.contents = nil; return } |
| 258 | layer?.contents = CKeyImageCache.shared.image(.symbol(symbol), pixels: pixels, style: style, |
| 259 | policy: policy, transparent: transparent) |
| 260 | } |
| 261 | |
| 262 | open override func viewDidChangeBackingProperties() { |
| 263 | super.viewDidChangeBackingProperties() |
| 264 | needsLayout = true |
| 265 | } |
| 266 | } |
| 267 | #endif |