App changes for Day 65
This commit is contained in:
parent
4fbebdd38d
commit
0732494928
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue