Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyathewhite committed Mar 31, 2021
0 parents commit 6a90646
Show file tree
Hide file tree
Showing 73 changed files with 5,087 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
43 changes: 43 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"object": {
"pins": [
{
"package": "CombineEx",
"repositoryURL": "https://github.com/RocketLaunchpad/CombineEx.git",
"state": {
"branch": null,
"revision": "f77496af0df6563525ae3e432d4a3fa894a6b4e1",
"version": "1.0.0"
}
},
{
"package": "FoundationEx",
"repositoryURL": "https://github.com/RocketLaunchpad/FoundationEx.git",
"state": {
"branch": null,
"revision": "1e29902f7c26fff9828c3f488a314786fc7756ff",
"version": "1.0.0"
}
},
{
"package": "ReducerArchitecture",
"repositoryURL": "https://github.com/RocketLaunchpad/ReducerArchitecture.git",
"state": {
"branch": null,
"revision": "99d9d0908a2e1558b34232a88f88acd870838148",
"version": "1.1.1"
}
},
{
"package": "Tagged",
"repositoryURL": "https://github.com/pointfreeco/swift-tagged.git",
"state": {
"branch": null,
"revision": "592e1eb4255c571ebffc29955011e352217501b4",
"version": "0.5.0"
}
}
]
},
"version": 1
}
32 changes: 32 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "UIKitEx",
platforms: [
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13)
],
products: [
.library(
name: "UIKitEx",
targets: ["UIKitEx"]),
],
dependencies: [
.package(name: "FoundationEx", url: "https://github.com/RocketLaunchpad/FoundationEx.git", from: "1.0.0"),
.package(name: "CombineEx", url: "https://github.com/RocketLaunchpad/CombineEx.git", from: "1.0.0"),
.package(name: "ReducerArchitecture", url: "https://github.com/RocketLaunchpad/ReducerArchitecture.git", from: "1.0.0"),
// .package(name: "Functional", url: "https://github.com/RocketLaunchpad/Functional.git", from: "1.0.0"),
],
targets: [
.target(
name: "UIKitEx",
dependencies: ["FoundationEx", "CombineEx", "ReducerArchitecture"]
),
.testTarget(
name: "UIKitExTests",
dependencies: ["UIKitEx"]
)
]
)
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# UIKitEx

A description of this package.
117 changes: 117 additions & 0 deletions Sources/UIKitEx/AppFlow/AnyFlow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//
// AnyFlow.swift
// Rocket Insights
//
// Created by Ilya Belenkiy on 2/8/21.
// Copyright © 2021 Rocket Insights. All rights reserved.
//

#if canImport(UIKit)

import UIKit
import Combine
import CombineEx

public struct AnyFlow {
public enum Mode {
case presentation
case inPlace
case embedded

public var isPresentation: Bool {
switch self {
case .presentation:
return true
default:
return false
}
}

public var canAddPromptAtEnd: Bool {
switch self {
case .presentation, .inPlace:
return true
case .embedded:
return false
}
}
}

public let nc: UINavigationController
public var start: () -> AppFlow.CancellableFinshedActionPublisher
public var finish: () -> AppFlow.CancellableFinshedActionPublisher
public var restart: () -> AppFlow.CancellableFinshedActionPublisher

public init(
on _vc: UINavigationController,
mode: Mode,
presentationContainerType: UINavigationController.Type,
delayFinish: Bool = false,
animateStart: Bool = true
) {
switch mode {
case .presentation:
weak var vc = _vc
let nc = presentationContainerType.init()
self.nc = nc

start = { [weak vc] in
guard let vc = vc else { return AppFlow.cancel() }
vc.present(nc, animated: animateStart)
return AppFlow.cancellableFinishedAction
}

finish = { [weak vc] in
guard let vc = vc else { return AppFlow.cancel() }
vc.dismiss(animated: true)
let res = delayFinish ? AppFlow.standardPresentationDelay : AppFlow.finishedAction
return res.addUserCanCancel().eraseType()
}

restart = {
AppFlow.popToRoot(of: nc).addUserCanCancel().eraseType()
}

case .inPlace, .embedded:
assert(_vc.topViewController != nil)
weak var nc = _vc
weak var startVC = AppFlow.getStartVC(on: _vc)
self.nc = _vc

start = {
AppFlow.cancellableNoAction
}

if mode == .inPlace {
finish = { [weak startVC] in
guard let startVC = startVC else {
assertionFailure()
return AppFlow.cancellableNoAction
}
let delayTime = delayFinish ? Styling.standardNavigationDelay : 0
return AppFlow.pop(to: startVC, delayFor: delayTime).addUserCanCancel().eraseType()
}
}
else {
finish = {
return AppFlow.cancellableNoAction
}
}

restart = { [weak startVC, weak nc] in
guard let nc = nc else { return AppFlow.cancel() }
guard let startVC = startVC else {
assertionFailure()
return AppFlow.cancel()
}
guard let flowFirstVC = AppFlow.vcAfter(startVC, on: nc) else {
assertionFailure()
return AppFlow.cancel()
}
return AppFlow.pop(to: flowFirstVC).addUserCanCancel().eraseType()
}
}
}
}

