Compare commits

...

44 Commits

Author SHA1 Message Date
Shin Yamamoto 27a2d81a71 Version 2.6.6 2023-08-11 14:17:09 +09:00
Shin Yamamoto 85ed3a6ce3 Fix scroll tracking issues of the scroll view with a positive scroll inset
These issues arose in 'Show Navigation Controller' sample of Samples app.

1. The scrollView's contentOffset always becomes (0, 0) instead of (0, -44),
   which is normal if there is a UINavigationBar.

2. The scrollView's contentOffset sometimes becomes (0, 0) after moving
   a panel quickly like picking.

Case 1 is caused by 7511ce5 commit.
Case 2 is caused by a workaround added at 81fd85e commit.

I tested this library from iOS 11 to iOS 16, and then I confirmed this
workaround doesn't need anymore.

Related to #602, #603.
2023-08-11 13:52:54 +09:00
Shin Yamamoto b34f1093de Version 2.6.5 2023-07-26 18:29:23 +09:00
Shin Yamamoto de1dbe70de Fix a method name for the method convention 2023-07-25 22:47:20 +09:00
Shin Yamamoto c593c646ca Fix a scroll tracking problem caused by a floating point error in LayoutAdapter.offsetFromMostExpandedAnchor
This problem arose after 6611ec8 commit. The root cause is linked to
this condition: `0 == layoutAdapter.offsetFromMostExpandedAnchor` at
line 589 in the method `Core.shouldScrollViewHandleTouch(_:point:velocity:)`.

If the value of `layoutAdapter.offsetFromMostExpandedAnchor` has a
floating point error, the condition evaluates to false. As a result,
the panel moves even when the tracking scroll view is intended to
scroll.

