@mentra/crust 0.1.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +35 -0
  2. package/android/build.gradle +146 -0
  3. package/android/src/internal/AndroidManifest.xml +9 -0
  4. package/android/src/internal/java/com/mentra/crust/receivers/CaptionsTesterIncidentReceiver.kt +46 -0
  5. package/android/src/main/AndroidManifest.xml +19 -0
  6. package/android/src/main/java/com/mentra/crust/CrustModule.kt +882 -0
  7. package/android/src/main/java/com/mentra/crust/CrustView.kt +30 -0
  8. package/android/src/main/java/com/mentra/crust/heading/HeadingManager.kt +150 -0
  9. package/android/src/main/java/com/mentra/crust/jsc/JSCDispatcher.kt +189 -0
  10. package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +246 -0
  11. package/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt +593 -0
  12. package/android/src/main/java/com/mentra/crust/navigation/NavigationManager.kt +1445 -0
  13. package/android/src/main/java/com/mentra/crust/services/NotificationListener.kt +319 -0
  14. package/android/src/main/java/com/mentra/crust/utils/ImageProcessor.java +452 -0
  15. package/android/src/main/java/com/mentra/crust/utils/VideoStabilizer.kt +556 -0
  16. package/android/src/main/res/values/strings.xml +3 -0
  17. package/app.plugin.js +3 -0
  18. package/build/Crust.types.d.ts +148 -0
  19. package/build/Crust.types.d.ts.map +1 -0
  20. package/build/Crust.types.js +2 -0
  21. package/build/Crust.types.js.map +1 -0
  22. package/build/CrustModule.d.ts +175 -0
  23. package/build/CrustModule.d.ts.map +1 -0
  24. package/build/CrustModule.js +4 -0
  25. package/build/CrustModule.js.map +1 -0
  26. package/build/CrustModule.web.d.ts +26 -0
  27. package/build/CrustModule.web.d.ts.map +1 -0
  28. package/build/CrustModule.web.js +54 -0
  29. package/build/CrustModule.web.js.map +1 -0
  30. package/build/CrustView.d.ts +4 -0
  31. package/build/CrustView.d.ts.map +1 -0
  32. package/build/CrustView.js +7 -0
  33. package/build/CrustView.js.map +1 -0
  34. package/build/CrustView.web.d.ts +4 -0
  35. package/build/CrustView.web.d.ts.map +1 -0
  36. package/build/CrustView.web.js +7 -0
  37. package/build/CrustView.web.js.map +1 -0
  38. package/build/index.d.ts +4 -0
  39. package/build/index.d.ts.map +1 -0
  40. package/build/index.js +6 -0
  41. package/build/index.js.map +1 -0
  42. package/expo-module.config.json +9 -0
  43. package/ios/Crust.podspec +65 -0
  44. package/ios/CrustModule.swift +544 -0
  45. package/ios/CrustView.swift +38 -0
  46. package/ios/Resources/startup.js +814 -0
  47. package/ios/Source/JSCDispatcher.swift +226 -0
  48. package/ios/Source/JSCPolyfillBridge.swift +378 -0
  49. package/ios/Source/JSCRuntime.swift +673 -0
  50. package/ios/Source/utils/ImageProcessor.swift +392 -0
  51. package/ios/Source/utils/SystemGestures.swift +53 -0
  52. package/ios/Source/utils/VideoStabilizer.swift +374 -0
  53. package/ios/heading/HeadingManager.swift +74 -0
  54. package/ios/navigation/NavPayloads.swift +62 -0
  55. package/ios/navigation/NavigationManager.swift +720 -0
  56. package/package.json +69 -0
  57. package/plugin/build/index.d.ts +19 -0
  58. package/plugin/build/index.js +23 -0
  59. package/plugin/build/withAndroid.d.ts +2 -0
  60. package/plugin/build/withAndroid.js +78 -0
  61. package/src/Crust.types.ts +157 -0
  62. package/src/CrustModule.ts +186 -0
  63. package/src/CrustModule.web.ts +57 -0
  64. package/src/CrustView.tsx +10 -0
  65. package/src/CrustView.web.tsx +11 -0
  66. package/src/index.ts +5 -0
