App changes for Day 65
This commit is contained in:
parent
4fbebdd38d
commit
0732494928
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,14 +27,18 @@ extension ImageFilteringContainerView {
|
|||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 42.0) {
|
||||
Spacer()
|
||||
Group {
|
||||
if currentInputImage != nil {
|
||||
ImageFilteringView(inputImage: currentInputImage!, store: store)
|
||||
} else {
|
||||
Spacer()
|
||||
|
||||
imageContent
|
||||
.layoutPriority(1)
|
||||
imageSelectionSection
|
||||
.padding()
|
||||
|
||||
imagePickerButton
|
||||
|
||||
Spacer()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +57,13 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
||||
|
@ -44,25 +45,32 @@ final class ImageFilteringViewModel: ObservableObject {
|
|||
// MARK: - Publishers
|
||||
extension ImageFilteringViewModel {
|
||||
|
||||
private var filteredImagesStatePublisher: AnyPublisher<FilteredImagesState, Never> {
|
||||
store.$state
|
||||
.map(\.filteredImages)
|
||||
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 filteredCGImagePublisher: AnyPublisher<CGImage, Never> {
|
||||
filteredImagesStatePublisher
|
||||
.map(\.filteredOutputCGImage)
|
||||
.compactMap { $0 }
|
||||
private var filterIntensityPublisher: AnyPublisher<CGFloat, Never> {
|
||||
$filterIntensity
|
||||
.debounce(for: .milliseconds(200), scheduler: DispatchQueue.main)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue