Setup initial structure for UI and filtering functionality

This commit is contained in:
CypherPoet 2019-11-29 10:39:23 -06:00
parent bce4cf1c98
commit 7106079684
19 changed files with 550 additions and 2 deletions

View File

@ -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 */;
}

View File

@ -0,0 +1,13 @@
//
// AppState.swift
// Instafilter
//
// Created by CypherPoet on 11/27/19.
//
//
import Foundation
import CypherPoetSwiftUIKit_DataFlowUtils

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

View File

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

View File

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

View File

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

View File

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

View File

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