Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes removedAfterProceeding not dismissing final view when applied to final item in Workflow. #33

Merged
merged 29 commits into from Jun 9, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
818749b
[removedAfterProceeding] - Adding complete to OrchestrationResponder.…
Richard-Gist Jun 2, 2021
502c8c3
[removedAfterProceeding] - Updated PersistenceTests in WorkflowTests …
Richard-Gist Jun 2, 2021
bcaee74
[removedAfterProceeding] - updated SkipThroughWorkflowTests - MOB
Richard-Gist Jun 2, 2021
ff7b0d5
[removedAfterProceeding] - Updated Workflow to call complete when pro…
Richard-Gist Jun 2, 2021
aadfd9a
[removedAfterProceeding] - Adding warning so we dont forget to do the…
Richard-Gist Jun 2, 2021
da45b6d
[removedAfterProceeding] - back to green but should be failing lintin…
Richard-Gist Jun 2, 2021
83c9618
[removedAfterProceeding] - minor linting and sonarcloud smells fixing…
Richard-Gist Jun 2, 2021
0c170b2
[removedAfterProceeding] - Adding additional tests to cover more scen…
Richard-Gist Jun 2, 2021
b2e82a5
[removedAfterProceeding] - call to complete was a round-a-bout way of…
Richard-Gist Jun 2, 2021
829878d
[removedAfterProceeding] - Adds support for destroying all items when…
morganzellers Jun 4, 2021
7bb55bd
[removedAfterProceeding] Adds implementation for linked list last whe…
morganzellers Jun 4, 2021
3243278
[removedAfterProceeding] - Adds tests to LinkedList.last(where:) and …
Richard-Gist Jun 4, 2021
68fde9a
[removedAfterProceeding] Replaces filter and last with a last(where:)…
Jun 4, 2021
08880e8
[removedAfterProceeding] Immediately completes if there's no first lo…
Jun 4, 2021
3184d37
[removedAfterProceeding] - Making Tyler and the styleguide happy... S…
Richard-Gist Jun 4, 2021
b1c6206
[removedAfterProceeding] - I should have done this in the last commit…
Richard-Gist Jun 4, 2021
7405cf7
[removedAfterProceeding] - Adds completion call to destroy - MOB
Jun 7, 2021
383294b
[removedAfterProceeding] Calls completion even when workflow item pre…
Jun 7, 2021
a9fda4e
Merge branch 'main' into removedAfterProceeding
Jun 8, 2021
77d5717
[removedAfterProceeding] - Cleans up the self created navigation cont…
Jun 8, 2021
a11c6eb
[removedAfterProceeding] - Renames test to be more accurate - MOB
Jun 8, 2021
c89766b
[removedAfterProceeding] - Adds _ to last(where:) - MOB
Jun 8, 2021
41e83a5
Update Tests/WorkflowTests/LinkedListTests.swift
wiemerm Jun 8, 2021
fc34132
[removedAfterProceeding] - Adds implementation for complete in Workfl…
Jun 8, 2021
9af0375
[removedAfterProceeding] - Adding test coverage for if all FRs skip w…
Richard-Gist Jun 8, 2021
7060832
[removedAfterProceeding] - Ramped minor version
Richard-Gist Jun 8, 2021
e8824ea
[removedAfterProceeding] - Removes unnecessary code for view controll…
Richard-Gist Jun 9, 2021
a90d7ef
[removedAfterProceeding] - Adding documentation to complete on UIKite…
Richard-Gist Jun 9, 2021
8e9e190
[removedAfterProceeding] - Generated docs and made minor improvements…
Richard-Gist Jun 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 32 additions & 0 deletions Sources/Workflow/LinkedList/LinkedList.swift
Expand Up @@ -66,4 +66,36 @@ public class LinkedList<Value>: Sequence, CustomStringConvertible {
func toArray() -> [Element.Value] {
map { $0.value }
}

/**
Returns the last element of the sequence that satisfies the given
predicate.

- Parameter predicate: A closure that takes an element of the sequence as
its argument and returns a Boolean value indicating whether the
element is a match.
- Returns: The last element of the sequence that satisfies `predicate`,
or `nil` if there is no element that satisfies `predicate`.
- Complexity: O(n). The linked list must traverse to the end.

#### Example
This example uses the `last(where:)` method to find the last
negative number in an array of integers:
*/
/// ```swift
/// let numbers = LinkedList([3, 7, 4, -2, 9, -6, 10, 1])
/// if let lastNegative = numbers.last(where: { $0.value < 0 }) {
/// print("The last negative number is \(lastNegative).")
/// }
/// // Prints "The last negative number is -6."
/// ```
public func last(where predicate: (Element) throws -> Bool) rethrows -> Element? {
var lastElement: Element?

for element in self where try predicate(element) {
lastElement = element
}

return lastElement
}
}
4 changes: 2 additions & 2 deletions Sources/Workflow/Models/Workflow.swift
Expand Up @@ -123,7 +123,7 @@ public final class Workflow<F: FlowRepresentable>: LinkedList<_WorkflowItem> {
}

