diff --git a/Demo/SwiftyCropDemo.xcodeproj/project.pbxproj b/Demo/SwiftyCropDemo.xcodeproj/project.pbxproj index de1f6b6..5609787 100644 --- a/Demo/SwiftyCropDemo.xcodeproj/project.pbxproj +++ b/Demo/SwiftyCropDemo.xcodeproj/project.pbxproj @@ -12,8 +12,8 @@ 7D05339D2B60AE9D001056D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7D05339C2B60AE9D001056D4 /* Assets.xcassets */; }; 7D0533A02B60AE9D001056D4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7D05339F2B60AE9D001056D4 /* Preview Assets.xcassets */; }; 7D0533A82B60AFE0001056D4 /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = 7D0533A72B60AFE0001056D4 /* SwiftyCrop */; }; - 7D3848F02B618832009AF289 /* ShapeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D3848EF2B618832009AF289 /* ShapeButton.swift */; }; 7D3848F22B61888F009AF289 /* LongText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D3848F12B61888F009AF289 /* LongText.swift */; }; + BA4737712BCE6ED500DC696D /* DecimalTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4737702BCE6ED500DC696D /* DecimalTextField.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -23,8 +23,8 @@ 7D05339C2B60AE9D001056D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 7D05339F2B60AE9D001056D4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 7D0C5DFA2B63F03F004BD980 /* SwiftyCropDemo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SwiftyCropDemo.xcconfig; sourceTree = ""; }; - 7D3848EF2B618832009AF289 /* ShapeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapeButton.swift; sourceTree = ""; }; 7D3848F12B61888F009AF289 /* LongText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongText.swift; sourceTree = ""; }; + BA4737702BCE6ED500DC696D /* DecimalTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalTextField.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -58,8 +58,7 @@ 7D0533972B60AE9C001056D4 /* SwiftyCropDemo */ = { isa = PBXGroup; children = ( - 7D3848F12B61888F009AF289 /* LongText.swift */, - 7D3848EF2B618832009AF289 /* ShapeButton.swift */, + BA47376F2BCE6EBE00DC696D /* UIElements */, 7D05339A2B60AE9C001056D4 /* ContentView.swift */, 7D0533982B60AE9C001056D4 /* SwiftyCropDemoApp.swift */, 7D05339C2B60AE9D001056D4 /* Assets.xcassets */, @@ -77,6 +76,15 @@ path = "Preview Content"; sourceTree = ""; }; + BA47376F2BCE6EBE00DC696D /* UIElements */ = { + isa = PBXGroup; + children = ( + 7D3848F12B61888F009AF289 /* LongText.swift */, + BA4737702BCE6ED500DC696D /* DecimalTextField.swift */, + ); + path = UIElements; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -154,7 +162,7 @@ buildActionMask = 2147483647; files = ( 7D05339B2B60AE9C001056D4 /* ContentView.swift in Sources */, - 7D3848F02B618832009AF289 /* ShapeButton.swift in Sources */, + BA4737712BCE6ED500DC696D /* DecimalTextField.swift in Sources */, 7D3848F22B61888F009AF289 /* LongText.swift in Sources */, 7D0533992B60AE9C001056D4 /* SwiftyCropDemoApp.swift in Sources */, ); diff --git a/Demo/SwiftyCropDemo/ContentView.swift b/Demo/SwiftyCropDemo/ContentView.swift index cfb1f01..f4e90ee 100644 --- a/Demo/SwiftyCropDemo/ContentView.swift +++ b/Demo/SwiftyCropDemo/ContentView.swift @@ -1,10 +1,3 @@ -// -// ContentView.swift -// SwiftyCropDemo -// -// Created by Leonid Zolotarev on 1/23/24. -// - import SwiftUI import SwiftyCrop @@ -12,12 +5,24 @@ struct ContentView: View { @State private var showImageCropper: Bool = false @State private var selectedImage: UIImage? @State private var selectedShape: MaskShape = .square - @State private var cropImageCircular: Bool = false - @State private var rotateImage: Bool = true - + @State private var cropImageCircular: Bool + @State private var rotateImage: Bool + @State private var maxMagnificationScale: CGFloat + @State private var maskRadius: CGFloat + @FocusState private var textFieldFocused: Bool + + init() { + let defaultConfiguration = SwiftyCropConfiguration() + _cropImageCircular = State(initialValue: defaultConfiguration.cropImageCircular) + _rotateImage = State(initialValue: defaultConfiguration.rotateImage) + _maxMagnificationScale = State(initialValue: defaultConfiguration.maxMagnificationScale) + _maskRadius = State(initialValue: defaultConfiguration.maskRadius) + } + var body: some View { VStack { Spacer() + Group { if let selectedImage = selectedImage { Image(uiImage: selectedImage) @@ -30,34 +35,72 @@ struct ContentView: View { } .scaledToFit() .padding() + Spacer() + GroupBox { - HStack { - Button { - loadImage() - } label: { - LongText(title: "Load image") + VStack(spacing: 15) { + HStack { + Button { + loadImage() + } label: { + LongText(title: "Load image") + } + Button { + showImageCropper.toggle() + } label: { + LongText(title: "Crop image") + } } - Button { - showImageCropper.toggle() - } label: { - LongText(title: "Crop image") + + HStack { + Text("Mask shape") + .frame(maxWidth: .infinity, alignment: .leading) + + Picker("maskShape", selection: $selectedShape) { + ForEach(MaskShape.allCases, id: \.self) { mask in + Text(String(describing: mask)) + } + } + .pickerStyle(.segmented) + } + + Toggle("Crop image to circle", isOn: $cropImageCircular) + + Toggle("Rotate image", isOn: $rotateImage) + + HStack { + Text("Max magnification") + .frame(maxWidth: .infinity, alignment: .leading) + + DecimalTextField(value: $maxMagnificationScale) + .focused($textFieldFocused) + } + + HStack { + Text("Mask radius") + .frame(maxWidth: .infinity, alignment: .leading) + + Button { + maskRadius = UIScreen.main.bounds.width / 2 + } label: { + Image(systemName: "arrow.up.left.and.arrow.down.right") + .font(.footnote) + } + + DecimalTextField(value: $maskRadius) + .focused($textFieldFocused) } } - HStack { - ShapeButton( - title: "Use square crop", - shape: .square, - selection: $selectedShape - ) - ShapeButton( - title: "Use circle crop", - shape: .circle, - selection: $selectedShape - ) + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + + Button("Done") { + textFieldFocused = false + } } - Toggle("Crop image to circle", isOn: $cropImageCircular) - Toggle("Rotate image", isOn: $rotateImage) } .buttonStyle(.bordered) .padding() @@ -71,6 +114,8 @@ struct ContentView: View { imageToCrop: selectedImage, maskShape: selectedShape, configuration: SwiftyCropConfiguration( + maxMagnificationScale: maxMagnificationScale, + maskRadius: maskRadius, cropImageCircular: cropImageCircular, rotateImage: rotateImage ) @@ -81,13 +126,13 @@ struct ContentView: View { } } } - + private func loadImage() { Task { selectedImage = await downloadExampleImage() } } - + // Example function for downloading an image private func downloadExampleImage() async -> UIImage? { let urlString = "https://picsum.photos/1000/1200" @@ -95,7 +140,7 @@ struct ContentView: View { let (data, _) = try? await URLSession.shared.data(from: url), let image = UIImage(data: data) else { return nil } - + return image } } diff --git a/Demo/SwiftyCropDemo/ShapeButton.swift b/Demo/SwiftyCropDemo/ShapeButton.swift deleted file mode 100644 index c7aae9f..0000000 --- a/Demo/SwiftyCropDemo/ShapeButton.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ShapeButton.swift -// SwiftyCropDemo -// -// Created by Leonid Zolotarev on 1/24/24. -// - -import SwiftUI -import SwiftyCrop - -struct ShapeButton: View { - let title: String - let shape: MaskShape - @Binding var selection: MaskShape - - var body: some View { - Button { - selection = shape - } label: { - LongText(title: title) - } - .foregroundColor(selection == shape ? .accentColor : .secondary) - } -} - -#Preview { - ShapeButton( - title: "title", - shape: .circle, - selection: .constant(.circle) - ) -} diff --git a/Demo/SwiftyCropDemo/UIElements/DecimalTextField.swift b/Demo/SwiftyCropDemo/UIElements/DecimalTextField.swift new file mode 100644 index 0000000..f616413 --- /dev/null +++ b/Demo/SwiftyCropDemo/UIElements/DecimalTextField.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct DecimalTextField: View { + @Binding var value: CGFloat + private let decimalFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.allowsFloats = true + formatter.minimumFractionDigits = 1 + formatter.decimalSeparator = "." + return formatter + }() + + var body: some View { + TextField("maxMagnification", value: $value, formatter: decimalFormatter) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .multilineTextAlignment(.trailing) + .keyboardType(.decimalPad) + } +} diff --git a/Demo/SwiftyCropDemo/LongText.swift b/Demo/SwiftyCropDemo/UIElements/LongText.swift similarity index 100% rename from Demo/SwiftyCropDemo/LongText.swift rename to Demo/SwiftyCropDemo/UIElements/LongText.swift diff --git a/Sources/SwiftyCrop/Models/MaskShape.swift b/Sources/SwiftyCrop/Models/MaskShape.swift index d20e722..38e5f20 100644 --- a/Sources/SwiftyCrop/Models/MaskShape.swift +++ b/Sources/SwiftyCrop/Models/MaskShape.swift @@ -1,3 +1,3 @@ -public enum MaskShape { +public enum MaskShape: CaseIterable { case circle, square } diff --git a/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift b/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift index b41ea53..8cda96c 100644 --- a/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift +++ b/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift @@ -2,10 +2,10 @@ import CoreGraphics /// `SwiftyCropConfiguration` is a struct that defines the configuration for cropping behavior. public struct SwiftyCropConfiguration { - let maxMagnificationScale: CGFloat - let maskRadius: CGFloat - let cropImageCircular: Bool - let rotateImage: Bool + public let maxMagnificationScale: CGFloat + public let maskRadius: CGFloat + public let cropImageCircular: Bool + public let rotateImage: Bool /// Creates a new instance of `SwiftyCropConfiguration`. ///