From eec001af54fb565ec22839f1bc3a022dbaa73884 Mon Sep 17 00:00:00 2001 From: Muukii Date: Sat, 3 Oct 2026 14:49:38 +0900 Subject: [PATCH] Fix demo compatibility after Storybook update --- .../project.pbxproj | 22 +-- .../xcshareddata/swiftpm/Package.resolved | 96 ++------- .../AnimatorPlaygroundViewController.swift | 13 ++ .../FluidInterfaceKit-Demo/AppDelegate.swift | 16 +- .../Sources/FluidInterfaceKit-Demo/Book.swift | 184 ++++++++++-------- .../DemoApplicationViewController.swift | 8 +- .../DemoContextMenuViewController.swift | 2 +- .../FluidInterfaceKit-Demo/DemoGridView.swift | 101 ++++++++++ .../DemoListContainerViewController.swift | 2 +- .../DemoPopoverSwiftUIViewController.swift | 28 ++- .../DemoPortalLayer.swift | 67 ++++++- .../DemoPortalStackViewController.swift | 69 +++++-- .../DemoRideauIntegrationViewController.swift | 2 +- .../DemoStageViewController.swift | 3 +- .../DemoTransitionViewController.swift | 2 +- .../Sources/FluidInterfaceKit-Demo/Info.plist | 23 +++ .../RootContainerViewController.swift | 12 +- ...lingVelocityPlaygroundViewController.swift | 2 +- ...tionVelocityPlaygroundViewController.swift | 2 +- 19 files changed, 448 insertions(+), 206 deletions(-) create mode 100644 Development/Sources/FluidInterfaceKit-Demo/DemoGridView.swift create mode 100644 Development/Sources/FluidInterfaceKit-Demo/Info.plist diff --git a/Development/FluidInterfaceKit.xcodeproj/project.pbxproj b/Development/FluidInterfaceKit.xcodeproj/project.pbxproj index 304ab3cab..d662dc079 100644 --- a/Development/FluidInterfaceKit.xcodeproj/project.pbxproj +++ b/Development/FluidInterfaceKit.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + F182100300000001A0000002 /* DemoGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F182100300000001A0000001 /* DemoGridView.swift */; }; 4B01AFE1281939CB00A44537 /* DemoSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B01AFE0281939CB00A44537 /* DemoSheetViewController.swift */; }; 4B2AFC4A2A7EBF05001B5BEF /* FluidGesture in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2AFC492A7EBF05001B5BEF /* FluidGesture */; }; 4B2AFC4C2A7EC243001B5BEF /* DemoDragViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2AFC4B2A7EC243001B5BEF /* DemoDragViewController.swift */; }; @@ -54,7 +55,6 @@ 4BC726342992679F0074A828 /* SwiftUISupport in Frameworks */ = {isa = PBXBuildFile; productRef = 4BC726332992679F0074A828 /* SwiftUISupport */; }; 4BC7B20D284E3C7F0087BC7A /* DemoPictureInPictureCoolController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC7B20C284E3C7F0087BC7A /* DemoPictureInPictureCoolController.swift */; }; 4BC8F5F427B6B02300095412 /* StorybookKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4BC8F5F327B6B02300095412 /* StorybookKit */; }; - 4BC8F5F627B6B02300095412 /* StorybookUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4BC8F5F527B6B02300095412 /* StorybookUI */; }; 4BDDD3CE28DB627500452B7D /* CompositionKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4BDDD3CD28DB627500452B7D /* CompositionKit */; }; 4BDDF269277F50C100636B00 /* MondrianLayout in Frameworks */ = {isa = PBXBuildFile; productRef = 4BDDF268277F50C100636B00 /* MondrianLayout */; }; 4BFB8FA22AD90E8100F0048D /* FluidPictureInPicture in Frameworks */ = {isa = PBXBuildFile; productRef = 4BFB8FA12AD90E8100F0048D /* FluidPictureInPicture */; }; @@ -72,6 +72,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + F182100300000001A0000003 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F182100300000001A0000001 /* DemoGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoGridView.swift; sourceTree = ""; }; 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 = ""; }; @@ -147,7 +149,6 @@ 4BBDECE0276648FD00982229 /* CompositionKit in Frameworks */, 4BDDD3CE28DB627500452B7D /* CompositionKit in Frameworks */, 4B658D0F276A59F700A17878 /* Wrap in Frameworks */, - 4BC8F5F627B6B02300095412 /* StorybookUI in Frameworks */, 4BDDF269277F50C100636B00 /* MondrianLayout in Frameworks */, 4B2AFC4A2A7EBF05001B5BEF /* FluidGesture in Frameworks */, 4BB5B31C277077EA00EE1028 /* MockKit in Frameworks */, @@ -190,6 +191,8 @@ 4B46805F279C58E800625FCB /* DemoApplicationViewController.swift */, 4B7410B827ACE975003D8AB5 /* DemoCompositionOrderViewController.swift */, 4B46805C279C58E800625FCB /* DemoContextMenuViewController.swift */, + F182100300000001A0000001 /* DemoGridView.swift */, + F182100300000001A0000003 /* Info.plist */, 4B46804F279C58E800625FCB /* DemoControlCenterViewController.swift */, 4B2F619427B818E800363580 /* DemoListContainerViewController.swift */, 4B468052279C58E800625FCB /* DemoPictureInPictureController.swift */, @@ -325,7 +328,6 @@ 4BB5B31B277077EA00EE1028 /* MockKit */, 4BDDF268277F50C100636B00 /* MondrianLayout */, 4BC8F5F327B6B02300095412 /* StorybookKit */, - 4BC8F5F527B6B02300095412 /* StorybookUI */, 4BDDD3CD28DB627500452B7D /* CompositionKit */, 4BC726332992679F0074A828 /* SwiftUISupport */, 4B2AFC492A7EBF05001B5BEF /* FluidGesture */, @@ -461,6 +463,7 @@ 4B33622B2B0FBABA00A7D094 /* Color.swift in Sources */, 4B468067279C58E800625FCB /* DemoThreadsMessagesViewController.swift in Sources */, 4B468072279C58E800625FCB /* DemoContextMenuViewController.swift in Sources */, + F182100300000001A0000002 /* DemoGridView.swift in Sources */, 4BC7B20D284E3C7F0087BC7A /* DemoPictureInPictureCoolController.swift in Sources */, 4B2F619527B818E800363580 /* DemoListContainerViewController.swift in Sources */, 4B468065279C58E800625FCB /* RootContainerViewController.swift in Sources */, @@ -674,7 +677,8 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + INFOPLIST_FILE = "Sources/FluidInterfaceKit-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -703,7 +707,8 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + INFOPLIST_FILE = "Sources/FluidInterfaceKit-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -853,7 +858,7 @@ repositoryURL = "https://github.com/FluidGroup/swiftui-hosting.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.3.0; + minimumVersion = 3.0.0; }; }; 4BFFBFD22AD6C1ED0028A73B /* XCRemoteSwiftPackageReference "StackScrollView" */ = { @@ -916,11 +921,6 @@ package = 4BC8F5F227B6B02300095412 /* XCRemoteSwiftPackageReference "Storybook-ios" */; productName = StorybookKit; }; - 4BC8F5F527B6B02300095412 /* StorybookUI */ = { - isa = XCSwiftPackageProductDependency; - package = 4BC8F5F227B6B02300095412 /* XCRemoteSwiftPackageReference "Storybook-ios" */; - productName = StorybookUI; - }; 4BDDD3CD28DB627500452B7D /* CompositionKit */ = { isa = XCSwiftPackageProductDependency; package = 4BDDD3CC28DB627500452B7D /* XCRemoteSwiftPackageReference "CompositionKit" */; diff --git a/Development/FluidInterfaceKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Development/FluidInterfaceKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5c8828f76..73c021f87 100644 --- a/Development/FluidInterfaceKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Development/FluidInterfaceKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a52be0dfd401b4bb508dedd71b3711b09cdd898c2ffa3b9f6aae5abeb69fd509", + "originHash" : "e862f02838e0a0bfd2b40a598ccf4171e031f5d2d683210cdc85ae569b000be7", "pins" : [ { "identity" : "compositionkit", @@ -7,16 +7,16 @@ "location" : "https://github.com/FluidGroup/CompositionKit.git", "state" : { "branch" : "main", - "revision" : "3382c98ff9778136b6a21667d8152faa452658e0" + "revision" : "083e8b59b8b14aee3f0476ec9919ee2d1ae4e81c" } }, { "identity" : "descriptors", "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/Descriptors", + "location" : "https://github.com/muukii/Descriptors.git", "state" : { - "revision" : "f41ce2605a76c5d378fe8c5e8c5c98b544dfd108", - "version" : "0.2.3" + "revision" : "1ec4fcd01b7e3a6df9dfafcca8155e297a7a1d9f", + "version" : "0.3.0" } }, { @@ -28,24 +28,6 @@ "version" : "1.1.0" } }, - { - "identity" : "iglistkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/3a4oT/IGListKit", - "state" : { - "branch" : "spmNumber10", - "revision" : "979eca9b2da15e35af36f4613452dd6e07641969" - } - }, - { - "identity" : "libwebp-xcode", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SDWebImage/libwebp-Xcode", - "state" : { - "revision" : "0f3bdb28a1edc5e8e43876d3835d20c601ef331f", - "version" : "1.2.3" - } - }, { "identity" : "mockkit", "kind" : "remoteSourceControl", @@ -73,33 +55,6 @@ "revision" : "864edeb19072716f2fdc7061bf346aa3fe4d06c6" } }, - { - "identity" : "pincache", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pinterest/PINCache.git", - "state" : { - "revision" : "875c654984fb52b47ca65ae70d24852b0003ccd9", - "version" : "3.0.3" - } - }, - { - "identity" : "pinoperation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pinterest/PINOperation.git", - "state" : { - "revision" : "44d8ca154a4e75a028a5548c31ff3a53b90cef15", - "version" : "1.2.1" - } - }, - { - "identity" : "pinremoteimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pinterest/PINRemoteImage.git", - "state" : { - "branch" : "master", - "revision" : "18637e4414d77fbc95cd7d499179a9880e9a78d8" - } - }, { "identity" : "resultbuilderkit", "kind" : "remoteSourceControl", @@ -114,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/FluidGroup/Rideau.git", "state" : { - "revision" : "fa560b52c246863dee5ab4b03197f6e4a9794607", - "version" : "2.3.1" + "revision" : "cc8c18213a3cb4196ebb0d20a61f2dbf65b8cbf3", + "version" : "2.4.2" } }, { @@ -133,7 +88,7 @@ "location" : "https://github.com/eure/Storybook-ios", "state" : { "branch" : "main", - "revision" : "051236b8432513370a0dfa65dd8dcfc660613426" + "revision" : "9d1c4ba4b829a57c9571f4561199d979dd17f55f" } }, { @@ -150,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/FluidGroup/swiftui-hosting.git", "state" : { - "revision" : "975f2565cff7625f610c36cd7c146d8ab104a1dc", - "version" : "2.3.0" + "revision" : "9714566b32f84b3edce049ca3b0df06ecd0ec0c2", + "version" : "3.0.0" } }, { @@ -160,34 +115,7 @@ "location" : "https://github.com/FluidGroup/swiftui-support.git", "state" : { "branch" : "main", - "revision" : "78c2d634a3f6255c04e405e32bb7584207e7906f" - } - }, - { - "identity" : "texture", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/Texture.git", - "state" : { - "branch" : "spm", - "revision" : "5d03875bcfb0d87f8057ebeb5f2f6e4527e18128" - } - }, - { - "identity" : "texturebridging", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/TextureBridging.git", - "state" : { - "branch" : "main", - "revision" : "ed8d3ac84c3fda90832c793fdeb8972154bd15fe" - } - }, - { - "identity" : "textureswiftsupport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/TextureSwiftSupport.git", - "state" : { - "branch" : "main", - "revision" : "23c2df6dace4e4be8231f23becc5fc51d26dfa47" + "revision" : "f8058066c1d310458390d6c73462bf6722b71fc9" } }, { @@ -196,7 +124,7 @@ "location" : "https://github.com/VergeGroup/Wrap.git", "state" : { "branch" : "main", - "revision" : "68191d84e48723cb5cd9f6432862b9f22438b0a2" + "revision" : "3b731a14d9ee74b80bd781bba8c4baac6510d51b" } } ], diff --git a/Development/Sources/FluidInterfaceKit-Demo/AnimatorPlaygroundViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/AnimatorPlaygroundViewController.swift index 68ba10adf..f9efd9bd3 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/AnimatorPlaygroundViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/AnimatorPlaygroundViewController.swift @@ -3,7 +3,12 @@ import MondrianLayout import Ne import UIKit +// Internal animation dumps are available only when FluidStack enables testing. +#if DEBUG @testable import FluidStack +#else +import FluidStack +#endif final class AnimatorPlaygroundViewController: UIViewController { @@ -83,7 +88,9 @@ final class AnimatorPlaygroundViewController: UIViewController { animator.startAnimation() } + #if DEBUG view.layer.dumpAllAnimations() + #endif } } .viewBlock @@ -98,7 +105,9 @@ final class AnimatorPlaygroundViewController: UIViewController { view.layer.add(e, forKey: i) } + #if DEBUG view.layer.dumpAllAnimations() + #endif } } .viewBlock @@ -134,7 +143,9 @@ final class AnimatorPlaygroundViewController: UIViewController { } a.startAnimation() + #if DEBUG box1.layer.dumpAllAnimations() + #endif state.alphaFlag.toggle() @@ -159,7 +170,9 @@ final class AnimatorPlaygroundViewController: UIViewController { } + #if DEBUG box2.layer.dumpAllAnimations() + #endif state.positionFlag.toggle() diff --git a/Development/Sources/FluidInterfaceKit-Demo/AppDelegate.swift b/Development/Sources/FluidInterfaceKit-Demo/AppDelegate.swift index 5579da77a..5b9837508 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/AppDelegate.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/AppDelegate.swift @@ -4,19 +4,29 @@ import UIKit @_exported import MockKit @_exported import Ne +/// Provides the application's entry point; each scene owns its demo window. @main final class AppDelegate: UIResponder, UIApplicationDelegate { +} + +/// Creates and retains the catalog window for the connected UIKit scene. +final class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + + guard let windowScene = scene as? UIWindowScene else { return } - let newWindow = UIWindow() + let newWindow = UIWindow(windowScene: windowScene) newWindow.rootViewController = RootContainerViewController() newWindow.tintColor = .systemBlue//.neon(.violet) newWindow.makeKeyAndVisible() self.window = newWindow - return true } } diff --git a/Development/Sources/FluidInterfaceKit-Demo/Book.swift b/Development/Sources/FluidInterfaceKit-Demo/Book.swift index 41bdfbd65..dc0658e32 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/Book.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/Book.swift @@ -1,155 +1,169 @@ import CompositionKit import MondrianLayout import StorybookKit +import SwiftUI import UIKit import FluidStack import FluidPictureInPicture +/// The demo pages registered with Storybook's catalog, search, and history. @MainActor let book = Book(title: "FluidInterfaceKit") { - BookCallout( - symbol: "💡", - text: """ - This is a demo application to see FluidInterfaceKit. - """ - ) + BookPage(title: "About") { + BookText("💡 This is a demo application to see FluidInterfaceKit.") + } - BookNavigationLink(title: "Velocity Playground") { + Book(title: "Velocity Playground") { - BookPush(title: "Scaling") { - ScalingVelocityPlaygroundViewController() + BookPage(title: "Scaling", usesScrollView: false) { + DemoControllerPreview { ScalingVelocityPlaygroundViewController() } } - BookPush(title: "Translation") { - TranslationVelocityPlaygroundViewController() + BookPage(title: "Translation", usesScrollView: false) { + DemoControllerPreview { TranslationVelocityPlaygroundViewController() } } } - BookNavigationLink(title: "Transition") { - BookPresent(title: "Adding - in fluid stack") { - DemoTransitionViewController() + Book(title: "Transition") { + BookPage(title: "Adding - in fluid stack") { + BookPresent(title: "Adding - in fluid stack") { + DemoTransitionViewController() + } } } - BookNavigationLink(title: "App") { - BookPresent(title: "Launch") { - let controller = DemoApplicationController() - controller.modalPresentationStyle = .fullScreen - return controller + Book(title: "App") { + BookPage(title: "Launch") { + BookPresent(title: "Launch") { + let controller = DemoApplicationController() + controller.modalPresentationStyle = .fullScreen + return controller + } } } - BookNavigationLink(title: "Draggable View") { - BookPush(title: "Draggable View") { - DemoDragViewController() + Book(title: "Draggable View") { + BookPage(title: "Draggable View", usesScrollView: false) { + DemoControllerPreview { DemoDragViewController() } } } - BookNavigationLink(title: "Experiments") { + Book(title: "Experiments") { if #available(iOS 15, *) { - BookPush(title: "ContextMenu") { - DemoContextMenuViewController() + BookPage(title: "ContextMenu", usesScrollView: false) { + DemoControllerPreview { DemoContextMenuViewController() } } } } - BookNavigationLink(title: "PiP") { - BookPush(title: "Push") { - DemoPictureInPictureController() + Book(title: "PiP") { + BookPage(title: "Push", usesScrollView: false) { + DemoControllerPreview { DemoPictureInPictureController() } } - BookPush(title: "Push - Cool") { - DemoPictureInPictureCoolController() + BookPage(title: "Push - Cool", usesScrollView: false) { + DemoControllerPreview { DemoPictureInPictureCoolController() } } } - BookPush(title: "SafeArea") { - DemoSafeAreaViewController() + BookPage(title: "SafeArea", usesScrollView: false) { + DemoControllerPreview { DemoSafeAreaViewController() } } - BookPush(title: "ControlCenter") { - DemoControlCenterViewController() + BookPage(title: "ControlCenter", usesScrollView: false) { + DemoControllerPreview { DemoControlCenterViewController() } } - BookPush(title: "AnimatorPlayground") { - AnimatorPlaygroundViewController() + BookPage(title: "AnimatorPlayground", usesScrollView: false) { + DemoControllerPreview { AnimatorPlaygroundViewController() } } - BookPresent(title: "Instagram Threads") { - let controller = DemoThreadsMessagesViewController() - controller.modalPresentationStyle = .fullScreen - return controller + BookPage(title: "Instagram Threads") { + BookPresent(title: "Instagram Threads") { + let controller = DemoThreadsMessagesViewController() + controller.modalPresentationStyle = .fullScreen + return controller + } } - BookPresent(title: "List + ZStack") { - let controller = DemoListContainerViewController() - controller.modalPresentationStyle = .fullScreen - return controller + BookPage(title: "List + ZStack") { + BookPresent(title: "List + ZStack") { + let controller = DemoListContainerViewController() + controller.modalPresentationStyle = .fullScreen + return controller + } } - BookPresent(title: "+ Rideau") { - let controller = DemoRideauIntegrationViewController() - controller.modalPresentationStyle = .fullScreen - return controller + BookPage(title: "+ Rideau") { + BookPresent(title: "+ Rideau") { + let controller = DemoRideauIntegrationViewController() + controller.modalPresentationStyle = .fullScreen + return controller + } } - BookPresent(title: "Sheet") { - let controller = DemoSheetViewController() - controller.modalPresentationStyle = .fullScreen - return controller + BookPage(title: "Sheet") { + BookPresent(title: "Sheet") { + let controller = DemoSheetViewController() + controller.modalPresentationStyle = .fullScreen + return controller + } } - BookPresent(title: "Stacking") { - let controller = DemoStackingViewController() - controller.modalPresentationStyle = .fullScreen - return controller + BookPage(title: "Stacking") { + BookPresent(title: "Stacking") { + let controller = DemoStackingViewController() + controller.modalPresentationStyle = .fullScreen + return controller + } } - BookPush(title: "CAPortalLayer") { - DemoPortalLayerViewController() + BookPage(title: "CAPortalLayer", usesScrollView: false) { + DemoControllerPreview { DemoPortalLayerViewController() } } - BookPush(title: "PortalStackView") { - DemoPortalStackViewController() + BookPage(title: "PortalStackView", usesScrollView: false) { + DemoControllerPreview { DemoPortalStackViewController() } } - BookPush(title: "Popover SwiftUI") { - DemoPopoverSwiftUIViewController() + BookPage(title: "Popover SwiftUI", usesScrollView: false) { + DemoControllerPreview { DemoPopoverSwiftUIViewController() } } - BookPush(title: "Popover UIKit") { - DemoPopoverViewController() + BookPage(title: "Popover UIKit", usesScrollView: false) { + DemoControllerPreview { DemoPopoverViewController() } } - BookPush(title: "Composition") { - DemoCompositionOrderViewController() + BookPage(title: "Composition", usesScrollView: false) { + DemoControllerPreview { DemoCompositionOrderViewController() } } - BookPush(title: "FloatingDisplayKit") { - DemoFloatingDisplayKit(rootView: .init()) + BookPage(title: "FloatingDisplayKit", usesScrollView: false) { + DemoControllerPreview { DemoFloatingDisplayKit(rootView: .init()) } } if #available(iOS 15, *) { - BookPush(title: "KeyboardLayoutGuide") { - DemoKeyboardGuideViewController() + BookPage(title: "KeyboardLayoutGuide", usesScrollView: false) { + DemoControllerPreview { DemoKeyboardGuideViewController() } } } - BookPush(title: "StageViewController") { - DemoStageViewController() + BookPage(title: "StageViewController", usesScrollView: false) { + DemoControllerPreview { DemoStageViewController() } } - BookNavigationLink(title: "iOS 14 Pickers") { + BookPage(title: "iOS 14 Pickers") { - BookPreview(expandsWidth: true, maxHeight: 400, minHeight: 400) { + BookPreview { _ in UIView()&>.do { $0.backgroundColor = .white } } + .previewFrame(maxWidth: .infinity, minHeight: 400, idealHeight: 400, maxHeight: 400) BookSection(title: "Time picker .compact") { - BookPreview { + BookPreview { _ in let datePicker = UIDatePicker() datePicker.date = Date() if #available(iOS 13.4, *) { @@ -164,16 +178,34 @@ let book = Book(title: "FluidInterfaceKit") { } } - BookPreview(expandsWidth: true, maxHeight: 800, minHeight: 800) { + BookPreview { _ in UIView()&>.do { $0.backgroundColor = .white } } + .previewFrame(maxWidth: .infinity, minHeight: 800, idealHeight: 800, maxHeight: 800) } } +/// Embeds a UIKit demo directly in a Storybook page without another launch link. +/// +/// SwiftUI owns containment and keeps the controller alive for the page's +/// lifetime. The factory runs only when SwiftUI creates the represented controller. +fileprivate struct DemoControllerPreview: UIViewControllerRepresentable { + + let makeController: @MainActor () -> Controller + + func makeUIViewController(context: Context) -> Controller { + makeController() + } + + func updateUIViewController(_ uiViewController: Controller, context: Context) { + // These demos own their state; view updates must not replace their controller. + } +} + @MainActor func makeButtonView(title: String, onTap: @escaping () -> Void) -> UIView { let button = UIButton(type: .system) diff --git a/Development/Sources/FluidInterfaceKit-Demo/DemoApplicationViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/DemoApplicationViewController.swift index 363bae216..a17e433bb 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/DemoApplicationViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/DemoApplicationViewController.swift @@ -42,7 +42,7 @@ final class AppSearchViewController: FluidStackController { view.backgroundColor = .neon(.cyan) - let list = VGridView(numberOfColumns: 1) + let list = DemoGridView(numberOfColumns: 1) Mondrian.buildSubviews(on: view) { ZStackBlock(alignment: .attach(.all)) { @@ -85,7 +85,7 @@ final class AppOptionsController: CodeBasedViewController { view.backgroundColor = .neon(.red) - let list = VGridView(numberOfColumns: 1) + let list = DemoGridView(numberOfColumns: 1) Mondrian.buildSubviews(on: view) { ZStackBlock(alignment: .attach(.all)) { @@ -136,7 +136,7 @@ final class AppOtherController: CodeBasedViewController { view.backgroundColor = .neon(.blue) - let list = VGridView(numberOfColumns: 1) + let list = DemoGridView(numberOfColumns: 1) Mondrian.buildSubviews(on: view) { ZStackBlock(alignment: .attach(.all)) { @@ -181,7 +181,7 @@ final class NavigationChildViewController: UIViewController { view.backgroundColor = .systemBackground - let list = VGridView(numberOfColumns: 1) + let list = DemoGridView(numberOfColumns: 1) Mondrian.buildSubviews(on: view) { ZStackBlock(alignment: .attach(.all)) { diff --git a/Development/Sources/FluidInterfaceKit-Demo/DemoContextMenuViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/DemoContextMenuViewController.swift index 4a70a260f..26a1a7b85 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/DemoContextMenuViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/DemoContextMenuViewController.swift @@ -17,7 +17,7 @@ final class DemoContextMenuViewController: FluidStackController { view.backgroundColor = .systemBackground - let gridView = VGridView(numberOfColumns: 3) + let gridView = DemoGridView(numberOfColumns: 3) let makeCell: () -> UIView = { diff --git a/Development/Sources/FluidInterfaceKit-Demo/DemoGridView.swift b/Development/Sources/FluidInterfaceKit-Demo/DemoGridView.swift new file mode 100644 index 000000000..5da5dfbad --- /dev/null +++ b/Development/Sources/FluidInterfaceKit-Demo/DemoGridView.swift @@ -0,0 +1,101 @@ +import UIKit + +/// Displays the demo's retained views in a vertically scrolling, self-sizing grid. +/// +/// Content views keep their identity and interactions as cells scroll offscreen. +/// Only the collection view cells are reused; this view owns the supplied content +/// until a subsequent call to ``setContents(_:)`` replaces it. +final class DemoGridView: UICollectionView, UICollectionViewDataSource { + + private var contents: [UIView] = [] + + init(numberOfColumns: Int) { + precondition(numberOfColumns > 0) + + let column = NSCollectionLayoutGroup.horizontal( + layoutSize: .init( + widthDimension: .fractionalWidth(1), + heightDimension: .estimated(10) + ), + subitems: (0.. Int { + contents.count + } + + func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: ContentCell.reuseIdentifier, + for: indexPath + ) as! ContentCell + cell.setContent(contents[indexPath.item]) + return cell + } + + /// Temporarily hosts one retained content view while its item is onscreen. + private final class ContentCell: UICollectionViewCell { + + static let reuseIdentifier = "DemoGridContent" + + override func prepareForReuse() { + super.prepareForReuse() + contentView.subviews.forEach { $0.removeFromSuperview() } + } + + func setContent(_ content: UIView) { + guard content.superview !== contentView else { return } + + contentView.subviews.forEach { $0.removeFromSuperview() } + contentView.addSubview(content) + content.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + content.topAnchor.constraint(equalTo: contentView.topAnchor), + content.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + content.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + content.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + ]) + } + } +} diff --git a/Development/Sources/FluidInterfaceKit-Demo/DemoListContainerViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/DemoListContainerViewController.swift index 5f765e8a7..646ae4a19 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/DemoListContainerViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/DemoListContainerViewController.swift @@ -166,7 +166,7 @@ final class DemoListDetailViewController: UIViewController { UILabel()&>.do { $0.text = "Detail" $0.font = UIFont.preferredFont(forTextStyle: .headline) - $0.textColor = UIColor.appBlack + $0.textColor = .label } UIView()&>.do { diff --git a/Development/Sources/FluidInterfaceKit-Demo/DemoPopoverSwiftUIViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/DemoPopoverSwiftUIViewController.swift index 06ec0f1d2..5f23481f8 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/DemoPopoverSwiftUIViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/DemoPopoverSwiftUIViewController.swift @@ -32,7 +32,7 @@ final class DemoPopoverSwiftUIViewController: UIViewController { ForEach.inefficient( items: [ - HostingEdge( + PopoverAnchor( content: { Button( "Hello", @@ -40,9 +40,6 @@ final class DemoPopoverSwiftUIViewController: UIViewController { } ) - }, - reference: { hoge in - } ), @@ -56,3 +53,26 @@ final class DemoPopoverSwiftUIViewController: UIViewController { } } + +/// Hosts a SwiftUI popover anchor in its own UIKit view controller. +/// +/// SwiftUI owns the controller's containment and lifetime. Content updates reuse +/// that controller so the anchor keeps its UIKit identity across view updates. +fileprivate struct PopoverAnchor: UIViewControllerRepresentable { + + private let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + func makeUIViewController(context: Context) -> UIHostingController { + UIHostingController(rootView: content) + } + + func updateUIViewController(_ uiViewController: UIHostingController, context: Context) { + withTransaction(context.transaction) { + uiViewController.rootView = content + } + } +} diff --git a/Development/Sources/FluidInterfaceKit-Demo/DemoPortalLayer.swift b/Development/Sources/FluidInterfaceKit-Demo/DemoPortalLayer.swift index 10ee1b97c..aefc66388 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/DemoPortalLayer.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/DemoPortalLayer.swift @@ -11,6 +11,7 @@ import MondrianLayout import UIKit import FluidStack +/// Demonstrates live portal projections, including one stretched to fill its container. final class DemoPortalLayerViewController: UIViewController { private let debuggingLabel = UILabel() @@ -58,7 +59,7 @@ final class DemoPortalLayerViewController: UIViewController { let portalView2 = PortalView() - let stretchView = StretchView(contentView: portalView2) + let stretchView = StretchedPortalView(portalView: portalView2) Mondrian.buildSubviews(on: view) { @@ -150,3 +151,67 @@ final class DemoPortalLayerViewController: UIViewController { } } + +/// Scales a portal's source-sized bounds to fill this view independently on each axis. +/// +/// The embedded portal retains its original bounds so its source projection stays +/// intact. This container observes those bounds and clips the stretched rendering. +fileprivate final class StretchedPortalView: UIView { + + private let portalView: PortalView + private var boundsObservation: NSKeyValueObservation? + + init(portalView: PortalView) { + self.portalView = portalView + super.init(frame: .zero) + + clipsToBounds = true + addSubview(portalView) + + boundsObservation = portalView.observe(\.bounds) { [weak self] _, _ in + // UIKit changes bounds on the main actor; KVO delivers synchronously. + MainActor.assumeIsolated { + self?.invalidateIntrinsicContentSize() + self?.setNeedsLayout() + } + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + boundsObservation?.invalidate() + } + + override var intrinsicContentSize: CGSize { + portalView.bounds.size + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + portalView.sizeThatFits(size) + } + + override func layoutSubviews() { + super.layoutSubviews() + + let originalSize = portalView.bounds.size + guard + bounds.width > 0, + bounds.height > 0, + originalSize.width > 0, + originalSize.height > 0 + else { + return + } + + portalView.transform = CGAffineTransform( + scaleX: bounds.width / originalSize.width, + y: bounds.height / originalSize.height + ) + // Position through center because frame is undefined while a transform is applied. + portalView.center = CGPoint(x: bounds.midX, y: bounds.midY) + } +} diff --git a/Development/Sources/FluidInterfaceKit-Demo/DemoPortalStackViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/DemoPortalStackViewController.swift index 5ccb82f25..9bbfb78a3 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/DemoPortalStackViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/DemoPortalStackViewController.swift @@ -1,11 +1,35 @@ -import CompositionKit import FluidPortal import MondrianLayout import UIKit +/// Demonstrates portal content that remains visible outside scrolling list cells. final class DemoPortalStackViewController: UIViewController { - private let listView = DynamicCompositionalLayoutView(scrollDirection: .vertical) + private let listView: UICollectionView = { + let item = NSCollectionLayoutItem( + layoutSize: .init( + widthDimension: .fractionalWidth(1), + heightDimension: .estimated(100) + ) + ) + let group = NSCollectionLayoutGroup.vertical( + layoutSize: .init( + widthDimension: .fractionalWidth(1), + heightDimension: .estimated(100) + ), + subitems: [item] + ) + let layout = UICollectionViewCompositionalLayout( + section: NSCollectionLayoutSection(group: group) + ) + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = .clear + collectionView.delaysContentTouches = false + collectionView.isPrefetchingEnabled = false + return collectionView + }() + + private var dataSource: UICollectionViewDiffableDataSource? private let portalStackView = PortalStackView() override func viewDidLoad() { @@ -25,20 +49,23 @@ final class DemoPortalStackViewController: UIViewController { } - listView.registerCell(Cell.self) + listView.register(Cell.self, forCellWithReuseIdentifier: Cell.reuseIdentifier) - listView.setUp( - cellProvider: .init({ [portalStackView] context in + dataSource = UICollectionViewDiffableDataSource( + collectionView: listView, + cellProvider: { [portalStackView] collectionView, indexPath, item in - let cell = context.dequeueReusableCell(Cell.self) - cell.setData(context.data, stack: portalStackView) + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: Cell.reuseIdentifier, + for: indexPath + ) as! Cell + cell.setData(item, stack: portalStackView) return cell - }), - actionHandler: { action in } + } ) - listView.setContents( + setContents( [ "1", "2", @@ -48,14 +75,28 @@ final class DemoPortalStackViewController: UIViewController { "6", "7", "8", - ], - inSection: "A" + ] ) } + /// Updates the scrolling rows while preserving each string's diffable identity. + private func setContents(_ contents: [String]) { + guard let dataSource else { + preconditionFailure("Configure the data source before updating the portal rows.") + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections(["A"]) + snapshot.appendItems(contents, toSection: "A") + dataSource.apply(snapshot) + } + + /// Hosts a row's label and a platter registered with the enclosing portal stack. private final class Cell: UICollectionViewCell { + static let reuseIdentifier = "PortalStackCell" + private let label = UILabel() private let containerView = TipContainerView(contentView: UIButton(type: .system), portalStackView: nil) @@ -104,6 +145,10 @@ final class DemoPortalStackViewController: UIViewController { } } +/// Keeps content in its row while projecting a platter through a portal stack. +/// +/// An explicitly supplied stack takes precedence over one found in the view +/// hierarchy. The platter is registered only while this view belongs to a window. final class TipContainerView: UIView { let contentView: ContentView diff --git a/Development/Sources/FluidInterfaceKit-Demo/DemoRideauIntegrationViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/DemoRideauIntegrationViewController.swift index 7ab2ff6ae..e5ba46d66 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/DemoRideauIntegrationViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/DemoRideauIntegrationViewController.swift @@ -26,7 +26,7 @@ final class DemoRideauIntegrationViewController: FluidStackController { view.backgroundColor = .systemBackground - let list = VGridView(numberOfColumns: 1) + let list = DemoGridView(numberOfColumns: 1) Mondrian.buildSubviews(on: view) { ZStackBlock(alignment: .attach(.all)) { diff --git a/Development/Sources/FluidInterfaceKit-Demo/DemoStageViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/DemoStageViewController.swift index 45a9c4b5c..5d6935d28 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/DemoStageViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/DemoStageViewController.swift @@ -72,7 +72,8 @@ private final class ContentViewController: UIViewController, FluidStageChildView super.init(nibName: nil, bundle: nil) - let hostingView = HostingView(ignoringSafeAreaEdges: []) { state in + // Stage children inherit safe-area insets from their container. + let hostingView = SwiftUIHostingView(configuration: .init(safeAreaRegions: .all)) { ZStack { Color.init(color) diff --git a/Development/Sources/FluidInterfaceKit-Demo/DemoTransitionViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/DemoTransitionViewController.swift index 15dab2fd5..2ef3fb722 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/DemoTransitionViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/DemoTransitionViewController.swift @@ -24,7 +24,7 @@ final class DemoTransitionViewController: FluidStackController { addContentViewController(rootController, transition: .disabled) - let list = VGridView(numberOfColumns: 1) + let list = DemoGridView(numberOfColumns: 1) Mondrian.buildSubviews(on: rootController.view) { ZStackBlock(alignment: .attach(.all)) { diff --git a/Development/Sources/FluidInterfaceKit-Demo/Info.plist b/Development/Sources/FluidInterfaceKit-Demo/Info.plist new file mode 100644 index 000000000..0eb786dc1 --- /dev/null +++ b/Development/Sources/FluidInterfaceKit-Demo/Info.plist @@ -0,0 +1,23 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/Development/Sources/FluidInterfaceKit-Demo/RootContainerViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/RootContainerViewController.swift index 87e018b64..48084fec7 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/RootContainerViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/RootContainerViewController.swift @@ -1,21 +1,25 @@ import StorybookKit -import StorybookUI +import SwiftUI import UIKit +/// Hosts the demo catalog and its UIKit presentation context. final class RootContainerViewController: UIViewController { init() { super.init(nibName: nil, bundle: nil) - let child = StorybookViewController( - book: book, - dismissHandler: nil + let child = UIHostingController( + rootView: StorybookDisplayRootView( + bookStore: BookStore(book: book), + launchRequest: .catalog + ) ) addChild(child) view.addSubview(child.view) child.view.frame = view.bounds child.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + child.didMove(toParent: self) } diff --git a/Development/Sources/FluidInterfaceKit-Demo/ScalingVelocityPlaygroundViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/ScalingVelocityPlaygroundViewController.swift index 18f40e8af..1f5012caf 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/ScalingVelocityPlaygroundViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/ScalingVelocityPlaygroundViewController.swift @@ -3,7 +3,7 @@ import MondrianLayout import Ne import UIKit -@testable import FluidStack +import FluidStack final class ScalingVelocityPlaygroundViewController: UIViewController { diff --git a/Development/Sources/FluidInterfaceKit-Demo/TranslationVelocityPlaygroundViewController.swift b/Development/Sources/FluidInterfaceKit-Demo/TranslationVelocityPlaygroundViewController.swift index c2a8fd360..f05d522e9 100644 --- a/Development/Sources/FluidInterfaceKit-Demo/TranslationVelocityPlaygroundViewController.swift +++ b/Development/Sources/FluidInterfaceKit-Demo/TranslationVelocityPlaygroundViewController.swift @@ -3,7 +3,7 @@ import MondrianLayout import Ne import UIKit -@testable import FluidStack +import FluidStack final class TranslationVelocityPlaygroundViewController: UIViewController {