From 1a47513291776aa590e70716b8a823c95f3d7c99 Mon Sep 17 00:00:00 2001 From: bart Date: Tue, 22 Jun 2021 13:11:47 +0700 Subject: [PATCH 1/3] =?UTF-8?q?feature/=20Edit=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- moody.xcodeproj/project.pbxproj | 54 ++++++++++++-- moody/BottomNavigationBar.swift | 49 +++++++++++++ moody/ContentView.swift | 6 +- moody/EditView.swift | 86 +++++++++++++++++++++++ moody/FilterView.swift | 71 +++++++++++++++++++ moody/Helper/BottomNavigationButton.swift | 23 ++++++ moody/Helper/TuneImage.swift | 51 ++++++++++++++ moody/Home.swift | 65 ----------------- moody/HomeView.swift | 39 ++++++++++ moody/ImagePicker.swift | 48 +++++++------ moody/Persistence.swift | 10 +-- moody/TuningPanel.swift | 70 ++++++++++++++++++ moody/ViewModel/HomeViewModel.swift | 1 + moody/ViewModel/ImageEditor.swift | 24 +++++++ 14 files changed, 501 insertions(+), 96 deletions(-) create mode 100644 moody/BottomNavigationBar.swift create mode 100644 moody/EditView.swift create mode 100644 moody/FilterView.swift create mode 100644 moody/Helper/BottomNavigationButton.swift create mode 100644 moody/Helper/TuneImage.swift delete mode 100644 moody/Home.swift create mode 100644 moody/HomeView.swift create mode 100644 moody/TuningPanel.swift create mode 100644 moody/ViewModel/ImageEditor.swift diff --git a/moody.xcodeproj/project.pbxproj b/moody.xcodeproj/project.pbxproj index a6c10a6..c9fc8c7 100644 --- a/moody.xcodeproj/project.pbxproj +++ b/moody.xcodeproj/project.pbxproj @@ -7,6 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 89110C812680AB50002779AA /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C802680AB50002779AA /* FilterView.swift */; }; + 89110C832680AD0E002779AA /* TuneImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C822680AD0E002779AA /* TuneImage.swift */; }; + 89110C862680BCA0002779AA /* EditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C852680BCA0002779AA /* EditView.swift */; }; + 89110C882680BCEB002779AA /* BottomNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C872680BCEB002779AA /* BottomNavigationButton.swift */; }; + 89110C8B2680BD57002779AA /* BottomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C8A2680BD57002779AA /* BottomNavigationBar.swift */; }; + 89110C8D2680C729002779AA /* ImageEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C8C2680C729002779AA /* ImageEditor.swift */; }; + 895BFC3A2680829000EB783E /* TuningPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895BFC392680829000EB783E /* TuningPanel.swift */; }; C19759F8267CCE0B006003CA /* moodyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19759F7267CCE0B006003CA /* moodyApp.swift */; }; C19759FA267CCE0B006003CA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19759F9267CCE0B006003CA /* ContentView.swift */; }; C19759FC267CCE0D006003CA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C19759FB267CCE0D006003CA /* Assets.xcassets */; }; @@ -15,7 +22,7 @@ C1975A04267CCE0D006003CA /* moody.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C1975A02267CCE0D006003CA /* moody.xcdatamodeld */; }; C1975A0F267CCE0D006003CA /* moodyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1975A0E267CCE0D006003CA /* moodyTests.swift */; }; C1975A1A267CCE0D006003CA /* moodyUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1975A19267CCE0D006003CA /* moodyUITests.swift */; }; - C1975A2B267CCE8E006003CA /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1975A2A267CCE8E006003CA /* Home.swift */; }; + C1975A2B267CCE8E006003CA /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1975A2A267CCE8E006003CA /* HomeView.swift */; }; C1975A2D267CCEAF006003CA /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1975A2C267CCEAF006003CA /* ImagePicker.swift */; }; C1975A2F267CCEC8006003CA /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1975A2E267CCEC8006003CA /* HomeViewModel.swift */; }; C1975A31267CCEE2006003CA /* FilteredImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1975A30267CCEE2006003CA /* FilteredImage.swift */; }; @@ -39,6 +46,13 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 89110C802680AB50002779AA /* FilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterView.swift; sourceTree = ""; }; + 89110C822680AD0E002779AA /* TuneImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuneImage.swift; sourceTree = ""; }; + 89110C852680BCA0002779AA /* EditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditView.swift; sourceTree = ""; }; + 89110C872680BCEB002779AA /* BottomNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomNavigationButton.swift; sourceTree = ""; }; + 89110C8A2680BD57002779AA /* BottomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomNavigationBar.swift; sourceTree = ""; }; + 89110C8C2680C729002779AA /* ImageEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageEditor.swift; sourceTree = ""; }; + 895BFC392680829000EB783E /* TuningPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuningPanel.swift; sourceTree = ""; }; C19759F4267CCE0A006003CA /* moody.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = moody.app; sourceTree = BUILT_PRODUCTS_DIR; }; C19759F7267CCE0B006003CA /* moodyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = moodyApp.swift; sourceTree = ""; }; C19759F9267CCE0B006003CA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -53,7 +67,7 @@ C1975A15267CCE0D006003CA /* moodyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = moodyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C1975A19267CCE0D006003CA /* moodyUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = moodyUITests.swift; sourceTree = ""; }; C1975A1B267CCE0D006003CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C1975A2A267CCE8E006003CA /* Home.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = ""; }; + C1975A2A267CCE8E006003CA /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; C1975A2C267CCEAF006003CA /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; C1975A2E267CCEC8006003CA /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; C1975A30267CCEE2006003CA /* FilteredImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilteredImage.swift; sourceTree = ""; }; @@ -84,6 +98,24 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 89110C842680AD30002779AA /* Helper */ = { + isa = PBXGroup; + children = ( + 89110C872680BCEB002779AA /* BottomNavigationButton.swift */, + 89110C822680AD0E002779AA /* TuneImage.swift */, + ); + path = Helper; + sourceTree = ""; + }; + 89110C892680BD18002779AA /* Edit */ = { + isa = PBXGroup; + children = ( + 89110C852680BCA0002779AA /* EditView.swift */, + 895BFC392680829000EB783E /* TuningPanel.swift */, + ); + name = Edit; + sourceTree = ""; + }; C19759EB267CCE0A006003CA = { isa = PBXGroup; children = ( @@ -111,6 +143,7 @@ C1975A28267CCE32006003CA /* ViewModel */, C1975A27267CCE2B006003CA /* Model */, C19759F7267CCE0B006003CA /* moodyApp.swift */, + 89110C842680AD30002779AA /* Helper */, C19759F9267CCE0B006003CA /* ContentView.swift */, C19759FB267CCE0D006003CA /* Assets.xcassets */, C1975A00267CCE0D006003CA /* Persistence.swift */, @@ -159,6 +192,7 @@ isa = PBXGroup; children = ( C1975A2E267CCEC8006003CA /* HomeViewModel.swift */, + 89110C8C2680C729002779AA /* ImageEditor.swift */, ); path = ViewModel; sourceTree = ""; @@ -166,7 +200,10 @@ C1975A29267CCE38006003CA /* View */ = { isa = PBXGroup; children = ( - C1975A2A267CCE8E006003CA /* Home.swift */, + 89110C892680BD18002779AA /* Edit */, + C1975A2A267CCE8E006003CA /* HomeView.swift */, + 89110C8A2680BD57002779AA /* BottomNavigationBar.swift */, + 89110C802680AB50002779AA /* FilterView.swift */, C1975A2C267CCEAF006003CA /* ImagePicker.swift */, ); name = View; @@ -303,11 +340,18 @@ files = ( C1975A31267CCEE2006003CA /* FilteredImage.swift in Sources */, C1975A01267CCE0D006003CA /* Persistence.swift in Sources */, + 89110C882680BCEB002779AA /* BottomNavigationButton.swift in Sources */, + 89110C862680BCA0002779AA /* EditView.swift in Sources */, C19759FA267CCE0B006003CA /* ContentView.swift in Sources */, C1975A2F267CCEC8006003CA /* HomeViewModel.swift in Sources */, - C1975A2B267CCE8E006003CA /* Home.swift in Sources */, + 89110C832680AD0E002779AA /* TuneImage.swift in Sources */, + C1975A2B267CCE8E006003CA /* HomeView.swift in Sources */, + 89110C8D2680C729002779AA /* ImageEditor.swift in Sources */, C1975A2D267CCEAF006003CA /* ImagePicker.swift in Sources */, C1975A04267CCE0D006003CA /* moody.xcdatamodeld in Sources */, + 895BFC3A2680829000EB783E /* TuningPanel.swift in Sources */, + 89110C812680AB50002779AA /* FilterView.swift in Sources */, + 89110C8B2680BD57002779AA /* BottomNavigationBar.swift in Sources */, C19759F8267CCE0B006003CA /* moodyApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -467,6 +511,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"moody/Preview Content\""; + DEVELOPMENT_TEAM = 96XCF965VW; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = moody/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -488,6 +533,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"moody/Preview Content\""; + DEVELOPMENT_TEAM = 96XCF965VW; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = moody/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; diff --git a/moody/BottomNavigationBar.swift b/moody/BottomNavigationBar.swift new file mode 100644 index 0000000..143d6ce --- /dev/null +++ b/moody/BottomNavigationBar.swift @@ -0,0 +1,49 @@ +// +// BottomNavigationBar.swift +// moody +// +// Created by bart Shin on 21/06/2021. +// + +import SwiftUI + +struct BottomNavigationBar: View { + + @Binding var navigationTag: String? + + var body: some View { + HStack { + createNavigationLink(for: EditView(), image: Image(systemName: "slider.horizontal.below.rectangle")) + createNavigationLink(for: FilterView(), image: Image(systemName: "camera.filters")) + } + } + + private func createNavigationLink(for destination: D, image: Image) -> some View where D: View { + NavigationLink( + destination: destination, + tag: String(describing: D.self), + selection: $navigationTag) + { + drawButton(for: D.self, image: image) + .padding(.horizontal) + } + } + + private func drawButton(for destination: D.Type, image: Image) -> some View where D: View{ + Button(action: { + navigationTag = String(describing: D.self) + } ) { + image + .resizable() + .frame(width: Constant.bottomButtonSize.width, + height: Constant.bottomButtonSize.height) + .padding() + } + .buttonStyle(BottomNavigation()) + } + + struct Constant { + static let bottomButtonSize = CGSize(width: 50, height: 50) + } +} + diff --git a/moody/ContentView.swift b/moody/ContentView.swift index 55ac461..7063d66 100644 --- a/moody/ContentView.swift +++ b/moody/ContentView.swift @@ -11,12 +11,14 @@ import CoreData import SwiftUI struct ContentView: View { + let editor = ImageEditor() + var body: some View { NavigationView{ - Home() - .navigationBarTitle("Filter") + HomeView() .preferredColorScheme(.dark) } + .environmentObject(editor) } } struct ContentView_Previews: PreviewProvider { diff --git a/moody/EditView.swift b/moody/EditView.swift new file mode 100644 index 0000000..dc44ab0 --- /dev/null +++ b/moody/EditView.swift @@ -0,0 +1,86 @@ +// +// EditView.swift +// moody +// +// Created by bart Shin on 21/06/2021. +// + +import SwiftUI +import Combine + +struct EditView: View { + + @EnvironmentObject var editor: ImageEditor + @State private var tuneAdjustment = ImageTuneFactor.defaults + @State private var isShowingPicker = false + + var body: some View { + VStack { + if editor.currentImage != nil { + ScrollView([.vertical, .horizontal], + showsIndicators: false) { + editingImage + .applyTuning(tuneAdjustment) + } + } + Spacer() + TuningPanel(tuneAdjustment: $tuneAdjustment) + .onChange(of: isShowingPicker, perform: resetTunner(_:)) + } + .toolbar{ + ToolbarItem(placement: .navigationBarTrailing, + content: drawPickerButton) + } + .sheet(isPresented: $isShowingPicker) { + imagePicker + } + .navigationTitle("Edit") + } + + private func resetTunner(_ pickerPresenting: Bool) { + guard !pickerPresenting, + editor.currentImage != nil else { + return + } + withAnimation { + tuneAdjustment = ImageTuneFactor.defaults + } + } + + private func drawPickerButton() -> some View { + Button(action: { + isShowingPicker.toggle() + }) { + Image(systemName: "photo") + .font(.title2) + } + .onAppear { + if editor.currentImage == nil { + DispatchQueue.main.async { + isShowingPicker = true + } + } + } + } + + private var imagePicker: ImagePicker { + ImagePicker( + picker: $isShowingPicker, + imageData: Binding.constant(Data()), + passImage: editor.pickNewImage) + } + + private var editingImage: some View { + Image(uiImage: editor.currentImage!) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: UIScreen.main.bounds.width) + } + +} + +struct EditView_Previews: PreviewProvider { + static var previews: some View { + EditView() + } +} diff --git a/moody/FilterView.swift b/moody/FilterView.swift new file mode 100644 index 0000000..c6ba4ab --- /dev/null +++ b/moody/FilterView.swift @@ -0,0 +1,71 @@ +// +// FilterView.swift +// moody +// +// Created by bart Shin on 21/06/2021. +// + +import SwiftUI + +struct FilterView: View { + + @StateObject private var homeData = HomeViewModel() + + var body: some View { + VStack { + if !homeData.allImages.isEmpty && homeData.mainView != nil{ + + Image(uiImage: homeData.mainView.image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: UIScreen.main.bounds.width) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 20) { + ForEach(homeData.allImages){ + filtered in + Image(uiImage: filtered.image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 150, height: 150) + .onTapGesture { + homeData.mainView = filtered + } + } + } + } + .padding() + } + else if homeData.imageData.count == 0 { + Text("Pick An Image To Progress") + } + else { + ProgressView() + } + } + .navigationTitle("Filter") + .onChange(of: homeData.imageData, perform: { (_) in + // When Ever image is changed Firing loadImage + homeData.allImages.removeAll() + homeData.loadFilter() + }) + .toolbar { + // image button + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: {homeData.imagePicker.toggle()}) { + Image(systemName: "photo") + .font(.title2) + } + } + } + .sheet(isPresented: $homeData.imagePicker) { + ImagePicker(picker: $homeData.imagePicker, imageData: $homeData.imageData) + } + } +} + +struct FilterView_Previews: PreviewProvider { + static var previews: some View { + FilterView() + } +} diff --git a/moody/Helper/BottomNavigationButton.swift b/moody/Helper/BottomNavigationButton.swift new file mode 100644 index 0000000..bae27b0 --- /dev/null +++ b/moody/Helper/BottomNavigationButton.swift @@ -0,0 +1,23 @@ +// +// BottomNavigationButton.swift +// moody +// +// Created by bart Shin on 21/06/2021. +// + +import SwiftUI + +struct BottomNavigation: ButtonStyle { + + let size: CGFloat = 50 + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .clipShape(Circle()) + .frame(width: size, height: size) + .scaleEffect(configuration.isPressed ? 1.2: 1) + .animation(.easeInOut, value: configuration.isPressed) + } +} + + diff --git a/moody/Helper/TuneImage.swift b/moody/Helper/TuneImage.swift new file mode 100644 index 0000000..13e55eb --- /dev/null +++ b/moody/Helper/TuneImage.swift @@ -0,0 +1,51 @@ +// +// TuneImage.swift +// moody +// +// Created by bart Shin on 21/06/2021. +// + +import SwiftUI + +enum ImageTuneFactor: String, Hashable, CaseIterable { + + case brightness = "밝기" + case saturation = "채도" + case contrast = "대비" + + var defaultValue: Double { + switch self { + case .brightness: + return 0 + case .saturation, .contrast: + return 1 + } + } + /// All factor is 0.5 by defaults + static var defaults: [Self: Double] { + Self.allCases.reduce(into: [Self: Double]()) { + $0[$1] = $1.defaultValue + } + } + + var label: some View { + switch self { + case .brightness: + return Image(systemName: "sun.max") + case .saturation: + return Image(systemName: "drop.fill") + case .contrast: + return Image(systemName: "circle.lefthalf.fill") + } + } +} + +extension View { + func applyTuning(_ adjustment: [ImageTuneFactor: Double]) -> some View { + self + .brightness(adjustment[.brightness] ?? ImageTuneFactor.brightness.defaultValue) + .contrast(adjustment[.contrast] ?? ImageTuneFactor.contrast.defaultValue) + .saturation(adjustment[.saturation] ?? ImageTuneFactor.saturation.defaultValue) + } +} + diff --git a/moody/Home.swift b/moody/Home.swift deleted file mode 100644 index 32f9028..0000000 --- a/moody/Home.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// Home.swift -// moody -// -// Created by 김두리 on 2021/06/18. -// - -import SwiftUI - -struct Home: View { - - @StateObject var homeData = HomeViewModel() - var body: some View { - - VStack { - if !homeData.allImages.isEmpty && homeData.mainView != nil{ - - Image(uiImage: homeData.mainView.image) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UIScreen.main.bounds.width) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 20) { - ForEach(homeData.allImages){ - filtered in - Image(uiImage: filtered.image) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 150, height: 150) - - .onTapGesture { - homeData.mainView = filtered - } - } - } - .padding() - } - } - else if homeData.imageData.count == 0 { - Text("Pick An Image To Progress") - } - else { - ProgressView() - } - } - .onChange(of: homeData.imageData, perform: { (_) in - // When Ever image is changed Firing loadImage - homeData.allImages.removeAll() - homeData.loadFilter() - }) - .toolbar { - // image button - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: {homeData.imagePicker.toggle()}) { - Image(systemName: "photo") - .font(.title2) - } - } - } - .sheet(isPresented: $homeData.imagePicker) { - ImagePicker(picker: $homeData.imagePicker, imageData: $homeData.imageData) - } - } -} diff --git a/moody/HomeView.swift b/moody/HomeView.swift new file mode 100644 index 0000000..e74a0a3 --- /dev/null +++ b/moody/HomeView.swift @@ -0,0 +1,39 @@ +// +// Home.swift +// moody +// +// Created by 김두리 on 2021/06/18. +// + +import SwiftUI + +struct HomeView: View { + + @State private var navigationDestination: String? + + var body: some View { + VStack { + Text("Camera") + .foregroundColor(.primary) + .frame(width: UIScreen.main.bounds.width, + height: UIScreen.main.bounds.width) + + BottomNavigationBar(navigationTag: $navigationDestination) + .padding(.top, Constant.verticalMargin) + } + .padding(.vertical, Constant.verticalMargin) + .navigationBarTitle("Home") + } + + + struct Constant { + static let verticalMargin: CGFloat = 50 + } +} + +struct HomeView_Previews: PreviewProvider { + static var previews: some View { + HomeView() + .preferredColorScheme(.dark) + } +} diff --git a/moody/ImagePicker.swift b/moody/ImagePicker.swift index a41d3cc..c8c7559 100644 --- a/moody/ImagePicker.swift +++ b/moody/ImagePicker.swift @@ -17,6 +17,7 @@ struct ImagePicker: UIViewControllerRepresentable { @Binding var picker: Bool @Binding var imageData: Data + let passImage: ((UIImage) -> Void)? func makeUIViewController(context: Context) -> PHPickerViewController { let picker = PHPickerViewController(configuration: PHPickerConfiguration()) @@ -36,27 +37,34 @@ struct ImagePicker: UIViewControllerRepresentable { init(parent: ImagePicker) { self.parent = parent - } - - func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - // checking image is selected or cacelled - - if !results.isEmpty { - // checking image can be loaded - if results.first!.itemProvider.canLoadObject(ofClass: UIImage.self){ - results.first!.itemProvider.loadObject(ofClass: UIImage.self) { - (image, error) in - DispatchQueue.main.async { - self.parent.imageData = (image as! UIImage).pngData()! - self.parent.picker.toggle() - } - } - }else { - self.parent.picker.toggle() - } - } - + } + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + // checking image is selected or cacelled + + if !results.isEmpty { + // checking image can be loaded + if results.first!.itemProvider.canLoadObject(ofClass: UIImage.self){ + results.first!.itemProvider.loadObject(ofClass: UIImage.self) { [self] + (image, error) in + if error == nil, + let uiImage = image as? UIImage { + parent.passImage?(uiImage) + } + DispatchQueue.main.async { + self.parent.imageData = (image as! UIImage).pngData()! + self.parent.picker.toggle() + } + } + } + } } } + + init(picker: Binding, imageData: Binding, passImage: ((UIImage) -> Void)? = nil ) { + self._picker = picker + self._imageData = imageData + self.passImage = passImage + } } diff --git a/moody/Persistence.swift b/moody/Persistence.swift index 92a7c0b..0af9899 100644 --- a/moody/Persistence.swift +++ b/moody/Persistence.swift @@ -8,12 +8,12 @@ import CoreData struct PersistenceController { - static let shared = PersistenceController() + static let shared = PersistenceController() - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - for _ in 0..<10 { + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + let viewContext = result.container.viewContext + for _ in 0..<10 { let newItem = Item(context: viewContext) newItem.timestamp = Date() } diff --git a/moody/TuningPanel.swift b/moody/TuningPanel.swift new file mode 100644 index 0000000..2e3799b --- /dev/null +++ b/moody/TuningPanel.swift @@ -0,0 +1,70 @@ +// +// TuningPanel.swift +// moody +// +// Created by bart Shin on 21/06/2021. +// + +import SwiftUI + +struct TuningPanel: View { + + @State private var currentTuneFactor = ImageTuneFactor.brightness + @Binding var tuneAdjustment: [ImageTuneFactor: Double] + + private var bindingToCurrentFactor: Binding { + Binding { + tuneAdjustment[currentTuneFactor] ?? 0.5 + } set: { + tuneAdjustment[currentTuneFactor] = $0 + } + } + + var body: some View { + + VStack { + HStack { + ForEach(ImageTuneFactor.allCases, id: \.rawValue) { + drawButton(for: $0) + } + } + sliderLabel + Slider(value: bindingToCurrentFactor, in: 0...1) { + // For accessbility + sliderLabel + } + } + .padding(.horizontal, Constant.horizontalPadding) + .padding(.vertical, Constant.verticalPadding) + } + + private func drawButton(for tuneFactor: ImageTuneFactor) -> some View { + Button(action: { + withAnimation{ + currentTuneFactor = tuneFactor + } + }) { + tuneFactor.label + } + .buttonStyle(BottomNavigation()) + .foregroundColor(tuneFactor == currentTuneFactor ? .yellow: .white) + .scaleEffect(tuneFactor == currentTuneFactor ? 1.3: 1) + .padding(.horizontal) + } + + private var sliderLabel: some View { + Text(currentTuneFactor.rawValue) + .foregroundColor(.blue) + } + + struct Constant { + static let horizontalPadding: CGFloat = 50 + static let verticalPadding: CGFloat = 100 + } +} + +struct ImageTuningPanel_Previews: PreviewProvider { + static var previews: some View { + TuningPanel(tuneAdjustment: Binding.constant(ImageTuneFactor.defaults)) + } +} diff --git a/moody/ViewModel/HomeViewModel.swift b/moody/ViewModel/HomeViewModel.swift index 2232612..1524b01 100644 --- a/moody/ViewModel/HomeViewModel.swift +++ b/moody/ViewModel/HomeViewModel.swift @@ -10,6 +10,7 @@ import CoreImage import CoreImage.CIFilterBuiltins class HomeViewModel: ObservableObject { + @Published var imagePicker = false @Published var imageData = Data(count: 0) diff --git a/moody/ViewModel/ImageEditor.swift b/moody/ViewModel/ImageEditor.swift new file mode 100644 index 0000000..8bded44 --- /dev/null +++ b/moody/ViewModel/ImageEditor.swift @@ -0,0 +1,24 @@ +// +// ImageEditor.swift +// moody +// +// Created by bart Shin on 21/06/2021. +// + +import CoreImage +import UIKit + +class ImageEditor: ObservableObject { + private(set) var currentImage: UIImage? + + func pickNewImage(_ image: UIImage) { + currentImage = image + if Thread.isMainThread { + objectWillChange.send() + }else { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + } +} From 64ca0bda0b9206c4fb09ea8ebe9cfa3cb5277627 Mon Sep 17 00:00:00 2001 From: bart Date: Fri, 25 Jun 2021 14:20:09 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Eidt=20view=20=E1=84=8B=E1=85=A6=E1=84=89?= =?UTF-8?q?=E1=85=A5=20=E1=84=87=E1=85=A9=E1=84=8B=E1=85=B5=E1=84=82?= =?UTF-8?q?=E1=85=B3=E1=86=AB=20=E1=84=8B=E1=85=B5=E1=84=86=E1=85=B5?= =?UTF-8?q?=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=AA=20=E1=84=89=E1=85=B5?= =?UTF-8?q?=E1=86=AF=E1=84=8C=E1=85=A6=20=E1=84=87=E1=85=A9=E1=84=8C?= =?UTF-8?q?=E1=85=A5=E1=86=BC=E1=84=83=E1=85=AC=E1=86=AB=20=E1=84=8B?= =?UTF-8?q?=E1=85=B5=E1=84=86=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=80=E1=85=A1?= =?UTF-8?q?=20=E1=84=89=E1=85=A5=E1=84=85=E1=85=A9=20=E1=84=83=E1=85=A1?= =?UTF-8?q?=E1=84=85=E1=85=B3=E1=86=AB=20=E1=84=86=E1=85=AE=E1=86=AB?= =?UTF-8?q?=E1=84=8C=E1=85=A6=20=E1=84=89=E1=85=AE=E1=84=8C=E1=85=A5?= =?UTF-8?q?=E1=86=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- moody.xcodeproj/project.pbxproj | 8 + .../selfie_dummy.imageset/Contents.json | 21 +++ .../selfie_dummy.imageset/selfie_dummy.png | Bin 0 -> 31894 bytes moody/ContentView.swift | 1 + moody/EditView.swift | 91 +++++----- moody/EditingImage.swift | 155 ++++++++++++++++++ moody/FeedBackView.swift | 40 +++++ moody/Helper/TuneImage.swift | 17 +- moody/ImagePicker.swift | 2 + moody/Info.plist | 2 + moody/TuningPanel.swift | 28 ++-- moody/ViewModel/ImageEditor.swift | 83 +++++++++- 12 files changed, 372 insertions(+), 76 deletions(-) create mode 100644 moody/Assets.xcassets/selfie_dummy.imageset/Contents.json create mode 100644 moody/Assets.xcassets/selfie_dummy.imageset/selfie_dummy.png create mode 100644 moody/EditingImage.swift create mode 100644 moody/FeedBackView.swift diff --git a/moody.xcodeproj/project.pbxproj b/moody.xcodeproj/project.pbxproj index c9fc8c7..7ad4eed 100644 --- a/moody.xcodeproj/project.pbxproj +++ b/moody.xcodeproj/project.pbxproj @@ -13,6 +13,8 @@ 89110C882680BCEB002779AA /* BottomNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C872680BCEB002779AA /* BottomNavigationButton.swift */; }; 89110C8B2680BD57002779AA /* BottomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C8A2680BD57002779AA /* BottomNavigationBar.swift */; }; 89110C8D2680C729002779AA /* ImageEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C8C2680C729002779AA /* ImageEditor.swift */; }; + 891DF6912684277A00A314B8 /* EditingImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891DF6902684277A00A314B8 /* EditingImage.swift */; }; + 891DF6932684283400A314B8 /* FeedBackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891DF6922684283400A314B8 /* FeedBackView.swift */; }; 895BFC3A2680829000EB783E /* TuningPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895BFC392680829000EB783E /* TuningPanel.swift */; }; C19759F8267CCE0B006003CA /* moodyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19759F7267CCE0B006003CA /* moodyApp.swift */; }; C19759FA267CCE0B006003CA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19759F9267CCE0B006003CA /* ContentView.swift */; }; @@ -52,6 +54,8 @@ 89110C872680BCEB002779AA /* BottomNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomNavigationButton.swift; sourceTree = ""; }; 89110C8A2680BD57002779AA /* BottomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomNavigationBar.swift; sourceTree = ""; }; 89110C8C2680C729002779AA /* ImageEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageEditor.swift; sourceTree = ""; }; + 891DF6902684277A00A314B8 /* EditingImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingImage.swift; sourceTree = ""; }; + 891DF6922684283400A314B8 /* FeedBackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedBackView.swift; sourceTree = ""; }; 895BFC392680829000EB783E /* TuningPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuningPanel.swift; sourceTree = ""; }; C19759F4267CCE0A006003CA /* moody.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = moody.app; sourceTree = BUILT_PRODUCTS_DIR; }; C19759F7267CCE0B006003CA /* moodyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = moodyApp.swift; sourceTree = ""; }; @@ -111,6 +115,7 @@ isa = PBXGroup; children = ( 89110C852680BCA0002779AA /* EditView.swift */, + 891DF6902684277A00A314B8 /* EditingImage.swift */, 895BFC392680829000EB783E /* TuningPanel.swift */, ); name = Edit; @@ -202,6 +207,7 @@ children = ( 89110C892680BD18002779AA /* Edit */, C1975A2A267CCE8E006003CA /* HomeView.swift */, + 891DF6922684283400A314B8 /* FeedBackView.swift */, 89110C8A2680BD57002779AA /* BottomNavigationBar.swift */, 89110C802680AB50002779AA /* FilterView.swift */, C1975A2C267CCEAF006003CA /* ImagePicker.swift */, @@ -342,10 +348,12 @@ C1975A01267CCE0D006003CA /* Persistence.swift in Sources */, 89110C882680BCEB002779AA /* BottomNavigationButton.swift in Sources */, 89110C862680BCA0002779AA /* EditView.swift in Sources */, + 891DF6912684277A00A314B8 /* EditingImage.swift in Sources */, C19759FA267CCE0B006003CA /* ContentView.swift in Sources */, C1975A2F267CCEC8006003CA /* HomeViewModel.swift in Sources */, 89110C832680AD0E002779AA /* TuneImage.swift in Sources */, C1975A2B267CCE8E006003CA /* HomeView.swift in Sources */, + 891DF6932684283400A314B8 /* FeedBackView.swift in Sources */, 89110C8D2680C729002779AA /* ImageEditor.swift in Sources */, C1975A2D267CCEAF006003CA /* ImagePicker.swift in Sources */, C1975A04267CCE0D006003CA /* moody.xcdatamodeld in Sources */, diff --git a/moody/Assets.xcassets/selfie_dummy.imageset/Contents.json b/moody/Assets.xcassets/selfie_dummy.imageset/Contents.json new file mode 100644 index 0000000..33342df --- /dev/null +++ b/moody/Assets.xcassets/selfie_dummy.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "selfie_dummy.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/moody/Assets.xcassets/selfie_dummy.imageset/selfie_dummy.png b/moody/Assets.xcassets/selfie_dummy.imageset/selfie_dummy.png new file mode 100644 index 0000000000000000000000000000000000000000..8c2bcdfb9d0dd399f06c7423e3a4b55d38a80051 GIT binary patch literal 31894 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 some View { + Button(action: { + editor.saveImage() + }) { + Text("SAVE") + .font(.title) } } private func drawPickerButton() -> some View { Button(action: { - isShowingPicker.toggle() + isShowingPicker = true }) { Image(systemName: "photo") .font(.title2) } - .onAppear { - if editor.currentImage == nil { - DispatchQueue.main.async { - isShowingPicker = true - } - } - } } - private var imagePicker: ImagePicker { + private func createImagePicker () -> ImagePicker { ImagePicker( - picker: $isShowingPicker, - imageData: Binding.constant(Data()), - passImage: editor.pickNewImage) + picker: $isShowingPicker, + imageData: Binding.constant(Data()), + passImage: editor.setNewImage) } - private var editingImage: some View { - Image(uiImage: editor.currentImage!) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UIScreen.main.bounds.width) + private func resetTunner(_ pickerPresenting: Bool) { + guard !isShowingPicker else { return } + withAnimation { + editor.colorControl = ImageColorControl.defaults + } } + private func showPickerIfNeeded() { + guard editor.delegate == nil else { + return + } + editor.delegate = self + if editor.currentImage == nil { + DispatchQueue.main.async { + isShowingPicker = true + } + } + } + + // Editor delegation + func savingCompletion(error: Error?) { + if error == nil { + withAnimation{ + feedBackImage = Image(systemName: "checkmark") + } + }else { + print("Fail to save image: \(error!.localizedDescription)") + } + } } struct EditView_Previews: PreviewProvider { static var previews: some View { EditView() + .environmentObject(ImageEditor()) } } diff --git a/moody/EditingImage.swift b/moody/EditingImage.swift new file mode 100644 index 0000000..922f198 --- /dev/null +++ b/moody/EditingImage.swift @@ -0,0 +1,155 @@ +// +// EditingImage.swift +// moody +// +// Created by bart Shin on 24/06/2021. +// + +import SwiftUI + +struct EditingImage: View { + + @EnvironmentObject var editor: ImageEditor + private var image: UIImage { + UIImage(cgImage: editor.currentImage!) + } + + var body: some View { + if editor.currentImage != nil { + GeometryReader { geometry in + editingImage + .position(x: geometry.size.width / 2 + panningOffset.width, + y: geometry.size.height / 2 + panningOffset.height) + .gesture(panGesture(in: geometry.size) + .simultaneously(with: zoomGesture(in: geometry.size)) + .simultaneously(with: doubleTapToFit(in: geometry.size))) + .onAppear { + fixedZoomScale = getScaleToFit(in: geometry.size) + } + .onChange(of: editor.currentImage) { + if $0 != nil { + fixedZoomScale = getScaleToFit(in: geometry.size) + fixedPanOffset = .zero + } + } + .clipped() + } + } + } + + private var editingImage: some View { + Image(uiImage: image) + .aspectRatio(contentMode: .fill) + .scaleEffect(zoomScale) + } + + //MARK:- Zooming + + @State private var fixedZoomScale: CGFloat = 1 + @GestureState private var gestureZoomScale: CGFloat = 1 + + private var zoomScale: CGFloat { + fixedZoomScale * gestureZoomScale + } + + private func zoomGesture(in size: CGSize) -> some Gesture { + MagnificationGesture() + .updating($gestureZoomScale) { lastScale, gestureZoomScale, _ in + gestureZoomScale = lastScale + } + .onEnded { scale in + let defaultScale = getScaleToFit(in: size) + fixedZoomScale *= scale + if defaultScale > fixedZoomScale * scale { + withAnimation { + fixedZoomScale = defaultScale + fixedPanOffset = .zero + } + } + } + } + + private func doubleTapToFit(in size: CGSize) -> some Gesture { + TapGesture(count: 2) + .onEnded { + withAnimation { + fixedPanOffset = .zero + fixedZoomScale = getScaleToFit(in: size) + } + } + } + + private func getScaleToFit(in size: CGSize) -> CGFloat { + let horizontal = size.width / image.size.width + let vertical = size.height / image.size.height + return min(horizontal, vertical) + } + + + //MARK:- Panning + + @State private var fixedPanOffset: CGSize = CGSize.zero + @GestureState private var gesturePanOffset: CGSize = CGSize.zero + + private var panningOffset: CGSize { + CGSize(width: (fixedPanOffset.width + gesturePanOffset.width) * zoomScale, + height: (fixedPanOffset.height + gesturePanOffset.height) * zoomScale) + } + + private func panGesture(in size: CGSize) -> some Gesture { + DragGesture() + .updating($gesturePanOffset) { lastestDragGestureValue, gesturePanOffset, _ in + guard calcPanableSpace(in: size) != nil else { + return + } + gesturePanOffset = CGSize( + width: lastestDragGestureValue.translation.width / zoomScale, + height: lastestDragGestureValue.translation.height / zoomScale) + } + .onEnded { endValue in + guard let panableSpace = calcPanableSpace(in: size) else { + return + } + fixedPanOffset = CGSize( + width: fixedPanOffset.width + endValue.translation.width / zoomScale, + height: fixedPanOffset.height + endValue.translation.height / zoomScale) + + if checkExceedEdge(in: panableSpace) { + withAnimation { + fixedPanOffset = calcMaxiumOffset(in: panableSpace) + } + } + } + } + + private func checkExceedEdge(in panableSpace: CGSize) -> Bool { + abs(panningOffset.width) > panableSpace.width || + abs(panningOffset.height) > panableSpace .height + } + + private func calcMaxiumOffset(in panableSpace: CGSize) -> CGSize { + let currentOffset = panningOffset + let horizontal: CGFloat + let vertical: CGFloat + if abs(currentOffset.width) > panableSpace.width { + horizontal = panableSpace.width * (currentOffset.width < 0 ? -1: 1) + }else { + horizontal = currentOffset.width + } + if abs(panningOffset.height) > panableSpace.height { + vertical = panableSpace.height * (currentOffset.height < 0 ? -1: 1) + }else { + vertical = currentOffset.height + } + return CGSize(width: horizontal / zoomScale, height: vertical / zoomScale) + } + private func calcPanableSpace(in viewSize: CGSize) -> CGSize? { + let defaultZoomScale = getScaleToFit(in: viewSize) + guard zoomScale > defaultZoomScale else { + return nil + } + return CGSize( + width: image.size.width * (zoomScale - defaultZoomScale) / 2, + height: image.size.height * (zoomScale - defaultZoomScale) / 2) + } +} diff --git a/moody/FeedBackView.swift b/moody/FeedBackView.swift new file mode 100644 index 0000000..6c8ea9e --- /dev/null +++ b/moody/FeedBackView.swift @@ -0,0 +1,40 @@ +// +// FeedBackView.swift +// moody +// +// Created by bart Shin on 24/06/2021. +// + +import SwiftUI + +struct FeedBackView: View { + @Binding var feedBackImage: Image? + + private var size: CGFloat { + min(UIScreen.main.bounds.width * 0.5, UIScreen.main.bounds.height * 0.5) + } + + var body: some View { + + if feedBackImage != nil { + feedBackImage! + .resizable() + .scaledToFit() + .frame(width: size, height: size) + .padding(size * 0.3) + .overlay(Circle() + .stroke(lineWidth: 5)) + .foregroundColor(.white) + .onAppear(perform: hideFeedback) + } + } + + private func hideFeedback() { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + withAnimation { + feedBackImage = nil + } + } + } +} + diff --git a/moody/Helper/TuneImage.swift b/moody/Helper/TuneImage.swift index 13e55eb..1ade09c 100644 --- a/moody/Helper/TuneImage.swift +++ b/moody/Helper/TuneImage.swift @@ -1,5 +1,5 @@ // -// TuneImage.swift +// ImageColorControl.swift // moody // // Created by bart Shin on 21/06/2021. @@ -7,7 +7,7 @@ import SwiftUI -enum ImageTuneFactor: String, Hashable, CaseIterable { +enum ImageColorControl: String, Hashable, CaseIterable { case brightness = "밝기" case saturation = "채도" @@ -21,7 +21,7 @@ enum ImageTuneFactor: String, Hashable, CaseIterable { return 1 } } - /// All factor is 0.5 by defaults + static var defaults: [Self: Double] { Self.allCases.reduce(into: [Self: Double]()) { $0[$1] = $1.defaultValue @@ -38,14 +38,5 @@ enum ImageTuneFactor: String, Hashable, CaseIterable { return Image(systemName: "circle.lefthalf.fill") } } + } - -extension View { - func applyTuning(_ adjustment: [ImageTuneFactor: Double]) -> some View { - self - .brightness(adjustment[.brightness] ?? ImageTuneFactor.brightness.defaultValue) - .contrast(adjustment[.contrast] ?? ImageTuneFactor.contrast.defaultValue) - .saturation(adjustment[.saturation] ?? ImageTuneFactor.saturation.defaultValue) - } -} - diff --git a/moody/ImagePicker.swift b/moody/ImagePicker.swift index c8c7559..f12b385 100644 --- a/moody/ImagePicker.swift +++ b/moody/ImagePicker.swift @@ -57,6 +57,8 @@ struct ImagePicker: UIViewControllerRepresentable { } } } + }else { + self.parent.picker.toggle() } } } diff --git a/moody/Info.plist b/moody/Info.plist index efc211a..6aa8e8a 100644 --- a/moody/Info.plist +++ b/moody/Info.plist @@ -39,6 +39,8 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSPhotoLibraryAddUsageDescription + 완성된 사진을 저장하기 위해 사용자의 앨범에 접근을 허용해주세요. UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/moody/TuningPanel.swift b/moody/TuningPanel.swift index 2e3799b..3160f1b 100644 --- a/moody/TuningPanel.swift +++ b/moody/TuningPanel.swift @@ -9,14 +9,14 @@ import SwiftUI struct TuningPanel: View { - @State private var currentTuneFactor = ImageTuneFactor.brightness - @Binding var tuneAdjustment: [ImageTuneFactor: Double] + @EnvironmentObject var editor: ImageEditor + @State private var currentColorControl = ImageColorControl.brightness - private var bindingToCurrentFactor: Binding { + private var bindingToCurrentControl: Binding { Binding { - tuneAdjustment[currentTuneFactor] ?? 0.5 + editor.colorControl[currentColorControl]! } set: { - tuneAdjustment[currentTuneFactor] = $0 + editor.colorControl[currentColorControl] = $0 } } @@ -24,12 +24,12 @@ struct TuningPanel: View { VStack { HStack { - ForEach(ImageTuneFactor.allCases, id: \.rawValue) { + ForEach(ImageColorControl.allCases, id: \.rawValue) { drawButton(for: $0) } } sliderLabel - Slider(value: bindingToCurrentFactor, in: 0...1) { + Slider(value: bindingToCurrentControl, in: 0...1) { // For accessbility sliderLabel } @@ -38,33 +38,33 @@ struct TuningPanel: View { .padding(.vertical, Constant.verticalPadding) } - private func drawButton(for tuneFactor: ImageTuneFactor) -> some View { + private func drawButton(for tuneFactor: ImageColorControl) -> some View { Button(action: { withAnimation{ - currentTuneFactor = tuneFactor + currentColorControl = tuneFactor } }) { tuneFactor.label } .buttonStyle(BottomNavigation()) - .foregroundColor(tuneFactor == currentTuneFactor ? .yellow: .white) - .scaleEffect(tuneFactor == currentTuneFactor ? 1.3: 1) + .foregroundColor(tuneFactor == currentColorControl ? .yellow: .white) + .scaleEffect(tuneFactor == currentColorControl ? 1.3: 1) .padding(.horizontal) } private var sliderLabel: some View { - Text(currentTuneFactor.rawValue) + Text(currentColorControl.rawValue) .foregroundColor(.blue) } struct Constant { static let horizontalPadding: CGFloat = 50 - static let verticalPadding: CGFloat = 100 + static let verticalPadding: CGFloat = 30 } } struct ImageTuningPanel_Previews: PreviewProvider { static var previews: some View { - TuningPanel(tuneAdjustment: Binding.constant(ImageTuneFactor.defaults)) + TuningPanel() } } diff --git a/moody/ViewModel/ImageEditor.swift b/moody/ViewModel/ImageEditor.swift index 8bded44..5cc0f07 100644 --- a/moody/ViewModel/ImageEditor.swift +++ b/moody/ViewModel/ImageEditor.swift @@ -6,19 +6,84 @@ // import CoreImage -import UIKit +import SwiftUI -class ImageEditor: ObservableObject { - private(set) var currentImage: UIImage? +class ImageEditor: NSObject, ObservableObject { - func pickNewImage(_ image: UIImage) { - currentImage = image - if Thread.isMainThread { - objectWillChange.send() + private var originalImage: CGImage? + private var editingImage: CIImage? { + didSet { + if Thread.isMainThread { + objectWillChange.send() + }else { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + } + } + var currentImage: CGImage? { + if editingImage != nil { + return ciContext.createCGImage(editingImage!, from: editingImage!.extent) }else { - DispatchQueue.main.async { - self.objectWillChange.send() + return nil + } + } + var colorControl = ImageColorControl.defaults { + didSet { + DispatchQueue.global(qos: .userInitiated).async { [self] in + editingImage = captureImage() } } } + + var delegate: EditorDelegation? + private lazy var ciContext = CIContext(options: nil) + + func setNewImage(_ image: UIImage) { + originalImage = image.cgImage + editingImage = CIImage(image: image) + } + + func captureImage() -> CIImage? { + guard originalImage != nil else { + assertionFailure("Try to save image not exist") + return nil + } + let colorControlFilter = createColorControlFilter() + colorControlFilter.setValue(CIImage(cgImage: originalImage!), + forKey: "inputImage") + return colorControlFilter.outputImage + } + + fileprivate func createColorControlFilter() -> CIFilter { + let filter = CIFilter(name: "CIColorControls")! + filter.setValue(colorControl[.brightness], forKey: kCIInputBrightnessKey) + filter.setValue(colorControl[.contrast], forKey: kCIInputContrastKey) + filter.setValue(colorControl[.saturation], forKey: kCIInputSaturationKey) + return filter + } + + func saveImage() { + guard currentImage != nil , + let image = ciContext.createCGImage(editingImage!, from: editingImage!.extent) else { + savingCompletion(UIImage(), + didFinishSavingWithError: ProcessError.convertingError, contextInfo: nil) + return + } + UIImageWriteToSavedPhotosAlbum(UIImage(cgImage: image), self, + #selector(savingCompletion(_:didFinishSavingWithError:contextInfo:)), nil) + } + + @objc fileprivate func savingCompletion(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer?) { + delegate?.savingCompletion(error: error) + } + + enum ProcessError: Error { + case convertingError + } +} + +protocol EditorDelegation { + func savingCompletion(error: Error?) -> Void } From 7de96e203e91c117bd6f93e1e6610c468225ae74 Mon Sep 17 00:00:00 2001 From: bart Date: Sun, 27 Jun 2021 14:20:59 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Feature=20/=20=E1=84=8B=E1=85=B5=E1=84=86?= =?UTF-8?q?=E1=85=B5=E1=84=8C=E1=85=B5=20=E1=84=87=E1=85=AE=E1=84=87?= =?UTF-8?q?=E1=85=AE=E1=86=AB=20=E1=84=87=E1=85=B3=E1=86=AF=E1=84=85?= =?UTF-8?q?=E1=85=A5=20=E1=84=8E=E1=85=A5=E1=84=85=E1=85=B5=20=E1=84=80?= =?UTF-8?q?=E1=85=B5=E1=84=82=E1=85=B3=E1=86=BC=20=E1=84=8E=E1=85=AE?= =?UTF-8?q?=E1=84=80=E1=85=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BlurMaskView.swift | 36 ++++++ moody.xcodeproj/project.pbxproj | 16 ++- moody/ContentView.swift | 2 +- moody/EditView.swift | 10 +- moody/EditingImage.swift | 81 +++++++++---- ...{TuneImage.swift => ImageEditFactor.swift} | 6 +- moody/Helper/Publish.swift | 21 ++++ moody/HomeView.swift | 1 + moody/TuningPanel.swift | 85 ++++++++++---- moody/ViewModel/ImageEditor.swift | 109 +++++++++++++----- 10 files changed, 281 insertions(+), 86 deletions(-) create mode 100644 BlurMaskView.swift rename moody/Helper/{TuneImage.swift => ImageEditFactor.swift} (91%) create mode 100644 moody/Helper/Publish.swift diff --git a/BlurMaskView.swift b/BlurMaskView.swift new file mode 100644 index 0000000..38971d9 --- /dev/null +++ b/BlurMaskView.swift @@ -0,0 +1,36 @@ +// +// BlurMaskView.swift +// moody +// +// Created by bart Shin on 25/06/2021. +// + +import SwiftUI +import PencilKit + +struct BlurMaskView: UIViewRepresentable { + + @Binding var canvas: PKCanvasView + @Binding var markerWidth: CGFloat + + private var tool: PKInkingTool { + PKInkingTool( + .marker, color: .white, width: markerWidth) + } + + func makeUIView(context: Context) -> PKCanvasView { + canvas.drawingPolicy = .anyInput + canvas.tool = tool + canvas.backgroundColor = .clear + return canvas + } + + func updateUIView(_ uiView: PKCanvasView, context: Context) { + canvas.tool = tool + } + + init(canvas: Binding, markerWidth: Binding) { + _canvas = canvas + _markerWidth = markerWidth + } +} diff --git a/moody.xcodeproj/project.pbxproj b/moody.xcodeproj/project.pbxproj index 7ad4eed..d1a69c0 100644 --- a/moody.xcodeproj/project.pbxproj +++ b/moody.xcodeproj/project.pbxproj @@ -8,14 +8,16 @@ /* Begin PBXBuildFile section */ 89110C812680AB50002779AA /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C802680AB50002779AA /* FilterView.swift */; }; - 89110C832680AD0E002779AA /* TuneImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C822680AD0E002779AA /* TuneImage.swift */; }; + 89110C832680AD0E002779AA /* ImageEditFactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C822680AD0E002779AA /* ImageEditFactor.swift */; }; 89110C862680BCA0002779AA /* EditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C852680BCA0002779AA /* EditView.swift */; }; 89110C882680BCEB002779AA /* BottomNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C872680BCEB002779AA /* BottomNavigationButton.swift */; }; 89110C8B2680BD57002779AA /* BottomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C8A2680BD57002779AA /* BottomNavigationBar.swift */; }; 89110C8D2680C729002779AA /* ImageEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110C8C2680C729002779AA /* ImageEditor.swift */; }; 891DF6912684277A00A314B8 /* EditingImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891DF6902684277A00A314B8 /* EditingImage.swift */; }; 891DF6932684283400A314B8 /* FeedBackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891DF6922684283400A314B8 /* FeedBackView.swift */; }; + 894A3B1E2686BE020091D5FE /* Publish.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894A3B1D2686BE020091D5FE /* Publish.swift */; }; 895BFC3A2680829000EB783E /* TuningPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895BFC392680829000EB783E /* TuningPanel.swift */; }; + 898C02CC2685BA78008A2B2C /* BlurMaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 898C02CB2685BA78008A2B2C /* BlurMaskView.swift */; }; C19759F8267CCE0B006003CA /* moodyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19759F7267CCE0B006003CA /* moodyApp.swift */; }; C19759FA267CCE0B006003CA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19759F9267CCE0B006003CA /* ContentView.swift */; }; C19759FC267CCE0D006003CA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C19759FB267CCE0D006003CA /* Assets.xcassets */; }; @@ -49,14 +51,16 @@ /* Begin PBXFileReference section */ 89110C802680AB50002779AA /* FilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterView.swift; sourceTree = ""; }; - 89110C822680AD0E002779AA /* TuneImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuneImage.swift; sourceTree = ""; }; + 89110C822680AD0E002779AA /* ImageEditFactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageEditFactor.swift; sourceTree = ""; }; 89110C852680BCA0002779AA /* EditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditView.swift; sourceTree = ""; }; 89110C872680BCEB002779AA /* BottomNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomNavigationButton.swift; sourceTree = ""; }; 89110C8A2680BD57002779AA /* BottomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomNavigationBar.swift; sourceTree = ""; }; 89110C8C2680C729002779AA /* ImageEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageEditor.swift; sourceTree = ""; }; 891DF6902684277A00A314B8 /* EditingImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingImage.swift; sourceTree = ""; }; 891DF6922684283400A314B8 /* FeedBackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedBackView.swift; sourceTree = ""; }; + 894A3B1D2686BE020091D5FE /* Publish.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publish.swift; sourceTree = ""; }; 895BFC392680829000EB783E /* TuningPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuningPanel.swift; sourceTree = ""; }; + 898C02CB2685BA78008A2B2C /* BlurMaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurMaskView.swift; sourceTree = ""; }; C19759F4267CCE0A006003CA /* moody.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = moody.app; sourceTree = BUILT_PRODUCTS_DIR; }; C19759F7267CCE0B006003CA /* moodyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = moodyApp.swift; sourceTree = ""; }; C19759F9267CCE0B006003CA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -106,7 +110,8 @@ isa = PBXGroup; children = ( 89110C872680BCEB002779AA /* BottomNavigationButton.swift */, - 89110C822680AD0E002779AA /* TuneImage.swift */, + 89110C822680AD0E002779AA /* ImageEditFactor.swift */, + 894A3B1D2686BE020091D5FE /* Publish.swift */, ); path = Helper; sourceTree = ""; @@ -124,6 +129,7 @@ C19759EB267CCE0A006003CA = { isa = PBXGroup; children = ( + 898C02CB2685BA78008A2B2C /* BlurMaskView.swift */, C19759F6267CCE0A006003CA /* moody */, C1975A0D267CCE0D006003CA /* moodyTests */, C1975A18267CCE0D006003CA /* moodyUITests */, @@ -345,13 +351,15 @@ buildActionMask = 2147483647; files = ( C1975A31267CCEE2006003CA /* FilteredImage.swift in Sources */, + 894A3B1E2686BE020091D5FE /* Publish.swift in Sources */, C1975A01267CCE0D006003CA /* Persistence.swift in Sources */, 89110C882680BCEB002779AA /* BottomNavigationButton.swift in Sources */, + 898C02CC2685BA78008A2B2C /* BlurMaskView.swift in Sources */, 89110C862680BCA0002779AA /* EditView.swift in Sources */, 891DF6912684277A00A314B8 /* EditingImage.swift in Sources */, C19759FA267CCE0B006003CA /* ContentView.swift in Sources */, C1975A2F267CCEC8006003CA /* HomeViewModel.swift in Sources */, - 89110C832680AD0E002779AA /* TuneImage.swift in Sources */, + 89110C832680AD0E002779AA /* ImageEditFactor.swift in Sources */, C1975A2B267CCE8E006003CA /* HomeView.swift in Sources */, 891DF6932684283400A314B8 /* FeedBackView.swift in Sources */, 89110C8D2680C729002779AA /* ImageEditor.swift in Sources */, diff --git a/moody/ContentView.swift b/moody/ContentView.swift index 3d55739..2a3c4de 100644 --- a/moody/ContentView.swift +++ b/moody/ContentView.swift @@ -24,6 +24,6 @@ struct ContentView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() - .environmentObject(ImageEditor()) + .environmentObject(ImageEditor.forPreview) } } diff --git a/moody/EditView.swift b/moody/EditView.swift index 73b9b37..851ccc8 100644 --- a/moody/EditView.swift +++ b/moody/EditView.swift @@ -12,14 +12,16 @@ struct EditView: View, EditorDelegation { @EnvironmentObject var editor: ImageEditor @State private var isShowingPicker = false @State private var feedBackImage: Image? + @State private var currentControl: String = ImageColorControl.brightness.rawValue var body: some View { ZStack { VStack(spacing: 0) { - EditingImage() + EditingImage(currentControl: $currentControl) .contentShape(Rectangle()) - TuningPanel() + TuningPanel(currentControl: $currentControl) .onChange(of: isShowingPicker, perform: resetTunner(_:)) + .disabled(editor.imageForDisplay == nil) } FeedBackView(feedBackImage: $feedBackImage) } @@ -70,7 +72,7 @@ struct EditView: View, EditorDelegation { return } editor.delegate = self - if editor.currentImage == nil { + if editor.imageForDisplay == nil { DispatchQueue.main.async { isShowingPicker = true } @@ -92,6 +94,6 @@ struct EditView: View, EditorDelegation { struct EditView_Previews: PreviewProvider { static var previews: some View { EditView() - .environmentObject(ImageEditor()) + .environmentObject(ImageEditor.forPreview) } } diff --git a/moody/EditingImage.swift b/moody/EditingImage.swift index 922f198..602e700 100644 --- a/moody/EditingImage.swift +++ b/moody/EditingImage.swift @@ -6,43 +6,65 @@ // import SwiftUI +import PencilKit struct EditingImage: View { @EnvironmentObject var editor: ImageEditor + @Binding var currentControl: String + private var image: UIImage { - UIImage(cgImage: editor.currentImage!) + editor.imageForDisplay! + } + + private var isDrawingMask: Bool { + currentControl == ImageBlurControl.mask.rawValue } var body: some View { - if editor.currentImage != nil { + if editor.imageForDisplay != nil { GeometryReader { geometry in - editingImage + drawImage(in: geometry.size) + .scaleEffect(zoomScale) .position(x: geometry.size.width / 2 + panningOffset.width, y: geometry.size.height / 2 + panningOffset.height) .gesture(panGesture(in: geometry.size) .simultaneously(with: zoomGesture(in: geometry.size)) - .simultaneously(with: doubleTapToFit(in: geometry.size))) - .onAppear { - fixedZoomScale = getScaleToFit(in: geometry.size) - } - .onChange(of: editor.currentImage) { - if $0 != nil { - fixedZoomScale = getScaleToFit(in: geometry.size) - fixedPanOffset = .zero - } - } - .clipped() + .simultaneously(with: doubleTapGesture(in: geometry.size))) + .allowsHitTesting(!isDrawingMask) + .clipped() } } } - private var editingImage: some View { + private func drawImage(in size: CGSize) -> some View { Image(uiImage: image) - .aspectRatio(contentMode: .fill) - .scaleEffect(zoomScale) + .aspectRatio(contentMode: .fit) + .onAppear { + fixedZoomScale = getScaleToFit(in: size) + editor.blurMarkerWidth = min(60 / fixedZoomScale, 60) + } + .onChange(of: editor.originalImage) { + if $0 != nil { + zoomToFit(for: size) + editor.blurMask.drawing.strokes = [] + } + } + .onChange(of: currentControl) { + if $0 == ImageBlurControl.mask.rawValue { + zoomToFit(for: size) + }else { + editor.blurMask.drawing.strokes = [] + } + } + .overlay( + BlurMaskView(canvas: $editor.blurMask, markerWidth: $editor.blurMarkerWidth) + .colorInvert() + .allowsHitTesting(isDrawingMask) + ) } + //MARK:- Zooming @State private var fixedZoomScale: CGFloat = 1 @@ -55,9 +77,11 @@ struct EditingImage: View { private func zoomGesture(in size: CGSize) -> some Gesture { MagnificationGesture() .updating($gestureZoomScale) { lastScale, gestureZoomScale, _ in + guard !isDrawingMask else { return } gestureZoomScale = lastScale } .onEnded { scale in + guard !isDrawingMask else { return } let defaultScale = getScaleToFit(in: size) fixedZoomScale *= scale if defaultScale > fixedZoomScale * scale { @@ -69,16 +93,20 @@ struct EditingImage: View { } } - private func doubleTapToFit(in size: CGSize) -> some Gesture { + private func doubleTapGesture(in size: CGSize) -> some Gesture { TapGesture(count: 2) .onEnded { - withAnimation { - fixedPanOffset = .zero - fixedZoomScale = getScaleToFit(in: size) - } + zoomToFit(for: size) } } + private func zoomToFit(for size: CGSize) { + withAnimation { + fixedPanOffset = .zero + fixedZoomScale = getScaleToFit(in: size) + } + } + private func getScaleToFit(in size: CGSize) -> CGFloat { let horizontal = size.width / image.size.width let vertical = size.height / image.size.height @@ -98,6 +126,7 @@ struct EditingImage: View { private func panGesture(in size: CGSize) -> some Gesture { DragGesture() + .updating($gesturePanOffset) { lastestDragGestureValue, gesturePanOffset, _ in guard calcPanableSpace(in: size) != nil else { return @@ -153,3 +182,11 @@ struct EditingImage: View { height: image.size.height * (zoomScale - defaultZoomScale) / 2) } } + + +struct EditingImage_Previews: PreviewProvider { + static var previews: some View { + EditingImage(currentControl: Binding.constant(ImageColorControl.brightness.rawValue)) + .environmentObject(ImageEditor.forPreview) + } +} diff --git a/moody/Helper/TuneImage.swift b/moody/Helper/ImageEditFactor.swift similarity index 91% rename from moody/Helper/TuneImage.swift rename to moody/Helper/ImageEditFactor.swift index 1ade09c..4f6685a 100644 --- a/moody/Helper/TuneImage.swift +++ b/moody/Helper/ImageEditFactor.swift @@ -7,6 +7,10 @@ import SwiftUI +enum ImageBlurControl: String { + case mask +} + enum ImageColorControl: String, Hashable, CaseIterable { case brightness = "밝기" @@ -28,7 +32,7 @@ enum ImageColorControl: String, Hashable, CaseIterable { } } - var label: some View { + var label: Image { switch self { case .brightness: return Image(systemName: "sun.max") diff --git a/moody/Helper/Publish.swift b/moody/Helper/Publish.swift new file mode 100644 index 0000000..0c15ff7 --- /dev/null +++ b/moody/Helper/Publish.swift @@ -0,0 +1,21 @@ +// +// Publish.swift +// moody +// +// Created by bart Shin on 26/06/2021. + +// +//import Foundation +//import Combine +// +//extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher { +// func publishOnMainThread() { +// if Thread.isMainThread { +// objectWillChange.send() +// }else { +// DispatchQueue.main.async { +// self.objectWillChange.send() +// } +// } +// } +//} diff --git a/moody/HomeView.swift b/moody/HomeView.swift index e74a0a3..bb4bd12 100644 --- a/moody/HomeView.swift +++ b/moody/HomeView.swift @@ -35,5 +35,6 @@ struct HomeView_Previews: PreviewProvider { static var previews: some View { HomeView() .preferredColorScheme(.dark) + .environmentObject(ImageEditor.forPreview) } } diff --git a/moody/TuningPanel.swift b/moody/TuningPanel.swift index 3160f1b..2cae34a 100644 --- a/moody/TuningPanel.swift +++ b/moody/TuningPanel.swift @@ -10,16 +10,8 @@ import SwiftUI struct TuningPanel: View { @EnvironmentObject var editor: ImageEditor - @State private var currentColorControl = ImageColorControl.brightness - - private var bindingToCurrentControl: Binding { - Binding { - editor.colorControl[currentColorControl]! - } set: { - editor.colorControl[currentColorControl] = $0 - } - } - + @Binding var currentControl: String + var body: some View { VStack { @@ -27,44 +19,91 @@ struct TuningPanel: View { ForEach(ImageColorControl.allCases, id: \.rawValue) { drawButton(for: $0) } + blurButton } - sliderLabel - Slider(value: bindingToCurrentControl, in: 0...1) { - // For accessbility - sliderLabel + controlLabel + if let colorControl = ImageColorControl(rawValue: currentControl) { + drawSlider(for: colorControl) + }else { + blurControlSlider } } .padding(.horizontal, Constant.horizontalPadding) .padding(.vertical, Constant.verticalPadding) } - private func drawButton(for tuneFactor: ImageColorControl) -> some View { + private var controlLabel: some View { + Text(currentControl) + .foregroundColor(.blue) + } + + private var blurButton: some View { + Button(action: { + withAnimation { + currentControl = ImageBlurControl.mask.rawValue + } + }) { + Image(systemName: "lasso") + } + .buttonStyle(BottomNavigation()) + .foregroundColor(currentControl == ImageBlurControl.mask.rawValue ? .yellow: .white) + .scaleEffect(currentControl == ImageBlurControl.mask.rawValue ? 1.3: 1) + } + + private var blurControlSlider: some View { + VStack { + HStack { + Text("강도") + Slider(value: $editor.blurIntensity, in: 0...20, step: 1) + } + HStack { + Text("범위") + Slider(value: $editor.blurMarkerWidth, in: 10...60, step: 5) + } + } + .padding() + } + + private func drawButton(for colorControl: ImageColorControl) -> some View { Button(action: { withAnimation{ - currentColorControl = tuneFactor + currentControl = colorControl.rawValue } }) { - tuneFactor.label + colorControl.label } .buttonStyle(BottomNavigation()) - .foregroundColor(tuneFactor == currentColorControl ? .yellow: .white) - .scaleEffect(tuneFactor == currentColorControl ? 1.3: 1) + .foregroundColor(colorControl.rawValue == currentControl ? .yellow: .white) + .scaleEffect(colorControl.rawValue == currentControl ? 1.3: 1) .padding(.horizontal) } + + private func drawSlider(for colorControl: ImageColorControl) -> some View { + Slider(value: createBinding(to: colorControl), in: -0.5...0.5, + step: 0.05) { + // For accessbility + controlLabel + } + } - private var sliderLabel: some View { - Text(currentColorControl.rawValue) - .foregroundColor(.blue) + private func createBinding(to colorControl: ImageColorControl) -> Binding { + Binding { + editor.colorControl[colorControl]! - colorControl.defaultValue + } set: { + editor.colorControl[colorControl] = $0 + colorControl.defaultValue + } } + struct Constant { static let horizontalPadding: CGFloat = 50 static let verticalPadding: CGFloat = 30 } + } struct ImageTuningPanel_Previews: PreviewProvider { static var previews: some View { - TuningPanel() + TuningPanel(currentControl: Binding.constant(ImageColorControl.brightness.rawValue)) } } diff --git a/moody/ViewModel/ImageEditor.swift b/moody/ViewModel/ImageEditor.swift index 5cc0f07..5ec9068 100644 --- a/moody/ViewModel/ImageEditor.swift +++ b/moody/ViewModel/ImageEditor.swift @@ -7,33 +7,26 @@ import CoreImage import SwiftUI +import PencilKit class ImageEditor: NSObject, ObservableObject { - private var originalImage: CGImage? - private var editingImage: CIImage? { - didSet { - if Thread.isMainThread { - objectWillChange.send() - }else { - DispatchQueue.main.async { - self.objectWillChange.send() - } - } - } - } - var currentImage: CGImage? { - if editingImage != nil { - return ciContext.createCGImage(editingImage!, from: editingImage!.extent) - }else { - return nil - } + static var forPreview: ImageEditor { + let editor = ImageEditor() + editor.setNewImage(UIImage(named: "selfie_dummy")!) + return editor } - var colorControl = ImageColorControl.defaults { + + private(set) var originalImage: CGImage? + private var imageOrientation: UIImage.Orientation? + var imageForDisplay: UIImage? + var blurMask: PKCanvasView + var blurIntensity: Double + @Published var blurMarkerWidth: CGFloat + + var colorControl: [ImageColorControl: Double] { didSet { - DispatchQueue.global(qos: .userInitiated).async { [self] in - editingImage = captureImage() - } + setImageForDisplay() } } @@ -42,14 +35,11 @@ class ImageEditor: NSObject, ObservableObject { func setNewImage(_ image: UIImage) { originalImage = image.cgImage - editingImage = CIImage(image: image) + imageOrientation = image.imageOrientation + setImageForDisplay() } - func captureImage() -> CIImage? { - guard originalImage != nil else { - assertionFailure("Try to save image not exist") - return nil - } + func applyColorFilter() -> CIImage? { let colorControlFilter = createColorControlFilter() colorControlFilter.setValue(CIImage(cgImage: originalImage!), forKey: "inputImage") @@ -64,14 +54,32 @@ class ImageEditor: NSObject, ObservableObject { return filter } + func applyBlurByMask() { + let sourceImage = CIImage(cgImage: originalImage!) + guard let mask = CIImage(image: blurMask.drawing.image(from: sourceImage.extent, scale: 1)) else { + assertionFailure("Fail to create mask image") + return + } + let blurFilter = CIFilter(name: "CIMaskedVariableBlur")! + blurFilter.setValue(mask, forKey: "inputMask") + blurFilter.setValue(sourceImage, forKey: kCIInputImageKey) + blurFilter.setValue(blurIntensity, forKey: kCIInputRadiusKey) + if let outputImage = blurFilter.outputImage, + let cgImage = ciContext.createCGImage(outputImage, from: sourceImage.extent) { + originalImage = cgImage + setImageForDisplay() + }else { + print("Fail to apply blur") + } + } + func saveImage() { - guard currentImage != nil , - let image = ciContext.createCGImage(editingImage!, from: editingImage!.extent) else { + guard imageForDisplay != nil else { savingCompletion(UIImage(), didFinishSavingWithError: ProcessError.convertingError, contextInfo: nil) return } - UIImageWriteToSavedPhotosAlbum(UIImage(cgImage: image), self, + UIImageWriteToSavedPhotosAlbum(imageForDisplay!, self, #selector(savingCompletion(_:didFinishSavingWithError:contextInfo:)), nil) } @@ -79,9 +87,48 @@ class ImageEditor: NSObject, ObservableObject { delegate?.savingCompletion(error: error) } + private func setImageForDisplay() { + if let ciImage = applyColorFilter(), + let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent), + imageOrientation != nil{ + imageForDisplay = UIImage(cgImage: cgImage, scale: 1, orientation: imageOrientation!) + } + publishOnMainThread() + } + + private func publishOnMainThread() { + if Thread.isMainThread { + objectWillChange.send() + }else { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + } + enum ProcessError: Error { case convertingError } + + override init() { + colorControl = ImageColorControl.defaults + blurMask = PKCanvasView() + blurIntensity = 10 + blurMarkerWidth = 30 + super.init() + blurMask.delegate = self + } +} + +extension ImageEditor: PKCanvasViewDelegate { + func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) { + guard !canvasView.drawing.strokes.isEmpty else { + return + } + DispatchQueue.global(qos: .userInitiated).async { + self.applyBlurByMask() + } + } } protocol EditorDelegation {