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

Size
10 KB
Lines
267
Language
swift
SHA-256
a656f1737326…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1import CKeyScanKit
2import CoreGraphics
3import Foundation
4#if canImport(SwiftUI)
5import SwiftUI
6#endif
7#if canImport(UIKit) && !os(watchOS)
8import UIKit
9#elseif os(macOS)
10import AppKit
11#endif
12
13/// What a view draws: a payload to encode, or an encoded symbol.
14enum 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.
55final 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)
102public 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.
165open 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.
218open 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