guard let first = firstLoadedInstance else {
onFinish?(passedArgs)
orchestrationResponder.complete(AnyWorkflow(self), passedArgs: passedArgs, onFinish: onFinish)
Richard-Gist marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

Expand Down Expand Up @@ -191,7 +191,7 @@ public final class Workflow<F: FlowRepresentable>: LinkedList<_WorkflowItem> {
}

guard let nextNode = nextLoadedNode else {
onFinish?(argsToPass)
orchestrationResponder?.complete(AnyWorkflow(self), passedArgs: argsToPass, onFinish: onFinish)
return
}

Expand Down
7 changes: 7 additions & 0 deletions Sources/Workflow/Protocols/OrchestrationResponder.swift
Expand Up @@ -32,6 +32,13 @@ public protocol OrchestrationResponder {
- Parameter onFinish: A closure that is executed when the responder is finished abandoning.
*/
func abandon(_ workflow: AnyWorkflow, onFinish: (() -> Void)?)
/**
Respond to the `Workflow` completing.
- Parameter workflow: The `AnyWorkflow` that is being completed.
- Parameter passedArgs: The `AnyWorkflow.PassedArgs` to be passed to `onFinish`.
- Parameter onFinish: A closure that is executed when the responder is finished completing.
*/
func complete(_ workflow: AnyWorkflow, passedArgs: AnyWorkflow.PassedArgs, onFinish: ((AnyWorkflow.PassedArgs) -> Void)?)
Richard-Gist marked this conversation as resolved.
Show resolved Hide resolved
}

extension OrchestrationResponder {
Expand Down
20 changes: 17 additions & 3 deletions Sources/Workflow/TypeErased/AnyWorkflow.swift
Expand Up @@ -42,6 +42,10 @@ extension AnyWorkflow: Sequence {
public func makeIterator() -> LinkedList<_WorkflowItem>.Iterator {
storageBase.makeIterator()
}

public func last(where predicate: (LinkedList<_WorkflowItem>.Element) throws -> Bool) rethrows -> LinkedList<_WorkflowItem>.Element? {
try storageBase.last(where: predicate)
}
}

extension AnyWorkflow {
Expand Down Expand Up @@ -69,15 +73,21 @@ extension AnyWorkflow {

fileprivate class AnyWorkflowStorageBase {
var orchestrationResponder: OrchestrationResponder?
var count: Int { fatalError("Count not overriden by AnyWorkflowStorage") }
var count: Int { fatalError("Count not overridden by AnyWorkflowStorage") }

// https://github.com/Tyler-Keith-Thompson/Workflow/blob/master/STYLEGUIDE.md#type-erasure
// swiftlint:disable:next unavailable_function
func _abandon() { fatalError("_abandon not overridden by AnyWorkflowStorage") }

// https://github.com/Tyler-Keith-Thompson/Workflow/blob/master/STYLEGUIDE.md#type-erasure
// swiftlint:disable:next unavailable_function
func _abandon() { fatalError("_abandon not overriden by AnyWorkflowStorage") }
func makeIterator() -> LinkedList<_WorkflowItem>.Iterator { fatalError("makeIterator not overridden by AnyWorkflowStorage") }

// https://github.com/Tyler-Keith-Thompson/Workflow/blob/master/STYLEGUIDE.md#type-erasure
// swiftlint:disable:next unavailable_function
func makeIterator() -> LinkedList<_WorkflowItem>.Iterator { fatalError("makeIterator not overriden by AnyWorkflowStorage") }
func last(where _: (LinkedList<_WorkflowItem>.Element) throws -> Bool) rethrows -> LinkedList<_WorkflowItem>.Element? {
fatalError("last(where:) not overridden by AnyWorkflowStorage")
}
}

fileprivate final class AnyWorkflowStorage<F: FlowRepresentable>: AnyWorkflowStorageBase {
Expand Down Expand Up @@ -105,4 +115,8 @@ fileprivate final class AnyWorkflowStorage<F: FlowRepresentable>: AnyWorkflowSto
override func makeIterator() -> LinkedList<_WorkflowItem>.Iterator {
workflow.makeIterator()
}

override func last(where predicate: (LinkedList<_WorkflowItem>.Element) throws -> Bool) rethrows -> LinkedList<_WorkflowItem>.Element? {
try workflow.last(where: predicate)
}
}
33 changes: 30 additions & 3 deletions Sources/WorkflowUIKit/UIKitPresenter.swift
Expand Up @@ -15,6 +15,7 @@ open class UIKitPresenter: OrchestrationResponder {
let launchedFromVC: UIViewController
let launchedPresentationType: LaunchStyle.PresentationType
var firstLoadedInstance: UIViewController?
weak var internalNavigationController: UINavigationController?

/**
Creates a `UIKitPresenter` that can respond to a `Workflow`'s actions.
Expand Down Expand Up @@ -61,6 +62,17 @@ open class UIKitPresenter: OrchestrationResponder {
abandon(workflow, animated: true, onFinish: onFinish)
}

public func complete(_ workflow: AnyWorkflow, passedArgs: AnyWorkflow.PassedArgs, onFinish: ((AnyWorkflow.PassedArgs) -> Void)?) {
let lastInstance = workflow.last { $0.value.instance != nil }

if lastInstance?.value.metadata.persistence == .removedAfterProceeding,
let view = lastInstance?.value.instance?.underlyingInstance as? UIViewController {
destroy(view) { onFinish?(passedArgs) }
} else {
onFinish?(passedArgs)
}
}

func abandon(_ workflow: AnyWorkflow, animated: Bool, onFinish: (() -> Void)?) {
guard let first = firstLoadedInstance else { return }
if let nav = first.navigationController {
Expand Down Expand Up @@ -89,12 +101,21 @@ open class UIKitPresenter: OrchestrationResponder {
}
}

private func destroy(_ view: UIViewController) {
private func destroy(_ view: UIViewController, completion: (() -> Void)? = nil) {
if let nav = view.navigationController {
let vcs = nav.viewControllers.filter {
$0 !== view
}
nav.setViewControllers(vcs, animated: false)

if vcs.isEmpty && nav === internalNavigationController {
nav.presentingViewController?.dismiss(animated: false) { [weak self] in
self?.internalNavigationController?.setViewControllers(vcs, animated: false)
Richard-Gist marked this conversation as resolved.
Show resolved Hide resolved
completion?()
}
} else {
nav.setViewControllers(vcs, animated: false)
completion?()
}
} else {
let parent = view.presentingViewController
let child = view.presentedViewController
Expand All @@ -104,7 +125,11 @@ open class UIKitPresenter: OrchestrationResponder {
parent?.dismiss(animated: false) {
if let p = parent,
let c = child {
p.present(c, animated: false)
p.present(c, animated: false) {
completion?()
}
} else {
completion?()
}
}
}
Expand Down Expand Up @@ -153,6 +178,7 @@ open class UIKitPresenter: OrchestrationResponder {
completion: (() -> Void)?) {
if LaunchStyle.PresentationType(rawValue: to.value.metadata.launchStyle) == .navigationStack {
let nav = UINavigationController(rootViewController: view)
internalNavigationController = nav
if let modalPresentationStyle = UIModalPresentationStyle.styleFor(style) {
nav.modalPresentationStyle = modalPresentationStyle
}
Expand All @@ -174,6 +200,7 @@ open class UIKitPresenter: OrchestrationResponder {
completion?()
} else {
let nav = UINavigationController(rootViewController: view)
internalNavigationController = nav
root.present(nav, animated: animated, completion: completion)
}
}
Expand Down
13 changes: 13 additions & 0 deletions Tests/WorkflowExampleTests/Mocks/MockOrchestrationResponder.swift
Expand Up @@ -10,6 +10,19 @@ import Foundation
import Workflow

class MockOrchestrationResponder: OrchestrationResponder {
var completeCalled = 0
var lastPassedArgs: AnyWorkflow.PassedArgs?
var lastCompleteOnFinish: ((AnyWorkflow.PassedArgs) -> Void)?
var complete_EnableDefaultImplementation = false
func complete(_ workflow: AnyWorkflow, passedArgs: AnyWorkflow.PassedArgs, onFinish: ((AnyWorkflow.PassedArgs) -> Void)?) {
lastWorkflow = workflow
lastPassedArgs = passedArgs
lastCompleteOnFinish = onFinish
completeCalled += 1

if complete_EnableDefaultImplementation { onFinish?(passedArgs) }
}

var launchCalled = 0
var lastTo: AnyWorkflow.Element?
func launch(to: AnyWorkflow.Element) {
Expand Down
27 changes: 27 additions & 0 deletions Tests/WorkflowTests/LinkedListTests.swift
Expand Up @@ -409,6 +409,33 @@ class LinkedListTests: XCTestCase {
XCTAssertEqual(list.last?.value, 4)
}

func testLastWhere() {
class Obj {
let num: Int
init(_ num: Int) { self.num = num }
}
let list = LinkedList([Obj(1), Obj(4), Obj(4), Obj(3)])
let expectedValue = list.first?.traverse(2)?.value

let last = list.last { $0.value.num == 4 }

XCTAssertEqual(last?.value.num, 4)
XCTAssert(last?.value === expectedValue)

let notFound = list.last { $0.value.num == 10 }

XCTAssertNil(notFound)
}

func testLastWhereAllowsThrowingClosure() {
class Obj { func throwing() throws -> Bool { true } }
let list = LinkedList([Obj(), Obj(), Obj()])

let foundObj = try? list.last { try $0.value.throwing() }

XCTAssert(foundObj?.value === list.last?.value)
}

func testEquatability() {
let list = LinkedList([1, 2, 3])
let list2 = LinkedList([1, 2, 3])
Expand Down
Expand Up @@ -26,7 +26,6 @@ class MockOrchestrationResponder: OrchestrationResponder {
allFroms.last
}

var lastCompletion:(() -> Void)?
func proceed(to: AnyWorkflow.Element,
from: AnyWorkflow.Element) {
allTos.append(to)
Expand All @@ -49,4 +48,17 @@ class MockOrchestrationResponder: OrchestrationResponder {
lastOnFinish = onFinish
abandonCalled += 1
}

var completeCalled = 0
var lastPassedArgs: AnyWorkflow.PassedArgs?
var lastCompleteOnFinish: ((AnyWorkflow.PassedArgs) -> Void)?
var complete_EnableDefaultImplementation = false
func complete(_ workflow: AnyWorkflow, passedArgs: AnyWorkflow.PassedArgs, onFinish: ((AnyWorkflow.PassedArgs) -> Void)?) {
lastWorkflow = workflow
lastPassedArgs = passedArgs
lastCompleteOnFinish = onFinish
completeCalled += 1

if complete_EnableDefaultImplementation { onFinish?(passedArgs) }
}
}
Expand Up @@ -53,6 +53,7 @@ class OrchestrationResponderTests: XCTestCase {
.thenProceed(with: FR2.self)
.thenProceed(with: FR3.self)
let responder = MockOrchestrationResponder()
responder.complete_EnableDefaultImplementation = true
let expectation = self.expectation(description: "OnFinish called")

let launchedRepresentable = wf.launch(withOrchestrationResponder: responder) { _ in expectation.fulfill() }
Expand All @@ -62,6 +63,10 @@ class OrchestrationResponderTests: XCTestCase {
(responder.lastTo?.value.instance?.underlyingInstance as? FR3)?.proceedInWorkflow()

wait(for: [expectation], timeout: 3)

XCTAssertEqual(responder.completeCalled, 1)
XCTAssertNotNil(responder.lastPassedArgs)
XCTAssertNotNil(responder.lastCompleteOnFinish)
}

func testWorkflowCallsOnFinishWhenItIsDone_andPassesForwardLastArguments() {
Expand All @@ -74,6 +79,7 @@ class OrchestrationResponderTests: XCTestCase {
.thenProceed(with: FR2.self)
.thenProceed(with: FR3.self)
let responder = MockOrchestrationResponder()
responder.complete_EnableDefaultImplementation = true
let expectation = self.expectation(description: "OnFinish called")

let launchedRepresentable = wf.launch(withOrchestrationResponder: responder) { args in
Expand All @@ -86,6 +92,10 @@ class OrchestrationResponderTests: XCTestCase {
(responder.lastTo?.value.instance?.underlyingInstance as? FR3)?.proceedInWorkflow(val)

wait(for: [expectation], timeout: 3)

XCTAssertEqual(responder.completeCalled, 1)
XCTAssertNotNil(responder.lastPassedArgs)
XCTAssertNotNil(responder.lastCompleteOnFinish)
}

func testWorkflowCanProceedForwardAndBackwardThroughFlow() {
Expand Down Expand Up @@ -137,6 +147,7 @@ class OrchestrationResponderTests: XCTestCase {
.thenProceed(with: FR2.self)
.thenProceed(with: FR3.self)
let responder = MockOrchestrationResponder()
responder.complete_EnableDefaultImplementation = true
let expectation = self.expectation(description: "OnFinish called")

let launchedRepresentable = wf.launch(withOrchestrationResponder: responder) { _ in expectation.fulfill() }
Expand All @@ -149,6 +160,10 @@ class OrchestrationResponderTests: XCTestCase {
(responder.lastTo?.value.instance?.underlyingInstance as? FR3)?.proceedInWorkflow()

wait(for: [expectation], timeout: 3)

XCTAssertEqual(responder.completeCalled, 1)
XCTAssertNotNil(responder.lastPassedArgs)
XCTAssertNotNil(responder.lastCompleteOnFinish)
}

func testWorkflowCallsOnFinishWhenItIsDone_andPassesForwardInitialArguments_EvenWhenMovingBackwardsForABit() {
Expand Down Expand Up @@ -192,6 +207,7 @@ class OrchestrationResponderTests: XCTestCase {
.thenProceed(with: FR2.self)
.thenProceed(with: FR3.self)
let responder = MockOrchestrationResponder()
responder.complete_EnableDefaultImplementation = true
let expectation = self.expectation(description: "OnFinish called")

let launchedRepresentable = wf.launch(withOrchestrationResponder: responder,
Expand All @@ -214,6 +230,10 @@ class OrchestrationResponderTests: XCTestCase {
(responder.lastTo?.value.instance?.underlyingInstance as? FR3)?.proceedInWorkflow(val)

wait(for: [expectation], timeout: 3)

XCTAssertEqual(responder.completeCalled, 1)
XCTAssertNotNil(responder.lastPassedArgs)
XCTAssertNotNil(responder.lastCompleteOnFinish)
}
}

Expand Down