Compare commits

...

128 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
Shin Yamamoto d6bbf92339 Version 2.6.0 2023-02-18 10:27:53 +09:00
Shin Yamamoto 0e833aee3c Fix SettingsViewController in Samples app 2023-02-17 09:51:22 +09:00
Shin Yamamoto 5949bc88ea Fix typo 2023-02-06 20:46:25 +09:00
Shin Yamamoto 30bd261a10 Update the swift version in the pod spec 2023-01-30 20:47:47 +09:00
Shin Yamamoto f92279484f Fix the buggy scroll tracking
This commit sets the initial scroll offset to the pinning offset.
The previous implementation, which set it to the current content offset,
leads various scroll tracking bugs. This reproduction is one of issues.

Using 'Scroll tracking(UITableView)' in Samples app.
1. Bounce the scroll content at the top most anchor.
2. Pull down the panel in bouncing at a minus content offset. (the
   scroll content stops at the minus offset.)
3. Pull up it

The previous implementation was implemented for #526/#527. But now the
issue hasn't been reproduced in v2.5.6.
2023-01-21 11:21:34 +09:00
Shin Yamamoto 2205d1186d Enable to restrict the content size in FloatingPanelAdaptiveLayoutAnchor (#518)
* Introduce FloatingPanelLayoutContentBoundingGuide property for FloatingPanelAdaptiveLayoutAnchor
* Revise doc comments
* Clean up code
* Update the minimum deployment target of Samples app to iOS 11
2023-01-15 10:30:25 +09:00
Shin Yamamoto cd0948a9a4 Update a doc comment 2023-01-15 08:52:29 +09:00
Shin Yamamoto 6589aff4ac ci: add xcode 14.1 env (#577) 2023-01-13 09:12:17 +09:00
Shin Yamamoto d34c16b1a5 Refactor BackdropView.dismissalTapGestureRecognizer
* Added the interface and implementation comment
* Changed the property as non-optional
2023-01-07 09:14:04 +09:00
Shin Yamamoto 05813253f9 Merge pull request #571 from scenee/release/2.5.5
Release 2.5.5
2022-10-17 20:26:36 +09:00
Shin Yamamoto 4ab7d26030 Version 2.5.5 2022-10-15 19:39:31 +09:00
Shin Yamamoto 162545d7e9 Add test_initial_surface_position() 2022-10-15 09:30:44 +09:00
Shin Yamamoto 4904ea19cb Fix the stiff animation of "show detail panel" panel.
When a panel moves to tip position, the moving animation has been stiff.
This issue has happened since Xcode 14.
2022-10-15 09:19:01 +09:00
Shin Yamamoto b941f91556 Fix the backdrop alpha when the view size or its size class changes (#573)
This fixes #572 to change the backdrop alpha when the view size or 
its size class changes.

The main change is that `true` is passed as a
`forceLayout` parameter into `viewWillTransition(to:with:)` callbacks.

Because it's necessary for the backdrop alpha's update when the view
size or its size class changes.

This also fixes a regression at `9c45c31` commit.

```diff
-        layoutAdapter.activateLayout(for: state, forceLayout: true)
+        layoutAdapter.activateLayout(for: state, forceLayout: forceLayout)
```

The behavior before the above change indicates that the method has
worked well even when `forceLayout` is set to `true` in their callbacks.

Additional improvements: 

* Format `activateLayout(forceLayout:contentInsetAdjustmentBehavior:)`
* Add `_floor` function for `test_updateBackdropAlpha()`
2022-10-15 09:06:14 +09:00
Shin Yamamoto f917316135 Remove the libswiftCoreGraphics.dylib workaround from Maps-SwiftUI.app
Because the deployment target is above iOS 14.
2022-10-10 11:15:58 +09:00
ll_shioi_atsumori 6235a19588 workaround: add libswiftCoreGraphics.tbd to some sample apps (#567)
This fixed the following error occurs and crashes when running an app built with xcode14 on iOS11.
```
dyld: Library not loaded: /usr/lib/swift/libswiftCoreGraphics.dylib
  Referenced from: /private/var/containers/Bundle/Application/0B28F8D6-D8CE-400B-98B7-052EAD3FB923/xxxxxx.app/Frameworks/FloatingPanel.framework/FloatingPanel
  Reason: image not found
```

These are the related forum threads.
- https://developer.apple.com/forums/thread/714629
- https://developer.apple.com/forums/thread/714795

Co-authored-by: atsunori.shioi <astunori.shioi@play.jp>
2022-10-10 11:01:11 +09:00
Shin Yamamoto c47fc3d1d5 Better to declare FloatingPanelLayout.anchors as constants if possible 2022-10-05 22:03:59 +09:00
Shin Yamamoto e6d285c6df Fix the initial position value of the surface view
'Show Top Positioned Panel' in Samples app was broken.
2022-10-05 22:03:50 +09:00
Shin Yamamoto a26a6beab2 Merge pull request #565 from scenee/release/2.5.4
Release 2.5.4
2022-09-11 08:44:13 +09:00
Shin Yamamoto b00a05b9ed Version 2.5.4 2022-09-08 12:42:23 +09:00
Shin Yamamoto bfbb7ad004 Remove the CI job with Xcode 13.1 and iOS 15.0
This job is unstable because of its environment.
2022-09-08 12:42:14 +09:00
Shin Yamamoto 4dd52b1790 Replace PR #551 change using fatalError() (#563)
This resolves issue #561. Instead of continuing to work a modal presentation transition even if a panel state is not .hidden, this library calls fatalError as a programmer error on the occasion.
2022-09-07 20:35:50 +09:00
Shin Yamamoto aef914e0fd Remove unused '.swiftformat' file 2022-07-30 11:04:29 +09:00
Shin Yamamoto 2ef35f58a3 Merge pull request #556 from scenee/iss-536
Fix the view index of FloatingPanelView for SwiftUI
2022-07-30 11:04:11 +09:00
Shin Yamamoto 8aaf8b7b25 Fix the view index of FloatingPanelView for SwiftUI
Resolve #536
2022-07-30 09:28:33 +09:00
Shin Yamamoto c330b59a9f Merge pull request #550 from scenee/release/2.5.3
Release 2.5.3
2022-06-14 21:14:09 +09:00
Shin Yamamoto 639e4221e8 Version 2.5.3 2022-06-13 21:06:44 +09:00
Shin Yamamoto 1ff3c28de8 Ensure the initial state is hidden in presenting it as a modality (#551) 2022-06-13 21:06:24 +09:00
Sean Hernandez 89092676e3 Fix floating panel content view constraints (#549)
Resolved Maps-SwiftUI Example Animation Stuttering (#546)
2022-06-13 20:00:12 +09:00
Anton Siliuk bacc871dd2 Fix cornerCurve property availability compilation fail (#548)
With Xcode 14 Beta 1 library fails to compile with following error.

> Stored properties cannot be marked potentially unavailable with `@available`

Because CALayerCornerCurve is not marked with any @available annotation it is possible to declare private optional storage of it.
2022-06-13 19:54:54 +09:00
Shin Yamamoto 0bc9812c23 Update GitHub Actions workflow using macOS-12 (#538)
* Updated 'ci' workflow for Xcode 13.3.1
* Modernized and simplified the workflow using 'strategy.matrix'
* 'arm64-apple-ios15.4' target is not included in swiftpm because its build gets an build error.
2022-05-31 21:42:36 +09:00
Shin Yamamoto a4d98971b3 Fix a warning in SamplesObjC 2022-04-20 21:16:45 +09:00
little-huang 1b0ec64489 fix: add missing import UIKit (#534)
Co-authored-by: 黄宝成 <huangbc@publink.cn>
2022-04-20 20:29:28 +09:00
Shin Yamamoto 345e894007 Stop scrolling if the initial location of a pan gesture is in the grabber area 2022-02-21 21:16:39 +09:00
Shin Yamamoto 1fb1708df9 Fix the grabber area detecion 2022-02-19 08:08:56 +09:00
Shin Yamamoto b9edbf766e Merge pull request #528 from scenee/release/2.5.2
Release 2.5.2
2022-02-10 17:07:24 +09:00
Shin Yamamoto fe30e60235 Version 2.5.2 2022-02-04 23:59:07 +09:00
Jakub Dudek 6e7c31110f Fix dragging on content placed above the trackedView resets scroll position (#527)
Changed how `initialScrollOffset` is set. This issue was described in #526.
2022-02-04 23:56:46 +09:00
Shin Yamamoto e39f634d34 Unlock a scroll view when untracked 2021-12-04 11:12:11 +09:00
Shin Yamamoto 5851fd77c4 Merge pull request #521 from scenee/release/2.5.1
Release 2.5.1
2021-12-03 18:06:12 +09:00
Shin Yamamoto 0fbbbc8d99 Version 2.5.1 2021-11-29 20:46:32 +09:00
Shin Yamamoto 69078366de Change constraint priorities of the content view on the default content mode (#519)
They were set to `.required - 1` is #359, which fixed #294. However #294
was an issue on `.fitToBounds` content mode. But because of #444 and #515,
if your panel’s content mode is `.static`, their priorities should be set to
`.required` on `.static` content mode.
2021-11-29 20:40:26 +09:00
Shin Yamamoto 084b589a10 Fix invalidateLayout() implementation as following the doc comment (#510)
* Fix FloatingPanelController.invalidateLayout() implementation as following the doc comment
* Revise the doc comments of invalidateLayout() method, layout and behavior properties of FloatingPanelController.
* Add a note in README 
* Fix some grammar errors
2021-11-25 23:19:05 +09:00
Shin Yamamoto 07ae324586 Clean up the example projects and codes (#512)
* Clean up Samples and Maps examples
* Rename the root view controller in Maps/Stocks/SampleObjC examples
* Reorder resource file references
* Rename examples' bundle ids with `example` domain.
* Remove 'Run Script' to modify CFBundleVersion: now this has not been used for testing since unit tests were added
* Revise methods to handle content view controllers in UseCaseController
* Fix a bug on PagePanelController
2021-11-24 20:26:35 +09:00
Shin Yamamoto 819c87c530 Stop changing the content mode whose panel is attracting (#513)
This code causes #511. Unfortunately I don't remember the reason
why the example code do that (I should have added a comment...).
And it's a bit tricky for the example. So I remove it.
2021-11-16 08:29:43 +09:00
Shin Yamamoto 3bb9c4fd0f Apply a part of build settings recommended by Xcode 13 2021-10-30 11:19:52 +09:00
Shin Yamamoto 910a304849 Update README 2021-10-30 11:04:24 +09:00
Shin Yamamoto 2ecdbbae52 Add the documentation catalog 2021-10-11 16:33:16 +09:00
Shin Yamamoto fe2e3173b8 Support DocC symbols 2021-10-09 10:09:14 +09:00
Shin Yamamoto 21c6e3c715 Merge pull request #505 from scenee/release/2.5.0
Release 2.5.0
2021-09-28 20:44:31 +09:00
Shin Yamamoto 29beea0ff9 Version 2.5.0 2021-09-28 08:43:42 +09:00
Shin Yamamoto 1e16d9c1fb Fix a tab bar's appearance in Samples app on iOS 15 2021-09-28 08:43:42 +09:00
milettal 31d7a2a301 Fix a crash in Samples app on iPad (#497)
* Fix a crash when the app shows an action sheet on iPad
* Use the UITableViewCell as the sourceView
2021-09-25 10:11:38 +09:00
Federico Zanetello 5f1d49b0bd Add SwiftUI proof of concept in Maps-SwiftUI example (#481)
Partially solves #281. This adds a new example app which mimics the Maps.app, written in SwiftUI. The code works for iOS 13+, however:
* the project has been created with Xcode 13
* the project uses the SwiftUI lifecycle (iOS 14+)

The source code in Examples/Maps-SwiftUI/Maps/FloatingPanel is ready to move into the library, but there is an issue on SwiftUI’s environment propagation into FloatingPanel. SwiftUI’s environment is propagated to all subviews. However FloatingPanel is not a subview, but a new view controller in the screen (and not a child view controller).

It’s possible to lead behaviors unexpected by SwiftUI users so that this is merged as a sample code until it will be resolved.
2021-09-25 10:06:23 +09:00
Shin Yamamoto ee66f218ca Merge pull request #502 from scenee/release/2.4.1
Release 2.4.1
2021-09-18 12:11:12 +09:00
Shin Yamamoto 3a0831a334 Version 2.4.1 2021-09-18 11:20:52 +09:00
Shin Yamamoto add2f9ba4f Remove .travis.yml 2021-09-18 11:20:52 +09:00
Shin Yamamoto ca6a8d5c16 Update ci workflow 2021-09-18 10:57:03 +09:00
Iran-Q 84eba1aefa fix objc can not obtain dismissalTapGestureRecognizer (#496)
Co-authored-by: CodingIran <codingiran@163.com>
2021-08-27 00:02:02 +09:00
Shin Yamamoto 289ea4c971 Fix a compile error on SampleObjC 2021-08-18 07:29:39 +09:00
Shin Yamamoto 821b03376c Make the pan gesture keep disabled (#486)
Because a panel's pan gesture becomes enabled in showing it even
after it is disabled. This issue was reported in #485.
2021-08-10 11:57:19 +09:00
Shin Yamamoto 19fe1cffef Merge pull request #472 from scenee/release/2.4.0
Release 2.4.0
2021-06-05 15:09:39 +09:00
Shin Yamamoto b8fcb50874 Version 2.4.0 2021-06-05 14:28:00 +09:00
Shin Yamamoto 9c45c31190 Fix a regression for the backdrop behavior (#466,#471)
The backdrop alpha of a panel must be updated only in `Controller.show(animated:completion:)`.
Because that prevents a backdrop flicking just before presenting a panel.

Resolve #466.
2021-06-05 14:24:19 +09:00
Shin Yamamoto 895790f697 Update README for the backdrop tap-to-dismiss action 2021-05-28 13:00:30 +09:00
Shin Yamamoto 43c7e8c2a0 Refactor LayoutAdapter
* Remove `edgeMostY`
* Rename the following properties
    * activeStates -> anchorStates
    * orderedStates -> sortedAnchorStates
    * sortedDirectionalStates -> sortedAnchorStatesByCoordinate
    * directionalLeastState -> leastCoordinateState
    * directionalMostState -> mostCoordinateState
    * edgeMostState -> mostExpandedState
    * edgeLeastState -> leastExpandedState
    * offsetFromEdgeMost -> offsetFromMostExpandedAnchor
2021-05-10 22:57:04 +09:00
Shin Yamamoto 904a66115c Add completion handler in addPanel(toParent:) (#402) 2021-05-10 22:14:01 +09:00
Shin Yamamoto d0932e8e37 Rename floatingPanelDidChangePosition to floatingPanelDidChangeState 2021-05-10 22:14:01 +09:00
Shin Yamamoto 22009013eb Merge pull request #461 from scenee/release/2.3.1
Release v2.3.1
2021-05-10 21:37:17 +09:00
Shin Yamamoto da4668aa2b Version 2.3.1 2021-05-08 13:52:17 +09:00
Shin Yamamoto 00ce232420 Refactor the samples app (#459)
* Add UseCaseController
* Add Layouts.swift
* Add PagePanelController
* Append access modifiers into some objects
* Split view controllers into each file
* Organize the project
* Rename currentMenu with currentUseCase
* Rename SampleListVC with MainVC
* Update UseCase enum
* Update DebugTableViewController to test the scroll (un)tracking functions
2021-05-08 13:17:02 +09:00
Shin Yamamoto 5b176b5cef Tidy up Extensions.swift 2021-04-24 09:38:13 +09:00
Shin Yamamoto 15709f62f8 Remove 'UI' prefix from UIExtensions 2021-04-24 09:38:13 +09:00
Shin Yamamoto 9168236534 Improve the function rounding a dimension by a display scale 2021-04-24 09:37:58 +09:00
Shin Yamamoto 16e709e8ab Fix backdrop flickering (#449)
* Add test_updateBackdropAlpha
* Fix backdrop alpha's flickers in Maps.app

This issue occurs when swinging down a panel with all one's might.
The trigger is here.
```
    func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
        if targetState.pointee != .full {
            owner.searchVC.hideHeader(animated: true)
        }
        if targetState.pointee == .tip {
>>>         vc.contentMode = .static
        }
    }
```
However, any library users expect to affect the backdrop by this code.
And then I reconsidered the reason why the backdrop alpha changes in
activateLayout(for:forceLayout:) and it's because the animation using
CAAnimation on v1.

Therefore I decided to move the point to change the backdrop alpha into
the move animation's completion handler.

And also the responsibility of `setBackdropAlpha(of:)` was moved into
`Core` because `Core` takes on a role of changing the backdrop alpha.
2021-04-13 18:26:22 +09:00
Shin Yamamoto 94c39f0f34 Merge pull request #442 from scenee/release-2.3.0
Release v2.3.0
2021-02-27 09:41:36 +09:00
Shin Yamamoto 16fea625be Version 2.3.0 2021-02-23 13:45:46 +09:00
Shin Yamamoto 6c69694cfe Update readme 2021-02-23 13:45:46 +09:00
Shin Yamamoto 561d783479 Prevent a memory leak in a nested function (#441)
To fix a crash "Layout.swift: Fatal error: Attempted to read an unowned reference but the object was already deallocated"  which is reported at issue #440.
2021-02-23 09:37:35 +09:00
Shin Yamamoto 1bd2e60200 Enable to add custom panel states (#438)
* Support over 3 states in LayoutAdapter
* Allow to inherite FloatingPanelState
* Support a custome FloatingPanelState in ObjC
* Replace Menu enum with UseCase enum in Samples.app
* Rename UIExtensions to Extensions
* Add CustomState use case in Samples app
2021-02-15 21:05:12 +09:00
Shin Yamamoto ec0e1b2dad Remove class keyword
class keyword will be deprecated and in Swift 5 doesn't need it for a
protocol attributed by @objc.
2021-02-12 12:15:58 +09:00
Shin Yamamoto 9958fc5017 Prevent the potential memory leaks in the modal transition (#429)
This dismisses the frame 'FloatingPanel Core.move(from:to:animated:completion:)'
in the following memory leaks

> BoardServices -[BSXPCServiceConnectionEventHandler remoteTarget]
> BoardServices __63+[BSXPCServiceConnectionProxy createImplementationForProtocol:]_block_invoke

These leaks happens when a panel showes and hides using "Show Multi Panel Modal"
in the Samples app.
2021-02-06 09:16:11 +09:00
Shin Yamamoto 11dfc0d2f3 Fix workaround for bouncing a scroll content 2021-02-06 09:05:40 +09:00
Shin Yamamoto 34246d1f37 Merge pull request #435 from SCENEE/release-2.2.0
Prepare v2.2.0
2021-01-24 16:08:13 +09:00
94 changed files with 4825 additions and 2990 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
+84 -47
View File
@@ -10,71 +10,108 @@ on:
jobs:
build:
runs-on: macOS-10.15
runs-on: ${{ matrix.runsOn }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
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.4.1"
runsOn: macos-12
- swift: "5.5"
xcode: "13.2.1"
runsOn: macos-11
- swift: "5.4"
xcode: "12.5.1"
runsOn: macos-11
- swift: "5.3"
xcode: "12.4"
runsOn: macos-11
- swift: "5.2"
xcode: "11.7"
runsOn: macos-11
steps:
- uses: actions/checkout@v1
- name: "Swift 5.1"
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 clean build
env:
DEVELOPER_DIR: /Applications/Xcode_11.3.1.app/Contents/Developer
- name: "Swift 5.2"
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.2 clean build
env:
DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
- name: "Swift 5.3"
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.3 clean build
env:
DEVELOPER_DIR: /Applications/Xcode_12.3.app/Contents/Developer
- uses: actions/checkout@v3
- name: Building in Swift ${{ matrix.swift }}
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=${{ matrix.swift }} clean build
testing:
runs-on: macOS-10.15
test:
runs-on: ${{ matrix.runsOn }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
- os: "16.4"
xcode: "14.3.1"
sim: "iPhone 14 Pro"
runsOn: macos-13
- os: "15.5"
xcode: "13.4.1"
sim: "iPhone 13 Pro"
runsOn: macos-12
steps:
- uses: actions/checkout@v1
- name: "Testing in iOS 13.7"
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.7,name=iPhone 11 Pro'
env:
DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
- name: "Testing in iOS 14.3"
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.3,name=iPhone 12 Pro'
env:
DEVELOPER_DIR: /Applications/Xcode_12.3.app/Contents/Developer
- 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-10.15
runs-on: macos-12
env:
DEVELOPER_DIR: /Applications/Xcode_12.3.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
- example: "Maps"
- example: "Maps-SwiftUI"
- example: "Stocks"
- example: "Samples"
steps:
- uses: actions/checkout@v1
- name: "Build Maps"
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Maps -sdk iphonesimulator clean build
- name: "Build Stocks"
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Stocks -sdk iphonesimulator clean build
- name: "Build Samples"
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Samples -sdk iphonesimulator clean build
- uses: actions/checkout@v3
- name: Building ${{ matrix.example }}
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme ${{ matrix.example }} -sdk iphonesimulator clean build
swiftpm:
runs-on: macOS-10.15
runs-on: macos-12
env:
DEVELOPER_DIR: /Applications/Xcode_12.3.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
# 15.7
- target: "x86_64-apple-ios15.7-simulator"
- target: "arm64-apple-ios15.7-simulator"
# 16.1
- target: "x86_64-apple-ios16.1-simulator"
- target: "arm64-apple-ios16.1-simulator"
steps:
- uses: actions/checkout@v1
- name: "Swift Package build"
run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios14.3-simulator"
- 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-10.15
env:
# Carthage doesn't fix issues on Xcode 12: https://github.com/Carthage/Carthage/releases/tag/0.36.0
DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
runs-on: macos-11
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- name: "Carthage build"
run: carthage build --no-skip-current
run: carthage build --use-xcframeworks --no-skip-current
cocoapods:
runs-on: macOS-10.15
runs-on: macos-12
steps:
- uses: actions/checkout@v1
- 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
-3
View File
@@ -1,3 +0,0 @@
--header "// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license."
--disable andOperator,anyObjectProtocol,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenScopes,braces,consecutiveBlankLines,consecutiveSpaces,duplicateImports,elseOnSameLine,emptyBraces,hoistPatternLet,indent,isEmpty,leadingDelimiters,linebreakAtEndOfFile,linebreaks,numberFormatting,redundantBackticks,redundantBreak,redundantExtensionACL,redundantFileprivate,redundantGet,redundantInit,redundantLet,redundantLetError,redundantNilInit,redundantObjc,redundantParens,redundantPattern,redundantRawValues,redundantReturn,redundantSelf,redundantVoidReturnType,semicolons,sortedImports,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBraces,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,specifiers,strongOutlets,strongifiedSelf,todos,trailingClosures,trailingCommas,trailingSpace,typeSugar,unusedArguments,void,wrapArguments,yodaConditions
-20
View File
@@ -1,20 +0,0 @@
language: objective-c
branches:
only:
- master
env:
global:
- LANG=en_US.UTF-8
- LC_ALL=en_US.UTF-8
jobs:
include:
- stage: "Tests"
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE (1st generation)'
osx_image: xcode11.6
name: "iPhone SE (iOS 10.3)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
osx_image: xcode11.6
name: "iPhone 7 (iOS 11.4)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.4,name=iPhone X'
osx_image: xcode11.6
name: "iPhone X (iOS 12.4)"
@@ -0,0 +1,426 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
6467E8642699AC5F00565F4F /* SurfaceAppearance+phone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6467E8632699AC5F00565F4F /* SurfaceAppearance+phone.swift */; };
6467E86A2699B19D00565F4F /* SearchPanelPhoneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6467E8692699B19D00565F4F /* SearchPanelPhoneDelegate.swift */; };
649A122926C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A122826C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift */; };
649A122D26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A122C26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift */; };
64A5B7232691323900BCAA05 /* MapsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B7222691323900BCAA05 /* MapsApp.swift */; };
64A5B7252691323900BCAA05 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B7242691323900BCAA05 /* ContentView.swift */; };
64A5B734269133DC00BCAA05 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A5B733269133DC00BCAA05 /* FloatingPanel.framework */; };
64A5B735269133DC00BCAA05 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 64A5B733269133DC00BCAA05 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
64A5B738269134CA00BCAA05 /* VisualEffectBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B737269134CA00BCAA05 /* VisualEffectBlur.swift */; };
64A5B73C2691469900BCAA05 /* FloatingPanelContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B73B2691469900BCAA05 /* FloatingPanelContentView.swift */; };
64A5B73E269147DC00BCAA05 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B73D269147DC00BCAA05 /* SearchBar.swift */; };
64A5B7402691532400BCAA05 /* ResultsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B73F2691532400BCAA05 /* ResultsList.swift */; };
64A5B7422691541A00BCAA05 /* HostingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B7412691541A00BCAA05 /* HostingCell.swift */; };
64F7E83126AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82C26AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift */; };
64F7E83226AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82D26AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift */; };
64F7E83326AD70EB00A0E0F7 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82E26AD70EB00A0E0F7 /* FloatingPanelView.swift */; };
64F7E83426AD70EB00A0E0F7 /* View+floatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82F26AD70EB00A0E0F7 /* View+floatingPanel.swift */; };
64F7E83526AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E83026AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
64A5B736269133DC00BCAA05 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
64A5B735269133DC00BCAA05 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
6467E8632699AC5F00565F4F /* SurfaceAppearance+phone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceAppearance+phone.swift"; sourceTree = "<group>"; };
6467E8692699B19D00565F4F /* SearchPanelPhoneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchPanelPhoneDelegate.swift; sourceTree = "<group>"; };
649A122826C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIHostingController+ignoreKeyboard.swift"; sourceTree = "<group>"; };
649A122C26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelGrabberHandlePadding.swift"; sourceTree = "<group>"; };
64A5B71F2691323900BCAA05 /* Maps-SwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Maps-SwiftUI.app"; sourceTree = BUILT_PRODUCTS_DIR; };
64A5B7222691323900BCAA05 /* MapsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapsApp.swift; sourceTree = "<group>"; };
64A5B7242691323900BCAA05 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
64A5B733269133DC00BCAA05 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
64A5B737269134CA00BCAA05 /* VisualEffectBlur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectBlur.swift; sourceTree = "<group>"; };
64A5B73B2691469900BCAA05 /* FloatingPanelContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelContentView.swift; sourceTree = "<group>"; };
64A5B73D269147DC00BCAA05 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
64A5B73F2691532400BCAA05 /* ResultsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsList.swift; sourceTree = "<group>"; };
64A5B7412691541A00BCAA05 /* HostingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingCell.swift; sourceTree = "<group>"; };
64F7E82C26AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelContentInsetAdjustmentBehavior.swift"; sourceTree = "<group>"; };
64F7E82D26AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelContentMode.swift"; sourceTree = "<group>"; };
64F7E82E26AD70EB00A0E0F7 /* FloatingPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
64F7E82F26AD70EB00A0E0F7 /* View+floatingPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanel.swift"; sourceTree = "<group>"; };
64F7E83026AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelSurfaceAppearance.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
64A5B71C2691323900BCAA05 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
64A5B734269133DC00BCAA05 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
64A5B7162691323900BCAA05 = {
isa = PBXGroup;
children = (
64A5B7212691323900BCAA05 /* Maps */,
64A5B7202691323900BCAA05 /* Products */,
64A5B732269133DC00BCAA05 /* Frameworks */,
);
sourceTree = "<group>";
};
64A5B7202691323900BCAA05 /* Products */ = {
isa = PBXGroup;
children = (
64A5B71F2691323900BCAA05 /* Maps-SwiftUI.app */,
);
name = Products;
sourceTree = "<group>";
};
64A5B7212691323900BCAA05 /* Maps */ = {
isa = PBXGroup;
children = (
64F7E82B26AD70EB00A0E0F7 /* FloatingPanel */,
64BE55702691BBB0006D98BD /* Representable */,
64A5B7222691323900BCAA05 /* MapsApp.swift */,
64A5B7242691323900BCAA05 /* ContentView.swift */,
64A5B73B2691469900BCAA05 /* FloatingPanelContentView.swift */,
6467E8692699B19D00565F4F /* SearchPanelPhoneDelegate.swift */,
6467E8632699AC5F00565F4F /* SurfaceAppearance+phone.swift */,
64A5B7412691541A00BCAA05 /* HostingCell.swift */,
64A5B73F2691532400BCAA05 /* ResultsList.swift */,
);
path = Maps;
sourceTree = "<group>";
};
64A5B732269133DC00BCAA05 /* Frameworks */ = {
isa = PBXGroup;
children = (
64A5B733269133DC00BCAA05 /* FloatingPanel.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
64BE55702691BBB0006D98BD /* Representable */ = {
isa = PBXGroup;
children = (
64A5B73D269147DC00BCAA05 /* SearchBar.swift */,
64A5B737269134CA00BCAA05 /* VisualEffectBlur.swift */,
);
path = Representable;
sourceTree = "<group>";
};
64F7E82B26AD70EB00A0E0F7 /* FloatingPanel */ = {
isa = PBXGroup;
children = (
64F7E82E26AD70EB00A0E0F7 /* FloatingPanelView.swift */,
64F7E82F26AD70EB00A0E0F7 /* View+floatingPanel.swift */,
64F7E82C26AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift */,
64F7E82D26AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift */,
64F7E83026AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift */,
649A122C26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift */,
649A122826C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift */,
);
path = FloatingPanel;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
64A5B71E2691323900BCAA05 /* Maps-SwiftUI */ = {
isa = PBXNativeTarget;
buildConfigurationList = 64A5B72D2691323A00BCAA05 /* Build configuration list for PBXNativeTarget "Maps-SwiftUI" */;
buildPhases = (
64A5B71B2691323900BCAA05 /* Sources */,
64A5B71C2691323900BCAA05 /* Frameworks */,
64A5B71D2691323900BCAA05 /* Resources */,
64A5B736269133DC00BCAA05 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "Maps-SwiftUI";
productName = "Maps-SwiftUI";
productReference = 64A5B71F2691323900BCAA05 /* Maps-SwiftUI.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
64A5B7172691323900BCAA05 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1300;
LastUpgradeCheck = 1300;
TargetAttributes = {
64A5B71E2691323900BCAA05 = {
CreatedOnToolsVersion = 13.0;
};
};
};
buildConfigurationList = 64A5B71A2691323900BCAA05 /* Build configuration list for PBXProject "Maps-SwiftUI" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 64A5B7162691323900BCAA05;
productRefGroup = 64A5B7202691323900BCAA05 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
64A5B71E2691323900BCAA05 /* Maps-SwiftUI */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
64A5B71D2691323900BCAA05 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
64A5B71B2691323900BCAA05 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6467E86A2699B19D00565F4F /* SearchPanelPhoneDelegate.swift in Sources */,
64A5B7422691541A00BCAA05 /* HostingCell.swift in Sources */,
64A5B738269134CA00BCAA05 /* VisualEffectBlur.swift in Sources */,
64A5B7402691532400BCAA05 /* ResultsList.swift in Sources */,
6467E8642699AC5F00565F4F /* SurfaceAppearance+phone.swift in Sources */,
64F7E83226AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift in Sources */,
64A5B7252691323900BCAA05 /* ContentView.swift in Sources */,
64A5B7232691323900BCAA05 /* MapsApp.swift in Sources */,
64A5B73C2691469900BCAA05 /* FloatingPanelContentView.swift in Sources */,
649A122D26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift in Sources */,
64F7E83326AD70EB00A0E0F7 /* FloatingPanelView.swift in Sources */,
64F7E83426AD70EB00A0E0F7 /* View+floatingPanel.swift in Sources */,
64F7E83526AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift in Sources */,
64F7E83126AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift in Sources */,
64A5B73E269147DC00BCAA05 /* SearchBar.swift in Sources */,
649A122926C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
64A5B72B2691323A00BCAA05 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
64A5B72C2691323A00BCAA05 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
64A5B72E2691323A00BCAA05 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "example.Maps-SwiftUI";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
64A5B72F2691323A00BCAA05 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "exmaple.Maps-SwiftUI";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
64A5B71A2691323900BCAA05 /* Build configuration list for PBXProject "Maps-SwiftUI" */ = {
isa = XCConfigurationList;
buildConfigurations = (
64A5B72B2691323A00BCAA05 /* Debug */,
64A5B72C2691323A00BCAA05 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
64A5B72D2691323A00BCAA05 /* Build configuration list for PBXNativeTarget "Maps-SwiftUI" */ = {
isa = XCConfigurationList;
buildConfigurations = (
64A5B72E2691323A00BCAA05 /* Debug */,
64A5B72F2691323A00BCAA05 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 64A5B7172691323900BCAA05 /* Project object */;
}
@@ -0,0 +1,33 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
import MapKit
struct ContentView: View {
@State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.623198015869235, longitude: -122.43066818432008),
span: MKCoordinateSpan(latitudeDelta: 0.4425100023575723, longitudeDelta: 0.28543697435880233)
)
var body: some View {
ZStack {
Map(coordinateRegion: $region)
.ignoresSafeArea()
statusBarBlur
}
}
private var statusBarBlur: some View {
GeometryReader { geometry in
VisualEffectBlur()
.frame(height: geometry.safeAreaInsets.top)
.ignoresSafeArea()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
@@ -0,0 +1,133 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
/// A proxy for exposing the methods of the floating panel controller.
public struct FloatingPanelProxy {
/// The associated floating panel controller.
public weak var fpc: FloatingPanelController?
/// Tracks the specified scroll view to correspond with the scroll.
///
/// - Parameter scrollView: Specify a scroll view to continuously and
/// seamlessly work in concert with interactions of the surface view.
public func track(scrollView: UIScrollView) {
fpc?.track(scrollView: scrollView)
}
/// Moves the floating panel to the specified position.
///
/// - Parameters:
/// - floatingPanelState: The state to move to.
/// - animated: `true` to animate the transition to the new state; `false`
/// otherwise.
public func move(
to floatingPanelState: FloatingPanelState,
animated: Bool,
completion: (() -> Void)? = nil
) {
fpc?.move(to: floatingPanelState, animated: animated, completion: completion)
}
}
/// A view with an associated floating panel.
struct FloatingPanelView<Content: View, FloatingPanelContent: View>: UIViewControllerRepresentable {
/// A type that conforms to the `FloatingPanelControllerDelegate` protocol.
var delegate: FloatingPanelControllerDelegate?
/// The behavior for determining the adjusted content offsets.
@Environment(\.contentInsetAdjustmentBehavior) var contentInsetAdjustmentBehavior
/// Constants that define how a panel content fills in the surface.
@Environment(\.contentMode) var contentMode
/// The floating panel grabber handle offset.
@Environment(\.grabberHandlePadding) var grabberHandlePadding
/// The floating panel `surfaceView` appearance.
@Environment(\.surfaceAppearance) var surfaceAppearance
/// The view builder that creates the floating panel parent view content.
@ViewBuilder var content: Content
/// The view builder that creates the floating panel content.
@ViewBuilder var floatingPanelContent: (FloatingPanelProxy) -> FloatingPanelContent
public func makeUIViewController(context: Context) -> UIHostingController<Content> {
let hostingController = UIHostingController(rootView: content)
hostingController.view.backgroundColor = nil
// We need to wait for the current runloop cycle to complete before our
// view is actually added (into the view hierarchy), otherwise the
// environment is not ready yet.
DispatchQueue.main.async {
context.coordinator.setupFloatingPanel(hostingController)
}
return hostingController
}
public func updateUIViewController(
_ uiViewController: UIHostingController<Content>,
context: Context
) {
context.coordinator.updateIfNeeded()
}
public func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
/// `FloatingPanelView` coordinator.
///
/// Responsible to setup the view hierarchy and floating panel.
final class Coordinator {
private let parent: FloatingPanelView<Content, FloatingPanelContent>
private lazy var fpc = FloatingPanelController()
init(parent: FloatingPanelView<Content, FloatingPanelContent>) {
self.parent = parent
}
func setupFloatingPanel(_ parentViewController: UIViewController) {
updateIfNeeded()
let panelContent = parent.floatingPanelContent(FloatingPanelProxy(fpc: fpc))
let hostingViewController = UIHostingController(
rootView: panelContent,
ignoresKeyboard: true
)
hostingViewController.view.backgroundColor = nil
let contentViewController = UIViewController()
contentViewController.view.addSubview(hostingViewController.view)
fpc.set(contentViewController: contentViewController)
fpc.addPanel(toParent: parentViewController, animated: false)
hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
let bottomConstraint = hostingViewController.view.bottomAnchor.constraint(equalTo: contentViewController.view.bottomAnchor)
bottomConstraint.priority = .defaultHigh
NSLayoutConstraint.activate([
hostingViewController.view.topAnchor.constraint(equalTo: contentViewController.view.topAnchor),
hostingViewController.view.leadingAnchor.constraint(equalTo: contentViewController.view.leadingAnchor),
hostingViewController.view.trailingAnchor.constraint(equalTo: contentViewController.view.trailingAnchor),
bottomConstraint
])
}
func updateIfNeeded() {
if fpc.contentInsetAdjustmentBehavior != parent.contentInsetAdjustmentBehavior {
fpc.contentInsetAdjustmentBehavior = parent.contentInsetAdjustmentBehavior
}
if fpc.contentMode != parent.contentMode {
fpc.contentMode = parent.contentMode
}
if fpc.delegate !== parent.delegate {
fpc.delegate = parent.delegate
}
if fpc.surfaceView.grabberHandlePadding != parent.grabberHandlePadding {
fpc.surfaceView.grabberHandlePadding = parent.grabberHandlePadding
}
if fpc.surfaceView.appearance != parent.surfaceAppearance {
fpc.surfaceView.appearance = parent.surfaceAppearance
}
}
}
}
@@ -0,0 +1,44 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
/// This extension makes sure SwiftUI views are not affected by iOS keyboard.
///
/// Credits to https://steipete.me/posts/disabling-keyboard-avoidance-in-swiftui-uihostingcontroller/
extension UIHostingController {
public convenience init(rootView: Content, ignoresKeyboard: Bool) {
self.init(rootView: rootView)
if ignoresKeyboard {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(
cString: class_getName(viewClass)
).appending("_IgnoresKeyboard")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
} else {
guard
let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String,
let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0)
else { return }
if let method = class_getInstanceMethod(
viewClass,
NSSelectorFromString("keyboardWillShowWithNotification:")
) {
let keyboardWillShow: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in }
class_addMethod(
viewSubclass,
NSSelectorFromString("keyboardWillShowWithNotification:"),
imp_implementationWithBlock(keyboardWillShow),
method_getTypeEncoding(method)
)
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
}
@@ -0,0 +1,31 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
extension View {
/// Presents a floating panel using the given closure as its content.
///
/// The modifier's content view builder receives a `FloatingPanelProxy`
/// instance; you use the proxy's methods to interact with the associated
/// `FloatingPanelController`.
///
/// - Parameters:
/// - delegate: A type that conforms to the
/// `FloatingPanelControllerDelegate` protocol. You have comprehensive
/// control over the floating panel behavior when you use a delegate.
/// - floatingPanelContent: The floating panel content. This view builder
/// receives a `FloatingPanelProxy` instance that you use to interact
/// with the `FloatingPanelController`.
public func floatingPanel<FloatingPanelContent: View>(
delegate: FloatingPanelControllerDelegate? = nil,
@ViewBuilder _ floatingPanelContent: @escaping (_: FloatingPanelProxy) -> FloatingPanelContent
) -> some View {
FloatingPanelView(
delegate: delegate,
content: { self },
floatingPanelContent: floatingPanelContent
)
.ignoresSafeArea()
}
}
@@ -0,0 +1,38 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
struct ContentInsetKey: EnvironmentKey {
static var defaultValue: FloatingPanelController.ContentInsetAdjustmentBehavior = .always
}
extension EnvironmentValues {
/// The behavior for determining the adjusted content offsets.
var contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior {
get { self[ContentInsetKey.self] }
set { self[ContentInsetKey.self] = newValue }
}
}
extension View {
/// Sets the content inset adjustment behavior for floating panels within
/// this view.
///
/// Use this modifier to set a specific content inset adjustment behavior
/// for floating panel instances within a view:
///
/// MainView()
/// .floatingPanel { _ in
/// FloatingPanelContent()
/// }
/// .floatingPanelContentInsetAdjustmentBehavior(.never)
///
/// - Parameter contentInsetAdjustmentBehavior: The content inset adjustment
/// behavior to set.
public func floatingPanelContentInsetAdjustmentBehavior(
_ contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior
) -> some View {
environment(\.contentInsetAdjustmentBehavior, contentInsetAdjustmentBehavior)
}
}
@@ -0,0 +1,37 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
struct ContentModeKey: EnvironmentKey {
static var defaultValue: FloatingPanelController.ContentMode = .static
}
extension EnvironmentValues {
/// Used to determine how the floating panel controller lays out the content
/// view when the surface position changes.
var contentMode: FloatingPanelController.ContentMode {
get { self[ContentModeKey.self] }
set { self[ContentModeKey.self] = newValue }
}
}
extension View {
/// Sets the content mode for floating panels within this view.
///
/// Use this modifier to set a specific content mode for floating panel
/// instances within a view:
///
/// MainView()
/// .floatingPanel { _ in
/// FloatingPanelContent()
/// }
/// .floatingPanelContentMode(.static)
///
/// - Parameter contentMode: The content mode to set.
public func floatingPanelContentMode(
_ contentMode: FloatingPanelController.ContentMode
) -> some View {
environment(\.contentMode, contentMode)
}
}
@@ -0,0 +1,36 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
struct GrabberHandlePaddingKey: EnvironmentKey {
static var defaultValue: CGFloat = 6.0
}
extension EnvironmentValues {
/// The offset of the grabber handle from the interactive edge.
var grabberHandlePadding: CGFloat {
get { self[GrabberHandlePaddingKey.self] }
set { self[GrabberHandlePaddingKey.self] = newValue }
}
}
extension View {
/// Sets the grabber handle padding for floating panels within this view.
///
/// Use this modifier to set a specific padding to floating panel instances
/// within a view:
///
/// MainView()
/// .floatingPanel { _ in
/// FloatingPanelContent()
/// }
/// .floatingPanelGrabberHandlePadding(16)
///
/// - Parameter padding: The grabber handle padding to set.
public func floatingPanelGrabberHandlePadding(
_ padding: CGFloat
) -> some View {
environment(\.grabberHandlePadding, padding)
}
}
@@ -0,0 +1,44 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
struct SurfaceAppearanceKey: EnvironmentKey {
static var defaultValue = SurfaceAppearance()
}
extension EnvironmentValues {
/// The appearance of a surface view.
var surfaceAppearance: SurfaceAppearance {
get { self[SurfaceAppearanceKey.self] }
set { self[SurfaceAppearanceKey.self] = newValue }
}
}
extension View {
/// Sets the surface appearance for floating panels within this view.
///
/// Use this modifier to set a specific surface appearance for floating
/// panel instances within a view:
///
/// MainView()
/// .floatingPanel { _ in
/// FloatingPanelContent()
/// }
/// .floatingPanelSurfaceAppearance(.transparent)
///
/// extension SurfaceAppearance {
/// static var transparent: SurfaceAppearance {
/// let appearance = SurfaceAppearance()
/// appearance.backgroundColor = .clear
/// return appearance
/// }
/// }
///
/// - Parameter surfaceAppearance: The surface appearance to set.
public func floatingPanelSurfaceAppearance(
_ surfaceAppearance: SurfaceAppearance
) -> some View {
environment(\.surfaceAppearance, surfaceAppearance)
}
}
@@ -0,0 +1,43 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
struct FloatingPanelContentView: View {
@State private var searchText = ""
@State private var isShowingCancelButton = false
var proxy: FloatingPanelProxy
var body: some View {
VStack(spacing: 0) {
searchBar
resultsList
}
// 👇🏻 for the floating panel grabber handle.
.padding(.top, 6)
.background(
VisualEffectBlur(blurStyle: .systemMaterial)
// If the `VisualEffectBlur` view receives taps, it's going
// to mess up with the whole panel and render it
// non-interactive, make sure it never receives any taps.
.allowsHitTesting(false)
)
.ignoresSafeArea()
}
var searchBar: some View {
SearchBar(
"Search for a place or address",
text: $searchText,
isShowingCancelButton: $isShowingCancelButton
) { isFocused in
proxy.move(to: isFocused ? .full : .half, animated: true)
isShowingCancelButton = isFocused
} onCancel: {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
var resultsList: some View {
ResultsList(onScrollViewCreated: proxy.track(scrollView:))
}
}
@@ -0,0 +1,49 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
/// A `UITableViewCell` that accepts a SwiftUI view as its content.
///
/// Credits to https://noahgilmore.com/blog/swiftui-self-sizing-cells/ .
public final class HostingCell<Content: View>: UITableViewCell {
private let hostingController = UIHostingController<Content?>(
rootView: nil,
ignoresKeyboard: true
)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
hostingController.view.backgroundColor = nil
backgroundColor = .clear
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func set(rootView: Content, parentController: UIViewController) {
hostingController.rootView = rootView
hostingController.view.invalidateIntrinsicContentSize()
let requiresControllerMove = hostingController.parent != parentController
if requiresControllerMove {
parentController.addChild(hostingController)
}
if !contentView.subviews.contains(hostingController.view) {
contentView.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
contentView.addConstraints([
hostingController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
hostingController.view.topAnchor.constraint(equalTo: contentView.topAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
if requiresControllerMove {
hostingController.didMove(toParent: parentController)
}
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
@main
struct MapsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.floatingPanel(delegate: SearchPanelPhoneDelegate()) { proxy in
FloatingPanelContentView(proxy: proxy)
}
.floatingPanelSurfaceAppearance(.phone)
.floatingPanelContentMode(.fitToBounds)
.floatingPanelContentInsetAdjustmentBehavior(.never)
}
}
}
@@ -0,0 +1,74 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
/// UIKit's `UISearchBar`brought to SwiftUI.
public struct SearchBar: UIViewRepresentable {
var title: String
@Binding var text: String
@Binding var isShowingCancelButton: Bool
var onEditingChanged: (Bool) -> Void
var onCancel: () -> Void
public init(
_ title: String = "",
text: Binding<String>,
isShowingCancelButton: Binding<Bool>,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCancel: @escaping () -> Void
) {
self.title = title
self._text = text
self._isShowingCancelButton = isShowingCancelButton
self.onEditingChanged = onEditingChanged
self.onCancel = onCancel
}
public func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.searchBarStyle = .minimal
searchBar.isTranslucent = true
searchBar.placeholder = title
searchBar.delegate = context.coordinator
searchBar.autocapitalizationType = .none
return searchBar
}
public func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
uiView.placeholder = title
uiView.setShowsCancelButton(isShowingCancelButton, animated: true)
}
public func makeCoordinator() -> SearchBar.Coordinator {
Coordinator(parent: self)
}
public class Coordinator: NSObject, UISearchBarDelegate {
var parent: SearchBar
init(parent: SearchBar) {
self.parent = parent
}
public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
parent.text = searchText
}
public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
parent.onEditingChanged(true)
}
public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
}
public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
parent.onEditingChanged(false)
}
public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
parent.onCancel()
}
}
}
@@ -0,0 +1,69 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
@available(iOS, introduced: 13, deprecated: 15, message: "Use iOS 15 material API.")
public struct VisualEffectBlur<Content: View>: UIViewRepresentable {
var blurStyle: UIBlurEffect.Style = .systemMaterial
var vibrancyStyle: UIVibrancyEffectStyle? = nil
@ViewBuilder var content: Content
public func makeUIView(context: Context) -> UIVisualEffectView {
context.coordinator.blurView
}
public func updateUIView(_ view: UIVisualEffectView, context: Context) {
context.coordinator.update(
content: content,
blurStyle: blurStyle,
vibrancyStyle: vibrancyStyle
)
}
public func makeCoordinator() -> Coordinator {
Coordinator(content: content)
}
public class Coordinator {
let blurView = UIVisualEffectView()
let vibrancyView = UIVisualEffectView()
let hostingController: UIHostingController<Content>
init(content: Content) {
hostingController = UIHostingController(rootView: content)
hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostingController.view.backgroundColor = nil
blurView.contentView.addSubview(vibrancyView)
blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
vibrancyView.contentView.addSubview(hostingController.view)
vibrancyView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
func update(content: Content, blurStyle: UIBlurEffect.Style, vibrancyStyle: UIVibrancyEffectStyle?) {
hostingController.rootView = content
let blurEffect = UIBlurEffect(style: blurStyle)
blurView.effect = blurEffect
if let vibrancyStyle = vibrancyStyle {
vibrancyView.effect = UIVibrancyEffect(blurEffect: blurEffect, style: vibrancyStyle)
} else {
vibrancyView.effect = nil
}
hostingController.view.setNeedsDisplay()
}
}
}
public extension VisualEffectBlur where Content == EmptyView {
init(
blurStyle: UIBlurEffect.Style = .systemMaterial,
vibrancyStyle: UIVibrancyEffectStyle? = nil
) {
self.init(blurStyle: blurStyle, vibrancyStyle: vibrancyStyle) {
EmptyView()
}
}
}
@@ -0,0 +1,138 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
struct ResultsList: UIViewControllerRepresentable {
var onScrollViewCreated: (_ scrollView: UIScrollView) -> Void
func makeUIViewController(
context: Context
) -> ResultsTableViewController {
let rtvc = ResultsTableViewController()
onScrollViewCreated(rtvc.tableView)
return rtvc
}
func updateUIViewController(
_ uiViewController: ResultsTableViewController,
context: Context
) {
}
}
final class ResultsTableViewController: UITableViewController {
private let reuseIdentifier = "HostingCell<ResultListCell>"
private enum Section: CaseIterable {
case main
}
private struct TableViewItem: Hashable {
let color: Color
let symbolName: String
let title: String
let description: String
}
private var dataSource: UITableViewDiffableDataSource<Section, TableViewItem>?
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
tableView.backgroundColor = nil
tableView.register(HostingCell<ResultListCell>.self, forCellReuseIdentifier: reuseIdentifier)
configureDataSource()
}
// MARK: UITableViewDataSource
private func configureDataSource() {
dataSource = UITableViewDiffableDataSource
<Section, TableViewItem>(tableView: tableView) { [weak self] tableView, _, tableItem -> UITableViewCell? in
self?.tableView(tableView, cellForTableViewItem: tableItem)
}
tableView.dataSource = dataSource
var snapshot = NSDiffableDataSourceSnapshot<Section, TableViewItem>()
snapshot.appendSections([.main])
let results: [TableViewItem] = (1...100).map {
TableViewItem(
color: Color(red: 255 / 255.0, green: 94 / 255.0 , blue: 94 / 255.0),
symbolName: "heart.fill",
title: "Favorites",
description: "\($0) Places"
)
}
snapshot.appendItems(results, toSection: .main)
dataSource?.apply(snapshot, animatingDifferences: false)
}
private func tableView(
_ tableView: UITableView,
cellForTableViewItem tableViewItem: TableViewItem
) -> UITableViewCell {
let cell: HostingCell<ResultListCell> = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) as! HostingCell<ResultListCell>
setupResultTableViewCell(
cell,
color: tableViewItem.color,
symbolName: tableViewItem.symbolName,
title: tableViewItem.title,
description: tableViewItem.description
)
return cell
}
private func setupResultTableViewCell(
_ cell: HostingCell<ResultListCell>,
color: Color,
symbolName: String,
title: String,
description: String
) {
cell.set(
rootView: ResultListCell(
color: color,
symbolName: symbolName,
title: title,
description: description
),
parentController: self
)
}
// MARK: UITableViewDelegate
override func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath
) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
struct ResultListCell: View {
let color: Color
let symbolName: String
let title: String
let description: String
var body: some View {
HStack {
Image(systemName: symbolName)
.foregroundColor(.white)
.font(.headline)
.padding(8)
.background(Circle().fill(color))
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.system(size: 20, weight: .bold))
.frame(maxWidth: .infinity, alignment: .leading)
Text(description)
.font(.system(size: 13))
.foregroundColor(Color(.secondaryLabel))
}
}
.padding()
}
}
@@ -0,0 +1,12 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import UIKit
final class SearchPanelPhoneDelegate: FloatingPanelControllerDelegate {
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.state == .full {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
@@ -0,0 +1,13 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
extension FloatingPanel.SurfaceAppearance {
static var phone: SurfaceAppearance {
let appearance = SurfaceAppearance()
appearance.cornerCurve = .continuous
appearance.cornerRadius = 8.0
appearance.backgroundColor = .clear
return appearance
}
}
+24 -10
View File
@@ -12,12 +12,13 @@
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */; };
549D23D3233C77D5008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54B5112A216C3D840033A6F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51129216C3D840033A6F3 /* AppDelegate.swift */; };
54B5112C216C3D840033A6F3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B5112B216C3D840033A6F3 /* ViewController.swift */; };
54B5112C216C3D840033A6F3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B5112B216C3D840033A6F3 /* MainViewController.swift */; };
54B5112F216C3D840033A6F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54B5112D216C3D840033A6F3 /* Main.storyboard */; };
54B51131216C3D860033A6F3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 54B51130216C3D860033A6F3 /* Assets.xcassets */; };
54B51134216C3D860033A6F3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54B51132216C3D860033A6F3 /* LaunchScreen.storyboard */; };
54E26CB624A989090066C720 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E26CB524A989090066C720 /* Utils.swift */; };
54E26CB824A98E310066C720 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E26CB724A98E310066C720 /* DetailViewController.swift */; };
5D82A6A728D18422006A44BA /* libswiftCoreGraphics.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D82A6A628D1841E006A44BA /* libswiftCoreGraphics.tbd */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -40,13 +41,14 @@
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54B51126216C3D840033A6F3 /* Maps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Maps.app; sourceTree = BUILT_PRODUCTS_DIR; };
54B51129216C3D840033A6F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
54B5112B216C3D840033A6F3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
54B5112B216C3D840033A6F3 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
54B5112E216C3D840033A6F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
54B51130216C3D860033A6F3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
54B51133216C3D860033A6F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
54B51135216C3D860033A6F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
54E26CB524A989090066C720 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
54E26CB724A98E310066C720 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = "<group>"; };
5D82A6A628D1841E006A44BA /* libswiftCoreGraphics.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreGraphics.tbd; path = usr/lib/swift/libswiftCoreGraphics.tbd; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -55,6 +57,7 @@
buildActionMask = 2147483647;
files = (
543844BD23D2BE2000D5EDE4 /* MapKit.framework in Frameworks */,
5D82A6A728D18422006A44BA /* libswiftCoreGraphics.tbd in Frameworks */,
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -65,6 +68,7 @@
543844BB23D2BE1F00D5EDE4 /* Frameworks */ = {
isa = PBXGroup;
children = (
5D82A6A628D1841E006A44BA /* libswiftCoreGraphics.tbd */,
543844BC23D2BE2000D5EDE4 /* MapKit.framework */,
);
name = Frameworks;
@@ -91,14 +95,14 @@
54B51128216C3D840033A6F3 /* Maps */ = {
isa = PBXGroup;
children = (
54B51130216C3D860033A6F3 /* Assets.xcassets */,
54B51132216C3D860033A6F3 /* LaunchScreen.storyboard */,
54B5112D216C3D840033A6F3 /* Main.storyboard */,
54B51129216C3D840033A6F3 /* AppDelegate.swift */,
54B5112B216C3D840033A6F3 /* ViewController.swift */,
54B5112B216C3D840033A6F3 /* MainViewController.swift */,
549A5F58244673FE0025F312 /* SearchViewController.swift */,
54E26CB724A98E310066C720 /* DetailViewController.swift */,
54E26CB524A989090066C720 /* Utils.swift */,
54B5112D216C3D840033A6F3 /* Main.storyboard */,
54B51130216C3D860033A6F3 /* Assets.xcassets */,
54B51132216C3D860033A6F3 /* LaunchScreen.storyboard */,
54B51135216C3D860033A6F3 /* Info.plist */,
);
path = Maps;
@@ -132,7 +136,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1000;
LastUpgradeCheck = 1000;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = scenee;
TargetAttributes = {
54B51125216C3D840033A6F3 = {
@@ -177,7 +181,7 @@
buildActionMask = 2147483647;
files = (
549A5F59244673FE0025F312 /* SearchViewController.swift in Sources */,
54B5112C216C3D840033A6F3 /* ViewController.swift in Sources */,
54B5112C216C3D840033A6F3 /* MainViewController.swift in Sources */,
54E26CB824A98E310066C720 /* DetailViewController.swift in Sources */,
54B5112A216C3D840033A6F3 /* AppDelegate.swift in Sources */,
54E26CB624A989090066C720 /* Utils.swift in Sources */,
@@ -232,6 +236,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -293,6 +298,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -334,7 +340,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Maps;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(SDKROOT)/usr/lib/swift",
);
PRODUCT_BUNDLE_IDENTIFIER = example.Maps;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -353,7 +363,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Maps;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(SDKROOT)/usr/lib/swift",
);
PRODUCT_BUNDLE_IDENTIFIER = example.Maps;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -27,8 +27,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -38,8 +36,8 @@
ReferencedContainer = "container:Maps.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -63,8 +61,6 @@
ReferencedContainer = "container:Maps.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
-4
View File
@@ -5,9 +5,5 @@ import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
}
+32 -34
View File
@@ -1,28 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
<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-->
<!--Main View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
<viewController id="BYZ-38-t0r" customClass="MainViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<mapView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" mapType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="5Jw-n2-Cpw">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
</mapView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="d9i-3g-8Ja">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="0.0"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="lMa-xa-AVV">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="0.0"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<blurEffect style="prominent"/>
@@ -54,31 +52,31 @@
<objects>
<viewController storyboardIdentifier="SearchViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0S1-Lk-JgE" customClass="SearchViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ncl-E9-yRn">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ye3-uU-bq3">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ED1-gT-FBj">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<searchBar contentMode="redraw" searchBarStyle="minimal" translatesAutoresizingMaskIntoConstraints="NO" id="Zcj-SE-gb8">
<rect key="frame" x="0.0" y="6" width="375" height="56"/>
<rect key="frame" x="0.0" y="6" width="600" height="51"/>
<textInputTraits key="textInputTraits"/>
</searchBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="D7r-re-InH">
<rect key="frame" x="0.0" y="66" width="375" height="746"/>
<rect key="frame" x="0.0" y="61" width="600" height="539"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="u28-LY-hIh" customClass="SearchHeaderView" customModule="Maps" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="116"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="116"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" baselineRelativeArrangement="YES" translatesAutoresizingMaskIntoConstraints="NO" id="era-8w-yA1">
<rect key="frame" x="24" y="10.666666666666664" width="327" height="97.333333333333343"/>
<rect key="frame" x="24" y="10.5" width="552" height="97.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="auI-1v-Yfk">
<rect key="frame" x="0.0" y="0.0" width="60" height="97.333333333333329"/>
<rect key="frame" x="0.0" y="0.0" width="60" height="97.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="food" translatesAutoresizingMaskIntoConstraints="NO" id="ErN-bC-qTx">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
@@ -88,7 +86,7 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Food &amp; Drinks" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nx2-fW-xAm">
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -96,7 +94,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="0vd-sD-XKv">
<rect key="frame" x="89" y="0.0" width="60" height="97.333333333333329"/>
<rect key="frame" x="164" y="0.0" width="60" height="97.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="shopping" translatesAutoresizingMaskIntoConstraints="NO" id="xcm-St-HAo">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
@@ -106,7 +104,7 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="H7q-q2-ga5">
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<string key="text">Shopping
</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -116,7 +114,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Jd8-YL-b5s">
<rect key="frame" x="178" y="0.0" width="60" height="97.333333333333329"/>
<rect key="frame" x="328" y="0.0" width="60" height="97.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="fun" translatesAutoresizingMaskIntoConstraints="NO" id="bMJ-Jn-Gi8">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
@@ -126,7 +124,7 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kKh-45-FZ2">
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<string key="text">Fun
</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -136,7 +134,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="dTL-e1-Arz">
<rect key="frame" x="267" y="0.0" width="60" height="97.333333333333329"/>
<rect key="frame" x="492" y="0.0" width="60" height="97.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="travel" translatesAutoresizingMaskIntoConstraints="NO" id="8h3-fo-pC3">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
@@ -146,7 +144,7 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WBT-Vj-7QA">
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<string key="text">Travel
</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -168,10 +166,10 @@
</view>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="Cell" rowHeight="70" id="LzC-B9-Adb" customClass="SearchCell" customModule="Maps" customModuleProvider="target">
<rect key="frame" x="0.0" y="144" width="375" height="70"/>
<rect key="frame" x="0.0" y="160.5" width="600" height="70"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="LzC-B9-Adb" id="evr-60-laS">
<rect key="frame" x="0.0" y="0.0" width="375" height="70"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="70"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="like" translatesAutoresizingMaskIntoConstraints="NO" id="GEk-yE-lLq">
@@ -183,16 +181,16 @@
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="Gfl-Oy-rsy">
<rect key="frame" x="57" y="12" width="303" height="46"/>
<rect key="frame" x="57" y="12" width="528" height="46"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" tag="1" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Favorites" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Spf-8L-Ne6">
<rect key="frame" x="0.0" y="0.0" width="303" height="22"/>
<rect key="frame" x="0.0" y="0.0" width="528" height="22"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0 Places" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gyo-3V-7U8">
<rect key="frame" x="0.0" y="24" width="303" height="22"/>
<rect key="frame" x="0.0" y="24" width="528" height="22"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.57647058819999997" green="0.57647058819999997" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
@@ -257,18 +255,18 @@
<objects>
<viewController storyboardIdentifier="DetailViewController" id="Tp2-MF-IFz" customClass="DetailViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="FmO-AT-4Y7">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c3d-2e-0b1">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="9fL-a5-0LS">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="kP7-56-wlG">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableView>
</subviews>
@@ -4,7 +4,7 @@ import UIKit
import MapKit
import FloatingPanel
class ViewController: UIViewController {
class MainViewController: UIViewController {
typealias PanelDelegate = FloatingPanelControllerDelegate & UIGestureRecognizerDelegate
// Search Panel
@@ -22,7 +22,9 @@ class ViewController: UIViewController {
storyboard?.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
@IBOutlet weak var mapView: MKMapView!
}
extension MainViewController {
override func viewDidLoad() {
super.viewDidLoad()
fpc.contentMode = .fitToBounds
@@ -40,7 +42,7 @@ class ViewController: UIViewController {
layoutPanelForPhone()
}
setupMapView()
setUpMapView()
setUpSearchView()
}
@@ -53,9 +55,11 @@ class ViewController: UIViewController {
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
teardownMapView()
tearDownMapView()
}
}
extension MainViewController {
func layoutPanelForPad() {
fpc.behavior = SearchPaneliPadBehavior()
fpc.panGestureRecognizer.delegateProxy = fpcDelegate
@@ -76,15 +80,14 @@ class ViewController: UIViewController {
self.didMove(toParent: self)
}
fpc.setAppearanceForPad()
detailFpc.setAppearanceForPad()
[fpc, detailFpc].forEach { $0.setAppearanceForPad() }
}
func layoutPanelForPhone() {
fpc.track(scrollView: searchVC.tableView) // Only track the tabvle view on iPhone
fpc.track(scrollView: searchVC.tableView) // Only track the table view on iPhone
fpc.addPanel(toParent: self, animated: true)
fpc.setAppearanceForPhone()
detailFpc.setAppearanceForPhone()
[fpc, detailFpc].forEach { $0.setAppearanceForPhone()}
}
}
@@ -116,7 +119,7 @@ extension FloatingPanelController {
// MARK: - UISearchBarDelegate
extension ViewController: UISearchBarDelegate {
extension MainViewController: UISearchBarDelegate {
func activate(searchBar: UISearchBar) {
searchBar.showsCancelButton = true
searchVC.showHeader(animated: true)
@@ -147,9 +150,9 @@ extension ViewController: UISearchBarDelegate {
// MARK: - iPhone
class SearchPanelPhoneDelegate: NSObject, FloatingPanelControllerDelegate, UIGestureRecognizerDelegate {
unowned let owner: ViewController
unowned let owner: MainViewController
init(owner: ViewController) {
init(owner: MainViewController) {
self.owner = owner
}
@@ -201,25 +204,16 @@ class SearchPanelPhoneDelegate: NSObject, FloatingPanelControllerDelegate, UIGes
if targetState.pointee != .full {
owner.searchVC.hideHeader(animated: true)
}
if targetState.pointee == .tip {
vc.contentMode = .static
}
}
func floatingPanelDidEndAttracting(_ fpc: FloatingPanelController) {
fpc.contentMode = .fitToBounds
}
}
class SearchPanelLandscapeLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
return [
@@ -239,20 +233,18 @@ class SearchPanelLandscapeLayout: FloatingPanelLayout {
}
class DetailPanelPhoneDelegate: NSObject, FloatingPanelControllerDelegate, UIGestureRecognizerDelegate {
unowned let owner: ViewController
unowned let owner: MainViewController
init(owner: ViewController) {
init(owner: MainViewController) {
self.owner = owner
}
}
class DetailPanelPhoneLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
]
let initialState: FloatingPanelState = .full
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
@@ -262,9 +254,9 @@ class DetailPanelPhoneLayout: FloatingPanelLayout {
// MARK: - iPad
class SearchPanelPadDelegate: NSObject, FloatingPanelControllerDelegate, UIGestureRecognizerDelegate {
unowned let owner: ViewController
unowned let owner: MainViewController
init(owner: ViewController) {
init(owner: MainViewController) {
self.owner = owner
}
@@ -301,13 +293,11 @@ class SearchPanelPadDelegate: NSObject, FloatingPanelControllerDelegate, UIGestu
class SearchPanelPadLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .top
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.tip: FloatingPanelLayoutAnchor(absoluteInset: 80.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 200.0, edge: .top, referenceGuide: .superview),
.full: FloatingPanelLayoutAnchor(absoluteInset: 60.0, edge: .bottom, referenceGuide: .superview),
]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.tip: FloatingPanelLayoutAnchor(absoluteInset: 80.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 200.0, edge: .top, referenceGuide: .superview),
.full: FloatingPanelLayoutAnchor(absoluteInset: 60.0, edge: .bottom, referenceGuide: .superview),
]
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
}
@@ -335,9 +325,9 @@ class SearchPaneliPadBehavior: FloatingPanelBehavior {
}
class DetailPanelPadDelegate: NSObject, FloatingPanelControllerDelegate, UIGestureRecognizerDelegate {
unowned let owner: ViewController
unowned let owner: MainViewController
init(owner: ViewController) {
init(owner: MainViewController) {
self.owner = owner
}
@@ -361,11 +351,9 @@ class DetailPanelPadDelegate: NSObject, FloatingPanelControllerDelegate, UIGestu
class DetailPanelPadLeftLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .left
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 375, edge: .left, referenceGuide: .superview)
]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 375, edge: .left, referenceGuide: .superview)
]
let initialState: FloatingPanelState = .full
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
@@ -374,11 +362,9 @@ class DetailPanelPadLeftLayout: FloatingPanelLayout {
class DetailPanelPadRightLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .right
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 375, edge: .right, referenceGuide: .superview)
]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 375, edge: .right, referenceGuide: .superview)
]
let initialState: FloatingPanelState = .full
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
@@ -387,8 +373,8 @@ class DetailPanelPadRightLayout: FloatingPanelLayout {
// MARK: - MKMapViewDelegate
extension ViewController: MKMapViewDelegate {
func setupMapView() {
extension MainViewController: MKMapViewDelegate {
func setUpMapView() {
let center = CLLocationCoordinate2D(latitude: 37.623198015869235,
longitude: -122.43066818432008)
let span = MKCoordinateSpan(latitudeDelta: 0.4425100023575723,
@@ -400,7 +386,7 @@ extension ViewController: MKMapViewDelegate {
mapView.delegate = self
}
func teardownMapView() {
func tearDownMapView() {
// Prevent a crash
mapView.delegate = nil
mapView = nil
@@ -4,7 +4,7 @@ import UIKit
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
extension MainViewController: UITableViewDelegate {
func setUpSearchView() {
searchVC.loadViewIfNeeded()
searchVC.tableView.delegate = self
+109 -280
View File
@@ -7,36 +7,34 @@
objects = {
/* Begin PBXBuildFile section */
5442E22425FC51AF00A26F43 /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22325FC51AF00A26F43 /* ImageViewController.swift */; };
5442E22825FC51E200A26F43 /* MultiPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22725FC51E200A26F43 /* MultiPanelController.swift */; };
5442E22C25FC521F00A26F43 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22B25FC521F00A26F43 /* SettingsViewController.swift */; };
5442E23025FC525200A26F43 /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22F25FC525200A26F43 /* TabBarViewController.swift */; };
5442E23425FC528400A26F43 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E23325FC528400A26F43 /* DetailViewController.swift */; };
5442E23A25FC52CD00A26F43 /* ModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E23925FC52CD00A26F43 /* ModalViewController.swift */; };
5442E24025FC533800A26F43 /* DebugTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E23F25FC533800A26F43 /* DebugTableViewController.swift */; };
5442E24425FC538200A26F43 /* InspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E24325FC538200A26F43 /* InspectorViewController.swift */; };
5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E24925FC53C100A26F43 /* DebugTextViewController.swift */; };
5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E25125FC541700A26F43 /* NestedScrollViewController.swift */; };
54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54496C58263A7E5A0031E0C8 /* UseCaseController.swift */; };
544BC56826CC918200D0A436 /* AdaptiveLayoutTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */; };
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9ED21511E6300CA77B8 /* AppDelegate.swift */; };
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* ViewController.swift */; };
545DB9F021511E6300CA77B8 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* MainViewController.swift */; };
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 */; };
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* UIExtensions.swift */; };
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* UIComponents.swift */; };
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* Extensions.swift */; };
54CDC5D8215BBE23007D205C /* SupplementaryViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* SupplementaryViews.swift */; };
54EAD35B263A75EB006A36EA /* PanelLayouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EAD35A263A75EB006A36EA /* PanelLayouts.swift */; };
54EAD365263A765F006A36EA /* PagePanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EAD364263A765F006A36EA /* PagePanelController.swift */; };
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;
@@ -52,22 +50,33 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5442E22325FC51AF00A26F43 /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
5442E22725FC51E200A26F43 /* MultiPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiPanelController.swift; sourceTree = "<group>"; };
5442E22B25FC521F00A26F43 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
5442E22F25FC525200A26F43 /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = "<group>"; };
5442E23325FC528400A26F43 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = "<group>"; };
5442E23925FC52CD00A26F43 /* ModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalViewController.swift; sourceTree = "<group>"; };
5442E23F25FC533800A26F43 /* DebugTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTableViewController.swift; sourceTree = "<group>"; };
5442E24325FC538200A26F43 /* InspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorViewController.swift; sourceTree = "<group>"; };
5442E24925FC53C100A26F43 /* DebugTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTextViewController.swift; sourceTree = "<group>"; };
5442E25125FC541700A26F43 /* NestedScrollViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedScrollViewController.swift; sourceTree = "<group>"; };
54496C58263A7E5A0031E0C8 /* UseCaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCaseController.swift; sourceTree = "<group>"; };
544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveLayoutTestViewController.swift; sourceTree = "<group>"; };
545DB9EA21511E6300CA77B8 /* Samples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Samples.app; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
545DB9EF21511E6300CA77B8 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
545DB9EF21511E6300CA77B8 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
545DB9F221511E6300CA77B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
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; };
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
54CDC5D7215BBE23007D205C /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = "<group>"; };
54B51115216AFE5F0033A6F3 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
54CDC5D7215BBE23007D205C /* SupplementaryViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupplementaryViews.swift; sourceTree = "<group>"; };
54EAD35A263A75EB006A36EA /* PanelLayouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanelLayouts.swift; sourceTree = "<group>"; };
54EAD364263A765F006A36EA /* PagePanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePanelController.swift; sourceTree = "<group>"; };
5D82A6AC28D18438006A44BA /* libswiftCoreGraphics.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreGraphics.tbd; path = usr/lib/swift/libswiftCoreGraphics.tbd; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -75,35 +84,39 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5D82A6AD28D1843C006A44BA /* libswiftCoreGraphics.tbd in Frameworks */,
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */,
);
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 */
5442E22225FC519700A26F43 /* ContentViewControllers */ = {
isa = PBXGroup;
children = (
5442E23F25FC533800A26F43 /* DebugTableViewController.swift */,
5442E24925FC53C100A26F43 /* DebugTextViewController.swift */,
5442E23325FC528400A26F43 /* DetailViewController.swift */,
5442E24325FC538200A26F43 /* InspectorViewController.swift */,
5442E22325FC51AF00A26F43 /* ImageViewController.swift */,
5442E25125FC541700A26F43 /* NestedScrollViewController.swift */,
5442E23925FC52CD00A26F43 /* ModalViewController.swift */,
5442E22725FC51E200A26F43 /* MultiPanelController.swift */,
5442E22B25FC521F00A26F43 /* SettingsViewController.swift */,
5442E22F25FC525200A26F43 /* TabBarViewController.swift */,
544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */,
);
path = ContentViewControllers;
sourceTree = "<group>";
};
545DB9E121511E6300CA77B8 = {
isa = PBXGroup;
children = (
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */,
545DB9EC21511E6300CA77B8 /* Sources */,
545DBA0121511E6400CA77B8 /* Tests */,
545DBA0C21511E6400CA77B8 /* UITests */,
545DB9EB21511E6300CA77B8 /* Products */,
5D82A6AB28D18438006A44BA /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -111,8 +124,6 @@
isa = PBXGroup;
children = (
545DB9EA21511E6300CA77B8 /* Samples.app */,
545DB9FE21511E6400CA77B8 /* SamplesTests.xctest */,
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
@@ -124,30 +135,34 @@
545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */,
545DB9F121511E6300CA77B8 /* Main.storyboard */,
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */,
545DB9EF21511E6300CA77B8 /* ViewController.swift */,
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */,
54CDC5D7215BBE23007D205C /* UIComponents.swift */,
545DB9EF21511E6300CA77B8 /* MainViewController.swift */,
546341AA25C6421000CA0596 /* UseCases */,
5442E22225FC519700A26F43 /* ContentViewControllers */,
54EAD35A263A75EB006A36EA /* PanelLayouts.swift */,
546341AB25C6426500CA0596 /* CustomState.swift */,
54CDC5D7215BBE23007D205C /* SupplementaryViews.swift */,
54B51115216AFE5F0033A6F3 /* Extensions.swift */,
545DB9F921511E6400CA77B8 /* Info.plist */,
);
path = Sources;
sourceTree = "<group>";
};
545DBA0121511E6400CA77B8 /* Tests */ = {
546341AA25C6421000CA0596 /* UseCases */ = {
isa = PBXGroup;
children = (
545DBA0221511E6400CA77B8 /* SampleTests.swift */,
545DBA0421511E6400CA77B8 /* Info.plist */,
546341A025C6415100CA0596 /* UseCase.swift */,
54496C58263A7E5A0031E0C8 /* UseCaseController.swift */,
54EAD364263A765F006A36EA /* PagePanelController.swift */,
);
path = Tests;
path = UseCases;
sourceTree = "<group>";
};
545DBA0C21511E6400CA77B8 /* UITests */ = {
5D82A6AB28D18438006A44BA /* Frameworks */ = {
isa = PBXGroup;
children = (
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */,
545DBA0F21511E6400CA77B8 /* Info.plist */,
5D82A6AC28D18438006A44BA /* libswiftCoreGraphics.tbd */,
);
path = UITests;
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
@@ -157,7 +172,6 @@
isa = PBXNativeTarget;
buildConfigurationList = 545DBA1221511E6400CA77B8 /* Build configuration list for PBXNativeTarget "Samples" */;
buildPhases = (
54D7209621D4DB970054A255 /* ShellScript */,
545DB9E621511E6300CA77B8 /* Sources */,
545DB9E721511E6300CA77B8 /* Frameworks */,
545DB9E821511E6300CA77B8 /* Resources */,
@@ -172,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 */
@@ -215,20 +193,12 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1000;
LastUpgradeCheck = 1000;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = scenee;
TargetAttributes = {
545DB9E921511E6300CA77B8 = {
CreatedOnToolsVersion = 10.0;
};
545DB9FD21511E6400CA77B8 = {
CreatedOnToolsVersion = 10.0;
TestTargetID = 545DB9E921511E6300CA77B8;
};
545DBA0821511E6400CA77B8 = {
CreatedOnToolsVersion = 10.0;
TestTargetID = 545DB9E921511E6300CA77B8;
};
};
};
buildConfigurationList = 545DB9E521511E6300CA77B8 /* Build configuration list for PBXProject "Samples" */;
@@ -245,8 +215,6 @@
projectRoot = "";
targets = (
545DB9E921511E6300CA77B8 /* Samples */,
545DB9FD21511E6400CA77B8 /* SamplesTests */,
545DBA0821511E6400CA77B8 /* SamplesUITests */,
);
};
/* End PBXProject section */
@@ -262,85 +230,38 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
545DB9FC21511E6400CA77B8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
545DBA0721511E6400CA77B8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
54D7209621D4DB970054A255 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $(git rev-parse --abbrev-ref HEAD)($(git rev-parse --short HEAD))\" \"$SRCROOT/$INFOPLIST_FILE\"\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
545DB9E621511E6300CA77B8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */,
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */,
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */,
5442E23425FC528400A26F43 /* DetailViewController.swift in Sources */,
54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */,
54CDC5D8215BBE23007D205C /* SupplementaryViews.swift in Sources */,
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */,
544BC56826CC918200D0A436 /* AdaptiveLayoutTestViewController.swift in Sources */,
5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */,
546341AC25C6426500CA0596 /* CustomState.swift in Sources */,
5442E23A25FC52CD00A26F43 /* ModalViewController.swift in Sources */,
5442E22425FC51AF00A26F43 /* ImageViewController.swift in Sources */,
5442E24025FC533800A26F43 /* DebugTableViewController.swift in Sources */,
5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */,
5442E22825FC51E200A26F43 /* MultiPanelController.swift in Sources */,
546341A125C6415100CA0596 /* UseCase.swift in Sources */,
545DB9F021511E6300CA77B8 /* MainViewController.swift in Sources */,
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */,
);
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 */,
5442E23025FC525200A26F43 /* TabBarViewController.swift in Sources */,
54EAD35B263A75EB006A36EA /* PanelLayouts.swift in Sources */,
54EAD365263A765F006A36EA /* PagePanelController.swift in Sources */,
5442E24425FC538200A26F43 /* InspectorViewController.swift in Sources */,
5442E22C25FC521F00A26F43 /* SettingsViewController.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;
@@ -387,6 +308,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -448,6 +370,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -484,12 +407,16 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Sources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelSample;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(SDKROOT)/usr/lib/swift",
);
PRODUCT_BUNDLE_IDENTIFIER = example.Samples;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -503,102 +430,22 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Sources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelSample;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(SDKROOT)/usr/lib/swift",
);
PRODUCT_BUNDLE_IDENTIFIER = example.Samples;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
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 */
@@ -620,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 */;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -27,8 +27,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -38,8 +36,8 @@
ReferencedContainer = "container:Samples.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -61,8 +59,6 @@
ReferencedContainer = "container:Samples.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -15,7 +15,7 @@
<objects>
<navigationController storyboardIdentifier="RootNavigationController" id="RoN-h0-uBD" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="hNW-5m-Omi">
<rect key="frame" x="0.0" y="44" width="375" height="96"/>
<rect key="frame" x="0.0" y="48" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
@@ -29,24 +29,24 @@
<!--Samples-->
<scene sceneID="35L-Gs-Vts">
<objects>
<viewController id="jF4-A0-Eq6" customClass="SampleListViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<viewController id="jF4-A0-Eq6" customClass="MainViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Smh-Bd-AAc">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="7IS-PU-x0P">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="M0G-C8-hAO" style="IBUITableViewCellStyleDefault" id="ySY-oA-g81">
<rect key="frame" x="0.0" y="28" width="375" height="43.666667938232422"/>
<rect key="frame" x="0.0" y="50" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ySY-oA-g81" id="sXB-nH-2g2">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666667938232422"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="M0G-C8-hAO">
<rect key="frame" x="16" y="0.0" width="343" height="43.666667938232422"/>
<rect key="frame" x="20" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -87,23 +87,23 @@
<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.33333333333334"/>
<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">
<rect key="frame" x="32" y="16" width="311" height="149.33333333333334"/>
<rect key="frame" x="32" y="16" width="311" height="149.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="DCi-Iv-o6d">
<rect key="frame" x="0.0" y="0.0" width="311" height="52.666666666666664"/>
<rect key="frame" x="0.0" y="0.0" width="311" height="53"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WmC-Tq-NDN">
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="17.333333333333332"/>
<rect key="frame" x="118.5" y="0.0" width="74.5" height="17.5"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UINavigationBar" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ulg-gS-ah0">
<rect key="frame" x="78.333333333333329" y="25.333333333333336" width="154.66666666666669" height="27.333333333333336"/>
<rect key="frame" x="78.5" y="25.5" width="154.5" height="27.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -111,38 +111,38 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="J8j-7w-yCZ">
<rect key="frame" x="0.0" y="68.666666666666657" width="311" height="80.666666666666657"/>
<rect key="frame" x="0.0" y="69" width="311" height="80.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
<rect key="frame" x="0.0" y="0.0" width="311" height="32"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Large Titles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogl-S5-4tJ">
<rect key="frame" x="0.0" y="5.9999999999999982" width="254" height="20.333333333333329"/>
<rect key="frame" x="0.0" y="5.5" width="254" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="js8-Qv-lUC">
<rect key="frame" x="262" y="0.66666666666665719" width="51" height="31"/>
<rect key="frame" x="262" y="0.5" width="51" height="31"/>
<connections>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="CxM-wn-r09"/>
</connections>
</switch>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
<rect key="frame" x="0.0" y="47.999999999999986" width="311" height="32.666666666666671"/>
<rect key="frame" x="0.0" y="48" width="311" height="32.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Translucent" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Z5i-rm-QgL">
<rect key="frame" x="0.0" y="6.333333333333341" width="254" height="20.333333333333329"/>
<rect key="frame" x="0.0" y="6" width="254" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="s6b-j9-8Kw">
<rect key="frame" x="262" y="1" width="51" height="31"/>
<rect key="frame" x="262" y="0.5" width="51" height="31"/>
<connections>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="w7r-AV-RqX"/>
</connections>
</switch>
</subviews>
@@ -163,8 +163,8 @@
</view>
<size key="freeformSize" width="375" height="197.33000000000001"/>
<connections>
<outlet property="largeTitlesSwicth" destination="js8-Qv-lUC" id="FOm-6k-ffi"/>
<outlet property="translucentSwicth" destination="s6b-j9-8Kw" id="jmf-WH-bzZ"/>
<outlet property="largeTitlesSwitch" destination="js8-Qv-lUC" id="szl-pU-uRE"/>
<outlet property="translucentSwitch" destination="s6b-j9-8Kw" id="8yK-Du-jkM"/>
<outlet property="versionLabel" destination="WmC-Tq-NDN" id="Woh-kK-U0m"/>
</connections>
</viewController>
@@ -177,11 +177,11 @@
<objects>
<viewController id="RpE-lI-27a" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JER-jz-KSq">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<rect key="frame" x="20" y="48" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="RpE-lI-27a" eventType="touchUpInside" id="hj3-Xv-6Gq"/>
@@ -208,11 +208,11 @@
<objects>
<viewController id="pOk-Zm-vD9" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="85d-ub-G8k">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<rect key="frame" x="20" y="48" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="pOk-Zm-vD9" eventType="touchUpInside" id="111-PD-Pop"/>
@@ -239,11 +239,11 @@
<objects>
<viewController id="lto-Zc-Vtp" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="ji9-Ez-N7i">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<rect key="frame" x="20" y="48" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="YL4-GP-ZEZ"/>
@@ -274,7 +274,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Change this text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ge4-RW-Gmz">
<rect key="frame" x="125.66666666666669" y="24" width="124" height="20.333333333333329"/>
<rect key="frame" x="125.5" y="24" width="124" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -383,22 +383,22 @@
<objects>
<viewController storyboardIdentifier="ModalViewController" id="bYI-y3-Rzb" customClass="ModalViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qwo-GK-p1U">
<rect key="frame" x="0.0" y="0.0" width="375" height="724"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="720"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vut-mK-Y4t" customClass="SafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="724" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="720" width="375" height="0.0"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<rect key="frame" x="20" y="48" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="MSC-ch-YJK"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
<rect key="frame" x="134.66666666666666" y="88" width="106" height="326"/>
<rect key="frame" x="134.5" y="136" width="106" height="326"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
@@ -598,11 +598,11 @@
</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="44" width="375" height="734"/>
<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">
<rect key="frame" x="319" y="44" width="44" height="44"/>
<rect key="frame" x="319" y="48" width="44" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="0jg-5D-A1F"/>
<constraint firstAttribute="width" constant="44" id="1Cq-PA-wgW"/>
@@ -612,10 +612,10 @@
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="qux-uG-4o2">
<rect key="frame" x="8" y="52" width="148" height="31"/>
<rect key="frame" x="8" y="56" width="148" height="31"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="fitToBounds" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7lq-d3-PKi">
<rect key="frame" x="0.0" y="5.3333333333333357" width="91" height="20.333333333333332"/>
<rect key="frame" x="0.0" y="5.5" width="91" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -629,7 +629,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
<rect key="frame" x="130.66666666666666" y="88" width="114" height="134"/>
<rect key="frame" x="130.5" y="88" width="114" height="134"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
<rect key="frame" x="0.0" y="0.0" width="114" height="30"/>
@@ -660,19 +660,19 @@
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="noi-1a-5bZ" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" id="EQy-cr-F2Y"/>
<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" id="JOL-wC-w74"/>
<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"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="g7l-kO-y7q" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="g7l-kO-y7q" secondAttribute="top" priority="750" constant="88" id="Zhb-Ss-epe"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="kkp-Yo-FQW"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
<constraint firstItem="qux-uG-4o2" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="8" id="naa-cf-ZIc"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="oVC-i1-TwS"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" id="rW2-mF-5DR"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" constant="88" id="vKQ-h9-uKt"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" priority="750" id="rW2-mF-5DR"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" priority="750" constant="88" id="vKQ-h9-uKt"/>
<constraint firstItem="qux-uG-4o2" firstAttribute="leading" secondItem="g7l-kO-y7q" secondAttribute="leading" constant="8" id="zXb-R9-bMO"/>
</constraints>
<connections>
@@ -782,6 +782,51 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
</objects>
<point key="canvasLocation" x="-1" y="734"/>
</scene>
<!--Adaptive Layout Test View Controller-->
<scene sceneID="rDI-lU-wEx">
<objects>
<viewController storyboardIdentifier="AdaptiveLayoutTestViewController" id="5nC-6E-bXf" customClass="AdaptiveLayoutTestViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="jXL-Ss-NCJ">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="W7W-ET-Wco" customClass="IntrinsicTableView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="Cell" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" id="Mqi-zK-WA7">
<rect key="frame" x="0.0" y="50" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Mqi-zK-WA7" id="X46-Fp-6Hr">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="5nC-6E-bXf" id="RHg-aY-HNW"/>
<outlet property="delegate" destination="5nC-6E-bXf" id="0YX-fh-bB8"/>
</connections>
</tableView>
</subviews>
<viewLayoutGuide key="safeArea" id="ZfG-sd-dcQ"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="W7W-ET-Wco" firstAttribute="trailing" secondItem="ZfG-sd-dcQ" secondAttribute="trailing" id="3kP-rg-7c6"/>
<constraint firstAttribute="bottom" secondItem="W7W-ET-Wco" secondAttribute="bottom" id="FdS-X9-D1D"/>
<constraint firstItem="W7W-ET-Wco" firstAttribute="leading" secondItem="ZfG-sd-dcQ" secondAttribute="leading" id="HXa-oO-jag"/>
<constraint firstItem="W7W-ET-Wco" firstAttribute="top" secondItem="jXL-Ss-NCJ" secondAttribute="top" id="gMX-Wq-7G8"/>
</constraints>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="tableView" destination="W7W-ET-Wco" id="N54-Fv-2Jq"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="7hJ-XW-9az" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4005" y="734"/>
</scene>
</scenes>
<designables>
<designable name="noi-1a-5bZ">
@@ -803,7 +848,7 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<color red="0.68627450980392157" green="0.32156862745098042" blue="0.87058823529411766" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemTealColor">
<color red="0.35294117647058826" green="0.78431372549019607" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.18823529411764706" green="0.69019607843137254" blue="0.7803921568627451" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
@@ -0,0 +1,89 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
class PanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
private unowned var targetGuide: UILayoutGuide
init(targetGuide: UILayoutGuide) {
self.targetGuide = targetGuide
}
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelAdaptiveLayoutAnchor(
absoluteOffset: 0.0,
contentLayout: targetGuide,
referenceGuide: .superview,
contentBoundingGuide: .safeArea
),
.half: FloatingPanelAdaptiveLayoutAnchor(
fractionalOffset: 0.5,
contentLayout: targetGuide,
referenceGuide: .superview,
contentBoundingGuide: .safeArea
),
]
}
}
@IBOutlet weak var tableView: IntrinsicTableView!
private let cellID = "Cell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableView.automaticDimension
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
}
// MARK: - UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int {
1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
50
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
headerView.backgroundColor = .orange
return headerView
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
44.0
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
40
}
}
class IntrinsicTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
@@ -0,0 +1,302 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
class DebugTableViewController: InspectableViewController {
// MARK: - Views
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
return tableView
}()
lazy var buttonStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.alignment = .trailing
stackView.spacing = 4
return stackView
}()
private lazy var reorderButton: UIButton = {
let button = UIButton()
button.setTitle(Menu.reorder.rawValue, for: .normal)
button.setTitleColor(view.tintColor, for: .normal)
button.addTarget(self, action: #selector(reorderItems), for: .touchUpInside)
return button
}()
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 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 trackScrolling = "Tracking"
case followScrolling = "Following"
case reorder = "Reorder"
}
enum Command: Int, CaseIterable {
case animateScroll
case changeContentSize
case moveToFull
case moveToHalf
var text: String {
switch self {
case .animateScroll: return "Scroll in the middle"
case .changeContentSize: return "Change content size"
case .moveToFull: return "Move to Full"
case.moveToHalf: return "Move to Half"
}
}
static func replace(items: [String]) -> [String] {
return items.enumerated().map { (index, text) -> String in
if let action = Command(rawValue: index) {
return "\(index). \(action.text)"
}
return text
}
}
func execute(for vc: DebugTableViewController, sourceView: UIView) {
switch self {
case .animateScroll:
vc.animateScroll()
case .changeContentSize:
vc.changeContentSize(sourceView: sourceView)
case .moveToFull:
vc.moveToFull()
case .moveToHalf:
vc.moveToHalf()
}
}
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
layoutTableView()
layoutMenuStackView()
setUpMenu()
}
private func layoutTableView() {
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor)
])
}
private func layoutMenuStackView() {
view.addSubview(buttonStackView)
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0),
buttonStackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0),
])
}
private func setUpMenu() {
for menu in Menu.allCases {
switch menu {
case .reorder:
buttonStackView.addArrangedSubview(reorderButton)
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 {
tableView.isEditing = true
reorderButton.setTitle("Cancel", for: .normal)
} else {
tableView.isEditing = false
reorderButton.setTitle(Menu.reorder.rawValue, for: .normal)
}
}
@objc
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) {
command.execute(for: self, sourceView: sourceView)
}
@objc
private func animateScroll() {
tableView.scrollToRow(at: IndexPath(row: lround(Double(items.count) / 2.0),
section: 0),
at: .top, animated: true)
}
@objc
private func changeContentSize(sourceView: UIView) {
let actionSheet = UIAlertController(title: "Change content size", message: "", preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Large", style: .default, handler: { (_) in
self.itemHeight = 66.0
self.changeItems(100)
}))
actionSheet.addAction(UIAlertAction(title: "Match", style: .default, handler: { (_) in
switch self.tableView.bounds.height {
case 585: // iPhone 6,7,8
self.itemHeight = self.tableView.bounds.height / 13.0
self.changeItems(13)
case 656: // iPhone {6,7,8} Plus
self.itemHeight = self.tableView.bounds.height / 16.0
self.changeItems(16)
default: // iPhone X family
self.itemHeight = self.tableView.bounds.height / 12.0
self.changeItems(12)
}
}))
actionSheet.addAction(UIAlertAction(title: "Short", style: .default, handler: { (_) in
self.itemHeight = 66.0
self.changeItems(3)
}))
if let popoverController = actionSheet.popoverPresentationController {
popoverController.sourceView = sourceView
popoverController.sourceRect = sourceView.bounds
}
self.present(actionSheet, animated: true, completion: nil)
}
private func changeItems(_ count: Int) {
items = Command.replace(items: (0..<count).map{ "\($0). No action" })
tableView.reloadData()
}
@objc
private func moveToFull() {
(self.parent as! FloatingPanelController).move(to: .full, animated: true)
}
@objc
private func moveToHalf() {
(self.parent as! FloatingPanelController).move(to: .half, animated: true)
}
@objc
private func close(sender: UIButton) {
// Remove FloatingPanel from a view
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
}
}
extension DebugTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return itemHeight
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
}
extension DebugTableViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if followsScrollViewBouncing {
fpc?.followScrollViewBouncing()
}
print("TableView --- ", scrollView.contentOffset, scrollView.contentInset)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("DebugTableViewController -- select row \(indexPath.row)")
guard let action = Command(rawValue: indexPath.row) else { return }
let cell = tableView.cellForRow(at: indexPath)
execute(command: action, sourceView: cell ?? tableView)
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
return [
UITableViewRowAction(style: .destructive, title: "Delete", handler: { (action, path) in
self.items.remove(at: path.row)
tableView.deleteRows(at: [path], with: .automatic)
}),
]
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
items.insert(items.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
}
}
@@ -0,0 +1,44 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
final class DebugTextViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var textViewTopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
print("viewDidLoad: TextView --- ", textView.contentOffset, textView.contentInset)
if #available(iOS 11.0, *) {
textView.contentInsetAdjustmentBehavior = .never
}
}
override func viewWillLayoutSubviews() {
print("viewWillLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("viewDidLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("TextView --- ", scrollView.contentOffset, scrollView.contentInset)
if #available(iOS 11.0, *) {
print("TextView --- ", scrollView.adjustedContentInset)
}
}
@IBAction func close(sender: UIButton) {
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}
}
@@ -0,0 +1,39 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class DetailViewController: InspectableViewController {
@IBOutlet weak var modeChangeView: UIStackView!
@IBOutlet weak var intrinsicHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var closeButton: UIButton!
@IBAction func close(sender: UIButton) {
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}
@IBAction func buttonPressed(_ sender: UIButton) {
switch sender.titleLabel?.text {
case "Show":
performSegue(withIdentifier: "ShowSegue", sender: self)
case "Present Modally":
performSegue(withIdentifier: "PresentModallySegue", sender: self)
default:
break
}
}
@IBAction func modeChanged(_ sender: Any) {
guard let fpc = parent as? FloatingPanelController else { return }
fpc.contentMode = (fpc.contentMode == .static) ? .fitToBounds : .static
}
@IBAction func tapped(_ sender: Any) {
print("Detail panel is tapped!")
}
@IBAction func swipped(_ sender: Any) {
print("Detail panel is swipped!")
}
@IBAction func longPressed(_ sender: Any) {
print("Detail panel is longPressed!")
}
}
@@ -0,0 +1,65 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class ImageViewController: UIViewController {
class PanelLayout: FloatingPanelLayout {
private unowned var targetGuide: UILayoutGuide
init(targetGuide: UILayoutGuide) {
self.targetGuide = targetGuide
}
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelAdaptiveLayoutAnchor(
absoluteOffset: 0,
contentLayout: targetGuide,
referenceGuide: .superview
),
.half: FloatingPanelAdaptiveLayoutAnchor(
fractionalOffset: 0.5,
contentLayout: targetGuide,
referenceGuide: .superview
)
]
}
}
@IBOutlet weak var headerView: UIView!
@IBOutlet weak var footerView: UIView!
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var stackView: UIStackView!
enum Mode {
case onlyImage
case withHeaderFooter
}
@available(iOS 11.0, *)
func layoutGuideFor(mode: Mode) -> UILayoutGuide {
switch mode {
case .onlyImage:
self.headerView.isHidden = true
self.footerView.isHidden = true
return scrollView.contentLayoutGuide
case .withHeaderFooter:
self.headerView.isHidden = false
self.footerView.isHidden = false
let guide = UILayoutGuide()
view.addLayoutGuide(guide)
NSLayoutConstraint.activate([
scrollView.heightAnchor.constraint(equalTo: scrollView.contentLayoutGuide.heightAnchor),
guide.topAnchor.constraint(equalTo: stackView.topAnchor),
guide.leftAnchor.constraint(equalTo: stackView.leftAnchor),
guide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor),
guide.rightAnchor.constraint(equalTo: stackView.rightAnchor),
])
return guide
}
}
}
@@ -0,0 +1,48 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
class InspectableViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print(">>> Content View: viewWillLayoutSubviews", layoutInsets)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(">>> Content View: viewDidLayoutSubviews", layoutInsets)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print(">>> Content View: viewWillAppear", layoutInsets)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(">>> Content View: viewDidAppear", view.bounds, layoutInsets)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print(">>> Content View: viewWillDisappear")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print(">>> Content View: viewDidDisappear")
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
print(">>> Content View: willMove(toParent: \(String(describing: parent))")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
print(">>> Content View: didMove(toParent: \(String(describing: parent))")
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
print(">>> Content View: willTransition(to: \(newCollection), with: \(coordinator))", layoutInsets)
}
}
@@ -0,0 +1,76 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController!
var consoleVC: DebugTextViewController!
@IBOutlet weak var safeAreaView: UIView!
var isNewlayout: Bool = false
override func viewDidLoad() {
// Initialize FloatingPanelController
fpc = FloatingPanelController()
fpc.delegate = self
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
fpc.surfaceView.appearance = appearance
// Set a content view controller and track the scroll view
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
fpc.set(contentViewController: consoleVC)
fpc.track(scrollView: consoleVC.textView)
self.consoleVC = consoleVC
// Add FloatingPanel to self.view
fpc.addPanel(toParent: self, at: view.subviews.firstIndex(of: safeAreaView) ?? -1)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// Remove FloatingPanel from a view
fpc.removePanelFromParent(animated: false)
}
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
}
@IBAction func moveToFull(sender: UIButton) {
fpc.move(to: .full, animated: true)
}
@IBAction func moveToHalf(sender: UIButton) {
fpc.move(to: .half, animated: true)
}
@IBAction func moveToTip(sender: UIButton) {
fpc.move(to: .tip, animated: true)
}
@IBAction func moveToHidden(sender: UIButton) {
fpc.move(to: .hidden, animated: true)
}
@IBAction func updateLayout(_ sender: Any) {
isNewlayout = !isNewlayout
UIView.animate(withDuration: 0.5) {
self.fpc.invalidateLayout()
}
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return (isNewlayout) ? ModalSecondLayout() : FloatingPanelBottomLayout()
}
class ModalSecondLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .half
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 262, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
@@ -0,0 +1,65 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
import WebKit
final class MultiPanelController: FloatingPanelController, FloatingPanelControllerDelegate {
private final class FirstPanelContentViewController: UIViewController {
lazy var webView: WKWebView = WKWebView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
webView.frame = view.bounds
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView.load(URLRequest(url: URL(string: "https://www.apple.com")!))
let vc = MultiSecondPanelController()
vc.setUpContent()
vc.addPanel(toParent: self)
}
}
private final class MultiSecondPanelController: FloatingPanelController {
private final class SecondPanelContentViewController: DebugTableViewController {}
func setUpContent() {
contentInsetAdjustmentBehavior = .never
let vc = SecondPanelContentViewController()
vc.loadViewIfNeeded()
vc.title = "Second Panel"
vc.buttonStackView.isHidden = true
let navigationController = UINavigationController(rootViewController: vc)
navigationController.navigationBar.barTintColor = .white
navigationController.navigationBar.titleTextAttributes = [
.foregroundColor: UIColor.black
]
set(contentViewController: navigationController)
self.track(scrollView: vc.tableView)
surfaceView.containerMargins = .init(top: 24.0, left: 0.0, bottom: layoutInsets.bottom, right: 0.0)
}
}
override func viewDidLoad() {
super.viewDidLoad()
layout = FirstViewLayout()
isRemovalInteractionEnabled = true
let vc = FirstPanelContentViewController()
set(contentViewController: vc)
track(scrollView: vc.webView.scrollView)
}
private final class FirstViewLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 40.0, edge: .top, referenceGuide: .superview)
]
}
}
@@ -0,0 +1,18 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
final class NestedScrollViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var nestedScrollView: UIScrollView!
@IBAction func longPressed(_ sender: Any) {
print("LongPressed!")
}
@IBAction func swipped(_ sender: Any) {
print("Swipped!")
}
@IBAction func tapped(_ sender: Any) {
print("Tapped!")
}
}
@@ -0,0 +1,48 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class SettingsViewController: InspectableViewController {
@IBOutlet weak var largeTitlesSwitch: UISwitch!
@IBOutlet weak var translucentSwitch: UISwitch!
@IBOutlet weak var versionLabel: UILabel!
override func viewDidLoad() {
versionLabel.text = "Version: \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "--")"
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
largeTitlesSwitch.setOn(prefersLargeTitles, animated: false)
} else {
largeTitlesSwitch.isEnabled = false
}
let isTranslucent = navigationController!.navigationBar.isTranslucent
translucentSwitch.setOn(isTranslucent, animated: false)
}
@IBAction func toggleLargeTitle(_ sender: UISwitch) {
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
}
}
@IBAction func toggleTranslucent(_ sender: UISwitch) {
// White non-translucent navigation bar, supports dark appearance
if #available(iOS 15, *) {
let appearance = UINavigationBarAppearance()
if sender.isOn {
appearance.configureWithTransparentBackground()
} else {
appearance.configureWithOpaqueBackground()
}
navigationController?.navigationBar.standardAppearance = appearance
navigationController?.navigationBar.scrollEdgeAppearance = appearance
} else {
navigationController?.navigationBar.isTranslucent = sender.isOn
}
}
}
@@ -0,0 +1,250 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class TabBarViewController: UITabBarController {}
final class TabBarContentViewController: UIViewController {
enum Tab3Mode {
case changeOffset
case changeAutoLayout
var label: String {
switch self {
case .changeAutoLayout: return "Use AutoLayout(OK)"
case .changeOffset: return "Use ContentOffset(NG)"
}
}
}
lazy var fpc = FloatingPanelController()
var consoleVC: DebugTextViewController!
var threeLayout: ThreeTabBarPanelLayout!
var tab3Mode: Tab3Mode = .changeAutoLayout
var switcherLabel: UILabel!
override func viewDidLoad() {
fpc.delegate = self
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
fpc.surfaceView.appearance = appearance
// Set a content view controller and track the scroll view
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
fpc.set(contentViewController: consoleVC)
consoleVC.textView.delegate = self // MUST call it before fpc.track(scrollView:)
fpc.track(scrollView: consoleVC.textView)
self.consoleVC = consoleVC
// Add FloatingPanel to self.view
fpc.addPanel(toParent: self)
if #available(iOS 15, *) {
tabBarController?.tabBar.scrollEdgeAppearance = UITabBarAppearance()
}
switch tabBarItem.tag {
case 1:
fpc.behavior = TwoTabBarPanelBehavior()
case 2:
let switcher = UISwitch()
fpc.view.addSubview(switcher)
switcher.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
switcher.bottomAnchor.constraint(equalTo: fpc.surfaceView.topAnchor, constant: -16.0),
switcher.rightAnchor.constraint(equalTo: fpc.surfaceView.rightAnchor, constant: -16.0),
])
switcher.isOn = true
switcher.tintColor = .white
switcher.backgroundColor = .white
switcher.layer.cornerRadius = 16.0
switcher.addTarget(self,
action: #selector(changeTab3Mode(_:)),
for: .valueChanged)
let label = UILabel()
label.text = tab3Mode.label
fpc.view.addSubview(label)
switcherLabel = label
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerYAnchor.constraint(equalTo: switcher.centerYAnchor, constant: 0.0),
label.rightAnchor.constraint(equalTo: switcher.leftAnchor, constant: -16.0),
])
// Turn off the mask instead of content inset change
consoleVC.textView.clipsToBounds = false
default:
break
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fpc.invalidateLayout()
}
// MARK: - Action
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
}
// MARK: - Private
@objc
private func changeTab3Mode(_ sender: UISwitch) {
if sender.isOn {
tab3Mode = .changeAutoLayout
} else {
tab3Mode = .changeOffset
}
switcherLabel.text = tab3Mode.label
}
}
extension TabBarContentViewController: UITextViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard self.tabBarItem.tag == 2 else { return }
// Reset an invalid content offset by a user after updating the layout
// of `consoleVC.textView`.
// NOTE: FloatingPanel doesn't implicitly reset the offset(i.e.
// Using KVO of `scrollView.contentOffset`). Because it can lead to an
// infinite loop if a user also resets a content offset as below and,
// in the situation, a user has to modify the library.
if fpc.state != .full, fpc.surfaceLocation.y > fpc.surfaceLocation(for: .full).y {
scrollView.contentOffset = .zero
}
}
}
extension TabBarContentViewController: FloatingPanelControllerDelegate {
// MARK: - FloatingPanel
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
switch self.tabBarItem.tag {
case 0:
return OneTabBarPanelLayout()
case 1:
return TwoTabBarPanelLayout()
case 2:
threeLayout = ThreeTabBarPanelLayout(parent: self)
return threeLayout
default:
return FloatingPanelBottomLayout()
}
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
guard self.tabBarItem.tag == 2 else { return }
switch tab3Mode {
case .changeAutoLayout:
/* Good solution: Manipulate top constraint */
assert(consoleVC.textViewTopConstraint != nil)
let safeAreaTop = vc.layoutInsets.top
if vc.surfaceLocation.y + threeLayout.topPadding < safeAreaTop {
consoleVC.textViewTopConstraint?.constant = min(safeAreaTop - vc.surfaceLocation.y,
safeAreaTop)
} else {
consoleVC.textViewTopConstraint?.constant = threeLayout.topPadding
}
case .changeOffset:
/*
Bad solution: Manipulate scroll content inset
FloatingPanelController keeps a content offset in moving a panel
so that changing content inset or offset causes a buggy behavior.
*/
guard let scrollView = consoleVC.textView else { return }
var insets = vc.adjustedContentInsets
if vc.surfaceView.frame.minY < vc.layoutInsets.top {
insets.top = vc.layoutInsets.top - vc.surfaceView.frame.minY
} else {
insets.top = 0.0
}
scrollView.contentInset = insets
if vc.surfaceView.frame.minY > 0 {
scrollView.contentOffset = CGPoint(x: 0.0,
y: 0.0 - scrollView.contentInset.top)
}
}
if vc.surfaceLocation.y > vc.surfaceLocation(for: .half).y {
let progress = (vc.surfaceLocation.y - vc.surfaceLocation(for: .half).y)
/ (vc.surfaceLocation(for: .tip).y - vc.surfaceLocation(for: .half).y)
threeLayout.leftConstraint.constant = max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
threeLayout.rightConstraint.constant = -max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
} else {
threeLayout.leftConstraint.constant = 0.0
threeLayout.rightConstraint.constant = 0.0
}
}
}
class OneTabBarPanelLayout: FloatingPanelLayout {
var initialState: FloatingPanelState { .tip }
var position: FloatingPanelPosition { .bottom }
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 22.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
class TwoTabBarPanelLayout: FloatingPanelLayout {
let initialState: FloatingPanelState = .half
let position: FloatingPanelPosition = .bottom
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 261.0, edge: .bottom, referenceGuide: .safeArea)
]
}
class TwoTabBarPanelBehavior: FloatingPanelBehavior {
func allowsRubberBanding(for edges: UIRectEdge) -> Bool {
return [UIRectEdge.top, UIRectEdge.bottom].contains(edges)
}
}
class ThreeTabBarPanelLayout: FloatingPanelLayout {
weak var parentVC: UIViewController!
var leftConstraint: NSLayoutConstraint!
var rightConstraint: NSLayoutConstraint!
let topPadding: CGFloat = 17.0
let sideMargin: CGFloat = 16.0
init(parent: UIViewController) {
parentVC = parent
}
var initialState: FloatingPanelState { .half }
var position: FloatingPanelPosition { .bottom }
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 261.0 + parentVC.layoutInsets.bottom, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 88.0 + parentVC.layoutInsets.bottom, edge: .bottom, referenceGuide: .superview),
]
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
} else {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0)
}
return [ leftConstraint, rightConstraint ]
}
}
@@ -0,0 +1,22 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import Foundation
import FloatingPanel
extension FloatingPanelState {
static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
}
class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
.lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
]
}
}
@@ -2,6 +2,20 @@
import UIKit
extension UIView {
func makeBoundsLayoutGuide() -> UILayoutGuide {
let guide = UILayoutGuide()
addLayoutGuide(guide)
NSLayoutConstraint.activate([
guide.topAnchor.constraint(equalTo: topAnchor),
guide.leftAnchor.constraint(equalTo: leftAnchor),
guide.bottomAnchor.constraint(equalTo: bottomAnchor),
guide.rightAnchor.constraint(equalTo: rightAnchor),
])
return guide
}
}
protocol LayoutGuideProvider {
var topAnchor: NSLayoutYAxisAnchor { get }
var bottomAnchor: NSLayoutYAxisAnchor { get }
@@ -0,0 +1,93 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class MainViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var observations: [NSKeyValueObservation] = []
private lazy var useCaseController = UseCaseController(mainVC: self)
}
extension MainViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
automaticallyAdjustsScrollViewInsets = false
let searchController = UISearchController(searchResultsController: nil)
if #available(iOS 11.0, *) {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.largeTitleDisplayMode = .automatic
}
var insets = UIEdgeInsets.zero
insets.bottom += 69.0
tableView.contentInset = insets
// Show the initial panel
useCaseController.set(useCase: .trackingTableView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 11.0, *) {
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
self.tableView.reloadData()
}) {
observations.append(observation)
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
observations.removeAll()
}
}
extension MainViewController {
@IBAction func showDebugMenu(_ sender: UIBarButtonItem) {
useCaseController.setUpSettingsPanel(for: self)
}
}
extension MainViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return UseCase.allCases.count + 30
} else {
return UseCase.allCases.count
}
} else {
return UseCase.allCases.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if UseCase.allCases.count > indexPath.row {
let menu = UseCase.allCases[indexPath.row]
cell.textLabel?.text = menu.name
} else {
cell.textLabel?.text = "\(indexPath.row) row"
}
return cell
}
}
extension MainViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard UseCase.allCases.count > indexPath.row else { return }
// Change panels
useCaseController.set(useCase: UseCase.allCases[indexPath.row])
}
@objc func dismissPresentedVC() {
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
@@ -0,0 +1,78 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
/**
- Attention: `FloatingPanelLayout` must not be applied by the parent view
controller of a panel. But here `MainViewController` adopts it
purposely to check if the library prints an appropriate warning.
*/
extension MainViewController: FloatingPanelLayout {
var position: FloatingPanelPosition { .bottom }
var initialState: FloatingPanelState { .half }
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 262.0, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
class TopPositionedPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .top
let initialState: FloatingPanelState = .full
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 88.0, edge: .bottom, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .top, referenceGuide: .safeArea)
]
}
class IntrinsicPanelLayout: FloatingPanelBottomLayout {
override var initialState: FloatingPanelState { .full }
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea)
]
}
}
class RemovablePanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .half
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 130.0, edge: .bottom, referenceGuide: .safeArea)
]
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
}
class RemovablePanelLandscapeLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .bottom, referenceGuide: .safeArea)
]
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
}
class ModalPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
]
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
}
@@ -3,7 +3,10 @@
import UIKit
@IBDesignable
class CloseButton: UIButton {
final class CloseButton: UIButton {
override var isHighlighted: Bool { didSet { setNeedsDisplay() } }
override var isSelected: Bool { didSet { setNeedsDisplay() } }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
@@ -17,14 +20,11 @@ class CloseButton: UIButton {
self.backgroundColor = .clear
}
func p(_ p: CGFloat) -> CGFloat {
return p * (2.0 / 3.0)
}
override var isHighlighted: Bool { didSet { setNeedsDisplay() } }
override var isSelected: Bool { didSet { setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
func p(_ p: CGFloat) -> CGFloat {
return p * (2.0 / 3.0)
}
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setLineWidth(p(1.0))
@@ -71,7 +71,7 @@ class CloseButton: UIButton {
}
@IBDesignable
class SafeAreaView: UIView {
final class SafeAreaView: UIView {
override func prepareForInterfaceBuilder() {
let label = UILabel()
label.text = "Safe Area"
@@ -86,7 +86,7 @@ class SafeAreaView: UIView {
@IBDesignable
class OnSafeAreaView: UIView {
final class OnSafeAreaView: UIView {
override func prepareForInterfaceBuilder() {
let label = UILabel()
label.text = "On Safe Area"
@@ -0,0 +1,85 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class PagePanelController: NSObject {
var pages: [UIViewController] = []
}
extension PagePanelController {
func makePageViewControllerForContent() -> UIPageViewController {
pages = [DebugTableViewController(), DebugTableViewController(), DebugTableViewController()]
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
pageVC.dataSource = self
pageVC.delegate = self
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
return pageVC
}
func makePageViewController(for vc: MainViewController) -> UIPageViewController {
pages = [UIColor.blue, .red, .green].compactMap({ (color) -> UIViewController in
let page = FloatingPanelController(delegate: self)
page.view.backgroundColor = color
page.panGestureRecognizer.delegateProxy = self
page.show()
return page
})
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
let closeButton = UIButton(type: .custom)
pageVC.view.addSubview(closeButton)
closeButton.setTitle("Close", for: .normal)
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.addTarget(vc, action: #selector(MainViewController.dismissPresentedVC), for: .touchUpInside)
NSLayoutConstraint.activate([
closeButton.topAnchor.constraint(equalTo: pageVC.layoutGuide.topAnchor, constant: 16.0),
closeButton.leftAnchor.constraint(equalTo: pageVC.view.leftAnchor, constant: 16.0),
])
pageVC.dataSource = self
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
pageVC.modalPresentationStyle = .fullScreen
return pageVC
}
}
extension PagePanelController: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return FloatingPanelBottomLayout()
}
}
extension PagePanelController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
extension PagePanelController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard
let index = pages.firstIndex(of: viewController),
index + 1 < pages.count
else { return nil }
return pages[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard
let index = pages.firstIndex(of: viewController),
index - 1 >= 0
else { return nil }
return pages[index - 1]
}
}
extension PagePanelController: UIPageViewControllerDelegate {
// For showPageContent
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed, let page = pageViewController.viewControllers?.first {
(pageViewController.parent as! FloatingPanelController).track(scrollView: (page as! DebugTableViewController).tableView)
}
}
}
@@ -0,0 +1,98 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
enum UseCase: Int, CaseIterable {
case trackingTableView
case trackingTextView
case showDetail
case showModal
case showPanelModal
case showMultiPanelModal
case showPanelInSheetModal
case showOnWindow
case showTabBar
case showPageView
case showPageContentView
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
case showContentInset
case showContainerMargins
case showNavigationController
case showTopPositionedPanel
case showAdaptivePanel
case showAdaptivePanelWithCustomGuide
case showCustomStatePanel
}
extension UseCase {
var name: String {
switch self {
case .trackingTableView: return "Scroll tracking(TableView)"
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
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"
case .showPageContentView: return "Show Page Content View"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
case .showContentInset: return "Show with ContentInset"
case .showContainerMargins: return "Show with ContainerMargins"
case .showNavigationController: return "Show Navigation Controller"
case .showTopPositionedPanel: return "Show Top Positioned Panel"
case .showAdaptivePanel: return "Show Adaptive Panel"
case .showAdaptivePanelWithCustomGuide: return "Show Adaptive Panel (Custom Layout Guide)"
case .showCustomStatePanel: return "Show Panel with Custom state"
}
}
}
extension UseCase {
private enum Content {
case storyboard(String)
case viewController(UIViewController)
}
private var content: Content {
switch self {
case .trackingTableView: return .viewController(DebugTableViewController())
case .trackingTextView: return .storyboard("ConsoleViewController") // Storyboard only
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))
case .showPageView: return .viewController(DebugTableViewController())
case .showPageContentView: return .viewController(DebugTableViewController())
case .showNestedScrollView: return .storyboard(String(describing: NestedScrollViewController.self))
case .showRemovablePanel: return .storyboard(String(describing: DetailViewController.self))
case .showIntrinsicView: return .storyboard("IntrinsicViewController") // Storyboard only
case .showContentInset: return .viewController(DebugTableViewController())
case .showContainerMargins: return .viewController(DebugTableViewController())
case .showNavigationController: return .storyboard("RootNavigationController") // Storyboard only
case .showTopPositionedPanel: return .viewController(DebugTableViewController())
case .showAdaptivePanel: return .storyboard(String(describing: ImageViewController.self))
case .showAdaptivePanelWithCustomGuide: return .storyboard(String(describing: AdaptiveLayoutTestViewController.self))
case .showCustomStatePanel: return .viewController(DebugTableViewController())
}
}
func makeContentViewController(with storyboard: UIStoryboard) -> UIViewController {
switch content {
case .storyboard(let id):
return storyboard.instantiateViewController(withIdentifier: id)
case .viewController(let vc):
vc.loadViewIfNeeded()
return vc
}
}
}
@@ -0,0 +1,413 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class UseCaseController: NSObject {
unowned let mainVC: MainViewController
private(set) var useCase: UseCase
private var mainPanelVC: FloatingPanelController!
private var detailPanelVC: FloatingPanelController!
private var settingsPanelVC: FloatingPanelController!
private lazy var pagePanelController = PagePanelController()
private lazy var overWindowPanelVC = FloatingPanelController()
init(mainVC: MainViewController) {
self.mainVC = mainVC
self.useCase = .trackingTableView
}
}
extension UseCaseController {
func set(useCase: UseCase) {
self.useCase = useCase
let contentVC = useCase.makeContentViewController(with: mainVC.storyboard!)
if let fpc = detailPanelVC {
fpc.removePanelFromParent(animated: true, completion: nil)
self.detailPanelVC = nil
}
switch useCase {
case .trackingTableView:
let fpc = FloatingPanelController()
fpc.delegate = self
fpc.contentInsetAdjustmentBehavior = .always
fpc.surfaceView.appearance = {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
return appearance
}()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(UseCaseController.handleSurface(tapGesture:)))
tapGesture.cancelsTouchesInView = false
tapGesture.numberOfTapsRequired = 2
// Prevents a delay to response a tap in menus of DebugTableViewController.
tapGesture.delaysTouchesEnded = false
fpc.surfaceView.addGestureRecognizer(tapGesture)
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
addMain(panel: fpc)
case .trackingTextView:
let fpc = FloatingPanelController()
fpc.delegate = self
fpc.contentInsetAdjustmentBehavior = .always
fpc.surfaceView.appearance = {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
return appearance
}()
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
addMain(panel: fpc)
case .showDetail:
// Initialize FloatingPanelController
let fpc = FloatingPanelController()
fpc.delegate = self
fpc.surfaceView.appearance = {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
return appearance
}()
// Set a content view controller
fpc.set(contentViewController: contentVC)
fpc.contentMode = .fitToBounds
(contentVC as? DetailViewController)?.intrinsicHeightConstraint.isActive = false
detailPanelVC = fpc
// Add FloatingPanel to self.view
fpc.addPanel(toParent: mainVC, animated: true)
case .showModal, .showTabBar:
let modalVC = contentVC
modalVC.modalPresentationStyle = .fullScreen
mainVC.present(modalVC, animated: true, completion: nil)
case .showPageView:
let pageVC = pagePanelController.makePageViewController(for: mainVC)
mainVC.present(pageVC, animated: true, completion: nil)
case .showPageContentView:
let fpc = FloatingPanelController()
fpc.delegate = self
fpc.contentInsetAdjustmentBehavior = .always
fpc.surfaceView.appearance = {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
return appearance
}()
let pageVC = pagePanelController.makePageViewControllerForContent()
if let page = (fpc.contentViewController as? UIPageViewController)?.viewControllers?.first {
fpc.track(scrollView: (page as! DebugTableViewController).tableView)
}
fpc.set(contentViewController: pageVC)
addMain(panel: fpc)
case .showRemovablePanel, .showIntrinsicView:
let fpc = FloatingPanelController()
fpc.isRemovalInteractionEnabled = true
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
fpc.delegate = self
fpc.contentInsetAdjustmentBehavior = .always
fpc.surfaceView.appearance = {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
return appearance
}()
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
addMain(panel: fpc)
case .showNestedScrollView:
let fpc = FloatingPanelController()
fpc.panGestureRecognizer.delegateProxy = self
fpc.delegate = self
fpc.contentInsetAdjustmentBehavior = .always
fpc.surfaceView.appearance = {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
return appearance
}()
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
addMain(panel: fpc)
case .showPanelModal:
let fpc = FloatingPanelController()
let contentVC = mainVC.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
contentVC.loadViewIfNeeded()
(contentVC as? DetailViewController)?.modeChangeView.isHidden = true
fpc.set(contentViewController: contentVC)
fpc.delegate = self
let appearance = SurfaceAppearance()
appearance.cornerRadius = 38.5
fpc.surfaceView.appearance = appearance
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
fpc.isRemovalInteractionEnabled = true
mainVC.present(fpc, animated: true, completion: nil)
case .showMultiPanelModal:
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()
fpc.set(contentViewController: contentVC)
fpc.delegate = self
let apprearance = SurfaceAppearance()
apprearance.cornerRadius = 38.5
apprearance.shadows = []
fpc.surfaceView.appearance = apprearance
fpc.isRemovalInteractionEnabled = true
let mvc = UIViewController()
mvc.view.backgroundColor = UIColor(displayP3Red: 2/255, green: 184/255, blue: 117/255, alpha: 1.0)
fpc.addPanel(toParent: mvc)
mainVC.present(mvc, animated: true, completion: nil)
case .showContentInset:
let contentViewController = UIViewController()
contentViewController.view.backgroundColor = .green
let fpc = FloatingPanelController()
fpc.set(contentViewController: contentViewController)
fpc.surfaceView.contentPadding = .init(top: 20, left: 20, bottom: 20, right: 20)
fpc.delegate = self
fpc.isRemovalInteractionEnabled = true
mainVC.present(fpc, animated: true, completion: nil)
case .showContainerMargins:
let fpc = FloatingPanelController()
let appearance = SurfaceAppearance()
appearance.cornerRadius = 38.5
fpc.surfaceView.appearance = appearance
fpc.surfaceView.backgroundColor = .red
fpc.surfaceView.containerMargins = .init(top: 24.0, left: 8.0, bottom: max(mainVC.layoutInsets.bottom, 8.0), right: 8.0)
#if swift(>=5.1) // Actually Xcode 11 or later
if #available(iOS 13.0, *) {
fpc.surfaceView.layer.cornerCurve = .continuous
}
#endif
fpc.delegate = self
fpc.isRemovalInteractionEnabled = true
mainVC.present(fpc, animated: true, completion: nil)
case .showNavigationController:
let fpc = FloatingPanelController()
fpc.contentInsetAdjustmentBehavior = .never
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
addMain(panel: fpc)
case .showTopPositionedPanel: // For debug
let fpc = FloatingPanelController(delegate: self)
let contentVC = UIViewController()
contentVC.view.backgroundColor = .red
fpc.set(contentViewController: contentVC)
addMain(panel: fpc)
case .showAdaptivePanel:
let fpc = FloatingPanelController()
fpc.isRemovalInteractionEnabled = true
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
if case let contentVC as ImageViewController = contentVC {
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithCustomGuide) ? .withHeaderFooter : .onlyImage
let layoutGuide = contentVC.layoutGuideFor(mode: mode)
fpc.layout = ImageViewController.PanelLayout(targetGuide: layoutGuide)
}
addMain(panel: fpc)
case .showAdaptivePanelWithCustomGuide:
let fpc = FloatingPanelController()
fpc.isRemovalInteractionEnabled = true
fpc.contentInsetAdjustmentBehavior = .always
fpc.surfaceView.appearance = {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
return appearance
}()
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
fpc.layout = AdaptiveLayoutTestViewController.PanelLayout(targetGuide: contentVC.view.makeBoundsLayoutGuide())
addMain(panel: fpc)
case .showCustomStatePanel:
let fpc = FloatingPanelController()
fpc.delegate = self
fpc.contentInsetAdjustmentBehavior = .always
fpc.surfaceView.appearance = {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
return appearance
}()
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
addMain(panel: fpc)
}
}
func setUpSettingsPanel(for mainVC: MainViewController) {
guard settingsPanelVC == nil else { return }
// Initialize FloatingPanelController
settingsPanelVC = FloatingPanelController()
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
settingsPanelVC.surfaceView.appearance = appearance
settingsPanelVC.isRemovalInteractionEnabled = true
settingsPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
settingsPanelVC.delegate = self
let contentVC = mainVC.storyboard?.instantiateViewController(withIdentifier: "SettingsViewController")
// Set a content view controller
settingsPanelVC.set(contentViewController: contentVC)
// Add FloatingPanel to self.view
settingsPanelVC.addPanel(toParent: mainVC, animated: true)
}
private func addMain(panel fpc: FloatingPanelController) {
let oldMainPanelVC = mainPanelVC
mainPanelVC = fpc
if let oldMainPanelVC = oldMainPanelVC {
oldMainPanelVC.removePanelFromParent(animated: true, completion: {
self.mainPanelVC.addPanel(toParent: self.mainVC, animated: true)
})
} else {
mainPanelVC.addPanel(toParent: mainVC, animated: true)
}
}
@objc
private func handleSurface(tapGesture: UITapGestureRecognizer) {
switch mainPanelVC.state {
case .full:
mainPanelVC.move(to: .half, animated: true)
default:
mainPanelVC.move(to: .full, animated: true)
}
}
}
extension UseCaseController: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint {
if useCase == .showNavigationController, #available(iOS 11.0, *) {
// 148.0 is the SafeArea's top value for a navigation bar with a large title.
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top - 148.0)
}
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top)
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
if vc == settingsPanelVC {
return IntrinsicPanelLayout()
}
switch useCase {
case .showTopPositionedPanel:
return TopPositionedPanelLayout()
case .showRemovablePanel:
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
case .showIntrinsicView:
return IntrinsicPanelLayout()
case .showPanelModal:
if vc != mainPanelVC && vc != detailPanelVC {
return ModalPanelLayout()
}
fallthrough
case .showContentInset:
return FloatingPanelBottomLayout()
case .showCustomStatePanel:
return FloatingPanelLayoutWithCustomState()
default:
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelBottomLayout() : mainVC
}
}
func floatingPanelDidRemove(_ vc: FloatingPanelController) {
switch vc {
case settingsPanelVC:
settingsPanelVC = nil
default:
break
}
}
}
extension UseCaseController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if case .showNestedScrollView = useCase {
return true
} else {
return false
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
false
}
}
private extension FloatingPanelController {
func ext_trackScrollView(in contentVC: UIViewController) {
switch contentVC {
case let consoleVC as DebugTextViewController:
track(scrollView: consoleVC.textView)
case let contentVC as DebugTableViewController:
let ob = contentVC.tableView.observe(\.isEditing) { [weak self] (tableView, _) in
self?.panGestureRecognizer.isEnabled = !tableView.isEditing
}
contentVC.kvoObservers.append(ob)
track(scrollView: contentVC.tableView)
case let contentVC as NestedScrollViewController:
track(scrollView: contentVC.scrollView)
case let navVC as UINavigationController:
if let rootVC = (navVC.topViewController as? MainViewController) {
rootVC.loadViewIfNeeded()
track(scrollView: rootVC.tableView)
}
case let contentVC as ImageViewController:
track(scrollView: contentVC.scrollView)
case let contentVC as AdaptiveLayoutTestViewController:
track(scrollView: contentVC.tableView)
default:
break
}
}
}
File diff suppressed because it is too large Load Diff
-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.
}
}
@@ -8,13 +8,14 @@
/* Begin PBXBuildFile section */
545BA70621BA3214007F7846 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 545BA70521BA3214007F7846 /* AppDelegate.m */; };
545BA70921BA3214007F7846 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 545BA70821BA3214007F7846 /* ViewController.m */; };
545BA70921BA3214007F7846 /* MainViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 545BA70821BA3214007F7846 /* MainViewController.m */; };
545BA70C21BA3214007F7846 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545BA70A21BA3214007F7846 /* Main.storyboard */; };
545BA70E21BA3217007F7846 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 545BA70D21BA3217007F7846 /* Assets.xcassets */; };
545BA71121BA3217007F7846 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545BA70F21BA3217007F7846 /* LaunchScreen.storyboard */; };
545BA71421BA3217007F7846 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 545BA71321BA3217007F7846 /* main.m */; };
545BA72621BA3BAF007F7846 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545BA72521BA3BAF007F7846 /* FloatingPanel.framework */; };
545BA72721BA3BAF007F7846 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 545BA72521BA3BAF007F7846 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5D82A6B028D18447006A44BA /* libswiftCoreGraphics.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D82A6AF28D18443006A44BA /* libswiftCoreGraphics.tbd */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -35,8 +36,8 @@
545BA70121BA3214007F7846 /* SamplesObjC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SamplesObjC.app; sourceTree = BUILT_PRODUCTS_DIR; };
545BA70421BA3214007F7846 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
545BA70521BA3214007F7846 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
545BA70721BA3214007F7846 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
545BA70821BA3214007F7846 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
545BA70721BA3214007F7846 /* MainViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MainViewController.h; sourceTree = "<group>"; };
545BA70821BA3214007F7846 /* MainViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MainViewController.m; sourceTree = "<group>"; };
545BA70B21BA3214007F7846 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
545BA70D21BA3217007F7846 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
545BA71021BA3217007F7846 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -44,6 +45,7 @@
545BA71321BA3217007F7846 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
545BA72221BA3867007F7846 /* SamplesObjC-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SamplesObjC-Bridging-Header.h"; sourceTree = "<group>"; };
545BA72521BA3BAF007F7846 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5D82A6AF28D18443006A44BA /* libswiftCoreGraphics.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreGraphics.tbd; path = usr/lib/swift/libswiftCoreGraphics.tbd; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -51,6 +53,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5D82A6B028D18447006A44BA /* libswiftCoreGraphics.tbd in Frameworks */,
545BA72621BA3BAF007F7846 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -64,6 +67,7 @@
545BA72521BA3BAF007F7846 /* FloatingPanel.framework */,
545BA70321BA3214007F7846 /* SamplesObjC */,
545BA70221BA3214007F7846 /* Products */,
5D82A6AE28D18443006A44BA /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -78,13 +82,13 @@
545BA70321BA3214007F7846 /* SamplesObjC */ = {
isa = PBXGroup;
children = (
545BA70421BA3214007F7846 /* AppDelegate.h */,
545BA70521BA3214007F7846 /* AppDelegate.m */,
545BA70721BA3214007F7846 /* ViewController.h */,
545BA70821BA3214007F7846 /* ViewController.m */,
545BA70A21BA3214007F7846 /* Main.storyboard */,
545BA70D21BA3217007F7846 /* Assets.xcassets */,
545BA70F21BA3217007F7846 /* LaunchScreen.storyboard */,
545BA70A21BA3214007F7846 /* Main.storyboard */,
545BA70421BA3214007F7846 /* AppDelegate.h */,
545BA70521BA3214007F7846 /* AppDelegate.m */,
545BA70721BA3214007F7846 /* MainViewController.h */,
545BA70821BA3214007F7846 /* MainViewController.m */,
545BA71221BA3217007F7846 /* Info.plist */,
545BA71321BA3217007F7846 /* main.m */,
545BA72221BA3867007F7846 /* SamplesObjC-Bridging-Header.h */,
@@ -92,6 +96,14 @@
path = SamplesObjC;
sourceTree = "<group>";
};
5D82A6AE28D18443006A44BA /* Frameworks */ = {
isa = PBXGroup;
children = (
5D82A6AF28D18443006A44BA /* libswiftCoreGraphics.tbd */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -119,7 +131,7 @@
545BA6F921BA3214007F7846 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1110;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "Shin Yamamoto";
TargetAttributes = {
545BA70021BA3214007F7846 = {
@@ -164,7 +176,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545BA70921BA3214007F7846 /* ViewController.m in Sources */,
545BA70921BA3214007F7846 /* MainViewController.m in Sources */,
545BA71421BA3217007F7846 /* main.m in Sources */,
545BA70621BA3214007F7846 /* AppDelegate.m in Sources */,
);
@@ -218,6 +230,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -277,6 +290,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -318,7 +332,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.SamplesObjC;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(SDKROOT)/usr/lib/swift",
);
PRODUCT_BUNDLE_IDENTIFIER = example.SamplesObjC;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "SamplesObjC/SamplesObjC-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -340,7 +358,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.SamplesObjC;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(SDKROOT)/usr/lib/swift",
);
PRODUCT_BUNDLE_IDENTIFIER = example.SamplesObjC;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "SamplesObjC/SamplesObjC-Bridging-Header.h";
SWIFT_VERSION = 4.2;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1110"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,20 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
<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-->
<!--Main View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="" sceneMemberID="viewController">
<viewController id="BYZ-38-t0r" customClass="MainViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
@@ -3,7 +3,7 @@
#import <UIKit/UIKit.h>
@import FloatingPanel;
@interface ViewController : UIViewController
@interface MainViewController : UIViewController
@end
@interface MyFloatingPanelLayout : NSObject <FloatingPanelLayout>
@@ -1,26 +1,41 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
#import "ViewController.h"
#import "MainViewController.h"
@import FloatingPanel;
@interface ViewController()<FloatingPanelControllerDelegate>
// Defining a custom FloatingPanelState
@interface FloatingPanelState(Extended)
+ (FloatingPanelState *)LastQuart;
@end
@implementation ViewController
@implementation FloatingPanelState(Extended)
static FloatingPanelState *_lastQuart;
+ (FloatingPanelState *)LastQuart {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_lastQuart = [[FloatingPanelState alloc] initWithRawValue:@"lastquart" order:750];
});
return _lastQuart;
}
@end
@interface MainViewController()<FloatingPanelControllerDelegate>
@end
@implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
FloatingPanelController *fpc = [[FloatingPanelController alloc] init];
[fpc setContentViewController:nil];
[fpc trackScrollView:nil];
[fpc setDelegate:self];
[fpc setLayout: [MyFloatingPanelLayout new]];
[fpc setBehavior:[MyFloatingPanelBehavior new]];
[fpc setRemovalInteractionEnabled:NO];
[fpc addPanelToParent:self at:self.view.subviews.count animated:NO];
[fpc addPanelToParent:self at:self.view.subviews.count animated:NO completion:nil];
[fpc moveToState:FloatingPanelState.Tip animated:true completion:nil];
[self updateAppearance: fpc];
@@ -59,6 +74,9 @@
}
- (NSDictionary<FloatingPanelState *, id<FloatingPanelLayoutAnchoring>> *)anchors {
return @{
FloatingPanelState.LastQuart: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.25
edge:FloatingPanelReferenceEdgeTop
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
FloatingPanelState.Half: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.5
edge:FloatingPanelReferenceEdgeTop
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
@@ -8,12 +8,13 @@
/* Begin PBXBuildFile section */
548DF95421705BE00041922A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95321705BE00041922A /* AppDelegate.swift */; };
548DF95621705BE00041922A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95521705BE00041922A /* ViewController.swift */; };
548DF95621705BE00041922A /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95521705BE00041922A /* MainViewController.swift */; };
548DF95921705BE00041922A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95721705BE00041922A /* Main.storyboard */; };
548DF95B21705BE10041922A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95A21705BE10041922A /* Assets.xcassets */; };
548DF95E21705BE10041922A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95C21705BE10041922A /* LaunchScreen.storyboard */; };
549D23CF233C77CF008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */; };
549D23D0233C77CF008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5D82A6AA28D18432006A44BA /* libswiftCoreGraphics.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D82A6A928D1842B006A44BA /* libswiftCoreGraphics.tbd */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -33,12 +34,13 @@
/* Begin PBXFileReference section */
548DF95021705BE00041922A /* Stocks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stocks.app; sourceTree = BUILT_PRODUCTS_DIR; };
548DF95321705BE00041922A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
548DF95521705BE00041922A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
548DF95521705BE00041922A /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
548DF95821705BE00041922A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
548DF95A21705BE10041922A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
548DF95D21705BE10041922A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
548DF95F21705BE10041922A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5D82A6A928D1842B006A44BA /* libswiftCoreGraphics.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreGraphics.tbd; path = usr/lib/swift/libswiftCoreGraphics.tbd; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -46,6 +48,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5D82A6AA28D18432006A44BA /* libswiftCoreGraphics.tbd in Frameworks */,
549D23CF233C77CF008EF4D7 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -59,6 +62,7 @@
549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */,
548DF95221705BE00041922A /* Stocks */,
548DF95121705BE00041922A /* Products */,
5D82A6A828D1842A006A44BA /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -73,16 +77,24 @@
548DF95221705BE00041922A /* Stocks */ = {
isa = PBXGroup;
children = (
548DF95321705BE00041922A /* AppDelegate.swift */,
548DF95521705BE00041922A /* ViewController.swift */,
548DF95721705BE00041922A /* Main.storyboard */,
548DF95A21705BE10041922A /* Assets.xcassets */,
548DF95C21705BE10041922A /* LaunchScreen.storyboard */,
548DF95721705BE00041922A /* Main.storyboard */,
548DF95321705BE00041922A /* AppDelegate.swift */,
548DF95521705BE00041922A /* MainViewController.swift */,
548DF95F21705BE10041922A /* Info.plist */,
);
path = Stocks;
sourceTree = "<group>";
};
5D82A6A828D1842A006A44BA /* Frameworks */ = {
isa = PBXGroup;
children = (
5D82A6A928D1842B006A44BA /* libswiftCoreGraphics.tbd */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -111,7 +123,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1000;
LastUpgradeCheck = 1000;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = scenee;
TargetAttributes = {
548DF94F21705BE00041922A = {
@@ -155,7 +167,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
548DF95621705BE00041922A /* ViewController.swift in Sources */,
548DF95621705BE00041922A /* MainViewController.swift in Sources */,
548DF95421705BE00041922A /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -208,6 +220,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -269,6 +282,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -310,7 +324,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Stocks;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(SDKROOT)/usr/lib/swift",
);
PRODUCT_BUNDLE_IDENTIFIER = example.Stocks;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -329,7 +347,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Stocks;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(SDKROOT)/usr/lib/swift",
);
PRODUCT_BUNDLE_IDENTIFIER = example.Stocks;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -27,8 +27,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -38,8 +36,8 @@
ReferencedContainer = "container:Stocks.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -61,8 +59,6 @@
ReferencedContainer = "container:Stocks.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -1,25 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
<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-->
<!--Main View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="Stocks" customModuleProvider="target" sceneMemberID="viewController">
<viewController id="BYZ-38-t0r" customClass="MainViewController" customModule="Stocks" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Uop-sw-I6p">
<rect key="frame" x="0.0" y="85" width="375" height="537.5"/>
<rect key="frame" x="0.0" y="65" width="600" height="490.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="375" placeholderIntrinsicHeight="625" image="stocks_list" translatesAutoresizingMaskIntoConstraints="NO" id="XJR-iK-fem">
<rect key="frame" x="0.0" y="0.0" width="375" height="625"/>
@@ -34,10 +30,10 @@
</constraints>
</scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dFl-81-6ok">
<rect key="frame" x="0.0" y="622.5" width="375" height="44.5"/>
<rect key="frame" x="0.0" y="555.5" width="600" height="44.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="yahoo_bottom_bar" translatesAutoresizingMaskIntoConstraints="NO" id="NKr-gS-mpx">
<rect key="frame" x="0.0" y="0.0" width="375" height="44.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="44.5"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="44.5" id="B5t-ZF-qUj"/>
@@ -53,10 +49,10 @@
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="-8" translatesAutoresizingMaskIntoConstraints="NO" id="f7r-Al-pIN">
<rect key="frame" x="16" y="20" width="153.5" height="57"/>
<rect key="frame" x="16" y="0.0" width="153.5" height="57"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="STOCKS" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PCG-Wl-fXa">
<rect key="frame" x="0.0" y="0.0" width="111.5" height="32.5"/>
<rect key="frame" x="0.0" y="0.0" width="111" height="32.5"/>
<fontDescription key="fontDescription" type="system" weight="heavy" pointSize="27"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -70,6 +66,7 @@
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" red="0.11764924973249435" green="0.11764311045408249" blue="0.11764728277921677" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<constraints>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="dFl-81-6ok" secondAttribute="trailing" id="20i-yz-AaQ"/>
@@ -83,7 +80,6 @@
<constraint firstItem="dFl-81-6ok" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="nlX-Ab-1aI"/>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="NKr-gS-mpx" secondAttribute="bottom" id="yeu-NH-Pmp"/>
</constraints>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
<connections>
<outlet property="bottomToolView" destination="dFl-81-6ok" id="NXn-af-lFv"/>
@@ -122,6 +118,7 @@
</constraints>
</scrollView>
</subviews>
<viewLayoutGuide key="safeArea" id="INo-op-FLO"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="h7M-7T-4k4" secondAttribute="bottom" id="2b8-Wo-tm0"/>
@@ -129,7 +126,6 @@
<constraint firstItem="h7M-7T-4k4" firstAttribute="trailing" secondItem="INo-op-FLO" secondAttribute="trailing" id="WTc-1l-3Ha"/>
<constraint firstItem="h7M-7T-4k4" firstAttribute="leading" secondItem="INo-op-FLO" secondAttribute="leading" id="na8-TO-WiG"/>
</constraints>
<viewLayoutGuide key="safeArea" id="INo-op-FLO"/>
</view>
<size key="freeformSize" width="375" height="600"/>
<connections>
@@ -3,7 +3,7 @@
import UIKit
import FloatingPanel
class ViewController: UIViewController, FloatingPanelControllerDelegate {
class MainViewController: UIViewController, FloatingPanelControllerDelegate {
@IBOutlet var topBannerView: UIImageView!
@IBOutlet weak var labelStackView: UIStackView!
@IBOutlet weak var bottomToolView: UIView!
@@ -101,14 +101,12 @@ class FloatingPanelStocksLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 56.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 262.0, edge: .bottom, referenceGuide: .safeArea),
/* Visible + ToolView */
.tip: FloatingPanelLayoutAnchor(absoluteInset: 85.0 + 44.0, edge: .bottom, referenceGuide: .safeArea),
]
}
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 56.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 262.0, edge: .bottom, referenceGuide: .safeArea),
/* Visible + ToolView */
.tip: FloatingPanelLayoutAnchor(absoluteInset: 85.0 + 44.0, edge: .bottom, referenceGuide: .safeArea),
]
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
+2 -2
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.2.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.
@@ -14,7 +14,7 @@ The new interface displays the related contents and utilities in parallel as a u
s.platform = :ios, "10.0"
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => s.version.to_s }
s.source_files = "Sources/*.swift"
s.swift_versions = ['5.1', '5.2', '5.3']
s.swift_version = '5.0'
s.framework = "UIKit"
+34 -154
View File
@@ -14,25 +14,25 @@
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
545DB9D02151169500CA77B8 /* ControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ControllerTests.swift */; };
545DB9D22151169500CA77B8 /* FloatingPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = 545DB9C42151169500CA77B8 /* FloatingPanel.h */; settings = {ATTRIBUTES = (Public, ); }; };
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DD215118C800CA77B8 /* UIExtensions.swift */; };
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 */; };
5469F4B424B30F3500537F8A /* LayoutReferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B324B30F3500537F8A /* LayoutReferences.swift */; };
549C371F2361E15E007D8058 /* UtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* UtilTests.swift */; };
5469F4B424B30F3500537F8A /* LayoutProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B324B30F3500537F8A /* LayoutProperties.swift */; };
549C371F2361E15E007D8058 /* ExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* ExtensionTests.swift */; };
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 */; };
54CFBFC5215CD09C006B5735 /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* Core.swift */; };
54DBA3DC262E938500D75969 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DBA3DB262E938500D75969 /* Extensions.swift */; };
54E3992727141F5100A8F9ED /* FloatingPanel.docc in Sources */ = {isa = PBXBuildFile; fileRef = 54E3992627141F5100A8F9ED /* FloatingPanel.docc */; };
5D82A6B528D18464006A44BA /* libswiftCoreGraphics.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D82A6B428D18461006A44BA /* libswiftCoreGraphics.tbd */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -43,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 */
@@ -64,26 +57,24 @@
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FloatingPanelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9CF2151169500CA77B8 /* ControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerTests.swift; sourceTree = "<group>"; };
545DB9D12151169500CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
545DB9DD215118C800CA77B8 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; 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>"; };
5469F4B324B30F3500537F8A /* LayoutReferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutReferences.swift; sourceTree = "<group>"; };
549C371E2361E15D007D8058 /* UtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilTests.swift; sourceTree = "<group>"; };
5469F4B324B30F3500537F8A /* LayoutProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutProperties.swift; sourceTree = "<group>"; };
549C371E2361E15D007D8058 /* ExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionTests.swift; sourceTree = "<group>"; };
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>"; };
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@@ -91,6 +82,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5D82A6B528D18464006A44BA /* libswiftCoreGraphics.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -102,13 +94,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
54E740C7218AFD67005C1A34 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -118,6 +103,7 @@
545DB9C32151169500CA77B8 /* Sources */,
545DB9CE2151169500CA77B8 /* Tests */,
545DB9C22151169500CA77B8 /* Products */,
5D82A6B328D18460006A44BA /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -126,7 +112,6 @@
children = (
545DB9C12151169500CA77B8 /* FloatingPanel.framework */,
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */,
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */,
);
name = Products;
sourceTree = "<group>";
@@ -139,7 +124,7 @@
5469F4B124B30F1100537F8A /* Position.swift */,
54CFBFC4215CD09C006B5735 /* Core.swift */,
54CFBFC2215CD045006B5735 /* Layout.swift */,
5469F4B324B30F3500537F8A /* LayoutReferences.swift */,
5469F4B324B30F3500537F8A /* LayoutProperties.swift */,
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */,
5450EEE321646DF500135936 /* Behavior.swift */,
54352E9721A521CA00CBCA08 /* PassthroughView.swift */,
@@ -147,10 +132,11 @@
54CDC5D4215B6D8D007D205C /* BackdropView.swift */,
545DBA2A2152383100CA77B8 /* GrabberView.swift */,
54352E9521A51A2500CBCA08 /* Transitioning.swift */,
545DB9DD215118C800CA77B8 /* UIExtensions.swift */,
54ABD7AE216CCFF7002E6C13 /* Logger.swift */,
54DBA3DB262E938500D75969 /* Extensions.swift */,
54ABD7AE216CCFF7002E6C13 /* Logging.swift */,
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9C52151169500CA77B8 /* Info.plist */,
54E3992627141F5100A8F9ED /* FloatingPanel.docc */,
);
path = Sources;
sourceTree = "<group>";
@@ -158,27 +144,24 @@
545DB9CE2151169500CA77B8 /* Tests */ = {
isa = PBXGroup;
children = (
5469F49E24B003EF00537F8A /* TestingApp */,
54A6B6B022968B530077F348 /* CoreTests.swift */,
545DB9CF2151169500CA77B8 /* ControllerTests.swift */,
542753C522C49A6E00D17955 /* LayoutTests.swift */,
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */,
549E944422CF295D0050AECF /* StateTests.swift */,
549C371E2361E15D007D8058 /* UtilTests.swift */,
549C371E2361E15D007D8058 /* ExtensionTests.swift */,
542753C722C49A8F00D17955 /* TestSupports.swift */,
545DB9D12151169500CA77B8 /* Info.plist */,
);
path = Tests;
sourceTree = "<group>";
};
5469F49E24B003EF00537F8A /* TestingApp */ = {
5D82A6B328D18460006A44BA /* Frameworks */ = {
isa = PBXGroup;
children = (
5469F49F24B003EF00537F8A /* LaunchScreen.storyboard */,
5469F4A024B003EF00537F8A /* AppDelegate.swift */,
5469F4A124B003EF00537F8A /* Info.plist */,
5D82A6B428D18461006A44BA /* libswiftCoreGraphics.tbd */,
);
path = TestingApp;
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
@@ -225,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 */
@@ -256,7 +221,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1000;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = scenee;
TargetAttributes = {
545DB9C02151169500CA77B8 = {
@@ -265,10 +230,6 @@
};
545DB9C92151169500CA77B8 = {
CreatedOnToolsVersion = 10.0;
TestTargetID = 54E740C9218AFD67005C1A34;
};
54E740C9218AFD67005C1A34 = {
CreatedOnToolsVersion = 10.1;
};
};
};
@@ -287,7 +248,6 @@
targets = (
545DB9C02151169500CA77B8 /* FloatingPanel */,
545DB9C92151169500CA77B8 /* FloatingPanelTests */,
54E740C9218AFD67005C1A34 /* TestingApp */,
);
};
/* End PBXProject section */
@@ -307,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 */
@@ -326,16 +278,17 @@
5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */,
54CDC5D3215B6D5A007D205C /* SurfaceView.swift in Sources */,
54CFBFC3215CD045006B5735 /* Layout.swift in Sources */,
5469F4B424B30F3500537F8A /* LayoutReferences.swift in Sources */,
5469F4B424B30F3500537F8A /* LayoutProperties.swift in Sources */,
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 */,
545DBA2B2152383100CA77B8 /* GrabberView.swift in Sources */,
54E3992727141F5100A8F9ED /* FloatingPanel.docc in Sources */,
54352E9621A51A2500CBCA08 /* Transitioning.swift in Sources */,
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
5469F4AE24B30D7E00537F8A /* State.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -345,7 +298,7 @@
buildActionMask = 2147483647;
files = (
54A6B6B122968B530077F348 /* CoreTests.swift in Sources */,
549C371F2361E15E007D8058 /* UtilTests.swift in Sources */,
549C371F2361E15E007D8058 /* ExtensionTests.swift in Sources */,
545DB9D02151169500CA77B8 /* ControllerTests.swift in Sources */,
549E944522CF295D0050AECF /* StateTests.swift in Sources */,
542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */,
@@ -354,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 */
@@ -370,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 */
@@ -404,6 +344,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -469,6 +410,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -575,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;
};
@@ -596,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;
};
@@ -666,6 +570,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -748,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;
};
@@ -803,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 */;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -27,6 +27,15 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9C02151169500CA77B8"
BuildableName = "FloatingPanel.framework"
BlueprintName = "FloatingPanel"
ReferencedContainer = "container:FloatingPanel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
@@ -41,17 +50,6 @@
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9C02151169500CA77B8"
BuildableName = "FloatingPanel.framework"
BlueprintName = "FloatingPanel"
ReferencedContainer = "container:FloatingPanel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -72,8 +70,6 @@
ReferencedContainer = "container:FloatingPanel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
+3
View File
@@ -10,6 +10,9 @@
<Group
location = "group:Examples"
name = "Examples">
<FileRef
location = "group:Maps-SwiftUI/Maps-SwiftUI.xcodeproj">
</FileRef>
<FileRef
location = "group:Maps/Maps.xcodeproj">
</FileRef>
+71 -42
View File
@@ -1,13 +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.
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)
@@ -36,6 +38,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [Use the intrinsic size of a content in your panel layout](#use-the-intrinsic-size-of-a-content-in-your-panel-layout)
- [Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specify-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame)
- [Change the backdrop alpha](#change-the-backdrop-alpha)
- [Using custome panel states](#using-custome-panel-states)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Activate the rubber-band effect on panel edges](#activate-the-rubber-band-effect-on-panel-edges)
@@ -54,6 +57,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
- [Move a position with an animation](#move-a-position-with-an-animation)
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
- [Enabling the tap-to-dismiss action of the backdrop view](#enabling-the-tap-to-dismiss-action-of-the-backdrop-view)
- [Notes](#notes)
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
- [UISearchController issue](#uisearchcontroller-issue)
@@ -71,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] 1~3 magnetic anchors(full, half, tip)
- [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.
@@ -87,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
@@ -265,13 +269,11 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
class MyFloatingPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea),
]
}
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea),
]
}
```
@@ -286,6 +288,8 @@ fpc.layout = MyPanelLayout()
fpc.invalidateLayout() // If needed
```
Note: If you already set the `delegate` property of your `FloatingPanelController` instance, `invalidateLayout()` overrides the layout object of `FloatingPanelController` with one returned by the delegate object.
2. Returns an appropriate layout object in one of 2 `floatingPanel(_:layoutFor:)` delegates.
```swift
@@ -315,12 +319,11 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
class LandscapePanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
}
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
@@ -339,12 +342,10 @@ class LandscapePanelLayout: FloatingPanelLayout {
class IntrinsicPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea),
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
]
}
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea),
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
]
...
}
```
@@ -358,14 +359,11 @@ Use `.superview` reference guide in your anchors.
```swift
class MyFullScreenLayout: FloatingPanelLayout {
...
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .superview),
]
}
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .superview),
]
}
```
@@ -388,6 +386,29 @@ class MyPanelLayout: FloatingPanelLayout {
}
```
#### Using custome panel states
You're able to define custom panel states and use them as the following example.
```swift
extension FloatingPanelState {
static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
}
class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
.lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
]
}
}
```
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
@@ -540,8 +561,8 @@ override func viewDidLoad() {
surfaceTapGesture.isEnabled = (fpc.position == .tip)
}
// Enable `surfaceTapGesture` only at `tip` position
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
// Enable `surfaceTapGesture` only at `tip` state
func floatingPanelDidChangeState(_ vc: FloatingPanelController) {
surfaceTapGesture.isEnabled = (vc.position == .tip)
}
```
@@ -634,6 +655,14 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
}
```
### Enabling the tap-to-dismiss action of the backdrop view
The tap-to-dismiss action is disabled by default. So it needs to be enabled as below.
```swift
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
```
## Notes
### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller
+15 -1
View File
@@ -7,5 +7,19 @@ import UIKit
public class BackdropView: UIView {
/// The gesture recognizer for tap gestures to dismiss a panel.
public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
///
/// By default, this gesture recognizer is disabled as following the default behavior of iOS modalities.
/// To dismiss a panel by tap gestures on the backdrop, `dismissalTapGestureRecognizer.isEnabled` is set to true.
@objc public var dismissalTapGestureRecognizer: UITapGestureRecognizer
init() {
dismissalTapGestureRecognizer = UITapGestureRecognizer()
dismissalTapGestureRecognizer.isEnabled = false
super.init(frame: .zero)
addGestureRecognizer(dismissalTapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ public protocol FloatingPanelBehavior {
/// Returns the velocity threshold for the default interactive removal gesture.
///
/// In case `floatingPanel:shouldRemoveAt:with` is implemented, this value will not be used. The default value of `FloatingPanelDefaultBehavior` is 5.5
/// In case ``FloatingPanel/FloatingPanelControllerDelegate/floatingPanel(_:shouldRemoveAt:with:)`` is implemented, this value will not be used. The default value of ``FloatingPanelDefaultBehavior`` is 5.5
@objc optional
var removalInteractionVelocityThreshold: CGFloat { get }
}
+86 -56
View File
@@ -1,11 +1,12 @@
// 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
/// dragging, attracting a panel, layout of a panel and the content, and transition animations.
@objc public protocol FloatingPanelControllerDelegate: class {
@objc public protocol FloatingPanelControllerDelegate {
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
@objc(floatingPanel:layoutForTraitCollection:) optional
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout
@@ -26,18 +27,20 @@ import UIKit
@objc(floatingPanel:animatorForDismissingWithVelocity:) optional
func floatingPanel(_ fpc: FloatingPanelController, animatorForDismissingWith velocity: CGVector) -> UIViewPropertyAnimator
/// Called when a panel has changed to a new position. Can be called inside an animation block, so any
/// view properties set inside this function will be automatically animated alongside a panel.
/// Called when a panel has changed to a new state.
///
/// This can be called inside an animation block for presenting, dismissing a panel or moving a panel with your
/// animation. So any view properties set inside this function will be automatically animated alongside a panel.
@objc optional
func floatingPanelDidChangePosition(_ fpc: FloatingPanelController)
func floatingPanelDidChangeState(_ fpc: FloatingPanelController)
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
@objc optional
func floatingPanelShouldBeginDragging(_ fpc: FloatingPanelController) -> Bool
/// Called when the user drags the surface or the surface is attracted to a state anchor.
/// Called while the user drags the surface or the surface moves to a state anchor.
@objc optional
func floatingPanelDidMove(_ fpc: FloatingPanelController) // any surface frame changes in dragging
func floatingPanelDidMove(_ fpc: FloatingPanelController)
/// Called on start of dragging (may require some time and or distance to move)
@objc optional
@@ -63,7 +66,7 @@ import UIKit
/// Asks the delegate whether a panel should be removed when dragging ended at the specified location
///
/// This delegate method is called only where `FloatingPanelController.isRemovalInteractionEnabled` is `true`.
/// This delegate method is called only where ``FloatingPanel/FloatingPanelController/isRemovalInteractionEnabled`` is `true`.
/// The velocity vector is calculated from the distance to a point of the hidden state and the pan gesture's velocity.
@objc(floatingPanel:shouldRemoveAtLocation:withVelocity:)
optional
@@ -155,26 +158,31 @@ open class FloatingPanelController: UIViewController {
return floatingPanel.isAttracting
}
/// The layout object managed by the controller
/// The layout object that the controller manages
///
/// You need to call ``invalidateLayout()`` if you want to apply a new layout object into the panel
/// immediately.
@objc
public var layout: FloatingPanelLayout {
get { _layout }
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)
}
}
}
/// The behavior object managed by the controller
/// The behavior object that the controller manages
@objc
public var behavior: FloatingPanelBehavior {
get { _behavior }
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)
}
}
}
@@ -187,7 +195,7 @@ open class FloatingPanelController: UIViewController {
/// The behavior for determining the adjusted content offsets.
///
/// This property specifies how the content area of the tracking scroll view is modified using `adjustedContentInsets`. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
/// This property specifies how the content area of the tracking scroll view is modified using ``adjustedContentInsets``. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
@objc
public var contentInsetAdjustmentBehavior: ContentInsetAdjustmentBehavior = .always
@@ -311,7 +319,7 @@ open class FloatingPanelController: UIViewController {
// Change a layout for the new view size
if let newLayout = self.delegate?.floatingPanel?(self, layoutFor: size) {
layout = newLayout
activateLayout(forceLayout: false)
activateLayout(forceLayout: true)
}
if view.translatesAutoresizingMaskIntoConstraints {
@@ -330,7 +338,7 @@ open class FloatingPanelController: UIViewController {
// Change a layout for the new trait collection
if let newLayout = self.delegate?.floatingPanel?(self, layoutFor: newCollection) {
self.layout = newLayout
activateLayout(forceLayout: false)
activateLayout(forceLayout: true)
}
}
@@ -370,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
@@ -396,20 +404,10 @@ open class FloatingPanelController: UIViewController {
}
private func activateLayout(forceLayout: Bool = false) {
floatingPanel.layoutAdapter.prepareLayout()
// preserve the current content offset if contentInsetAdjustmentBehavior is `.always`
var contentOffset: CGPoint?
if contentInsetAdjustmentBehavior == .always {
contentOffset = trackingScrollView?.contentOffset
}
floatingPanel.layoutAdapter.updateStaticConstraint()
floatingPanel.layoutAdapter.activateLayout(for: floatingPanel.state, forceLayout: forceLayout)
if let contentOffset = contentOffset {
trackingScrollView?.contentOffset = contentOffset
}
floatingPanel.activateLayout(
forceLayout: forceLayout,
contentInsetAdjustmentBehavior: contentInsetAdjustmentBehavior
)
}
func remove() {
@@ -419,14 +417,23 @@ 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)
}
}
}
// MARK: - Container view controller interface
/// Shows the surface view at the initial position defined by the current layout
/// - Parameters:
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the presentation finishes. This block has no return value and takes no parameters. You may specify nil for this parameter.
@objc(show:completion:)
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
@@ -443,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 {
@@ -468,10 +481,11 @@ open class FloatingPanelController: UIViewController {
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
/// - viewIndex: Insert the surface view managed by the controller below the specified view index. By default, the surface view will be added to the end of the parent list of subviews.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
@objc(addPanelToParent:at:animated:)
public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false) {
/// - completion: The block to execute after the presentation finishes. This block has no return value and takes no parameters. You may specify nil for this parameter.
@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.")
@@ -500,6 +514,7 @@ open class FloatingPanelController: UIViewController {
show(animated: animated) { [weak self] in
guard let self = self else { return }
self.didMove(toParent: parent)
completion?()
}
}
@@ -531,6 +546,7 @@ open class FloatingPanelController: UIViewController {
}
/// Moves the position to the specified position.
///
/// - Parameters:
/// - to: Pass a FloatingPanelPosition value to move the surface view to the position.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
@@ -552,7 +568,7 @@ open class FloatingPanelController: UIViewController {
addChild(vc)
let surfaceView = floatingPanel.surfaceView
surfaceView.set(contentView: vc.view)
surfaceView.set(contentView: vc.view, mode: contentMode)
vc.didMove(toParent: self)
}
@@ -587,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.
///
@@ -607,15 +633,19 @@ open class FloatingPanelController: UIViewController {
// MARK: - Utilities
/// Updates the layout object from the delegate and lays out the views managed
/// by the controller immediately.
/// Invalidates all layout information of the panel and apply the ``layout`` property into it immediately.
///
/// This method updates the `FloatingPanelLayout` object from the delegate and
/// then it calls `layoutIfNeeded()` of the root view to force the view
/// to update the layout immediately. It can be called in an
/// animation block.
/// This lays out subviews of the view that the controller manages with the ``layout`` property by
/// calling the view's `layoutIfNeeded()`. Thus this method can be called in an animation block to
/// animate the panel's changes.
///
/// If the controller has a delegate object, this will lay them out using the layout object returned by
/// `floatingPanel(_:layoutFor:)` delegate method for the current `UITraitCollection`.
@objc
public func invalidateLayout() {
if let newLayout = self.delegate?.floatingPanel?(self, layoutFor: traitCollection) {
layout = newLayout
}
activateLayout(forceLayout: true)
}
@@ -671,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)
}
@@ -704,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)
}
@@ -712,6 +742,6 @@ public extension UIViewController {
}
// For other view controllers
self.fp_original_dismiss(animated: flag, completion: completion)
dismissImp(self, sel, flag, completion)
}
}
+229 -126
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
@@ -17,14 +18,27 @@ class Core: NSObject, UIGestureRecognizerDelegate {
didSet {
oldValue?.panGestureRecognizer.removeTarget(self, action: nil)
scrollView?.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
if let cur = scrollView {
if oldValue == nil {
initialScrollOffset = cur.contentOffset
scrollBounce = cur.bounces
scrollIndictorVisible = cur.showsVerticalScrollIndicator
}
} else {
if let pre = oldValue {
pre.isDirectionalLockEnabled = false
pre.bounces = scrollBounce
pre.showsVerticalScrollIndicator = scrollIndictorVisible
}
}
}
}
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?.floatingPanelDidChangePosition?(vc)
vc.delegate?.floatingPanelDidChangeState?(vc)
}
}
}
@@ -32,7 +46,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
var isRemovalInteractionEnabled: Bool = false
fileprivate var animator: UIViewPropertyAnimator?
fileprivate var isSuspended: Bool = false // Prevent a memory leak in the modal transition
fileprivate var transitionAnimator: UIViewPropertyAnimator?
fileprivate var moveAnimator: NumericSpringAnimator?
private var initialSurfaceLocation: CGPoint = .zero
@@ -52,9 +67,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private var stopScrollDeceleration: Bool = false
private var scrollBounce = false
private var scrollIndictorVisible = false
private var grabberAreaFrame: CGRect {
return surfaceView.grabberAreaFrame
}
private var scrollBounceThreshold: CGFloat = -30.0
// MARK: - Interface
@@ -62,6 +75,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
ownerVC = vc
surfaceView = SurfaceView()
surfaceView.position = layout.position
surfaceView.backgroundColor = .white
backdropView = BackdropView()
@@ -84,11 +98,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
panGestureRecognizer.delegate = self
// Set tap-to-dismiss in the backdrop view
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
tapGesture.isEnabled = false
backdropView.dismissalTapGestureRecognizer = tapGesture
backdropView.addGestureRecognizer(tapGesture)
// Set the tap-to-dismiss action of the backdrop view.
// It's disabled by default. See also BackdropView.dismissalTapGestureRecognizer.
backdropView.dismissalTapGestureRecognizer.addTarget(self, action: #selector(handleBackdrop(tapGesture:)))
}
deinit {
@@ -106,7 +118,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
completion?()
return
}
if state != layoutAdapter.edgeMostState {
if state != layoutAdapter.mostExpandedState {
lockScrollView()
}
tearDownActiveInteraction()
@@ -114,8 +126,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
interruptAnimationIfNeeded()
if animated {
func updateScrollView() {
if self.state == self.layoutAdapter.edgeMostState, abs(self.layoutAdapter.offsetFromEdgeMost) <= 1.0 {
let updateScrollView: () -> Void = { [weak self] in
guard let self = self else { return }
if self.state == self.layoutAdapter.mostExpandedState, 0 == self.layoutAdapter.offsetFromMostExpandedAnchor {
self.unlockScrollView()
} else {
self.lockScrollView()
@@ -151,24 +164,27 @@ 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)
}
}
animator.addCompletion { [weak self] _ in
guard let self = self else { return }
self.animator = nil
self.transitionAnimator = nil
updateScrollView()
self.ownerVC?.notifyDidMove()
completion?()
}
self.animator = animator
self.transitionAnimator = animator
if isSuspended {
return
}
animator.startAnimation()
} else {
self.state = to
self.updateLayout(to: to)
if self.state == self.layoutAdapter.edgeMostState {
if self.state == self.layoutAdapter.mostExpandedState {
self.unlockScrollView()
} else {
self.lockScrollView()
@@ -181,18 +197,49 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// MARK: - Layout update
func activateLayout(
forceLayout: Bool = false,
contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior
) {
layoutAdapter.prepareLayout()
// preserve the current content offset if contentInsetAdjustmentBehavior is `.always`
var contentOffset: CGPoint?
if contentInsetAdjustmentBehavior == .always {
contentOffset = scrollView?.contentOffset
}
layoutAdapter.updateStaticConstraint()
layoutAdapter.activateLayout(for: state, forceLayout: forceLayout)
// Update the backdrop alpha only when called in `Controller.show(animated:completion:)`
// Because that prevents a backdrop flicking just before presenting a panel(#466).
if forceLayout {
backdropView.alpha = getBackdropAlpha(for: state)
}
if let contentOffset = contentOffset {
scrollView?.contentOffset = contentOffset
}
}
private func updateLayout(to target: FloatingPanelState) {
self.layoutAdapter.activateLayout(for: state, forceLayout: true)
self.layoutAdapter.activateLayout(for: target, forceLayout: true)
self.backdropView.alpha = self.getBackdropAlpha(for: target)
}
private func getBackdropAlpha(for target: FloatingPanelState) -> CGFloat {
return target == .hidden ? 0.0 : layoutAdapter.backdropAlpha(for: target)
}
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)
let lowerState = segment.lower ?? layoutAdapter.edgeMostState
let upperState = segment.upper ?? layoutAdapter.edgeLeastState
let lowerState = segment.lower ?? layoutAdapter.mostExpandedState
let upperState = segment.upper ?? layoutAdapter.leastExpandedState
let preState = forwardY ? lowerState : upperState
let nextState = forwardY ? upperState : lowerState
@@ -205,9 +252,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if pre == next {
return preAlpha
} else {
return preAlpha + max(min(1.0, 1.0 - (next - cur) / (next - pre) ), 0.0) * (nextAlpha - preAlpha)
}
return preAlpha + max(min(1.0, 1.0 - (next - cur) / (next - pre) ), 0.0) * (nextAlpha - preAlpha)
}
// MARK: - UIGestureRecognizerDelegate
@@ -220,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:
@@ -231,7 +277,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
is UIRotationGestureRecognizer,
is UIScreenEdgePanGestureRecognizer,
is UIPinchGestureRecognizer:
if grabberAreaFrame.contains(gestureRecognizer.location(in: gestureRecognizer.view)) {
if surfaceView.grabberAreaContains(gestureRecognizer.location(in: surfaceView)) {
return true
}
// all gestures of the tracking scroll view should be recognized in parallel
@@ -262,7 +308,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return true
}
if grabberAreaFrame.contains(gestureRecognizer.location(in: gestureRecognizer.view)) {
if surfaceView.grabberAreaContains(gestureRecognizer.location(in: surfaceView)) {
return true
}
@@ -290,10 +336,13 @@ class Core: NSObject, UIGestureRecognizerDelegate {
scrollGestureRecognizers.contains(otherGestureRecognizer) {
switch otherGestureRecognizer {
case scrollView.panGestureRecognizer:
if grabberAreaFrame.contains(gestureRecognizer.location(in: gestureRecognizer.view)) {
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
}
@@ -318,7 +367,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// Should begin the pan gesture without waiting the dismiss gesture of a sheet modal.
return false
}
if grabberAreaFrame.contains(gestureRecognizer.location(in: gestureRecognizer.view)) {
if surfaceView.grabberAreaContains(gestureRecognizer.location(in: surfaceView)) {
return false
}
// Do not begin the pan gesture until these gestures fail
@@ -344,26 +393,27 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let velocity = value(of: panGesture.velocity(in: panGesture.view))
let location = panGesture.location(in: surfaceView)
let belowEdgeMost = 0 > layoutAdapter.offsetFromEdgeMost + (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.edgeMostState {
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 grabberAreaFrame.contains(location) {
if surfaceView.grabberAreaContains(location) {
// Preserve the current content offset in moving from full.
stopScrolling(at: initialScrollOffset)
}
@@ -376,7 +426,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if interactionInProgress {
lockScrollView()
} else {
if state == layoutAdapter.edgeMostState, self.animator == nil {
if state == layoutAdapter.mostExpandedState, self.transitionAnimator == nil {
switch layoutAdapter.position {
case .top, .left:
if offsetDiff < 0 && velocity > 0 {
@@ -404,34 +454,37 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return
}
}
if state == layoutAdapter.edgeMostState {
if state == layoutAdapter.mostExpandedState {
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation) {
stopScrolling(at: initialScrollOffset)
}
}
} else {
if state == layoutAdapter.edgeMostState {
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()
}
}
// Adjust a small gap of the scroll offset just before swiping down starts in the grabber area,
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation) {
stopScrolling(at: initialScrollOffset)
}
}
@@ -442,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)), \
@@ -478,7 +531,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// doesn't pass through .changed state after an interruptible animator is interrupted.
let diff = translation - .leastNonzeroMagnitude
layoutAdapter.updateInteractiveEdgeConstraint(diff: value(of: diff),
overflow: true,
scrollingContent: true,
allowsRubberBanding: behaviorAdapter.allowsRubberBanding(for:))
}
panningEnd(with: translation, velocity: velocity)
@@ -492,20 +545,20 @@ 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.animator {
guard 0 >= layoutAdapter.offsetFromEdgeMost else { return }
log.debug("a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
if let animator = self.transitionAnimator {
guard 0 >= layoutAdapter.offsetFromMostExpandedAnchor else { return }
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.offsetFromEdgeMost) <= 1.0 {
layoutAdapter.surfaceLocation = layoutAdapter.surfaceLocation(for: layoutAdapter.edgeMostState)
if 0 == layoutAdapter.offsetFromMostExpandedAnchor {
layoutAdapter.surfaceLocation = layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState)
}
animator.finishAnimation(at: .current)
} else {
@@ -531,27 +584,27 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
guard
state == layoutAdapter.edgeMostState, // When not top most(i.e. .full), don't scroll.
state == layoutAdapter.mostExpandedState, // When not top most(i.e. .full), don't scroll.
interactionInProgress == false, // When interaction already in progress, don't scroll.
0 == layoutAdapter.offsetFromEdgeMost
0 == layoutAdapter.offsetFromMostExpandedAnchor
else {
return false
}
// When the current point is within grabber area but the initial point is not, do scroll.
if grabberAreaFrame.contains(point), !grabberAreaFrame.contains(initialLocation) {
if surfaceView.grabberAreaContains(point), !surfaceView.grabberAreaContains(initialLocation) {
return true
}
// When the initial point is within grabber area and the current point is out of surface, don't scroll.
if grabberAreaFrame.contains(initialLocation), !surfaceView.frame.contains(point) {
if surfaceView.grabberAreaContains(initialLocation), !surfaceView.frame.contains(point) {
return false
}
let scrollViewFrame = scrollView.convert(scrollView.bounds, to: surfaceView)
guard
scrollViewFrame.contains(initialLocation), // When the initial point not in scrollView, don't scroll.
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
!surfaceView.grabberAreaContains(point) // When point within grabber area, don't scroll.
else {
return false
}
@@ -590,11 +643,11 @@ 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.edgeMostState {
if grabberAreaFrame.contains(location) {
if state == layoutAdapter.mostExpandedState {
if surfaceView.grabberAreaContains(location) {
initialScrollOffset = scrollView.contentOffset
}
} else {
@@ -603,14 +656,13 @@ 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
let overflow = shouldOverflow(from: pre, to: next)
layoutAdapter.updateInteractiveEdgeConstraint(diff: diff,
overflow: overflow,
scrollingContent: shouldScrollingContentInMoving(from: pre, to: next),
allowsRubberBanding: behaviorAdapter.allowsRubberBanding(for:))
let cur = value(of: layoutAdapter.surfaceLocation)
@@ -624,48 +676,55 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
}
private func shouldOverflow(from pre: CGFloat, to next: CGFloat) -> Bool {
private func shouldScrollingContentInMoving(from pre: CGFloat, to next: CGFloat) -> Bool {
// Don't allow scrolling if the initial panning location is in the grabber area.
if surfaceView.grabberAreaContains(initialLocation) {
return false
}
if let scrollView = scrollView, scrollView.panGestureRecognizer.state == .changed {
switch layoutAdapter.position {
case .top:
if pre > .zero, pre < next,
scrollView.contentSize.height > scrollView.bounds.height || scrollView.alwaysBounceVertical {
return false
return true
}
case .left:
if pre > .zero, pre < next,
scrollView.contentSize.width > scrollView.bounds.width || scrollView.alwaysBounceHorizontal {
return false
return true
}
case .bottom:
if pre > .zero, pre > next,
scrollView.contentSize.height > scrollView.bounds.height || scrollView.alwaysBounceVertical {
return false
return true
}
case .right:
if pre > .zero, pre > next,
scrollView.contentSize.width > scrollView.bounds.width || scrollView.alwaysBounceHorizontal {
return false
return true
}
}
}
return true
return false
}
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.offsetFromEdgeMost + (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)")
}
}
@@ -708,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 != .full {
scrollView.isScrollEnabled = false
}
startAttraction(to: targetPosition, with: velocity)
// Workaround: Reset `self.scrollView.isScrollEnabled`
if let scrollView = scrollView, targetPosition != .full,
let isScrollEnabled = isScrollEnabled {
scrollView.isScrollEnabled = isScrollEnabled
}
}
// MARK: - Behavior
@@ -745,32 +792,38 @@ 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
initialSurfaceLocation = layoutAdapter.surfaceLocation
if state == layoutAdapter.edgeMostState, let scrollView = scrollView {
if grabberAreaFrame.contains(location) {
if state == layoutAdapter.mostExpandedState, let scrollView = scrollView {
if surfaceView.grabberAreaContains(location) {
initialScrollOffset = scrollView.contentOffset
} else {
initialScrollOffset = contentOffsetForPinning(of: scrollView)
let offsetDiff = scrollView.contentOffset - contentOffsetForPinning(of: scrollView)
let pinningOffset = contentOffsetForPinning(of: scrollView)
// `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
// 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:
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
if value(of: offsetDiff) > 0 {
offset = -offsetDiff
}
case .bottom, .right:
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
if value(of: offsetDiff) < 0 {
offset = -offsetDiff
}
}
}
log.debug("initial scroll offset --", initialScrollOffset)
os_log(msg, log: devLog, type: .debug, "initial scroll offset -- \(initialScrollOffset)")
}
initialTranslation = translation
@@ -787,16 +840,16 @@ 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
// Prevent to keep a scroll view indicator visible at the half/tip position
if targetPosition != layoutAdapter.edgeMostState {
if targetPosition != layoutAdapter.mostExpandedState {
lockScrollView()
}
@@ -804,6 +857,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func tearDownActiveInteraction() {
guard panGestureRecognizer.isEnabled else { return }
// Cancel the pan gesture so that panningEnd(with:velocity:) is called
panGestureRecognizer.isEnabled = false
panGestureRecognizer.isEnabled = true
@@ -817,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
@@ -849,7 +903,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
completion: { [weak self] in
guard let self = self,
self.ownerVC != nil else { return }
self.layoutAdapter.activateLayout(for: targetPosition, forceLayout: true)
self.updateLayout(to: targetPosition)
completion()
})
moveAnimator?.startAnimation()
@@ -865,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.edgeMostState))
edge most position = \(layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState))
""")
if finished, state == layoutAdapter.edgeMostState, abs(layoutAdapter.offsetFromEdgeMost) <= 1.0 {
if finished, state == layoutAdapter.mostExpandedState, 0 == layoutAdapter.offsetFromMostExpandedAnchor {
unlockScrollView()
} else if finished, shouldLooselyLockScrollView {
unlockScrollView()
}
}
@@ -884,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:
@@ -900,9 +960,9 @@ 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.sortedDirectionalStates
let sortedPositions = layoutAdapter.sortedAnchorStatesByCoordinate
guard sortedPositions.count > 1 else {
return state
@@ -910,7 +970,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// Projection
let decelerationRate = behaviorAdapter.momentumProjectionRate
let baseY = abs(layoutAdapter.position(for: layoutAdapter.edgeLeastState) - layoutAdapter.position(for: layoutAdapter.edgeMostState))
let baseY = abs(layoutAdapter.position(for: layoutAdapter.leastExpandedState) - layoutAdapter.position(for: layoutAdapter.mostExpandedState))
let vecY = velocity / baseY
var pY = project(initialVelocity: vecY, decelerationRate: decelerationRate) * baseY + currentY
@@ -926,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
@@ -955,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 }
@@ -1005,16 +1100,15 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
}
private func allowScrollPanGesture(for scrollView: UIScrollView) -> Bool {
guard state == layoutAdapter.edgeMostState 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
@@ -1038,7 +1132,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
initialLocation = touches.first?.location(in: view) ?? .zero
if floatingPanel?.animator != nil || floatingPanel?.moveAnimator != nil {
if floatingPanel?.transitionAnimator != nil || floatingPanel?.moveAnimator != nil {
self.state = .began
}
}
@@ -1138,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
@@ -1150,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 {
@@ -1199,3 +1293,12 @@ private class NumericSpringAnimator: NSObject {
v = (v + h * o2 * (xt - x)) / det
}
}
extension FloatingPanelController {
func suspendTransitionAnimator(_ suspended: Bool) {
self.floatingPanel.isSuspended = suspended
}
var transitionAnimator: UIViewPropertyAnimator? {
return self.floatingPanel.transitionAnimator
}
}
@@ -2,16 +2,38 @@
import UIKit
internal func displayTrunc(_ v: CGFloat, by s: CGFloat) -> CGFloat {
let base = (1 / s)
let t = v.rounded(.down)
return t + ((v - t) / base).rounded(.toNearestOrAwayFromZero) * base
// MARK: - CoreGraphics
extension CGFloat {
/// Returns this value rounded to an logical pixel value by a display scale
func rounded(by displayScale: CGFloat) -> CGFloat {
return (self * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
}
func isEqual(to: CGFloat, on displayScale: CGFloat) -> Bool {
return rounded(by: displayScale) == to.rounded(by: displayScale)
}
}
internal func displayEqual(_ lhs: CGFloat, _ rhs: CGFloat, by displayScale: CGFloat) -> Bool {
return displayTrunc(lhs, by: displayScale) == displayTrunc(rhs, by: displayScale)
extension CGPoint {
static var leastNonzeroMagnitude: CGPoint {
return CGPoint(x: CGFloat.leastNonzeroMagnitude, y: CGFloat.leastNonzeroMagnitude)
}
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
static prefix func - (point: CGPoint) -> CGPoint {
return CGPoint(x: -point.x, y: -point.y)
}
}
// MARK: - UIKit
protocol LayoutGuideProvider {
var topAnchor: NSLayoutYAxisAnchor { get }
var leftAnchor: NSLayoutXAxisAnchor { get }
@@ -164,24 +186,6 @@ extension UISpringTimingParameters {
}
}
extension CGPoint {
static var leastNonzeroMagnitude: CGPoint {
return CGPoint(x: CGFloat.leastNonzeroMagnitude, y: CGFloat.leastNonzeroMagnitude)
}
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
static prefix func - (point: CGPoint) -> CGPoint {
return CGPoint(x: -point.x, y: -point.y)
}
}
extension NSLayoutConstraint {
static func activate(constraint: NSLayoutConstraint?) {
guard let constraint = constraint else { return }
@@ -0,0 +1,53 @@
# ``FloatingPanel``
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 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
### Essentials
- ``FloatingPanelController``
- ``FloatingPanelControllerDelegate``
### Views
- ``SurfaceView``
- ``SurfaceAppearance``
- ``BackdropView``
- ``GrabberView``
### Gestures
- ``FloatingPanelPanGestureRecognizer``
### Layouts and Anchors
- ``FloatingPanelLayout``
- ``FloatingPanelBottomLayout``
- ``FloatingPanelLayoutAnchoring``
- ``FloatingPanelLayoutAnchor``
- ``FloatingPanelAdaptiveLayoutAnchor``
- ``FloatingPanelIntrinsicLayoutAnchor``
### States
- ``FloatingPanelState``
### Positions
- ``FloatingPanelPosition``
- ``FloatingPanelReferenceEdge``
- ``FloatingPanelLayoutReferenceGuide``
- ``FloatingPanelLayoutContentBoundingGuide``
### Behaviors
- ``FloatingPanelBehavior``
- ``FloatingPanelDefaultBehavior``
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.2.0</string>
<string>2.6.6</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+93 -88
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 {
@@ -82,9 +83,8 @@ class LayoutAdapter {
private var initialConst: CGFloat = 0.0
private var fixedConstraints: [NSLayoutConstraint] = []
private var fullConstraints: [NSLayoutConstraint] = []
private var halfConstraints: [NSLayoutConstraint] = []
private var tipConstraints: [NSLayoutConstraint] = []
private var stateConstraints: [FloatingPanelState: [NSLayoutConstraint]] = [:]
private var offConstraints: [NSLayoutConstraint] = []
private var fitToBoundsConstraint: NSLayoutConstraint?
@@ -93,10 +93,19 @@ class LayoutAdapter {
private var staticConstraint: NSLayoutConstraint?
private var activeStates: Set<FloatingPanelState> {
/// A layout constraint to limit the content size in ``FloatingPanelAdaptiveLayoutAnchor``.
private var contentBoundingConstraint: NSLayoutConstraint?
private var anchorStates: Set<FloatingPanelState> {
return Set(layout.anchors.keys)
}
private var sortedAnchorStates: [FloatingPanelState] {
return anchorStates.sorted(by: {
return $0.order < $1.order
})
}
var initialState: FloatingPanelState {
layout.initialState
}
@@ -105,18 +114,12 @@ class LayoutAdapter {
layout.position
}
var orderedStates: [FloatingPanelState] {
return activeStates.sorted(by: {
return $0.order < $1.order
})
}
var validStates: Set<FloatingPanelState> {
return activeStates.union([.hidden])
return anchorStates.union([.hidden])
}
var sortedDirectionalStates: [FloatingPanelState] {
return activeStates.sorted(by: {
var sortedAnchorStatesByCoordinate: [FloatingPanelState] {
return anchorStates.sorted(by: {
switch position {
case .top, .left:
return $0.order < $1.order
@@ -126,30 +129,26 @@ class LayoutAdapter {
})
}
private var directionalLeastState: FloatingPanelState {
return sortedDirectionalStates.first ?? .hidden
private var leastCoordinateState: FloatingPanelState {
return sortedAnchorStatesByCoordinate.first ?? .hidden
}
private var directionalMostState: FloatingPanelState {
return sortedDirectionalStates.last ?? .hidden
private var mostCoordinateState: FloatingPanelState {
return sortedAnchorStatesByCoordinate.last ?? .hidden
}
var edgeLeastState: FloatingPanelState {
if orderedStates.count == 1 {
var leastExpandedState: FloatingPanelState {
if sortedAnchorStates.count == 1 {
return .hidden
}
return orderedStates.first ?? .hidden
return sortedAnchorStates.first ?? .hidden
}
var edgeMostState: FloatingPanelState {
if orderedStates.count == 1 {
return orderedStates[0]
var mostExpandedState: FloatingPanelState {
if sortedAnchorStates.count == 1 {
return sortedAnchorStates[0]
}
return orderedStates.last ?? .hidden
}
var edgeMostY: CGFloat {
return position(for: edgeMostState)
return sortedAnchorStates.last ?? .hidden
}
var adjustedContentInsets: UIEdgeInsets {
@@ -224,7 +223,7 @@ class LayoutAdapter {
}
}
} else {
pos = displayTrunc(edgePosition(surfaceView.frame), by: surfaceView.fp_displayScale)
pos = edgePosition(surfaceView.frame).rounded(by: surfaceView.fp_displayScale)
}
switch position {
case .top, .bottom:
@@ -266,13 +265,15 @@ class LayoutAdapter {
}
}
var offsetFromEdgeMost: CGFloat {
var offsetFromMostExpandedAnchor: CGFloat {
let offset: CGFloat
switch position {
case .top, .left:
return edgePosition(surfaceView.presentationFrame) - position(for: directionalMostState)
offset = edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
case .bottom, .right:
return position(for: directionalLeastState) - edgePosition(surfaceView.presentationFrame)
offset = position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
}
return offset.rounded(by: surfaceView.fp_displayScale)
}
private var hiddenAnchor: FloatingPanelLayoutAnchoring {
@@ -294,7 +295,7 @@ class LayoutAdapter {
}
func surfaceLocation(for state: FloatingPanelState) -> CGPoint {
let pos = displayTrunc(position(for: state), by: surfaceView.fp_displayScale)
let pos = position(for: state).rounded(by: surfaceView.fp_displayScale)
switch layout.position {
case .top, .bottom:
return CGPoint(x: 0.0, y: pos)
@@ -335,12 +336,27 @@ class LayoutAdapter {
if anchor.referenceGuide == .safeArea {
referenceBoundsLength += position.inset(safeAreaInsets)
}
return dimension - diff
let maxPosition: CGFloat = {
if let maxBounds = anchor.contentBoundingGuide.maxBounds(vc) {
return layout.position.mainLocation(maxBounds.origin)
+ layout.position.mainDimension(maxBounds.size)
} else {
return .infinity
}
}()
return min(dimension - diff, maxPosition)
case .bottom, .right:
if anchor.referenceGuide == .safeArea {
referenceBoundsLength -= position.inset(safeAreaInsets)
}
return referenceBoundsLength - dimension + diff
let minPosition: CGFloat = {
if let maxBounds = anchor.contentBoundingGuide.maxBounds(vc) {
return layout.position.mainLocation(maxBounds.origin)
} else {
return -(.infinity)
}
}()
return max(referenceBoundsLength - dimension + diff, minPosition)
}
case let anchor as FloatingPanelLayoutAnchor:
let referenceBounds = anchor.referenceGuide == .safeArea ? bounds.inset(by: safeAreaInsets) : bounds
@@ -452,25 +468,13 @@ class LayoutAdapter {
}
private func updateStateConstraints() {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
if let fullAnchor = layout.anchors[.full] {
fullConstraints = fullAnchor.layoutConstraints(vc, for: position)
fullConstraints.forEach {
$0.identifier = "FloatingPanel-full-constraint"
}
}
if let halfAnchor = layout.anchors[.half] {
halfConstraints = halfAnchor.layoutConstraints(vc, for: position)
halfConstraints.forEach {
$0.identifier = "FloatingPanel-half-constraint"
}
}
if let tipAnchors = layout.anchors[.tip] {
tipConstraints = tipAnchors.layoutConstraints(vc, for: position)
tipConstraints.forEach {
$0.identifier = "FloatingPanel-tip-constraint"
}
let allStateConstraints = stateConstraints.flatMap { $1 }
NSLayoutConstraint.deactivate(allStateConstraints + offConstraints)
stateConstraints.removeAll()
for state in layout.anchors.keys {
stateConstraints[state] = layout.anchors[state]?
.layoutConstraints(vc, for: position)
.map{ $0.identifier = "FloatingPanel-\(state)-constraint"; return $0 }
}
let hiddenAnchor = layout.anchors[.hidden] ?? self.hiddenAnchor
offConstraints = hiddenAnchor.layoutConstraints(vc, for: position)
@@ -487,7 +491,7 @@ class LayoutAdapter {
tearDownAttraction()
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
initialConst = edgePosition(surfaceView.frame) + offset.y
@@ -527,7 +531,7 @@ class LayoutAdapter {
let anchor = layout.anchors[state] ?? self.hiddenAnchor
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
NSLayoutConstraint.deactivate(constraint: interactionConstraint)
interactionConstraint = nil
@@ -646,15 +650,16 @@ class LayoutAdapter {
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateStaticConstraint() {
NSLayoutConstraint.deactivate(constraint: staticConstraint)
NSLayoutConstraint.deactivate([staticConstraint, contentBoundingConstraint].compactMap{ $0 })
staticConstraint = nil
contentBoundingConstraint = nil
if vc.contentMode == .fitToBounds {
surfaceView.containerOverflow = 0
return
}
let anchor = layout.anchors[self.edgeMostState]!
let anchor = layout.anchors[self.mostExpandedState]!
let surfaceAnchor = position.mainDimensionAnchor(surfaceView)
switch anchor {
case let anchor as FloatingPanelIntrinsicLayoutAnchor:
@@ -671,15 +676,26 @@ class LayoutAdapter {
constant = 0.0
}
let baseAnchor = position.mainDimensionAnchor(anchor.contentLayoutGuide)
staticConstraint = surfaceAnchor.constraint(equalTo: baseAnchor, constant: constant)
if let boundingLayoutGuide = anchor.contentBoundingGuide.layoutGuide(vc) {
if anchor.isAbsolute {
contentBoundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide),
constant: anchor.offset)
} else {
contentBoundingConstraint = baseAnchor.constraint(lessThanOrEqualTo: position.mainDimensionAnchor(boundingLayoutGuide),
multiplier: anchor.offset)
}
staticConstraint = surfaceAnchor.constraint(lessThanOrEqualTo: baseAnchor, constant: constant)
} else {
staticConstraint = surfaceAnchor.constraint(equalTo: baseAnchor, constant: constant)
}
default:
switch position {
case .top, .left:
staticConstraint = surfaceAnchor.constraint(equalToConstant: position(for: self.directionalMostState))
staticConstraint = surfaceAnchor.constraint(equalToConstant: position(for: self.mostCoordinateState))
case .bottom, .right:
let rootViewAnchor = position.mainDimensionAnchor(vc.view)
staticConstraint = rootViewAnchor.constraint(equalTo: surfaceAnchor,
constant: position(for: self.directionalLeastState))
constant: position(for: self.leastCoordinateState))
}
}
@@ -690,18 +706,18 @@ class LayoutAdapter {
staticConstraint?.identifier = "FloatingPanel-static-width"
}
NSLayoutConstraint.activate(constraint: staticConstraint)
NSLayoutConstraint.activate([staticConstraint, contentBoundingConstraint].compactMap{ $0 })
surfaceView.containerOverflow = position.mainDimension(vc.view.bounds.size)
}
func updateInteractiveEdgeConstraint(diff: CGFloat, overflow: Bool, allowsRubberBanding: (UIRectEdge) -> Bool) {
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: directionalLeastState)
let maxConst: CGFloat = position(for: directionalMostState)
let minConst: CGFloat = position(for: leastCoordinateState)
let maxConst: CGFloat = position(for: mostCoordinateState)
var const = initialConst + diff
@@ -718,7 +734,7 @@ class LayoutAdapter {
const = maxConst + rubberBandEffect(for: buffer, base: base)
}
if overflow == false {
if scrollingContent {
const = min(max(const, minConst), maxConst)
}
@@ -737,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)")
}
}
@@ -757,8 +773,6 @@ class LayoutAdapter {
var state = state
setBackdropAlpha(of: state)
if validStates.contains(state) == false {
state = layout.initialState
}
@@ -768,17 +782,16 @@ class LayoutAdapter {
// on-screen and off-screen view which includes
// UIStackView(i.e. Settings view in Samples.app)
updateStateConstraints()
switch state {
case .full:
NSLayoutConstraint.activate(fullConstraints)
case .half:
NSLayoutConstraint.activate(halfConstraints)
case .tip:
NSLayoutConstraint.activate(tipConstraints)
case .hidden:
NSLayoutConstraint.activate(offConstraints)
default:
break
if let constraints = stateConstraints[state] {
NSLayoutConstraint.activate(constraints)
} else {
os_log(msg, log: sysLog, type: .fault, "Error: can not find any constraints for \(state)")
}
}
}
@@ -789,21 +802,13 @@ class LayoutAdapter {
surfaceView.superview?.layoutIfNeeded()
}
private func setBackdropAlpha(of target: FloatingPanelState) {
if target == .hidden {
self.backdropView.alpha = 0.0
} else {
self.backdropView.alpha = backdropAlpha(for: target)
}
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return layout.backdropAlpha?(for: state) ?? defaultLayout.backdropAlpha(for: state)
}
fileprivate func checkLayout() {
// Verify layout configurations
assert(activeStates.count > 0)
assert(anchorStates.count > 0)
assert(validStates.contains(layout.initialState),
"Does not include an initial state (\(layout.initialState)) in (\(validStates))")
/* This assertion does not work in a device rotating
@@ -823,7 +828,7 @@ extension LayoutAdapter {
/// |-------|-------|===o===| |-------|===o===|-------|
/// pos: o/x, segment: =
let sortedStates = sortedDirectionalStates
let sortedStates = sortedAnchorStatesByCoordinate
let upperIndex: Int?
if forward {
+66 -10
View File
@@ -15,6 +15,11 @@ import UIKit
///
/// The inset is an amount to inset a panel from an edge of the reference guide. The edge refers to a panel
/// positioning.
///
/// - Parameters:
/// - absoluteOffset: An absolute offset to attach the panel from the edge.
/// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset.
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
@objc public init(absoluteInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
self.inset = absoluteInset
self.referenceGuide = referenceGuide
@@ -27,6 +32,11 @@ import UIKit
/// The inset is an amount to inset a panel from the edge of the specified reference guide. The value is
/// a floating-point number in the range 0.0 to 1.0, where 0.0 represents zero distance from the edge and
/// 1.0 represents a distance to the opposite edge.
///
/// - Parameters:
/// - fractionalOffset: A fractional value of the size of ``FloatingPanelController``'s view to attach the panel from the edge.
/// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset.
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
@objc public init(fractionalInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
self.inset = fractionalInset
self.referenceGuide = referenceGuide
@@ -49,7 +59,7 @@ public extension FloatingPanelLayoutAnchor {
case .left:
return layoutConstraints(layoutGuide, for: vc.surfaceView.rightAnchor)
case .bottom:
return layoutConstraints(layoutGuide, for: vc.surfaceView.topAnchor)
return layoutConstraints(layoutGuide, for: vc.surfaceView.topAnchor)
case .right:
return layoutConstraints(layoutGuide, for: vc.surfaceView.leftAnchor)
}
@@ -101,6 +111,10 @@ public extension FloatingPanelLayoutAnchor {
///
/// The offset is an amount to offset a position of panel that displays the entire content from an edge of
/// the reference guide. The edge refers to a panel positioning.
///
/// - Parameters:
/// - absoluteOffset: An absolute offset from the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel.
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
@objc public init(absoluteOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = offset
self.referenceGuide = referenceGuide
@@ -111,6 +125,10 @@ public extension FloatingPanelLayoutAnchor {
///
/// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content
/// is displayed and 0.5 represents the half of content is displayed.
///
/// - Parameters:
/// - fractionalOffset: A fractional offset of the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel.
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
@objc public init(fractionalOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = offset
self.referenceGuide = referenceGuide
@@ -144,37 +162,72 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
/// An object that defines how to settles a panel with a layout guide of a content view.
@objc final public class FloatingPanelAdaptiveLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
/// Returns a layout anchor with the specified offset by an absolute value, layout guide to display content and reference guide for a panel.
/// Returns a layout anchor with the specified offset by an absolute value to display a panel with its intrinsic content size.
///
/// The offset is an amount to offset a position of panel that displays the entire content of the specified guide from an edge of
/// the reference guide. The edge refers to a panel positioning.
@objc public init(absoluteOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
///
/// ``contentBoundingGuide`` restricts the content size which a panel displays. For example, given ``referenceGuide`` is `.superview` and ``contentBoundingGuide`` is `.safeArea` for a bottom positioned panel, the panel content is laid out inside the superview of the view of FloatingPanelController(not its safe area), but its content size is limited to its safe area size. Normally both of ``referenceGuide`` and ``contentBoundingGuide`` are specified with the same rectangle area.
///
/// - Parameters:
/// - absoluteOffset: An absolute offset from the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel.
/// - contentLayout: The content layout guide to calculate the content size in the panel.
/// - referenceGuide: The rectangular area to lay out the content of a panel. If it's set to `.safeArea`, the panel content displays inside the safe area of its ``FloatingPanelController``'s view. This argument doesn't limit its content size.
/// - contentBoundingGuide: The rectangular area to restrict the content size of a panel in the main dimension(i.e. y axis is the main dimension for a bottom panel).
///
/// - Warning: If ``contentBoundingGuide`` is set to none, the panel may expand out of the screen size, depending on the intrinsic size of its content.
@objc public init(
absoluteOffset offset: CGFloat,
contentLayout: UILayoutGuide,
referenceGuide: FloatingPanelLayoutReferenceGuide,
contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide = .none
) {
self.offset = offset
self.contentLayoutGuide = contentLayout
self.referenceGuide = referenceGuide
self.contentBoundingGuide = contentBoundingGuide
self.isAbsolute = true
}
/// Returns a layout anchor with the specified offset by a fractional value, layout guide to display content and reference guide for a panel.
/// Returns a layout anchor with the specified offset by a fractional value to display a panel with its intrinsic content size.
///
/// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content
/// is displayed and 0.5 represents the half of content is displayed.
@objc public init(fractionalOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
///
/// ``contentBoundingGuide`` restricts the content size which a panel displays. For example, given ``referenceGuide`` is `.superview` and ``contentBoundingGuide`` is `.safeArea` for a bottom positioned panel, the panel content is laid out inside the superview of the view of FloatingPanelController(not its safe area), but its content size is limited to its safe area size. Normally both of ``referenceGuide`` and ``contentBoundingGuide`` are specified with the same rectangle area.
///
/// - Parameters:
/// - fractionalOffset: A fractional offset of the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel.
/// - contentLayout: The content layout guide to calculate the content size in the panel.
/// - referenceGuide: The rectangular area to lay out the content of a panel. If it's set to `.safeArea`, the panel content displays inside the safe area of its ``FloatingPanelController``'s view. This argument doesn't limit its content size.
/// - contentBoundingGuide: The rectangular area to restrict the content size of a panel in the main dimension(i.e. y axis is the main dimension for a bottom panel).
///
/// - Warning: If ``contentBoundingGuide`` is set to none, the panel may expand out of the screen size, depending on the intrinsic size of its content.
@objc public init(
fractionalOffset offset: CGFloat,
contentLayout: UILayoutGuide,
referenceGuide: FloatingPanelLayoutReferenceGuide,
contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide = .none
) {
self.offset = offset
self.contentLayoutGuide = contentLayout
self.referenceGuide = referenceGuide
self.contentBoundingGuide = contentBoundingGuide
self.isAbsolute = false
}
fileprivate let offset: CGFloat
fileprivate let isAbsolute: Bool
let offset: CGFloat
let isAbsolute: Bool
let contentLayoutGuide: UILayoutGuide
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
@objc public let contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide
}
public extension FloatingPanelAdaptiveLayoutAnchor {
func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
var constraints = [NSLayoutConstraint]()
let layoutGuide = referenceGuide.layoutGuide(vc: vc)
let offsetConstraint: NSLayoutConstraint
let offsetAnchor: NSLayoutDimension
switch position {
case .top:
@@ -187,10 +240,13 @@ public extension FloatingPanelAdaptiveLayoutAnchor {
offsetAnchor = vc.surfaceView.leftAnchor.anchorWithOffset(to: layoutGuide.rightAnchor)
}
if isAbsolute {
return [offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), constant: -offset)]
offsetConstraint = offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), constant: -offset)
} else {
return [offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), multiplier: (1 - offset))]
offsetConstraint = offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), multiplier: (1 - offset))
}
constraints.append(offsetConstraint)
return constraints
}
}
@@ -27,7 +27,7 @@ extension FloatingPanelReferenceEdge {
}
}
/// Constants that specify a layout guide to lay out a panel.
/// A representation to specify a rectangular area to lay out a panel.
@objc public enum FloatingPanelLayoutReferenceGuide: Int {
case superview = 0
case safeArea = 1
@@ -43,3 +43,34 @@ extension FloatingPanelLayoutReferenceGuide {
}
}
}
/// A representation to specify a bounding box which limit the content size of a panel.
@objc public enum FloatingPanelLayoutContentBoundingGuide: Int {
case none = 0
case superview = 1
case safeArea = 2
}
extension FloatingPanelLayoutContentBoundingGuide {
func layoutGuide(_ fpc: FloatingPanelController) -> LayoutGuideProvider? {
switch self {
case .superview:
return fpc.view
case .safeArea:
return fpc.fp_safeAreaLayoutGuide
case .none:
return nil
}
}
func maxBounds(_ fpc: FloatingPanelController) -> CGRect? {
switch self {
case .superview:
return fpc.view.bounds
case .safeArea:
return fpc.view.bounds.inset(by: fpc.fp_safeAreaInsets)
case .none:
return nil
}
}
}
-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() {}
}
+3 -2
View File
@@ -4,7 +4,7 @@ import Foundation
/// An object that represents the display state of a panel in a screen.
@objc
public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
open class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
public typealias RawValue = String
required public init?(rawValue: RawValue) {
@@ -13,6 +13,7 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
super.init()
}
@objc
public init(rawValue: RawValue, order: Int) {
self.rawValue = rawValue
self.order = order
@@ -33,7 +34,7 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
}
public override var debugDescription: String {
return description
return "<FloatingPanel.FloatingPanelState: \(Unmanaged.passUnretained(self).toOpaque())>"
}
/// A panel state indicates the entire panel is shown.
+36 -7
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)
@@ -54,7 +55,12 @@ public class SurfaceAppearance: NSObject {
///
/// Defaults to `.circular`.
@available(iOS 13.0, *)
public lazy var cornerCurve: CALayerCornerCurve = .circular
public var cornerCurve: CALayerCornerCurve {
get { _cornerCurve ?? .circular }
set { _cornerCurve = newValue }
}
private var _cornerCurve: CALayerCornerCurve?
/// An array of shadows used to create drop shadows underneath a surface view.
public var shadows: [Shadow] = [Shadow()]
@@ -85,7 +91,7 @@ public class SurfaceView: UIView {
/// The grabber handle size
///
/// On left/right positioned panel the width dimension is used as the height of `grabberHandle`, and vice versa.
/// On left/right positioned panel the width dimension is used as the height of ``grabberHandle``, and vice versa.
public var grabberHandleSize: CGSize = CGSize(width: 36.0, height: 5.0) { didSet {
setNeedsUpdateConstraints()
} }
@@ -313,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
@@ -408,12 +414,11 @@ public class SurfaceView: UIView {
containerView.layer.borderWidth = appearance.borderWidth
}
func set(contentView: UIView) {
func set(contentView: UIView, mode: FloatingPanelController.ContentMode) {
containerView.addSubview(contentView)
self.contentView = contentView
/* contentView.frame = bounds */ // MUST NOT: Because the top safe area inset of a content VC will be incorrect.
contentView.translatesAutoresizingMaskIntoConstraints = false
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: containerMargins.top + contentPadding.top)
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: containerMargins.left + contentPadding.left)
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: containerMargins.right + contentPadding.right)
@@ -424,9 +429,15 @@ public class SurfaceView: UIView {
rightConstraint,
bottomConstraint,
].map {
$0.priority = .required - 1;
switch mode {
case .static:
$0.priority = .required
// The reason why this priority is set to .required - 1 is #359, which fixed #294.
case .fitToBounds:
$0.priority = .required - 1
}
$0.identifier = "FloatingPanel-surface-content"
return $0;
return $0
})
self.contentViewTopConstraint = topConstraint
self.contentViewLeftConstraint = leftConstraint
@@ -437,4 +448,22 @@ public class SurfaceView: UIView {
func hasStackView() -> Bool {
return contentView?.subviews.reduce(false) { $0 || ($1 is UIStackView) } ?? false
}
func grabberAreaContains(_ location: CGPoint) -> Bool {
// Sometimes a dragging finger's location is out of surface frame.
let cappedLocation: CGPoint
// Because the maximum width / height is out of bounds in CGRect.contains(_:)
let adjustment = 1 / fp_displayScale
switch position {
case .top:
cappedLocation = CGPoint(x: location.x, y: min(location.y, bounds.height - adjustment))
case .left:
cappedLocation = CGPoint(x: min(location.x, bounds.width - adjustment), y: location.y)
case .bottom:
cappedLocation = CGPoint(x: location.x, y: max(location.y, 0))
case .right:
cappedLocation = CGPoint(x: max(location.x, 0), y: location.y)
}
return grabberAreaFrame.contains(cappedLocation)
}
}
+29 -4
View File
@@ -90,14 +90,28 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
guard
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
else { fatalError() }
fpc.show(animated: true) {
if let animator = fpc.transitionAnimator {
return animator
}
fpc.suspendTransitionAnimator(true)
fpc.show(animated: true) { [weak fpc] in
fpc?.suspendTransitionAnimator(false)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
guard let transitionAnimator = fpc.transitionAnimator else {
fatalError("The panel state must be `hidden` but it is `\(fpc.state)`")
}
return transitionAnimator
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.interruptibleAnimator(using: transitionContext).startAnimation()
}
}
@@ -111,14 +125,25 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
guard
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
else { fatalError() }
fpc.hide(animated: true) {
if let animator = fpc.transitionAnimator {
return animator
}
fpc.suspendTransitionAnimator(true)
fpc.hide(animated: true) { [weak fpc] in
fpc?.suspendTransitionAnimator(false)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
return fpc.transitionAnimator!
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.interruptibleAnimator(using: transitionContext).startAnimation()
}
}
+30 -17
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)
@@ -336,8 +351,7 @@ private class MyZombieViewController: UIViewController, FloatingPanelLayout, Flo
return .half
}
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0,
edge: .top,
referenceGuide: .superview),
@@ -347,6 +361,5 @@ private class MyZombieViewController: UIViewController, FloatingPanelLayout, Flo
.tip: FloatingPanelLayoutAnchor(absoluteInset: 60.0,
edge: .bottom,
referenceGuide: .superview),
]
}
]
}
+144 -39
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)
@@ -61,9 +61,8 @@ class CoreTests: XCTestCase {
class FloatingPanelLayout1Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview)]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] =
[.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview)]
}
let delegate = FloatingPanelTestDelegate()
@@ -106,12 +105,10 @@ class CoreTests: XCTestCase {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .half
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
]
}
let delegate = FloatingPanelTestDelegate()
@@ -185,13 +182,122 @@ class CoreTests: XCTestCase {
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: -1 * distance2), 0.0)
}
func test_updateBackdropAlpha() {
class BackdropTestLayout: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .hidden }
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
switch state {
case .full: return 0.3
case .half: return 0.0
case .tip: return 0.3
default: return 0.0
}
}
}
class BackdropTestLayout2: FloatingPanelTestLayout {
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
}
}
class TestDelegate: FloatingPanelControllerDelegate {
var layout: FloatingPanelLayout = BackdropTestLayout2()
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout { layout }
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout { layout }
}
func _floor(_ alpha: CGFloat) -> CGFloat {
return floor(fpc.backdropView.alpha * 1e+06) / 1e+06
}
let delegate = TestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = BackdropTestLayout()
fpc.showForTest()
fpc.move(to: .full, animated: false)
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
let exp1 = expectation(description: "move to full with animation")
fpc.move(to: .full, animated: true) {
exp1.fulfill()
}
wait(for: [exp1], timeout: 1.0)
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
let exp2 = expectation(description: "move to half with animation")
fpc.move(to: .half, animated: true) {
exp2.fulfill()
}
wait(for: [exp2], timeout: 1.0)
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
// Test a content mode change of FloatingPanelController
let exp3 = expectation(description: "move to tip with animation")
fpc.move(to: .tip, animated: true) {
exp3.fulfill()
}
fpc.contentMode = .fitToBounds
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must not affect the backdrop alpha by changing the content mode
wait(for: [exp3], timeout: 1.0)
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
// Test a size class change of FloatingPanelController.view
fpc.move(to: .full, animated: false)
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
fpc.willTransition(to: UITraitCollection(horizontalSizeClass: .regular), with: MockTransitionCoordinator())
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must update the alpha by BackdropTestLayout2 in TestDelegate.
// Test a view size change of FloatingPanelController.view
fpc.move(to: .full, animated: false)
delegate.layout = BackdropTestLayout()
fpc.invalidateLayout()
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
delegate.layout = BackdropTestLayout2()
fpc.viewWillTransition(to: CGSize.zero, with: MockTransitionCoordinator())
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must update the alpha by BackdropTestLayout2 in TestDelegate.
}
func test_initial_surface_position() {
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
class Layout: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .top
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring]
= [.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .superview)]
}
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
Layout()
}
}
do {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(fpc.surfaceView.position, .top)
}
do {
let fpc = FloatingPanelController()
fpc.layout = FloatingPanelTestDelegate.Layout()
XCTAssertEqual(fpc.surfaceView.position, .top)
}
}
func test_targetPosition_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview)]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring]
= [.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview)]
}
let delegate = FloatingPanelTestDelegate()
@@ -218,12 +324,10 @@ class CoreTests: XCTestCase {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .half
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
]
}
let delegate = FloatingPanelTestDelegate()
@@ -275,12 +379,10 @@ class CoreTests: XCTestCase {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
let delegate = FloatingPanelTestDelegate()
@@ -638,13 +740,11 @@ class CoreTests: XCTestCase {
class FloatingPanelLayout3PositionsWithHidden: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
@@ -670,13 +770,11 @@ class CoreTests: XCTestCase {
class FloatingPanelLayout3Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 60.0, edge: .bottom, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.half: FloatingPanelLayoutAnchor(absoluteInset: 250.0, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 60.0, edge: .bottom, referenceGuide: .superview),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
let delegate = FloatingPanelTestDelegate()
@@ -715,6 +813,13 @@ class CoreTests: XCTestCase {
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden),
])
}
func test_keep_pan_gesture_disabled() {
let fpc = FloatingPanelController()
fpc.panGestureRecognizer.isEnabled = false
fpc.showForTest()
XCTAssertFalse(fpc.panGestureRecognizer.isEnabled)
}
}
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class ExtensionTests: XCTestCase {
func test_roundedByDisplayScale() {
XCTAssertEqual(CGFloat(333.222).rounded(by: 3), 333.3333333333333)
XCTAssertNotEqual(CGFloat(333.5).rounded(by: 3), 333.66666666666674)
XCTAssertTrue(CGFloat(333.5).isEqual(to: 333.66666666666674, on: 3.0))
}
}
+46 -57
View File
@@ -13,37 +13,33 @@ class LayoutTests: XCTestCase {
override func tearDown() {}
func test_layoutAdapter_topAndBottomMostState() {
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .tip)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.mostExpandedState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.leastExpandedState, .tip)
class FloatingPanelLayoutWithHidden: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview)
]
}
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview)
]
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
}
class FloatingPanelLayout2Positions: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
}
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
let initialState: FloatingPanelState = .tip
let position: FloatingPanelPosition = .bottom
}
fpc.layout = FloatingPanelLayoutWithHidden()
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .hidden)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.mostExpandedState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.leastExpandedState, .hidden)
fpc.layout = FloatingPanelLayout2Positions()
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .half)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .tip)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.mostExpandedState, .half)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.leastExpandedState, .tip)
}
func test_layoutSegment_3position() {
@@ -138,31 +134,31 @@ class LayoutTests: XCTestCase {
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: false,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos + 100.0,
overflow: false,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos + 100.0,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos + 100.0)
@@ -190,31 +186,31 @@ class LayoutTests: XCTestCase {
var next: CGFloat
pre = fpc.surfaceLocation.y
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: false,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, pre)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: fullPos - tipPos,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: fullPos - tipPos + 100,
overflow: false,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: fullPos - tipPos + 100,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos + 100.0)
@@ -224,12 +220,10 @@ class LayoutTests: XCTestCase {
func test_updateInteractiveEdgeConstraintWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .bottom, referenceGuide: .safeArea),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
}
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .bottom, referenceGuide: .safeArea),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .bottom, referenceGuide: .superview),
]
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
}
@@ -245,19 +239,19 @@ class LayoutTests: XCTestCase {
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: false,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos - 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: hiddenPos - fullPos + 100.0,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, hiddenPos + 100.0)
@@ -267,12 +261,10 @@ class LayoutTests: XCTestCase {
func test_updateInteractiveEdgeConstraintWithHidden_bottomEdge() {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
[
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .bottom, referenceGuide: .safeArea),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .top, referenceGuide: .superview),
]
}
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 18.0, edge: .bottom, referenceGuide: .safeArea),
.hidden: FloatingPanelLayoutAnchor(absoluteInset: 0, edge: .top, referenceGuide: .superview),
]
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .top
}
@@ -288,19 +280,19 @@ class LayoutTests: XCTestCase {
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
overflow: false,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: 100.0,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: hiddenPos - fullPos + 100.0,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, hiddenPos + 100.0)
@@ -310,12 +302,10 @@ class LayoutTests: XCTestCase {
func test_updateInteractiveTopConstraintWithMinusInsets() {
class FloatingPanelLayoutMinusInsets: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
[
.full: FloatingPanelLayoutAnchor(absoluteInset: -200, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: -200, edge: .bottom, referenceGuide: .safeArea),
]
}
let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: -200, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: -200, edge: .bottom, referenceGuide: .safeArea),
]
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .bottom
}
@@ -328,19 +318,19 @@ class LayoutTests: XCTestCase {
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: false,
scrollingContent: true,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: -100.0,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, fullPos - 100)
fpc.floatingPanel.layoutAdapter.updateInteractiveEdgeConstraint(diff: tipPos - fullPos + 100.0,
overflow: true,
scrollingContent: false,
allowsRubberBanding: fpc.floatingPanel.behaviorAdapter.allowsRubberBanding(for:))
next = fpc.surfaceLocation.y
XCTAssertEqual(next, tipPos + 100)
@@ -474,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() {
+132 -47
View File
@@ -48,35 +48,37 @@ class SurfaceViewTests: XCTestCase {
}
func test_surfaceView_contentView() {
XCTContext.runActivity(named: "Bottom sheet") { _ in
for (position, mode, line) in [
(.top, .static, #line),
(.top, .fitToBounds, #line),
(.bottom, .static, #line),
(.bottom, .fitToBounds, #line),
] as [(FloatingPanelPosition, FloatingPanelController.ContentMode, UInt)] {
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.position = position
surface.layoutIfNeeded()
let contentView = UIView()
surface.set(contentView: contentView)
surface.set(contentView: contentView, mode: mode)
let height = surface.bounds.height * 2
surface.containerOverflow = height
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
}
XCTContext.runActivity(named: "Top sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.position = .top
surface.layoutIfNeeded()
let contentView = UIView()
surface.set(contentView: contentView)
let height = surface.bounds.height * 2
surface.containerOverflow = height
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, CGRect(x: 0.0, y: -height, width: 320.0, height: 480.0 * 3))
XCTAssertEqual(surface.convert(surface.contentView?.frame ?? .zero, from: surface.containerView),
surface.bounds)
switch position {
case .top:
XCTAssertEqual(surface.containerView.frame,
CGRect(x: 0.0, y: -height, width: 320.0, height: 480.0 * 3),
line: line)
XCTAssertEqual(surface.convert(surface.contentView?.frame ?? .zero, from: surface.containerView),
surface.bounds,
line: line)
case .bottom:
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds, line: line)
default:
break
}
}
}
@@ -140,46 +142,45 @@ class SurfaceViewTests: XCTestCase {
}
func test_surfaceView_contentInsets() {
XCTContext.runActivity(named: "Top sheet") { _ in
for (position, mode, line) in [
(.top, .static, #line),
(.top, .fitToBounds, #line),
(.bottom, .static, #line),
(.bottom, .fitToBounds, #line),
] as [(FloatingPanelPosition, FloatingPanelController.ContentMode, UInt)] {
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.position = .top
surface.position = position
let contentView = UIView()
surface.set(contentView: contentView)
surface.set(contentView: contentView, mode: mode)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds, line: line)
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds.inset(by: surface.contentPadding))
}
XCTContext.runActivity(named: "Bottom sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
let contentView = UIView()
surface.set(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds.inset(by: surface.contentPadding))
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds.inset(by: surface.contentPadding), line: line)
}
}
func test_surfaceView_containerMargins_and_contentInsets() {
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
let contentView = UIView()
surface.set(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.containerView.bounds.inset(by: surface.contentPadding))
for (mode, line) in [
(.static, #line),
(.fitToBounds, #line),
] as [(FloatingPanelController.ContentMode, UInt)] {
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
let contentView = UIView()
surface.set(contentView: contentView, mode: mode)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds, line: line)
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins), line: line)
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.containerView.bounds.inset(by: surface.contentPadding), line: line)
}
}
func test_surfaceView_cornderRaduis() {
func test_surfaceView_cornerRadius() {
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssert(surface.containerView.layer.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.masksToBounds == false)
@@ -227,4 +228,88 @@ class SurfaceViewTests: XCTestCase {
XCTAssert(surface.containerView.layer.borderColor == UIColor.red.cgColor)
XCTAssert(surface.containerView.layer.borderWidth == 3.0)
}
func test_surfaceView_grabberArea() {
let sv = SurfaceView()
sv.bounds = .init(x: 0, y: 0, width: 375, height: 500)
sv.grabberAreaOffset = 44.0
// Top
do {
sv.position = .top
XCTAssertEqual(sv.grabberAreaFrame,
.init(x: 0,
y: sv.bounds.height - sv.grabberAreaOffset,
width: sv.bounds.width,
height: sv.grabberAreaOffset))
}
// Bottom
do {
sv.position = .bottom
XCTAssertEqual(sv.grabberAreaFrame,
.init(x: 0,
y: 0,
width: sv.bounds.width,
height: sv.grabberAreaOffset))
}
// Left
do {
sv.position = .left
XCTAssertEqual(sv.grabberAreaFrame,
.init(x: sv.bounds.width - sv.grabberAreaOffset,
y: 0,
width: sv.grabberAreaOffset,
height: sv.bounds.height))
}
// Right
do {
sv.position = .right
XCTAssertEqual(sv.grabberAreaFrame,
.init(x: 0,
y: 0,
width: sv.grabberAreaOffset,
height: sv.bounds.height))
}
}
func test_surfaceView_grabberAreaDetection() {
let sv = SurfaceView()
sv.bounds = .init(x: 0, y: 0, width: 375, height: 500)
// Top
do {
sv.position = .top
XCTAssertTrue(sv.grabberAreaContains(.init(x: 0, y: sv.bounds.height)))
XCTAssertTrue(sv.grabberAreaContains(.init(x: sv.bounds.width / 2, y: sv.bounds.height)))
XCTAssertTrue(sv.grabberAreaContains(.init(x: sv.bounds.width / 2, y: sv.bounds.height + 2)))
XCTAssertFalse(sv.grabberAreaContains(.init(x: -2, y: sv.bounds.height)))
XCTAssertFalse(sv.grabberAreaContains(.init(x: -2, y: -2)))
}
// Bottom
do {
sv.position = .bottom
XCTAssertTrue(sv.grabberAreaContains(.init(x: 0, y: 0)))
XCTAssertTrue(sv.grabberAreaContains(.init(x: sv.bounds.width / 2, y: 0)))
XCTAssertTrue(sv.grabberAreaContains(.init(x: sv.bounds.width / 2, y: -2)))
XCTAssertFalse(sv.grabberAreaContains(.init(x: -2, y: 0)))
XCTAssertFalse(sv.grabberAreaContains(.init(x: -2, y: -2)))
}
// Left
do {
sv.position = .left
XCTAssertTrue(sv.grabberAreaContains(.init(x: sv.bounds.width, y: 0)))
XCTAssertTrue(sv.grabberAreaContains(.init(x: sv.bounds.width, y: sv.bounds.height / 2)))
XCTAssertTrue(sv.grabberAreaContains(.init(x: sv.bounds.width + 2, y: sv.bounds.height / 2)))
XCTAssertFalse(sv.grabberAreaContains(.init(x: sv.bounds.width, y: -2)))
XCTAssertFalse(sv.grabberAreaContains(.init(x: -2, y: -2)))
}
// Right
do {
sv.position = .right
XCTAssertTrue(sv.grabberAreaContains(.init(x: 0, y: 0)))
XCTAssertTrue(sv.grabberAreaContains(.init(x: 0, y: sv.bounds.height / 2)))
XCTAssertTrue(sv.grabberAreaContains(.init(x: -2, y: sv.bounds.height / 2)))
XCTAssertFalse(sv.grabberAreaContains(.init(x: 0, y: -2)))
XCTAssertFalse(sv.grabberAreaContains(.init(x: -2, y: -2)))
}
}
}
+23 -1
View File
@@ -18,7 +18,7 @@ extension FloatingPanelController {
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
var position: FloatingPanelState = .hidden
var didMoveCallback: ((FloatingPanelController) -> Void)?
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
func floatingPanelDidChangeState(_ vc: FloatingPanelController) {
position = vc.state
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
@@ -77,3 +77,25 @@ class FloatingPanelProjectableBehavior: FloatingPanelBehavior {
return true
}
}
class MockTransitionCoordinator: NSObject, UIViewControllerTransitionCoordinator {
func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool { true }
func animateAlongsideTransition(in view: UIView?, animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool { true }
func notifyWhenInteractionEnds(_ handler: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {}
func notifyWhenInteractionChanges(_ handler: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {}
var isAnimated: Bool = false
var presentationStyle: UIModalPresentationStyle = .fullScreen
var initiallyInteractive: Bool = false
var isInterruptible: Bool = false
var isInteractive: Bool = false
var isCancelled: Bool = false
var transitionDuration: TimeInterval = 0.25
var percentComplete: CGFloat = 0
var completionVelocity: CGFloat = 0
var completionCurve: UIView.AnimationCurve = .easeInOut
func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController? { nil }
func view(forKey key: UITransitionContextViewKey) -> UIView? { nil }
var containerView: UIView { UIView() }
var targetTransform: CGAffineTransform = .identity
}
-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>
-12
View File
@@ -1,12 +0,0 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class UtilTests: XCTestCase {
func test_displayTrunc() {
XCTAssertEqual(displayTrunc(333.222, by: 3), 333.3333333333333)
XCTAssertNotEqual(displayTrunc(333.5, by: 3), 333.66666666666674)
XCTAssertTrue(displayEqual(333.5, 333.66666666666674, by: 3))
}
}