diff --git a/Development/FluidInterfaceKit.xcodeproj/project.pbxproj b/Development/FluidInterfaceKit.xcodeproj/project.pbxproj index c964d58c0..4f6ff0711 100644 --- a/Development/FluidInterfaceKit.xcodeproj/project.pbxproj +++ b/Development/FluidInterfaceKit.xcodeproj/project.pbxproj @@ -74,8 +74,6 @@ 4B01AFE0281939CB00A44537 /* DemoSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoSheetViewController.swift; sourceTree = ""; }; 4B2AFC4B2A7EC243001B5BEF /* DemoDragViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoDragViewController.swift; sourceTree = ""; }; 4B2F619427B818E800363580 /* DemoListContainerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoListContainerViewController.swift; sourceTree = ""; }; - 4B2F9E9F27FC080000F74ADB /* UIKitPresentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitPresentationTests.swift; sourceTree = ""; }; - 4B3BE33B27EDF6370060BAD3 /* FluidLocalEnvironmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FluidLocalEnvironmentTests.swift; sourceTree = ""; }; 4B46804B279C58E800625FCB /* TranslationVelocityPlaygroundViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationVelocityPlaygroundViewController.swift; sourceTree = ""; }; 4B46804C279C58E800625FCB /* DemoTransitionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoTransitionViewController.swift; sourceTree = ""; }; 4B46804D279C58E800625FCB /* AnimatorPlaygroundViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatorPlaygroundViewController.swift; sourceTree = ""; }; @@ -96,10 +94,7 @@ 4B46805F279C58E800625FCB /* DemoApplicationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoApplicationViewController.swift; sourceTree = ""; }; 4B468060279C58E800625FCB /* ScalingVelocityPlaygroundViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScalingVelocityPlaygroundViewController.swift; sourceTree = ""; }; 4B4E93DE29810AAC0060C775 /* DemoStageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoStageViewController.swift; sourceTree = ""; }; - 4B69049527B6685F0088FDDD /* FluidViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FluidViewControllerTests.swift; sourceTree = ""; }; - 4B6F87C927B599B100216F4E /* UINavigationItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationItemTests.swift; sourceTree = ""; }; 4B7410B827ACE975003D8AB5 /* DemoCompositionOrderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoCompositionOrderViewController.swift; sourceTree = ""; }; - 4B79ABBC27E7CB9A008DE44F /* UIViewControllerExtentionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExtentionTests.swift; sourceTree = ""; }; 4B853AB229D9940100F62504 /* DemoPopoverSwiftUIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoPopoverSwiftUIViewController.swift; sourceTree = ""; }; 4B853AB429D9A32A00F62504 /* DemoPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoPopoverViewController.swift; sourceTree = ""; }; 4B959B062764F77E0003FFB6 /* FluidInterfaceKit-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "FluidInterfaceKit-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -112,10 +107,7 @@ 4BC0A15727C178A300CD3A46 /* FirstViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstViewController.swift; sourceTree = ""; }; 4BC0A15927C178C000CD3A46 /* SecondViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondViewController.swift; sourceTree = ""; }; 4BC57A3B29D70A9400CA32A9 /* DemoPortalStackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoPortalStackViewController.swift; sourceTree = ""; }; - 4BC667B927B2E69C00182E6C /* FluidInterfaceKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FluidInterfaceKitTests.swift; sourceTree = ""; }; 4BC7B20C284E3C7F0087BC7A /* DemoPictureInPictureCoolController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoPictureInPictureCoolController.swift; sourceTree = ""; }; - 4BC8F5F727B6B1DA00095412 /* TreeBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeBuilder.swift; sourceTree = ""; }; - 4BD2A2B627B6A9AC00E20F07 /* FluidStackControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FluidStackControllerTests.swift; sourceTree = ""; }; 4BE0AAC4277FD5620086FD7E /* */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ""; sourceTree = ""; }; 4BF417742AD910E700F6E561 /* FluidInterfaceKit-work1 */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "FluidInterfaceKit-work1"; path = ..; sourceTree = ""; }; 4BFC696127A7F3BA0011238E /* FluidInterfaceKit-Demo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FluidInterfaceKit-Demo-Bridging-Header.h"; sourceTree = ""; }; @@ -171,7 +163,6 @@ children = ( 4BC0A14727C1731D00CD3A46 /* ExampleFluidApp */, 4B468049279C58E800625FCB /* FluidInterfaceKit-Demo */, - 4BC667B827B2E69C00182E6C /* FluidInterfaceKitTests */, ); path = Sources; sourceTree = ""; @@ -266,21 +257,6 @@ path = ExampleFluidApp; sourceTree = ""; }; - 4BC667B827B2E69C00182E6C /* FluidInterfaceKitTests */ = { - isa = PBXGroup; - children = ( - 4BC8F5F727B6B1DA00095412 /* TreeBuilder.swift */, - 4BC667B927B2E69C00182E6C /* FluidInterfaceKitTests.swift */, - 4B6F87C927B599B100216F4E /* UINavigationItemTests.swift */, - 4B69049527B6685F0088FDDD /* FluidViewControllerTests.swift */, - 4BD2A2B627B6A9AC00E20F07 /* FluidStackControllerTests.swift */, - 4B79ABBC27E7CB9A008DE44F /* UIViewControllerExtentionTests.swift */, - 4B2F9E9F27FC080000F74ADB /* UIKitPresentationTests.swift */, - 4B3BE33B27EDF6370060BAD3 /* FluidLocalEnvironmentTests.swift */, - ); - path = FluidInterfaceKitTests; - sourceTree = ""; - }; FCEA76EB29349CA600E8B0E6 /* DemoFloatingDisplayKit */ = { isa = PBXGroup; children = ( diff --git a/Sources/FluidStack/ViewController/FluidStackController.swift b/Sources/FluidStack/ViewController/FluidStackController.swift index 80ce6e7f6..90b429e06 100644 --- a/Sources/FluidStack/ViewController/FluidStackController.swift +++ b/Sources/FluidStack/ViewController/FluidStackController.swift @@ -41,6 +41,7 @@ public struct FluidStackContentConfiguration { } + /// A container view controller that manages view controller and view as child view controllers. /// It provides transitions when adding and removing. /// @@ -55,7 +56,9 @@ open class FluidStackController: UIViewController { } // MARK: - Properties - + + public private(set) var path: FluidStackPath = .init() + /// A closure that receives ``Action`` public final var stackActionHandler: (Action) -> Void = { _ in } @@ -87,7 +90,17 @@ open class FluidStackController: UIViewController { didSet { if stackingItems != oldValue { - + + let components = stackingItems.map { item -> FluidStackPath.Component in + if let identifiable = item.viewController as? (any FluidIdentifiableViewController) { + return .identifiable(.init(identifiable)) + } else { + return .volatile(.init(objectIdentifier: .init(item.viewController), ref: item.viewController)) + } + } + + self.path = .init(components: components) + // TODO: Update with animation UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { self.setNeedsStatusBarAppearanceUpdate() diff --git a/Sources/FluidStack/ViewController/FluidStackPath.swift b/Sources/FluidStack/ViewController/FluidStackPath.swift new file mode 100644 index 000000000..1ae7dd7ce --- /dev/null +++ b/Sources/FluidStack/ViewController/FluidStackPath.swift @@ -0,0 +1,75 @@ +import UIKit + +public struct FluidStackPath: Equatable { + + public enum Component: Equatable { + + public struct Volatile: Equatable { + public let objectIdentifier: ObjectIdentifier + public private(set) weak var ref: UIViewController? + } + + public struct Identifiable: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + guard lhs.id == rhs.id else { return false } + guard lhs.viewControllerType == rhs.viewControllerType else { return false } + return true + } + + public let id: AnyHashable + public let viewControllerType: UIViewController.Type + + @MainActor + public init(_ viewController: some FluidIdentifiableViewController) { + self.id = viewController.fluidIdentifier as AnyHashable + self.viewControllerType = type(of: viewController) + } + + public func restore(_ target: Target.Type) -> Target.FluidID? { + guard viewControllerType == target else { return nil } + guard let id = id as? Target.FluidID else { return nil } + return id + } + } + + case volatile(Volatile) + case identifiable(AnyFluidIdentifiableViewController) + } + + public private(set) var components: [Component] = [] + + public init() { + + } + + public init(components: [Component]) { + self.components = components + } + + public mutating func append(_ component: Component) { + components.append(component) + } + + public mutating func removeLast(_ k: Int = 1) { + components.removeLast(k) + } +} + +public struct AnyFluidIdentifiableViewController: Equatable { + + public var base: AnyHashable + + public init(_ base: some FluidIdentifiableViewController) { + self.base = base as AnyHashable + } + +} + +@MainActor +public protocol FluidIdentifiableViewController: UIViewController { + + associatedtype FluidID: Hashable + + var fluidIdentifier: FluidID { get } + +} diff --git a/Tests/FluidStackTests/FluidStackPathTests.swift b/Tests/FluidStackTests/FluidStackPathTests.swift new file mode 100644 index 000000000..bd4743f1d --- /dev/null +++ b/Tests/FluidStackTests/FluidStackPathTests.swift @@ -0,0 +1,26 @@ +import FluidStack +import XCTest + +@MainActor +final class FluidStackPathTests: XCTestCase { + + func test_identifiable() { + + class A: UIViewController, FluidIdentifiableViewController { + var fluidIdentifier: String { "1" } + } + + class B: UIViewController, FluidIdentifiableViewController { + var fluidIdentifier: String { "1" } + } + + XCTAssertNotEqual( + FluidStackPath.Component.Identifiable(A()), + FluidStackPath.Component.Identifiable(B()) + ) + + XCTAssertNotNil(FluidStackPath.Component.Identifiable(A()).restore(A.self)) + XCTAssertNil(FluidStackPath.Component.Identifiable(A()).restore(B.self)) + + } +}