Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c87d6c42c9 | |||
| dfa9a77816 | |||
| afff000d8c | |||
| dd49fdea5e | |||
| a1f20cedb1 | |||
| 9592baa16c | |||
| 370e306904 | |||
| 479cce4546 | |||
| b0fd0d4427 | |||
| b6e8928b1a | |||
| 3a3d53424c | |||
| 71f419a3cd | |||
| 0e27410460 |
+30
-1
@@ -1,6 +1,27 @@
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
build-swift_5_7:
|
||||
macos:
|
||||
xcode: 13.4.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: xcodebuild -scheme FloatingPanel -workspace FloatingPanel.xcworkspace SWIFT_VERSION=5.7 clean build
|
||||
|
||||
build-swiftpm_ios15_7:
|
||||
macos:
|
||||
xcode: 13.4.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios15.7-simulator"
|
||||
- run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "arm64-apple-ios15.7-simulator"
|
||||
|
||||
test-ios15_5-iPhone_13_Pro:
|
||||
macos:
|
||||
xcode: 13.4.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=15.5,name=iPhone 13 Pro'
|
||||
test-ios14_5-iPhone_12_Pro:
|
||||
macos:
|
||||
xcode: 13.4.1
|
||||
@@ -8,7 +29,15 @@ jobs:
|
||||
- checkout
|
||||
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.5,name=iPhone 12 Pro'
|
||||
|
||||
|
||||
workflows:
|
||||
test:
|
||||
jobs:
|
||||
- test-ios14_5-iPhone_12_Pro
|
||||
- build-swift_5_7:
|
||||
name: build (5.7, 13.4.1)
|
||||
- build-swiftpm_ios15_7:
|
||||
name: swiftpm ({x86_64,arm64}-apple-ios15.5-simulator, 13.4.1)
|
||||
- test-ios14_5-iPhone_12_Pro:
|
||||
name: test (15.5, 13.4.1, iPhone 12 Pro)
|
||||
- test-ios15_5-iPhone_13_Pro:
|
||||
name: test (14.5, 13.4.1, iPhone 13 Pro)
|
||||
|
||||
+27
-29
@@ -18,6 +18,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- swift: "5"
|
||||
xcode: "16.2"
|
||||
runs-on: macos-15
|
||||
- swift: "5.10"
|
||||
xcode: "15.4"
|
||||
runs-on: macos-14
|
||||
@@ -27,15 +30,6 @@ jobs:
|
||||
- swift: "5.8"
|
||||
xcode: "14.3.1"
|
||||
runs-on: macos-13
|
||||
- swift: "5.7"
|
||||
xcode: "14.1"
|
||||
runs-on: macos-12
|
||||
- swift: "5.6"
|
||||
xcode: "13.4.1"
|
||||
runs-on: macos-12
|
||||
- swift: "5.5"
|
||||
xcode: "13.2.1"
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Building in Swift ${{ matrix.swift }}
|
||||
@@ -49,6 +43,11 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: "18.2"
|
||||
xcode: "16.2"
|
||||
sim: "iPhone 16 Pro"
|
||||
parallel: NO # Stop random test job failures
|
||||
runs-on: macos-15
|
||||
- os: "17.5"
|
||||
xcode: "15.4"
|
||||
sim: "iPhone 15 Pro"
|
||||
@@ -59,11 +58,6 @@ jobs:
|
||||
sim: "iPhone 14 Pro"
|
||||
parallel: NO # Stop random test job failures
|
||||
runs-on: macos-13
|
||||
- os: "15.5"
|
||||
xcode: "13.4.1"
|
||||
sim: "iPhone 13 Pro"
|
||||
parallel: NO # Stop random test job failures
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Testing in iOS ${{ matrix.os }}
|
||||
@@ -76,9 +70,9 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
|
||||
example:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
|
||||
DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -90,6 +84,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Building ${{ matrix.example }}
|
||||
# Need to use iphonesimulator18.1 because randomly 18.2<DVTBuildVersion 22C146> isn't available.
|
||||
run: |
|
||||
xcodebuild clean build \
|
||||
-workspace FloatingPanel.xcworkspace \
|
||||
@@ -97,22 +92,32 @@ jobs:
|
||||
-sdk iphonesimulator
|
||||
|
||||
swiftpm:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
|
||||
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
xcode: ["16.2", "15.4"]
|
||||
platform: [iphoneos, iphonesimulator]
|
||||
arch: [x86_64, arm64]
|
||||
exclude:
|
||||
- platform: iphoneos
|
||||
arch: x86_64
|
||||
include:
|
||||
# 18.2
|
||||
- platform: iphoneos
|
||||
xcode: "16.2"
|
||||
sys: "ios18.2"
|
||||
- platform: iphonesimulator
|
||||
xcode: "16.2"
|
||||
sys: "ios18.2-simulator"
|
||||
# 17.2
|
||||
- platform: iphoneos
|
||||
xcode: "15.4"
|
||||
sys: "ios17.2"
|
||||
- platform: iphonesimulator
|
||||
xcode: "15.4"
|
||||
sys: "ios17.2-simulator"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -137,13 +142,6 @@ jobs:
|
||||
- target: "arm64-apple-ios16.4-simulator"
|
||||
xcode: "14.3.1"
|
||||
runs-on: macos-13
|
||||
# 15.7
|
||||
- target: "x86_64-apple-ios15.7-simulator"
|
||||
xcode: "14.1"
|
||||
runs-on: macos-12
|
||||
- target: "arm64-apple-ios15.7-simulator"
|
||||
xcode: "14.1"
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Swift Package Manager build"
|
||||
@@ -153,12 +151,12 @@ jobs:
|
||||
-Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
|
||||
|
||||
cocoapods:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
|
||||
DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "CocoaPods: pod lib lint"
|
||||
run: pod lib lint --allow-warnings
|
||||
run: pod lib lint --allow-warnings --verbose
|
||||
- name: "CocoaPods: pod spec lint"
|
||||
run: pod spec lint --allow-warnings
|
||||
run: pod spec lint --allow-warnings --verbose
|
||||
|
||||
@@ -76,3 +76,18 @@ class ModalPanelLayout: FloatingPanelLayout {
|
||||
return 0.3
|
||||
}
|
||||
}
|
||||
|
||||
class ModalPanelLayout2: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .half
|
||||
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
[
|
||||
.full: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
|
||||
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview)
|
||||
]
|
||||
}
|
||||
func backdropAlpha(for _: FloatingPanelState) -> CGFloat {
|
||||
0.6
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ enum UseCase: Int, CaseIterable {
|
||||
case showDetail
|
||||
case showModal
|
||||
case showPanelModal
|
||||
case showPanelModal2
|
||||
case showMultiPanelModal
|
||||
case showPanelInSheetModal
|
||||
case showOnWindow
|
||||
@@ -39,6 +40,7 @@ extension UseCase {
|
||||
case .showDetail: return "Show Detail Panel"
|
||||
case .showModal: return "Show Modal"
|
||||
case .showPanelModal: return "Show Panel Modal"
|
||||
case .showPanelModal2: return "Show Panel Modal 2"
|
||||
case .showMultiPanelModal: return "Show Multi Panel Modal"
|
||||
case .showOnWindow: return "Show Panel over Window"
|
||||
case .showPanelInSheetModal: return "Show Panel in Sheet Modal"
|
||||
@@ -81,10 +83,11 @@ extension UseCase {
|
||||
case .trackingTextView: return .storyboard("ConsoleViewController") // Storyboard only
|
||||
case .showDetail: return .storyboard(String(describing: DetailViewController.self))
|
||||
case .showModal: return .storyboard(String(describing: ModalViewController.self))
|
||||
case .showPanelModal: return .viewController(DebugTableViewController())
|
||||
case .showPanelModal2: return .storyboard("ConsoleViewController")
|
||||
case .showMultiPanelModal: return .viewController(DebugTableViewController())
|
||||
case .showOnWindow: return .viewController(DebugTableViewController())
|
||||
case .showPanelInSheetModal: return .viewController(DebugTableViewController())
|
||||
case .showPanelModal: return .viewController(DebugTableViewController())
|
||||
case .showTabBar: return .storyboard(String(describing: TabBarViewController.self))
|
||||
case .showPageView: return .viewController(DebugTableViewController())
|
||||
case .showPageContentView: return .viewController(DebugTableViewController())
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
@MainActor
|
||||
final class UseCaseController: NSObject {
|
||||
unowned let mainVC: MainViewController
|
||||
private(set) var useCase: UseCase
|
||||
@@ -179,6 +178,14 @@ extension UseCaseController {
|
||||
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showPanelModal2:
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.delegate = self
|
||||
fpc.track(scrollView: (contentVC as? DebugTextViewController)!.textView)
|
||||
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showMultiPanelModal:
|
||||
let fpc = MultiPanelController()
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
@@ -203,10 +210,10 @@ extension UseCaseController {
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.delegate = self
|
||||
|
||||
let apprearance = SurfaceAppearance()
|
||||
apprearance.cornerRadius = 38.5
|
||||
apprearance.shadows = []
|
||||
fpc.surfaceView.appearance = apprearance
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 38.5
|
||||
appearance.shadows = []
|
||||
fpc.surfaceView.appearance = appearance
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
|
||||
let mvc = UIViewController()
|
||||
@@ -436,6 +443,8 @@ extension UseCaseController: FloatingPanelControllerDelegate {
|
||||
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
|
||||
case .showIntrinsicView:
|
||||
return IntrinsicPanelLayout()
|
||||
case .showPanelModal2:
|
||||
return ModalPanelLayout2()
|
||||
case .showPanelModal:
|
||||
if vc != mainPanelVC && vc != detailPanelVC {
|
||||
return ModalPanelLayout()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "2.8.4"
|
||||
s.version = "2.8.7"
|
||||
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
|
||||
s.description = <<-DESC
|
||||
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
|
||||
@@ -9,7 +9,7 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
DESC
|
||||
s.homepage = "https://github.com/scenee/FloatingPanel"
|
||||
s.author = "Shin Yamamoto"
|
||||
s.social_media_url = "https://twitter.com/scenee"
|
||||
s.social_media_url = "https://x.com/scenee"
|
||||
|
||||
s.platform = :ios, "11.0"
|
||||
s.source = { :git => "https://github.com/scenee/FloatingPanel.git", :tag => s.version.to_s }
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -475,19 +475,6 @@
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||
SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT = YES;
|
||||
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -507,7 +494,7 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -518,19 +505,6 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||
SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT = YES;
|
||||
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -657,7 +631,7 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -670,19 +644,6 @@
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST DEBUG FP_LOG";
|
||||
SWIFT_COMPILATION_MODE = singlefile;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||
SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT = YES;
|
||||
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
FloatingPanel is a simple and easy-to-use UI component designed for a user interface featured in Apple Maps, Shortcuts and Stocks app.
|
||||
The user interface displays related content and utilities alongside the main content.
|
||||
|
||||
Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.4/documentation/floatingpanel) for more details.
|
||||
Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.7/documentation/floatingpanel) for more details.
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -86,9 +86,9 @@ open class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
|
||||
|
||||
class BehaviorAdapter {
|
||||
unowned let vc: FloatingPanelController
|
||||
fileprivate var behavior: any FloatingPanelBehavior
|
||||
fileprivate var behavior: FloatingPanelBehavior
|
||||
|
||||
init(vc: FloatingPanelController, behavior: any FloatingPanelBehavior) {
|
||||
init(vc: FloatingPanelController, behavior: FloatingPanelBehavior) {
|
||||
self.vc = vc
|
||||
self.behavior = behavior
|
||||
}
|
||||
@@ -123,7 +123,7 @@ class BehaviorAdapter {
|
||||
}
|
||||
|
||||
extension FloatingPanelController {
|
||||
var _behavior: any FloatingPanelBehavior {
|
||||
var _behavior: FloatingPanelBehavior {
|
||||
get { floatingPanel.behaviorAdapter.behavior }
|
||||
set { floatingPanel.behaviorAdapter.behavior = newValue}
|
||||
}
|
||||
|
||||
+18
-21
@@ -6,15 +6,14 @@ import os.log
|
||||
/// A set of methods implemented by the delegate of a panel controller allows the adopting delegate to respond to
|
||||
/// messages from the FloatingPanelController class and thus respond to, and in some affect, operations such as
|
||||
/// dragging, attracting a panel, layout of a panel and the content, and transition animations.
|
||||
@MainActor
|
||||
@objc public protocol FloatingPanelControllerDelegate {
|
||||
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
|
||||
@objc(floatingPanel:layoutForTraitCollection:) optional
|
||||
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> any FloatingPanelLayout
|
||||
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout
|
||||
|
||||
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
|
||||
@objc(floatingPanel:layoutForSize:) optional
|
||||
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> any FloatingPanelLayout
|
||||
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout
|
||||
|
||||
/// Returns a UIViewPropertyAnimator object to add/present the panel to a position.
|
||||
///
|
||||
@@ -151,7 +150,7 @@ open class FloatingPanelController: UIViewController {
|
||||
|
||||
/// The delegate of a panel controller object.
|
||||
@objc
|
||||
public weak var delegate: (any FloatingPanelControllerDelegate)?{
|
||||
public weak var delegate: FloatingPanelControllerDelegate?{
|
||||
didSet{
|
||||
didUpdateDelegate()
|
||||
}
|
||||
@@ -199,7 +198,7 @@ open class FloatingPanelController: UIViewController {
|
||||
/// You need to call ``invalidateLayout()`` if you want to apply a new layout object into the panel
|
||||
/// immediately.
|
||||
@objc
|
||||
public var layout: any FloatingPanelLayout {
|
||||
public var layout: FloatingPanelLayout {
|
||||
get { _layout }
|
||||
set {
|
||||
_layout = newValue
|
||||
@@ -212,7 +211,7 @@ open class FloatingPanelController: UIViewController {
|
||||
|
||||
/// The behavior object that the controller manages
|
||||
@objc
|
||||
public var behavior: any FloatingPanelBehavior {
|
||||
public var behavior: FloatingPanelBehavior {
|
||||
get { _behavior }
|
||||
set {
|
||||
_behavior = newValue
|
||||
@@ -283,7 +282,7 @@ open class FloatingPanelController: UIViewController {
|
||||
|
||||
/// Initialize a newly created panel controller.
|
||||
@objc
|
||||
public init(delegate: (any FloatingPanelControllerDelegate)? = nil) {
|
||||
public init(delegate: FloatingPanelControllerDelegate? = nil) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
self.delegate = delegate
|
||||
setUp()
|
||||
@@ -295,7 +294,7 @@ open class FloatingPanelController: UIViewController {
|
||||
modalPresentationStyle = .custom
|
||||
transitioningDelegate = modalTransition
|
||||
|
||||
let initialLayout: any FloatingPanelLayout
|
||||
let initialLayout: FloatingPanelLayout
|
||||
if let layout = delegate?.floatingPanel?(self, layoutFor: traitCollection) {
|
||||
initialLayout = layout
|
||||
} else {
|
||||
@@ -345,7 +344,7 @@ open class FloatingPanelController: UIViewController {
|
||||
floatingPanel.adjustScrollContentInsetIfNeeded()
|
||||
}
|
||||
|
||||
open override func viewWillTransition(to size: CGSize, with coordinator: any UIViewControllerTransitionCoordinator) {
|
||||
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
if self.view.bounds.size == size {
|
||||
@@ -364,7 +363,7 @@ open class FloatingPanelController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
open override func willTransition(to newCollection: UITraitCollection, with coordinator: any UIViewControllerTransitionCoordinator) {
|
||||
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.willTransition(to: newCollection, with: coordinator)
|
||||
|
||||
if shouldUpdateLayout(from: traitCollection, to: newCollection) == false {
|
||||
@@ -484,18 +483,16 @@ open class FloatingPanelController: UIViewController {
|
||||
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
|
||||
// That's why it needs the observation to keep `adjustedContentInsets` correct.
|
||||
safeAreaInsetsObservation = self.view.observe(\.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (_, change) in
|
||||
MainActor.assumeIsolated {
|
||||
// Use `self.view.safeAreaInsets` because `change.newValue` can be nil in particular case when
|
||||
// is reported in https://github.com/SCENEE/FloatingPanel/issues/330
|
||||
guard let self = self, change.oldValue != self.view.safeAreaInsets else { return }
|
||||
// Use `self.view.safeAreaInsets` because `change.newValue` can be nil in particular case when
|
||||
// is reported in https://github.com/SCENEE/FloatingPanel/issues/330
|
||||
guard let self = self, change.oldValue != self.view.safeAreaInsets else { return }
|
||||
|
||||
// Sometimes the bounding rectangle of the controlled view becomes invalid when the screen is rotated.
|
||||
// This results in its safeAreaInsets change. In that case, `self.update(safeAreaInsets:)` leads
|
||||
// an unsatisfied constraints error. So this method should not be called with those bounds.
|
||||
guard self.view.bounds.height > 0 && self.view.bounds.width > 0 else { return }
|
||||
// Sometimes the bounding rectangle of the controlled view becomes invalid when the screen is rotated.
|
||||
// This results in its safeAreaInsets change. In that case, `self.update(safeAreaInsets:)` leads
|
||||
// an unsatisfied constraints error. So this method should not be called with those bounds.
|
||||
guard self.view.bounds.height > 0 && self.view.bounds.width > 0 else { return }
|
||||
|
||||
self.update(safeAreaInsets: self.view.safeAreaInsets)
|
||||
}
|
||||
self.update(safeAreaInsets: self.view.safeAreaInsets)
|
||||
}
|
||||
|
||||
move(to: floatingPanel.layoutAdapter.initialState,
|
||||
@@ -732,7 +729,7 @@ extension FloatingPanelController {
|
||||
|
||||
// MARK: - Swizzling
|
||||
|
||||
@MainActor private var originalDismissImp: IMP?
|
||||
private var originalDismissImp: IMP?
|
||||
private typealias DismissFunction = @convention(c) (AnyObject, Selector, Bool, (() -> Void)?) -> Void
|
||||
extension FloatingPanelController {
|
||||
private static let dismissSwizzling: Void = {
|
||||
|
||||
+50
-32
@@ -3,6 +3,7 @@
|
||||
import UIKit
|
||||
import os.log
|
||||
|
||||
///
|
||||
/// The presentation model of FloatingPanel
|
||||
///
|
||||
class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
@@ -70,7 +71,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
var removalVector: CGVector = .zero
|
||||
|
||||
// Scroll handling
|
||||
private var initialScrollOffset: CGPoint = .zero
|
||||
private var initialScrollOffset: CGPoint?
|
||||
private var scrollBounce = false
|
||||
private var scrollIndictorVisible = false
|
||||
private var scrollBounceThreshold: CGFloat = -30.0
|
||||
@@ -78,7 +79,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
// MARK: - Interface
|
||||
|
||||
init(_ vc: FloatingPanelController, layout: any FloatingPanelLayout, behavior: any FloatingPanelBehavior) {
|
||||
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
|
||||
ownerVC = vc
|
||||
|
||||
surfaceView = SurfaceView()
|
||||
@@ -113,7 +114,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
deinit {
|
||||
moveAnimator?.stopAnimation(false)
|
||||
// Release `NumericSpringAnimator.displayLink` from the run loop.
|
||||
self.moveAnimator?.stopAnimation(false)
|
||||
}
|
||||
|
||||
func move(to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
@@ -409,7 +411,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
if insideMostExpandedAnchor {
|
||||
// Prevent scrolling if needed
|
||||
if isScrollable(state: state) {
|
||||
if isScrollable(state: state), let initialScrollOffset = initialScrollOffset {
|
||||
if interactionInProgress {
|
||||
os_log(msg, log: devLog, type: .debug, "settle offset -- \(value(of: initialScrollOffset))")
|
||||
// Return content offset to initial offset to prevent scrolling
|
||||
@@ -427,7 +429,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if let initialScrollOffset = initialScrollOffset {
|
||||
// Return content offset to initial offset to prevent scrolling
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
@@ -469,7 +471,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
if isScrollable(state: state) {
|
||||
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
|
||||
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation) {
|
||||
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation),
|
||||
let initialScrollOffset = initialScrollOffset {
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
}
|
||||
@@ -497,7 +500,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
// Adjust a small gap of the scroll offset just before swiping down starts in the grabber area,
|
||||
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation) {
|
||||
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation),
|
||||
let initialScrollOffset = initialScrollOffset {
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
}
|
||||
@@ -606,7 +610,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
guard
|
||||
isScrollable(state: state), // When not top most(i.e. .full), don't scroll.
|
||||
interactionInProgress == false, // When interaction already in progress, don't scroll.
|
||||
0 == layoutAdapter.offset(from: state),
|
||||
abs(layoutAdapter.offset(from: state)) < 1, // Indistinguishably close to an anchor point.
|
||||
!surfaceView.grabberAreaContains(initialLocation) // When the initial point is within grabber area, don't scroll
|
||||
else {
|
||||
return false
|
||||
@@ -845,7 +849,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
} else {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
os_log(msg, log: devLog, type: .debug, "initial scroll offset -- \(initialScrollOffset)")
|
||||
os_log(msg, log: devLog, type: .debug, "initial scroll offset -- \(optional: initialScrollOffset)")
|
||||
}
|
||||
|
||||
initialTranslation = translation
|
||||
@@ -892,17 +896,6 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
func endWithoutAttraction(_ target: FloatingPanelState) {
|
||||
self.state = target
|
||||
self.updateLayout(to: target)
|
||||
self.unlockScrollView()
|
||||
// The `floatingPanelDidEndDragging(_:willAttract:)` must be called after the state property changes.
|
||||
// This allows library users to get the correct state in the delegate method.
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func startAttraction(to state: FloatingPanelState, with velocity: CGPoint, completion: @escaping (() -> Void)) {
|
||||
os_log(msg, log: devLog, type: .debug, "startAnimation to \(state) -- velocity = \(value(of: velocity))")
|
||||
guard let vc = ownerVC else { return }
|
||||
@@ -932,8 +925,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
self.backdropView.alpha = self.getBackdropAlpha(at: current, with: translation)
|
||||
|
||||
// Pin the offset of the tracking scroll view while moving by this animator
|
||||
if let scrollView = self.scrollView {
|
||||
self.stopScrolling(at: self.initialScrollOffset)
|
||||
if let scrollView = self.scrollView, let initialScrollOffset = self.initialScrollOffset {
|
||||
self.stopScrolling(at: initialScrollOffset)
|
||||
os_log(msg, log: devLog, type: .debug, "move -- pinning scroll offset = \(scrollView.contentOffset)")
|
||||
}
|
||||
|
||||
@@ -957,6 +950,12 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
self.isAttracting = false
|
||||
self.moveAnimator = nil
|
||||
|
||||
// We need to reset `initialScrollOffset` because the scroll offset can become unexpected
|
||||
// under the following circumstances:
|
||||
// 1. The scroll offset changes while the panel does not move.
|
||||
// 2. The panel is then moved using `move(to:animate:completion:)`.
|
||||
self.initialScrollOffset = nil
|
||||
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndAttracting?(vc)
|
||||
}
|
||||
@@ -979,6 +978,20 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func endWithoutAttraction(_ target: FloatingPanelState) {
|
||||
// See comments in `endAttraction`
|
||||
self.initialScrollOffset = nil
|
||||
|
||||
self.state = target
|
||||
self.updateLayout(to: target)
|
||||
self.unlockScrollView()
|
||||
// The `floatingPanelDidEndDragging(_:willAttract:)` must be called after the state property changes.
|
||||
// This allows library users to get the correct state in the delegate method.
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
|
||||
}
|
||||
}
|
||||
|
||||
func value(of point: CGPoint) -> CGFloat {
|
||||
return layoutAdapter.position.mainLocation(point)
|
||||
}
|
||||
@@ -1184,16 +1197,21 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return state == layoutAdapter.mostExpandedState
|
||||
}
|
||||
|
||||
/// Adjust content inset of the tracking scroll view if the controller's
|
||||
/// `contentInsetAdjustmentBehavior` is `.always` and its `contentMode` is `.static`.
|
||||
/// if its content is scrollable, the content might not be fully visible on `.half`
|
||||
/// state, for example. Therefore the content inset needs to adjust to display the
|
||||
/// full content.
|
||||
// Adjusts content inset of the tracking scroll view when the following conditions are met:
|
||||
// - The controller's `contentInsetAdjustmentBehavior` is `.always`
|
||||
// - Its `contentMode` is `.static`
|
||||
// - Its content is scrollable
|
||||
// This ensures that the content remains fully visible in intermediate states like `.half`,
|
||||
// by using `UIScrollView.safeAreaInsets` and the panel's current position.
|
||||
// This method must not be invoked in the fully expanded state, as it may lead to unexpected
|
||||
// behavior under the top safe area (i.e., the status bar).
|
||||
func adjustScrollContentInsetIfNeeded() {
|
||||
guard
|
||||
let fpc = ownerVC,
|
||||
let scrollView = scrollView,
|
||||
fpc.contentInsetAdjustmentBehavior == .always
|
||||
fpc.contentInsetAdjustmentBehavior == .always,
|
||||
fpc.state != layoutAdapter.mostExpandedState,
|
||||
isScrollable(state: fpc.state)
|
||||
else { return }
|
||||
|
||||
switch fpc.contentMode {
|
||||
@@ -1245,7 +1263,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
///
|
||||
/// - Note: The delegate is used by FloatingPanel itself. If you set your own delegate object, an
|
||||
/// exception is raised. If you want to handle the methods of UIGestureRecognizerDelegate, you can use `delegateProxy`.
|
||||
public override weak var delegate: (any UIGestureRecognizerDelegate)? {
|
||||
public override weak var delegate: UIGestureRecognizerDelegate? {
|
||||
get {
|
||||
return super.delegate
|
||||
}
|
||||
@@ -1266,7 +1284,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
/// The default object implementing a set methods of the delegate of the gesture recognizer.
|
||||
///
|
||||
/// Use this property with ``delegateProxy`` when you need to use the default gesture behaviors in a proxy implementation.
|
||||
public var delegateOrigin: any UIGestureRecognizerDelegate {
|
||||
public var delegateOrigin: UIGestureRecognizerDelegate {
|
||||
return floatingPanel
|
||||
}
|
||||
|
||||
@@ -1274,7 +1292,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
///
|
||||
/// `UIGestureRecognizerDelegate` methods implementing by this object are called instead of the default delegate,
|
||||
/// ``delegateOrigin``.
|
||||
public weak var delegateProxy: (any UIGestureRecognizerDelegate)? {
|
||||
public weak var delegateProxy: UIGestureRecognizerDelegate? {
|
||||
didSet {
|
||||
self.delegate = floatingPanel?.panGestureDelegateRouter // Update the cached IMP
|
||||
}
|
||||
@@ -1307,7 +1325,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
|
||||
// MARK: - Animator
|
||||
|
||||
private final class NumericSpringAnimator: NSObject, @unchecked Sendable {
|
||||
private class NumericSpringAnimator: NSObject {
|
||||
struct Data {
|
||||
let value: CGFloat
|
||||
let velocity: CGFloat
|
||||
|
||||
@@ -36,7 +36,6 @@ extension CGPoint {
|
||||
|
||||
// MARK: - UIKit
|
||||
|
||||
@MainActor
|
||||
protocol LayoutGuideProvider {
|
||||
var topAnchor: NSLayoutYAxisAnchor { get }
|
||||
var leftAnchor: NSLayoutXAxisAnchor { get }
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.8.4</string>
|
||||
<string>2.8.7</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
+8
-10
@@ -4,7 +4,6 @@ import UIKit
|
||||
import os.log
|
||||
|
||||
/// An interface for generating layout information for a panel.
|
||||
@MainActor
|
||||
@objc public protocol FloatingPanelLayout {
|
||||
/// Returns the position of a panel in a `FloatingPanelController` view .
|
||||
@objc var position: FloatingPanelPosition { get }
|
||||
@@ -13,7 +12,7 @@ import os.log
|
||||
@objc var initialState: FloatingPanelState { get }
|
||||
|
||||
/// Returns the layout anchors to specify the snapping locations for each state.
|
||||
@objc var anchors: [FloatingPanelState: any FloatingPanelLayoutAnchoring] { get }
|
||||
@objc var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { get }
|
||||
|
||||
/// Returns layout constraints to determine the cross dimension of a panel.
|
||||
@objc optional func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint]
|
||||
@@ -32,7 +31,7 @@ open class FloatingPanelBottomLayout: NSObject, FloatingPanelLayout {
|
||||
return .half
|
||||
}
|
||||
|
||||
open var anchors: [FloatingPanelState: any FloatingPanelLayoutAnchoring] {
|
||||
open var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .top, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
|
||||
@@ -61,12 +60,11 @@ struct LayoutSegment {
|
||||
let upper: FloatingPanelState?
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class LayoutAdapter {
|
||||
private unowned var vc: FloatingPanelController
|
||||
private let defaultLayout = FloatingPanelBottomLayout()
|
||||
|
||||
fileprivate var layout: any FloatingPanelLayout {
|
||||
fileprivate var layout: FloatingPanelLayout {
|
||||
didSet {
|
||||
surfaceView.position = position
|
||||
}
|
||||
@@ -289,7 +287,7 @@ class LayoutAdapter {
|
||||
return offset.rounded(by: surfaceView.fp_displayScale)
|
||||
}
|
||||
|
||||
private var hiddenAnchor: any FloatingPanelLayoutAnchoring {
|
||||
private var hiddenAnchor: FloatingPanelLayoutAnchoring {
|
||||
switch position {
|
||||
case .top:
|
||||
return FloatingPanelLayoutAnchor(absoluteInset: -100, edge: .top, referenceGuide: .superview)
|
||||
@@ -302,7 +300,7 @@ class LayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
init(vc: FloatingPanelController, layout: any FloatingPanelLayout) {
|
||||
init(vc: FloatingPanelController, layout: FloatingPanelLayout) {
|
||||
self.vc = vc
|
||||
self.layout = layout
|
||||
}
|
||||
@@ -406,7 +404,7 @@ class LayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private func referenceEdge(of anchor: any FloatingPanelLayoutAnchoring) -> FloatingPanelReferenceEdge {
|
||||
private func referenceEdge(of anchor: FloatingPanelLayoutAnchoring) -> FloatingPanelReferenceEdge {
|
||||
switch anchor {
|
||||
case is FloatingPanelIntrinsicLayoutAnchor,
|
||||
is FloatingPanelAdaptiveLayoutAnchor:
|
||||
@@ -548,7 +546,7 @@ class LayoutAdapter {
|
||||
NSLayoutConstraint.deactivate(constraint: interactionConstraint)
|
||||
interactionConstraint = nil
|
||||
|
||||
let layoutGuideProvider: any LayoutGuideProvider
|
||||
let layoutGuideProvider: LayoutGuideProvider
|
||||
switch anchor.referenceGuide {
|
||||
case .safeArea:
|
||||
layoutGuideProvider = vc.view.safeAreaLayoutGuide
|
||||
@@ -856,7 +854,7 @@ extension LayoutAdapter {
|
||||
}
|
||||
|
||||
extension FloatingPanelController {
|
||||
var _layout: any FloatingPanelLayout {
|
||||
var _layout: FloatingPanelLayout {
|
||||
get {
|
||||
floatingPanel.layoutAdapter.layout
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import UIKit
|
||||
|
||||
/// An interface for implementing custom layout anchor objects.
|
||||
@MainActor
|
||||
@objc public protocol FloatingPanelLayoutAnchoring {
|
||||
var referenceGuide: FloatingPanelLayoutReferenceGuide { get }
|
||||
func layoutConstraints(_ fpc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint]
|
||||
@@ -18,7 +17,7 @@ import UIKit
|
||||
/// positioning.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - absoluteInset: An absolute offset to attach the panel from the edge.
|
||||
/// - absoluteInset: An absolute distance to attach the panel from the specified edge.
|
||||
/// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset.
|
||||
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
|
||||
@objc public init(absoluteInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
|
||||
@@ -35,7 +34,7 @@ import UIKit
|
||||
/// 1.0 represents a distance to the opposite edge.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - fractionalInset: A fractional value of the size of ``FloatingPanelController``'s view to attach the panel from the edge.
|
||||
/// - fractionalInset: A fractional value of the size of ``FloatingPanelController``'s view to attach the panel from the specified edge.
|
||||
/// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset.
|
||||
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
|
||||
@objc public init(fractionalInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
|
||||
@@ -66,7 +65,7 @@ public extension FloatingPanelLayoutAnchor {
|
||||
}
|
||||
}
|
||||
|
||||
private func layoutConstraints(_ layoutGuide: any LayoutGuideProvider, for edgeAnchor: NSLayoutYAxisAnchor) -> [NSLayoutConstraint] {
|
||||
private func layoutConstraints(_ layoutGuide: LayoutGuideProvider, for edgeAnchor: NSLayoutYAxisAnchor) -> [NSLayoutConstraint] {
|
||||
switch referenceEdge {
|
||||
case .top:
|
||||
if isAbsolute {
|
||||
@@ -85,7 +84,7 @@ public extension FloatingPanelLayoutAnchor {
|
||||
}
|
||||
}
|
||||
|
||||
private func layoutConstraints(_ layoutGuide: any LayoutGuideProvider, for edgeAnchor: NSLayoutXAxisAnchor) -> [NSLayoutConstraint] {
|
||||
private func layoutConstraints(_ layoutGuide: LayoutGuideProvider, for edgeAnchor: NSLayoutXAxisAnchor) -> [NSLayoutConstraint] {
|
||||
switch referenceEdge {
|
||||
case .left:
|
||||
if isAbsolute {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import UIKit
|
||||
|
||||
/// Constants that specify the edge of the container of a panel.
|
||||
@MainActor
|
||||
@objc public enum FloatingPanelReferenceEdge: Int {
|
||||
case top
|
||||
case left
|
||||
@@ -29,14 +28,13 @@ extension FloatingPanelReferenceEdge {
|
||||
}
|
||||
|
||||
/// A representation to specify a rectangular area to lay out a panel.
|
||||
@MainActor
|
||||
@objc public enum FloatingPanelLayoutReferenceGuide: Int {
|
||||
case superview = 0
|
||||
case safeArea = 1
|
||||
}
|
||||
|
||||
extension FloatingPanelLayoutReferenceGuide {
|
||||
func layoutGuide(vc: UIViewController) -> any LayoutGuideProvider {
|
||||
func layoutGuide(vc: UIViewController) -> LayoutGuideProvider {
|
||||
switch self {
|
||||
case .safeArea:
|
||||
return vc.view.safeAreaLayoutGuide
|
||||
@@ -47,7 +45,6 @@ extension FloatingPanelLayoutReferenceGuide {
|
||||
}
|
||||
|
||||
/// A representation to specify a bounding box which limit the content size of a panel.
|
||||
@MainActor
|
||||
@objc public enum FloatingPanelLayoutContentBoundingGuide: Int {
|
||||
case none = 0
|
||||
case superview = 1
|
||||
@@ -55,7 +52,7 @@ extension FloatingPanelLayoutReferenceGuide {
|
||||
}
|
||||
|
||||
extension FloatingPanelLayoutContentBoundingGuide {
|
||||
func layoutGuide(_ fpc: FloatingPanelController) -> (any LayoutGuideProvider)? {
|
||||
func layoutGuide(_ fpc: FloatingPanelController) -> LayoutGuideProvider? {
|
||||
switch self {
|
||||
case .superview:
|
||||
return fpc.view
|
||||
|
||||
+13
-2
@@ -3,11 +3,11 @@
|
||||
import os.log
|
||||
|
||||
let msg = StaticString("%{public}@")
|
||||
nonisolated(unsafe) let sysLog = OSLog(subsystem: Logging.subsystem, category: Logging.category)
|
||||
let sysLog = OSLog(subsystem: Logging.subsystem, category: Logging.category)
|
||||
#if FP_LOG
|
||||
let devLog = OSLog(subsystem: Logging.subsystem, category: "\(Logging.category):dev")
|
||||
#else
|
||||
nonisolated(unsafe) let devLog = OSLog.disabled
|
||||
let devLog = OSLog.disabled
|
||||
#endif
|
||||
|
||||
struct Logging {
|
||||
@@ -15,3 +15,14 @@ struct Logging {
|
||||
static let category = "FloatingPanel"
|
||||
private init() {}
|
||||
}
|
||||
|
||||
extension String.StringInterpolation {
|
||||
mutating func appendInterpolation<T>(optional: T?, defaultValue: String = "nil") {
|
||||
switch optional {
|
||||
case let value?:
|
||||
appendLiteral(String(describing: value))
|
||||
case nil:
|
||||
appendLiteral(defaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import UIKit
|
||||
|
||||
/// Constants describing the position of a panel in a screen
|
||||
@MainActor
|
||||
@objc public enum FloatingPanelPosition: Int {
|
||||
case top
|
||||
case left
|
||||
@@ -26,7 +25,7 @@ extension FloatingPanelPosition {
|
||||
}
|
||||
}
|
||||
|
||||
func mainDimensionAnchor(_ layoutGuide: any LayoutGuideProvider) -> NSLayoutDimension {
|
||||
func mainDimensionAnchor(_ layoutGuide: LayoutGuideProvider) -> NSLayoutDimension {
|
||||
switch self {
|
||||
case .top, .bottom: return layoutGuide.heightAnchor
|
||||
case .left, .right: return layoutGuide.widthAnchor
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import Foundation
|
||||
|
||||
/// An object that represents the display state of a panel in a screen.
|
||||
@objc
|
||||
public final class FloatingPanelState: NSObject, NSCopying, RawRepresentable, Sendable {
|
||||
open class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
|
||||
public typealias RawValue = String
|
||||
|
||||
required public init?(rawValue: RawValue) {
|
||||
|
||||
+10
-10
@@ -5,11 +5,11 @@ import UIKit
|
||||
class ModalTransition: NSObject, UIViewControllerTransitioningDelegate {
|
||||
func animationController(forPresented presented: UIViewController,
|
||||
presenting: UIViewController,
|
||||
source: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? {
|
||||
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
return ModalPresentTransition()
|
||||
}
|
||||
|
||||
func animationController(forDismissed dismissed: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? {
|
||||
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
return ModalDismissTransition()
|
||||
}
|
||||
|
||||
@@ -81,16 +81,16 @@ class PresentationController: UIPresentationController {
|
||||
}
|
||||
|
||||
class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
guard
|
||||
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
else { return 0.0 }
|
||||
|
||||
let animator = fpc.animatorForPresenting(to: fpc.layout.initialState)
|
||||
return TimeInterval(animator.duration)
|
||||
}
|
||||
|
||||
func interruptibleAnimator(using transitionContext: any UIViewControllerContextTransitioning) -> any UIViewImplicitlyAnimating {
|
||||
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
|
||||
guard
|
||||
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
@@ -110,22 +110,22 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
return transitionAnimator
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) {
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
self.interruptibleAnimator(using: transitionContext).startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
guard
|
||||
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
else { return 0.0 }
|
||||
|
||||
let animator = fpc.animatorForDismissing(with: .zero)
|
||||
return TimeInterval(animator.duration)
|
||||
}
|
||||
|
||||
func interruptibleAnimator(using transitionContext: any UIViewControllerContextTransitioning) -> any UIViewImplicitlyAnimating {
|
||||
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
|
||||
guard
|
||||
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
@@ -142,7 +142,7 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
return fpc.transitionAnimator!
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) {
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
self.interruptibleAnimator(using: transitionContext).startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,11 +863,18 @@ class CoreTests: XCTestCase {
|
||||
customSafeAreaInsets
|
||||
}
|
||||
}
|
||||
class PanelDelegate: FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ fpc: FloatingPanelController, shouldAllowToScroll scrollView: UIScrollView, in state: FloatingPanelState) -> Bool {
|
||||
return state == .full || state == .half
|
||||
}
|
||||
}
|
||||
let delegate = PanelDelegate()
|
||||
|
||||
do {
|
||||
let scrollView = CustomScrollView()
|
||||
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 34, right: 0)
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.delegate = delegate
|
||||
fpc.track(scrollView: scrollView)
|
||||
fpc.layout = FloatingPanelBottomLayout()
|
||||
fpc.contentInsetAdjustmentBehavior = .always
|
||||
@@ -894,6 +901,7 @@ class CoreTests: XCTestCase {
|
||||
let scrollView = CustomScrollView()
|
||||
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 91, left: 0, bottom: 0, right: 0)
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.delegate = delegate
|
||||
fpc.track(scrollView: scrollView)
|
||||
fpc.layout = FloatingPanelTopPositionedLayout()
|
||||
fpc.contentInsetAdjustmentBehavior = .always
|
||||
@@ -916,6 +924,68 @@ class CoreTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func test_adjustScrollContentInsetIfNeeded_normal() {
|
||||
class CustomScrollView: UIScrollView {
|
||||
var customSafeAreaInsets: UIEdgeInsets = .zero
|
||||
override var safeAreaInsets: UIEdgeInsets {
|
||||
customSafeAreaInsets
|
||||
}
|
||||
}
|
||||
do {
|
||||
let scrollView = CustomScrollView()
|
||||
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 42, left: 0, bottom: 34, right: 0)
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.track(scrollView: scrollView)
|
||||
fpc.layout = FloatingPanelBottomLayout()
|
||||
fpc.contentInsetAdjustmentBehavior = .always
|
||||
fpc.contentMode = .static
|
||||
fpc.showForTest()
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
fpc.floatingPanel.adjustScrollContentInsetIfNeeded()
|
||||
|
||||
XCTAssertEqual(
|
||||
scrollView.contentInset,
|
||||
UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
fpc.floatingPanel.adjustScrollContentInsetIfNeeded()
|
||||
XCTAssertEqual(
|
||||
scrollView.contentInset,
|
||||
UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func test_initial_scroll_offset_reset() {
|
||||
let fpc = FloatingPanelController()
|
||||
let scrollView = UIScrollView()
|
||||
fpc.layout = FloatingPanelBottomLayout()
|
||||
fpc.track(scrollView: scrollView)
|
||||
fpc.showForTest()
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
|
||||
fpc.panGestureRecognizer.state = .began
|
||||
fpc.floatingPanel.handle(panGesture: fpc.panGestureRecognizer)
|
||||
|
||||
fpc.panGestureRecognizer.state = .cancelled
|
||||
fpc.floatingPanel.handle(panGesture: fpc.panGestureRecognizer)
|
||||
|
||||
waitRunLoop(secs: 1.0)
|
||||
|
||||
let expect = CGPoint(x: 0, y: 100)
|
||||
|
||||
scrollView.setContentOffset(expect, animated: false)
|
||||
|
||||
fpc.move(to: .half, animated: true)
|
||||
|
||||
waitRunLoop(secs: 1.0)
|
||||
|
||||
XCTAssertEqual(expect, scrollView.contentOffset)
|
||||
}
|
||||
|
||||
func test_handleGesture_endWithoutAttraction() throws {
|
||||
class Delegate: FloatingPanelControllerDelegate {
|
||||
var willAttract: Bool?
|
||||
|
||||
Reference in New Issue
Block a user