diff --git a/ACKategories-iOS/Base/FlowCoordinator.swift b/ACKategories-iOS/Base/FlowCoordinator.swift index a93e16b8..a2901176 100644 --- a/ACKategories-iOS/Base/FlowCoordinator.swift +++ b/ACKategories-iOS/Base/FlowCoordinator.swift @@ -69,10 +69,6 @@ extension Base { /// Clean up. Must be called when FC finished the flow to avoid memory leaks and unexpected behavior. open func stop(animated: Bool = false, completion: (() -> Void)? = nil) { - /// Determines whether dismiss should be called on `presentingViewController` of root, - /// based on whether there are remaining VCs in the navigation stack. - var shouldCallDismissOnPresentingVC = true - let animationGroup = DispatchGroup() // stop all children @@ -81,22 +77,63 @@ extension Base { $0.stop(animated: animated, completion: animationGroup.leave) } - // dismiss all VCs presented from root or nav - if rootViewController.presentedViewController != nil { + if rootViewController == nil { + ErrorHandlers.rootViewControllerDeallocatedBeforeStop?() + } + + dismissPresentedViewControllerIfPossible(animated: animated, group: animationGroup) + + /// Determines whether dismiss should be called on `presentingViewController` of root, + /// based on whether there are remaining VCs in the navigation stack. + let shouldCallDismissOnPresentingVC = popAllViewControllersIfPossible(animated: animated, group: animationGroup) + + // ensure that dismiss will be called on presentingVC of root only when appropriate, + // as presentingVC of root when modally presenting can be UITabBarController, + // but the whole navigation shouldn't be dismissed, as there are still VCs + // remaining in the navigation stack + if shouldCallDismissOnPresentingVC, let presentingViewController = rootViewController?.presentingViewController { + // dismiss when root was presented animationGroup.enter() - rootViewController.dismiss(animated: animated, completion: animationGroup.leave) + presentingViewController.dismiss(animated: animated, completion: animationGroup.leave) + } + + // stopping FC doesn't need to be nav delegate anymore -> pass it to parent + navigationController?.delegate = parentCoordinator + + parentCoordinator?.removeChild(self) + + animationGroup.notify(queue: DispatchQueue(label: "animationGroup")) { + completion?() + } + } + + // MARK: - Stop helpers + + /// Dismiss all VCs presented from root or nav if possible + private func dismissPresentedViewControllerIfPossible(animated: Bool, group: DispatchGroup) { + if let rootViewController = rootViewController, rootViewController.presentedViewController != nil { + group.enter() + rootViewController.dismiss(animated: animated, completion: group.leave) } + } - // pop all view controllers when started within navigation controller - if let navigationController = navigationController, let index = navigationController.viewControllers.firstIndex(of: rootViewController) { + /// Pop all view controllers when started within navigation controller + /// - Returns: Flag whether dismiss should be called on `presentingViewController` of root, + /// based on whether there are remaining VCs in the navigation stack. + private func popAllViewControllersIfPossible(animated: Bool, group: DispatchGroup) -> Bool { + if + let navigationController = navigationController, + let rootViewController = rootViewController, + let index = navigationController.viewControllers.firstIndex(of: rootViewController) + { // VCs to be removed from navigation stack let toRemoveViewControllers = navigationController.viewControllers[index.. pass it to parent - navigationController?.delegate = parentCoordinator - - parentCoordinator?.removeChild(self) - - animationGroup.notify(queue: DispatchQueue(label: "animationGroup")) { - completion?() - } + // Return the default value for the flag + return true } // MARK: - Child coordinators diff --git a/ACKategories-iOS/ErrorHandlers.swift b/ACKategories-iOS/ErrorHandlers.swift new file mode 100644 index 00000000..c7f47789 --- /dev/null +++ b/ACKategories-iOS/ErrorHandlers.swift @@ -0,0 +1,16 @@ +// +// ErrorHandlers.swift +// ACKategories-iOS +// +// Created by Lukáš Hromadník on 07.12.2020. +// + +import Foundation + +/// Set of handlers for some undefined behaviors in the framework +public enum ErrorHandlers { + + /// Called when `rootViewController` of the respective flow coordinator + /// is deallocated before the actual stop method logic is called + public static var rootViewControllerDeallocatedBeforeStop: (() -> Void)? +} diff --git a/ACKategories-iOS/GradientView.swift b/ACKategories-iOS/GradientView.swift index 269a2994..ea7a1edf 100644 --- a/ACKategories-iOS/GradientView.swift +++ b/ACKategories-iOS/GradientView.swift @@ -15,12 +15,7 @@ import UIKit `[UIColor.white, UIColor.white.withAlphaComponent(0)]` */ open class GradientView: UIView { - - override open class var layerClass: Swift.AnyClass { - get { - return CAGradientLayer.self - } - } + override open class var layerClass: Swift.AnyClass { CAGradientLayer.self } /** Creates a gradient view with colors and axis @@ -30,9 +25,14 @@ open class GradientView: UIView { */ public init(colors: [UIColor], axis: NSLayoutConstraint.Axis) { super.init(frame: CGRect(x: 0, y: 0, width: 1, height: 1)) + guard let gradientLayer = layer as? CAGradientLayer else { return } + + isUserInteractionEnabled = false + gradientLayer.frame = bounds gradientLayer.colors = colors.map { $0.cgColor } + if axis == .vertical { gradientLayer.startPoint = CGPoint(x: 0.5, y: 0) gradientLayer.endPoint = CGPoint(x: 0.5, y: 1) diff --git a/ACKategories-iOSTests/FlowCoordinator/FlowCoordinatorTests.swift b/ACKategories-iOSTests/FlowCoordinator/FlowCoordinatorTests.swift index 3db01f6a..be285d76 100644 --- a/ACKategories-iOSTests/FlowCoordinator/FlowCoordinatorTests.swift +++ b/ACKategories-iOSTests/FlowCoordinator/FlowCoordinatorTests.swift @@ -16,6 +16,8 @@ final class FlowCoordinatorTests: XCTestCase { override func setUp() { super.setUp() + ErrorHandlers.rootViewControllerDeallocatedBeforeStop = nil + window = .dummy } @@ -188,4 +190,29 @@ final class FlowCoordinatorTests: XCTestCase { XCTAssertEqual(navigationController.viewControllers.count, 1) XCTAssertEqual(fc.childCoordinators.count, 0) } + + func testRootViewControllerIsNil() { + let fc = NavigationFC() + fc.start(in: window) + _ = fc.rootViewController.view + fc.rootViewController = nil + + let exp = expectation(description: "Flow did finish") + fc.stop(animated: false) { exp.fulfill() } + wait(for: [exp], timeout: 0.3) + } + + func testErrorCallbackIsCalled() { + let rootExp = expectation(description: "Root is deallocated") + ErrorHandlers.rootViewControllerDeallocatedBeforeStop = { rootExp.fulfill() } + + let fc = NavigationFC() + fc.start(in: window) + _ = fc.rootViewController.view + fc.rootViewController = nil + + let exp = expectation(description: "Flow did finish") + fc.stop(animated: false) { exp.fulfill() } + wait(for: [exp, rootExp], timeout: 0.3) + } } diff --git a/ACKategories.xcodeproj/project.pbxproj b/ACKategories.xcodeproj/project.pbxproj index 2aeac363..0e418e41 100644 --- a/ACKategories.xcodeproj/project.pbxproj +++ b/ACKategories.xcodeproj/project.pbxproj @@ -107,6 +107,7 @@ 69FA5FC223C8690A00B44BCD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 69FA5FC123C8690A00B44BCD /* LaunchScreen.storyboard */; }; 6A31C9F3250572FE0047A983 /* SelfSizingTableHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A31C9F2250572FE0047A983 /* SelfSizingTableHeaderFooterView.swift */; }; A33559012555270F009B9D89 /* FlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33559002555270F009B9D89 /* FlowCoordinatorTests.swift */; }; + A38883E3257E2D2D00B958DD /* ErrorHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A38883E2257E2D2D00B958DD /* ErrorHandlers.swift */; }; A3BA685B256BEC7B006DB42F /* UIWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BA685A256BEC7B006DB42F /* UIWindow.swift */; }; A3BA6867256BECC6006DB42F /* UINavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BA6866256BECC6006DB42F /* UINavigationController.swift */; }; A3BA686E256BED96006DB42F /* Dummies.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BA686D256BED96006DB42F /* Dummies.swift */; }; @@ -266,6 +267,7 @@ 69FA5FE423C8712D00B44BCD /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 6A31C9F2250572FE0047A983 /* SelfSizingTableHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingTableHeaderFooterView.swift; sourceTree = ""; }; A33559002555270F009B9D89 /* FlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowCoordinatorTests.swift; sourceTree = ""; }; + A38883E2257E2D2D00B958DD /* ErrorHandlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandlers.swift; sourceTree = ""; }; A3BA685A256BEC7B006DB42F /* UIWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIWindow.swift; sourceTree = ""; }; A3BA6866256BECC6006DB42F /* UINavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationController.swift; sourceTree = ""; }; A3BA686D256BED96006DB42F /* Dummies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dummies.swift; sourceTree = ""; }; @@ -444,6 +446,7 @@ children = ( 6950963023C7747C00E8F457 /* Base */, 697CECF023C877B20019FE61 /* Aliases.swift */, + A38883E2257E2D2D00B958DD /* ErrorHandlers.swift */, 6950964823C7751600E8F457 /* GradientView.swift */, 6950964D23C7752B00E8F457 /* NSMutableParagraphStyleExtensions.swift */, 6950965223C7753D00E8F457 /* ReusableView.swift */, @@ -995,6 +998,7 @@ 6950965123C7753500E8F457 /* NumberFormatterExtensions.swift in Sources */, 6950964523C774DB00E8F457 /* ConditionalAssignment.swift in Sources */, 6950962823C7740B00E8F457 /* Base.swift in Sources */, + A38883E3257E2D2D00B958DD /* ErrorHandlers.swift in Sources */, 6950964C23C7751E00E8F457 /* NSAttributedStringExtensions.swift in Sources */, 6950967123C78AC900E8F457 /* UILabelExtensions.swift in Sources */, F8B81694246C59F7005D1D74 /* Date+Random.swift in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index d3ef1385..8c5d77a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ ### Fixed - Check if `navigationController != rootViewController` before running navigation delegate method ([#100](https://github.com/AckeeCZ/ACKategories/pull/100), kudos to @lukashromadnik) +- `GradientView` has `isUserInteractionEnabled = false` as it is not supposed to be interactive by design ([#99](https://github.com/AckeeCZ/ACKategories/pull/99), kudos to @olejnjak) +- Check the value of `rootViewController` before stopping the flow ([#98](https://github.com/AckeeCZ/ACKategories/pull/98), kudos to @lukashromadnik) ## 6.7.2