App changes for Day 65

This commit is contained in:
CypherPoet 2019-12-02 15:35:52 -06:00
parent 4fbebdd38d
commit 0732494928
4 changed files with 111 additions and 90 deletions

View File

@ -13,37 +13,15 @@ import CypherPoetSwiftUIKit_DataFlowUtils
struct FilteredImagesState { struct FilteredImagesState {
var currentInputImage: UIImage?
var filteredOutputCGImage: CGImage? var filteredOutputCGImage: CGImage?
var filteringErrorMessage: String?
} }
enum FilteredImagesSideEffect: SideEffect { //enum FilteredImagesSideEffect: SideEffect {}
case apply(filter: CIFilter, to: UIImage)
func mapToAction() -> AnyPublisher<AppAction, Never> {
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 FilteredImagesAction { enum FilteredImagesAction {
case setCurrentInput(image: UIImage)
case setFilteredOutput(image: CGImage) case setFilteredOutput(image: CGImage)
case setFilteringError(message: String)
} }
@ -51,11 +29,7 @@ enum FilteredImagesAction {
// MARK: - Reducer // MARK: - Reducer
let filteredImagesReducer = Reducer<FilteredImagesState, FilteredImagesAction> { state, action in let filteredImagesReducer = Reducer<FilteredImagesState, FilteredImagesAction> { state, action in
switch action { switch action {
case .setCurrentInput(let image):
state.currentInputImage = image
case .setFilteredOutput(let image): case .setFilteredOutput(let image):
state.filteredOutputCGImage = image state.filteredOutputCGImage = image
case .setFilteringError(let message):
state.filteringErrorMessage = message
} }
} }

View File

@ -27,14 +27,18 @@ extension ImageFilteringContainerView {
var body: some View { var body: some View {
NavigationView { NavigationView {
VStack(spacing: 42.0) { VStack(spacing: 42.0) {
Spacer() Group {
if currentInputImage != nil {
imageContent ImageFilteringView(inputImage: currentInputImage!, store: store)
.layoutPriority(1) } else {
Spacer()
imagePickerButton
imageSelectionSection
Spacer() .padding()
Spacer()
}
}
} }
.navigationBarTitle("📸 Instafilter") .navigationBarTitle("📸 Instafilter")
.navigationBarItems(trailing: saveButton) .navigationBarItems(trailing: saveButton)
@ -60,14 +64,13 @@ extension ImageFilteringContainerView {
// MARK: - View Variables // MARK: - View Variables
extension ImageFilteringContainerView { extension ImageFilteringContainerView {
private var imageContent: some View { private var imageSelectionSection: some View {
Group { VStack(spacing: 36) {
if currentInputImage != nil { Text("Select an Image to begin filtering.")
ImageFilteringView(inputImage: currentInputImage!, store: store) .font(.title)
} else { .multilineTextAlignment(.center)
Text("Select an Image to begin filtering.")
.font(.title) imagePickerButton
}
} }
} }

View File

@ -10,12 +10,18 @@ import SwiftUI
struct ImageFilteringView: View { struct ImageFilteringView: View {
@EnvironmentObject private var store: AppStore
@ObservedObject private(set) var viewModel: ImageFilteringViewModel @ObservedObject private(set) var viewModel: ImageFilteringViewModel
private let inputImage: UIImage
init( init(
inputImage: UIImage, inputImage: UIImage,
store: AppStore store: AppStore
) { ) {
self.inputImage = inputImage
self.viewModel = ImageFilteringViewModel( self.viewModel = ImageFilteringViewModel(
inputImage: inputImage, inputImage: inputImage,
store: store store: store
@ -34,9 +40,15 @@ extension ImageFilteringView {
Text(viewModel.filteringErrorMessage!) Text(viewModel.filteringErrorMessage!)
} }
(viewModel.filteredImage ?? Image(uiImage: viewModel.inputImage)) (outputImage ?? Image(uiImage: inputImage))
.resizable() .resizable()
.scaledToFit() .scaledToFit()
controls
.padding()
.padding(.top)
Spacer()
} }
} }
} }
@ -44,7 +56,14 @@ extension ImageFilteringView {
// MARK: - Computeds // MARK: - Computeds
extension ImageFilteringView { 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 // MARK: - View Variables
extension ImageFilteringView { 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 { struct ImageFilteringView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ImageFilteringView( NavigationView {
inputImage: UIImage(named: "earth-night")!, ImageFilteringView(
store: SampleStore.default inputImage: UIImage(named: "earth-night")!,
) store: SampleStore.default
)
.environmentObject(SampleStore.default)
}
} }
} }

View File

@ -19,10 +19,11 @@ final class ImageFilteringViewModel: ObservableObject {
@Published var inputImage: UIImage @Published var inputImage: UIImage
@Published var currentFilter: CIFilter @Published var currentFilter: CIFilter
@Published var filterIntensity: CGFloat = 0.0
// MARK: - Published Properties // MARK: - Published Properties
@Published var filteredImage: Image? // @Published var filteredImage: Image?
@Published var filteringErrorMessage: String? @Published var filteringErrorMessage: String?
@ -43,26 +44,33 @@ final class ImageFilteringViewModel: ObservableObject {
// MARK: - Publishers // MARK: - Publishers
extension ImageFilteringViewModel { extension ImageFilteringViewModel {
private var filteredCGImagePublisher: AnyPublisher<CGImage, ImageFilteringService.Error> {
ciFilterPublisher
.setFailureType(to: ImageFilteringService.Error.self)
.flatMap { filter in
ImageFilteringService.shared.apply(filter, to: self.inputImage)
}
.eraseToAnyPublisher()
}
private var filteredImagesStatePublisher: AnyPublisher<FilteredImagesState, Never> {
store.$state private var filterIntensityPublisher: AnyPublisher<CGFloat, Never> {
.map(\.filteredImages) $filterIntensity
.debounce(for: .milliseconds(200), scheduler: DispatchQueue.main)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
private var filteredCGImagePublisher: AnyPublisher<CGImage, Never> { // TODO: Should this create a copy and return it? Should this be used straight-on
filteredImagesStatePublisher // instead of the other `currentFilter` property?
.map(\.filteredOutputCGImage) var ciFilterPublisher: AnyPublisher<CIFilter, Never> {
.compactMap { $0 } // Publishers.CombineLatest(filterIntensityPublisher, )
.eraseToAnyPublisher() filterIntensityPublisher
} .map {
self.currentFilter.setValue($0, forKey: kCIInputIntensityKey)
private var filteredImagePublisher: AnyPublisher<Image?, Never> { return self.currentFilter
filteredCGImagePublisher
.map { cgImage in
Image(uiImage: UIImage(cgImage: cgImage))
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -84,31 +92,28 @@ extension ImageFilteringViewModel {
private extension ImageFilteringViewModel { private extension ImageFilteringViewModel {
func setupSubscribers() { func setupSubscribers() {
filteredImagePublisher
filteredCGImagePublisher
.receive(on: DispatchQueue.main) .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) .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)
} }
} }