Setup initial structure for UI and filtering functionality
|
@ -3,18 +3,27 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objectVersion = 52;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
F329952E23908BD400D2D963 /* ImageFilteringView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F329952D23908BD400D2D963 /* ImageFilteringView.swift */; };
|
||||
F329953023908CB500D2D963 /* ImageFilteringViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F329952F23908CB500D2D963 /* ImageFilteringViewModel.swift */; };
|
||||
F32995322390C29100D2D963 /* ImageFilteringContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32995312390C29100D2D963 /* ImageFilteringContainerView.swift */; };
|
||||
F352424E238F3F18009DF1F9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F352424D238F3F18009DF1F9 /* AppDelegate.swift */; };
|
||||
F3524250238F3F18009DF1F9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F352424F238F3F18009DF1F9 /* SceneDelegate.swift */; };
|
||||
F3524254238F3F19009DF1F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3524253238F3F19009DF1F9 /* Assets.xcassets */; };
|
||||
F3524257238F3F19009DF1F9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3524256238F3F19009DF1F9 /* Preview Assets.xcassets */; };
|
||||
F352425A238F3F19009DF1F9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F3524258238F3F19009DF1F9 /* LaunchScreen.storyboard */; };
|
||||
F3F566AF238F6556009E1FB0 /* ImageFilteringService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F566AE238F6556009E1FB0 /* ImageFilteringService.swift */; };
|
||||
F3F566B2238F68E2009E1FB0 /* CypherPoetSwiftUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = F3F566B1238F68E2009E1FB0 /* CypherPoetSwiftUIKit */; };
|
||||
F3F566B6238F6A48009E1FB0 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F566B5238F6A48009E1FB0 /* AppState.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
F329952D23908BD400D2D963 /* ImageFilteringView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilteringView.swift; sourceTree = "<group>"; };
|
||||
F329952F23908CB500D2D963 /* ImageFilteringViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilteringViewModel.swift; sourceTree = "<group>"; };
|
||||
F32995312390C29100D2D963 /* ImageFilteringContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilteringContainerView.swift; sourceTree = "<group>"; };
|
||||
F352424A238F3F18009DF1F9 /* Instafilter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Instafilter.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F352424D238F3F18009DF1F9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
F352424F238F3F18009DF1F9 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
|
@ -22,6 +31,8 @@
|
|||
F3524256238F3F19009DF1F9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
F3524259238F3F19009DF1F9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
F352425B238F3F19009DF1F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
F3F566AE238F6556009E1FB0 /* ImageFilteringService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilteringService.swift; sourceTree = "<group>"; };
|
||||
F3F566B5238F6A48009E1FB0 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -29,6 +40,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F3F566B2238F68E2009E1FB0 /* CypherPoetSwiftUIKit in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -54,6 +66,7 @@
|
|||
F352424C238F3F18009DF1F9 /* Instafilter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F3F566B3238F6A39009E1FB0 /* Data */,
|
||||
F352425B238F3F19009DF1F9 /* Info.plist */,
|
||||
F352424D238F3F18009DF1F9 /* AppDelegate.swift */,
|
||||
F352424F238F3F18009DF1F9 /* SceneDelegate.swift */,
|
||||
|
@ -77,6 +90,9 @@
|
|||
F3524261238F45C9009DF1F9 /* Scenes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F329952D23908BD400D2D963 /* ImageFilteringView.swift */,
|
||||
F32995312390C29100D2D963 /* ImageFilteringContainerView.swift */,
|
||||
F329952F23908CB500D2D963 /* ImageFilteringViewModel.swift */,
|
||||
);
|
||||
path = Scenes;
|
||||
sourceTree = "<group>";
|
||||
|
@ -84,6 +100,7 @@
|
|||
F3524262238F45CD009DF1F9 /* Reusables */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F3F566AE238F6556009E1FB0 /* ImageFilteringService.swift */,
|
||||
);
|
||||
path = Reusables;
|
||||
sourceTree = "<group>";
|
||||
|
@ -96,6 +113,22 @@
|
|||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F3F566B3238F6A39009E1FB0 /* Data */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F3F566B4238F6A3E009E1FB0 /* State */,
|
||||
);
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F3F566B4238F6A3E009E1FB0 /* State */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F3F566B5238F6A48009E1FB0 /* AppState.swift */,
|
||||
);
|
||||
path = State;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
@ -112,6 +145,9 @@
|
|||
dependencies = (
|
||||
);
|
||||
name = Instafilter;
|
||||
packageProductDependencies = (
|
||||
F3F566B1238F68E2009E1FB0 /* CypherPoetSwiftUIKit */,
|
||||
);
|
||||
productName = Instafilter;
|
||||
productReference = F352424A238F3F18009DF1F9 /* Instafilter.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
|
@ -140,6 +176,9 @@
|
|||
Base,
|
||||
);
|
||||
mainGroup = F3524241238F3F18009DF1F9;
|
||||
packageReferences = (
|
||||
F3F566B0238F68E2009E1FB0 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */,
|
||||
);
|
||||
productRefGroup = F352424B238F3F18009DF1F9 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
|
@ -167,8 +206,13 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F3F566AF238F6556009E1FB0 /* ImageFilteringService.swift in Sources */,
|
||||
F329952E23908BD400D2D963 /* ImageFilteringView.swift in Sources */,
|
||||
F32995322390C29100D2D963 /* ImageFilteringContainerView.swift in Sources */,
|
||||
F352424E238F3F18009DF1F9 /* AppDelegate.swift in Sources */,
|
||||
F329953023908CB500D2D963 /* ImageFilteringViewModel.swift in Sources */,
|
||||
F3524250238F3F18009DF1F9 /* SceneDelegate.swift in Sources */,
|
||||
F3F566B6238F6A48009E1FB0 /* AppState.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -362,6 +406,25 @@
|
|||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
F3F566B0238F68E2009E1FB0 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.0.23;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
F3F566B1238F68E2009E1FB0 /* CypherPoetSwiftUIKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = F3F566B0238F68E2009E1FB0 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */;
|
||||
productName = CypherPoetSwiftUIKit;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = F3524242238F3F18009DF1F9 /* Project object */;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// AppState.swift
|
||||
// Instafilter
|
||||
//
|
||||
// Created by CypherPoet on 11/27/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CypherPoetSwiftUIKit_DataFlowUtils
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "earth-night@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "earth-night@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "earth-night@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 168 KiB |
After Width: | Height: | Size: 886 KiB |
After Width: | Height: | Size: 2.8 MiB |
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ocean-1@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ocean-1@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ocean-1@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
day-062/Projects/Instafilter/Instafilter/Resources/Assets.xcassets/ocean-1.imageset/ocean-1@1x.png
vendored
Normal file
After Width: | Height: | Size: 294 KiB |
BIN
day-062/Projects/Instafilter/Instafilter/Resources/Assets.xcassets/ocean-1.imageset/ocean-1@2x.png
vendored
Normal file
After Width: | Height: | Size: 2.6 MiB |
BIN
day-062/Projects/Instafilter/Instafilter/Resources/Assets.xcassets/ocean-1.imageset/ocean-1@3x.png
vendored
Normal file
After Width: | Height: | Size: 6.8 MiB |
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ocean-2@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ocean-2@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ocean-2@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
day-062/Projects/Instafilter/Instafilter/Resources/Assets.xcassets/ocean-2.imageset/ocean-2@1x.png
vendored
Normal file
After Width: | Height: | Size: 976 KiB |
BIN
day-062/Projects/Instafilter/Instafilter/Resources/Assets.xcassets/ocean-2.imageset/ocean-2@2x.png
vendored
Normal file
After Width: | Height: | Size: 3.6 MiB |
BIN
day-062/Projects/Instafilter/Instafilter/Resources/Assets.xcassets/ocean-2.imageset/ocean-2@3x.png
vendored
Normal file
After Width: | Height: | Size: 7.8 MiB |
|
@ -0,0 +1,116 @@
|
|||
//
|
||||
// ImageFilteringService.swift
|
||||
// Instafilter
|
||||
//
|
||||
// Created by CypherPoet on 11/27/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreImage
|
||||
import CoreImage.CIFilterBuiltins
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
|
||||
final class ImageFilteringService {
|
||||
public typealias FilterAtrributes = [String: Any]
|
||||
|
||||
enum Error: Swift.Error {
|
||||
case cgImage(_ message: String)
|
||||
case ciImage(_ message: String)
|
||||
case filtering(_ message: String)
|
||||
}
|
||||
|
||||
|
||||
let filteringQueue: DispatchQueue
|
||||
|
||||
// 🔑 Creating a CIContext is expensive, so we'll create it once and reuse it throughout the app.
|
||||
lazy var context = CIContext()
|
||||
|
||||
|
||||
init(
|
||||
queue filteringQueue: DispatchQueue = DispatchQueue(
|
||||
label: "Image Filtering Service",
|
||||
qos: .userInitiated
|
||||
)
|
||||
) {
|
||||
self.filteringQueue = filteringQueue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension ImageFilteringService {
|
||||
|
||||
func createCGImage(from filteredImage: CIImage) -> AnyPublisher<CGImage, ImageFilteringService.Error> {
|
||||
Just(filteredImage)
|
||||
.print("createCGImage")
|
||||
.tryMap { filteredImage -> CGImage in
|
||||
guard
|
||||
let cgImage = self.context.createCGImage(filteredImage, from: filteredImage.extent)
|
||||
else {
|
||||
print("createCGImage failed")
|
||||
throw Error.cgImage("Failed to create cgImage from filtered ciImage")
|
||||
}
|
||||
|
||||
return cgImage
|
||||
}
|
||||
.mapError( { $0 as! ImageFilteringService.Error })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
func createCIImage(
|
||||
from uiImage: UIImage,
|
||||
byApplying filter: CIFilter,
|
||||
withAttributes filterAtrributes: FilterAtrributes = [:]
|
||||
) -> AnyPublisher<CIImage, ImageFilteringService.Error> {
|
||||
Just(uiImage)
|
||||
.print("createCIImage")
|
||||
.tryMap { uiImage -> CIImage in
|
||||
guard let startingImage = CIImage(image: uiImage) else {
|
||||
throw Error.ciImage("Failed to create ciImage from uiImage before filtering")
|
||||
}
|
||||
|
||||
return startingImage
|
||||
}
|
||||
.tryMap { startingImage -> CIImage in
|
||||
filter.setValue(startingImage, forKey: kCIInputImageKey)
|
||||
|
||||
for (key, value) in filterAtrributes {
|
||||
filter.setValue(value, forKey: key)
|
||||
}
|
||||
|
||||
guard let filteredImage = filter.outputImage else {
|
||||
throw Error.filtering("Failed to output image during filtering")
|
||||
}
|
||||
|
||||
return filteredImage
|
||||
}
|
||||
.mapError( { $0 as! ImageFilteringService.Error })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
|
||||
func apply(
|
||||
_ filter: CIFilter,
|
||||
to uiImage: UIImage,
|
||||
withAttributes filterAtrributes: FilterAtrributes = [:]
|
||||
) -> AnyPublisher<CGImage, ImageFilteringService.Error> {
|
||||
createCIImage(from: uiImage, byApplying: filter, withAttributes: filterAtrributes)
|
||||
.subscribe(on: filteringQueue)
|
||||
.flatMap { ciImage in
|
||||
self.createCGImage(from: ciImage)
|
||||
}
|
||||
// .map(createCGImage(from:))
|
||||
// .switchToLatest()
|
||||
// .map({ $0 })
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension ImageFilteringService {
|
||||
public static let shared = ImageFilteringService()
|
||||
}
|
|
@ -25,7 +25,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
let window = UIWindow(windowScene: windowScene)
|
||||
|
||||
// Create the SwiftUI view that provides the window contents.
|
||||
let entryView = EmptyView()
|
||||
let entryView = ImageFilteringContainerView()
|
||||
.accentColor(.pink)
|
||||
|
||||
window.rootViewController = UIHostingController(rootView: entryView)
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
//
|
||||
// ImageFilteringContainerView.swift
|
||||
// Instafilter
|
||||
//
|
||||
// Created by CypherPoet on 11/28/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct ImageFilteringContainerView: View {
|
||||
@State private var currentInputImage: UIImage? = nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Body
|
||||
extension ImageFilteringContainerView {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 42.0) {
|
||||
Spacer()
|
||||
|
||||
imageContent
|
||||
.layoutPriority(1)
|
||||
|
||||
imagePickerButton
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Computeds
|
||||
extension ImageFilteringContainerView {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - View Variables
|
||||
extension ImageFilteringContainerView {
|
||||
|
||||
private var imageContent: some View {
|
||||
Group {
|
||||
if currentInputImage != nil {
|
||||
ImageFilteringView(inputImage: currentInputImage!)
|
||||
} else {
|
||||
Text("Select an Image to begin filtering.")
|
||||
.font(.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var imagePickerButton: some View {
|
||||
Button(action: {
|
||||
self.selectImage()
|
||||
}) {
|
||||
Image(systemName: "camera.fill")
|
||||
.renderingMode(.original)
|
||||
.colorInvert()
|
||||
.scaleEffect(2)
|
||||
}
|
||||
.frame(minWidth: 80, minHeight: 40)
|
||||
.padding()
|
||||
.background(Color.accentColor)
|
||||
.cornerRadius(12)
|
||||
.shadow(color: .gray, radius: 8, x: 0, y: 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private Helpers
|
||||
private extension ImageFilteringContainerView {
|
||||
|
||||
func selectImage() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Preview
|
||||
struct ImageFilteringContainerView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
ImageFilteringContainerView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// ImageFilteringView.swift
|
||||
// Instafilter
|
||||
//
|
||||
// Created by CypherPoet on 11/28/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct ImageFilteringView: View {
|
||||
@ObservedObject private(set) var viewModel: ImageFilteringViewModel
|
||||
|
||||
// @ObservedObject private(set) var viewModel = ImageFilteringViewModel()
|
||||
|
||||
init(inputImage: UIImage) {
|
||||
self.viewModel = ImageFilteringViewModel(inputImage: inputImage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Body
|
||||
extension ImageFilteringView {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if viewModel.filteringErrorMessage != nil {
|
||||
Text(viewModel.filteringErrorMessage!)
|
||||
}
|
||||
|
||||
viewModel.filteredImage?
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
// .onAppear {
|
||||
// self.viewModel.inputImage = UIImage(named: "earth-night")
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Computeds
|
||||
extension ImageFilteringView {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - View Variables
|
||||
extension ImageFilteringView {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Preview
|
||||
struct ImageFilteringView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
ImageFilteringView(
|
||||
inputImage: UIImage(named: "earth-night")!
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
//
|
||||
// ImageFilteringViewModel.swift
|
||||
// Instafilter
|
||||
//
|
||||
// Created by CypherPoet on 11/28/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
|
||||
final class ImageFilteringViewModel: ObservableObject {
|
||||
private var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
private var filteringService = ImageFilteringService.shared
|
||||
|
||||
@Published var inputImage: UIImage
|
||||
@Published var currentFilter: CIFilter
|
||||
|
||||
|
||||
// MARK: - Published Properties
|
||||
@Published var filteredImage: Image?
|
||||
@Published var filteringErrorMessage: String?
|
||||
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
init(inputImage: UIImage) {
|
||||
self.inputImage = inputImage
|
||||
self.currentFilter = .sepiaTone()
|
||||
|
||||
setupSubscribers()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Publishers
|
||||
extension ImageFilteringViewModel {
|
||||
|
||||
private var filteredImagePublisher: AnyPublisher<Image, ImageFilteringService.Error> {
|
||||
$inputImage
|
||||
.print("filteredImagePublisher")
|
||||
.compactMap { $0 }
|
||||
.setFailureType(to: ImageFilteringService.Error.self)
|
||||
.map { inputImage in
|
||||
self.filteringService.apply(self.currentFilter, to: inputImage)
|
||||
}
|
||||
.switchToLatest()
|
||||
.map { cgImage in
|
||||
print("filteredImagePublisher mapping cgImage")
|
||||
return Image(uiImage: UIImage(cgImage: cgImage))
|
||||
}
|
||||
// .catch { error in
|
||||
// switch error {
|
||||
// case .cgImage(let message),
|
||||
// .ciImage(let message),
|
||||
// .filtering(let message):
|
||||
// self.filteringErrorMessage = message
|
||||
// }
|
||||
//
|
||||
// return Just(nil).eraseToAnyPublisher()
|
||||
// }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Computeds
|
||||
extension ImageFilteringViewModel {
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Public Methods
|
||||
extension ImageFilteringViewModel {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Private Helpers
|
||||
private extension ImageFilteringViewModel {
|
||||
|
||||
func setupSubscribers() {
|
||||
filteredImagePublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
// .handleEvents(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")
|
||||
// }
|
||||
// })
|
||||
.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)
|
||||
|
||||
|
||||
|
||||
// filteredImagePublisher
|
||||
// .replaceError(with: nil)
|
||||
// .compactMap( { $0 })
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .assign(to: \.filteredImage, on: self)
|
||||
// .store(in: &subscriptions)
|
||||
}
|
||||
}
|