├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .spi.yml ├── .swift-format ├── BuildTools ├── .gitignore ├── Package.swift ├── Package@swift-5.9.swift └── Plugins │ └── swift-format-plugin.swift ├── CHANGELOG.md ├── Documentation ├── FloatingPanel 2.0 Migration Guide.md ├── FloatingPanel API Guide.md ├── FloatingPanel SwiftUI API Guide.md └── assets │ ├── maps-landscape.gif │ ├── maps.gif │ └── stocks.gif ├── Examples ├── Maps-SwiftUI │ ├── Maps-SwiftUI.xcodeproj │ │ └── project.pbxproj │ └── Maps │ │ ├── ContentView.swift │ │ ├── FloatingPanelContentView.swift │ │ ├── HostingCell.swift │ │ ├── MapsApp.swift │ │ ├── Representable │ │ ├── SearchBar.swift │ │ └── VisualEffectBlur.swift │ │ ├── ResultsList.swift │ │ ├── SearchPanelPhoneDelegate.swift │ │ ├── SurfaceAppearance+phone.swift │ │ └── UIHostingController+ignoreKeyboard.swift ├── Maps │ ├── Maps.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Maps.xcscheme │ └── Maps │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── food.imageset │ │ │ ├── Contents.json │ │ │ └── food@2x.png │ │ ├── fun.imageset │ │ │ ├── Contents.json │ │ │ └── fun@2x.png │ │ ├── like.imageset │ │ │ ├── Contents.json │ │ │ └── like@2x.png │ │ ├── mark.imageset │ │ │ ├── Contents.json │ │ │ └── mark@2x.png │ │ ├── shopping.imageset │ │ │ ├── Contents.json │ │ │ └── shopping@2x.png │ │ └── travel.imageset │ │ │ ├── Contents.json │ │ │ └── travel@2x.png │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── DetailViewController.swift │ │ ├── Info.plist │ │ ├── MainViewController.swift │ │ ├── SearchViewController.swift │ │ └── Utils.swift ├── Samples │ ├── Samples.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Samples.xcscheme │ └── Sources │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── IMG_0003.imageset │ │ │ ├── Contents.json │ │ │ └── IMG_0003.jpg │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── ContentViewControllers │ │ ├── AdaptiveLayout │ │ │ ├── CollectionViewControllerForAdaptiveLayout.swift │ │ │ ├── ImageViewController.swift │ │ │ └── TableViewControllerForAdaptiveLayout.swift │ │ ├── DebugListCollectionViewController.swift │ │ ├── DebugTableViewController.swift │ │ ├── DebugTextViewController.swift │ │ ├── DetailViewController.swift │ │ ├── InspectorViewController.swift │ │ ├── ModalViewController.swift │ │ ├── MultiPanelController.swift │ │ ├── NestedScrollViewController.swift │ │ ├── SettingsViewController.swift │ │ ├── TabBarViewController.swift │ │ └── UnavailableViewController.swift │ │ ├── CustomState.swift │ │ ├── Extensions.swift │ │ ├── Info.plist │ │ ├── MainViewController.swift │ │ ├── PanelLayouts.swift │ │ ├── SupplementaryViews.swift │ │ └── UseCases │ │ ├── PagePanelController.swift │ │ ├── UseCase.swift │ │ └── UseCaseController.swift ├── SamplesObjC │ ├── SamplesObjC.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── SamplesObjC.xcscheme │ └── SamplesObjC │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── MainViewController.h │ │ ├── MainViewController.m │ │ ├── SamplesObjC-Bridging-Header.h │ │ └── main.m ├── SamplesSwiftUI │ ├── README.md │ ├── SamplesSwiftUI.xcodeproj │ │ └── project.pbxproj │ └── SamplesSwiftUI │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ ├── SampleApp.swift │ │ ├── UseCases │ │ ├── InsideTab.swift │ │ ├── MainView.swift │ │ └── MultiPanel.swift │ │ └── Views │ │ └── ContentView.swift └── Stocks │ ├── Stocks.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Stocks.xcscheme │ └── Stocks │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── news.imageset │ │ ├── Contents.json │ │ └── news@2x.png │ ├── stocks_list.imageset │ │ ├── Contents.json │ │ └── stocks_list@2x.png │ ├── top_banner.imageset │ │ ├── Contents.json │ │ └── top_bar@2x.png │ └── yahoo_bottom_bar.imageset │ │ ├── Contents.json │ │ └── yahoo_bottom_bar@2x.png │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── MainViewController.swift ├── FloatingPanel.podspec ├── FloatingPanel.xcodeproj ├── project.pbxproj └── xcshareddata │ ├── IDETemplateMacros.plist │ └── xcschemes │ └── FloatingPanel.xcscheme ├── FloatingPanel.xcworkspace ├── .xcodesamplecode.plist ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── BackdropView.swift ├── Behavior.swift ├── Controller.swift ├── Core.swift ├── Extensions.swift ├── FloatingPanel.docc │ ├── FloatingPanel API Guide.md │ ├── FloatingPanel SwiftUI API Guide.md │ └── FloatingPanel.md ├── FloatingPanel.h ├── GrabberView.swift ├── Info.plist ├── Layout.swift ├── LayoutAnchoring.swift ├── LayoutProperties.swift ├── Logging.swift ├── PassthroughView.swift ├── Position.swift ├── State.swift ├── SurfaceView.swift ├── SwiftUI │ ├── FloatingPanelCoordinator.swift │ ├── FloatingPanelProxy.swift │ ├── FloatingPanelView.swift │ ├── SurfaceAppearance+.swift │ ├── View+floatingPanel.swift │ ├── View+floatingPanelBehavior.swift │ ├── View+floatingPanelConfiguration.swift │ ├── View+floatingPanelLayout.swift │ ├── View+floatingPanelScrollTracking.swift │ ├── View+floatingPanelState.swift │ └── View+floatingPanelSurface.swift └── Transitioning.swift └── Tests ├── ControllerTests.swift ├── CoreTests.swift ├── ExtensionTests.swift ├── GestureTests.swift ├── Info.plist ├── LayoutTests.swift ├── StateTests.swift ├── SurfaceViewTests.swift └── TestSupports.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: SCENEE 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > Please fill out this template appropriately when filing a bug report. 2 | > 3 | > Please remove this line and everything above it before submitting. 4 | 5 | ### Description 6 | 7 | ### Expected behavior 8 | 9 | ### Actual behavior 10 | 11 | ### Steps to reproduce 12 | 13 | **Code example that reproduces the issue** 14 | 15 | 16 | **How do you display panel(s)?** 17 | 18 | * Add as child view controllers 19 | * Present modally 20 | 21 | **How many panels do you displays?** 22 | 23 | * 1 24 | * 2+ 25 | 26 | ### Environment 27 | 28 | **Library version** 29 | 30 | **Installation method** 31 | 32 | * CocoaPods 33 | * Carthage 34 | * Swift Package Manager 35 | 36 | **iOS version(s)** 37 | 38 | 39 | **Xcode version** 40 | 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Xcode 3 | # 4 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 5 | 6 | ## Build generated 7 | build/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | IDEWorkspaceChecks.plis 21 | *.xcodeproj/project.xcworkspace 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | *.xcsettings 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Swift Package Manager Specific 36 | .swiftpm/ 37 | 38 | ## Playgrounds 39 | timeline.xctimeline 40 | playground.xcworkspace 41 | 42 | # Swift Package Manager 43 | # 44 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 45 | # Packages/ 46 | # Package.pins 47 | # Package.resolved 48 | .build/ 49 | 50 | # CocoaPods 51 | # 52 | # We recommend against adding the Pods directory to your .gitignore. However 53 | # you should judge for yourself, the pros and cons are mentioned at: 54 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 55 | # 56 | # Pods/ 57 | 58 | # Carthage 59 | # 60 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 61 | # Carthage/Checkouts 62 | 63 | Carthage/Build 64 | 65 | # fastlane 66 | # 67 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 68 | # screenshots whenever they are needed. 69 | # For more information about the recommended setup visit: 70 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 71 | 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots/**/*.png 75 | fastlane/test_output 76 | 77 | 78 | # macOS 79 | .DS_Store 80 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [FloatingPanel] 5 | platform: ios 6 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentConditionalCompilationBlocks" : false, 6 | "indentSwitchCaseLabels" : false, 7 | "indentation" : { 8 | "spaces" : 4 9 | }, 10 | "lineBreakAroundMultilineExpressionChainComponents" : false, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : false, 13 | "lineBreakBeforeEachGenericRequirement" : false, 14 | "lineBreakBetweenDeclarationAttributes" : false, 15 | "lineLength" : 120, 16 | "maximumBlankLines" : 1, 17 | "multiElementCollectionTrailingCommas" : true, 18 | "noAssignmentInExpressions" : { 19 | "allowedFunctions" : [ 20 | "XCTAssertNoThrow" 21 | ] 22 | }, 23 | "prioritizeKeepingFunctionOutputTogether" : false, 24 | "reflowMultilineStringLiterals" : { 25 | "never" : { 26 | 27 | } 28 | }, 29 | "respectsExistingLineBreaks" : true, 30 | "rules" : { 31 | "AllPublicDeclarationsHaveDocumentation" : false, 32 | "AlwaysUseLiteralForEmptyCollectionInit" : false, 33 | "AlwaysUseLowerCamelCase" : true, 34 | "AmbiguousTrailingClosureOverload" : true, 35 | "AvoidRetroactiveConformances" : true, 36 | "BeginDocumentationCommentWithOneLineSummary" : false, 37 | "DoNotUseSemicolons" : true, 38 | "DontRepeatTypeInStaticProperties" : true, 39 | "FileScopedDeclarationPrivacy" : true, 40 | "FullyIndirectEnum" : true, 41 | "GroupNumericLiterals" : true, 42 | "IdentifiersMustBeASCII" : true, 43 | "NeverForceUnwrap" : false, 44 | "NeverUseForceTry" : false, 45 | "NeverUseImplicitlyUnwrappedOptionals" : false, 46 | "NoAccessLevelOnExtensionDeclaration" : true, 47 | "NoAssignmentInExpressions" : true, 48 | "NoBlockComments" : true, 49 | "NoCasesWithOnlyFallthrough" : true, 50 | "NoEmptyLinesOpeningClosingBraces" : false, 51 | "NoEmptyTrailingClosureParentheses" : true, 52 | "NoLabelsInCasePatterns" : true, 53 | "NoLeadingUnderscores" : false, 54 | "NoParensAroundConditions" : true, 55 | "NoPlaygroundLiterals" : true, 56 | "NoVoidReturnOnFunctionSignature" : true, 57 | "OmitExplicitReturns" : false, 58 | "OneCasePerLine" : true, 59 | "OneVariableDeclarationPerLine" : true, 60 | "OnlyOneTrailingClosureArgument" : true, 61 | "OrderedImports" : true, 62 | "ReplaceForEachWithForLoop" : true, 63 | "ReturnVoidInsteadOfEmptyTuple" : true, 64 | "TypeNamesShouldBeCapitalized" : true, 65 | "UseEarlyExits" : false, 66 | "UseExplicitNilCheckInConditions" : true, 67 | "UseLetInEveryBoundCaseVariable" : true, 68 | "UseShorthandTypeNames" : true, 69 | "UseSingleLinePropertyGetter" : true, 70 | "UseSynthesizedInitializer" : true, 71 | "UseTripleSlashForDocumentationComments" : true, 72 | "UseWhereClausesInForLoops" : false, 73 | "ValidateDocumentationComments" : false 74 | }, 75 | "spacesAroundRangeFormationOperators" : false, 76 | "spacesBeforeEndOfLineComments" : 2, 77 | "tabWidth" : 8, 78 | "version" : 1 79 | } 80 | -------------------------------------------------------------------------------- /BuildTools/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | 10 | -------------------------------------------------------------------------------- /BuildTools/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "build-tools", 7 | products: [ 8 | .plugin( 9 | name: "swift-format-plugin", 10 | targets: ["swift-format-plugin"] 11 | ) 12 | ], 13 | targets: [ 14 | .plugin( 15 | name: "swift-format-plugin", 16 | capability: .buildTool() 17 | ) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /BuildTools/Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // This file is used when the 'built-tools' package is built by Xcode 15 or earlier. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "build-tools", 8 | products: [ 9 | .plugin( 10 | name: "swift-format-plugin", 11 | targets: ["swift-format-plugin"] 12 | ) 13 | ], 14 | targets: [ 15 | .plugin( 16 | name: "swift-format-plugin", 17 | capability: .buildTool() 18 | ) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /BuildTools/Plugins/swift-format-plugin.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PackagePlugin 3 | 4 | @main 5 | struct SwiftFormatBuildToolPlugin: BuildToolPlugin { 6 | func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { 7 | // Currently the build tool plugin is not supported. 8 | return [] 9 | } 10 | } 11 | 12 | #if canImport(XcodeProjectPlugin) 13 | import XcodeProjectPlugin 14 | 15 | /// Formats Swift source files using the `swift format` command and a root configuration file during Xcode builds. 16 | extension SwiftFormatBuildToolPlugin: XcodeBuildToolPlugin { 17 | // Entry point for creating build commands for targets in Xcode projects. 18 | func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { 19 | #if swift(>=6.0) 20 | let swift = try context.tool(named: "swift").url 21 | let xcodeProjectDirectoryURL = context.xcodeProject.directoryURL 22 | // Find the code generator tool to run (replace this with the actual one). 23 | print("SwiftFormatBuildToolPlugin -> \(xcodeProjectDirectoryURL.filePath)") 24 | let configFile = xcodeProjectDirectoryURL.appending(path: ".swift-format").filePath 25 | let targetFiles = [ 26 | // Currently check only 'SwiftUI' source code in 'Sources' dir. 27 | xcodeProjectDirectoryURL.appending(path: "Sources/SwiftUI"), 28 | // xcodeProjectDirectoryURL.appending(path: "Sources"), 29 | // xcodeProjectDirectoryURL.appending(path: "Tests"), 30 | xcodeProjectDirectoryURL.appending(path: "BuildTools"), 31 | // Currently check only 'SamplesSwiftUI' source code in 'Examples' dir. 32 | xcodeProjectDirectoryURL.appending(path: "Examples/SamplesSwiftUI"), 33 | ].map { $0.filePath } 34 | return [ 35 | .buildCommand( 36 | displayName: "Run swift format(xcode)", 37 | executable: swift, 38 | arguments: [ 39 | "format", 40 | "lint", 41 | "--configuration", 42 | configFile, 43 | "-r", 44 | ] + targetFiles, 45 | inputFiles: [], 46 | outputFiles: [] 47 | ) 48 | ] 49 | #else 50 | // Skip running `swift format`; this subcommand ships with the Swift 6 compiler. 51 | return [] 52 | #endif 53 | } 54 | } 55 | 56 | extension URL { 57 | /// Returns a non–percent-encoded path for use with components containing non-ASCII characters. 58 | var filePath: String { path(percentEncoded: false) } 59 | } 60 | #endif 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | ## [3.0.0](https://github.com/scenee/FloatingPanel/releases/tag/3.0.0) 4 | 5 | ### Breaking Changes 6 | 7 | - The minimum deployment target is now **iOS 13.0**. 8 | - Dropped support for building with **Xcode 13.4.1**. 9 | 10 | ### Added 11 | 12 | - Introduced new SwiftUI APIs. 13 | - Added `Documentation/FloatingPanel SwiftUI API Guide.md`. 14 | - Added `Documentation/FloatingPanel API Guide.md`, migrating from `README.md`. 15 | - Added `Examples/SamplesSwiftUI` example app. 16 | - Added `FloatingPanelControllerDelegate/floatingPanel(_:animatorForMovingTo:)`. 17 | - Added partial `swift-format` support via the BuildTools plugin package. 18 | **Limitation:** Formatting currently applies only to the source code for the 19 | new SwiftUI API and the `SamplesSwiftUI` example app. 20 | - Enabled README preview mode in Xcode via `.xcodesamplecode.plist`. 21 | 22 | ### Changed 23 | 24 | - Updated `README.md` to cover the new SwiftUI APIs. 25 | - Moved UIKit-specific details to `Documentation/FloatingPanel API Guide.md`. 26 | - Updated DocC documentation for the new SwiftUI APIs. 27 | - Moved the `assets` folder. 28 | 29 | ## [2.8.8](https://github.com/scenee/FloatingPanel/releases/tag/2.8.8) 30 | 31 | ### Bugfixes 32 | 33 | - Allowed slight deviation when checking for anchor position. 34 | - Addressed #661 issue since v2.8.0 (#662) 35 | 36 | ## [2.8.7](https://github.com/scenee/FloatingPanel/releases/tag/2.8.7) 37 | 38 | :warning: [NOTICE] This release contains a regression. Please use v2.8.8 instead. 39 | 40 | ### Bugfixes 41 | 42 | - Disallow interrupting the panel interaction while bouncing over the most 43 | expanded state (#652) 44 | - Reset initialScrollOffset after the attracting animation ends (#659) 45 | 46 | ## [2.8.6](https://github.com/scenee/FloatingPanel/releases/tag/2.8.6) 47 | 48 | ### Bugfixes 49 | 50 | - Fix doc comment errors (#643) 51 | 52 | ## [2.8.5](https://github.com/scenee/FloatingPanel/releases/tag/2.8.5) 53 | 54 | ### Bugfixes 55 | 56 | - Replaced fatal errors in transitionDuration delegate methods (#642) 57 | 58 | ## [2.8.4](https://github.com/scenee/FloatingPanel/releases/tag/2.8.4) 59 | 60 | ### Bugfixes 61 | 62 | - Fixed an inappropriate condition to determine scrolling content (#633) 63 | 64 | ## [2.8.3](https://github.com/scenee/FloatingPanel/releases/tag/2.8.3) 65 | 66 | ### Bugfixes 67 | 68 | - Fix the scroll tracking of WKWebView on iOS 17.4 (#630) 69 | - Fix a broken panel layout with a compositional collection view (#634) 70 | - Fix a compilation error in Xcode 16 by @WillBishop (#636) 71 | 72 | ## [2.8.2](https://github.com/scenee/FloatingPanel/releases/tag/2.8.2) 73 | 74 | ### New features 75 | 76 | - Enabled to define and use a subclass object of BackdropView (#617) 77 | 78 | ### Improvements 79 | 80 | - Fixed the scroll locking behavior by @futuretap (#615) 81 | - Supported Xcode 15.2 on the GitHub Actions (#619) 82 | 83 | ### Bugfixes 84 | 85 | - Added a possible fix for #586 86 | - Fixed a bug that state was not changed property after v2.8.1 87 | 88 | ## [2.8.1](https://github.com/scenee/FloatingPanel/releases/tag/2.8.1) 89 | 90 | - Fixed an invalid behavior after switching to a new layout object (#611) 91 | 92 | ## [2.8.0](https://github.com/scenee/FloatingPanel/releases/tag/2.8.0) 93 | 94 | ### Breaking changes 95 | 96 | - The minimum deployment target of this library became iOS 11.0 on this release. 97 | 98 | ### New features 99 | 100 | - Added the new delegate method, `floatingPanel(_:shouldAllowToScroll:in:)`. 101 | 102 | ### Improvements 103 | 104 | - Enabled content scrolling in non-expanded states (#455) 105 | 106 | ### Bugfixes 107 | 108 | - Fixed CGFloat.rounded(by:) for a floating point error 109 | - Fixed scroll offset reset when moving in grabber area 110 | - Fixed a panel not moving when picked up in certain area 111 | - Fixed errors of offset value from a state position 112 | 113 | ## [2.7.0](https://github.com/scenee/FloatingPanel/releases/tag/2.7.0) 114 | 115 | ### Breaking changes 116 | 117 | - Calls the `floatingPanelDidMove` delegate method at the end of the move 118 | interaction. 119 | - Calls the `floatingPanelDidEndDragging` delegate method after 120 | `FloatingPanelController.state` changes when `willAttract` is `false`. 121 | - Sets `isAttracting` to `true` even when moving between states by 122 | `FloatingPanelController.move(to:animated:completion)` except for moves from 123 | or to `.hidden`. 124 | - Do not reset the scroll offset of its tracking scroll view when a user moves a 125 | panel outside its scroll view or on the navigation bar above it. 126 | 127 | ## Improvements 128 | 129 | - Added `FloatingPanelPanGestureRecognizer.delegateOrigin` to allow to access 130 | the default delegate implementations (It's useful when using `delegateProxy`). 131 | 132 | ## Bugfixes 133 | 134 | - Retains scroll view position while moving between states (#587) 135 | - Fixed invalid scroll offsets after moving between states 136 | - Calls `floatingPanelWillRemove` delegate method when a panel is removed from a 137 | window 138 | -------------------------------------------------------------------------------- /Documentation/assets/maps-landscape.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Documentation/assets/maps-landscape.gif -------------------------------------------------------------------------------- /Documentation/assets/maps.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Documentation/assets/maps.gif -------------------------------------------------------------------------------- /Documentation/assets/stocks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Documentation/assets/stocks.gif -------------------------------------------------------------------------------- /Examples/Maps-SwiftUI/Maps/ContentView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import SwiftUI 4 | import MapKit 5 | 6 | struct ContentView: View { 7 | @State private var region = MKCoordinateRegion( 8 | center: CLLocationCoordinate2D(latitude: 37.623198015869235, longitude: -122.43066818432008), 9 | span: MKCoordinateSpan(latitudeDelta: 0.4425100023575723, longitudeDelta: 0.28543697435880233) 10 | ) 11 | 12 | var body: some View { 13 | ZStack { 14 | Map(coordinateRegion: $region) 15 | .ignoresSafeArea() 16 | statusBarBlur 17 | } 18 | } 19 | 20 | private var statusBarBlur: some View { 21 | GeometryReader { geometry in 22 | VisualEffectBlur() 23 | .frame(height: geometry.safeAreaInsets.top) 24 | .ignoresSafeArea() 25 | } 26 | } 27 | } 28 | 29 | struct ContentView_Previews: PreviewProvider { 30 | static var previews: some View { 31 | ContentView() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Examples/Maps-SwiftUI/Maps/FloatingPanelContentView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import SwiftUI 4 | import FloatingPanel 5 | 6 | struct FloatingPanelContentView: View { 7 | @State private var searchText = "" 8 | @State private var isShowingCancelButton = false 9 | var proxy: FloatingPanelProxy 10 | 11 | var body: some View { 12 | VStack(spacing: 0) { 13 | searchBar 14 | ResultsList() 15 | .ignoresSafeArea() // Needs here with `floatingPanelScrollTracking(proxy:)` on iOS 15 16 | .floatingPanelScrollTracking(proxy: proxy) 17 | } 18 | // 👇🏻 for the floating panel grabber handle. 19 | .padding(.top, 6) 20 | .background( 21 | VisualEffectBlur(blurStyle: .systemMaterial) 22 | // ⚠️ If the `VisualEffectBlur` view receives taps, it's going 23 | // to mess up with the whole panel and render it 24 | // non-interactive, make sure it never receives any taps. 25 | .allowsHitTesting(false) 26 | ) 27 | .ignoresSafeArea() 28 | } 29 | 30 | var searchBar: some View { 31 | SearchBar( 32 | "Search for a place or address", 33 | text: $searchText, 34 | isShowingCancelButton: $isShowingCancelButton 35 | ) { isFocused in 36 | proxy.move(to: isFocused ? .full : .half, animated: true) 37 | isShowingCancelButton = isFocused 38 | } onCancel: { 39 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Examples/Maps-SwiftUI/Maps/HostingCell.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import SwiftUI 4 | 5 | /// A `UITableViewCell` that accepts a SwiftUI view as its content. 6 | /// 7 | /// Credits to https://noahgilmore.com/blog/swiftui-self-sizing-cells/ . 8 | public final class HostingCell: UITableViewCell { 9 | private let hostingController = UIHostingController( 10 | rootView: nil, 11 | ignoresKeyboard: true 12 | ) 13 | 14 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 15 | super.init(style: style, reuseIdentifier: reuseIdentifier) 16 | hostingController.view.backgroundColor = nil 17 | backgroundColor = .clear 18 | } 19 | 20 | @available(*, unavailable) 21 | required init?(coder aDecoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | public func set(rootView: Content, parentController: UIViewController) { 26 | hostingController.rootView = rootView 27 | hostingController.view.invalidateIntrinsicContentSize() 28 | 29 | let requiresControllerMove = hostingController.parent != parentController 30 | if requiresControllerMove { 31 | parentController.addChild(hostingController) 32 | } 33 | 34 | if !contentView.subviews.contains(hostingController.view) { 35 | contentView.addSubview(hostingController.view) 36 | hostingController.view.translatesAutoresizingMaskIntoConstraints = false 37 | contentView.addConstraints([ 38 | hostingController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 39 | hostingController.view.topAnchor.constraint(equalTo: contentView.topAnchor), 40 | hostingController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 41 | hostingController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) 42 | ]) 43 | } 44 | 45 | if requiresControllerMove { 46 | hostingController.didMove(toParent: parentController) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Examples/Maps-SwiftUI/Maps/MapsApp.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import SwiftUI 4 | import FloatingPanel 5 | 6 | @main 7 | struct MapsApp: App { 8 | var body: some Scene { 9 | WindowGroup { 10 | ContentView() 11 | .floatingPanel( 12 | coordinator: MapPanelCoordinator.self 13 | ) { proxy in 14 | FloatingPanelContentView(proxy: proxy) 15 | } 16 | .floatingPanelSurfaceAppearance(.phone) 17 | .floatingPanelContentMode(.fitToBounds) 18 | .floatingPanelContentInsetAdjustmentBehavior(.never) 19 | } 20 | } 21 | func onFloatingPanelEvent(_ event: MapPanelCoordinator.Event) {} 22 | } 23 | 24 | final class MapPanelCoordinator: FloatingPanelCoordinator { 25 | enum Event {} 26 | 27 | let action: (Event) -> () 28 | let proxy: FloatingPanelProxy 29 | 30 | private lazy var delegate: FloatingPanelControllerDelegate? = self 31 | 32 | init(action: @escaping (Event) -> ()) { 33 | self.action = action 34 | self.proxy = .init(controller: FloatingPanelController()) 35 | } 36 | 37 | public func setupFloatingPanel( 38 | mainHostingController: UIHostingController
, 39 | contentHostingController: UIHostingController 40 | ) { 41 | mainHostingController.ignoresKeyboardSafeArea() 42 | contentHostingController.ignoresKeyboardSafeArea() 43 | 44 | if #available(iOS 16, *) { 45 | // Set the delegate object 46 | controller.delegate = delegate 47 | 48 | // Set up the content 49 | contentHostingController.view.backgroundColor = nil 50 | controller.set(contentViewController: contentHostingController) 51 | 52 | // Show the panel 53 | controller.addPanel(toParent: mainHostingController, animated: false) 54 | } else { 55 | // NOTE: Fix floating panel content view constraints (#549) 56 | // This issue happens on iOS 15 or earlier. 57 | 58 | // Set the delegate object 59 | controller.delegate = delegate 60 | 61 | // Set up the content 62 | contentHostingController.view.backgroundColor = nil 63 | let contentWrapperViewController = UIViewController() 64 | contentWrapperViewController.view.addSubview(contentHostingController.view) 65 | contentWrapperViewController.addChild(contentHostingController) 66 | contentHostingController.didMove(toParent: contentWrapperViewController) 67 | controller.set(contentViewController: contentWrapperViewController) 68 | 69 | // Show the panel 70 | controller.addPanel(toParent: mainHostingController, animated: false) 71 | 72 | contentHostingController.view.translatesAutoresizingMaskIntoConstraints = false 73 | let bottomConstraint = contentHostingController.view.bottomAnchor.constraint( 74 | equalTo: contentWrapperViewController.view.bottomAnchor 75 | ) 76 | bottomConstraint.priority = .defaultHigh 77 | NSLayoutConstraint.activate([ 78 | contentHostingController.view.topAnchor.constraint( 79 | equalTo: contentWrapperViewController.view.topAnchor 80 | ), 81 | contentHostingController.view.leadingAnchor.constraint( 82 | equalTo: contentWrapperViewController.view.leadingAnchor 83 | ), 84 | contentHostingController.view.trailingAnchor.constraint( 85 | equalTo: contentWrapperViewController.view.trailingAnchor 86 | ), 87 | bottomConstraint 88 | ]) 89 | } 90 | } 91 | 92 | func onUpdate( 93 | context: UIViewControllerRepresentableContext 94 | ) where Representable: UIViewControllerRepresentable {} 95 | } 96 | 97 | extension MapPanelCoordinator: FloatingPanelControllerDelegate { 98 | func floatingPanelWillBeginAttracting( 99 | _ fpc: FloatingPanelController, 100 | to state: FloatingPanelState 101 | ) { 102 | if fpc.state == .full { 103 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Examples/Maps-SwiftUI/Maps/Representable/SearchBar.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import SwiftUI 4 | 5 | /// UIKit's `UISearchBar`brought to SwiftUI. 6 | public struct SearchBar: UIViewRepresentable { 7 | var title: String 8 | @Binding var text: String 9 | @Binding var isShowingCancelButton: Bool 10 | var onEditingChanged: (Bool) -> Void 11 | var onCancel: () -> Void 12 | 13 | public init( 14 | _ title: String = "", 15 | text: Binding, 16 | isShowingCancelButton: Binding, 17 | onEditingChanged: @escaping (Bool) -> Void = { _ in }, 18 | onCancel: @escaping () -> Void 19 | ) { 20 | self.title = title 21 | self._text = text 22 | self._isShowingCancelButton = isShowingCancelButton 23 | self.onEditingChanged = onEditingChanged 24 | self.onCancel = onCancel 25 | } 26 | 27 | public func makeUIView(context: UIViewRepresentableContext) -> UISearchBar { 28 | let searchBar = UISearchBar(frame: .zero) 29 | searchBar.searchBarStyle = .minimal 30 | searchBar.isTranslucent = true 31 | searchBar.placeholder = title 32 | searchBar.delegate = context.coordinator 33 | searchBar.autocapitalizationType = .none 34 | return searchBar 35 | } 36 | 37 | public func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext) { 38 | uiView.text = text 39 | uiView.placeholder = title 40 | uiView.setShowsCancelButton(isShowingCancelButton, animated: true) 41 | } 42 | 43 | public func makeCoordinator() -> SearchBar.Coordinator { 44 | Coordinator(parent: self) 45 | } 46 | 47 | public class Coordinator: NSObject, UISearchBarDelegate { 48 | var parent: SearchBar 49 | 50 | init(parent: SearchBar) { 51 | self.parent = parent 52 | } 53 | 54 | public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { 55 | parent.text = searchText 56 | } 57 | 58 | public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { 59 | parent.onEditingChanged(true) 60 | } 61 | 62 | public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 63 | searchBar.resignFirstResponder() 64 | } 65 | 66 | public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { 67 | parent.onEditingChanged(false) 68 | } 69 | 70 | public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 71 | parent.onCancel() 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Examples/Maps-SwiftUI/Maps/Representable/VisualEffectBlur.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import SwiftUI 4 | 5 | @available(iOS, introduced: 13, deprecated: 15, message: "Use iOS 15 material API.") 6 | public struct VisualEffectBlur: UIViewRepresentable { 7 | var blurStyle: UIBlurEffect.Style = .systemMaterial 8 | var vibrancyStyle: UIVibrancyEffectStyle? = nil 9 | @ViewBuilder var content: Content 10 | 11 | public func makeUIView(context: Context) -> UIVisualEffectView { 12 | context.coordinator.blurView 13 | } 14 | 15 | public func updateUIView(_ view: UIVisualEffectView, context: Context) { 16 | context.coordinator.update( 17 | content: content, 18 | blurStyle: blurStyle, 19 | vibrancyStyle: vibrancyStyle 20 | ) 21 | } 22 | 23 | public func makeCoordinator() -> Coordinator { 24 | Coordinator(content: content) 25 | } 26 | 27 | public class Coordinator { 28 | let blurView = UIVisualEffectView() 29 | let vibrancyView = UIVisualEffectView() 30 | let hostingController: UIHostingController 31 | 32 | init(content: Content) { 33 | hostingController = UIHostingController(rootView: content) 34 | hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 35 | hostingController.view.backgroundColor = nil 36 | blurView.contentView.addSubview(vibrancyView) 37 | 38 | blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 39 | vibrancyView.contentView.addSubview(hostingController.view) 40 | vibrancyView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 41 | } 42 | 43 | func update(content: Content, blurStyle: UIBlurEffect.Style, vibrancyStyle: UIVibrancyEffectStyle?) { 44 | hostingController.rootView = content 45 | 46 | let blurEffect = UIBlurEffect(style: blurStyle) 47 | blurView.effect = blurEffect 48 | 49 | if let vibrancyStyle = vibrancyStyle { 50 | vibrancyView.effect = UIVibrancyEffect(blurEffect: blurEffect, style: vibrancyStyle) 51 | } else { 52 | vibrancyView.effect = nil 53 | } 54 | 55 | hostingController.view.setNeedsDisplay() 56 | } 57 | } 58 | } 59 | 60 | public extension VisualEffectBlur where Content == EmptyView { 61 | init( 62 | blurStyle: UIBlurEffect.Style = .systemMaterial, 63 | vibrancyStyle: UIVibrancyEffectStyle? = nil 64 | ) { 65 | self.init(blurStyle: blurStyle, vibrancyStyle: vibrancyStyle) { 66 | EmptyView() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Examples/Maps-SwiftUI/Maps/ResultsList.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import SwiftUI 4 | 5 | struct ResultsList: UIViewControllerRepresentable { 6 | func makeUIViewController( 7 | context: Context 8 | ) -> ResultsTableViewController { 9 | ResultsTableViewController() 10 | } 11 | 12 | func updateUIViewController( 13 | _ uiViewController: ResultsTableViewController, 14 | context: Context 15 | ) {} 16 | } 17 | 18 | final class ResultsTableViewController: UITableViewController { 19 | private let reuseIdentifier = "HostingCell" 20 | 21 | private enum Section: CaseIterable { 22 | case main 23 | } 24 | 25 | private struct TableViewItem: Hashable { 26 | let color: Color 27 | let symbolName: String 28 | let title: String 29 | let description: String 30 | } 31 | 32 | private var dataSource: UITableViewDiffableDataSource? 33 | 34 | // MARK: Lifecycle 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | tableView.backgroundColor = nil 39 | tableView.register(HostingCell.self, forCellReuseIdentifier: reuseIdentifier) 40 | configureDataSource() 41 | } 42 | 43 | // MARK: UITableViewDataSource 44 | 45 | private func configureDataSource() { 46 | dataSource = UITableViewDiffableDataSource 47 | (tableView: tableView) { [weak self] tableView, _, tableItem -> UITableViewCell? in 48 | self?.tableView(tableView, cellForTableViewItem: tableItem) 49 | } 50 | tableView.dataSource = dataSource 51 | 52 | var snapshot = NSDiffableDataSourceSnapshot() 53 | snapshot.appendSections([.main]) 54 | let results: [TableViewItem] = (1...100).map { 55 | TableViewItem( 56 | color: Color(red: 255 / 255.0, green: 94 / 255.0 , blue: 94 / 255.0), 57 | symbolName: "heart.fill", 58 | title: "Favorites", 59 | description: "\($0) Places" 60 | ) 61 | } 62 | snapshot.appendItems(results, toSection: .main) 63 | dataSource?.apply(snapshot, animatingDifferences: false) 64 | } 65 | 66 | private func tableView( 67 | _ tableView: UITableView, 68 | cellForTableViewItem tableViewItem: TableViewItem 69 | ) -> UITableViewCell { 70 | let cell: HostingCell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) as! HostingCell 71 | setupResultTableViewCell( 72 | cell, 73 | color: tableViewItem.color, 74 | symbolName: tableViewItem.symbolName, 75 | title: tableViewItem.title, 76 | description: tableViewItem.description 77 | ) 78 | return cell 79 | } 80 | 81 | private func setupResultTableViewCell( 82 | _ cell: HostingCell, 83 | color: Color, 84 | symbolName: String, 85 | title: String, 86 | description: String 87 | ) { 88 | cell.set( 89 | rootView: ResultListCell( 90 | color: color, 91 | symbolName: symbolName, 92 | title: title, 93 | description: description 94 | ), 95 | parentController: self 96 | ) 97 | } 98 | 99 | // MARK: UITableViewDelegate 100 | 101 | override func tableView( 102 | _ tableView: UITableView, 103 | didSelectRowAt indexPath: IndexPath 104 | ) { 105 | tableView.deselectRow(at: indexPath, animated: true) 106 | } 107 | } 108 | 109 | struct ResultListCell: View { 110 | let color: Color 111 | let symbolName: String 112 | let title: String 113 | let description: String 114 | 115 | var body: some View { 116 | HStack { 117 | Image(systemName: symbolName) 118 | .foregroundColor(.white) 119 | .font(.headline) 120 | .padding(8) 121 | .background(Circle().fill(color)) 122 | VStack(alignment: .leading, spacing: 8) { 123 | Text(title) 124 | .font(.system(size: 20, weight: .bold)) 125 | .frame(maxWidth: .infinity, alignment: .leading) 126 | Text(description) 127 | .font(.system(size: 13)) 128 | .foregroundColor(Color(.secondaryLabel)) 129 | } 130 | } 131 | .padding() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Examples/Maps-SwiftUI/Maps/SearchPanelPhoneDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import FloatingPanel 4 | import UIKit 5 | 6 | final class SearchPanelPhoneDelegate: FloatingPanelControllerDelegate { 7 | func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) { 8 | if vc.state == .full { 9 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Examples/Maps-SwiftUI/Maps/SurfaceAppearance+phone.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import FloatingPanel 4 | 5 | extension FloatingPanel.SurfaceAppearance { 6 | static var phone: SurfaceAppearance { 7 | let appearance = SurfaceAppearance() 8 | appearance.cornerCurve = .continuous 9 | appearance.cornerRadius = 8.0 10 | appearance.backgroundColor = .clear 11 | return appearance 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/Maps-SwiftUI/Maps/UIHostingController+ignoreKeyboard.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import SwiftUI 4 | 5 | /// This extension makes sure SwiftUI views are not affected by iOS keyboard. 6 | /// 7 | /// Credits to https://steipete.me/posts/disabling-keyboard-avoidance-in-swiftui-uihostingcontroller/ 8 | extension UIHostingController { 9 | convenience init(rootView: Content, ignoresKeyboard: Bool) { 10 | self.init(rootView: rootView) 11 | if ignoresKeyboard { 12 | ignoresKeyboardSafeArea() 13 | } 14 | } 15 | func ignoresKeyboardSafeArea() { 16 | guard let viewClass = object_getClass(view) else { return } 17 | 18 | let viewSubclassName = String( 19 | cString: class_getName(viewClass) 20 | ).appending("_IgnoresKeyboard") 21 | 22 | if let viewSubclass = NSClassFromString(viewSubclassName) { 23 | object_setClass(view, viewSubclass) 24 | } else { 25 | guard 26 | let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String, 27 | let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) 28 | else { return } 29 | 30 | if let method = class_getInstanceMethod( 31 | viewClass, 32 | NSSelectorFromString("keyboardWillShowWithNotification:") 33 | ) { 34 | let keyboardWillShow: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in } 35 | class_addMethod( 36 | viewSubclass, 37 | NSSelectorFromString("keyboardWillShowWithNotification:"), 38 | imp_implementationWithBlock(keyboardWillShow), 39 | method_getTypeEncoding(method) 40 | ) 41 | } 42 | objc_registerClassPair(viewSubclass) 43 | object_setClass(view, viewSubclass) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Examples/Maps/Maps.xcodeproj/xcshareddata/xcschemes/Maps.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 54 | 56 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /Examples/Maps/Maps/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | var window: UIWindow? 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/food.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "food@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/food.imageset/food@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Maps/Maps/Assets.xcassets/food.imageset/food@2x.png -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/fun.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "fun@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/fun.imageset/fun@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Maps/Maps/Assets.xcassets/fun.imageset/fun@2x.png -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/like.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "like@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/like.imageset/like@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Maps/Maps/Assets.xcassets/like.imageset/like@2x.png -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/mark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "mark@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/mark.imageset/mark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Maps/Maps/Assets.xcassets/mark.imageset/mark@2x.png -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/shopping.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "shopping@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/shopping.imageset/shopping@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Maps/Maps/Assets.xcassets/shopping.imageset/shopping@2x.png -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/travel.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "travel@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Maps/Maps/Assets.xcassets/travel.imageset/travel@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Maps/Maps/Assets.xcassets/travel.imageset/travel@2x.png -------------------------------------------------------------------------------- /Examples/Maps/Maps/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Examples/Maps/Maps/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | class DetailViewController: UIViewController { 6 | var item: LocationItem? 7 | } 8 | -------------------------------------------------------------------------------- /Examples/Maps/Maps/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/Maps/Maps/Utils.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | extension Collection { 6 | subscript (safe index: Index) -> Element? { 7 | return indices.contains(index) ? self[index] : nil 8 | } 9 | } 10 | 11 | extension UIViewController { 12 | var isLandscape: Bool { 13 | if #available(iOS 13.0, *) { 14 | return view.window?.windowScene?.interfaceOrientation.isLandscape ?? false 15 | } else { 16 | return UIApplication.shared.statusBarOrientation.isLandscape 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Examples/Samples/Samples.xcodeproj/xcshareddata/xcschemes/Samples.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | var window: UIWindow? 8 | } 9 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Examples/Samples/Sources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/Assets.xcassets/IMG_0003.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_0003.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/Assets.xcassets/IMG_0003.imageset/IMG_0003.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Samples/Sources/Assets.xcassets/IMG_0003.imageset/IMG_0003.jpg -------------------------------------------------------------------------------- /Examples/Samples/Sources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/AdaptiveLayout/ImageViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | import FloatingPanel 5 | 6 | final class ImageViewController: UIViewController { 7 | class PanelLayout: FloatingPanelLayout { 8 | private unowned var targetGuide: UILayoutGuide 9 | init(targetGuide: UILayoutGuide) { 10 | self.targetGuide = targetGuide 11 | } 12 | let position: FloatingPanelPosition = .bottom 13 | let initialState: FloatingPanelState = .full 14 | var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] { 15 | return [ 16 | .full: FloatingPanelAdaptiveLayoutAnchor( 17 | absoluteOffset: 0, 18 | contentLayout: targetGuide, 19 | referenceGuide: .superview 20 | ), 21 | .half: FloatingPanelAdaptiveLayoutAnchor( 22 | fractionalOffset: 0.5, 23 | contentLayout: targetGuide, 24 | referenceGuide: .superview 25 | ) 26 | ] 27 | } 28 | } 29 | 30 | @IBOutlet weak var headerView: UIView! 31 | @IBOutlet weak var footerView: UIView! 32 | @IBOutlet weak var scrollView: UIScrollView! 33 | @IBOutlet weak var stackView: UIStackView! 34 | 35 | enum Mode { 36 | case onlyImage 37 | case withHeaderFooter 38 | } 39 | 40 | func layoutGuideFor(mode: Mode) -> UILayoutGuide { 41 | switch mode { 42 | case .onlyImage: 43 | self.headerView.isHidden = true 44 | self.footerView.isHidden = true 45 | return scrollView.contentLayoutGuide 46 | case .withHeaderFooter: 47 | self.headerView.isHidden = false 48 | self.footerView.isHidden = false 49 | let guide = UILayoutGuide() 50 | view.addLayoutGuide(guide) 51 | 52 | NSLayoutConstraint.activate([ 53 | scrollView.heightAnchor.constraint(equalTo: scrollView.contentLayoutGuide.heightAnchor), 54 | 55 | guide.topAnchor.constraint(equalTo: stackView.topAnchor), 56 | guide.leftAnchor.constraint(equalTo: stackView.leftAnchor), 57 | guide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), 58 | guide.rightAnchor.constraint(equalTo: stackView.rightAnchor), 59 | ]) 60 | return guide 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/AdaptiveLayout/TableViewControllerForAdaptiveLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | import FloatingPanel 5 | 6 | final class TableViewControllerForAdaptiveLayout: UIViewController, UITableViewDataSource, UITableViewDelegate { 7 | class PanelLayout: FloatingPanelLayout { 8 | let position: FloatingPanelPosition = .bottom 9 | let initialState: FloatingPanelState = .full 10 | 11 | private unowned var targetGuide: UILayoutGuide 12 | 13 | init(targetGuide: UILayoutGuide) { 14 | self.targetGuide = targetGuide 15 | } 16 | 17 | var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] { 18 | return [ 19 | .full: FloatingPanelAdaptiveLayoutAnchor( 20 | absoluteOffset: 0.0, 21 | contentLayout: targetGuide, 22 | referenceGuide: .superview, 23 | contentBoundingGuide: .safeArea 24 | ), 25 | .half: FloatingPanelAdaptiveLayoutAnchor( 26 | fractionalOffset: 0.5, 27 | contentLayout: targetGuide, 28 | referenceGuide: .superview, 29 | contentBoundingGuide: .safeArea 30 | ), 31 | ] 32 | } 33 | } 34 | 35 | @IBOutlet weak var tableView: IntrinsicTableView! 36 | private let cellID = "Cell" 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | tableView.rowHeight = UITableView.automaticDimension 41 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID) 42 | } 43 | 44 | // MARK: - UITableViewDataSource 45 | 46 | func numberOfSections(in tableView: UITableView) -> Int { 47 | 1 48 | } 49 | 50 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 51 | 50 52 | } 53 | 54 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 55 | let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) 56 | cell.textLabel?.text = "\(indexPath.row)" 57 | return cell 58 | } 59 | 60 | // MARK: - UITableViewDelegate 61 | 62 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 63 | let headerView = UIView() 64 | headerView.backgroundColor = .orange 65 | return headerView 66 | } 67 | 68 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 69 | 44.0 70 | } 71 | 72 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 73 | 40 74 | } 75 | } 76 | 77 | class IntrinsicTableView: UITableView { 78 | override var contentSize:CGSize { 79 | didSet { 80 | invalidateIntrinsicContentSize() 81 | } 82 | } 83 | 84 | override var intrinsicContentSize: CGSize { 85 | layoutIfNeeded() 86 | return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/DebugListCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | @available(iOS 14, *) 6 | class DebugListCollectionViewController: UIViewController { 7 | 8 | enum Section { 9 | case main 10 | } 11 | 12 | var dataSource: UICollectionViewDiffableDataSource! = nil 13 | var collectionView: UICollectionView! = nil 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | navigationItem.title = "List" 18 | configureHierarchy() 19 | configureDataSource() 20 | } 21 | } 22 | 23 | @available(iOS 14, *) 24 | extension DebugListCollectionViewController { 25 | /// - Tag: List 26 | private func createLayout() -> UICollectionViewLayout { 27 | var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) 28 | config.trailingSwipeActionsConfigurationProvider = { indexPath -> UISwipeActionsConfiguration? in 29 | return UISwipeActionsConfiguration( 30 | actions: [UIContextualAction( 31 | style: .destructive, 32 | title: "Delete", 33 | handler: { _, _, completion in 34 | // Do nothing now 35 | } 36 | )] 37 | ) 38 | } 39 | return UICollectionViewCompositionalLayout.list(using: config) 40 | } 41 | } 42 | 43 | @available(iOS 14, *) 44 | extension DebugListCollectionViewController { 45 | private func configureHierarchy() { 46 | collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) 47 | collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 48 | view.addSubview(collectionView) 49 | collectionView.delegate = self 50 | } 51 | private func configureDataSource() { 52 | 53 | let cellRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in 54 | var content = cell.defaultContentConfiguration() 55 | content.text = "\(item)" 56 | cell.contentConfiguration = content 57 | } 58 | 59 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 60 | (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in 61 | 62 | return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier) 63 | } 64 | 65 | var snapshot = NSDiffableDataSourceSnapshot() 66 | snapshot.appendSections([.main]) 67 | snapshot.appendItems(Array(0..<94)) 68 | dataSource.apply(snapshot, animatingDifferences: false) 69 | } 70 | } 71 | 72 | @available(iOS 14, *) 73 | extension DebugListCollectionViewController: UICollectionViewDelegate { 74 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 75 | collectionView.deselectItem(at: indexPath, animated: true) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/DebugTextViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | final class DebugTextViewController: UIViewController, UITextViewDelegate { 6 | @IBOutlet weak var textView: UITextView! 7 | @IBOutlet weak var textViewTopConstraint: NSLayoutConstraint! 8 | 9 | override func viewDidLoad() { 10 | super.viewDidLoad() 11 | textView.delegate = self 12 | print("viewDidLoad: TextView --- ", textView.contentOffset, textView.contentInset) 13 | 14 | textView.contentInsetAdjustmentBehavior = .never 15 | } 16 | 17 | override func viewWillLayoutSubviews() { 18 | print("viewWillLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame) 19 | } 20 | 21 | override func viewDidLayoutSubviews() { 22 | super.viewDidLayoutSubviews() 23 | print("viewDidLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame) 24 | } 25 | 26 | override func viewDidAppear(_ animated: Bool) { 27 | super.viewDidAppear(animated) 28 | print("TextView --- ", textView.contentOffset, textView.contentInset, textView.frame) 29 | } 30 | 31 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 32 | print("TextView --- ", scrollView.contentOffset, scrollView.contentInset) 33 | print("TextView --- ", scrollView.adjustedContentInset) 34 | } 35 | 36 | @IBAction func toggleTopMargin(_ sender: UISwitch) { 37 | if sender.isOn { 38 | textViewTopConstraint.constant = 160 39 | } else { 40 | textViewTopConstraint.constant = 16 41 | } 42 | } 43 | 44 | @IBAction func close(sender: UIButton) { 45 | // (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil) 46 | dismiss(animated: true, completion: nil) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | import FloatingPanel 5 | 6 | final class DetailViewController: InspectableViewController { 7 | @IBOutlet weak var modeChangeView: UIStackView! 8 | @IBOutlet weak var intrinsicHeightConstraint: NSLayoutConstraint! 9 | @IBOutlet weak var closeButton: UIButton! 10 | @IBAction func close(sender: UIButton) { 11 | // (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil) 12 | dismiss(animated: true, completion: nil) 13 | } 14 | 15 | @IBAction func buttonPressed(_ sender: UIButton) { 16 | switch sender.titleLabel?.text { 17 | case "Show": 18 | performSegue(withIdentifier: "ShowSegue", sender: self) 19 | case "Present Modally": 20 | performSegue(withIdentifier: "PresentModallySegue", sender: self) 21 | default: 22 | break 23 | } 24 | } 25 | @IBAction func modeChanged(_ sender: Any) { 26 | guard let fpc = parent as? FloatingPanelController else { return } 27 | fpc.contentMode = (fpc.contentMode == .static) ? .fitToBounds : .static 28 | } 29 | 30 | @IBAction func tapped(_ sender: Any) { 31 | print("Detail panel is tapped!") 32 | } 33 | @IBAction func swipped(_ sender: Any) { 34 | print("Detail panel is swipped!") 35 | } 36 | @IBAction func longPressed(_ sender: Any) { 37 | print("Detail panel is longPressed!") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/InspectorViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | class InspectableViewController: UIViewController { 6 | override func viewWillLayoutSubviews() { 7 | super.viewWillLayoutSubviews() 8 | print(">>> Content View: viewWillLayoutSubviews", layoutInsets) 9 | } 10 | 11 | override func viewDidLayoutSubviews() { 12 | super.viewDidLayoutSubviews() 13 | print(">>> Content View: viewDidLayoutSubviews", layoutInsets) 14 | } 15 | 16 | override func viewWillAppear(_ animated: Bool) { 17 | super.viewWillAppear(animated) 18 | print(">>> Content View: viewWillAppear", layoutInsets) 19 | } 20 | 21 | override func viewDidAppear(_ animated: Bool) { 22 | super.viewDidAppear(animated) 23 | print(">>> Content View: viewDidAppear", view.bounds, layoutInsets) 24 | } 25 | 26 | override func viewWillDisappear(_ animated: Bool) { 27 | super.viewWillDisappear(animated) 28 | print(">>> Content View: viewWillDisappear") 29 | } 30 | 31 | override func viewDidDisappear(_ animated: Bool) { 32 | super.viewDidDisappear(animated) 33 | print(">>> Content View: viewDidDisappear") 34 | } 35 | 36 | override func willMove(toParent parent: UIViewController?) { 37 | super.willMove(toParent: parent) 38 | print(">>> Content View: willMove(toParent: \(String(describing: parent))") 39 | } 40 | 41 | override func didMove(toParent parent: UIViewController?) { 42 | super.didMove(toParent: parent) 43 | print(">>> Content View: didMove(toParent: \(String(describing: parent))") 44 | } 45 | public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { 46 | print(">>> Content View: willTransition(to: \(newCollection), with: \(coordinator))", layoutInsets) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/ModalViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | import FloatingPanel 5 | 6 | final class ModalViewController: UIViewController, FloatingPanelControllerDelegate { 7 | var fpc: FloatingPanelController! 8 | var consoleVC: DebugTextViewController! 9 | 10 | @IBOutlet weak var safeAreaView: UIView! 11 | 12 | var isNewlayout: Bool = false 13 | 14 | override func viewDidLoad() { 15 | // Initialize FloatingPanelController 16 | fpc = FloatingPanelController() 17 | fpc.delegate = self 18 | 19 | let appearance = SurfaceAppearance() 20 | appearance.cornerRadius = 6.0 21 | fpc.surfaceView.appearance = appearance 22 | 23 | // Set a content view controller and track the scroll view 24 | let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController 25 | fpc.set(contentViewController: consoleVC) 26 | fpc.track(scrollView: consoleVC.textView) 27 | 28 | self.consoleVC = consoleVC 29 | 30 | // Add FloatingPanel to self.view 31 | fpc.addPanel(toParent: self, at: view.subviews.firstIndex(of: safeAreaView) ?? -1) 32 | } 33 | 34 | override func viewDidDisappear(_ animated: Bool) { 35 | super.viewDidDisappear(animated) 36 | // Remove FloatingPanel from a view 37 | fpc.removePanelFromParent(animated: false) 38 | } 39 | 40 | @IBAction func close(sender: UIButton) { 41 | dismiss(animated: true, completion: nil) 42 | } 43 | 44 | @IBAction func moveToFull(sender: UIButton) { 45 | fpc.move(to: .full, animated: true) 46 | } 47 | @IBAction func moveToHalf(sender: UIButton) { 48 | fpc.move(to: .half, animated: true) 49 | } 50 | @IBAction func moveToTip(sender: UIButton) { 51 | fpc.move(to: .tip, animated: true) 52 | } 53 | @IBAction func moveToHidden(sender: UIButton) { 54 | fpc.move(to: .hidden, animated: true) 55 | } 56 | @IBAction func updateLayout(_ sender: Any) { 57 | isNewlayout = !isNewlayout 58 | UIView.animate(withDuration: 0.5) { 59 | self.fpc.invalidateLayout() 60 | } 61 | } 62 | 63 | func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout { 64 | return (isNewlayout) ? ModalSecondLayout() : FloatingPanelBottomLayout() 65 | } 66 | 67 | class ModalSecondLayout: FloatingPanelLayout { 68 | let position: FloatingPanelPosition = .bottom 69 | let initialState: FloatingPanelState = .half 70 | let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [ 71 | .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), 72 | .half: FloatingPanelLayoutAnchor(absoluteInset: 262, edge: .top, referenceGuide: .safeArea), 73 | .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea) 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/MultiPanelController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | import FloatingPanel 5 | import WebKit 6 | 7 | final class MultiPanelController: FloatingPanelController, FloatingPanelControllerDelegate { 8 | 9 | private final class FirstPanelContentViewController: UIViewController { 10 | 11 | lazy var webView: WKWebView = WKWebView() 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | view.addSubview(webView) 16 | webView.frame = view.bounds 17 | webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 18 | webView.load(URLRequest(url: URL(string: "https://www.apple.com")!)) 19 | 20 | let vc = MultiSecondPanelController() 21 | vc.setUpContent() 22 | vc.addPanel(toParent: self) 23 | } 24 | } 25 | 26 | private final class MultiSecondPanelController: FloatingPanelController { 27 | 28 | private final class SecondPanelContentViewController: DebugTableViewController {} 29 | 30 | func setUpContent() { 31 | contentInsetAdjustmentBehavior = .never 32 | let vc = SecondPanelContentViewController() 33 | vc.loadViewIfNeeded() 34 | vc.title = "Second Panel" 35 | vc.buttonStackView.isHidden = true 36 | let navigationController = UINavigationController(rootViewController: vc) 37 | navigationController.navigationBar.barTintColor = .white 38 | navigationController.navigationBar.titleTextAttributes = [ 39 | .foregroundColor: UIColor.black 40 | ] 41 | set(contentViewController: navigationController) 42 | self.track(scrollView: vc.tableView) 43 | surfaceView.containerMargins = .init(top: 24.0, left: 0.0, bottom: layoutInsets.bottom, right: 0.0) 44 | } 45 | } 46 | 47 | override func viewDidLoad() { 48 | super.viewDidLoad() 49 | layout = FirstViewLayout() 50 | isRemovalInteractionEnabled = true 51 | 52 | let vc = FirstPanelContentViewController() 53 | set(contentViewController: vc) 54 | track(scrollView: vc.webView.scrollView) 55 | } 56 | 57 | private final class FirstViewLayout: FloatingPanelLayout { 58 | let position: FloatingPanelPosition = .bottom 59 | let initialState: FloatingPanelState = .full 60 | let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [ 61 | .full: FloatingPanelLayoutAnchor(absoluteInset: 40.0, edge: .top, referenceGuide: .superview) 62 | ] 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/NestedScrollViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | final class NestedScrollViewController: UIViewController { 6 | @IBOutlet weak var scrollView: UIScrollView! 7 | @IBOutlet weak var nestedScrollView: UIScrollView! 8 | 9 | @IBAction func longPressed(_ sender: Any) { 10 | print("LongPressed!") 11 | } 12 | @IBAction func swipped(_ sender: Any) { 13 | print("Swipped!") 14 | } 15 | @IBAction func tapped(_ sender: Any) { 16 | print("Tapped!") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | import FloatingPanel 5 | 6 | final class SettingsViewController: InspectableViewController { 7 | @IBOutlet weak var largeTitlesSwitch: UISwitch! 8 | @IBOutlet weak var translucentSwitch: UISwitch! 9 | @IBOutlet weak var versionLabel: UILabel! 10 | 11 | override func viewDidLoad() { 12 | versionLabel.text = "Version: \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "--")" 13 | } 14 | 15 | override func viewDidLayoutSubviews() { 16 | super.viewDidLayoutSubviews() 17 | let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles 18 | largeTitlesSwitch.setOn(prefersLargeTitles, animated: false) 19 | 20 | let isTranslucent = navigationController!.navigationBar.isTranslucent 21 | translucentSwitch.setOn(isTranslucent, animated: false) 22 | } 23 | 24 | @IBAction func toggleLargeTitle(_ sender: UISwitch) { 25 | navigationController?.navigationBar.prefersLargeTitles = sender.isOn 26 | } 27 | 28 | @IBAction func toggleTranslucent(_ sender: UISwitch) { 29 | // White non-translucent navigation bar, supports dark appearance 30 | if #available(iOS 15, *) { 31 | let appearance = UINavigationBarAppearance() 32 | if sender.isOn { 33 | appearance.configureWithTransparentBackground() 34 | } else { 35 | appearance.configureWithOpaqueBackground() 36 | } 37 | navigationController?.navigationBar.standardAppearance = appearance 38 | navigationController?.navigationBar.scrollEdgeAppearance = appearance 39 | } else { 40 | navigationController?.navigationBar.isTranslucent = sender.isOn 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/ContentViewControllers/UnavailableViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | class UnavailableViewController: UIViewController { 6 | weak var label: UILabel! 7 | 8 | override func viewDidLoad() { 9 | super.viewDidLoad() 10 | let label = UILabel() 11 | label.text = "Unavailable content" 12 | label.numberOfLines = 0 13 | label.textAlignment = .center 14 | label.frame = view.bounds 15 | label.autoresizingMask = [ 16 | .flexibleTopMargin, 17 | .flexibleLeftMargin, 18 | .flexibleBottomMargin, 19 | .flexibleRightMargin 20 | ] 21 | view.addSubview(label) 22 | self.label = label 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/CustomState.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import Foundation 4 | import FloatingPanel 5 | 6 | extension FloatingPanelState { 7 | static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750) 8 | static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250) 9 | } 10 | 11 | class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout { 12 | override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { 13 | return [ 14 | .full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea), 15 | .lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea), 16 | .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), 17 | .firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea), 18 | .tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea), 19 | ] 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | extension UIView { 6 | func makeBoundsLayoutGuide() -> UILayoutGuide { 7 | let guide = UILayoutGuide() 8 | addLayoutGuide(guide) 9 | NSLayoutConstraint.activate([ 10 | guide.topAnchor.constraint(equalTo: topAnchor), 11 | guide.leftAnchor.constraint(equalTo: leftAnchor), 12 | guide.bottomAnchor.constraint(equalTo: bottomAnchor), 13 | guide.rightAnchor.constraint(equalTo: rightAnchor), 14 | ]) 15 | return guide 16 | } 17 | } 18 | 19 | protocol LayoutGuideProvider { 20 | var topAnchor: NSLayoutYAxisAnchor { get } 21 | var bottomAnchor: NSLayoutYAxisAnchor { get } 22 | } 23 | extension UILayoutGuide: LayoutGuideProvider {} 24 | 25 | class CustomLayoutGuide: LayoutGuideProvider { 26 | let topAnchor: NSLayoutYAxisAnchor 27 | let bottomAnchor: NSLayoutYAxisAnchor 28 | init(topAnchor: NSLayoutYAxisAnchor, bottomAnchor: NSLayoutYAxisAnchor) { 29 | self.topAnchor = topAnchor 30 | self.bottomAnchor = bottomAnchor 31 | } 32 | } 33 | 34 | extension UIViewController { 35 | var layoutInsets: UIEdgeInsets { 36 | return view.safeAreaInsets 37 | } 38 | 39 | var layoutGuide: LayoutGuideProvider { 40 | return view.safeAreaLayoutGuide 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | import FloatingPanel 5 | 6 | final class MainViewController: UIViewController { 7 | @IBOutlet weak var tableView: UITableView! 8 | private var observations: [NSKeyValueObservation] = [] 9 | private lazy var useCaseController = UseCaseController(mainVC: self) 10 | } 11 | 12 | extension MainViewController { 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | tableView.dataSource = self 16 | tableView.delegate = self 17 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 18 | 19 | let searchController = UISearchController(searchResultsController: nil) 20 | navigationItem.searchController = searchController 21 | navigationItem.hidesSearchBarWhenScrolling = false 22 | navigationItem.largeTitleDisplayMode = .automatic 23 | var insets = UIEdgeInsets.zero 24 | insets.bottom += 69.0 25 | tableView.contentInset = insets 26 | 27 | // Show the initial panel 28 | useCaseController.set(useCase: .trackingTableView) 29 | } 30 | 31 | override func viewDidAppear(_ animated: Bool) { 32 | super.viewDidAppear(animated) 33 | if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in 34 | self.tableView.reloadData() 35 | }) { 36 | observations.append(observation) 37 | } 38 | } 39 | 40 | override func viewWillDisappear(_ animated: Bool) { 41 | super.viewWillDisappear(animated) 42 | observations.removeAll() 43 | } 44 | } 45 | 46 | extension MainViewController { 47 | @IBAction func showDebugMenu(_ sender: UIBarButtonItem) { 48 | useCaseController.setUpSettingsPanel(for: self) 49 | } 50 | } 51 | 52 | extension MainViewController: UITableViewDataSource { 53 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 54 | if navigationController?.navigationBar.prefersLargeTitles == true { 55 | return UseCase.allCases.count + 30 56 | } else { 57 | return UseCase.allCases.count 58 | } 59 | } 60 | 61 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 62 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 63 | if UseCase.allCases.count > indexPath.row { 64 | let menu = UseCase.allCases[indexPath.row] 65 | cell.textLabel?.text = menu.name 66 | } else { 67 | cell.textLabel?.text = "\(indexPath.row) row" 68 | } 69 | return cell 70 | } 71 | } 72 | 73 | extension MainViewController: UITableViewDelegate { 74 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 75 | guard UseCase.allCases.count > indexPath.row else { return } 76 | 77 | // Change panels 78 | useCaseController.set(useCase: UseCase.allCases[indexPath.row]) 79 | } 80 | 81 | @objc func dismissPresentedVC() { 82 | self.presentedViewController?.dismiss(animated: true, completion: nil) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/PanelLayouts.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | import FloatingPanel 5 | 6 | /** 7 | - Attention: `FloatingPanelLayout` must not be applied by the parent view 8 | controller of a panel. But here `MainViewController` adopts it 9 | purposely to check if the library prints an appropriate warning. 10 | */ 11 | extension MainViewController: FloatingPanelLayout { 12 | var position: FloatingPanelPosition { .bottom } 13 | var initialState: FloatingPanelState { .half } 14 | var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { 15 | return [ 16 | .full: FloatingPanelLayoutAnchor(absoluteInset: UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0, edge: .top, referenceGuide: .safeArea), 17 | .half: FloatingPanelLayoutAnchor(absoluteInset: 262.0, edge: .bottom, referenceGuide: .safeArea), 18 | .tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea) 19 | ] 20 | } 21 | } 22 | 23 | class TopPositionedPanelLayout: FloatingPanelLayout { 24 | let position: FloatingPanelPosition = .top 25 | let initialState: FloatingPanelState = .full 26 | let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [ 27 | .full: FloatingPanelLayoutAnchor(absoluteInset: 88.0, edge: .bottom, referenceGuide: .safeArea), 28 | .half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .top, referenceGuide: .safeArea), 29 | .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .top, referenceGuide: .safeArea) 30 | ] 31 | } 32 | 33 | class IntrinsicPanelLayout: FloatingPanelBottomLayout { 34 | override var initialState: FloatingPanelState { .full } 35 | override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] { 36 | return [ 37 | .full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea) 38 | ] 39 | } 40 | } 41 | 42 | class RemovablePanelLayout: FloatingPanelLayout { 43 | let position: FloatingPanelPosition = .bottom 44 | let initialState: FloatingPanelState = .half 45 | let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [ 46 | .full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea), 47 | .half: FloatingPanelLayoutAnchor(absoluteInset: 130.0, edge: .bottom, referenceGuide: .safeArea) 48 | ] 49 | 50 | func backdropAlpha(for state: FloatingPanelState) -> CGFloat { 51 | return 0.3 52 | } 53 | } 54 | 55 | class RemovablePanelLandscapeLayout: FloatingPanelLayout { 56 | let position: FloatingPanelPosition = .bottom 57 | let initialState: FloatingPanelState = .full 58 | let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [ 59 | .full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea), 60 | .half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .bottom, referenceGuide: .safeArea) 61 | ] 62 | 63 | func backdropAlpha(for state: FloatingPanelState) -> CGFloat { 64 | return 0.3 65 | } 66 | } 67 | 68 | class ModalPanelLayout: FloatingPanelLayout { 69 | let position: FloatingPanelPosition = .bottom 70 | let initialState: FloatingPanelState = .full 71 | let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [ 72 | .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea), 73 | ] 74 | 75 | func backdropAlpha(for state: FloatingPanelState) -> CGFloat { 76 | return 0.3 77 | } 78 | } 79 | 80 | class ModalPanelLayout2: FloatingPanelLayout { 81 | let position: FloatingPanelPosition = .bottom 82 | let initialState: FloatingPanelState = .half 83 | var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { 84 | [ 85 | .full: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview), 86 | .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview) 87 | ] 88 | } 89 | func backdropAlpha(for _: FloatingPanelState) -> CGFloat { 90 | 0.6 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/SupplementaryViews.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | @IBDesignable 6 | final class CloseButton: UIButton { 7 | override var isHighlighted: Bool { didSet { setNeedsDisplay() } } 8 | override var isSelected: Bool { didSet { setNeedsDisplay() } } 9 | 10 | required init?(coder aDecoder: NSCoder) { 11 | super.init(coder: aDecoder) 12 | render() 13 | } 14 | override init(frame: CGRect) { 15 | super.init(frame: frame) 16 | render() 17 | } 18 | 19 | func render() { 20 | self.backgroundColor = .clear 21 | } 22 | 23 | override func draw(_ rect: CGRect) { 24 | func p(_ p: CGFloat) -> CGFloat { 25 | return p * (2.0 / 3.0) 26 | } 27 | 28 | guard let context = UIGraphicsGetCurrentContext() else { return } 29 | 30 | context.setLineWidth(p(1.0)) 31 | 32 | let color = UIColor(displayP3Red: 0.76, 33 | green: 0.77, 34 | blue: 0.76, 35 | alpha: 1.0) 36 | 37 | context.setFillColor(color.cgColor) 38 | 39 | context.beginPath() 40 | context.addArc(center: CGPoint(x: rect.width * 0.5, 41 | y: rect.height * 0.5), 42 | radius: p(36.0) * 0.5, 43 | startAngle: 0, 44 | endAngle: CGFloat.pi * 2.0, 45 | clockwise: true) 46 | context.fillPath() 47 | 48 | let highlightedColor = UIColor(displayP3Red: 0.53, 49 | green: 0.53, 50 | blue: 0.53, 51 | alpha: 1.0) 52 | 53 | let crossColor: UIColor = isHighlighted || isSelected ? highlightedColor : .white 54 | context.setStrokeColor(crossColor.cgColor) 55 | context.setBlendMode(.normal) 56 | context.setLineWidth(p(3.5)) 57 | context.setLineCap(.round) 58 | 59 | let offset = (rect.width - p(36.0)) * 0.5 60 | 61 | context.beginPath() 62 | context.addLines(between: [CGPoint(x: offset + p(12.0), y: offset + p(12.0)), 63 | CGPoint(x: offset + p(24.0), y: offset + p(24.0))]) 64 | context.strokePath() 65 | 66 | context.beginPath() 67 | context.addLines(between: [CGPoint(x: offset + p(24.0), y: offset + p(12.0)), 68 | CGPoint(x: offset + p(12.0), y: offset + p(24.0))]) 69 | context.strokePath() 70 | } 71 | } 72 | 73 | @IBDesignable 74 | final class SafeAreaView: UIView { 75 | override func prepareForInterfaceBuilder() { 76 | let label = UILabel() 77 | label.text = "Safe Area" 78 | addSubview(label) 79 | label.translatesAutoresizingMaskIntoConstraints = false 80 | NSLayoutConstraint.activate([ 81 | label.centerXAnchor.constraint(equalTo: self.centerXAnchor), 82 | label.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -4.0), 83 | ]) 84 | } 85 | } 86 | 87 | 88 | @IBDesignable 89 | final class OnSafeAreaView: UIView { 90 | override func prepareForInterfaceBuilder() { 91 | let label = UILabel() 92 | label.text = "On Safe Area" 93 | addSubview(label) 94 | label.translatesAutoresizingMaskIntoConstraints = false 95 | NSLayoutConstraint.activate([ 96 | label.centerXAnchor.constraint(equalTo: self.centerXAnchor), 97 | label.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -4.0), 98 | ]) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Examples/Samples/Sources/UseCases/PagePanelController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | import FloatingPanel 5 | 6 | final class PagePanelController: NSObject { 7 | var pages: [UIViewController] = [] 8 | } 9 | 10 | extension PagePanelController { 11 | func makePageViewControllerForContent() -> UIPageViewController { 12 | pages = [DebugTableViewController(), DebugTableViewController(), DebugTableViewController()] 13 | let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:]) 14 | pageVC.dataSource = self 15 | pageVC.delegate = self 16 | pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil) 17 | return pageVC 18 | } 19 | 20 | func makePageViewController(for vc: MainViewController) -> UIPageViewController { 21 | pages = [UIColor.blue, .red, .green].compactMap({ (color) -> UIViewController in 22 | let page = FloatingPanelController(delegate: self) 23 | page.view.backgroundColor = color 24 | page.panGestureRecognizer.delegateProxy = self 25 | page.show() 26 | return page 27 | }) 28 | let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:]) 29 | let closeButton = UIButton(type: .custom) 30 | pageVC.view.addSubview(closeButton) 31 | closeButton.setTitle("Close", for: .normal) 32 | closeButton.translatesAutoresizingMaskIntoConstraints = false 33 | closeButton.addTarget(vc, action: #selector(MainViewController.dismissPresentedVC), for: .touchUpInside) 34 | NSLayoutConstraint.activate([ 35 | closeButton.topAnchor.constraint(equalTo: pageVC.layoutGuide.topAnchor, constant: 16.0), 36 | closeButton.leftAnchor.constraint(equalTo: pageVC.view.leftAnchor, constant: 16.0), 37 | ]) 38 | pageVC.dataSource = self 39 | pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil) 40 | pageVC.modalPresentationStyle = .fullScreen 41 | return pageVC 42 | } 43 | } 44 | 45 | extension PagePanelController: FloatingPanelControllerDelegate { 46 | func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout { 47 | return FloatingPanelBottomLayout() 48 | } 49 | } 50 | 51 | extension PagePanelController: UIGestureRecognizerDelegate { 52 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 53 | return true 54 | } 55 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { 56 | return false 57 | } 58 | } 59 | 60 | 61 | extension PagePanelController: UIPageViewControllerDataSource { 62 | func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { 63 | guard 64 | let index = pages.firstIndex(of: viewController), 65 | index + 1 < pages.count 66 | else { return nil } 67 | return pages[index + 1] 68 | } 69 | func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { 70 | guard 71 | let index = pages.firstIndex(of: viewController), 72 | index - 1 >= 0 73 | else { return nil } 74 | return pages[index - 1] 75 | } 76 | } 77 | 78 | extension PagePanelController: UIPageViewControllerDelegate { 79 | // For showPageContent 80 | func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { 81 | if completed, let page = pageViewController.viewControllers?.first { 82 | (pageViewController.parent as! FloatingPanelController).track(scrollView: (page as! DebugTableViewController).tableView) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC.xcodeproj/xcshareddata/xcschemes/SamplesObjC.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #import 4 | 5 | @interface AppDelegate : UIResponder 6 | 7 | @property (strong, nonatomic) UIWindow* window; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #import "AppDelegate.h" 4 | 5 | @interface AppDelegate () 6 | @end 7 | 8 | @implementation AppDelegate 9 | @end 10 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/MainViewController.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #import 4 | @import FloatingPanel; 5 | 6 | @interface MainViewController : UIViewController 7 | @end 8 | 9 | @interface MyFloatingPanelLayout : NSObject 10 | @end 11 | 12 | @interface MyFloatingPanelBehavior : NSObject 13 | @end 14 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/MainViewController.m: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #import "MainViewController.h" 4 | @import FloatingPanel; 5 | 6 | // Defining a custom FloatingPanelState 7 | @interface FloatingPanelState(Extended) 8 | + (FloatingPanelState *)LastQuart; 9 | @end 10 | 11 | @implementation FloatingPanelState(Extended) 12 | static FloatingPanelState *_lastQuart; 13 | + (FloatingPanelState *)LastQuart { 14 | static dispatch_once_t onceToken; 15 | dispatch_once(&onceToken, ^{ 16 | _lastQuart = [[FloatingPanelState alloc] initWithRawValue:@"lastquart" order:750]; 17 | }); 18 | return _lastQuart; 19 | } 20 | @end 21 | 22 | @interface MainViewController() 23 | @end 24 | 25 | @implementation MainViewController 26 | 27 | - (void)viewDidLoad { 28 | [super viewDidLoad]; 29 | 30 | FloatingPanelController *fpc = [[FloatingPanelController alloc] init]; 31 | [fpc setContentViewController:nil]; 32 | [fpc setDelegate:self]; 33 | 34 | [fpc setLayout: [MyFloatingPanelLayout new]]; 35 | [fpc setBehavior:[MyFloatingPanelBehavior new]]; 36 | [fpc setRemovalInteractionEnabled:NO]; 37 | 38 | [fpc addPanelToParent:self at:self.view.subviews.count animated:NO completion:nil]; 39 | [fpc moveToState:FloatingPanelState.Tip animated:true completion:nil]; 40 | 41 | [self updateAppearance: fpc]; 42 | } 43 | 44 | - (id)floatingPanel:(FloatingPanelController *)vc layoutFor:(UITraitCollection *)newCollection { 45 | FloatingPanelBottomLayout *layout = [FloatingPanelBottomLayout new]; 46 | return layout; 47 | } 48 | 49 | - (void)updateAppearance: (FloatingPanelController*)fpc 50 | { 51 | FloatingPanelSurfaceAppearance *appearance = [[FloatingPanelSurfaceAppearance alloc] init]; 52 | appearance.backgroundColor = [UIColor clearColor]; 53 | appearance.cornerRadius = 23.0; 54 | if (@available(iOS 13.0, *)) { 55 | fpc.surfaceView.containerView.layer.cornerCurve = kCACornerCurveContinuous; 56 | } 57 | FloatingPanelSurfaceAppearanceShadow *shadow = [[FloatingPanelSurfaceAppearanceShadow alloc] init]; 58 | shadow.color = [UIColor redColor]; 59 | shadow.radius = 10.0; 60 | shadow.spread = 10.0; 61 | FloatingPanelSurfaceAppearanceShadow *shadow2 = [[FloatingPanelSurfaceAppearanceShadow alloc] init]; 62 | shadow2.color = [UIColor blueColor]; 63 | shadow2.radius = 10.0; 64 | shadow2.spread = 10.0; 65 | 66 | appearance.shadows = @[shadow, shadow2]; 67 | fpc.surfaceView.appearance = appearance; 68 | } 69 | @end 70 | 71 | @implementation MyFloatingPanelLayout 72 | - (FloatingPanelState *)initialState { 73 | return FloatingPanelState.Half; 74 | } 75 | - (NSDictionary> *)anchors { 76 | return @{ 77 | FloatingPanelState.LastQuart: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.25 78 | edge:FloatingPanelReferenceEdgeTop 79 | referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea], 80 | FloatingPanelState.Half: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.5 81 | edge:FloatingPanelReferenceEdgeTop 82 | referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea], 83 | FloatingPanelState.Tip: [[FloatingPanelLayoutAnchor alloc] initWithAbsoluteInset:44.0 84 | edge:FloatingPanelReferenceEdgeBottom 85 | referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea], 86 | }; 87 | } 88 | 89 | - (enum FloatingPanelPosition)position { 90 | return FloatingPanelPositionBottom; 91 | } 92 | @end 93 | 94 | 95 | @implementation MyFloatingPanelBehavior 96 | @end 97 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/SamplesObjC-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /Examples/SamplesObjC/SamplesObjC/main.m: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #import 4 | #import "AppDelegate.h" 5 | 6 | int main(int argc, char * argv[]) { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Examples/SamplesSwiftUI/README.md: -------------------------------------------------------------------------------- 1 | # SamplesSwiftUI 2 | 3 | ## Requirements 4 | 5 | * iOS 15 or later 6 | * Xcode 16 or later 7 | -------------------------------------------------------------------------------- /Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/SamplesSwiftUI/SamplesSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/SamplesSwiftUI/SamplesSwiftUI/SampleApp.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import SwiftUI 4 | 5 | @main 6 | struct SampleApp: App { 7 | var body: some Scene { 8 | WindowGroup { 9 | MainView() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/InsideTab.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import FloatingPanel 4 | import SwiftUI 5 | 6 | struct InsideTab: View { 7 | var body: some View { 8 | if #available(iOS 18.0, *) { 9 | TabView { 10 | Tab("Main", systemImage: "lanyardcard") { 11 | MainView() 12 | } 13 | Tab("Multi Panel", systemImage: "lanyardcard") { 14 | MultiPanelView() 15 | } 16 | } 17 | } else { 18 | TabView { 19 | MainView() 20 | .tabItem { 21 | Label("Main", systemImage: "lanyardcard") 22 | } 23 | MultiPanelView() 24 | .tabItem { 25 | Label("Multi Panel 22", systemImage: "lanyardcard") 26 | } 27 | } 28 | } 29 | } 30 | } 31 | 32 | #Preview { 33 | InsideTab() 34 | } 35 | -------------------------------------------------------------------------------- /Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import FloatingPanel 4 | import SwiftUI 5 | import UIKit 6 | 7 | struct MainView: View { 8 | @State private var panelLayout: FloatingPanelLayout? = MyFloatingPanelLayout() 9 | @State private var panelState: FloatingPanelState? 10 | 11 | var body: some View { 12 | ZStack { 13 | Color.orange 14 | .ignoresSafeArea() 15 | .floatingPanel( 16 | coordinator: MyPanelCoordinator.self 17 | ) { proxy in 18 | ContentView(proxy: proxy) 19 | } 20 | .floatingPanelSurfaceAppearance(.transparent()) 21 | .floatingPanelLayout(panelLayout) 22 | .floatingPanelState($panelState) 23 | 24 | VStack(spacing: 32) { 25 | Button("Move to full") { 26 | withAnimation(.interactiveSpring) { 27 | panelState = .full 28 | } 29 | } 30 | Button { 31 | withAnimation(.interactiveSpring) { 32 | if panelLayout is MyFloatingPanelLayout { 33 | panelLayout = nil 34 | } else { 35 | panelLayout = MyFloatingPanelLayout() 36 | } 37 | } 38 | } label: { 39 | if panelLayout is MyFloatingPanelLayout { 40 | Text("Switch to Default layout") 41 | } else { 42 | Text("Switch to My layout") 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | // A custom coordinator object which handles panel context updates and setting up `FloatingPanelControllerDelegate` methods 51 | class MyPanelCoordinator: FloatingPanelCoordinator { 52 | enum Event {} 53 | 54 | let action: (Event) -> Void 55 | let proxy: FloatingPanelProxy 56 | 57 | required init(action: @escaping (MyPanelCoordinator.Event) -> Void) { 58 | self.action = action 59 | self.proxy = .init(controller: FloatingPanelController()) 60 | } 61 | 62 | func setupFloatingPanel( 63 | mainHostingController: UIHostingController
, 64 | contentHostingController: UIHostingController 65 | ) where Main: View, Content: View { 66 | // Set this as the delegate object 67 | controller.delegate = self 68 | 69 | // Set up the content 70 | contentHostingController.view.backgroundColor = .clear 71 | controller.set(contentViewController: contentHostingController) 72 | 73 | // Show the panel 74 | controller.addPanel(toParent: mainHostingController, animated: false) 75 | } 76 | 77 | func onUpdate( 78 | context: UIViewControllerRepresentableContext 79 | ) where Representable: UIViewControllerRepresentable {} 80 | } 81 | 82 | extension MyPanelCoordinator: FloatingPanelControllerDelegate { 83 | func floatingPanelDidChangeState(_ fpc: FloatingPanelController) { 84 | // NOTE: This timing is difference from one of the change of the binding value 85 | // to `floatingPanelState(_:)` modifier 86 | } 87 | } 88 | 89 | // A custom layout object 90 | class MyFloatingPanelLayout: FloatingPanelLayout { 91 | let position: FloatingPanelPosition = .bottom 92 | let initialState: FloatingPanelState = .tip 93 | let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ 94 | .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), 95 | .half: FloatingPanelLayoutAnchor(fractionalInset: 0.4, edge: .bottom, referenceGuide: .safeArea), 96 | .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea), 97 | ] 98 | } 99 | 100 | #Preview("MainView") { 101 | MainView() 102 | } 103 | -------------------------------------------------------------------------------- /Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MultiPanel.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import FloatingPanel 4 | import SwiftUI 5 | 6 | struct MultiPanelView: View { 7 | var body: some View { 8 | ZStack { 9 | Color.orange 10 | .ignoresSafeArea() 11 | .floatingPanel( 12 | coordinator: MyPanelCoordinator.self 13 | ) { proxy in 14 | ContentView(proxy: proxy) 15 | } 16 | .floatingPanelSurfaceAppearance(.transparent()) 17 | .floatingPanelContentMode(.fitToBounds) 18 | .floatingPanel( 19 | coordinator: MyPanelCoordinator.self 20 | ) { proxy in 21 | ContentView(proxy: proxy) 22 | } 23 | .floatingPanelContentMode(.static) 24 | .floatingPanelSurfaceAppearance(.transparent(cornerRadius: 24)) 25 | } 26 | } 27 | } 28 | 29 | #Preview { 30 | MultiPanelView() 31 | } 32 | -------------------------------------------------------------------------------- /Examples/SamplesSwiftUI/SamplesSwiftUI/Views/ContentView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import FloatingPanel 4 | import SwiftUI 5 | 6 | struct ContentView: View { 7 | let proxy: FloatingPanelProxy 8 | var body: some View { 9 | Group { 10 | if #available(iOS 17.0, *) { 11 | ScrollView { 12 | LazyVStack(spacing: 0) { 13 | ForEach(0...100, id: \.self) { i in 14 | Text("Index \(i)") 15 | .frame(maxWidth: .infinity, alignment: .leading) 16 | .frame(height: 60) 17 | .background(.clear) 18 | } 19 | } 20 | } 21 | .scrollClipDisabled() 22 | .floatingPanelScrollTracking(proxy: proxy) 23 | } else { 24 | ScrollView { 25 | LazyVStack(spacing: 0) { 26 | ForEach(0...100, id: \.self) { i in 27 | Text("Index \(i)") 28 | .frame(maxWidth: .infinity, alignment: .leading) 29 | .frame(height: 60) 30 | .background(.clear) 31 | } 32 | } 33 | } 34 | .floatingPanelScrollTracking(proxy: proxy) { scrollView, _ in 35 | scrollView.clipsToBounds = false 36 | } 37 | } 38 | } 39 | // Prevent revealing underlying content at the bottom of the panel when the panel is moving beyond its fully‑expanded position. 40 | .background { 41 | GeometryReader { geometry in 42 | Rectangle() 43 | .fill(.clear) 44 | .frame(height: geometry.size.height * 2) 45 | .background(.regularMaterial) 46 | } 47 | } 48 | } 49 | } 50 | 51 | #Preview("ContentView") { 52 | // `FloatingPanelProxy` can be instantiated like this. 53 | ContentView(proxy: FloatingPanelProxy(controller: FloatingPanelController())) 54 | } 55 | -------------------------------------------------------------------------------- /Examples/Stocks/Stocks.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Stocks/Stocks.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Stocks/Stocks.xcodeproj/xcshareddata/xcschemes/Stocks.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | var window: UIWindow? 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Assets.xcassets/news.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "news@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Assets.xcassets/news.imageset/news@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Stocks/Stocks/Assets.xcassets/news.imageset/news@2x.png -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Assets.xcassets/stocks_list.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "stocks_list@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Assets.xcassets/stocks_list.imageset/stocks_list@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Stocks/Stocks/Assets.xcassets/stocks_list.imageset/stocks_list@2x.png -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Assets.xcassets/top_banner.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "top_bar@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Assets.xcassets/top_banner.imageset/top_bar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Stocks/Stocks/Assets.xcassets/top_banner.imageset/top_bar@2x.png -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Assets.xcassets/yahoo_bottom_bar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "yahoo_bottom_bar@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Assets.xcassets/yahoo_bottom_bar.imageset/yahoo_bottom_bar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scenee/FloatingPanel/d5177203c08e01a7ffe07c8cccf3d19123db89d2/Examples/Stocks/Stocks/Assets.xcassets/yahoo_bottom_bar.imageset/yahoo_bottom_bar@2x.png -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIStatusBarStyle 32 | UIStatusBarStyleLightContent 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/Stocks/Stocks/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | import FloatingPanel 5 | 6 | class MainViewController: UIViewController, FloatingPanelControllerDelegate { 7 | @IBOutlet var topBannerView: UIImageView! 8 | @IBOutlet weak var labelStackView: UIStackView! 9 | @IBOutlet weak var bottomToolView: UIView! 10 | 11 | var fpc: FloatingPanelController! 12 | var newsVC: NewsViewController! 13 | 14 | var initialColor: UIColor = .black 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | initialColor = view.backgroundColor! 19 | // Initialize FloatingPanelController 20 | fpc = FloatingPanelController() 21 | fpc.delegate = self 22 | fpc.behavior = FloatingPanelStocksBehavior() 23 | 24 | // Initialize FloatingPanelController and add the view 25 | fpc.surfaceView.backgroundColor = UIColor(displayP3Red: 30.0/255.0, green: 30.0/255.0, blue: 30.0/255.0, alpha: 1.0) 26 | fpc.surfaceView.appearance.cornerRadius = 24.0 27 | fpc.surfaceView.appearance.shadows = [] 28 | fpc.surfaceView.appearance.borderWidth = 1.0 / traitCollection.displayScale 29 | fpc.surfaceView.appearance.borderColor = UIColor.black.withAlphaComponent(0.2) 30 | 31 | newsVC = storyboard?.instantiateViewController(withIdentifier: "News") as? NewsViewController 32 | 33 | // Set a content view controller 34 | fpc.set(contentViewController: newsVC) 35 | fpc.track(scrollView: newsVC.scrollView) 36 | 37 | fpc.addPanel(toParent: self, at: view.subviews.firstIndex(of: bottomToolView) ?? -1 , animated: false) 38 | 39 | topBannerView.frame = .zero 40 | topBannerView.alpha = 0.0 41 | view.addSubview(topBannerView) 42 | topBannerView.translatesAutoresizingMaskIntoConstraints = false 43 | NSLayoutConstraint.activate([ 44 | topBannerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0.0), 45 | topBannerView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0), 46 | ]) 47 | } 48 | 49 | override var preferredStatusBarStyle: UIStatusBarStyle { 50 | return .lightContent 51 | } 52 | 53 | // MARK: FloatingPanelControllerDelegate 54 | 55 | func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout { 56 | return FloatingPanelStocksLayout() 57 | } 58 | 59 | func floatingPanelDidMove(_ vc: FloatingPanelController) { 60 | if vc.isAttracting == false { 61 | let loc = vc.surfaceLocation 62 | let minY = vc.surfaceLocation(for: .full).y 63 | let maxY = vc.surfaceLocation(for: .tip).y 64 | vc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY)) 65 | } 66 | 67 | if vc.surfaceLocation.y <= vc.surfaceLocation(for: .full).y + 100 { 68 | showStockTickerBanner() 69 | } else { 70 | hideStockTickerBanner() 71 | } 72 | } 73 | 74 | private func showStockTickerBanner() { 75 | // Present top bar with dissolve animation 76 | UIView.animate(withDuration: 0.25) { 77 | self.topBannerView.alpha = 1.0 78 | self.labelStackView.alpha = 0.0 79 | self.view.backgroundColor = .black 80 | } 81 | } 82 | 83 | private func hideStockTickerBanner() { 84 | // Dismiss top bar with dissolve animation 85 | UIView.animate(withDuration: 0.25) { 86 | self.topBannerView.alpha = 0.0 87 | self.labelStackView.alpha = 1.0 88 | self.view.backgroundColor = .black 89 | } 90 | } 91 | } 92 | 93 | class NewsViewController: UIViewController { 94 | @IBOutlet weak var scrollView: UIScrollView! 95 | } 96 | 97 | 98 | // MARK: - FloatingPanelLayout 99 | 100 | class FloatingPanelStocksLayout: FloatingPanelLayout { 101 | let position: FloatingPanelPosition = .bottom 102 | let initialState: FloatingPanelState = .tip 103 | 104 | let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ 105 | .full: FloatingPanelLayoutAnchor(absoluteInset: 56.0, edge: .top, referenceGuide: .safeArea), 106 | .half: FloatingPanelLayoutAnchor(absoluteInset: 262.0, edge: .bottom, referenceGuide: .safeArea), 107 | /* Visible + ToolView */ 108 | .tip: FloatingPanelLayoutAnchor(absoluteInset: 85.0 + 44.0, edge: .bottom, referenceGuide: .safeArea), 109 | ] 110 | 111 | func backdropAlpha(for state: FloatingPanelState) -> CGFloat { 112 | return 0.0 113 | } 114 | } 115 | 116 | // MARK: - FloatingPanelBehavior 117 | 118 | class FloatingPanelStocksBehavior: FloatingPanelBehavior { 119 | let springDecelerationRate: CGFloat = UIScrollView.DecelerationRate.fast.rawValue 120 | let springResponseTime: CGFloat = 0.25 121 | } 122 | -------------------------------------------------------------------------------- /FloatingPanel.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "FloatingPanel" 4 | s.version = "3.0.0" 5 | s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface." 6 | s.description = <<-DESC 7 | FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app. 8 | The new interface displays the related contents and utilities in parallel as a user wants. 9 | DESC 10 | s.homepage = "https://github.com/scenee/FloatingPanel" 11 | s.author = "Shin Yamamoto" 12 | s.social_media_url = "https://x.com/scenee" 13 | 14 | s.platform = :ios, "13.0" 15 | s.source = { :git => "https://github.com/scenee/FloatingPanel.git", :tag => s.version.to_s } 16 | s.source_files = "Sources/*.swift" 17 | s.swift_version = '5.0' 18 | 19 | s.framework = "UIKit" 20 | 21 | s.license = { :type => "MIT", :file => "LICENSE" } 22 | end 23 | -------------------------------------------------------------------------------- /FloatingPanel.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 7 | 8 | 9 | -------------------------------------------------------------------------------- /FloatingPanel.xcodeproj/xcshareddata/xcschemes/FloatingPanel.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 80 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /FloatingPanel.xcworkspace/.xcodesamplecode.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /FloatingPanel.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 24 | 25 | 27 | 28 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /FloatingPanel.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-Present Shin Yamamoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "FloatingPanel", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "FloatingPanel", 15 | targets: ["FloatingPanel"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target(name: "FloatingPanel", path: "Sources"), 25 | ], 26 | swiftLanguageVersions: [.version("5")] 27 | ) 28 | -------------------------------------------------------------------------------- /Sources/BackdropView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | /// A view that presents a backdrop interface behind a panel. 6 | @objc(FloatingPanelBackdropView) 7 | open class BackdropView: UIView { 8 | 9 | /// The gesture recognizer for tap gestures to dismiss a panel. 10 | /// 11 | /// By default, this gesture recognizer is disabled as following the default behavior of iOS modalities. 12 | /// To dismiss a panel by tap gestures on the backdrop, `dismissalTapGestureRecognizer.isEnabled` is set to true. 13 | @objc public var dismissalTapGestureRecognizer: UITapGestureRecognizer 14 | 15 | public init() { 16 | dismissalTapGestureRecognizer = UITapGestureRecognizer() 17 | dismissalTapGestureRecognizer.isEnabled = false 18 | super.init(frame: .zero) 19 | addGestureRecognizer(dismissalTapGestureRecognizer) 20 | } 21 | 22 | required public init?(coder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/FloatingPanel.docc/FloatingPanel API Guide.md: -------------------------------------------------------------------------------- 1 | ../../Documentation/FloatingPanel API Guide.md -------------------------------------------------------------------------------- /Sources/FloatingPanel.docc/FloatingPanel SwiftUI API Guide.md: -------------------------------------------------------------------------------- 1 | ../../Documentation/FloatingPanel SwiftUI API Guide.md -------------------------------------------------------------------------------- /Sources/FloatingPanel.docc/FloatingPanel.md: -------------------------------------------------------------------------------- 1 | # ``FloatingPanel`` 2 | 3 | Create a user interface to display the related content and utilities alongside the main content. 4 | 5 | ## Overview 6 | 7 | FloatingPanel is a simple and easy-to-use UI component designed for a user interface featured in the Apple Maps, Shortcuts and Stocks app. 8 | The user interface displays related content and utilities alongside the main content. 9 | 10 | ## Topics 11 | 12 | ### API Guides 13 | 14 | - 15 | - 16 | 17 | ### Creations 18 | 19 | - ``FloatingPanelController`` 20 | - ``FloatingPanelControllerDelegate`` 21 | - ``SwiftUICore/View/floatingPanel(coordinator:onEvent:content:)`` 22 | - ``SwiftUICore/View/floatingPanelScrollTracking(proxy:onScrollViewDetected:)`` 23 | - ``FloatingPanelCoordinator`` 24 | - ``FloatingPanelDefaultCoordinator`` 25 | - ``FloatingPanelProxy`` 26 | 27 | ### Layout 28 | 29 | - ``FloatingPanelLayout`` 30 | - ``FloatingPanelBottomLayout`` 31 | - ``FloatingPanelState`` 32 | - ``FloatingPanelPosition`` 33 | - ``SwiftUICore/View/floatingPanelLayout(_:)`` 34 | - ``SwiftUICore/View/floatingPanelContentMode(_:)`` 35 | - ``SwiftUICore/View/floatingPanelContentInsetAdjustmentBehavior(_:)`` 36 | 37 | ### Behavior 38 | 39 | - ``FloatingPanelBehavior`` 40 | - ``FloatingPanelDefaultBehavior`` 41 | - ``SwiftUICore/View/floatingPanelBehavior(_:)`` 42 | 43 | ### Layout Properties 44 | 45 | - ``FloatingPanelLayoutAnchoring`` 46 | - ``FloatingPanelLayoutAnchor`` 47 | - ``FloatingPanelAdaptiveLayoutAnchor`` 48 | - ``FloatingPanelIntrinsicLayoutAnchor`` 49 | - ``FloatingPanelReferenceEdge`` 50 | - ``FloatingPanelLayoutReferenceGuide`` 51 | - ``FloatingPanelLayoutContentBoundingGuide`` 52 | 53 | ### Appearance 54 | 55 | - ``SurfaceView`` 56 | - ``SurfaceAppearance`` 57 | - ``GrabberView`` 58 | - ``BackdropView`` 59 | - ``SwiftUICore/View/floatingPanelSurfaceAppearance(_:)`` 60 | - ``SwiftUICore/View/floatingPanelGrabberHandlePadding(_:)`` 61 | 62 | ### Gesture 63 | 64 | - ``FloatingPanelPanGestureRecognizer`` 65 | 66 | -------------------------------------------------------------------------------- /Sources/FloatingPanel.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #ifndef FloatingPanel_h 4 | #define FloatingPanel_h 5 | 6 | #import 7 | 8 | FOUNDATION_EXPORT double FloatingPanelVersionNumber; 9 | FOUNDATION_EXPORT const unsigned char FloatingPanelVersionString[]; 10 | 11 | #endif /* FloatingPanel_h */ 12 | -------------------------------------------------------------------------------- /Sources/GrabberView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | /// A view that presents a grabber handle in the surface of a panel. 6 | @objc(FloatingPanelGrabberView) 7 | public class GrabberView: UIView { 8 | 9 | public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) { didSet { backgroundColor = barColor } } 10 | 11 | required public init?(coder aDecoder: NSCoder) { 12 | super.init(coder: aDecoder) 13 | } 14 | 15 | init() { 16 | super.init(frame: .zero) 17 | backgroundColor = barColor 18 | } 19 | 20 | public override func layoutSubviews() { 21 | super.layoutSubviews() 22 | render() 23 | } 24 | 25 | public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 26 | let view = super.hitTest(point, with: event) 27 | return view == self ? nil : view 28 | } 29 | 30 | private func render() { 31 | self.layer.masksToBounds = true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 3.0.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/LayoutProperties.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | /// Constants that specify the edge of the container of a panel. 6 | @objc public enum FloatingPanelReferenceEdge: Int { 7 | case top 8 | case left 9 | case bottom 10 | case right 11 | } 12 | 13 | extension FloatingPanelReferenceEdge { 14 | func inset(of insets: UIEdgeInsets) -> CGFloat { 15 | switch self { 16 | case .top: return insets.top 17 | case .left: return insets.left 18 | case .bottom: return insets.bottom 19 | case .right: return insets.right 20 | } 21 | } 22 | func mainDimension(_ size: CGSize) -> CGFloat { 23 | switch self { 24 | case .top, .bottom: return size.height 25 | case .left, .right: return size.width 26 | } 27 | } 28 | } 29 | 30 | /// A representation to specify a rectangular area to lay out a panel. 31 | @objc public enum FloatingPanelLayoutReferenceGuide: Int { 32 | case superview = 0 33 | case safeArea = 1 34 | } 35 | 36 | extension FloatingPanelLayoutReferenceGuide { 37 | func layoutGuide(vc: UIViewController) -> LayoutGuideProvider { 38 | switch self { 39 | case .safeArea: 40 | return vc.view.safeAreaLayoutGuide 41 | case .superview: 42 | return vc.view 43 | } 44 | } 45 | } 46 | 47 | /// A representation to specify a bounding box which limit the content size of a panel. 48 | @objc public enum FloatingPanelLayoutContentBoundingGuide: Int { 49 | case none = 0 50 | case superview = 1 51 | case safeArea = 2 52 | } 53 | 54 | extension FloatingPanelLayoutContentBoundingGuide { 55 | func layoutGuide(_ fpc: FloatingPanelController) -> LayoutGuideProvider? { 56 | switch self { 57 | case .superview: 58 | return fpc.view 59 | case .safeArea: 60 | return fpc.view.safeAreaLayoutGuide 61 | case .none: 62 | return nil 63 | } 64 | } 65 | func maxBounds(_ fpc: FloatingPanelController) -> CGRect? { 66 | switch self { 67 | case .superview: 68 | return fpc.view.bounds 69 | case .safeArea: 70 | return fpc.view.bounds.inset(by: fpc.fp_safeAreaInsets) 71 | case .none: 72 | return nil 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Logging.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import os.log 4 | 5 | let msg = StaticString("%{public}@") 6 | let sysLog = OSLog(subsystem: Logging.subsystem, category: Logging.category) 7 | #if FP_LOG 8 | let devLog = OSLog(subsystem: Logging.subsystem, category: "\(Logging.category):dev") 9 | #else 10 | let devLog = OSLog.disabled 11 | #endif 12 | 13 | struct Logging { 14 | static let subsystem = "com.scenee.FloatingPanel" 15 | static let category = "FloatingPanel" 16 | private init() {} 17 | } 18 | 19 | extension String.StringInterpolation { 20 | mutating func appendInterpolation(optional: T?, defaultValue: String = "nil") { 21 | switch optional { 22 | case let value?: 23 | appendLiteral(String(describing: value)) 24 | case nil: 25 | appendLiteral(defaultValue) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/PassthroughView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | @objc(FloatingPanelPassthroughView) 6 | class PassthroughView: UIView { 7 | public weak var eventForwardingView: UIView? 8 | public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 9 | let hitView = super.hitTest(point, with: event) 10 | switch hitView { 11 | case self: 12 | return eventForwardingView?.hitTest(self.convert(point, to: eventForwardingView), with: event) 13 | default: 14 | return hitView 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Position.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import UIKit 4 | 5 | /// Constants describing the position of a panel in a screen 6 | @objc public enum FloatingPanelPosition: Int { 7 | case top 8 | case left 9 | case bottom 10 | case right 11 | } 12 | 13 | extension FloatingPanelPosition { 14 | func mainLocation(_ point: CGPoint) -> CGFloat { 15 | switch self { 16 | case .top, .bottom: return point.y 17 | case .left, .right: return point.x 18 | } 19 | } 20 | 21 | func mainDimension(_ size: CGSize) -> CGFloat { 22 | switch self { 23 | case .top, .bottom: return size.height 24 | case .left, .right: return size.width 25 | } 26 | } 27 | 28 | func mainDimensionAnchor(_ layoutGuide: LayoutGuideProvider) -> NSLayoutDimension { 29 | switch self { 30 | case .top, .bottom: return layoutGuide.heightAnchor 31 | case .left, .right: return layoutGuide.widthAnchor 32 | } 33 | } 34 | 35 | func crossDimension(_ size: CGSize) -> CGFloat { 36 | switch self { 37 | case .top, .bottom: return size.width 38 | case .left, .right: return size.height 39 | } 40 | } 41 | 42 | func inset(_ insets: UIEdgeInsets) -> CGFloat { 43 | switch self { 44 | case .top: return insets.top 45 | case .left: return insets.left 46 | case .bottom: return insets.bottom 47 | case .right: return insets.right 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/State.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. 2 | 3 | import Foundation 4 | 5 | /// An object that represents the display state of a panel in a screen. 6 | @objc 7 | open class FloatingPanelState: NSObject, NSCopying, RawRepresentable { 8 | public typealias RawValue = String 9 | 10 | required public init?(rawValue: RawValue) { 11 | self.order = 0 12 | self.rawValue = rawValue 13 | super.init() 14 | } 15 | 16 | @objc 17 | public init(rawValue: RawValue, order: Int) { 18 | self.rawValue = rawValue 19 | self.order = order 20 | super.init() 21 | } 22 | 23 | /// The corresponding value of the raw type. 24 | public let rawValue: RawValue 25 | /// The sorting order for states 26 | public let order: Int 27 | 28 | public func copy(with zone: NSZone? = nil) -> Any { 29 | return self 30 | } 31 | 32 | public override var description: String { 33 | return rawValue 34 | } 35 | 36 | public override var debugDescription: String { 37 | return "" 38 | } 39 | 40 | /// A panel state indicates the entire panel is shown. 41 | @objc(Full) public static let full: FloatingPanelState = FloatingPanelState(rawValue: "full", order: 1000) 42 | /// A panel state indicates the half of a panel is shown. 43 | @objc(Half) public static let half: FloatingPanelState = FloatingPanelState(rawValue: "half", order: 500) 44 | /// A panel state indicates the tip of a panel is shown. 45 | @objc(Tip) public static let tip: FloatingPanelState = FloatingPanelState(rawValue: "tip", order: 100) 46 | /// A panel state indicates it is hidden. 47 | @objc(Hidden) public static let hidden: FloatingPanelState = FloatingPanelState(rawValue: "hidden", order: 0) 48 | } 49 | 50 | extension FloatingPanelState { 51 | func next(in states: [FloatingPanelState]) -> FloatingPanelState { 52 | if let index = states.firstIndex(of: self), states.indices.contains(index + 1) { 53 | return states[index + 1] 54 | } 55 | return self 56 | } 57 | 58 | func pre(in states: [FloatingPanelState]) -> FloatingPanelState { 59 | if let index = states.firstIndex(of: self), states.indices.contains(index - 1) { 60 | return states[index - 1] 61 | } 62 | return self 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/SwiftUI/FloatingPanelProxy.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #if canImport(SwiftUI) 4 | import SwiftUI 5 | 6 | /// A proxy for exposing and controlling the floating panel within SwiftUI views. 7 | /// 8 | /// `FloatingPanelProxy` provides a bridge between SwiftUI views and the underlying 9 | /// `FloatingPanelController`, enabling you to programmatically interact with the 10 | /// floating panel from your SwiftUI content. This proxy is automatically provided to 11 | /// the content view through the `floatingPanel()` modifier's content closure. 12 | /// 13 | /// Use this proxy to: 14 | /// - Programmatically move the panel to different positions 15 | /// - Access the underlying UIKit controller for advanced customization 16 | /// 17 | /// ```swift 18 | /// MyView() 19 | /// .floatingPanel { proxy in 20 | /// ScrollView { 21 | /// VStack { 22 | /// // Your content 23 | /// 24 | /// Button("Move To Full") { 25 | /// proxy.move(to: .full, animated: true) 26 | /// } 27 | /// } 28 | /// } 29 | /// } 30 | /// ``` 31 | @available(iOS 14, *) 32 | public struct FloatingPanelProxy { 33 | /// The associated floating panel controller. 34 | /// 35 | /// This gives direct access to the underlying `FloatingPanelController` instance, 36 | /// allowing you to use any features not directly exposed by the proxy methods. 37 | /// Use this property when you need advanced control over the panel's behavior. 38 | public let controller: FloatingPanelController 39 | 40 | public init(controller: FloatingPanelController) { 41 | self.controller = controller 42 | } 43 | 44 | /// Moves the floating panel to the specified position. 45 | /// 46 | /// Use this method to programmatically change the panel's position in response to 47 | /// user actions or application state changes. The available positions are defined 48 | /// by the current `FloatingPanelLayout` and typically include `.full`, `.half`, 49 | /// and `.tip`. 50 | /// 51 | /// ```swift 52 | /// Button("Show Full Panel") { 53 | /// proxy.move(to: .full, animated: true) 54 | /// } 55 | /// ``` 56 | /// 57 | /// You can also use this method with a completion handler to perform actions 58 | /// after the panel has finished moving: 59 | /// 60 | /// ```swift 61 | /// proxy.move(to: .full, animated: true) { 62 | /// // Code to execute after the panel reaches the full position 63 | /// self.loadDetailedData() 64 | /// } 65 | /// ``` 66 | /// 67 | /// - Parameters: 68 | /// - floatingPanelState: The state to move to (e.g., `.full`, `.half`, `.tip`). 69 | /// The available states depend on the current `FloatingPanelLayout`. 70 | /// - animated: `true` to animate the transition to the new state; `false` 71 | /// for an immediate transition without animation. 72 | /// - completion: An optional closure that will be executed after the panel 73 | /// has completed moving to the new position. 74 | public func move( 75 | to floatingPanelState: FloatingPanelState, 76 | animated: Bool, 77 | completion: (() -> Void)? = nil 78 | ) { 79 | // Need to use this method which doesn't use the custom NumericSpringingAnimator because it doesn't work with 80 | // SwiftUI animation. 81 | controller.moveForSwiftUI(to: floatingPanelState, animated: animated, completion: completion) 82 | } 83 | } 84 | 85 | @available(iOS 14, *) 86 | extension FloatingPanelProxy { 87 | /// Tracks the specified scroll view to coordinate panel and scroll movements. 88 | /// 89 | /// - Important: It is strongly recommended to use ``SwiftUICore/View/floatingPanelScrollTracking(proxy:onScrollViewDetected:)`` 90 | /// instead of this method, as it provides a more SwiftUI-friendly approach to scroll tracking. 91 | /// 92 | /// - Parameter scrollView: The scroll view to track. The panel will coordinate 93 | /// its movements with this scroll view. 94 | public func track(scrollView: UIScrollView) { 95 | controller.track(scrollView: scrollView) 96 | } 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /Sources/SwiftUI/SurfaceAppearance+.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #if canImport(SwiftUI) 4 | import SwiftUI 5 | 6 | @available(iOS 14, *) 7 | extension FloatingPanel.SurfaceAppearance { 8 | /// Creates a transparent surface appearance with customizable borders, corners, and shadows. 9 | /// 10 | /// This utility method makes it easy to create visually appealing panel surfaces with 11 | /// common styling options like borders and shadows. The surface is transparent by default, 12 | /// allowing you to add background effects through your content using SwiftUI views if needed. 13 | /// 14 | /// Example usage: 15 | /// 16 | /// ```swift 17 | /// MainView() 18 | /// .floatingPanel { _ in 19 | /// ZStack { 20 | /// // Your panel content 21 | /// VStack { 22 | /// Text("Panel Title") 23 | /// // ... 24 | /// } 25 | /// .padding() 26 | /// } 27 | /// /// A material effect background within your content 28 | /// .background { 29 | /// GeometryReader { geometry in 30 | /// Rectangle() 31 | /// .fill(.clear) 32 | /// .frame(height: geometry.size.height * 2) 33 | /// .background(.regularMaterial) 34 | /// } 35 | /// } 36 | /// } 37 | /// .floatingPanelSurfaceAppearance( 38 | /// .transparent( 39 | /// borderColor: .secondary.opacity(0.3), 40 | /// borderWidth: 1.0, 41 | /// cornerRadius: 16.0, 42 | /// shadows: [ 43 | /// .init(color: .black, radius: 10, opacity: 0.1, offset: .zero), 44 | /// .init(color: .black, radius: 3, opacity: 0.1, offset: CGSize(width: 0, height: 2)) 45 | /// ] 46 | /// ) 47 | /// ) 48 | /// ``` 49 | /// 50 | /// - Parameters: 51 | /// - borderColor: The color of the border around the panel's edges. Pass `nil` for no border. 52 | /// - borderWidth: The width of the border in points. Defaults to 0.0. 53 | /// - cornerRadius: The radius of the panel's corners in points. Defaults to 8.0. 54 | /// - shadows: An array of `Shadow` objects defining layered shadow effects. 55 | /// Defaults to a single subtle shadow. 56 | /// 57 | /// - Returns: A configured `SurfaceAppearance` instance with the specified styling. 58 | public static func transparent( 59 | borderColor: Color? = nil, 60 | borderWidth: Double = 0.0, 61 | cornerRadius: Double = 8.0, 62 | shadows: [Shadow] = [Shadow()] 63 | ) -> SurfaceAppearance { 64 | let appearance = SurfaceAppearance() 65 | appearance.backgroundColor = .clear 66 | let borderUIColor: UIColor? 67 | if let borderColor { 68 | borderUIColor = UIColor(borderColor) 69 | } else { 70 | borderUIColor = nil 71 | } 72 | appearance.borderColor = borderUIColor 73 | appearance.borderWidth = CGFloat(borderWidth) 74 | appearance.cornerCurve = .continuous 75 | appearance.cornerRadius = cornerRadius 76 | appearance.shadows = shadows 77 | return appearance 78 | } 79 | } 80 | #endif 81 | -------------------------------------------------------------------------------- /Sources/SwiftUI/View+floatingPanel.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #if canImport(SwiftUI) 4 | import SwiftUI 5 | 6 | @available(iOS 14, *) 7 | extension View { 8 | /// Overlays this view with a floating panel. 9 | /// 10 | /// This modifier is the recommended way to add a floating panel to any SwiftUI view. 11 | /// It creates a `FloatingPanelView` with the current view as the main content and 12 | /// adds your custom content to the floating panel. 13 | /// 14 | /// ```swift 15 | /// ScrollView { 16 | /// LazyVStack { 17 | /// // Main content 18 | /// ForEach(items) { item in 19 | /// ItemView(item) 20 | /// } 21 | /// } 22 | /// } 23 | /// .floatingPanel { proxy in 24 | /// // Panel content 25 | /// DetailView() 26 | /// } 27 | /// ``` 28 | /// 29 | /// You can customize the panel by using additional modifiers: 30 | /// 31 | /// ```swift 32 | /// ContentView() 33 | /// .floatingPanel { proxy in 34 | /// PanelContent() 35 | /// } 36 | /// .floatingPanelLayout(MyCustomLayout()) 37 | /// .floatingPanelBehavior(MyCustomBehavior()) 38 | /// .floatingPanelSurfaceAppearance(MySurfaceAppearance()) 39 | /// ``` 40 | /// 41 | /// - Parameters: 42 | /// - coordinator: A coordinator type that conforms to the ``FloatingPanelCoordinator`` protocol. 43 | /// Defaults to ``FloatingPanelDefaultCoordinator``. Use a custom coordinator for advanced control 44 | /// over panel behavior and events. 45 | /// - action: A closure that is called when events occur in the panel. The event type is defined 46 | /// by the coordinator's associated `Event` type. This parameter is ignored if you use the default 47 | /// coordinator, which doesn't emit events. 48 | /// - content: A closure that returns the content to display in the floating panel. 49 | /// This view builder receives a ``FloatingPanelProxy`` instance that you can use to 50 | /// interact with the panel, such as tracking scroll views or moving the panel programmatically. 51 | public func floatingPanel( 52 | coordinator: T.Type = FloatingPanelDefaultCoordinator.self, 53 | onEvent action: ((T.Event) -> Void)? = nil, 54 | @ViewBuilder content: @escaping (FloatingPanelProxy) -> some View 55 | ) -> some View { 56 | FloatingPanelView( 57 | coordinator: { T.init(action: action ?? { _ in }) }, 58 | main: { self }, 59 | content: content 60 | ) 61 | .ignoresSafeArea() 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /Sources/SwiftUI/View+floatingPanelBehavior.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #if canImport(SwiftUI) 4 | import SwiftUI 5 | 6 | @available(iOS 14, *) 7 | extension EnvironmentValues { 8 | struct BehaviorKey: EnvironmentKey { 9 | static var defaultValue: FloatingPanelBehavior = FloatingPanelDefaultBehavior() 10 | } 11 | 12 | var behavior: FloatingPanelBehavior { 13 | get { self[BehaviorKey.self] } 14 | set { self[BehaviorKey.self] = newValue } 15 | } 16 | } 17 | 18 | @available(iOS 14, *) 19 | extension View { 20 | /// Sets the behavior object controlling the interactive dynamics of floating panels within this view. 21 | /// 22 | /// The behavior object defines how the floating panel responds to user interactions, 23 | /// including: 24 | /// - Momentum and velocity effects during dragging 25 | /// - Position snapping behavior when released 26 | /// - Projection behavior after a swipe gesture 27 | /// - Interaction restrictions for certain positions 28 | /// 29 | /// By default, the panel uses `FloatingPanelDefaultBehavior`, but you can create 30 | /// your own custom behavior by implementing the `FloatingPanelBehavior` protocol: 31 | /// 32 | /// ```swift 33 | /// struct MyCustomBehavior: FloatingPanelBehavior { 34 | /// func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelState) -> Bool { 35 | /// return true 36 | /// } 37 | /// 38 | /// func momentumProjection(from initialVelocity: CGPoint) -> CGPoint { 39 | /// return CGPoint(x: 0, y: initialVelocity.y * 0.5) 40 | /// } 41 | /// } 42 | /// ``` 43 | /// 44 | /// Apply the behavior to your floating panel: 45 | /// 46 | /// ```swift 47 | /// MainView() 48 | /// .floatingPanel { _ in 49 | /// FloatingPanelContent() 50 | /// } 51 | /// .floatingPanelBehavior(MyCustomBehavior()) 52 | /// ``` 53 | /// 54 | /// - Parameter behavior: An object conforming to the `FloatingPanelBehavior` protocol 55 | /// that controls the panel's interactive dynamics, or `nil` to use the default behavior. 56 | public func floatingPanelBehavior( 57 | _ behavior: FloatingPanelBehavior? 58 | ) -> some View { 59 | environment(\.behavior, behavior ?? FloatingPanelDefaultBehavior()) 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/SwiftUI/View+floatingPanelConfiguration.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #if canImport(SwiftUI) 4 | import SwiftUI 5 | 6 | @available(iOS 14, *) 7 | extension EnvironmentValues { 8 | struct ContentInsetAdjustmentBehaviorKey: EnvironmentKey { 9 | static var defaultValue: FloatingPanelController.ContentInsetAdjustmentBehavior = .always 10 | } 11 | 12 | var contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior { 13 | get { self[ContentInsetAdjustmentBehaviorKey.self] } 14 | set { self[ContentInsetAdjustmentBehaviorKey.self] = newValue } 15 | } 16 | 17 | struct ContentModeKey: EnvironmentKey { 18 | static var defaultValue: FloatingPanelController.ContentMode = .static 19 | } 20 | 21 | var contentMode: FloatingPanelController.ContentMode { 22 | get { self[ContentModeKey.self] } 23 | set { self[ContentModeKey.self] = newValue } 24 | } 25 | } 26 | 27 | @available(iOS 14, *) 28 | extension View { 29 | /// Sets the content mode for floating panels within this view. 30 | /// 31 | /// The content mode controls how the panel's content view is sized and positioned 32 | /// when the panel's position changes. Each mode has different behavior: 33 | /// 34 | /// - `.static`: The content view maintains its current frame regardless of the 35 | /// panel's position. This is the default mode and is suitable for most use cases 36 | /// where the content should remain stable. 37 | /// 38 | /// - `.fitToBounds`: The content view is resized to fit within the panel's bounds 39 | /// at each position. This is useful when you want the content to always fill 40 | /// the available space within the panel. 41 | /// 42 | /// Example usage: 43 | /// 44 | /// ```swift 45 | /// MainView() 46 | /// .floatingPanel { _ in 47 | /// VStack { 48 | /// Text("Panel Content") 49 | /// Image("illustration") 50 | /// } 51 | /// } 52 | /// .floatingPanelContentMode(.fitToBounds) 53 | /// ``` 54 | /// 55 | /// - Parameter contentMode: The content mode to use for the floating panel. 56 | public func floatingPanelContentMode( 57 | _ contentMode: FloatingPanelController.ContentMode 58 | ) -> some View { 59 | environment(\.contentMode, contentMode) 60 | } 61 | 62 | /// Sets the content inset adjustment behavior for floating panels within this view. 63 | /// 64 | /// This modifier controls how the panel adjusts its content insets in relation to 65 | /// the safe area and the panel's position. This is particularly important for 66 | /// scrollable content within the panel. 67 | /// 68 | /// Available behaviors: 69 | /// 70 | /// - `.always`: Always adjust content insets to account for safe areas and panel 71 | /// position. This ensures content is properly inset beneath system bars and panel 72 | /// elements like the grabber handle. This is the default and recommended for most cases. 73 | /// 74 | /// - `.never`: Never adjust content insets. Content will extend to the edges of the 75 | /// panel regardless of safe areas. Use this when you want to manually manage insets 76 | /// or create custom overlay effects. 77 | /// 78 | /// Example usage: 79 | /// 80 | /// ```swift 81 | /// MainView() 82 | /// .floatingPanel { _ in 83 | /// ScrollView { 84 | /// LazyVStack { 85 | /// ForEach(items) { item in 86 | /// ItemRow(item) 87 | /// } 88 | /// } 89 | /// } 90 | /// } 91 | /// .floatingPanelContentInsetAdjustmentBehavior(.always) 92 | /// ``` 93 | /// 94 | /// - Parameter contentInsetAdjustmentBehavior: The content inset adjustment behavior 95 | /// to use for the floating panel. 96 | public func floatingPanelContentInsetAdjustmentBehavior( 97 | _ contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior 98 | ) -> some View { 99 | environment(\.contentInsetAdjustmentBehavior, contentInsetAdjustmentBehavior) 100 | } 101 | } 102 | #endif 103 | -------------------------------------------------------------------------------- /Sources/SwiftUI/View+floatingPanelLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #if canImport(SwiftUI) 4 | import SwiftUI 5 | 6 | @available(iOS 14, *) 7 | extension EnvironmentValues { 8 | struct LayoutKey: EnvironmentKey { 9 | static var defaultValue: FloatingPanelLayout = FloatingPanelBottomLayout() 10 | } 11 | 12 | var layout: FloatingPanelLayout { 13 | get { self[LayoutKey.self] } 14 | set { self[LayoutKey.self] = newValue } 15 | } 16 | } 17 | 18 | @available(iOS 14, *) 19 | extension View { 20 | /// Sets the layout object that defines the position and dimensions of floating panels within this view. 21 | /// 22 | /// The layout object controls critical aspects of the floating panel's appearance: 23 | /// - Available positions (full, half, tip, etc.) and their insets from screen edges 24 | /// - Initial position when the panel first appears 25 | /// - Anchoring behavior and constraints 26 | /// - Layout adaptation for different size classes and device orientations 27 | /// 28 | /// FloatingPanel comes with several built-in layouts: 29 | /// - `FloatingPanelBottomLayout`: Standard bottom-anchored panel (default) 30 | /// 31 | /// You can also create custom layouts by implementing the `FloatingPanelLayout` protocol: 32 | /// 33 | /// ```swift 34 | /// struct MyCustomLayout: FloatingPanelLayout { 35 | /// var position: FloatingPanelPosition { 36 | /// return .bottom 37 | /// } 38 | /// 39 | /// var initialState: FloatingPanelState { 40 | /// return .half 41 | /// } 42 | /// 43 | /// var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { 44 | /// return [ 45 | /// .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), 46 | /// .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), 47 | /// .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea) 48 | /// ] 49 | /// } 50 | /// } 51 | /// ``` 52 | /// 53 | /// Apply the layout to your floating panel: 54 | /// 55 | /// ```swift 56 | /// MainView() 57 | /// .floatingPanel { _ in 58 | /// FloatingPanelContent() 59 | /// } 60 | /// .floatingPanelLayout(MyCustomLayout()) 61 | /// ``` 62 | /// 63 | /// - Parameter layout: An object conforming to the `FloatingPanelLayout` protocol 64 | /// that defines the panel's position and dimensions, or `nil` to use the default layout. 65 | public func floatingPanelLayout( 66 | _ layout: FloatingPanelLayout? 67 | ) -> some View { 68 | environment(\.layout, layout ?? FloatingPanelBottomLayout()) 69 | } 70 | } 71 | #endif 72 | -------------------------------------------------------------------------------- /Sources/SwiftUI/View+floatingPanelScrollTracking.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #if canImport(SwiftUI) 4 | import SwiftUI 5 | 6 | @available(iOS 14, *) 7 | extension View { 8 | /// Automatically tracks scroll views within this view for seamless integration with a floating panel. 9 | /// 10 | /// This modifier automatically detects and tracks a `UIScrollView` instance within your SwiftUI content, 11 | /// linking it with the floating panel for coordinated scrolling behavior. This is essential for 12 | /// creating a smooth user experience when a scrollable view is contained in a floating panel. 13 | /// 14 | /// Example usage: 15 | /// 16 | /// ```swift 17 | /// MainView() 18 | /// .floatingPanel { proxy in 19 | /// ScrollView { 20 | /// VStack(spacing: 20) { 21 | /// ForEach(items) { item in 22 | /// ItemRow(item) 23 | /// } 24 | /// } 25 | /// .padding() 26 | /// } 27 | /// .floatingPanelScrollTracking(proxy: proxy) 28 | /// } 29 | /// ``` 30 | /// 31 | /// For advanced customization, you can provide an onScrollViewDetected closure to access the hosting controller 32 | /// and scroll view directly: 33 | /// 34 | /// ```swift 35 | /// .floatingPanelScrollTracking(proxy: proxy) { scrollView, _ in 36 | /// // Customize scroll view behavior or appearance 37 | /// scrollView.showsVerticalScrollIndicator = false 38 | /// scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0) 39 | /// } 40 | /// ``` 41 | /// 42 | /// - Parameters: 43 | /// - proxy: The ``FloatingPanelProxy`` instance from the floating panel's content closure. 44 | /// - onScrollViewDetected: Optional closure called when a scroll view is found, allowing for additional customization. 45 | /// The closure receives the detected scroll view and its hosting view controller in that order. 46 | public func floatingPanelScrollTracking( 47 | proxy: FloatingPanelProxy, 48 | onScrollViewDetected: ((UIScrollView, UIHostingController) -> Void)? = nil 49 | ) -> some View { 50 | ScrollViewRepresentable(proxy: proxy, onScrollViewDetected: onScrollViewDetected) { self } 51 | } 52 | } 53 | 54 | @available(iOS 14, *) 55 | private struct ScrollViewRepresentable: UIViewControllerRepresentable where Content: View { 56 | let proxy: FloatingPanelProxy 57 | let onScrollViewDetected: ((UIScrollView, UIHostingController) -> Void)? 58 | @ViewBuilder 59 | let content: () -> Content 60 | 61 | func makeUIViewController(context: Context) -> ScrollViewHostingController { 62 | let vc = ScrollViewHostingController( 63 | rootView: content(), 64 | proxy: proxy, 65 | onScrollViewDetected: onScrollViewDetected 66 | ) 67 | vc.view.backgroundColor = .clear 68 | return vc 69 | } 70 | 71 | func updateUIViewController(_ uiViewController: ScrollViewHostingController, context: Context) { 72 | } 73 | 74 | class ScrollViewHostingController: UIHostingController where V: View { 75 | let proxy: FloatingPanelProxy 76 | let onScrollViewDetected: ((UIScrollView, UIHostingController) -> Void)? 77 | 78 | private weak var detectedScrollView: UIScrollView? 79 | 80 | init( 81 | rootView: V, 82 | proxy: FloatingPanelProxy, 83 | onScrollViewDetected: ((UIScrollView, UIHostingController) -> Void)? 84 | ) { 85 | self.proxy = proxy 86 | self.onScrollViewDetected = onScrollViewDetected 87 | super.init(rootView: rootView) 88 | } 89 | 90 | @MainActor @preconcurrency required dynamic init?(coder aDecoder: NSCoder) { 91 | fatalError("init(coder:) has not been implemented") 92 | } 93 | 94 | override func viewWillLayoutSubviews() { 95 | super.viewWillLayoutSubviews() 96 | if detectedScrollView == nil, 97 | let scrollView = findUIScrollView(in: self.view) 98 | { 99 | proxy.track(scrollView: scrollView) 100 | onScrollViewDetected?(scrollView, self) 101 | detectedScrollView = scrollView 102 | } 103 | } 104 | 105 | func findUIScrollView(in root: UIView?) -> UIScrollView? { 106 | guard let root = root else { return nil } 107 | var queue = ArraySlice([root]) 108 | while !queue.isEmpty { 109 | let view = queue.popFirst() 110 | if view?.isKind(of: UIScrollView.self) ?? false { 111 | return (view as? UIScrollView) 112 | } 113 | queue += view?.subviews ?? [] 114 | } 115 | return nil 116 | } 117 | } 118 | } 119 | #endif 120 | -------------------------------------------------------------------------------- /Sources/SwiftUI/View+floatingPanelState.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #if canImport(SwiftUI) 4 | import SwiftUI 5 | 6 | @available(iOS 14, *) 7 | extension EnvironmentValues { 8 | struct StateKey: EnvironmentKey { 9 | static var defaultValue: Binding = .constant(nil) 10 | } 11 | 12 | var state: Binding { 13 | get { self[StateKey.self] } 14 | set { self[StateKey.self] = newValue } 15 | } 16 | } 17 | 18 | @available(iOS 14, *) 19 | extension View { 20 | /// Sets a binding to track and control the floating panel's state. 21 | /// 22 | /// - Important: The timing of changes to this state differs from the timing of 23 | /// ``FloatingPanelController/state`` and ``FloatingPanelControllerDelegate/floatingPanelDidChangeState(_:)``. 24 | /// This state updates slightly later due to differences between UIKit animations and SwiftUI view management. 25 | /// 26 | /// This modifier provides two-way communication with the floating panel: 27 | /// - When the user interacts with the panel, the binding updates to reflect the new state 28 | /// - When you programmatically change the binding value, the panel changes or animates to the new state 29 | /// 30 | /// You can use this binding to: 31 | /// - Respond to state changes when the user interacts with the panel 32 | /// - Programmatically control the panel position with SwiftUI animations 33 | /// - Synchronize the panel state with other parts of your UI 34 | /// 35 | /// Example usage: 36 | /// 37 | /// ```swift 38 | /// struct MainView: View { 39 | /// @State private var panelState: FloatingPanelState? 40 | /// 41 | /// var body: some View { 42 | /// ZStack { 43 | /// Color.orange 44 | /// .ignoresSafeArea() 45 | /// .floatingPanel { _ in 46 | /// ContentView() 47 | /// } 48 | /// .floatingPanelState($panelState) 49 | /// 50 | /// Button("Move to full") { 51 | /// withAnimation(.interactiveSpring) { 52 | /// panelState = .full 53 | /// } 54 | /// } 55 | /// } 56 | /// } 57 | /// } 58 | /// ``` 59 | /// 60 | /// - Parameter state: A binding to a `FloatingPanelState` value that tracks and controls 61 | /// the current state of the floating panel. 62 | public func floatingPanelState( 63 | _ state: Binding 64 | ) -> some View { 65 | environment(\.state, state) 66 | } 67 | } 68 | #endif 69 | -------------------------------------------------------------------------------- /Sources/SwiftUI/View+floatingPanelSurface.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | #if canImport(SwiftUI) 4 | import SwiftUI 5 | 6 | @available(iOS 14, *) 7 | extension EnvironmentValues { 8 | struct SurfaceAppearanceKey: EnvironmentKey { 9 | static var defaultValue = SurfaceAppearance() 10 | } 11 | 12 | var surfaceAppearance: SurfaceAppearance { 13 | get { self[SurfaceAppearanceKey.self] } 14 | set { self[SurfaceAppearanceKey.self] = newValue } 15 | } 16 | 17 | struct GrabberHandlePaddingKey: EnvironmentKey { 18 | static var defaultValue: CGFloat = 6.0 19 | } 20 | 21 | var grabberHandlePadding: CGFloat { 22 | get { self[GrabberHandlePaddingKey.self] } 23 | set { self[GrabberHandlePaddingKey.self] = newValue } 24 | } 25 | } 26 | 27 | @available(iOS 14, *) 28 | extension View { 29 | /// Sets the surface appearance for floating panels within this view. 30 | /// 31 | /// This modifier allows you to fully customize the visual styling of the floating panel's 32 | /// surface, including background color, corner radius, shadows, and borders. 33 | /// 34 | /// Example using a pre-defined appearance: 35 | /// 36 | /// ```swift 37 | /// MainView() 38 | /// .floatingPanel { _ in 39 | /// FloatingPanelContent() 40 | /// } 41 | /// .floatingPanelSurfaceAppearance(.transparent) 42 | /// ``` 43 | /// 44 | /// - Parameter surfaceAppearance: The surface appearance to set for the floating panel. 45 | public func floatingPanelSurfaceAppearance( 46 | _ surfaceAppearance: SurfaceAppearance 47 | ) -> some View { 48 | environment(\.surfaceAppearance, surfaceAppearance) 49 | } 50 | 51 | /// Sets the grabber handle padding for floating panels within this view. 52 | /// 53 | /// This modifier adjusts the vertical spacing between the grabber handle, such as the visual 54 | /// indicator at the top of the bottom positioned panel that users can drag. 55 | /// 56 | /// Adjusting this value can help with: 57 | /// - Visual balance and spacing within the panel 58 | /// - Providing more space for touch interactions with the grabber 59 | /// 60 | /// ```swift 61 | /// MainView() 62 | /// .floatingPanel { _ in 63 | /// VStack(spacing: 0) { 64 | /// Text("Panel Title") 65 | /// .font(.headline) 66 | /// 67 | /// Divider() 68 | /// .padding(.vertical) 69 | /// 70 | /// // Panel content 71 | /// } 72 | /// .padding(.horizontal) 73 | /// } 74 | /// // Add more space between the grabber and content for visual balance 75 | /// .floatingPanelGrabberHandlePadding(16) 76 | /// ``` 77 | /// 78 | /// The default padding is 6.0 points. 79 | /// 80 | /// - Parameter padding: The vertical padding in points between the grabber handle 81 | /// and the panel content. 82 | public func floatingPanelGrabberHandlePadding( 83 | _ padding: CGFloat 84 | ) -> some View { 85 | environment(\.grabberHandlePadding, padding) 86 | } 87 | } 88 | #endif 89 | -------------------------------------------------------------------------------- /Tests/ExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import XCTest 4 | @testable import FloatingPanel 5 | 6 | class ExtensionTests: XCTestCase { 7 | func test_roundedByDisplayScale() { 8 | XCTAssertEqual(CGFloat(333.222).rounded(by: 3), 333.3333333333333) 9 | XCTAssertNotEqual(CGFloat(333.5).rounded(by: 3), 333.66666666666674) 10 | XCTAssertTrue(CGFloat(333.5).isEqual(to: 333.66666666666674, on: 3.0)) 11 | } 12 | 13 | func test_roundedByDisplayScale_2() { 14 | XCTAssertEqual(CGFloat(-0.16666666666674246).rounded(by: 3), 0.0) 15 | XCTAssertEqual(CGFloat(0.16666666666674246).rounded(by: 3), 0.0) 16 | 17 | XCTAssertEqual(CGFloat(-0.3333333333374246).rounded(by: 3), -0.3333333333333333) 18 | XCTAssertEqual(CGFloat(-0.3333333333074246).rounded(by: 3), -0.3333333333333333) 19 | XCTAssertEqual(CGFloat(0.33333333333374246).rounded(by: 3), 0.3333333333333333) 20 | XCTAssertEqual(CGFloat(0.33333333333074246).rounded(by: 3), 0.3333333333333333) 21 | 22 | XCTAssertEqual(CGFloat(-0.16666666666674246).rounded(by: 2), 0.0) 23 | XCTAssertEqual(CGFloat(0.16666666666674246).rounded(by: 2), 0.0) 24 | 25 | XCTAssertEqual(CGFloat(-0.16666666666674246).rounded(by: 6), -0.16666666666666666) 26 | XCTAssertEqual(CGFloat(0.16666666666674246).rounded(by: 6), 0.16666666666666666) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/StateTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import XCTest 4 | @testable import FloatingPanel 5 | 6 | class StateTests: XCTestCase { 7 | override func setUp() { } 8 | override func tearDown() { } 9 | 10 | func test_nextAndPre() { 11 | var positions: [FloatingPanelState] 12 | positions = [.full, .half, .tip, .hidden] 13 | XCTAssertEqual(FloatingPanelState.full.next(in: positions), .half) 14 | XCTAssertEqual(FloatingPanelState.full.pre(in: positions), .full) 15 | XCTAssertEqual(FloatingPanelState.hidden.next(in: positions), .hidden) 16 | XCTAssertEqual(FloatingPanelState.hidden.pre(in: positions), .tip) 17 | 18 | positions = [.full, .hidden] 19 | XCTAssertEqual(FloatingPanelState.full.next(in: positions), .hidden) 20 | XCTAssertEqual(FloatingPanelState.full.pre(in: positions), .full) 21 | XCTAssertEqual(FloatingPanelState.hidden.next(in: positions), .hidden) 22 | XCTAssertEqual(FloatingPanelState.hidden.pre(in: positions), .full) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/TestSupports.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. 2 | 3 | import Foundation 4 | @testable import FloatingPanel 5 | 6 | func waitRunLoop(secs: TimeInterval = 0) { 7 | RunLoop.main.run(until: Date(timeIntervalSinceNow: secs)) 8 | } 9 | 10 | extension FloatingPanelController { 11 | func showForTest() { 12 | loadViewIfNeeded() 13 | view.frame = CGRect(x: 0, y: 0, width: 375, height: 667) 14 | show(animated: false, completion: nil) 15 | } 16 | } 17 | 18 | class FloatingPanelTestDelegate: FloatingPanelControllerDelegate { 19 | var position: FloatingPanelState = .hidden 20 | var didMoveCallback: ((FloatingPanelController) -> Void)? 21 | func floatingPanelDidChangeState(_ vc: FloatingPanelController) { 22 | position = vc.state 23 | } 24 | func floatingPanelDidMove(_ vc: FloatingPanelController) { 25 | didMoveCallback?(vc) 26 | } 27 | } 28 | 29 | class FloatingPanelTestLayout: FloatingPanelLayout { 30 | let fullInset: CGFloat = 20.0 31 | let halfInset: CGFloat = 250.0 32 | let tipInset: CGFloat = 60.0 33 | 34 | var initialState: FloatingPanelState { 35 | return .half 36 | } 37 | var position: FloatingPanelPosition { 38 | return .bottom 39 | } 40 | var referenceGuide: FloatingPanelLayoutReferenceGuide { 41 | return .superview 42 | } 43 | var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] { 44 | return [ 45 | .full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .top, referenceGuide: referenceGuide), 46 | .half: FloatingPanelLayoutAnchor(absoluteInset: halfInset, edge: .bottom, referenceGuide: referenceGuide), 47 | .tip: FloatingPanelLayoutAnchor(absoluteInset: tipInset, edge: .bottom, referenceGuide: referenceGuide), 48 | ] 49 | } 50 | } 51 | 52 | class FloatingPanelTop2BottomTestLayout: FloatingPanelLayout { 53 | let fullInset: CGFloat = 0.0 54 | let halfInset: CGFloat = 250.0 55 | let tipInset: CGFloat = 60.0 56 | 57 | var initialState: FloatingPanelState { 58 | return .half 59 | } 60 | var position: FloatingPanelPosition { 61 | return .top 62 | } 63 | var referenceGuide: FloatingPanelLayoutReferenceGuide { 64 | return .superview 65 | } 66 | var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] { 67 | return [ 68 | .full: FloatingPanelLayoutAnchor(absoluteInset: fullInset, edge: .bottom, referenceGuide: referenceGuide), 69 | .half: FloatingPanelLayoutAnchor(absoluteInset: halfInset, edge: .top, referenceGuide: referenceGuide), 70 | .tip: FloatingPanelLayoutAnchor(absoluteInset: tipInset, edge: .top, referenceGuide: referenceGuide), 71 | ] 72 | } 73 | } 74 | 75 | class FloatingPanelTopPositionedLayout: FloatingPanelLayout { 76 | let position: FloatingPanelPosition = .top 77 | let initialState: FloatingPanelState = .full 78 | let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [ 79 | .full: FloatingPanelLayoutAnchor(absoluteInset: 88.0, edge: .bottom, referenceGuide: .safeArea), 80 | .half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .top, referenceGuide: .safeArea), 81 | .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .top, referenceGuide: .safeArea) 82 | ] 83 | } 84 | 85 | class FloatingPanelProjectableBehavior: FloatingPanelBehavior { 86 | func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool { 87 | return true 88 | } 89 | } 90 | 91 | class MockTransitionCoordinator: NSObject, UIViewControllerTransitionCoordinator { 92 | func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool { true } 93 | func animateAlongsideTransition(in view: UIView?, animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool { true } 94 | func notifyWhenInteractionEnds(_ handler: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {} 95 | func notifyWhenInteractionChanges(_ handler: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {} 96 | var isAnimated: Bool = false 97 | var presentationStyle: UIModalPresentationStyle = .fullScreen 98 | var initiallyInteractive: Bool = false 99 | var isInterruptible: Bool = false 100 | var isInteractive: Bool = false 101 | var isCancelled: Bool = false 102 | var transitionDuration: TimeInterval = 0.25 103 | var percentComplete: CGFloat = 0 104 | var completionVelocity: CGFloat = 0 105 | var completionCurve: UIView.AnimationCurve = .easeInOut 106 | func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController? { nil } 107 | func view(forKey key: UITransitionContextViewKey) -> UIView? { nil } 108 | var containerView: UIView { UIView() } 109 | var targetTransform: CGAffineTransform = .identity 110 | } 111 | 112 | --------------------------------------------------------------------------------