@@ -0,0 +1,392 @@
1
+ import CoreImage
2
+ import UIKit
3
+
4
+ /// Image processor for gallery photos synced from Mentra glasses.
5
+ /// Applies lens distortion correction and color correction using CoreImage.
6
+ class ImageProcessor {
7
+ private static let TAG = "ImageProcessor"
8
+
9
+ // --- Lens distortion coefficients ---
10
+ // Brown-Conrady model for the Mentra Live camera (Sony sensor, 118° FOV fisheye)
11
+ // Calibrated from chessboard photos at 3264x2448 native sensor resolution.
12
+ private static let k1: Double = -0.10
13
+ private static let k2: Double = 0.02
14
+ private static let p1: Double = 0.0
15
+ private static let p2: Double = 0.0
16
+
17
+ // --- Color pipeline tuning parameters ---
18
+
19
+ // Tone curve anchor points (CIToneCurve: x = input, y = output)
20
+ // Slight shadow lift for low-light camera, mild highlight compression, gentle S-curve.
21
+ private static let toneCurvePoint0 = CIVector(x: 0.00, y: 0.05)
22
+ private static let toneCurvePoint1 = CIVector(x: 0.25, y: 0.22)
23
+ private static let toneCurvePoint2 = CIVector(x: 0.50, y: 0.50)
24
+ private static let toneCurvePoint3 = CIVector(x: 0.75, y: 0.78)
25
+ private static let toneCurvePoint4 = CIVector(x: 1.00, y: 0.95)
26
+
27
+ // Vibrance: selective saturation boost for desaturated colors (0.0 = off, 1.0 = max)
28
+ private static let vibranceAmount: Double = 0.3
29
+
30
+ // Color correction matrix (CIColorMatrix vectors: [R, G, B, A])
31
+ // Adjusts warmth/white balance to compensate for the glasses camera's color cast.
32
+ private static let rVector = CIVector(x: 1.06, y: 0.02, z: -0.01, w: 0)
33
+ private static let gVector = CIVector(x: 0.01, y: 1.04, z: -0.01, w: 0)
34
+ private static let bVector = CIVector(x: -0.02, y: 0.01, z: 1.02, w: 0)
35
+ private static let aVector = CIVector(x: 0, y: 0, z: 0, w: 1)
36
+ private static let biasVector = CIVector(x: 5.0 / 255.0, y: 3.0 / 255.0, z: 0, w: 0)
37
+
38
+ /// Process a gallery image with the specified corrections.
39
+ /// - Parameters:
40
+ /// - inputPath: Path to the input JPEG file
41
+ /// - outputPath: Path to write the processed JPEG
42
+ /// - lensCorrection: Whether to apply barrel distortion correction
43
+ /// - colorCorrection: Whether to apply color/white balance correction
44
+ /// - Returns: Processing time in milliseconds, or -1 on failure
45
+ static func process(
46
+ inputPath: String,
47
+ outputPath: String,
48
+ lensCorrection: Bool,
49
+ colorCorrection: Bool
50
+ ) -> Int64 {
51
+ let startTime = CFAbsoluteTimeGetCurrent()
52
+
53
+ guard let inputData = FileManager.default.contents(atPath: inputPath),
54
+ let ciImage = CIImage(data: inputData)
55
+ else {
56
+ NSLog("\(TAG): Failed to load image: \(inputPath)")
57
+ return -1
58
+ }
59
+
60
+ let w = ciImage.extent.width
61
+ let h = ciImage.extent.height
62
+ NSLog("\(TAG): Processing \(Int(w))x\(Int(h)) lens=\(lensCorrection) color=\(colorCorrection)")
63
+
64
+ var image = ciImage
65
+
66
+ // Step 1: Lens distortion correction
67
+ if lensCorrection {
68
+ image = applyLensCorrection(image)
69
+ }
70
+
71
+ // Step 2: Tone mapping — S-curve + vibrance
72
+ if colorCorrection {
73
+ image = applyToneCurve(image)
74
+ image = applyVibrance(image)
75
+ }
76
+
77
+ // Step 3: Color correction — linear warmth/tint
78
+ if colorCorrection {
79
+ image = applyColorCorrection(image)
80
+ }
81
+
82
+ // Render to JPEG
83
+ let context = CIContext(options: [.useSoftwareRenderer: false])
84
+ let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
85
+
86
+ guard
87
+ let jpegData = context.jpegRepresentation(
88
+ of: image,
89
+ colorSpace: colorSpace,
90
+ options: [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 0.95]
91
+ )
92
+ else {
93
+ NSLog("\(TAG): Failed to render JPEG")
94
+ return -1
95
+ }
96
+
97
+ do {
98
+ try jpegData.write(to: URL(fileURLWithPath: outputPath))
99
+ } catch {
100
+ NSLog("\(TAG): Failed to write output: \(error.localizedDescription)")
101
+ return -1
102
+ }
103
+
104
+ let elapsed = Int64((CFAbsoluteTimeGetCurrent() - startTime) * 1000)
105
+ NSLog("\(TAG): Processing complete in \(elapsed)ms -> \(outputPath)")
106
+ return elapsed
107
+ }
108
+
109
+ /// Apply Brown-Conrady lens distortion correction using CIFilter.
110
+ private static func applyLensCorrection(_ image: CIImage) -> CIImage {
111
+ // Use a CIWarpKernel for the distortion model
112
+ // The kernel maps output coordinates to input (distorted) coordinates
113
+ let w = image.extent.width
114
+ let h = image.extent.height
115
+ let cx = w / 2.0
116
+ let cy = h / 2.0
117
+ let norm = sqrt(cx * cx + cy * cy)
118
+
119
+ // Use vImage-style approach: render pixel-by-pixel via a custom CIKernel
120
+ // For performance, we use the CGImage path with a bitmap context
121
+ let context = CIContext(options: [.useSoftwareRenderer: false])
122
+ guard let cgImage = context.createCGImage(image, from: image.extent) else {
123
+ NSLog("\(TAG): Failed to create CGImage for lens correction")
124
+ return image
125
+ }
126
+
127
+ let width = cgImage.width
128
+ let height = cgImage.height
129
+ let bytesPerPixel = 4
130
+ let bytesPerRow = width * bytesPerPixel
131
+ let totalBytes = height * bytesPerRow
132
+
133
+ // Read source pixels
134
+ guard
135
+ let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
136
+ let srcContext = CGContext(
137
+ data: nil,
138
+ width: width,
139
+ height: height,
140
+ bitsPerComponent: 8,
141
+ bytesPerRow: bytesPerRow,
142
+ space: colorSpace,
143
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
144
+ )
145
+ else {
146
+ NSLog("\(TAG): Failed to create bitmap context")
147
+ return image
148
+ }
149
+
150
+ srcContext.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
151
+ guard let srcData = srcContext.data else { return image }
152
+ let srcPixels = srcData.bindMemory(to: UInt8.self, capacity: totalBytes)
153
+
154
+ // Create output context
155
+ guard
156
+ let dstContext = CGContext(
157
+ data: nil,
158
+ width: width,
159
+ height: height,
160
+ bitsPerComponent: 8,
161
+ bytesPerRow: bytesPerRow,
162
+ space: colorSpace,
163
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
164
+ )
165
+ else { return image }
166
+
167
+ guard let dstData = dstContext.data else { return image }
168
+ let dstPixels = dstData.bindMemory(to: UInt8.self, capacity: totalBytes)
169
+
170
+ let cxI = Double(width) / 2.0
171
+ let cyI = Double(height) / 2.0
172
+ let normI = sqrt(cxI * cxI + cyI * cyI)
173
+
174
+ // Remap each pixel with bilinear interpolation for sub-pixel accuracy
175
+ let maxX = width - 2 // -2 because bilinear reads x0+1
176
+ let maxY = height - 2
177
+
178
+ for y in 0 ..< height {
179
+ for x in 0 ..< width {
180
+ let xn = (Double(x) - cxI) / normI
181
+ let yn = (Double(y) - cyI) / normI
182
+ let r2 = xn * xn + yn * yn
183
+ let r4 = r2 * r2
184
+
185
+ let radial = 1.0 + k1 * r2 + k2 * r4
186
+ let xd = xn * radial + 2.0 * p1 * xn * yn + p2 * (r2 + 2.0 * xn * xn)
187
+ let yd = yn * radial + p1 * (r2 + 2.0 * yn * yn) + 2.0 * p2 * xn * yn
188
+
189
+ let srcXf = xd * normI + cxI
190
+ let srcYf = yd * normI + cyI
191
+ let x0 = Int(srcXf)
192
+ let y0 = Int(srcYf)
193
+
194
+ let dstIdx = (y * width + x) * bytesPerPixel
195
+ if x0 < 0 || x0 > maxX || y0 < 0 || y0 > maxY {
196
+ // Out of bounds — black pixel
197
+ dstPixels[dstIdx] = 0
198
+ dstPixels[dstIdx + 1] = 0
199
+ dstPixels[dstIdx + 2] = 0
200
+ dstPixels[dstIdx + 3] = 255
201
+ continue
202
+ }
203
+
204
+ // Fractional parts for bilinear blend
205
+ let fx = Float(srcXf - Double(x0))
206
+ let fy = Float(srcYf - Double(y0))
207
+ let ifx = 1.0 - fx
208
+ let ify = 1.0 - fy
209
+
210
+ // Four source pixels
211
+ let idx00 = (y0 * width + x0) * bytesPerPixel
212
+ let idx10 = idx00 + bytesPerPixel
213
+ let idx01 = idx00 + bytesPerRow
214
+ let idx11 = idx01 + bytesPerPixel
215
+
216
+ // Bilinear blend per channel
217
+ for c in 0 ..< 4 {
218
+ let v = ifx * ify * Float(srcPixels[idx00 + c])
219
+ + fx * ify * Float(srcPixels[idx10 + c])
220
+ + ifx * fy * Float(srcPixels[idx01 + c])
221
+ + fx * fy * Float(srcPixels[idx11 + c])
222
+ dstPixels[dstIdx + c] = UInt8(min(255, max(0, Int(v + 0.5))))
223
+ }
224
+ }
225
+ }
226
+
227
+ guard let outputCGImage = dstContext.makeImage() else { return image }
228
+ return CIImage(cgImage: outputCGImage)
229
+ }
230
+
231
+ /// Apply S-curve tone mapping using CIToneCurve.
232
+ private static func applyToneCurve(_ image: CIImage) -> CIImage {
233
+ guard let filter = CIFilter(name: "CIToneCurve") else { return image }
234
+ filter.setValue(image, forKey: kCIInputImageKey)
235
+ filter.setValue(toneCurvePoint0, forKey: "inputPoint0")
236
+ filter.setValue(toneCurvePoint1, forKey: "inputPoint1")
237
+ filter.setValue(toneCurvePoint2, forKey: "inputPoint2")
238
+ filter.setValue(toneCurvePoint3, forKey: "inputPoint3")
239
+ filter.setValue(toneCurvePoint4, forKey: "inputPoint4")
240
+ return filter.outputImage ?? image
241
+ }
242
+
243
+ /// Apply vibrance — selectively boosts undersaturated colors.
244
+ private static func applyVibrance(_ image: CIImage) -> CIImage {
245
+ guard let filter = CIFilter(name: "CIVibrance") else { return image }
246
+ filter.setValue(image, forKey: kCIInputImageKey)
247
+ filter.setValue(vibranceAmount, forKey: "inputAmount")
248
+ return filter.outputImage ?? image
249
+ }
250
+
251
+ /// Apply color correction using CIColorMatrix filter.
252
+ private static func applyColorCorrection(_ image: CIImage) -> CIImage {
253
+ guard let filter = CIFilter(name: "CIColorMatrix") else {
254
+ NSLog("\(TAG): CIColorMatrix filter not available")
255
+ return image
256
+ }
257
+
258
+ filter.setValue(image, forKey: kCIInputImageKey)
259
+ filter.setValue(rVector, forKey: "inputRVector")
260
+ filter.setValue(gVector, forKey: "inputGVector")
261
+ filter.setValue(bVector, forKey: "inputBVector")
262
+ filter.setValue(aVector, forKey: "inputAVector")
263
+ filter.setValue(biasVector, forKey: "inputBiasVector")
264
+
265
+ return filter.outputImage ?? image
266
+ }
267
+
268
+ /// Merge 3 exposure-bracketed images into a single HDR result using
269
+ /// simple exposure fusion (Mertens' method approximation).
270
+ /// - Parameters:
271
+ /// - underPath: Path to underexposed image (EV-2)
272
+ /// - normalPath: Path to normally exposed image (EV0)
273
+ /// - overPath: Path to overexposed image (EV+2)
274
+ /// - outputPath: Path to write the merged result
275
+ /// - Returns: Processing time in ms, or -1 on failure
276
+ static func mergeHdr(
277
+ underPath: String, normalPath: String, overPath: String, outputPath: String
278
+ ) -> Int64 {
279
+ let startTime = CFAbsoluteTimeGetCurrent()
280
+
281
+ guard let underData = FileManager.default.contents(atPath: underPath),
282
+ let normalData = FileManager.default.contents(atPath: normalPath),
283
+ let overData = FileManager.default.contents(atPath: overPath),
284
+ let underImage = CIImage(data: underData),
285
+ let normalImage = CIImage(data: normalData),
286
+ let overImage = CIImage(data: overData)
287
+ else {
288
+ NSLog("\(TAG): Failed to load HDR bracket images")
289
+ return -1
290
+ }
291
+
292
+ let context = CIContext(options: [.useSoftwareRenderer: false])
293
+ let w = Int(normalImage.extent.width)
294
+ let h = Int(normalImage.extent.height)
295
+
296
+ // Render all three to CGImages for pixel access
297
+ guard let underCG = context.createCGImage(underImage, from: underImage.extent),
298
+ let normalCG = context.createCGImage(normalImage, from: normalImage.extent),
299
+ let overCG = context.createCGImage(overImage, from: overImage.extent)
300
+ else {
301
+ NSLog("\(TAG): Failed to create CGImages for HDR merge")
302
+ return -1
303
+ }
304
+
305
+ let bpp = 4
306
+ let bytesPerRow = w * bpp
307
+ let totalBytes = h * bytesPerRow
308
+ guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) else { return -1 }
309
+ let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
310
+
311
+ // Create contexts and read pixel data.
312
+ // IMPORTANT: We must retain the CGContexts so their backing memory stays valid
313
+ // while we access the pixel pointers.
314
+ func makePixelContext(_ cgImage: CGImage) -> CGContext? {
315
+ guard
316
+ let ctx = CGContext(
317
+ data: nil, width: w, height: h, bitsPerComponent: 8,
318
+ bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo
319
+ )
320
+ else { return nil }
321
+ ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: w, height: h))
322
+ return ctx
323
+ }
324
+
325
+ guard let underCtx = makePixelContext(underCG),
326
+ let normalCtx = makePixelContext(normalCG),
327
+ let overCtx = makePixelContext(overCG),
328
+ let uPx = underCtx.data?.bindMemory(to: UInt8.self, capacity: totalBytes),
329
+ let nPx = normalCtx.data?.bindMemory(to: UInt8.self, capacity: totalBytes),
330
+ let oPx = overCtx.data?.bindMemory(to: UInt8.self, capacity: totalBytes)
331
+ else { return -1 }
332
+
333
+ // Create output context
334
+ guard
335
+ let outCtx = CGContext(
336
+ data: nil, width: w, height: h, bitsPerComponent: 8,
337
+ bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo
338
+ )
339
+ else { return -1 }
340
+ guard let outData = outCtx.data else { return -1 }
341
+ let outPx = outData.bindMemory(to: UInt8.self, capacity: totalBytes)
342
+
343
+ // Exposure fusion
344
+ for i in stride(from: 0, to: totalBytes, by: bpp) {
345
+ let uR = Float(uPx[i])
346
+ let uG = Float(uPx[i + 1])
347
+ let uB = Float(uPx[i + 2])
348
+ let uLum = (uR + uG + uB) / 3.0 / 255.0
349
+ let uW = 4.0 * uLum * (1.0 - uLum) + 0.01
350
+
351
+ let nR = Float(nPx[i])
352
+ let nG = Float(nPx[i + 1])
353
+ let nB = Float(nPx[i + 2])
354
+ let nLum = (nR + nG + nB) / 3.0 / 255.0
355
+ let nW = 4.0 * nLum * (1.0 - nLum) + 0.01
356
+
357
+ let oR = Float(oPx[i])
358
+ let oG = Float(oPx[i + 1])
359
+ let oB = Float(oPx[i + 2])
360
+ let oLum = (oR + oG + oB) / 3.0 / 255.0
361
+ let oW = 4.0 * oLum * (1.0 - oLum) + 0.01
362
+
363
+ let total = uW + nW + oW
364
+ outPx[i] = UInt8(min(255, (uR * uW + nR * nW + oR * oW) / total))
365
+ outPx[i + 1] = UInt8(min(255, (uG * uW + nG * nW + oG * oW) / total))
366
+ outPx[i + 2] = UInt8(min(255, (uB * uW + nB * nW + oB * oW) / total))
367
+ outPx[i + 3] = 255
368
+ }
369
+
370
+ guard let outCGImage = outCtx.makeImage() else { return -1 }
371
+ let resultImage = CIImage(cgImage: outCGImage)
372
+
373
+ let resultColorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
374
+ guard
375
+ let jpegData = context.jpegRepresentation(
376
+ of: resultImage, colorSpace: resultColorSpace,
377
+ options: [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 0.95]
378
+ )
379
+ else { return -1 }
380
+
381
+ do {
382
+ try jpegData.write(to: URL(fileURLWithPath: outputPath))
383
+ } catch {
384
+ NSLog("\(TAG): Failed to write HDR result: \(error)")
385
+ return -1
386
+ }
387
+
388
+ let elapsed = Int64((CFAbsoluteTimeGetCurrent() - startTime) * 1000)
389
+ NSLog("\(TAG): HDR merge complete in \(elapsed)ms")
390
+ return elapsed
391
+ }
392
+ }
@@ -0,0 +1,53 @@
1
+ import UIKit
2
+
3
+ /// Lets JS override `preferredScreenEdgesDeferringSystemGestures` on the
4
+ /// app's root view controllers. iOS 11+ requires a swipe across a screen
5
+ /// edge to first be "deferred" before the system gesture (Control Center,
6
+ /// Notification Center, Home indicator) fires — useful when an app wants
7
+ /// the user to confirm before backgrounding, e.g. a two-swipe exit.
8
+ ///
9
+ /// Implemented via method swizzling because Expo creates the root view
10
+ /// controller internally — there is no host code path to subclass it
11
+ /// without forking ExpoModulesCore. The swizzle is installed lazily on
12
+ /// first call and is idempotent.
13
+ enum SystemGestures {
14
+ /// Edges to defer. `[]` means default behavior (system gestures fire
15
+ /// on first swipe). Read by the swizzled getter on every gesture.
16
+ static var deferredEdges: UIRectEdge = []
17
+
18
+ private static var swizzled = false
19
+
20
+ static func setDeferredEdges(_ edges: UIRectEdge) {
21
+ installSwizzleIfNeeded()
22
+ deferredEdges = edges
23
+ DispatchQueue.main.async {
24
+ for scene in UIApplication.shared.connectedScenes {
25
+ guard let windowScene = scene as? UIWindowScene else { continue }
26
+ for window in windowScene.windows {
27
+ window.rootViewController?.setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ private static func installSwizzleIfNeeded() {
34
+ guard !swizzled else { return }
35
+ swizzled = true
36
+
37
+ let cls: AnyClass = UIViewController.self
38
+ let originalSel = #selector(getter: UIViewController.preferredScreenEdgesDeferringSystemGestures)
39
+ let swizzledSel = #selector(getter: UIViewController.crust_preferredScreenEdgesDeferringSystemGestures)
40
+
41
+ guard let original = class_getInstanceMethod(cls, originalSel),
42
+ let replacement = class_getInstanceMethod(cls, swizzledSel)
43
+ else { return }
44
+
45
+ method_exchangeImplementations(original, replacement)
46
+ }
47
+ }
48
+
49
+ extension UIViewController {
50
+ @objc var crust_preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
51
+ SystemGestures.deferredEdges
52
+ }
53
+ }