Skip to content

Commit 594796e

Browse files
committed
[claudesquad] update from 'ClassicImageEdit in SwiftUI' on 13 Jun 25 14:03 PDT (paused)
1 parent 8396689 commit 594796e

23 files changed

+2433
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// Copyright (c) 2021 Muukii <[email protected]>
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy
5+
// of this software and associated documentation files (the "Software"), to deal
6+
// in the Software without restriction, including without limitation the rights
7+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the Software is
9+
// furnished to do so, subject to the following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included in
12+
// all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
// THE SOFTWARE.
21+
22+
import SwiftUI
23+
import Combine
24+
#if !COCOAPODS
25+
import BrightroomEngine
26+
#endif
27+
28+
@MainActor
29+
public final class ClassicImageEditSwiftUIViewModel: ObservableObject {
30+
31+
public enum Mode {
32+
case crop
33+
case masking
34+
case editing
35+
case preview
36+
}
37+
38+
@Published public private(set) var title: String = ""
39+
@Published public private(set) var mode: Mode = .preview
40+
@Published public private(set) var maskingBrushSize: CanvasView.BrushSize = .point(30)
41+
@Published public private(set) var isLoading: Bool = false
42+
43+
public let options: ClassicImageEditOptions
44+
public let editingStack: EditingStack
45+
public let localizedStrings: ClassicImageEditViewController.LocalizedStrings
46+
public let style: ClassicImageEditStyle
47+
48+
private var subscriptions: Set<AnyCancellable> = []
49+
private var proposedCrop: EditingCrop?
50+
51+
public init(
52+
editingStack: EditingStack,
53+
options: ClassicImageEditOptions,
54+
localizedStrings: ClassicImageEditViewController.LocalizedStrings,
55+
style: ClassicImageEditStyle = .default
56+
) {
57+
self.editingStack = editingStack
58+
self.options = options
59+
self.localizedStrings = localizedStrings
60+
self.style = style
61+
62+
if options.isFaceDetectionEnabled {
63+
editingStack.cropModifier = .faceDetection(aspectRatio: options.croppingAspectRatio)
64+
} else if let aspectRatio = options.croppingAspectRatio {
65+
editingStack.cropModifier = .init { image, crop, completion in
66+
var new = crop
67+
new.updateCropExtentIfNeeded(toFitAspectRatio: aspectRatio)
68+
completion(new)
69+
}
70+
}
71+
72+
editingStack.sinkState { [weak self] state in
73+
guard let self = self else { return }
74+
Task { @MainActor in
75+
self.isLoading = state.loadedState?.isLoading ?? false
76+
}
77+
}
78+
.store(in: &subscriptions)
79+
}
80+
81+
public func setTitle(_ title: String) {
82+
self.title = title
83+
}
84+
85+
public func setMode(_ mode: Mode) {
86+
self.mode = mode
87+
88+
switch mode {
89+
case .crop:
90+
title = localizedStrings.editAdjustment
91+
case .masking:
92+
title = localizedStrings.editMask
93+
case .editing:
94+
break
95+
case .preview:
96+
title = ""
97+
}
98+
}
99+
100+
public func endMasking(save: Bool) {
101+
if save {
102+
editingStack.takeSnapshot()
103+
} else {
104+
editingStack.revertEdit()
105+
}
106+
}
107+
108+
public func setBrushSize(_ brushSize: CGFloat) {
109+
maskingBrushSize = .point(brushSize)
110+
}
111+
112+
public func endCrop(save: Bool) {
113+
if save {
114+
if let proposed = proposedCrop {
115+
editingStack.crop(proposed)
116+
editingStack.takeSnapshot()
117+
}
118+
} else {
119+
if let loadedState = editingStack.state.loadedState {
120+
proposedCrop = loadedState.currentEdit.crop
121+
}
122+
}
123+
}
124+
125+
public func setProposedCrop(_ proposedCrop: EditingCrop) {
126+
self.proposedCrop = proposedCrop
127+
}
128+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
//
2+
// Copyright (c) 2021 Muukii <[email protected]>
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy
5+
// of this software and associated documentation files (the "Software"), to deal
6+
// in the Software without restriction, including without limitation the rights
7+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the Software is
9+
// furnished to do so, subject to the following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included in
12+
// all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
// THE SOFTWARE.
21+
22+
import SwiftUI
23+
import Combine
24+
#if !COCOAPODS
25+
import BrightroomEngine
26+
#endif
27+
28+
public struct ClassicImageEditView: View {
29+
30+
@StateObject private var viewModel: ClassicImageEditSwiftUIViewModel
31+
32+
public var onEndEditing: (EditingStack) -> Void = { _ in }
33+
public var onCancelEditing: () -> Void = { }
34+
35+
public init(
36+
imageProvider: ImageProvider,
37+
options: ClassicImageEditOptions = .default,
38+
localizedStrings: ClassicImageEditViewController.LocalizedStrings = .init()
39+
) {
40+
let editingStack = EditingStack(imageProvider: imageProvider)
41+
self._viewModel = StateObject(wrappedValue: ClassicImageEditSwiftUIViewModel(
42+
editingStack: editingStack,
43+
options: options,
44+
localizedStrings: localizedStrings
45+
))
46+
}
47+
48+
public init(
49+
editingStack: EditingStack,
50+
options: ClassicImageEditOptions = .default,
51+
localizedStrings: ClassicImageEditViewController.LocalizedStrings = .init()
52+
) {
53+
self._viewModel = StateObject(wrappedValue: ClassicImageEditSwiftUIViewModel(
54+
editingStack: editingStack,
55+
options: options,
56+
localizedStrings: localizedStrings
57+
))
58+
}
59+
60+
public var body: some View {
61+
NavigationView {
62+
VStack(spacing: 0) {
63+
editArea
64+
.background(Color.white)
65+
66+
controlArea
67+
.background(Color(viewModel.style.control.backgroundColor))
68+
}
69+
.ignoresSafeArea(.container, edges: .bottom)
70+
.navigationBarTitleDisplayMode(.inline)
71+
.navigationTitle(viewModel.title)
72+
.toolbar {
73+
toolbarContent
74+
}
75+
}
76+
.onAppear {
77+
viewModel.editingStack.start()
78+
}
79+
}
80+
81+
@ViewBuilder
82+
private var editArea: some View {
83+
GeometryReader { geometry in
84+
let squareSize = min(geometry.size.width, geometry.size.height)
85+
86+
ZStack {
87+
switch viewModel.mode {
88+
case .crop:
89+
CropViewWrapper(
90+
editingStack: viewModel.editingStack,
91+
croppingAspectRatio: viewModel.options.croppingAspectRatio
92+
)
93+
94+
case .masking:
95+
ImagePreviewViewWrapper(editingStack: viewModel.editingStack)
96+
BlurryMaskingViewWrapper(
97+
editingStack: viewModel.editingStack,
98+
brushSize: viewModel.maskingBrushSize,
99+
isBlurryImageViewHidden: false
100+
)
101+
102+
case .editing:
103+
ImagePreviewViewWrapper(editingStack: viewModel.editingStack)
104+
105+
case .preview:
106+
ImagePreviewViewWrapper(editingStack: viewModel.editingStack)
107+
BlurryMaskingViewWrapper(
108+
editingStack: viewModel.editingStack,
109+
brushSize: viewModel.maskingBrushSize,
110+
isBlurryImageViewHidden: false
111+
)
112+
.allowsHitTesting(false)
113+
}
114+
115+
if viewModel.isLoading {
116+
ZStack {
117+
Color.white.opacity(0.5)
118+
ProgressView()
119+
.progressViewStyle(CircularProgressViewStyle(tint: .gray))
120+
.scaleEffect(1.5)
121+
}
122+
}
123+
}
124+
.frame(width: squareSize, height: squareSize)
125+
.frame(maxWidth: .infinity, maxHeight: .infinity)
126+
}
127+
}
128+
129+
@ViewBuilder
130+
private var controlArea: some View {
131+
ClassicImageEditControlStackSwiftUIView(viewModel: viewModel)
132+
}
133+
134+
@ToolbarContentBuilder
135+
private var toolbarContent: some ToolbarContent {
136+
switch viewModel.mode {
137+
case .preview:
138+
ToolbarItem(placement: .navigationBarLeading) {
139+
Button(viewModel.localizedStrings.cancel) {
140+
onCancelEditing()
141+
}
142+
}
143+
144+
ToolbarItem(placement: .navigationBarTrailing) {
145+
Button(viewModel.localizedStrings.done) {
146+
onEndEditing(viewModel.editingStack)
147+
}
148+
.fontWeight(.semibold)
149+
.disabled(viewModel.isLoading)
150+
}
151+
152+
default:
153+
ToolbarItem(placement: .navigationBarLeading) {
154+
EmptyView()
155+
}
156+
}
157+
}
158+
}
159+
160+
struct CropViewWrapper: UIViewRepresentable {
161+
let editingStack: EditingStack
162+
let croppingAspectRatio: PixelAspectRatio?
163+
164+
func makeUIView(context: Context) -> CropView {
165+
let cropView = CropView(editingStack: editingStack, contentInset: .zero)
166+
cropView.setCropOutsideOverlay(
167+
.init()&>.do {
168+
$0.backgroundColor = .white
169+
}
170+
)
171+
cropView.setCropInsideOverlay(nil)
172+
cropView.isGuideInteractionEnabled = false
173+
cropView.isAutoApplyEditingStackEnabled = false
174+
cropView.setCroppingAspectRatio(croppingAspectRatio)
175+
return cropView
176+
}
177+
178+
func updateUIView(_ uiView: CropView, context: Context) {
179+
}
180+
}
181+
182+
struct ImagePreviewViewWrapper: UIViewRepresentable {
183+
let editingStack: EditingStack
184+
185+
func makeUIView(context: Context) -> ImagePreviewView {
186+
return ImagePreviewView(editingStack: editingStack)
187+
}
188+
189+
func updateUIView(_ uiView: ImagePreviewView, context: Context) {
190+
}
191+
}
192+
193+
struct BlurryMaskingViewWrapper: UIViewRepresentable {
194+
let editingStack: EditingStack
195+
let brushSize: CanvasView.BrushSize
196+
let isBlurryImageViewHidden: Bool
197+
198+
func makeUIView(context: Context) -> BlurryMaskingView {
199+
let view = BlurryMaskingView(editingStack: editingStack)
200+
view.isBackdropImageViewHidden = true
201+
return view
202+
}
203+
204+
func updateUIView(_ uiView: BlurryMaskingView, context: Context) {
205+
uiView.setBrushSize(brushSize)
206+
uiView.isBlurryImageViewHidden = isBlurryImageViewHidden
207+
}
208+
}
209+
210+
#Preview {
211+
ClassicImageEditView(
212+
imageProvider: .init(image: UIImage(systemName: "photo")!)
213+
)
214+
}

0 commit comments

Comments
 (0)