#endif
68 changes: 68 additions & 0 deletions Sources/UIKitEx/AppFlow/AppFlow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// AppFlow.swift
// Rocket Insights
//
// Created by Ilya Belenkiy on 7/19/20.
// Copyright © 2021 Rocket Insights. All rights reserved.
//

#if canImport(UIKit)

import Combine
import CombineEx
import UIKit

public protocol SingleValueFlow {
associatedtype Value
associatedtype AsyncValue: SingleValuePublisher where AsyncValue.Output == Value
func run() -> AsyncValue
}

public protocol ReplaceableInAppFlow: UIViewController {}

public protocol AppFlowViewController: UIViewController {
func configureBackButton(_ nc: UINavigationController)
func cancel()
}

public typealias AsyncValueUIasFlowVC = AsyncValueUI & AppFlowViewController

public enum AppFlow {
public typealias FinshedActionPublisher = AnySingleValuePublisher<Void, Never>
public typealias CancellableFinshedActionPublisher = AnySingleValuePublisher<Void, Cancel>
public static let finishedAction: FinshedActionPublisher = Just(()).eraseType()
public static let cancellableFinishedAction: CancellableFinshedActionPublisher = Just(()).addUserCanCancel().eraseType()
public static let noAction = Just(()).eraseType()
public static let cancellableNoAction = Just(()).addUserCanCancel().eraseType()

public static func start() -> Just<Void> {
Just(())
}

public static func never<T, E: Error>() -> AnySingleValuePublisher<T, E> {
Combine.Empty(completeImmediately: false).eraseType()
}

public static func cancel<T>() -> AnySingleValuePublisher<T, Cancel> {
Fail(error: .cancel).eraseType()
}

public static func cancel<T>() -> AnyPublisher<T, Cancel> {
Fail(error: .cancel).eraseToAnyPublisher()
}
}

public extension Publisher where Failure == Never {
func addUserCanCancel() -> Publishers.MapError<Self, Cancel> {
addErrorType(Cancel.self)
}
}

public extension Publisher where Output == Void, Failure == Cancel {
func catchCancelAsVoid() -> Publishers.ReplaceError<Self> {
replaceError(with: ())
}
}

#endif

89 changes: 89 additions & 0 deletions Sources/UIKitEx/AppFlow/AppFlowNavigation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// AppFlowNavigation.swift
// Rocket Insights
//
// Created by Ilya Belenkiy on 2/27/21.
// Copyright © 2021 Rocket Insights. All rights reserved.
//

#if canImport(UIKit)

import Combine
import CombineEx
import UIKit

public extension AppFlow {
static func delay(for interval: DispatchQueue.SchedulerTimeType.Stride) -> AnySingleValuePublisher<Void, Never> {
Just(()).delay(for: interval, scheduler: DispatchQueue.main).eraseType()
}

static let standardPresentationDelay = delay(for: .seconds(Styling.standardPresentationDelay))

static func getStartVC(on nc: UINavigationController) -> UIViewController {
if let startVC = nc.viewControllers.reversed().first(where: { vc in
(vc is AppFlowViewController) && !(vc is ReplaceableInAppFlow)
})
.flatMap({ $0 as? AppFlowViewController }) {
return startVC
}
else if let vc = nc.topViewController {
return vc
}
else {
fatalError()
}
}

static func vcAfter(_ vc: UIViewController, on nc: UINavigationController) -> UIViewController? {
zip(nc.viewControllers, nc.viewControllers.dropFirst()).first { $0.0 == vc }?.1
}

static func vcBefore(_ vc: UIViewController, on nc: UINavigationController) -> UIViewController? {
zip(nc.viewControllers, nc.viewControllers.dropFirst()).first { $0.1 == vc }?.0
}

static func show<UI: AsyncValueUIasFlowVC>(_ vc: UI, on nc: UINavigationController) -> AnyPublisher<UI.Value, Cancel> {
showVC(vc, on: nc)
vc.configureBackButton(nc)
return vc.value
}

static func showVC(_ vc: UIViewController, on nc: UINavigationController) {
if let topVC = nc.topViewController as? ReplaceableInAppFlow {
topVC.replace(by: vc)
}
else {
let animated = !nc.viewControllers.isEmpty
nc.pushViewController(vc, animated: animated)
}
}

static func pop(to vc: UIViewController, delayFor time: TimeInterval = 0) -> FinshedActionPublisher {
guard let nc = vc.navigationController else {
assertionFailure()
return finishedAction
}

nc.viewControllers
.reversed()
.prefix(while: {$0 != vc})
.forEach { ($0 as? BasicViewController)?.endValue = .fromCode }

nc.popToViewController(vc, animated: true)
if time > 0 {
return finishedAction
.delay(for: .seconds(time), scheduler: DispatchQueue.main)
.eraseType()
}
else {
return finishedAction
}
}

static func popToRoot(of nc: UINavigationController, delayFor time: TimeInterval = 0) -> FinshedActionPublisher {
guard let firstVC = nc.viewControllers.first else { return finishedAction }
return pop(to: firstVC, delayFor: time)
}
}

#endif
Loading

0 comments on commit 6a90646

Please sign in to comment.