diff --git a/day-062/Projects/Instafilter/Instafilter/Data/State/FilteredImagesState.swift b/day-062/Projects/Instafilter/Instafilter/Data/State/FilteredImagesState.swift index 6aaf31b..c551e69 100644 --- a/day-062/Projects/Instafilter/Instafilter/Data/State/FilteredImagesState.swift +++ b/day-062/Projects/Instafilter/Instafilter/Data/State/FilteredImagesState.swift @@ -13,37 +13,15 @@ import CypherPoetSwiftUIKit_DataFlowUtils struct FilteredImagesState { - var currentInputImage: UIImage? var filteredOutputCGImage: CGImage? - var filteringErrorMessage: String? } -enum FilteredImagesSideEffect: SideEffect { - case apply(filter: CIFilter, to: UIImage) - - func mapToAction() -> AnyPublisher { - switch self { - case let .apply(filter, inputImage): - return ImageFilteringService.shared - .apply(filter, to: inputImage) - .map { cgImage in - AppAction.filteredImages(.setFilteredOutput(image: cgImage)) - } - .catch { (error: ImageFilteringService.Error) in - // TODO: Better handling here 🙈 - Just(AppAction.filteredImages(.setFilteringError(message: "Failed"))) - } - .eraseToAnyPublisher() - } - } -} +//enum FilteredImagesSideEffect: SideEffect {} enum FilteredImagesAction { - case setCurrentInput(image: UIImage) case setFilteredOutput(image: CGImage) - case setFilteringError(message: String) } @@ -51,11 +29,7 @@ enum FilteredImagesAction { // MARK: - Reducer let filteredImagesReducer = Reducer { state, action in switch action { - case .setCurrentInput(let image): - state.currentInputImage = image case .setFilteredOutput(let image): state.filteredOutputCGImage = image - case .setFilteringError(let message): - state.filteringErrorMessage = message } } diff --git a/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringContainerView.swift b/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringContainerView.swift index d047ab2..b3c043f 100644 --- a/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringContainerView.swift +++ b/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringContainerView.swift @@ -27,14 +27,18 @@ extension ImageFilteringContainerView { var body: some View { NavigationView { VStack(spacing: 42.0) { - Spacer() - - imageContent - .layoutPriority(1) - - imagePickerButton - - Spacer() + Group { + if currentInputImage != nil { + ImageFilteringView(inputImage: currentInputImage!, store: store) + } else { + Spacer() + + imageSelectionSection + .padding() + + Spacer() + } + } } .navigationBarTitle("📸 Instafilter") .navigationBarItems(trailing: saveButton) @@ -60,14 +64,13 @@ extension ImageFilteringContainerView { // MARK: - View Variables extension ImageFilteringContainerView { - private var imageContent: some View { - Group { - if currentInputImage != nil { - ImageFilteringView(inputImage: currentInputImage!, store: store) - } else { - Text("Select an Image to begin filtering.") - .font(.title) - } + private var imageSelectionSection: some View { + VStack(spacing: 36) { + Text("Select an Image to begin filtering.") + .font(.title) + .multilineTextAlignment(.center) + + imagePickerButton } } diff --git a/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringView.swift b/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringView.swift index 662c725..ebfed16 100644 --- a/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringView.swift +++ b/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringView.swift @@ -10,12 +10,18 @@ import SwiftUI struct ImageFilteringView: View { + @EnvironmentObject private var store: AppStore @ObservedObject private(set) var viewModel: ImageFilteringViewModel + + private let inputImage: UIImage + init( inputImage: UIImage, store: AppStore ) { + self.inputImage = inputImage + self.viewModel = ImageFilteringViewModel( inputImage: inputImage, store: store @@ -34,9 +40,15 @@ extension ImageFilteringView { Text(viewModel.filteringErrorMessage!) } - (viewModel.filteredImage ?? Image(uiImage: viewModel.inputImage)) + (outputImage ?? Image(uiImage: inputImage)) .resizable() .scaledToFit() + + controls + .padding() + .padding(.top) + + Spacer() } } } @@ -44,7 +56,14 @@ extension ImageFilteringView { // MARK: - Computeds extension ImageFilteringView { - + + var outputImage: Image? { + guard let cgImage = store.state.filteredImages.filteredOutputCGImage else { + return nil + } + + return Image(uiImage: UIImage(cgImage: cgImage)) + } } @@ -52,7 +71,24 @@ extension ImageFilteringView { // MARK: - View Variables extension ImageFilteringView { - + private var controls: some View { + VStack(spacing: 24) { + + VStack(spacing: 2.0) { + Text("Intensity") + .font(.headline) + .fontWeight(.bold) + + Slider( + value: $viewModel.filterIntensity, + minimumValueLabel: Text("0"), + maximumValueLabel: Text("1") + ) { + Text("Intensity") + } + } + } + } } @@ -61,9 +97,12 @@ extension ImageFilteringView { struct ImageFilteringView_Previews: PreviewProvider { static var previews: some View { - ImageFilteringView( - inputImage: UIImage(named: "earth-night")!, - store: SampleStore.default - ) + NavigationView { + ImageFilteringView( + inputImage: UIImage(named: "earth-night")!, + store: SampleStore.default + ) + .environmentObject(SampleStore.default) + } } } diff --git a/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringViewModel.swift b/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringViewModel.swift index 7e084c9..cc753ba 100644 --- a/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringViewModel.swift +++ b/day-062/Projects/Instafilter/Instafilter/Scenes/ImageFilteringViewModel.swift @@ -19,10 +19,11 @@ final class ImageFilteringViewModel: ObservableObject { @Published var inputImage: UIImage @Published var currentFilter: CIFilter + @Published var filterIntensity: CGFloat = 0.0 // MARK: - Published Properties - @Published var filteredImage: Image? +// @Published var filteredImage: Image? @Published var filteringErrorMessage: String? @@ -43,26 +44,33 @@ final class ImageFilteringViewModel: ObservableObject { // MARK: - Publishers extension ImageFilteringViewModel { + + private var filteredCGImagePublisher: AnyPublisher { + ciFilterPublisher + .setFailureType(to: ImageFilteringService.Error.self) + .flatMap { filter in + ImageFilteringService.shared.apply(filter, to: self.inputImage) + } + .eraseToAnyPublisher() + } - private var filteredImagesStatePublisher: AnyPublisher { - store.$state - .map(\.filteredImages) + + private var filterIntensityPublisher: AnyPublisher { + $filterIntensity + .debounce(for: .milliseconds(200), scheduler: DispatchQueue.main) .eraseToAnyPublisher() } - private var filteredCGImagePublisher: AnyPublisher { - filteredImagesStatePublisher - .map(\.filteredOutputCGImage) - .compactMap { $0 } - .eraseToAnyPublisher() - } - - - private var filteredImagePublisher: AnyPublisher { - filteredCGImagePublisher - .map { cgImage in - Image(uiImage: UIImage(cgImage: cgImage)) + // TODO: Should this create a copy and return it? Should this be used straight-on + // instead of the other `currentFilter` property? + var ciFilterPublisher: AnyPublisher { +// Publishers.CombineLatest(filterIntensityPublisher, ) + filterIntensityPublisher + .map { + self.currentFilter.setValue($0, forKey: kCIInputIntensityKey) + + return self.currentFilter } .eraseToAnyPublisher() } @@ -84,31 +92,28 @@ extension ImageFilteringViewModel { private extension ImageFilteringViewModel { func setupSubscribers() { - filteredImagePublisher + + filteredCGImagePublisher .receive(on: DispatchQueue.main) - .assign(to: \.filteredImage, on: self) + .sink( + receiveCompletion: { completion in + switch completion { + case .failure(let error): + print("filteredImagePublisher error") + switch error { + case .cgImage(let message), + .ciImage(let message), + .filtering(let message): + self.filteringErrorMessage = message + } + case .finished: + print("filteredImagePublisher finished") + } + }, + receiveValue: { + self.store.send(.filteredImages(.setFilteredOutput(image: $0))) + } + ) .store(in: &subscriptions) - - -// filteredImagePublisher -// .receive(on: DispatchQueue.main) -// .sink( -// receiveCompletion: { completion in -// switch completion { -// case .failure(let error): -// print("filteredImagePublisher error") -// switch error { -// case .cgImage(let message), -// .ciImage(let message), -// .filtering(let message): -// self.filteringErrorMessage = message -// } -// case .finished: -// print("filteredImagePublisher finished") -// } -// }, -// receiveValue: { self.filteredImage = $0 } -// ) -// .store(in: &subscriptions) } }