11import SwiftUI
2+ import PhotosUI
23
34struct CropView : View {
45 @Environment ( \. dismiss) private var dismiss
56 @StateObject private var viewModel : CropViewModel
7+
8+ @State private var isCropping : Bool = false
69
710 private let image : UIImage
811 private let maskShape : MaskShape
@@ -31,8 +34,28 @@ struct CropView: View {
3134 localizableTableName = " Localizable "
3235 }
3336
37+ // MARK: - Body
3438 var body : some View {
35- let magnificationGesture = MagnificationGesture ( )
39+ ZStack {
40+ VStack {
41+ instructionText
42+ . padding ( . top, 16 )
43+ Spacer ( )
44+ cropImageView
45+ Spacer ( )
46+ cropToolbar
47+ }
48+ . background ( configuration. colors. background)
49+
50+ if isCropping {
51+ progressLayer
52+ }
53+ }
54+ }
55+
56+ // MARK: - Gestures
57+ private var magnificationGesture : some Gesture {
58+ MagnificationGesture ( )
3659 . onChanged { value in
3760 let sensitivity : CGFloat = 0.1 * configuration. zoomSensitivity
3861 let scaledValue = ( value. magnitude - 1 ) * sensitivity + 1
@@ -46,8 +69,10 @@ struct CropView: View {
4669 viewModel. lastScale = viewModel. scale
4770 viewModel. lastOffset = viewModel. offset
4871 }
72+ }
4973
50- let dragGesture = DragGesture ( )
74+ private var dragGesture : some Gesture {
75+ DragGesture ( )
5176 . onChanged { value in
5277 let maxOffsetPoint = viewModel. calculateDragGestureMax ( )
5378 let newX = min (
@@ -63,96 +88,140 @@ struct CropView: View {
6388 . onEnded { _ in
6489 viewModel. lastOffset = viewModel. offset
6590 }
91+ }
6692
67- let rotationGesture = RotationGesture ( )
93+ private var rotationGesture : some Gesture {
94+ RotationGesture ( )
6895 . onChanged { value in
6996 viewModel. angle = viewModel. lastAngle + value
7097 }
7198 . onEnded { _ in
7299 viewModel. lastAngle = viewModel. angle
73100 }
101+ }
74102
75- VStack {
76- Text (
77- configuration. texts. interactionInstructions ??
78- NSLocalizedString ( " interaction_instructions " , tableName: localizableTableName, bundle: . module, comment: " " )
79- )
80- . font ( configuration. fonts. interactionInstructions)
81- . foregroundColor ( configuration. colors. interactionInstructions)
82- . padding ( . top, 30 )
83- . zIndex ( 1 )
84-
85- ZStack {
86- Image ( uiImage: image)
87- . resizable ( )
88- . scaledToFit ( )
89- . rotationEffect ( viewModel. angle)
90- . scaleEffect ( viewModel. scale)
91- . offset ( viewModel. offset)
92- . opacity ( 0.5 )
93- . overlay (
94- GeometryReader { geometry in
95- Color . clear
96- . onAppear {
97- viewModel. updateMaskDimensions ( for: geometry. size)
98- }
99- }
100- )
101-
102- Image ( uiImage: image)
103- . resizable ( )
104- . scaledToFit ( )
105- . rotationEffect ( viewModel. angle)
106- . scaleEffect ( viewModel. scale)
107- . offset ( viewModel. offset)
108- . mask (
109- MaskShapeView ( maskShape: maskShape)
110- . frame ( width: viewModel. maskSize. width, height: viewModel. maskSize. height)
111- )
112-
113- VStack {
114- Spacer ( )
115- HStack {
116- Button {
117- dismiss ( )
118- } label: {
119- Text (
120- configuration. texts. cancelButton ??
121- NSLocalizedString ( " cancel_button " , tableName: localizableTableName, bundle: . module, comment: " " )
122- )
123- . padding ( )
124- . font ( configuration. fonts. cancelButton)
125- . foregroundColor ( configuration. colors. cancelButton)
126- }
127- . padding ( )
128-
129- Spacer ( )
130-
131- Button {
132- onComplete ( cropImage ( ) )
133- dismiss ( )
134- } label: {
135- Text (
136- configuration. texts. saveButton ??
137- NSLocalizedString ( " save_button " , tableName: localizableTableName, bundle: . module, comment: " " )
138- )
139- . padding ( )
140- . font ( configuration. fonts. saveButton)
141- . foregroundColor ( configuration. colors. saveButton)
142- }
143- . padding ( )
103+ // MARK: - UI Components
104+ private var instructionText : some View {
105+ Text (
106+ configuration. texts. interactionInstructions ??
107+ NSLocalizedString ( " interaction_instructions " , tableName: localizableTableName, bundle: . module, comment: " " )
108+ )
109+ . font ( configuration. fonts. interactionInstructions)
110+ . foregroundColor ( configuration. colors. interactionInstructions)
111+ . padding ( . top, 30 )
112+ . zIndex ( 1 )
113+ }
114+
115+ private var cropImageView : some View {
116+ ZStack {
117+ Image ( uiImage: image)
118+ . resizable ( )
119+ . scaledToFit ( )
120+ . rotationEffect ( viewModel. angle)
121+ . scaleEffect ( viewModel. scale)
122+ . offset ( viewModel. offset)
123+ . opacity ( 0.5 )
124+ . overlay (
125+ GeometryReader { geometry in
126+ Color . clear
127+ . onAppear {
128+ viewModel. updateMaskDimensions ( for: geometry. size)
129+ }
130+ }
131+ )
132+
133+ Image ( uiImage: image)
134+ . resizable ( )
135+ . scaledToFit ( )
136+ . rotationEffect ( viewModel. angle)
137+ . scaleEffect ( viewModel. scale)
138+ . offset ( viewModel. offset)
139+ . mask (
140+ MaskShapeView ( maskShape: maskShape)
141+ . frame ( width: viewModel. maskSize. width, height: viewModel. maskSize. height)
142+ )
143+ }
144+ . frame ( maxWidth: . infinity, maxHeight: . infinity)
145+ . simultaneousGesture ( magnificationGesture)
146+ . simultaneousGesture ( dragGesture)
147+ . simultaneousGesture ( configuration. rotateImage ? rotationGesture : nil )
148+ }
149+
150+ private var cropToolbar : some View {
151+ HStack {
152+ Button {
153+ dismiss ( )
154+ } label: {
155+ Text (
156+ configuration. texts. cancelButton ??
157+ NSLocalizedString ( " cancel_button " , tableName: localizableTableName, bundle: . module, comment: " " )
158+ )
159+ . padding ( )
160+ . font ( configuration. fonts. cancelButton)
161+ . foregroundColor ( configuration. colors. cancelButton)
162+ }
163+ . padding ( )
164+
165+ Spacer ( )
166+
167+ Button {
168+ Task {
169+ isCropping = true
170+ let result = cropImage ( )
171+ await MainActor . run {
172+ onComplete ( result)
173+ dismiss ( )
144174 }
145- . frame ( maxWidth: . infinity, alignment: . bottom)
146175 }
176+ } label: {
177+ Text (
178+ configuration. texts. saveButton ??
179+ NSLocalizedString ( " save_button " , tableName: localizableTableName, bundle: . module, comment: " " )
180+ )
181+ . padding ( )
182+ . font ( configuration. fonts. saveButton)
183+ . foregroundColor ( configuration. colors. saveButton)
184+ }
185+ . padding ( )
186+ . disabled ( isCropping)
187+ }
188+ . frame ( maxWidth: . infinity, alignment: . bottom)
189+ }
190+
191+ private var progressLayer : some View {
192+ ZStack {
193+ configuration. colors. background. opacity ( 0.4 )
194+ . ignoresSafeArea ( )
195+
196+ VStack ( alignment: . center, spacing: 5 ) {
197+
198+ Spacer ( minLength: 35 )
199+
200+ ProgressView ( )
201+ . progressViewStyle ( CircularProgressViewStyle ( tint: configuration. colors. interactionInstructions) )
202+ . scaleEffect ( 1.2 )
203+
204+ Spacer ( )
205+
206+ Text (
207+ configuration. texts. progressLayerText ??
208+ NSLocalizedString ( " processing_label " , tableName: localizableTableName, bundle: . module, comment: " " )
209+ )
210+ . font ( . body)
211+ . foregroundColor ( configuration. colors. interactionInstructions)
212+ . padding ( . bottom, 12 )
213+
147214 }
148- . frame ( maxWidth: . infinity, maxHeight: . infinity)
149- . simultaneousGesture ( magnificationGesture)
150- . simultaneousGesture ( dragGesture)
151- . simultaneousGesture ( configuration. rotateImage ? rotationGesture : nil )
215+ . frame ( width: 120 , height: 110 )
216+ . background ( configuration. colors. background. opacity ( 0.8 ) )
217+ . cornerRadius ( 12 )
218+ . padding ( . vertical, 5 )
219+ . padding ( . horizontal, 15 )
152220 }
153- . background ( configuration . colors . background )
221+ . transition ( . opacity )
154222 }
155223
224+ // MARK: - Helpers
156225 private func updateOffset( ) {
157226 let maxOffsetPoint = viewModel. calculateDragGestureMax ( )
158227 let newX = min ( max ( viewModel. offset. width, - maxOffsetPoint. x) , maxOffsetPoint. x)
@@ -180,6 +249,7 @@ struct CropView: View {
180249 }
181250 }
182251
252+ // MARK: - Mask Shape View
183253 private struct MaskShapeView : View {
184254 let maskShape : MaskShape
185255
0 commit comments