Compare commits

...

9 Commits

Author SHA1 Message Date
Shin Yamamoto 8f2be39bf4 Version 2.8.2 2024-02-18 15:25:08 +09:00
Shin Yamamoto 92c10830ff ci: use Xcode 15.2 on the GitHub Actions (#619)
- Replaced 'runsOn' with 'runs-on'.
- Supported build jobs for Xcode 15.2.
- Didn't use `macos-11` as possible which was deprecated.
2024-02-18 14:46:32 +09:00
Shin Yamamoto dcb89f58c3 Add CoreTest.test_handleGesture_endWithoutAttraction() 2024-02-17 09:04:09 +09:00
Shin Yamamoto d39c4b54d1 Enable to define and use a subclass object of BackdropView (#617)
* Enable to create a subclass of BackdropView
* Add a custom backdrop sample in the Samples example
2024-02-16 22:07:14 +09:00
Ortwin Gentz, FutureTap 504182ceae Fix scroll locking behavior (#615)
use a separate `scrollLocked` var instead of abusing the scrollView's properties to store the locked state;
fixes an issue where the scroll indicators were no longer visible because lockScrollView() was executed twice before unlockScrollView() was called (due to the user changing `showsVerticalScrollIndicator` mid-animation);
2024-02-16 22:04:10 +09:00
Shin Yamamoto bc1cfe444b Fix a bug state was not changed property after v2.8.1
The state was not changed after moving a panel without attractive
interaction. For example, a panel is moved from half to full and
the scroll content continues to scroll with its deceleration
animation. We can test it on 'Show tracking(TextView)' in Samples app.

dbef6a6 commit causes this issue.
2024-02-03 13:36:36 +09:00
Shin Yamamoto 0e0f773df7 Possible fix for #586 2024-02-03 10:27:21 +09:00
Shin Yamamoto be2be99537 Fix a typo 2023-12-02 09:03:31 +09:00
Shin Yamamoto afcf1ced36 ci: support xcode 15.0.1 2023-11-18 11:51:15 +09:00
13 changed files with 201 additions and 59 deletions
+79 -23
View File
@@ -11,67 +11,80 @@ on:
jobs:
build:
runs-on: ${{ matrix.runsOn }}
runs-on: ${{ matrix.runs-on }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
- swift: "5.9"
xcode: "15.2"
runs-on: macos-13
- swift: "5.8"
xcode: "14.3"
runsOn: macos-13
xcode: "14.3.1"
runs-on: macos-13
- swift: "5.7"
xcode: "14.1"
runsOn: macos-12
runs-on: macos-12
- swift: "5.6"
xcode: "13.4.1"
runsOn: macos-12
runs-on: macos-12
- swift: "5.5"
xcode: "13.2.1"
runsOn: macos-11
runs-on: macos-12
- swift: "5.4"
xcode: "12.5.1"
runsOn: macos-11
runs-on: macos-11
- swift: "5.3"
xcode: "12.4"
runsOn: macos-11
runs-on: macos-11
- swift: "5.2"
xcode: "11.7"
runsOn: macos-11
runs-on: macos-11
steps:
- uses: actions/checkout@v3
- name: Building in Swift ${{ matrix.swift }}
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=${{ matrix.swift }} clean build
test:
runs-on: ${{ matrix.runsOn }}
runs-on: ${{ matrix.runs-on }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
- os: "17.2"
xcode: "15.2"
sim: "iPhone 15 Pro"
parallel: NO # Stop random test job failures
runs-on: macos-13
- os: "16.4"
xcode: "14.3.1"
sim: "iPhone 14 Pro"
parallel: NO # Stop random test job failures
runsOn: macos-13
runs-on: macos-13
- os: "15.5"
xcode: "13.4.1"
sim: "iPhone 13 Pro"
parallel: NO # Stop random test job failures
runsOn: macos-12
runs-on: macos-12
steps:
- uses: actions/checkout@v3
- name: Testing in iOS ${{ matrix.os }}
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}' -parallel-testing-enabled '${{ matrix.parallel }}'
run: |
xcodebuild clean test \
-workspace FloatingPanel.xcworkspace \
-scheme FloatingPanel \
-destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}' \
-parallel-testing-enabled '${{ matrix.parallel }}'
timeout-minutes: 20
example:
runs-on: macos-12
runs-on: macos-13
env:
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
strategy:
fail-fast: false
matrix:
@@ -83,26 +96,67 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Building ${{ matrix.example }}
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme ${{ matrix.example }} -sdk iphonesimulator clean build
run: |
xcodebuild clean build \
-workspace FloatingPanel.xcworkspace \
-scheme ${{ matrix.example }} \
-sdk iphonesimulator
swiftpm:
runs-on: macos-12
runs-on: macos-13
env:
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
strategy:
fail-fast: false
matrix:
platform: [iphoneos, iphonesimulator]
arch: [x86_64, arm64]
exclude:
- platform: iphoneos
arch: x86_64
include:
# 17.2
- platform: iphoneos
sys: "ios17.2"
- platform: iphonesimulator
sys: "ios17.2-simulator"
steps:
- uses: actions/checkout@v3
- name: "Swift Package Manager build"
run: |
xcrun swift build \
--sdk "$(xcrun --sdk ${{ matrix.platform }} --show-sdk-path)" \
-Xswiftc "-target" -Xswiftc "${{ matrix.arch }}-apple-${{ matrix.sys }}"
swiftpm_old:
runs-on: ${{ matrix.runs-on }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
# 16.4
- target: "x86_64-apple-ios16.4-simulator"
xcode: "14.3.1"
runs-on: macos-13
- 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"
# 16.1
- target: "x86_64-apple-ios16.1-simulator"
- target: "arm64-apple-ios16.1-simulator"
xcode: "14.1"
runs-on: macos-12
steps:
- uses: actions/checkout@v3
- name: "Swift Package Manager build"
run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
run: |
swift build \
-Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" \
-Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
carthage:
runs-on: macos-11
@@ -112,7 +166,9 @@ jobs:
run: carthage build --use-xcframeworks --no-skip-current
cocoapods:
runs-on: macos-12
runs-on: macos-13
env:
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
steps:
- uses: actions/checkout@v3
- name: "CocoaPods: pod lib lint"
@@ -24,6 +24,7 @@ enum UseCase: Int, CaseIterable {
case showAdaptivePanel
case showAdaptivePanelWithCustomGuide
case showCustomStatePanel
case showCustomBackdrop
}
extension UseCase {
@@ -50,6 +51,7 @@ extension UseCase {
case .showAdaptivePanel: return "Show Adaptive Panel"
case .showAdaptivePanelWithCustomGuide: return "Show Adaptive Panel (Custom Layout Guide)"
case .showCustomStatePanel: return "Show Panel with Custom state"
case .showCustomBackdrop: return "Show Panel with Custom Backdrop"
}
}
}
@@ -83,6 +85,7 @@ extension UseCase {
case .showAdaptivePanel: return .storyboard(String(describing: ImageViewController.self))
case .showAdaptivePanelWithCustomGuide: return .storyboard(String(describing: AdaptiveLayoutTestViewController.self))
case .showCustomStatePanel: return .viewController(DebugTableViewController())
case .showCustomBackdrop: return .viewController(UIViewController())
}
}
@@ -273,6 +273,52 @@ extension UseCaseController {
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
addMain(panel: fpc)
case .showCustomBackdrop:
class BlurBackdropView: BackdropView {
var effectView: UIVisualEffectView!
override var alpha: CGFloat {
set {
effectView.alpha = newValue
}
get {
effectView.alpha
}
}
override init() {
super.init()
let effect = UIBlurEffect(style: .prominent)
let effectView = UIVisualEffectView(effect: effect)
addSubview(effectView)
effectView.frame = bounds
effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.effectView = effectView
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class CustomBottomLayout: FloatingPanelBottomLayout {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(fractionalInset: 0.1, edge: .bottom, referenceGuide: .safeArea),
]
}
override func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return state == .full ? 0.8 : 0.0
}
}
let fpc = FloatingPanelController()
fpc.delegate = self
fpc.set(contentViewController: contentVC)
fpc.backdropView = BlurBackdropView()
fpc.layout = CustomBottomLayout()
addMain(panel: fpc)
}
}
@@ -81,7 +81,7 @@ class MainViewController: UIViewController, FloatingPanelControllerDelegate {
}
private func hideStockTickerBanner() {
// Dimiss top bar with dissolve animation
// Dismiss top bar with dissolve animation
UIView.animate(withDuration: 0.25) {
self.topBannerView.alpha = 0.0
self.labelStackView.alpha = 1.0
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.8.1"
s.version = "2.8.2"
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.
+6
View File
@@ -468,6 +468,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
@@ -499,6 +500,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
@@ -521,6 +523,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@@ -541,6 +544,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@@ -633,6 +637,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
@@ -657,6 +662,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
+1 -1
View File
@@ -9,7 +9,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](https://floatingpanel.github.io/2.8.1/documentation/floatingpanel/) for more details.
Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.2/documentation/floatingpanel) for more details.
![Maps](https://github.com/SCENEE/FloatingPanel/blob/master/assets/maps.gif)
![Stocks](https://github.com/SCENEE/FloatingPanel/blob/master/assets/stocks.gif)
+3 -3
View File
@@ -4,7 +4,7 @@ import UIKit
/// A view that presents a backdrop interface behind a panel.
@objc(FloatingPanelBackdropView)
public class BackdropView: UIView {
open class BackdropView: UIView {
/// The gesture recognizer for tap gestures to dismiss a panel.
///
@@ -12,14 +12,14 @@ public class BackdropView: UIView {
/// To dismiss a panel by tap gestures on the backdrop, `dismissalTapGestureRecognizer.isEnabled` is set to true.
@objc public var dismissalTapGestureRecognizer: UITapGestureRecognizer
init() {
public init() {
dismissalTapGestureRecognizer = UITapGestureRecognizer()
dismissalTapGestureRecognizer.isEnabled = false
super.init(frame: .zero)
addGestureRecognizer(dismissalTapGestureRecognizer)
}
required init?(coder: NSCoder) {
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
+5 -3
View File
@@ -158,14 +158,15 @@ open class FloatingPanelController: UIViewController {
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
@objc
public var surfaceView: SurfaceView! {
public var surfaceView: SurfaceView {
return floatingPanel.surfaceView
}
/// Returns the backdrop view managed by the controller object.
@objc
public var backdropView: BackdropView! {
return floatingPanel.backdropView
public var backdropView: BackdropView {
set { floatingPanel.backdropView = newValue }
get { return floatingPanel.backdropView }
}
/// Returns the scroll view that the controller tracks.
@@ -732,6 +733,7 @@ private var originalDismissImp: IMP?
private typealias DismissFunction = @convention(c) (AnyObject, Selector, Bool, (() -> Void)?) -> Void
extension FloatingPanelController {
private static let dismissSwizzling: Void = {
guard originalDismissImp == nil else { return }
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
if let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:))),
let swizzledImp = class_getMethodImplementation(aClass, #selector(__swizzled_dismiss(animated:completion:))) {
+34 -20
View File
@@ -10,7 +10,13 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private weak var ownerVC: FloatingPanelController?
let surfaceView: SurfaceView
let backdropView: BackdropView
var backdropView: BackdropView {
didSet {
backdropView.dismissalTapGestureRecognizer
.addTarget(self, action: #selector(handleBackdrop(tapGesture:)))
}
}
let layoutAdapter: LayoutAdapter
let behaviorAdapter: BehaviorAdapter
@@ -24,6 +30,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
scrollBounce = cur.bounces
scrollIndictorVisible = cur.showsVerticalScrollIndicator
}
scrollLocked = false
} else {
if let pre = oldValue {
pre.isDirectionalLockEnabled = false
@@ -68,6 +75,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private var scrollBounce = false
private var scrollIndictorVisible = false
private var scrollBounceThreshold: CGFloat = -30.0
private var scrollLocked = false
// MARK: - Interface
@@ -728,13 +736,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
guard shouldAttract(to: target) else {
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)
}
self.endWithoutAttraction(target)
return
}
@@ -877,6 +879,17 @@ 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 }
@@ -1051,29 +1064,25 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private func lockScrollView(strict: Bool = false) {
guard let scrollView = scrollView else { return }
if scrollLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll locked")
return
}
scrollBounce = scrollView.bounces
if !strict, shouldLooselyLockScrollView {
if scrollView.isLooselyLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll locked loosely.")
return
}
// Don't change its `bounces` property. If it's changed, it will cause its scroll content offset jump at
// the most expanded anchor position while seamlessly scrolling content. This problem only occurs where its
// content mode is `.fitToBounds` and the tracking scroll content is smaller than the content view size.
// The reason why is because `bounces` prop change leads to the "content frame" change on `.fitToBounds`.
// See also https://github.com/scenee/FloatingPanel/issues/524.
} else {
if scrollView.isLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll locked.")
return
}
scrollBounce = scrollView.bounces
scrollView.bounces = false
}
os_log(msg, log: devLog, type: .debug, "lock scroll view")
scrollView.isDirectionalLockEnabled = true
scrollLocked = true
scrollView.isDirectionalLockEnabled = true
switch layoutAdapter.position {
case .top, .bottom:
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
@@ -1085,9 +1094,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func unlockScrollView() {
guard let scrollView = scrollView, scrollView.isLocked else { return }
guard let scrollView = scrollView else { return }
if !scrollLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll unlocked.")
return
}
os_log(msg, log: devLog, type: .debug, "unlock scroll view")
scrollLocked = false
scrollView.bounces = scrollBounce
scrollView.isDirectionalLockEnabled = false
switch layoutAdapter.position {
-6
View File
@@ -109,12 +109,6 @@ extension UIGestureRecognizer.State: CustomDebugStringConvertible {
#endif
extension UIScrollView {
var isLocked: Bool {
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
}
var isLooselyLocked: Bool {
return !showsVerticalScrollIndicator && isDirectionalLockEnabled
}
var fp_contentOffsetMax: CGPoint {
return CGPoint(x: max((contentSize.width + adjustedContentInset.right) - bounds.width, 0.0),
y: max((contentSize.height + adjustedContentInset.bottom) - bounds.height, 0.0))
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.8.1</string>
<string>2.8.2</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+21
View File
@@ -913,6 +913,27 @@ class CoreTests: XCTestCase {
)
}
}
func test_handleGesture_endWithoutAttraction() throws {
class Delegate: FloatingPanelControllerDelegate {
var willAttract: Bool?
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willAttract attract: Bool) {
willAttract = attract
}
}
let fpc = FloatingPanelController()
let scrollView = UIScrollView()
let delegate = Delegate()
fpc.showForTest()
fpc.delegate = delegate
XCTAssertEqual(fpc.state, .half)
fpc.floatingPanel.endWithoutAttraction(.full)
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(fpc.surfaceLocation(for: .full).y, fpc.surfaceLocation.y)
XCTAssertEqual(delegate.willAttract, false)
}
}
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {