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 {
var currentInputImage: UIImage?
var filteredOutputCGImage: CGImage?
var filteringErrorMessage: String?
}
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 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<FilteredImagesState, FilteredImagesAction> { 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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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<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
.map(\.filteredImages)
private var filterIntensityPublisher: AnyPublisher<CGFloat, Never> {
$filterIntensity
.debounce(for: .milliseconds(200), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
private var filteredCGImagePublisher: AnyPublisher<CGImage, Never> {
filteredImagesStatePublisher
.map(\.filteredOutputCGImage)
.compactMap { $0 }
.eraseToAnyPublisher()
}
private var filteredImagePublisher: AnyPublisher<Image?, Never> {
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<CIFilter, Never> {
// 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)
}
}