This problem may not occur if there is no floating point error.
2023-07-25 22:13:30 +09:00
Shin Yamamoto 0cb5307a61 Version 2.6.4 2023-07-22 10:18:18 +09:00
Shin Yamamoto 8ab3b7986c Fix test failures in ControllerTests 2023-07-20 22:42:58 +09:00
Shin Yamamoto bbdf3e7c6f Update the logging impl using unified logging system for Xcode 15
The Logging API document says that `os_log` is one of the legacy logging
symbols. However,  this library needs to be supported below iOS 14 so
`log(level:_:)` cannot be used.
2023-07-19 22:16:43 +09:00
Shin Yamamoto bdb756b665 ci: update the test matrix for iOS 16 2023-07-19 16:20:12 +09:00
Shin Yamamoto 6611ec83a2 Fix an issue where a tracking scroll content stops and the panel doesn't move (#530) 2023-07-19 16:18:41 +09:00
Shin Yamamoto a917d6a626 Version 2.6.3 2023-07-01 13:54:25 +09:00
Sören Gade 7511ce577d Fix scrollview content staying non-interactive after slowly swiping down (#597)
This was noticed when updating contained SwiftUI views rapidly.

This change is what fixed a certain bug the scroll content can be locked at a negative offset. It was most obvious when the whitespace was large, hence the offset was something around -100.0. At the same time, the contents (like buttons) were non-interactive.

Co-authored-by: Sören Gade <soeren.gade@lichtblick.de>
2023-07-01 11:52:38 +09:00
Shin Yamamoto e7d0a72440 Fix an issue where dismissalTapGestureRecognizer doesn't work in one case (#590)
`dismissalTapGestureRecognizer` didn't work when the panel is added into UIWindow directly as its subview. This PR fixes this issue and also adds the use case in Samples.app.
2023-07-01 11:49:36 +09:00
Shin Yamamoto 44923ef66e [Revised] Fix an issue scrolling jumps with a small scroll view content (#524)
This is the revised version of commit 448fc5c.

Commit 448fc5c has a critical regression in scroll tracking that can cause the
scroll content to bounce after moving a panel, for example, pulling down it from
full to half state.

By re-investigating #524, I found that this problem only occurred with the
`fitToBounds` content mode and a small scroll view content.

Therefore I fixed it in the more specific way.
2023-07-01 11:46:23 +09:00
Shin Yamamoto 2760bc7298 Modify a private typealias name 2023-06-21 21:04:28 +09:00
Shin Yamamoto d428e96b03 Add the SPI manifest YAML file 2023-06-12 20:32:04 +09:00
Shin Yamamoto 67495961e5 Remove unnecessary prints in unit testing 2023-06-11 21:59:29 +09:00
Shin Yamamoto 7b88703e43 ci: add Xcode 14.3 / Swift 5.8 build 2023-06-10 10:01:46 +09:00
Shin Yamamoto c40e66ef3d Fix a trivial bug on Samples app 2023-06-06 11:35:49 +09:00
Shin Yamamoto 53fb131d0e Update the swizzling way in FloatingPanelController 2023-06-03 12:26:57 +09:00
Shin Yamamoto 37969f6cb3 Merge pull request #588 from scenee/release/2.6.2
Release v2.6.2
2023-06-03 10:40:57 +09:00
Shin Yamamoto a0ba3af012 Version 2.6.2 2023-06-03 10:23:03 +09:00
Shin Yamamoto 6082dba6a7 ci: add circleci config for testing on ios13/14 2023-06-03 10:12:13 +09:00
Shin Yamamoto 43327e8123 ci: add timeout-minutes 2023-06-03 10:12:13 +09:00
Shin Yamamoto 0a5bc19d0f Remove the host app for the unit testing 2023-06-03 10:12:13 +09:00
Shin Yamamoto dd52e1eaee Revise an illegible variable name 2023-04-16 10:14:54 +09:00
Shin Yamamoto 22aa055843 Revise the short descriptions 2023-04-16 10:09:39 +09:00
Shin Yamamoto 448fc5cbb4 Stop changing UIScrollView.bounces when locking or unlocking a scroll view (#584)
UIScrollView would unexpectedly change its scroll offset after updating the bounces property when dealing with small scrollable content. This fixes issue #524, "Scrolling jumps when tableView content is small".
2023-04-08 10:30:11 +09:00
Shin Yamamoto b4fe3b408c Remove unused testing targets in the Samples project 2023-04-08 10:11:41 +09:00
Shin Yamamoto 6ecc7924ba Update badges in README 2023-04-06 21:50:49 +09:00
Shin Yamamoto 0b82902233 Update README for the API documentation on GitHub Pages 2023-03-25 11:57:24 +09:00
Shin Yamamoto 597dbd4145 Revise some sentences in README 2023-03-25 11:04:55 +09:00
Shin Yamamoto 1ee949ff1b Merge pull request #583 from scenee/release/2.6.1
Release 2.6.1
2023-03-04 10:46:11 +09:00
Shin Yamamoto 2a29cb5b3e Version 2.6.1 2023-03-04 09:43:23 +09:00
Shin Yamamoto 208ab665da ci: add Maps-SwiftUI example build 2023-03-04 09:42:40 +09:00
Shin Yamamoto 3e20314cfa Update DebugTableViewController with followScrollViewBouncing() API 2023-03-02 21:53:24 +09:00
Shin Yamamoto 5b8e9a54d9 Update trackingScrollViewDidScroll() with doc comments 2023-03-02 21:53:24 +09:00
Vlad d3c30b35d9 track scroll view bouncing (#525) 2023-02-25 11:11:50 +09:00
Shin Yamamoto 8bb3795931 Take care of an invalid bounding rectangle of PassthroughView in a screen rotation 2023-02-25 10:51:19 +09:00
Shin Yamamoto 9383cd001d ci: replace iOS 15 envs on testing
Because testing on iOS 15.4 is buggy.
2023-02-18 12:15:26 +09:00
Shin Yamamoto 6d5f770066 ci: replace macos-10.15 with macos-11
macos-10.15 will be completely removed by 2023-03-31.
https://github.com/actions/runner-images/issues/5583#issuecomment-1426004850
2023-02-18 11:54:19 +09:00
Shin Yamamoto ce2cafed5b Workaround: fix a laggy animation in Samples app
The AutoLayout rendering engine has been changed since iOS 16.
As a result, "Show Detail Panel" in Samples.apps has a laggy animation
when pulling it down to the bottom of the screen. The issue hadn't
occurred until iOS 15. "On Safe Area View" causes this issue. I could
fix it by removing the constraint of its view to the top of the safe area.

As a workaround, I've replaced its top constraint with one of a
constraint to the top of the superview.
2023-02-18 11:46:41 +09:00
Shin Yamamoto 68352218ac ci: upgrade actions/checkout to suppress Node.js 12 warnings 2023-02-18 11:10:35 +09:00
Shin Yamamoto e3736e4214 Merge pull request #582 from scenee/release/2.6.0
Release 2.6.0
2023-02-18 11:06:19 +09:00
30 changed files with 355 additions and 902 deletions
+21
View File
@@ -0,0 +1,21 @@
version: 2.1
jobs:
test-ios14_5-iPhone_12_Pro:
macos:
xcode: 13.4.1
steps:
- checkout
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.5,name=iPhone 12 Pro'
test-ios13_7-iPhone_11_Pro:
macos:
xcode: 12.5.1
steps:
- checkout
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.7,name=iPhone 11 Pro'
workflows:
test:
jobs:
- test-ios14_5-iPhone_12_Pro
- test-ios13_7-iPhone_11_Pro
+19 -33
View File
@@ -17,11 +17,14 @@ jobs:
fail-fast: false
matrix:
include:
- swift: "5.8"
xcode: "14.3"
runsOn: macos-13
- swift: "5.7"
xcode: "14.1"
runsOn: macos-12
- swift: "5.6"
xcode: "13.3.1"
xcode: "13.4.1"
runsOn: macos-12
- swift: "5.5"
xcode: "13.2.1"
@@ -31,15 +34,12 @@ jobs:
runsOn: macos-11
- swift: "5.3"
xcode: "12.4"
runsOn: macos-10.15
runsOn: macos-11
- swift: "5.2"
xcode: "11.7"
runsOn: macos-10.15
- swift: "5.1"
xcode: "11.3.1"
runsOn: macos-10.15
runsOn: macos-11
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Building in Swift ${{ matrix.swift }}
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=${{ matrix.swift }} clean build
@@ -51,34 +51,19 @@ jobs:
fail-fast: false
matrix:
include:
- os: "16.1"
xcode: "14.1"
- os: "16.4"
xcode: "14.3.1"
sim: "iPhone 14 Pro"
runsOn: macos-12
- os: "15.4"
xcode: "13.3.1"
runsOn: macos-13
- os: "15.5"
xcode: "13.4.1"
sim: "iPhone 13 Pro"
runsOn: macos-12
- os: "15.2"
xcode: "13.2.1"
sim: "iPhone 13 Pro"
runsOn: macos-11
- os: "14.5"
xcode: "12.5.1"
sim: "iPhone 12 Pro"
runsOn: macos-11
- os: "14.4"
xcode: "12.4"
sim: "iPhone 12 Pro"
runsOn: macos-10.15
- os: "13.7"
xcode: "11.7"
sim: "iPhone 11 Pro"
runsOn: macos-10.15
steps:
- uses: actions/checkout@v2
- 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 }}'
timeout-minutes: 20
example:
runs-on: macos-12
@@ -89,10 +74,11 @@ jobs:
matrix:
include:
- example: "Maps"
- example: "Maps-SwiftUI"
- example: "Stocks"
- example: "Samples"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Building ${{ matrix.example }}
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme ${{ matrix.example }} -sdk iphonesimulator clean build
@@ -111,21 +97,21 @@ jobs:
- target: "x86_64-apple-ios16.1-simulator"
- target: "arm64-apple-ios16.1-simulator"
steps:
- uses: actions/checkout@v2
- 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 }}"
carthage:
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: "Carthage build"
run: carthage build --use-xcframeworks --no-skip-current
cocoapods:
runs-on: macos-12
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: "CocoaPods: pod lib lint"
run: pod lib lint --allow-warnings
- name: "CocoaPods: pod spec lint"
+5
View File
@@ -0,0 +1,5 @@
version: 1
builder:
configs:
- documentation_targets: [FloatingPanel]
platform: ios
@@ -24,8 +24,6 @@
545DB9F321511E6300CA77B8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F121511E6300CA77B8 /* Main.storyboard */; };
545DB9F521511E6400CA77B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F421511E6400CA77B8 /* Assets.xcassets */; };
545DB9F821511E6400CA77B8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */; };
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0221511E6400CA77B8 /* SampleTests.swift */; };
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0D21511E6400CA77B8 /* SampleUITests.swift */; };
546341A125C6415100CA0596 /* UseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 546341A025C6415100CA0596 /* UseCase.swift */; };
546341AC25C6426500CA0596 /* CustomState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 546341AB25C6426500CA0596 /* CustomState.swift */; };
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; };
@@ -37,23 +35,6 @@
5D82A6AD28D1843C006A44BA /* libswiftCoreGraphics.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D82A6AC28D18438006A44BA /* libswiftCoreGraphics.tbd */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
545DB9FF21511E6400CA77B8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 545DB9E221511E6300CA77B8 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 545DB9E921511E6300CA77B8;
remoteInfo = FloatingModalSample;
};
545DBA0A21511E6400CA77B8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 545DB9E221511E6300CA77B8 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 545DB9E921511E6300CA77B8;
remoteInfo = FloatingModalSample;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
549D23CD233C7779008EF4D7 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
@@ -88,12 +69,6 @@
545DB9F421511E6400CA77B8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
545DB9F721511E6400CA77B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
545DB9F921511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
545DB9FE21511E6400CA77B8 /* SamplesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DBA0221511E6400CA77B8 /* SampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleTests.swift; sourceTree = "<group>"; };
545DBA0421511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleUITests.swift; sourceTree = "<group>"; };
545DBA0F21511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
546341A025C6415100CA0596 /* UseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCase.swift; sourceTree = "<group>"; };
546341AB25C6426500CA0596 /* CustomState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomState.swift; sourceTree = "<group>"; };
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -114,20 +89,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
545DB9FB21511E6400CA77B8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
545DBA0621511E6400CA77B8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -154,8 +115,6 @@
children = (
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */,
545DB9EC21511E6300CA77B8 /* Sources */,
545DBA0121511E6400CA77B8 /* Tests */,
545DBA0C21511E6400CA77B8 /* UITests */,
545DB9EB21511E6300CA77B8 /* Products */,
5D82A6AB28D18438006A44BA /* Frameworks */,
);
@@ -165,8 +124,6 @@
isa = PBXGroup;
children = (
545DB9EA21511E6300CA77B8 /* Samples.app */,
545DB9FE21511E6400CA77B8 /* SamplesTests.xctest */,
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
@@ -190,24 +147,6 @@
path = Sources;
sourceTree = "<group>";
};
545DBA0121511E6400CA77B8 /* Tests */ = {
isa = PBXGroup;
children = (
545DBA0221511E6400CA77B8 /* SampleTests.swift */,
545DBA0421511E6400CA77B8 /* Info.plist */,
);
path = Tests;
sourceTree = "<group>";
};
545DBA0C21511E6400CA77B8 /* UITests */ = {
isa = PBXGroup;
children = (
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */,
545DBA0F21511E6400CA77B8 /* Info.plist */,
);
path = UITests;
sourceTree = "<group>";
};
546341AA25C6421000CA0596 /* UseCases */ = {
isa = PBXGroup;
children = (
@@ -247,42 +186,6 @@
productReference = 545DB9EA21511E6300CA77B8 /* Samples.app */;
productType = "com.apple.product-type.application";
};
545DB9FD21511E6400CA77B8 /* SamplesTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 545DBA1521511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesTests" */;
buildPhases = (
545DB9FA21511E6400CA77B8 /* Sources */,
545DB9FB21511E6400CA77B8 /* Frameworks */,
545DB9FC21511E6400CA77B8 /* Resources */,
);
buildRules = (
);
dependencies = (
545DBA0021511E6400CA77B8 /* PBXTargetDependency */,
);
name = SamplesTests;
productName = FloatingModalSampleTests;
productReference = 545DB9FE21511E6400CA77B8 /* SamplesTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
545DBA0821511E6400CA77B8 /* SamplesUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 545DBA1821511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesUITests" */;
buildPhases = (
545DBA0521511E6400CA77B8 /* Sources */,
545DBA0621511E6400CA77B8 /* Frameworks */,
545DBA0721511E6400CA77B8 /* Resources */,
);
buildRules = (
);
dependencies = (
545DBA0B21511E6400CA77B8 /* PBXTargetDependency */,
);
name = SamplesUITests;
productName = FloatingModalSampleUITests;
productReference = 545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -296,14 +199,6 @@
545DB9E921511E6300CA77B8 = {
CreatedOnToolsVersion = 10.0;
};
545DB9FD21511E6400CA77B8 = {
CreatedOnToolsVersion = 10.0;
TestTargetID = 545DB9E921511E6300CA77B8;
};
545DBA0821511E6400CA77B8 = {
CreatedOnToolsVersion = 10.0;
TestTargetID = 545DB9E921511E6300CA77B8;
};
};
};
buildConfigurationList = 545DB9E521511E6300CA77B8 /* Build configuration list for PBXProject "Samples" */;
@@ -320,8 +215,6 @@
projectRoot = "";
targets = (
545DB9E921511E6300CA77B8 /* Samples */,
545DB9FD21511E6400CA77B8 /* SamplesTests */,
545DBA0821511E6400CA77B8 /* SamplesUITests */,
);
};
/* End PBXProject section */
@@ -337,20 +230,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
545DB9FC21511E6400CA77B8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
545DBA0721511E6400CA77B8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -381,37 +260,8 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
545DB9FA21511E6400CA77B8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
545DBA0521511E6400CA77B8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
545DBA0021511E6400CA77B8 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 545DB9E921511E6300CA77B8 /* Samples */;
targetProxy = 545DB9FF21511E6400CA77B8 /* PBXContainerItemProxy */;
};
545DBA0B21511E6400CA77B8 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 545DB9E921511E6300CA77B8 /* Samples */;
targetProxy = 545DBA0A21511E6400CA77B8 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
545DB9F121511E6300CA77B8 /* Main.storyboard */ = {
isa = PBXVariantGroup;
@@ -596,90 +446,6 @@
};
name = Release;
};
545DBA1621511E6400CA77B8 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Samples.app/Samples";
};
name = Debug;
};
545DBA1721511E6400CA77B8 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Samples.app/Samples";
};
name = Release;
};
545DBA1921511E6400CA77B8 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = J3D7L9FHSS;
INFOPLIST_FILE = UITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = FloatingModalSample;
};
name = Debug;
};
545DBA1A21511E6400CA77B8 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = J3D7L9FHSS;
INFOPLIST_FILE = UITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalSampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = FloatingModalSample;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -701,24 +467,6 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
545DBA1521511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
545DBA1621511E6400CA77B8 /* Debug */,
545DBA1721511E6400CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
545DBA1821511E6400CA77B8 /* Build configuration list for PBXNativeTarget "SamplesUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
545DBA1921511E6400CA77B8 /* Debug */,
545DBA1A21511E6400CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 545DB9E221511E6300CA77B8 /* Project object */;
@@ -87,7 +87,7 @@
<objects>
<viewController storyboardIdentifier="SettingsViewController" id="C1X-9Z-TyQ" customClass="SettingsViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="af9-Zr-Ppc">
<rect key="frame" x="0.0" y="0.0" width="375" height="197"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="197.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
@@ -598,7 +598,7 @@
</constraints>
</view>
<view alpha="0.5" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="48" width="375" height="730"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
@@ -660,9 +660,9 @@
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="top" secondItem="g7l-kO-y7q" secondAttribute="top" id="A4b-Le-m4I"/>
<constraint firstItem="noi-1a-5bZ" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" priority="750" id="EQy-cr-F2Y"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="centerX" secondItem="aOK-7l-cA6" secondAttribute="centerX" id="EsD-Vf-dNZ"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" id="Fff-HL-4mo"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="bottom" secondItem="g7l-kO-y7q" secondAttribute="bottom" priority="750" id="JOL-wC-w74"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="RiJ-Hb-OOZ"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="Sof-yL-mwK"/>
@@ -18,7 +18,7 @@ class DebugTableViewController: InspectableViewController {
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.alignment = .trailing
stackView.spacing = 10.0
stackView.spacing = 4
return stackView
}()
private lazy var reorderButton: UIButton = {
@@ -28,40 +28,48 @@ class DebugTableViewController: InspectableViewController {
button.addTarget(self, action: #selector(reorderItems), for: .touchUpInside)
return button
}()
private lazy var trackingSwitchWrapper: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fillProportionally
stackView.alignment = .center
stackView.spacing = 8.0
stackView.addArrangedSubview(trackingLabel)
stackView.addArrangedSubview(trackingSwitch)
return stackView
private lazy var trackingButton: UIButton = {
let button = UIButton()
button.setTitle(Menu.trackScrolling.rawValue, for: .normal)
button.setTitleColor(view.tintColor, for: .normal)
button.addTarget(self, action: #selector(toggleTrackingScroll), for: .touchUpInside)
return button
}()
private lazy var trackingLabel: UILabel = {
let label = UILabel()
label.text = "Tracking"
label.font = UIFont.systemFont(ofSize: 17.0, weight: .regular)
return label
}()
private lazy var trackingSwitch: UISwitch = {
let trackingSwitch = UISwitch()
trackingSwitch.isOn = true
trackingSwitch.addTarget(self, action: #selector(turnTrackingOn), for: .touchUpInside)
return trackingSwitch
private lazy var followingButton: UIButton = {
let button = UIButton()
button.setTitleColor(view.tintColor, for: .normal)
button.addTarget(self, action: #selector(toggleFollowingScroll), for: .touchUpInside)
return button
}()
// MARK: - Properties
var kvoObservers: [NSKeyValueObservation] = []
private var fpc: FloatingPanelController? { parent as? FloatingPanelController }
private lazy var items: [String] = {
let items = (0..<100).map { "Items \($0)" }
return Command.replace(items: items)
}()
private var itemHeight: CGFloat = 66.0
// MARK: Flags
private var tracksScrollView: Bool = false {
didSet {
let title = "\(Menu.trackScrolling.rawValue): \(tracksScrollView ? "on" : "off")"
trackingButton.setTitle(title, for: .normal)
}
}
private var followsScrollViewBouncing: Bool = false {
didSet {
let title = "\(Menu.followScrolling.rawValue): \(followsScrollViewBouncing ? "on" : "off")"
followingButton.setTitle(title, for: .normal)
}
}
enum Menu: String, CaseIterable {
case turnOffTracking = "Tracking"
case trackScrolling = "Tracking"
case followScrolling = "Following"
case reorder = "Reorder"
}
@@ -136,13 +144,19 @@ class DebugTableViewController: InspectableViewController {
switch menu {
case .reorder:
buttonStackView.addArrangedSubview(reorderButton)
case .turnOffTracking:
buttonStackView.addArrangedSubview(trackingSwitchWrapper)
case .trackScrolling:
buttonStackView.addArrangedSubview(trackingButton)
case .followScrolling:
buttonStackView.addArrangedSubview(followingButton)
}
}
// Set titles
tracksScrollView = true
followsScrollViewBouncing = false
}
// MARK: - Menu
@objc
private func reorderItems() {
if reorderButton.titleLabel?.text == Menu.reorder.rawValue {
@@ -155,15 +169,21 @@ class DebugTableViewController: InspectableViewController {
}
@objc
private func turnTrackingOn(_ sender: UISwitch) {
guard let fpc = self.parent as? FloatingPanelController else { return }
if sender.isOn {
private func toggleTrackingScroll() {
tracksScrollView.toggle()
guard let fpc = fpc else { return }
if tracksScrollView {
fpc.track(scrollView: tableView)
} else {
fpc.untrack(scrollView: tableView)
}
}
@objc
private func toggleFollowingScroll() {
followsScrollViewBouncing.toggle()
}
// MARK: - Actions
private func execute(command: Command, sourceView: UIView) {
@@ -250,6 +270,9 @@ extension DebugTableViewController: UITableViewDataSource {
extension DebugTableViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if followsScrollViewBouncing {
fpc?.followScrollViewBouncing()
}
print("TableView --- ", scrollView.contentOffset, scrollView.contentInset)
}
@@ -10,6 +10,7 @@ enum UseCase: Int, CaseIterable {
case showPanelModal
case showMultiPanelModal
case showPanelInSheetModal
case showOnWindow
case showTabBar
case showPageView
case showPageContentView
@@ -34,6 +35,7 @@ extension UseCase {
case .showModal: return "Show Modal"
case .showPanelModal: return "Show Panel Modal"
case .showMultiPanelModal: return "Show Multi Panel Modal"
case .showOnWindow: return "Show Panel over Window"
case .showPanelInSheetModal: return "Show Panel in Sheet Modal"
case .showTabBar: return "Show Tab Bar"
case .showPageView: return "Show Page View"
@@ -65,6 +67,7 @@ extension UseCase {
case .showDetail: return .storyboard(String(describing: DetailViewController.self))
case .showModal: return .storyboard(String(describing: ModalViewController.self))
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))
@@ -11,6 +11,7 @@ final class UseCaseController: NSObject {
private var detailPanelVC: FloatingPanelController!
private var settingsPanelVC: FloatingPanelController!
private lazy var pagePanelController = PagePanelController()
private lazy var overWindowPanelVC = FloatingPanelController()
init(mainVC: MainViewController) {
self.mainVC = mainVC
@@ -157,6 +158,19 @@ extension UseCaseController {
let fpc = MultiPanelController()
mainVC.present(fpc, animated: true, completion: nil)
case .showOnWindow:
let fpc = overWindowPanelVC
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
guard let window = UIApplication.shared.windows.first else { fatalError("Any window not found") }
window.addSubview(fpc.view)
fpc.view.frame = window.bounds
fpc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
fpc.show(animated: true)
case .showPanelInSheetModal:
let fpc = FloatingPanelController()
let contentVC = UIViewController()
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
-28
View File
@@ -1,28 +0,0 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanelSample
class SampleTests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
@@ -1,28 +0,0 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
class SampleUITests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
XCUIApplication().launch()
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
// Use recording to get started writing UI tests.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.6.0"
s.version = "2.6.6"
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.
+7 -146
View File
@@ -17,8 +17,6 @@
545DB9E021511AC100CA77B8 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* Controller.swift */; };
545DBA2B2152383100CA77B8 /* GrabberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberView.swift */; };
546055BF2333C4740069F400 /* TestSupports.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* TestSupports.swift */; };
5469F4A224B003EF00537F8A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5469F49F24B003EF00537F8A /* LaunchScreen.storyboard */; };
5469F4A324B003EF00537F8A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4A024B003EF00537F8A /* AppDelegate.swift */; };
5469F4AE24B30D7E00537F8A /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AD24B30D7E00537F8A /* State.swift */; };
5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */; };
5469F4B224B30F1100537F8A /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B124B30F1100537F8A /* Position.swift */; };
@@ -27,7 +25,7 @@
549E944522CF295D0050AECF /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* StateTests.swift */; };
54A6B6B122968B530077F348 /* CoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* CoreTests.swift */; };
54A6B6B82296A8520077F348 /* SurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */; };
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logger.swift */; };
54ABD7AF216CCFF7002E6C13 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logging.swift */; };
54CDC5D3215B6D5A007D205C /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* SurfaceView.swift */; };
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* BackdropView.swift */; };
54CFBFC3215CD045006B5735 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* Layout.swift */; };
@@ -45,13 +43,6 @@
remoteGlobalIDString = 545DB9C02151169500CA77B8;
remoteInfo = FloatingModalController;
};
54E740DC218AFE9F005C1A34 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 545DB9B82151169500CA77B8 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 54E740C9218AFD67005C1A34;
remoteInfo = TestingHost;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
@@ -68,9 +59,6 @@
545DB9D12151169500CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
545DB9DF21511AC100CA77B8 /* Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Controller.swift; sourceTree = "<group>"; };
545DBA2A2152383100CA77B8 /* GrabberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberView.swift; sourceTree = "<group>"; };
5469F49F24B003EF00537F8A /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
5469F4A024B003EF00537F8A /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
5469F4A124B003EF00537F8A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5469F4AD24B30D7E00537F8A /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = "<group>"; };
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutAnchoring.swift; sourceTree = "<group>"; };
5469F4B124B30F1100537F8A /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = "<group>"; };
@@ -79,14 +67,13 @@
549E944422CF295D0050AECF /* StateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateTests.swift; sourceTree = "<group>"; };
54A6B6B022968B530077F348 /* CoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTests.swift; sourceTree = "<group>"; };
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceViewTests.swift; sourceTree = "<group>"; };
54ABD7AE216CCFF7002E6C13 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
54ABD7AE216CCFF7002E6C13 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
54CDC5D2215B6D5A007D205C /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = "<group>"; };
54CDC5D4215B6D8D007D205C /* BackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackdropView.swift; sourceTree = "<group>"; };
54CFBFC2215CD045006B5735 /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = "<group>"; };
54CFBFC4215CD09C006B5735 /* Core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core.swift; sourceTree = "<group>"; };
54DBA3DB262E938500D75969 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
54E3992627141F5100A8F9ED /* FloatingPanel.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = FloatingPanel.docc; sourceTree = "<group>"; };
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
5D82A6B428D18461006A44BA /* libswiftCoreGraphics.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreGraphics.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/usr/lib/swift/libswiftCoreGraphics.tbd; sourceTree = DEVELOPER_DIR; };
/* End PBXFileReference section */
@@ -107,13 +94,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
54E740C7218AFD67005C1A34 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -132,7 +112,6 @@
children = (
545DB9C12151169500CA77B8 /* FloatingPanel.framework */,
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */,
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */,
);
name = Products;
sourceTree = "<group>";
@@ -154,7 +133,7 @@
545DBA2A2152383100CA77B8 /* GrabberView.swift */,
54352E9521A51A2500CBCA08 /* Transitioning.swift */,
54DBA3DB262E938500D75969 /* Extensions.swift */,
54ABD7AE216CCFF7002E6C13 /* Logger.swift */,
54ABD7AE216CCFF7002E6C13 /* Logging.swift */,
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9C52151169500CA77B8 /* Info.plist */,
54E3992627141F5100A8F9ED /* FloatingPanel.docc */,
@@ -165,7 +144,6 @@
545DB9CE2151169500CA77B8 /* Tests */ = {
isa = PBXGroup;
children = (
5469F49E24B003EF00537F8A /* TestingApp */,
54A6B6B022968B530077F348 /* CoreTests.swift */,
545DB9CF2151169500CA77B8 /* ControllerTests.swift */,
542753C522C49A6E00D17955 /* LayoutTests.swift */,
@@ -178,16 +156,6 @@
path = Tests;
sourceTree = "<group>";
};
5469F49E24B003EF00537F8A /* TestingApp */ = {
isa = PBXGroup;
children = (
5469F49F24B003EF00537F8A /* LaunchScreen.storyboard */,
5469F4A024B003EF00537F8A /* AppDelegate.swift */,
5469F4A124B003EF00537F8A /* Info.plist */,
);
path = TestingApp;
sourceTree = "<group>";
};
5D82A6B328D18460006A44BA /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -240,30 +208,12 @@
);
dependencies = (
545DB9CD2151169500CA77B8 /* PBXTargetDependency */,
54E740DD218AFE9F005C1A34 /* PBXTargetDependency */,
);
name = FloatingPanelTests;
productName = FloatingModalControllerTests;
productReference = 545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
54E740C9218AFD67005C1A34 /* TestingApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = 54E740D9218AFD6A005C1A34 /* Build configuration list for PBXNativeTarget "TestingApp" */;
buildPhases = (
54E740C6218AFD67005C1A34 /* Sources */,
54E740C7218AFD67005C1A34 /* Frameworks */,
54E740C8218AFD67005C1A34 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = TestingApp;
productName = TestingHost;
productReference = 54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -280,10 +230,6 @@
};
545DB9C92151169500CA77B8 = {
CreatedOnToolsVersion = 10.0;
TestTargetID = 54E740C9218AFD67005C1A34;
};
54E740C9218AFD67005C1A34 = {
CreatedOnToolsVersion = 10.1;
};
};
};
@@ -302,7 +248,6 @@
targets = (
545DB9C02151169500CA77B8 /* FloatingPanel */,
545DB9C92151169500CA77B8 /* FloatingPanelTests */,
54E740C9218AFD67005C1A34 /* TestingApp */,
);
};
/* End PBXProject section */
@@ -322,14 +267,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
54E740C8218AFD67005C1A34 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5469F4A224B003EF00537F8A /* LaunchScreen.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -345,7 +282,7 @@
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* PassthroughView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* Core.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logging.swift in Sources */,
545DB9E021511AC100CA77B8 /* Controller.swift in Sources */,
54DBA3DC262E938500D75969 /* Extensions.swift in Sources */,
5450EEE421646DF500135936 /* Behavior.swift in Sources */,
@@ -370,14 +307,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
54E740C6218AFD67005C1A34 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5469F4A324B003EF00537F8A /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -386,11 +315,6 @@
target = 545DB9C02151169500CA77B8 /* FloatingPanel */;
targetProxy = 545DB9CC2151169500CA77B8 /* PBXContainerItemProxy */;
};
54E740DD218AFE9F005C1A34 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 54E740C9218AFD67005C1A34 /* TestingApp */;
targetProxy = 54E740DC218AFE9F005C1A34 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@@ -593,11 +517,10 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
};
name = Debug;
};
@@ -614,47 +537,10 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
};
name = Release;
};
54E740DA218AFD6A005C1A34 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/TestingApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
PRODUCT_NAME = FloatingPanelTesting;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
54E740DB218AFD6A005C1A34 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/TestingApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
PRODUCT_NAME = FloatingPanelTesting;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
@@ -767,25 +653,10 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FloatingPanelTesting.app/FloatingPanelTesting";
};
name = Test;
};
54E8AC6A2286CFB6000C5A12 /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/TestingApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTesting;
PRODUCT_NAME = FloatingPanelTesting;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Test;
};
@@ -822,16 +693,6 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
54E740D9218AFD6A005C1A34 /* Build configuration list for PBXNativeTarget "TestingApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
54E740DA218AFD6A005C1A34 /* Debug */,
54E740DB218AFD6A005C1A34 /* Release */,
54E8AC6A2286CFB6000C5A12 /* Test */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 545DB9B82151169500CA77B8 /* Project object */;
+13 -13
View File
@@ -1,15 +1,15 @@
[![Build Status](https://travis-ci.org/SCENEE/FloatingPanel.svg?branch=master)](https://travis-ci.org/SCENEE/FloatingPanel)
[![Version](https://img.shields.io/cocoapods/v/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![Swift 5](https://img.shields.io/badge/swift-5-orange.svg?style=flat)](https://swift.org/)
[![Platform](https://img.shields.io/cocoapods/p/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
[![Swift 5](https://img.shields.io/badge/Swift-5-orange.svg?style=flat)](https://swift.org/)
[![Version](https://img.shields.io/cocoapods/v/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/scenee/FloatingPanel/ci.yml?branch=master)
[![Carthage compatible](https://img.shields.io/badge/carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
# FloatingPanel
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
The new interface displays the related contents and utilities in parallel as a user wants.
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.
📝[Here](https://docs.scenee.com/documentation/floatingpanel) is the API references for the latest version powered by [DocC](https://developer.apple.com/documentation/docc).
Please see also [the API reference](https://floatingpanel.github.io/2.6.6/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)
@@ -75,14 +75,14 @@ The new interface displays the related contents and utilities in parallel as a u
- [x] Removal interaction
- [x] Multi panel support
- [x] Modal presentation
- [x] 4 positioning support(top, left, bottom, right)
- [x] Support for 4 positions (top, left, bottom, right)
- [x] 1 or more magnetic anchors(full, half, tip and more)
- [x] Layout support for all trait environments(i.e. Landscape orientation)
- [x] Common UI elements: surface, backdrop and grabber handle
- [x] Free from common issues of Auto Layout and gesture handling
- [x] Free from common Auto Layout and gesture handling issues
- [x] Compatible with Objective-C
Examples are here.
Examples can be found here:
- [Examples/Maps](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Maps) like Apple Maps.app.
- [Examples/Stocks](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Stocks) like Apple Stocks.app.
@@ -91,11 +91,11 @@ Examples are here.
## Requirements
FloatingPanel is written in Swift 5.0+. Compatible with iOS 11.0+.
FloatingPanel is written in Swift 5.0+ and compatible with iOS 11.0+.
The deployment is still iOS 10, but it is recommended to use this library on iOS 11+.
While it still supports iOS 10, it is recommended to use this library on iOS 11+.
:pencil2: You would like to use Swift 4.0. Please use FloatingPanel v1.
:pencil2: If you'd like to use Swift 4.0, please use FloatingPanel v1.
## Installation
+46 -21
View File
@@ -1,6 +1,7 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
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
@@ -167,7 +168,8 @@ open class FloatingPanelController: UIViewController {
set {
_layout = newValue
if let parent = parent, let layout = newValue as? UIViewController, layout == parent {
log.warning("A memory leak will occur by a retain cycle because \(self) owns the parent view controller(\(parent)) as the layout object. Don't let the parent adopt FloatingPanelLayout.")
let log = "Warning: A memory leak occurs due to a retain cycle, as \(self) owns the parent view controller(\(parent)) as its layout object. Don't allow the parent to adopt FloatingPanelLayout."
os_log(msg, log: sysLog, type: .error, log)
}
}
}
@@ -179,7 +181,8 @@ open class FloatingPanelController: UIViewController {
set {
_behavior = newValue
if let parent = parent, let behavior = newValue as? UIViewController, behavior == parent {
log.warning("A memory leak will occur by a retain cycle because \(self) owns the parent view controller(\(parent)) as the behavior object. Don't let the parent adopt FloatingPanelBehavior.")
let log = "Warning: A memory leak occurs due to a retain cycle, as \(self) owns the parent view controller(\(parent)) as its behavior object. Don't allow the parent to adopt FloatingPanelBehavior."
os_log(msg, log: sysLog, type: .error, log)
}
}
}
@@ -375,7 +378,7 @@ open class FloatingPanelController: UIViewController {
preSafeAreaInsets != safeAreaInsets
else { return }
log.debug("Update safeAreaInsets", safeAreaInsets)
os_log(msg, log: devLog, type: .debug, "Update safeAreaInsets = \(safeAreaInsets)")
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
@@ -414,8 +417,14 @@ open class FloatingPanelController: UIViewController {
guard let self = self else { return }
self.delegate?.floatingPanelDidRemove?(self)
}
} else {
} else if parent != nil {
removePanelFromParent(animated: true)
} else {
hide(animated: true) { [weak self] in
guard let self = self else { return }
self.view.removeFromSuperview()
self.delegate?.floatingPanelDidRemove?(self)
}
}
}
@@ -441,6 +450,12 @@ open class FloatingPanelController: UIViewController {
// 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 }
self.update(safeAreaInsets: self.view.safeAreaInsets)
}
} else {
@@ -470,7 +485,7 @@ open class FloatingPanelController: UIViewController {
@objc(addPanelToParent:at:animated:completion:)
public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false, completion: (() -> Void)? = nil) {
guard self.parent == nil else {
log.warning("Already added to a parent(\(parent))")
os_log(msg, log: sysLog, type: .error, "Warning: already added to a parent(\(parent))")
return
}
assert((parent is UINavigationController) == false, "UINavigationController displays only one child view controller at a time.")
@@ -588,6 +603,16 @@ open class FloatingPanelController: UIViewController {
break
}
}
/// [Experimental] Allows the panel to move as its tracking scroll view bounces.
///
/// This method must be called in the delegate method, `UIScrollViewDelegate.scrollViewDidScroll(_:)`,
/// of its tracking scroll view. This method only supports a bottom positioned panel for now.
///
/// - TODO: Support top, left and right positioned panels.
public func followScrollViewBouncing() {
floatingPanel.followScrollViewBouncing()
}
/// Cancel tracking the specify scroll view.
///
@@ -676,30 +701,30 @@ extension FloatingPanelController {
}
}
// MARK: - Swizzling
private var originalDismissImp: IMP?
private typealias DismissFunction = @convention(c) (AnyObject, Selector, Bool, (() -> Void)?) -> Void
extension FloatingPanelController {
private static let dismissSwizzling: Void = {
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
if let imp = class_getMethodImplementation(aClass, #selector(dismiss(animated:completion:))),
let originalAltMethod = class_getInstanceMethod(aClass, #selector(fp_original_dismiss(animated:completion:))) {
method_setImplementation(originalAltMethod, imp)
}
let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:)))
let swizzledMethod = class_getInstanceMethod(aClass, #selector(fp_dismiss(animated:completion:)))
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
method_exchangeImplementations(originalMethod, swizzledMethod)
if let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:))),
let swizzledImp = class_getMethodImplementation(aClass, #selector(__swizzled_dismiss(animated:completion:))) {
originalDismissImp = method_setImplementation(originalMethod, swizzledImp)
}
}()
}
public extension UIViewController {
@objc func fp_original_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// Implementation will be replaced by IMP of self.dismiss(animated:completion:)
}
@objc func fp_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
extension UIViewController {
@objc
fileprivate func __swizzled_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
let dismissImp = unsafeBitCast(originalDismissImp, to: DismissFunction.self)
let sel = #selector(UIViewController.dismiss(animated:completion:))
// Call dismiss(animated:completion:) to a content view controller
if let fpc = parent as? FloatingPanelController {
if fpc.presentingViewController != nil {
self.fp_original_dismiss(animated: flag, completion: completion)
dismissImp(self, sel, flag, completion)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
}
@@ -709,7 +734,7 @@ public extension UIViewController {
if let fpc = self as? FloatingPanelController {
// When a panel is presented modally and it's not a child view controller of the presented view controller.
if fpc.presentingViewController != nil, fpc.parent == nil {
self.fp_original_dismiss(animated: flag, completion: completion)
dismissImp(self, sel, flag, completion)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
}
@@ -717,6 +742,6 @@ public extension UIViewController {
}
// For other view controllers
self.fp_original_dismiss(animated: flag, completion: completion)
dismissImp(self, sel, flag, completion)
}
}
+110 -69
View File
@@ -1,6 +1,7 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import os.log
///
/// The presentation model of FloatingPanel
@@ -35,7 +36,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private(set) var state: FloatingPanelState = .hidden {
didSet {
log.debug("state changed: \(oldValue) -> \(state)")
os_log(msg, log: devLog, type: .debug, "state changed: \(oldValue) -> \(state)")
if let vc = ownerVC {
vc.delegate?.floatingPanelDidChangeState?(vc)
}
@@ -66,6 +67,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private var stopScrollDeceleration: Bool = false
private var scrollBounce = false
private var scrollIndictorVisible = false
private var scrollBounceThreshold: CGFloat = -30.0
// MARK: - Interface
@@ -126,7 +128,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if animated {
let updateScrollView: () -> Void = { [weak self] in
guard let self = self else { return }
if self.state == self.layoutAdapter.mostExpandedState, abs(self.layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
if self.state == self.layoutAdapter.mostExpandedState, 0 == self.layoutAdapter.offsetFromMostExpandedAnchor {
self.unlockScrollView()
} else {
self.lockScrollView()
@@ -162,7 +164,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
self.updateLayout(to: to)
if shouldDoubleLayout {
log.info("Lay out the surface again to modify an intrinsic size error according to UIStackView")
os_log(msg, log: sysLog, type: .info, "Lay out the surface again to modify an intrinsic size error according to UIStackView")
self.updateLayout(to: to)
}
}
@@ -231,7 +233,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
func getBackdropAlpha(at cur: CGFloat, with translation: CGFloat) -> CGFloat {
/* log.debug("currentY: \(currentY) translation: \(translation)") */
/* os_log(msg, log: devLog, type: .debug, "currentY: \(currentY) translation: \(translation)") */
let forwardY = (translation >= 0)
let segment = layoutAdapter.segment(at: cur, forward: forwardY)
@@ -264,7 +266,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
guard gestureRecognizer == panGestureRecognizer else { return false }
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
/* os_log(msg, log: devLog, type: .debug, "shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
switch otherGestureRecognizer {
case is FloatingPanelPanGestureRecognizer:
@@ -337,7 +339,10 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if surfaceView.grabberAreaContains(gestureRecognizer.location(in: surfaceView)) {
return false
}
return allowScrollPanGesture(for: scrollView)
guard state == layoutAdapter.mostExpandedState else { return false }
// The condition where offset > 0 must not be included here. Because it will stop recognizing
// the panel pan gesture if a user starts scrolling content from an offset greater than 0.
return allowScrollPanGesture(of: scrollView) { offset in offset <= scrollBounceThreshold }
default:
return false
}
@@ -388,23 +393,24 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let velocity = value(of: panGesture.velocity(in: panGesture.view))
let location = panGesture.location(in: surfaceView)
let belowEdgeMost = 0 > layoutAdapter.offsetFromMostExpandedAnchor + (1.0 / surfaceView.fp_displayScale)
let insideMostExpandedAnchor = 0 > layoutAdapter.offsetFromMostExpandedAnchor
log.debug("""
os_log(msg, log: devLog, type: .debug, """
scroll gesture(\(state):\(panGesture.state)) -- \
belowTop = \(belowEdgeMost), \
inside expanded anchor = \(insideMostExpandedAnchor), \
interactionInProgress = \(interactionInProgress), \
scroll offset = \(value(of: scrollView.contentOffset)), \
location = \(value(of: location)), velocity = \(velocity)
""")
"""
)
let offsetDiff = value(of: scrollView.contentOffset - contentOffsetForPinning(of: scrollView))
if belowEdgeMost {
if insideMostExpandedAnchor {
// Scroll offset pinning
if state == layoutAdapter.mostExpandedState {
if interactionInProgress {
log.debug("settle offset --", value(of: initialScrollOffset))
os_log(msg, log: devLog, type: .debug, "settle offset -- \(value(of: initialScrollOffset))")
stopScrolling(at: initialScrollOffset)
} else {
if surfaceView.grabberAreaContains(location) {
@@ -456,21 +462,24 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
} else {
if state == layoutAdapter.mostExpandedState {
let allowScroll = allowScrollPanGesture(of: scrollView) { offset in
offset <= scrollBounceThreshold || 0 < offset
}
switch layoutAdapter.position {
case .top, .left:
if velocity < 0, !allowScrollPanGesture(for: scrollView) {
lockScrollView()
if velocity < 0, !allowScroll {
lockScrollView(strict: true)
}
if velocity > 0, allowScrollPanGesture(for: scrollView) {
if velocity > 0, allowScroll {
unlockScrollView()
}
case .bottom, .right:
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
if velocity > 0, !allowScrollPanGesture(for: scrollView) {
lockScrollView()
if velocity > 0, !allowScroll {
lockScrollView(strict: true)
}
// Show a scroll indicator when an animation is interrupted at the top and content is scrolled up
if velocity < 0, allowScrollPanGesture(for: scrollView) {
if velocity < 0, allowScroll {
unlockScrollView()
}
}
@@ -486,7 +495,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let velocity = panGesture.velocity(in: panGesture.view)
let location = panGesture.location(in: panGesture.view)
log.debug("""
os_log(msg, log: devLog, type: .debug, """
panel gesture(\(state):\(panGesture.state)) -- \
translation = \(value(of: translation)), \
location = \(value(of: location)), \
@@ -536,19 +545,19 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private func interruptAnimationIfNeeded() {
if let animator = self.moveAnimator, animator.isRunning {
log.debug("the attraction animator interrupted!!!")
os_log(msg, log: devLog, type: .debug, "the attraction animator interrupted!!!")
animator.stopAnimation(true)
endAttraction(false)
}
if let animator = self.transitionAnimator {
guard 0 >= layoutAdapter.offsetFromMostExpandedAnchor else { return }
log.debug("a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
os_log(msg, log: devLog, type: .debug, "a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
if animator.isInterruptible {
animator.stopAnimation(false)
// A user can stop a panel at the nearest Y of a target position so this fine-tunes
// the a small gap between the presentation layer frame and model layer frame
// to unlock scroll view properly at finishAnimation(at:)
if abs(layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
if 0 == layoutAdapter.offsetFromMostExpandedAnchor {
layoutAdapter.surfaceLocation = layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState)
}
animator.finishAnimation(at: .current)
@@ -634,7 +643,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// A user interaction does not always start from Began state of the pan gesture
// because it can be recognized in scrolling a content in a content view controller.
// So here just preserve the current state if needed.
log.debug("panningBegan -- location = \(value(of: location))")
os_log(msg, log: devLog, type: .debug, "panningBegan -- location = \(value(of: location))")
guard let scrollView = scrollView else { return }
if state == layoutAdapter.mostExpandedState {
@@ -647,7 +656,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func panningChange(with translation: CGPoint) {
log.debug("panningChange -- translation = \(value(of: translation))")
os_log(msg, log: devLog, type: .debug, "panningChange -- translation = \(value(of: translation))")
let pre = value(of: layoutAdapter.surfaceLocation)
let diff = value(of: translation - initialTranslation)
let next = pre + diff
@@ -700,19 +709,22 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func panningEnd(with translation: CGPoint, velocity: CGPoint) {
log.debug("panningEnd -- translation = \(value(of: translation)), velocity = \(value(of: velocity))")
os_log(msg, log: devLog, type: .debug, "panningEnd -- translation = \(value(of: translation)), velocity = \(value(of: velocity))")
if state == .hidden {
log.debug("Already hidden")
os_log(msg, log: devLog, type: .debug, "Already hidden")
return
}
stopScrollDeceleration = (0 > layoutAdapter.offsetFromMostExpandedAnchor + (1.0 / surfaceView.fp_displayScale)) // Projecting the dragging to the scroll dragging or not
// Determine whether the panel's dragging should be projected onto the scroll content scrolling
stopScrollDeceleration = 0 > layoutAdapter.offsetFromMostExpandedAnchor
if stopScrollDeceleration {
os_log(msg, log: devLog, type: .debug, "panningEnd -- will stop scrolling at initialScrollOffset = \(initialScrollOffset)")
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.stopScrolling(at: self.initialScrollOffset)
os_log(msg, log: devLog, type: .debug, "panningEnd -- did stop scrolling at initialScrollOffset = \(self.initialScrollOffset)")
}
}
@@ -755,19 +767,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: true)
}
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
let isScrollEnabled = scrollView?.isScrollEnabled
if let scrollView = scrollView, targetPosition != layoutAdapter.mostExpandedState {
scrollView.isScrollEnabled = false
}
startAttraction(to: targetPosition, with: velocity)
// Workaround: Reset `self.scrollView.isScrollEnabled`
if let scrollView = scrollView, targetPosition != layoutAdapter.mostExpandedState,
let isScrollEnabled = isScrollEnabled {
scrollView.isScrollEnabled = isScrollEnabled
}
}
// MARK: - Behavior
@@ -792,7 +792,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private func startInteraction(with translation: CGPoint, at location: CGPoint) {
/* Don't lock a scroll view to show a scroll indicator after hitting the top */
log.debug("startInteraction -- translation = \(value(of: translation)), location = \(value(of: location))")
os_log(msg, log: devLog, type: .debug, "startInteraction -- translation = \(value(of: translation)), location = \(value(of: location))")
guard interactionInProgress == false else { return }
var offset: CGPoint = .zero
@@ -804,12 +804,13 @@ class Core: NSObject, UIGestureRecognizerDelegate {
} else {
let pinningOffset = contentOffsetForPinning(of: scrollView)
// `scrollView.contentOffset` can be a value in [-30, 0) at this time by `allowScrollPanGesture(for:)`.
// Therefore the initial scroll offset must be reset to the pinning offset. Otherwise, the following
// `Fit the surface bounds` logic don't working and also the scroll content offset can be invalid.
// `initialScrollOffset` must be reset to the pinning offset because the value of `scrollView.contentOffset`,
// for instance, is a value in [-30, 0) on a bottom positioned panel with `allowScrollPanGesture(of:condition:)`.
// If it's not reset, the following logic to shift the surface frame will not work and then the scroll
// content offset will become an unexpected value.
initialScrollOffset = pinningOffset
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
// Shift the surface frame to negate the scroll content offset at startInteraction(at:offset:)
let offsetDiff = scrollView.contentOffset - pinningOffset
switch layoutAdapter.position {
case .top, .left:
@@ -822,7 +823,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
}
}
log.debug("initial scroll offset --", initialScrollOffset)
os_log(msg, log: devLog, type: .debug, "initial scroll offset -- \(initialScrollOffset)")
}
initialTranslation = translation
@@ -839,10 +840,10 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func endInteraction(for targetPosition: FloatingPanelState) {
log.debug("endInteraction to \(targetPosition)")
os_log(msg, log: devLog, type: .debug, "endInteraction to \(targetPosition)")
if let scrollView = scrollView {
log.debug("endInteraction -- scroll offset = \(scrollView.contentOffset)")
os_log(msg, log: devLog, type: .debug, "endInteraction -- scroll offset = \(scrollView.contentOffset)")
}
interactionInProgress = false
@@ -870,7 +871,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func startAttraction(to targetPosition: FloatingPanelState, with velocity: CGPoint) {
log.debug("startAnimation to \(targetPosition) -- velocity = \(value(of: velocity))")
os_log(msg, log: devLog, type: .debug, "startAnimation to \(targetPosition) -- velocity = \(value(of: velocity))")
guard let vc = ownerVC else { return }
isAttracting = true
@@ -918,17 +919,19 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
if let scrollView = scrollView {
log.debug("finishAnimation -- scroll offset = \(scrollView.contentOffset)")
os_log(msg, log: devLog, type: .debug, "finishAnimation -- scroll offset = \(scrollView.contentOffset)")
}
stopScrollDeceleration = false
log.debug("""
os_log(msg, log: devLog, type: .debug, """
finishAnimation -- state = \(state) \
surface location = \(layoutAdapter.surfaceLocation) \
edge most position = \(layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState))
""")
if finished, state == layoutAdapter.mostExpandedState, abs(layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
if finished, state == layoutAdapter.mostExpandedState, 0 == layoutAdapter.offsetFromMostExpandedAnchor {
unlockScrollView()
} else if finished, shouldLooselyLockScrollView {
unlockScrollView()
}
}
@@ -937,6 +940,10 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return layoutAdapter.position.mainLocation(point)
}
func value(of size: CGSize) -> CGFloat {
return layoutAdapter.position.mainDimension(size)
}
func setValue(_ newValue: CGPoint, to point: inout CGPoint) {
switch layoutAdapter.position {
case .top, .bottom:
@@ -953,7 +960,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
func targetPosition(from currentY: CGFloat, with velocity: CGFloat) -> (FloatingPanelState) {
log.debug("targetPosition -- currentY = \(currentY), velocity = \(velocity)")
os_log(msg, log: devLog, type: .debug, "targetPosition -- currentY = \(currentY), velocity = \(velocity)")
let sortedPositions = layoutAdapter.sortedAnchorStatesByCoordinate
@@ -979,7 +986,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
if behaviorAdapter.shouldProjectMomentum(to: toPos) == false {
log.debug("targetPosition -- negate projection: distance = \(distance)")
os_log(msg, log: devLog, type: .debug, "targetPosition -- negate projection: distance = \(distance)")
let segment = layoutAdapter.segment(at: currentY, forward: forwardY)
var (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
// Equate the segment out of {top,bottom} most state to the {top,bottom} most segment
@@ -1008,32 +1015,67 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// MARK: - ScrollView handling
private func lockScrollView() {
func followScrollViewBouncing() {
guard let scrollView = scrollView else {
return
}
let contentOffset = scrollView.contentOffset.y
guard contentOffset < 0, layoutAdapter.position == .bottom, state == layoutAdapter.mostExpandedState else {
if surfaceView.transform != .identity {
surfaceView.transform = .identity
scrollView.transform = .identity
}
return
}
surfaceView.transform = CGAffineTransform(translationX: 0, y: -contentOffset)
scrollView.transform = CGAffineTransform(translationX: 0, y: contentOffset)
}
private func lockScrollView(strict: Bool = false) {
guard let scrollView = scrollView else { return }
if scrollView.isLocked {
log.debug("Already scroll locked.")
os_log(msg, log: devLog, type: .debug, "Already scroll locked.")
return
}
log.debug("lock scroll view")
os_log(msg, log: devLog, type: .debug, "lock scroll view")
scrollBounce = scrollView.bounces
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
if !strict, shouldLooselyLockScrollView {
// 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 {
scrollBounce = scrollView.bounces
scrollView.bounces = false
}
scrollView.isDirectionalLockEnabled = true
scrollView.bounces = false
scrollView.showsVerticalScrollIndicator = false
}
private func unlockScrollView() {
guard let scrollView = scrollView, scrollView.isLocked else { return }
log.debug("unlock scroll view")
os_log(msg, log: devLog, type: .debug, "unlock scroll view")
scrollView.isDirectionalLockEnabled = false
scrollView.bounces = scrollBounce
scrollView.isDirectionalLockEnabled = false
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
}
private var shouldLooselyLockScrollView: Bool {
var isSmallScrollContentAndFitToBoundsMode: Bool {
if ownerVC?.contentMode == .fitToBounds, let scrollView = scrollView,
value(of: scrollView.contentSize) < value(of: scrollView.bounds.size) - min(layoutAdapter.offsetFromMostExpandedAnchor, 0) {
return true
}
return false
}
return isSmallScrollContentAndFitToBoundsMode
}
private func stopScrolling(at contentOffset: CGPoint) {
// Must use setContentOffset(_:animated) to force-stop deceleration
guard let scrollView = scrollView else { return }
@@ -1058,16 +1100,15 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
}
private func allowScrollPanGesture(for scrollView: UIScrollView) -> Bool {
guard state == layoutAdapter.mostExpandedState else { return false }
var offsetY: CGFloat = 0
private func allowScrollPanGesture(of scrollView: UIScrollView, condition: (_ offset: CGFloat) -> Bool) -> Bool {
var offset: CGFloat = 0
switch layoutAdapter.position {
case .top, .left:
offsetY = value(of: scrollView.fp_contentOffsetMax - scrollView.contentOffset)
offset = value(of: scrollView.fp_contentOffsetMax - scrollView.contentOffset)
case .bottom, .right:
offsetY = value(of: scrollView.contentOffset - contentOffsetForPinning(of: scrollView))
offset = value(of: scrollView.contentOffset - contentOffsetForPinning(of: scrollView))
}
return offsetY <= -30.0 || offsetY > 0
return condition(offset)
}
// MARK: - UIPanGestureRecognizer Intermediation
@@ -1191,7 +1232,7 @@ private class NumericSpringAnimator: NSObject {
if isRunning {
return false
}
log.debug("startAnimation --", displayLink)
os_log(msg, log: devLog, type: .debug, "startAnimation --", displayLink)
isRunning = true
displayLink.add(to: RunLoop.main, forMode: .common)
return true
@@ -1203,7 +1244,7 @@ private class NumericSpringAnimator: NSObject {
if locked { lock.unlock() }
}
log.debug("stopAnimation --", displayLink)
os_log(msg, log: devLog, type: .debug, "stopAnimation --", displayLink)
isRunning = false
displayLink.invalidate()
if withoutFinishing {
+1 -1
View File
@@ -10,7 +10,7 @@ extension CGFloat {
return (self * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
}
func isEqual(to: CGFloat, on displayScale: CGFloat) -> Bool {
return self.rounded(by: displayScale) == to.rounded(by: displayScale)
return rounded(by: displayScale) == to.rounded(by: displayScale)
}
}
+4 -3
View File
@@ -1,11 +1,12 @@
# ``FloatingPanel``
The new interface displays the related contents and utilities in parallel as a user wants.
Create a user interface to display the related content and utilities alongside the main content.
## Overview
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
The new interface displays the related contents and utilities in parallel as a user wants.
FloatingPanel is a simple and easy-to-use UI component designed for a user interface featured in the Apple Maps, Shortcuts and Stocks app.
The user interface displays related content and utilities alongside the main content.
## Topics
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.6.0</string>
<string>2.6.6</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+9 -6
View File
@@ -1,6 +1,7 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import os.log
/// An interface for generating layout information for a panel.
@objc public protocol FloatingPanelLayout {
@@ -265,12 +266,14 @@ class LayoutAdapter {
}
var offsetFromMostExpandedAnchor: CGFloat {
let offset: CGFloat
switch position {
case .top, .left:
return edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
offset = edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
case .bottom, .right:
return position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
offset = position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
}
return offset.rounded(by: surfaceView.fp_displayScale)
}
private var hiddenAnchor: FloatingPanelLayoutAnchoring {
@@ -710,7 +713,7 @@ class LayoutAdapter {
func updateInteractiveEdgeConstraint(diff: CGFloat, scrollingContent: Bool, allowsRubberBanding: (UIRectEdge) -> Bool) {
defer {
log.debug("update surface location = \(surfaceLocation)")
os_log(msg, log: devLog, type: .debug, "update surface location = \(surfaceLocation)")
}
let minConst: CGFloat = position(for: leastCoordinateState)
@@ -750,9 +753,9 @@ class LayoutAdapter {
defer {
if forceLayout {
layoutSurfaceIfNeeded()
log.debug("activateLayout for \(state) -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
os_log(msg, log: devLog, type: .debug, "activateLayout for \(state) -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
} else {
log.debug("activateLayout for \(state)")
os_log(msg, log: devLog, type: .debug, "activateLayout for \(state)")
}
}
@@ -787,7 +790,7 @@ class LayoutAdapter {
if let constraints = stateConstraints[state] {
NSLayoutConstraint.activate(constraints)
} else {
log.error("Couldn't find any constraints for \(state)")
os_log(msg, log: sysLog, type: .fault, "Error: can not find any constraints for \(state)")
}
}
}
-100
View File
@@ -1,100 +0,0 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import Foundation
import os.log
// Must be a variable to use `hook` property in testing
var log = {
return Logger()
}()
struct Logger {
private let osLog: OSLog
private let s = DispatchSemaphore(value: 1)
enum Level: Int, Comparable {
case debug = 0
case info = 1
case warning = 2
case error = 3
var displayName: String {
switch self {
case .debug:
return "Debug:"
case .info:
return "Info:"
case .warning:
return "Warning:"
case .error:
return "Error:"
}
}
@available(iOS 10.0, *)
var osLogType: OSLogType {
switch self {
case .debug: return .debug
case .info: return .info
case .warning: return .default
case .error: return .error
}
}
static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
typealias Hook = ((String, Level) -> Void)
var hook: Hook?
fileprivate init() {
osLog = OSLog(subsystem: "com.scenee.FloatingPanel", category: "FloatingPanel")
}
private func log(_ level: Level, _ message: Any, _ arguments: [Any], tag: String, function: String, line: UInt) {
_ = s.wait(timeout: .now() + 0.033)
defer { s.signal() }
let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
let _tag = tag.isEmpty ? "" : "\(tag):"
let log: String = {
switch level {
case .debug:
return "\(level.displayName)\(_tag) \(message) \(extraMessage) (\(function):\(line))"
default:
return "\(level.displayName)\(_tag) \(message) \(extraMessage)"
}
}()
hook?(log, level)
os_log("%{public}@", log: osLog, type: level.osLogType, log)
}
private func getPrettyFunction(_ function: String, _ file: String) -> String {
if let filename = file.split(separator: "/").last {
return filename + ":" + function
} else {
return file + ":" + function
}
}
func debug(_ log: Any, _ arguments: Any..., tag: String = "", function: String = #function, file: String = #file, line: UInt = #line) {
#if __FP_LOG
self.log(.debug, log, arguments, tag: tag, function: getPrettyFunction(function, file), line: line)
#endif
}
func info(_ log: Any, _ arguments: Any..., tag: String = "", function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.info, log, arguments, tag: tag, function: getPrettyFunction(function, file), line: line)
}
func warning(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.warning, log, arguments, tag: "", function: getPrettyFunction(function, file), line: line)
}
func error(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.error, log, arguments, tag: "", function: getPrettyFunction(function, file), line: line)
}
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import os.log
let msg = StaticString("%{public}@")
let sysLog = OSLog(subsystem: Logging.subsystem, category: Logging.category)
#if FP_LOG
let devLog = OSLog(subsystem: Logging.subsystem, category: "\(Logging.category):dev")
#else
let devLog = OSLog.disabled
#endif
struct Logging {
static let subsystem = "com.scenee.FloatingPanel"
static let category = "FloatingPanel"
private init() {}
}
+2 -1
View File
@@ -1,6 +1,7 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import os.log
/// An object for customizing the appearance of a surface view
@objc(FloatingPanelSurfaceAppearance)
@@ -318,7 +319,7 @@ public class SurfaceView: UIView {
public override func layoutSubviews() {
super.layoutSubviews()
log.debug("surface view frame = \(frame)")
os_log(msg, log: devLog, type: .debug, "surface view frame = \(frame)")
containerView.backgroundColor = appearance.backgroundColor
+28 -13
View File
@@ -1,5 +1,6 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import OSLog
import XCTest
@testable import FloatingPanel
@@ -7,22 +8,32 @@ class ControllerTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_warningRetainCycle() {
let exp = expectation(description: "Warning retain cycle")
exp.expectedFulfillmentCount = 2 // For layout & behavior logs
log.hook = {(log, level) in
if log.contains("A memory leak will occur by a retain cycle because") {
XCTAssert(level == .warning)
exp.fulfill()
}
#if swift(>=5.5) // Avoid the 'No exact matches in call to initializer' build failure for OSLogStore when running this test case on iOS 13.7 using Xcode 12.5.1
func test_warningRetainCycle() throws {
guard #available(iOS 15.0, *) else {
throw XCTSkip("Unsupported iOS version: this test needs iOS 15 or later")
}
let myVC = MyZombieViewController(nibName: nil, bundle: nil)
myVC.loadViewIfNeeded()
wait(for: [exp], timeout: 10)
let store = try OSLogStore(scope: .currentProcessIdentifier)
let found = try store
.getEntries(
at: store.position(timeIntervalSinceLatestBoot: 0),
matching: .init(format: "subsystem == '\(Logging.subsystem)'")
)
.contains {
$0.composedMessage.contains("A memory leak occurs due to a retain cycle, as")
}
XCTAssertTrue(found)
}
#endif
func test_addPanel() {
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else { fatalError() }
let rootVC = UIViewController()
rootVC.loadViewIfNeeded()
rootVC.view.bounds = .init(origin: .zero, size: .init(width: 390, height: 844))
let fpc = FloatingPanelController()
fpc.addPanel(toParent: rootVC)
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .half).y)
@@ -30,8 +41,13 @@ class ControllerTests: XCTestCase {
XCTAssertEqual(fpc.surfaceLocation.y, fpc.surfaceLocation(for: .tip).y)
}
@available(iOS 12.0, *)
func test_updateLayout_willTransition() {
func test_updateLayout_willTransition() throws {
guard #available(iOS 12, *) else {
throw XCTSkip("Unsupported iOS version: this test needs iOS 12 or later")
}
if #available(iOS 17, *) {
throw XCTSkip("Unsupported iOS version: this test doesn't support iOS 17 or later")
}
class MyDelegate: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
if newCollection.userInterfaceStyle == .dark {
@@ -314,7 +330,6 @@ class ControllerTests: XCTestCase {
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
fpc.move(to: .half, animated: false)
print(1 / fpc.surfaceView.fp_displayScale)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .half).y)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .tip).y)
+1 -1
View File
@@ -7,7 +7,7 @@ class CoreTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_scrolllock() {
func test_scrollLock() {
let fpc = FloatingPanelController()
let contentVC1 = UITableViewController(nibName: nil, bundle: nil)
-1
View File
@@ -464,7 +464,6 @@ class LayoutTests: XCTestCase {
if #available(iOS 11, *) {
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
print(c)
}
}
func test_layoutAnchor_bottomPosition() {
-19
View File
@@ -1,19 +0,0 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
let rootVC = UIViewController(nibName: nil, bundle: nil)
rootVC.view.backgroundColor = .gray
let window = UIWindow()
window.rootViewController = rootVC
window.makeKeyAndVisible()
self.window = window
return true
}
}
-43
View File
@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
-48
View File
@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13142" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12042"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Copyright © 2019 scenee. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="obG-Y5-kRd">
<rect key="frame" x="0.0" y="626.5" width="375" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="TestingApp" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="obG-Y5-kRd" secondAttribute="centerX" id="5cz-MP-9tL"/>
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="SfN-ll-jLj"/>
<constraint firstAttribute="bottom" secondItem="obG-Y5-kRd" secondAttribute="bottom" constant="20" id="Y44-ml-fuU"/>
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/3" constant="1" id="moa-c2-u7t"/>
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" constant="20" symbolic="YES" id="x7j-FC-K8j"/>
</constraints>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>