-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #152 from cocoatype/151-improve-debug-overlay
Add debug overlay preferences view
- Loading branch information
Showing
14 changed files
with
300 additions
and
82 deletions.
There are no files selected for viewing
20 changes: 20 additions & 0 deletions
20
Modules/Capabilities/DebugOverlay/Sources/OverlayPreferencesHostingController.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Created by Geoff Pado on 6/17/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import SwiftUI | ||
import UIKit | ||
|
||
@available(iOS 15.0, *) | ||
public class OverlayPreferencesHostingController: UIHostingController<OverlayPreferencesView> { | ||
public init() { | ||
super.init(rootView: OverlayPreferencesView()) | ||
|
||
sheetPresentationController?.detents = [.medium()] | ||
} | ||
|
||
@available(*, unavailable) | ||
required init(coder: NSCoder) { | ||
let typeName = NSStringFromClass(type(of: self)) | ||
fatalError("\(typeName) does not implement init(coder:)") | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
Modules/Capabilities/DebugOverlay/Sources/OverlayPreferencesView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Created by Geoff Pado on 6/17/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import Defaults | ||
import DesignSystem | ||
import SwiftUI | ||
|
||
@available(iOS 15.0, *) | ||
public struct OverlayPreferencesView: View { | ||
@Defaults.Binding(key: .showDetectedTextOverlay) private var isDetectedTextOverlayEnabled: Bool | ||
@Defaults.Binding(key: .showDetectedCharactersOverlay) private var isDetectedCharactersOverlayEnabled: Bool | ||
@Defaults.Binding(key: .showRecognizedTextOverlay) private var isRecognizedTextOverlayEnabled: Bool | ||
@Defaults.Binding(key: .showCalculatedOverlay) private var isCalculatedOverlayEnabled: Bool | ||
|
||
public var body: some View { | ||
List { | ||
PreferencesCell(isOn: $isDetectedTextOverlayEnabled, title: "Detected Text", color: .red) | ||
PreferencesCell(isOn: $isDetectedCharactersOverlayEnabled, title: "Detected Characters", color: .blue) | ||
PreferencesCell(isOn: $isRecognizedTextOverlayEnabled, title: "Recognized Text", color: .yellow) | ||
PreferencesCell(isOn: $isCalculatedOverlayEnabled, title: "Calculated Area", color: .green) | ||
} | ||
} | ||
|
||
private struct PreferencesCell: View { | ||
@Binding var isOn: Bool | ||
let title: String | ||
let color: Color | ||
|
||
var body: some View { | ||
Toggle(isOn: $isOn) { | ||
Text(title) | ||
}.tint(color) | ||
} | ||
} | ||
} | ||
|
||
@available(iOS 15.0, *) | ||
enum OverlayPreferencesViewPreviews: PreviewProvider { | ||
static var previews: some View { | ||
OverlayPreferencesView() | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
Modules/Capabilities/Defaults/Sources/DefaultsBinding.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// Created by Geoff Pado on 6/17/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import Foundation | ||
import SwiftUI | ||
|
||
extension Defaults { | ||
@propertyWrapper public struct Binding<ValueType> { | ||
public var wrappedValue: ValueType { | ||
get { | ||
Self.object(for: key, fallback: fallback) | ||
} | ||
nonmutating set { | ||
Self.userDefaults.set(newValue, forKey: key.rawValue) | ||
NotificationCenter.default.post(name: valueDidChange, object: nil) | ||
} | ||
} | ||
|
||
public init(key: Defaults.Key, fallback: ValueType) { | ||
self.key = key | ||
self.fallback = fallback | ||
let valueDidChange = Notification.Name("Defaults.valueDidChange.\(key.rawValue)") | ||
self.binding = SwiftUI.Binding(get: { | ||
Self.object(for: key, fallback: fallback) | ||
}, set: { | ||
Self.userDefaults.set($0, forKey: key.rawValue) | ||
NotificationCenter.default.post(name: valueDidChange, object: nil) | ||
}) | ||
self.valueDidChange = valueDidChange | ||
} | ||
|
||
public init(key: Defaults.Key) where ValueType == Bool { | ||
self.init(key: key, fallback: false) | ||
} | ||
|
||
public init(key: Defaults.Key) where ValueType == Int { | ||
self.init(key: key, fallback: 0) | ||
} | ||
|
||
public init<ElementType>(key: Defaults.Key) where ValueType == [ElementType] { | ||
self.init(key: key, fallback: []) | ||
} | ||
|
||
public init<DictKey, DictValue>(key: Defaults.Key) where ValueType == [DictKey: DictValue] { | ||
self.init(key: key, fallback: [:]) | ||
} | ||
|
||
private let key: Defaults.Key | ||
private let fallback: ValueType | ||
|
||
// MARK: Projected Value | ||
|
||
public var projectedValue: SwiftUI.Binding<ValueType> { | ||
return binding | ||
} | ||
|
||
private let binding: SwiftUI.Binding<ValueType> | ||
|
||
// MARK: Boilerplate | ||
|
||
private static func object(for key: Key, fallback: ValueType) -> ValueType { | ||
guard let object = Self.userDefaults.object(forKey: key.rawValue) as? ValueType else { return fallback } | ||
return object | ||
} | ||
|
||
public let valueDidChange: Notification.Name | ||
|
||
static var userDefaults: UserDefaults { | ||
guard ProcessInfo.processInfo.environment["IS_TEST"] == nil else { | ||
return UserDefaults.test | ||
} | ||
|
||
return UserDefaults.standard | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
...iting/Sources/Editing View/Observation View/Debug/PhotoEditingObservationDebugLayer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Created by Geoff Pado on 6/17/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
class PhotoEditingObservationDebugLayer: CAShapeLayer { | ||
init(fillColor: UIColor, frame: CGRect, path: CGPath) { | ||
super.init() | ||
self.fillColor = fillColor.withAlphaComponent(0.3).cgColor | ||
self.frame = frame | ||
self.path = path | ||
} | ||
|
||
override init(layer: Any) { | ||
super.init(layer: layer) | ||
} | ||
|
||
@available(*, unavailable) | ||
required init(coder: NSCoder) { | ||
let typeName = NSStringFromClass(type(of: self)) | ||
fatalError("\(typeName) does not implement init(coder:)") | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
...diting/Sources/Editing View/Observation View/Debug/PhotoEditingObservationDebugView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
// Created by Geoff Pado on 7/8/22. | ||
// Copyright © 2022 Cocoatype, LLC. All rights reserved. | ||
|
||
@_implementationOnly import ClippingBezier | ||
import Combine | ||
import Defaults | ||
import Observations | ||
import UIKit | ||
|
||
class PhotoEditingObservationDebugView: PhotoEditingRedactionView { | ||
override init() { | ||
super.init() | ||
isUserInteractionEnabled = false | ||
subscribeToUpdates() | ||
} | ||
|
||
deinit { | ||
_ = cancellables.map(NotificationCenter.default.removeObserver(_:)) | ||
} | ||
|
||
// MARK: Text Observations | ||
|
||
var textObservations: [TextRectangleObservation]? { | ||
didSet { | ||
updateDebugLayers() | ||
setNeedsDisplay() | ||
} | ||
} | ||
|
||
var recognizedTextObservations: [RecognizedTextObservation]? { | ||
didSet { | ||
updateDebugLayers() | ||
setNeedsDisplay() | ||
} | ||
} | ||
|
||
// MARK: Preferences | ||
|
||
@Defaults.Value(key: .showDetectedTextOverlay) private var isDetectedTextOverlayEnabled: Bool | ||
@Defaults.Value(key: .showDetectedCharactersOverlay) private var isDetectedCharactersOverlayEnabled: Bool | ||
@Defaults.Value(key: .showRecognizedTextOverlay) private var isRecognizedTextOverlayEnabled: Bool | ||
@Defaults.Value(key: .showCalculatedOverlay) private var isCalculatedOverlayEnabled: Bool | ||
private var cancellables = [any NSObjectProtocol]() | ||
|
||
private func subscribeToUpdates() { | ||
let update: @MainActor @Sendable () -> Void = { [weak self] in self?.updateDebugLayers() } | ||
cancellables.append(NotificationCenter.default.addObserver(for: _isDetectedTextOverlayEnabled, block: update)) | ||
cancellables.append(NotificationCenter.default.addObserver(for: _isDetectedCharactersOverlayEnabled, block: update)) | ||
cancellables.append(NotificationCenter.default.addObserver(for: _isRecognizedTextOverlayEnabled, block: update)) | ||
cancellables.append(NotificationCenter.default.addObserver(for: _isCalculatedOverlayEnabled, block: update)) | ||
} | ||
|
||
private func updateDebugLayers() { | ||
Task { | ||
layer.sublayers = await debugLayers | ||
} | ||
} | ||
|
||
// MARK: Debug Layers | ||
|
||
private var debugLayers: [CAShapeLayer] { | ||
get async { | ||
guard FeatureFlag.shouldShowDebugOverlay, let textObservations, let recognizedTextObservations else { return [] } | ||
|
||
// find words (new system) | ||
let wordLayers: [PhotoEditingObservationDebugLayer] | ||
if isRecognizedTextOverlayEnabled { | ||
wordLayers = recognizedTextObservations.map { wordObservation in | ||
PhotoEditingObservationDebugLayer(fillColor: .systemYellow, frame: bounds, path: wordObservation.path) | ||
} | ||
} else { wordLayers = [] } | ||
|
||
// find text (old system) | ||
let textLayers = textObservations.flatMap { textObservation -> [CAShapeLayer] in | ||
let characterObservations = textObservation.characterObservations | ||
let characterLayers: [PhotoEditingObservationDebugLayer] | ||
if isDetectedCharactersOverlayEnabled { | ||
characterLayers = characterObservations.map { observation -> PhotoEditingObservationDebugLayer in | ||
PhotoEditingObservationDebugLayer(fillColor: .systemBlue, frame: bounds, path: observation.bounds.path) | ||
} | ||
} else { characterLayers = [] } | ||
|
||
if Defaults.Value(key: .showDetectedTextOverlay).wrappedValue { | ||
let textLayer = PhotoEditingObservationDebugLayer(fillColor: .systemRed, frame: bounds, path: CGPath(rect: textObservation.bounds.boundingBox, transform: nil)) | ||
|
||
return characterLayers + [textLayer] | ||
} else { return characterLayers } | ||
} | ||
|
||
let calculator = PhotoEditingObservationCalculator(detectedTextObservations: textObservations, recognizedTextObservations: recognizedTextObservations) | ||
let calculatedObservations = await calculator.calculatedObservations | ||
let wordCharacterLayers: [PhotoEditingObservationDebugLayer] | ||
if isCalculatedOverlayEnabled { | ||
wordCharacterLayers = calculatedObservations.map { (calculatedObservation: CharacterObservation) -> PhotoEditingObservationDebugLayer in | ||
PhotoEditingObservationDebugLayer(fillColor: .systemGreen, frame: bounds, path: calculatedObservation.bounds.path) | ||
} | ||
} else { wordCharacterLayers = [] } | ||
|
||
return textLayers + wordLayers + wordCharacterLayers | ||
} | ||
} | ||
} |
81 changes: 0 additions & 81 deletions
81
...gacy/Editing/Sources/Editing View/Observation View/PhotoEditingObservationDebugView.swift
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.