Compare commits

...

196 Commits

Author SHA1 Message Date
Shin Yamamoto 3a840df79e Release v1.5.0 2019-04-04 12:08:27 +09:00
Shin Yamamoto 6851e3b072 Support both of Swift 5 and 4.2
The default Swift version leaves 4.0. Because it avoids build errors
with Carthage on each Xcode version from the source compatibility
between Swift 4.0, 4.2 and 5.0.

With regard to CocoaPods, the pod spec is going to support
`swift_versions` introduced in CocoaPods v1.7.0. For now, a user needs
to override `SWIFT_VERSION` appropriately in Podfile.
2019-04-04 12:08:27 +09:00
Shin Yamamoto 5a2b079872 Merge pull request #183 from SCENEE/release-1.4.1
Release 1.4.1
2019-04-04 09:40:10 +09:00
Shin Yamamoto f683f987d8 Release v1.4.1 2019-04-03 23:14:56 +09:00
Shin Yamamoto 3626621e87 Fix the umbrella header name 2019-04-03 23:14:56 +09:00
Shin Yamamoto cc2d1eb002 Merge pull request #178 from SCENEE/notify-retain-cycle
Notify a retain cycle
2019-04-03 23:13:20 +09:00
Shin Yamamoto 5ba19bcf8b Add an attention comment in Samples app 2019-04-03 21:23:49 +09:00
Shin Yamamoto 8391686e28 Clean up Logger
`.fault` log is removed because the level are intended for capturing
system-level or multi-process errors only in `os_log`.
2019-04-03 21:23:49 +09:00
Shin Yamamoto 38327c917f Add tests to quick-check warnings 2019-04-03 21:23:49 +09:00
Shin Yamamoto fca0f399b2 Merge pull request #181 from SCENEE/fix-loop-crash
Fix an infinite loop crash in iOS 10
2019-04-03 21:17:39 +09:00
Shin Yamamoto e3b7ac0e99 Add 'Test' configuration 2019-04-03 08:21:35 +09:00
Shin Yamamoto 04cd357f68 Print retain cycle warnings 2019-04-03 08:21:35 +09:00
Shin Yamamoto 68f48f714d Fix an infinite loop crash in iOS 10 2019-04-02 21:47:56 +09:00
Shin Yamamoto fa586c494f Merge pull request #180 from SCENEE/fix-scroll-fitting
Fix scroll fitting
2019-04-02 09:43:37 +09:00
Shin Yamamoto b886a0da64 Fix scroll fitting
This is to prevent a surface frame jump on scroll fitting. In fact,
a scroll offset range for fitting should be -10..<0.
2019-03-30 18:52:48 +09:00
Shin Yamamoto 0616aec3d2 Prevent an unexpected layout update
The KVO for `safeAreaInset` can be invoked even when new and old values
are the same value so that the surface of a floating panel sometimes
jumps in dragging by an unexpected layout update called from a
`safeAreaInset` update.
2019-03-30 15:45:41 +09:00
Shin Yamamoto 7d6f295e72 Merge pull request #169 from SCENEE/fix-ambiguous-cornerradius
Open FloatingPanelSurfaceView.backgroundView
2019-03-29 21:05:25 +09:00
Shin Yamamoto 1c952b6dcb Open FloatingPanelSurfaceView.backgroundView
This change lets a user be able to set up a corner radius and shadow of
the surface view, and has the library not update them unnecessarily.

As a result, a user can escape "Ambiguous use of 'cornerRadius'" error
as below, if it's used with a `cornerRadius` property of `UIView`
extension defined by the user.

```swift
extension UIView {
    @objc dynamic var cornerRadius: CGFloat {
        get { return self.layer.cornerRadius }
        set(cornerRadius) {
            self.layer.masksToBounds = true
            self.layer.cornerRadius = cornerRadius
        }
    }
}

...

public extension FloatingPanelSurfaceView {
    @objc public dynamic var fp_cornerRadius: CGFloat {
        get { return backgroundView.layer.cornerRadius }
        set {
             if #available(iOS 11.0, *) {
                 contentView?.layer.masksToBounds = true
                 contentView?.layer.cornerRadius = newValue
                 contentView?.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
             }
             backgroundView.layer.masksToBounds = true
             backgroundView.layer.cornerRadius = newValue
       }
    }
}
```

squash! Open FloatingPanelSurfaceView.backgroundView
2019-03-29 20:34:11 +09:00
Shin Yamamoto 7e7c2a0fd7 Merge pull request #170 from SCENEE/fix-top-dragging-boundary
Fix top dragging boundary
2019-03-27 22:41:56 +09:00
Shin Yamamoto a15444d237 Modify the height calc with a top-most position
According to the previous change, `.half` position meaning is
also changed so that the height calc logic must be modified.

Previously, for example, `.half` position has 2 meaning. One is
a position at half of a screen. Another is a position when a
content is visible half. `.tip` position is also same.

Now `.half`/`.tip` position can display a full content if it's
top most. In the case, the surface height should fit to a height
for `.half`/`.tip` position.
2019-03-26 22:23:27 +09:00
Shin Yamamoto 5b100f3b22 Add FloatingPanelLayout.topMostState
The previous implementation has an implicit pre-condition where .full
position is supposed to be top most. It also means .full position should
be included in a set of the supported positions. But it's not
appropriate because there is a case when a user wants to configure a top
most position of a layout with a bottom inset. This commit lets a panel
work well even if .half/.tip position is top most.

According to the change, .full position means a position where a content
is not just visible fully, but also filled in a full screen almost.
2019-03-26 22:23:16 +09:00
Shin Yamamoto 9fa8a48c56 Fix top panning boundary 2019-03-26 11:41:13 +09:00
Shin Yamamoto cb54a2a7e1 Merge pull request #151 from SCENEE/remove-swapping-scroll-delegate
Remove swapping delegate
2019-03-26 09:55:47 +09:00
Shin Yamamoto ed02713ccc Merge pull request #168 from SCENEE/fix-safeareainsets
Change FloatingPanelLayoutAdapter.safeAreaInsets as a read-only property
2019-03-25 09:50:46 +09:00
Shin Yamamoto 68e3fd2093 ChangeFloatingPanelLayoutAdapter.safeAreaInsets as a read-only property
This solves the problem that a panel's layout and dragging can be
inappropriate. The root cause is that a value of the `safeAreaInsets`
is sometimes different from an actual value referenced from Auto Layout.
It's led by the design that the `safeAreaInsets` is defined as a stored
property. Therefore this commit resolves the issue.
2019-03-23 18:36:26 +09:00
Shin Yamamoto a43f73d7b1 Merge pull request #159 from SCENEE/fix-interruption
Fix interruption
2019-03-20 21:39:20 +09:00
Shin Yamamoto 61e0c4ed0a Merge pull request #165 from SCENEE/fix-ui-extensions
Fix UI extensions
2019-03-20 21:38:28 +09:00
Shin Yamamoto 53e0629b1d Merge pull request #162 from zntfdr/patch-2
Fix typo
2019-03-19 09:31:13 +09:00
Shin Yamamoto 2b1d6a3d8a Fix 'Redundant conformance' error of UIGestureRecognizerState 2019-03-18 10:21:36 +09:00
Federico Zanetello 5ba1fb3d95 Add missing space 2019-03-16 12:42:51 +07:00
Federico Zanetello 1b7c15cdb5 Fix typo 2019-03-16 12:38:54 +07:00
Shin Yamamoto 81fd85e993 Prevent bouncing a scroll content in a panel animating
This issue happens when a panel interacts from tip to half
in 'Show Tab Bar' -> 'Tab 3'.
2019-03-09 10:41:26 +09:00
Shin Yamamoto 8f4c08d5b3 Fix TabBarContentViewController for Tab 3 2019-03-09 10:36:31 +09:00
Shin Yamamoto 61b6429851 Fix the buggy top buffer in scroll tracking
Should not handle a panel gesture even when a zero scroll velocity.
If not, it causes that a scroll view will be handled after a panel
moves a bit in a top buffer. As a result, a panel frame jumps from
a location in a top buffer to topY.
2019-03-09 10:04:18 +09:00
Shin Yamamoto bbc6b39c08 Fix issues on an animation interruption
* Adjust a final position to work scroll tracking well
* Fix issues on an animation cancellation
2019-03-09 10:04:18 +09:00
Shin Yamamoto 3f812f4d6d Merge pull request #156 from SCENEE/fix-layout-assertion
Fix the layout assertion
2019-03-09 10:03:44 +09:00
Shin Yamamoto c9b15e4239 Merge pull request #158 from bryansum/master
Fix retain cycle in FloatingPanel
2019-03-09 09:57:11 +09:00
Bryan Summersett 5fbdb3d481 Make floatingPanel weak 2019-03-08 15:02:33 -08:00
Shin Yamamoto c59e1cd7fc Fix the layout assertion
The assertion didn't allow `.hidden` initial position.
2019-03-08 19:39:10 +09:00
Shin Yamamoto ce891e47da Merge pull request #155 from kfinteractive/master
Adds support for respecting initial safeAreaInsets.
2019-03-08 19:23:58 +09:00
Gunnar Herzog 7160e4a42e Removes unnecessary guard statement. 2019-03-07 08:56:29 +01:00
Gunnar Herzog eba857a285 Adds support for respecting initial safeAreaInsets. 2019-03-07 08:45:05 +01:00
Shin Yamamoto a1dd02c780 Stop scroll deceleration without swapping delegate 2019-03-05 08:31:27 +09:00
Shin Yamamoto 87ff5d629b Merge pull request #143 from SCENEE/release-v1.4.0
Release v1.4.0
2019-02-27 10:00:47 +09:00
Shin Yamamoto 6b75523428 Release v1.4.0 2019-02-27 09:33:14 +09:00
Shin Yamamoto 773434d4f6 Merge pull request #119 from SCENEE/improve-operability
Improve the operability
2019-02-27 09:32:15 +09:00
Shin Yamamoto ad6dcd0314 Clean up sample 2019-02-27 09:04:42 +09:00
Shin Yamamoto b9e29ad87d Fix typo 2019-02-26 10:07:30 +09:00
Shin Yamamoto 32b965ba87 Merge pull request #142 from zntfdr/patch-1
Add README missing character
2019-02-26 09:22:30 +09:00
Federico Zanetello f1b315c9ea Add missing character 2019-02-25 12:50:37 +07:00
Shin Yamamoto 459fc75af3 Tidy up logs 2019-02-25 13:02:49 +09:00
Shin Yamamoto 9b0cd3511f Revamp scroll draw-down action
A scroll view offset often isn't around zero by a user action.
Therefore, rather than checking a offset value, it's working well
to fits a scroll view offset to the surface view bounds by
manipulating their frames a bit temporarily.
2019-02-25 13:02:49 +09:00
Shin Yamamoto af9b988507 Merge pull request #136 from SCENEE/release-v1.3.5
Release v1.3.5
2019-02-23 11:18:21 +09:00
Shin Yamamoto 36f297c35b Allow the grabber area just on touch began 2019-02-23 10:28:34 +09:00
Shin Yamamoto ff959f71a7 Prevent the unexpected scrolling on full state by the second finger 2019-02-23 10:28:34 +09:00
Shin Yamamoto 0a4312ada6 Reduce logger function calls 2019-02-23 10:28:34 +09:00
Shin Yamamoto 5411cdc07a Change time to call floatingPanelWillBeginDecelerating() 2019-02-23 10:28:34 +09:00
Shin Yamamoto a8c6fba3c1 Call floatingPanelDidEndDecelerating(_:) when an animation is interruptted 2019-02-23 10:28:34 +09:00
Shin Yamamoto 11b115b47b Tear down the active interaction in moving programmatically 2019-02-23 10:28:34 +09:00
Shin Yamamoto 22edf5ce46 Prevent unexpected crash
This assertion can be called unexpectedly by the gesture recognizer behavior
of UIScrollView
2019-02-23 10:28:34 +09:00
Shin Yamamoto f43f7df7f3 Prevent calling didMove delegate when it's not moving 2019-02-23 10:28:34 +09:00
Shin Yamamoto 3a2633d818 Must update a panel layout after it becomes a new state
To synchronize layout updates in floatingPanelDidChangePosition(_:) by
a user
2019-02-23 10:28:34 +09:00
Shin Yamamoto 04a62bcf74 Refactor FloatingPanelSurfaceView for AutoLayout backed animation 2019-02-23 10:28:34 +09:00
Shin Yamamoto 6c1320168c Fix a buggy scroll offset on a scroll inset change by a user
* The controller can bother a user who wants to adjust a scroll offset
for a position at `floatingPanelDidChangePosition()` delegate method.
* This prevents interrupting a tracking scroll offset at the end of interaction
2019-02-23 10:28:34 +09:00
Shin Yamamoto 8657c91002 Fix the boundary of the interactive top constraint 2019-02-23 10:28:34 +09:00
Shin Yamamoto bafe492009 Normalize the projected position
It resolves the behavior is determined by the screen size.
2019-02-23 10:28:34 +09:00
Shin Yamamoto c6197ef6a3 Modify the default momentum projection behavior 2019-02-23 10:28:34 +09:00
Shin Yamamoto 1b3f16bcd5 Fix disabling bottom constraints 2019-02-23 10:28:34 +09:00
Shin Yamamoto 28712fdeca Add ThreeTabBarPanelLayout as an advanced layout sample 2019-02-23 10:28:34 +09:00
Shin Yamamoto 0c30b68a9e Update FloatingPanelFullScreenLayout spec
It's useful that all insets(tip, half and full) indicates a inset
from the superview, not the safe area because a user can layout
a floating panel regardless of the safe area or layout guides(iOS10)
whose values depend on iOS system behavior.
2019-02-21 19:19:48 +09:00
Shin Yamamoto 30c4bee432 Remove FloatingPanel.getCurrentY(from:with) 2019-02-21 19:19:48 +09:00
Shin Yamamoto ece9ced085 Configure momentum project with FloatingPanelBehavior
* shouldProjectMomentum(_:for:)
* momentumProjectionRate(_:)
2019-02-21 19:19:48 +09:00
Shin Yamamoto f231105752 Enable to cancel tracking a scroll view 2019-02-21 19:19:48 +09:00
Shin Yamamoto 91dfc1e086 Open the default layout/behavior 2019-02-21 19:19:48 +09:00
Shin Yamamoto b2c59c17aa Modify setBackdropAlpha(of:) as private 2019-02-21 19:19:48 +09:00
Shin Yamamoto 10d1a920f0 Add floatingPanelShouldBeginDragging(_:) delegate method
* Update doc comments
2019-02-21 19:19:48 +09:00
Shin Yamamoto 4cb79a14fc Move the surface view with a top layout constraint 2019-02-21 19:19:48 +09:00
Shin Yamamoto 402b9bd8dc Release v1.3.5 2019-02-21 18:42:02 +09:00
Shin Yamamoto c39cc9d93b Fix a regression of the interaction 2019-02-21 18:42:02 +09:00
Shin Yamamoto aad56ab0a7 Merge pull request #135 from SCENEE/fix-interaction
Fix interaction
2019-02-16 14:51:56 +09:00
Shin Yamamoto fe18e493a9 Fix dragging outside safe area 2019-02-16 10:31:17 +09:00
Shin Yamamoto 5d14166508 Fix an interruptive animator handling
* A velocity parameter passed by an animator had a wrong sign so that an
interruptive animator could be buggy.
* Improve the default pan gesture recognizer to interrupt an animation smoothly.
2019-02-16 10:29:11 +09:00
Shin Yamamoto e1a745e3b5 Clean up 2019-02-15 15:02:10 +09:00
Shin Yamamoto a0cac28ed0 Improve {re}directional position calc 2019-02-15 15:02:10 +09:00
Shin Yamamoto c205dc8672 Improve animator handling 2019-02-15 15:02:10 +09:00
Shin Yamamoto 5c0ed4cf7d Fix the wrong layout update on iOS10
On iOS 10, there is a case when a floating panel is updated by a
different position(the previous position) from the target position in
animating. This is because `FloatingPanelController` calls
`update(safeAreaInsets:)` in `viewDidLayoutSubviews()` unexpectedly.
2019-02-15 10:29:01 +09:00
Shin Yamamoto 780472a17f Stop unexpected scrolling by decelerating on tip and half 2019-02-15 10:15:34 +09:00
Shin Yamamoto 0264db3d54 Should always recognize tap/long press gestures in parallel 2019-02-15 09:55:36 +09:00
Shin Yamamoto c117594669 Merge pull request #132 from SCENEE/release-v1.3.4
Release v1.3.4
2019-02-14 09:45:31 +09:00
Shin Yamamoto 5214bd8936 Release v1.3.4 2019-02-14 09:13:09 +09:00
Shin Yamamoto a1195be08e Merge pull request #134 from g00m/feature/fix_typo_in_readme
Fix typo in `README.md`
2019-02-14 09:11:53 +09:00
Etienne Negro b69d366538 Fix typo in README-md 2019-02-13 14:28:39 +01:00
Shin Yamamoto c9de6f0dc3 Merge pull request #131 from modestman/master
Fix issue with jumping floating panel while dragging
2019-02-12 09:50:10 +09:00
Anton Glezman 21be693a9a Fix issue with jumping floating panel while dragging 2019-02-11 12:28:13 +03:00
Shin Yamamoto 129362fcd0 Merge pull request #129 from SCENEE/fix-typo
Fix typo
2019-02-06 09:40:58 +09:00
Shin Yamamoto 75e2fcc3ce Fix README 2019-02-05 22:46:56 +09:00
Shin Yamamoto 4f56b57b0e Replace FloatingPanel.panGesture with panGestureRecognizer 2019-02-05 22:10:03 +09:00
Shin Yamamoto f9bbdf3427 Update ISSUE_TEMPLATE.md 2019-02-02 18:19:06 +09:00
Shin Yamamoto fcf200e169 Merge pull request #124 from SCENEE/release-1.3.3
Release v1.3.3
2019-02-02 12:19:59 +09:00
Shin Yamamoto 7d668c8525 Release v1.3.3 2019-02-02 11:38:37 +09:00
Shin Yamamoto ebd4a32bfc Merge pull request #123 from SCENEE/fix-cut-off-by-mask
Expand the surface mask's height
2019-02-02 11:37:20 +09:00
Shin Yamamoto 6aa739231d Expand the surface mask's height
`FloatingPanelSurfaceView.updateContentViewMask()` causes a content to be cut off.
2019-02-01 10:19:51 +09:00
Shin Yamamoto 8877d32ced Merge pull request #122 from SCENEE/fix-animated-presentation-modally
Fix the presentation modally when fpc is reused
2019-01-31 19:22:46 +09:00
Shin Yamamoto 7f025ae845 Remove the unnecessary code to fix the presentation modally
It's added at `a095ace` commit, but now not needed by the previous
commit.
2019-01-31 12:48:36 +09:00
Shin Yamamoto 1b2dae2135 Fix presentation modally when fpc is reused 2019-01-30 13:09:04 +09:00
Shin Yamamoto 6e4e9df616 Merge pull request #114 from datwelk/hotfix/restore-scroll-view-delegate
Restore original scroll view delegate when updating content VC
2019-01-28 19:11:39 +09:00
Damiaan Twelker 49e868a505 restore original scroll view delegate when updating content viewcontroller 2019-01-26 12:41:17 +01:00
Shin Yamamoto f1b70e0367 Add the issue template 2019-01-25 22:08:44 +09:00
Shin Yamamoto 1d0e747578 Update README.md 2019-01-24 11:00:45 +09:00
Shin Yamamoto 2c72d07cab Merge pull request #97 from SCENEE/support-full-screen
Support full screen layout
2019-01-19 16:41:32 +09:00
Shin Yamamoto 31c057f9f8 Fix typo 2019-01-19 14:53:36 +09:00
Shin Yamamoto d3033df9da Add FloatingPanelFullScreenLayout doc comment 2019-01-19 14:51:04 +09:00
Shin Yamamoto 459d82b1c6 Update 'Show Tab Bar' for full screen layout 2019-01-19 14:06:23 +09:00
Shin Yamamoto 85d7ca640e Stop manipulating a scroll content inset for FloatingPanelFullScreenLayout
It's better that the manipulation should be operated in a user application code.
2019-01-19 14:06:23 +09:00
Shin Yamamoto c1b7f2f092 Merge pull request #106 from SCENEE/release-1.3.2
Release v1.3.2
2019-01-18 09:31:28 +09:00
Shin Yamamoto b7a7e0d4ad Release v1.3.2 2019-01-17 10:47:45 +09:00
Shin Yamamoto dc7f6d58f9 Fix the swift version in podspec 2019-01-17 10:47:45 +09:00
Shin Yamamoto a2a10bd0d3 Merge pull request #104 from SCENEE/fix-modal-presentation
Fix the modal presentation
2019-01-15 09:31:39 +09:00
Shin Yamamoto b54c8ee6ee Merge pull request #105 from CedricGatay/fix/delegateIgnored
fix(delegateIgnored): Allow to set delegate on init
2019-01-14 19:43:39 +09:00
Cedric Gatay 08d275690a fix(delegateIgnored): fix for animation use case
Do not force update layout to allow the user to animate the change later on.
2019-01-14 11:05:50 +01:00
Cedric Gatay 1c307f751e fix(delegateIgnored): listen on delegate change
Properly listens for delegate changes and trigger behavior / layout changes accordingly.

Made constructor parameter name explicit.
2019-01-14 08:49:23 +01:00
Cedric Gatay 6f06a0f7fc fix(delegateIgnored): Allow to set delegate on init
Typical use case is :

```
let floatingVC = FloatingPanelController()
floatingVC.delegate = self
```

This PR allows to set the delegate straight away by using `let floatingVC = FloatingPanelController(self)`
Its true reason is that the setup code that fetches behavior / layout through delegate is called without the delegate being set is useless on `init`ing.
Another implementation could be observing the `didSet` event on delegate to do the `setUp` for `FloatingPanel`
2019-01-12 15:15:48 +01:00
Shin Yamamoto a095ace30e Fix the modal presentation 2019-01-11 19:04:39 +09:00
Shin Yamamoto a486f61f5f Merge pull request #102 from SCENEE/fix-secound-touch-crash
Fix a crash on tap dismissal with a second touch
2019-01-10 09:38:06 +09:00
Shin Yamamoto 14e0abc240 Merge pull request #103 from SCENEE/fix-unsatisfiable-constraints-error
Fix unsatisfiable constraints error for safe area bottom on full state
2019-01-10 09:37:46 +09:00
Shin Yamamoto 32203c48bd Fix unsatisfiable constraints error for safe area bottom on full state 2019-01-10 09:05:17 +09:00
Shin Yamamoto 9d6024f603 Fix a crash on tap dismissal with a second touch
For example, a user tap the backdrop view to dismiss it while dragging
the surface view on presentation modally.
2019-01-10 08:43:46 +09:00
Shin Yamamoto a4dd4e48e7 Merge pull request #98 from SCENEE/support-swift4.1
Support Swift 4.1
2019-01-10 08:25:50 +09:00
Shin Yamamoto e6f7456a0f Merge pull request #101 from SCENEE/fix-touch-handling-on-presentation-modally
Fix the event handling on presentation modally
2019-01-10 08:25:26 +09:00
Shin Yamamoto fca79c9b0c Fix the event handling on presentation modally
If an alpha of the controller's backdrop view is zero, the presentation controller
must not block any touch event outside of surface view.
2019-01-09 09:32:17 +09:00
Shin Yamamoto 4ad7f11e93 Support Swift 4.1 2019-01-05 21:44:05 +09:00
Shin Yamamoto 0412bdc996 Support full screen layout 2019-01-05 12:26:49 +09:00
Shin Yamamoto 31faeaada3 Merge pull request #93 from SCENEE/release-1.3.1
Release v1.3.1
2018-12-30 09:52:25 +09:00
Shin Yamamoto 72539ca973 Release v1.3.1 2018-12-30 09:22:33 +09:00
Shin Yamamoto ef94630aa1 Update README 2018-12-30 09:22:33 +09:00
Shin Yamamoto cb696f9992 Merge pull request #86 from SCENEE/improve-samples-app
Fix layout bugs on v1.3.0
2018-12-29 14:33:13 +09:00
Shin Yamamoto aa23e404e1 Fix a content offset of a tracking scroll view
This bug only happens on 'Shoe Tab Bar' in Samples app on landscape
orientation.
2018-12-28 16:08:10 +09:00
Shin Yamamoto 4c0749640f Remove an unnecessary comment 2018-12-28 15:13:59 +09:00
Shin Yamamoto ee5661f304 Fix UI freezes on presentation modally from the second time 2018-12-28 15:13:59 +09:00
Shin Yamamoto ddefdc4f34 Fix crash when content view is nil 2018-12-28 15:13:59 +09:00
Shin Yamamoto f2a0af1646 Fix intrinsic height 2018-12-28 15:13:59 +09:00
Shin Yamamoto 7ded61c2bc No longer need traitCollectionDidChange(_:) 2018-12-28 15:13:59 +09:00
Shin Yamamoto 08aabcf6dd Fix bugs on iOS 10 2018-12-28 15:13:59 +09:00
Shin Yamamoto e19af7e67d Set up the height constraints with the root view's heightAnchor
* `vc.view.bounds.height` causes some layout problems. This change
  makes a content view layout more robust.
* Enable to configure the content-hugging and compression-resistance
2018-12-28 15:13:59 +09:00
Shin Yamamoto 62aa07e28e Refactor layout logic
- Update README
- Remove FloatingPanelSurfaceWrapperView
- Remove content wrapper view of FloatingPanelSurfaceView
- Remove {setUp,tearDown}Views in FloatingPanel
- Modify timing to call FloatingPanelLayoutAdapter.checkLayoutConsistance()
- Fix invalid surface height on orientation change
- Fix a layout problem on SafeArea.Top
    - Fix invalid top inset of safe area on a navigation bar with search bar
- Fix content offsets of a tracking scroll view
    - Fix the content offsets on orientation change(Regression)
- Fix FloatingPanelPresentationController
- Fix intrinsic height handling
- Reduce re-rendering the surface view unexpectedly
2018-12-28 15:13:59 +09:00
Shin Yamamoto 03b0bf747e Improve Sample apps for debugging
- Add InspectableViewController
- Add version label in Samples app
2018-12-28 15:13:59 +09:00
Shin Yamamoto 8dc75aed55 Always disable Auto Layout for Safe area bottom 2018-12-28 15:13:59 +09:00
Shin Yamamoto d5f5e99010 Fix dismiss swizzling 2018-12-28 15:13:59 +09:00
Shin Yamamoto 18c46d191e Fix panel is duplicated on orientation change 2018-12-28 15:13:59 +09:00
Shin Yamamoto e9f92430b2 Add a disable tracking sample to edit a table view 2018-12-28 15:13:59 +09:00
Shin Yamamoto 3106865449 Add SettingsViewController in Samples App 2018-12-28 15:13:44 +09:00
Shin Yamamoto 375e7a59e2 Merge pull request #89 from SCENEE/fix-failure-requirements
Fix failure requirements
2018-12-28 12:59:37 +09:00
Shin Yamamoto bc4a2def42 Add FloatingPanelControllerDelegate.floatingPanel(_:shouldRecognizeSimultaneouslyWith:) 2018-12-20 11:20:22 +09:00
Shin Yamamoto 8204a6cf27 Any gestures don't wait for the pan gesture 2018-12-18 21:43:18 +09:00
Shin Yamamoto 797292dbe5 Merge pull request #85 from SCENEE/release-1.3.0
Release v1.3.0(conflicts resolved )
2018-12-15 09:32:16 +09:00
Shin Yamamoto 6e7a33b3a1 Merge branch 'next' into release-1.3.0 2018-12-15 08:55:39 +09:00
Shin Yamamoto 2bfea00c72 Release v1.3.0 2018-12-15 08:48:00 +09:00
Shin Yamamoto 81441a724b Fix a crash in checkLayoutConsistance()
The crash happened on orientation change from landscape to portrait in
"Show Tab Bar" scene.
2018-12-12 11:47:02 +09:00
Shin Yamamoto 82c8d8dd9a Fix the boundary condition for top/bottom buffers 2018-12-12 09:59:19 +09:00
Shin Yamamoto 8751a9fe53 Fix the broken landscape layout 2018-12-12 09:59:19 +09:00
Shin Yamamoto 9cf561fcf1 Update README 2018-12-12 09:59:19 +09:00
Shin Yamamoto 332559c67d Update Samples
- Make "Show Intrinsic View" panel dismissable
- Add tap-to-hide sample
2018-12-12 09:59:19 +09:00
Shin Yamamoto 56557f0092 Merge pull request #81 from SCENEE/release-1.2.3
Release v1.2.3
2018-12-07 19:10:14 +09:00
Shin Yamamoto 1e6cb7b1ad Release v1.2.3 2018-12-07 16:50:08 +09:00
Shin Yamamoto 7f419b7e78 Merge pull request #79 from SCENEE/improve-intrinsic-height
Improve intrinsic height
2018-12-07 16:23:07 +09:00
Shin Yamamoto 973a90e071 Update README 2018-12-07 15:31:59 +09:00
Shin Yamamoto 66a8ca36e4 Fix FloatingPanelIntrinsicLayout
- `.full` position's height must be the intrinsic height.
- Work it for a safe area bottom anchor
- Remove FloatingPanelIntrinsicLayout.contentViewController because it
isn't actually needed
2018-12-07 14:37:48 +09:00
Shin Yamamoto 3715007156 Merge pull request #63 from SCENEE/feat-modality
FloatingPanelController as a Modality
2018-12-07 14:36:43 +09:00
Shin Yamamoto 5158685c02 Update README
- Show/Hide usage
- Modality usage
2018-12-07 13:25:17 +09:00
Shin Yamamoto 61d3371ea5 Fix a bug in Samples app 2018-12-06 09:11:25 +09:00
Shin Yamamoto 0491507e67 Build 'next' branch on Travis CI 2018-12-06 09:11:25 +09:00
Shin Yamamoto fcd4ad874a Use autoresizing masks instead of Auto Layout constraints 2018-12-06 09:11:25 +09:00
Shin Yamamoto e2668fcdf2 Fix the condition of removal interaction 2018-12-06 09:11:25 +09:00
Shin Yamamoto f3ac2b2cec Add the modal dismissal on backdrop tap 2018-12-06 09:11:25 +09:00
Shin Yamamoto 8b84391e36 Fix FloatingPanelModalTransitioning 2018-12-06 09:11:25 +09:00
Shin Yamamoto 1b3ca347f5 Update comment of FloatingPanelSurfaceView.grabberHandle 2018-12-06 09:11:25 +09:00
Shin Yamamoto 2dced7bfbf Improve FloatingPanelController implementation 2018-12-06 09:11:25 +09:00
Shin Yamamoto 8ba4ce36a1 Merge pull request #75 from SCENEE/fix-animation-wobbling
The default interaction animator should be uninterruptible
2018-12-05 16:01:56 +09:00
Shin Yamamoto cf60b09225 Fix invalid backdrop alpha
The bug was found when I commented out `animator.startAnimation()`.
2018-12-05 14:08:31 +09:00
Shin Yamamoto 427ec45d42 Let the default interaction animator be uninterruptible
Because an interruptible animator causes a wobbling at the animation start.
when a user flick a panel quickly to move to full position nearby the position.
2018-12-05 13:56:43 +09:00
Shin Yamamoto ac9f8fe89c Update Samples App for .hidden 2018-12-04 22:25:22 +09:00
Shin Yamamoto 6817990555 Add .hidden position 2018-12-04 22:25:22 +09:00
Shin Yamamoto d395cde316 Fix a backdrop's cut off on orientation change 2018-12-04 22:25:22 +09:00
Shin Yamamoto 20272eccb8 Add FloatingPanelTransitioning to present as Modality 2018-12-04 22:25:22 +09:00
Shin Yamamoto 091ae8abff Add 'Show Floating Panel Modal' in Samples app 2018-12-04 22:25:22 +09:00
Shin Yamamoto 6e87690649 FloatingPanelController as a Modality
* Change a floating panel view hierarchy
* Add FloatingPanelController.{show,hide}(animated:completion)
2018-12-04 22:24:26 +09:00
Shin Yamamoto d5a1bd3859 Update Samples App to use dismiss(animated:completion) 2018-12-04 22:21:50 +09:00
Shin Yamamoto a1e4643a25 Swizzling UIViewController.dismiss(animated:completion) for a content VC 2018-12-04 22:21:50 +09:00
Shin Yamamoto 71c0450614 Merge pull request #74 from SCENEE/feat-intrinsic-height
Feat intrinsic height
2018-12-04 22:20:45 +09:00
Shin Yamamoto d469caad69 Fix unexpected assertion failure 2018-12-04 08:19:41 +09:00
Derek Schade 5cc3d4fbfb Enabled inset checks again 2018-12-04 08:14:46 +09:00
Derek Schade a8691ee3a5 Update layout when layout is intrinsiclayout 2018-12-04 08:14:46 +09:00
Derek Schade 91d7941921 Add IntrinsicPanelLayout 2018-12-04 08:14:46 +09:00
Derek Schade 0bc7a0953e Intrinsic layout protocol 2018-12-04 08:14:46 +09:00
Derek Schade c60bea5952 add intrinsic viewcontroller to storyboard 2018-12-04 08:14:46 +09:00
Shin Yamamoto 4db648ad25 Merge pull request #72 from SCENEE/release-v1.2.2
Release v1.2.2
2018-12-03 12:27:47 +09:00
27 changed files with 2562 additions and 912 deletions
+27
View File
@@ -0,0 +1,27 @@
> Please fill out this template appropriately when filing a bug report.
>
> Please remove this line and everything above it before submitting.
### Short description
### Expected behavior
### Actual behavior
### Steps to reproduce
**Code example that reproduces the issue**
### Environment
**Library version**
**Installation method**
- [ ] CocoaPods
- [ ] Carthage
- [ ] Git submodules
**iOS version(s)**
**Xcode version**
+26 -11
View File
@@ -4,10 +4,10 @@ branches:
- master
cache:
directories:
- build
- vendor
- /usr/local/Homebrew
- $HOME/Library/Caches/Homebrew
before_cache:
- brew cleanup
env:
global:
- LANG=en_US.UTF-8
@@ -15,30 +15,45 @@ env:
skip_cleanup: true
jobs:
include:
- stage: carthage
- stage: Build framework(swift 4.1)
osx_image: xcode9.4
script:
- xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.1 clean build
- stage: Build framework(swift 4.2)
osx_image: xcode10
script:
- xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.2 clean build
- stage: Build framework(swift 5.0)
osx_image: xcode10.2
script:
- xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.0 clean build
- stage: Carthage
osx_image: xcode10.2
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage
script:
- carthage build --no-skip-current
- stage: podspec
osx_image: xcode10
- stage: Podspec
osx_image: xcode10.2
script:
- pod spec lint
- stage: check Maps example
osx_image: xcode10
- stage: Build maps example
osx_image: xcode10.2
script:
- xcodebuild -scheme Maps -sdk iphonesimulator clean build
- stage: check Stocks example
osx_image: xcode10
- stage: Build stocks example
osx_image: xcode10.2
script:
- xcodebuild -scheme Stocks -sdk iphonesimulator clean build
- stage: check Samples example
osx_image: xcode10
- stage: Build samples example
osx_image: xcode10.2
script:
- xcodebuild -scheme Samples -sdk iphonesimulator clean build
+2 -2
View File
@@ -312,7 +312,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Maps;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -331,7 +331,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Maps;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -165,6 +165,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 545DBA1221511E6400CA77B8 /* Build configuration list for PBXNativeTarget "Samples" */;
buildPhases = (
54D7209621D4DB970054A255 /* ShellScript */,
545DB9E621511E6300CA77B8 /* Sources */,
545DB9E721511E6300CA77B8 /* Frameworks */,
545DB9E821511E6300CA77B8 /* Resources */,
@@ -285,6 +286,26 @@
};
/* 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;
@@ -478,7 +499,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelSample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -497,7 +518,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelSample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina4_7" orientation="portrait">
<device id="retina5_9" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
@@ -14,8 +14,8 @@
<scene sceneID="Cjh-iX-VQw">
<objects>
<navigationController id="RoN-h0-uBD" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="hNW-5m-Omi">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<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"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
@@ -31,22 +31,22 @@
<objects>
<viewController id="jF4-A0-Eq6" customClass="SampleListViewController" 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="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<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="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<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="44"/>
<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.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666666666666664"/>
<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="15" y="0.0" width="345" height="43.5"/>
<rect key="frame" x="15" y="0.0" width="345" height="43.666666666666664"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -61,13 +61,19 @@
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="7IS-PU-x0P" firstAttribute="top" secondItem="Smh-Bd-AAc" secondAttribute="top" id="6yd-jv-ey3"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="leading" secondItem="TkN-Oh-wF8" secondAttribute="leading" id="Z6Y-Dc-cei"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="TkN-Oh-wF8" secondAttribute="bottom" id="fNW-DP-lhV"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="trailing" secondItem="TkN-Oh-wF8" secondAttribute="trailing" id="vfY-Rc-FOI"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="leading" secondItem="39L-Nq-qfp" secondAttribute="leading" id="Z6Y-Dc-cei"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="39L-Nq-qfp" secondAttribute="bottom" id="fNW-DP-lhV"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="trailing" secondItem="39L-Nq-qfp" secondAttribute="trailing" id="vfY-Rc-FOI"/>
</constraints>
<viewLayoutGuide key="safeArea" id="TkN-Oh-wF8"/>
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
</view>
<navigationItem key="navigationItem" title="Samples" id="wCF-su-7up"/>
<navigationItem key="navigationItem" title="Samples" id="wCF-su-7up">
<barButtonItem key="rightBarButtonItem" title="Settings" id="rbH-U3-XyA">
<connections>
<action selector="showDebugMenu:" destination="jF4-A0-Eq6" id="j02-db-ZM5"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="tableView" destination="7IS-PU-x0P" id="YFM-9W-eP4"/>
</connections>
@@ -76,22 +82,96 @@
</objects>
<point key="canvasLocation" x="57" y="27"/>
</scene>
<!--Item 2-->
<!--Settings View Controller-->
<scene sceneID="Bd0-D2-agO">
<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.33000000000001"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
<rect key="frame" x="32" y="16" width="311" height="147.33333333333334"/>
<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"/>
<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="90.666666666666686" y="33" width="130" height="20.333333333333329"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
<rect key="frame" x="23.333333333333343" y="69.333333333333329" width="264.33333333333326" height="31"/>
<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.3333333333333428" width="89.333333333333329" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<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="215.33333333333334" y="0.0" width="51.000000000000028" height="31"/>
<connections>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
</connections>
</switch>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
<rect key="frame" x="23.333333333333343" y="116.33333333333334" width="264.66666666666663" height="31"/>
<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="0.0" width="89.666666666666671" height="31"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<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="215.66666666666666" y="0.0" width="50.999999999999972" height="31"/>
<connections>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
</connections>
</switch>
</subviews>
</stackView>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="0hr-ty-yWm" firstAttribute="bottom" secondItem="n93-ZL-fmC" secondAttribute="bottom" id="2Ey-ou-E1M"/>
<constraint firstAttribute="trailing" secondItem="n93-ZL-fmC" secondAttribute="trailing" constant="32" id="DdZ-eB-F5s"/>
<constraint firstItem="n93-ZL-fmC" firstAttribute="leading" secondItem="af9-Zr-Ppc" secondAttribute="leading" constant="32" id="TyK-GP-Ari"/>
<constraint firstItem="n93-ZL-fmC" firstAttribute="top" secondItem="af9-Zr-Ppc" secondAttribute="topMargin" constant="16" id="mbC-6H-z9M"/>
</constraints>
<viewLayoutGuide key="safeArea" id="0hr-ty-yWm"/>
</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="versionLabel" destination="WmC-Tq-NDN" id="Woh-kK-U0m"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="M9h-4V-3M0" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="708" y="-200"/>
</scene>
<!--Layout 2-->
<scene sceneID="lRc-OZ-sL4">
<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="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Item 2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AiP-dx-mFn">
<rect key="frame" x="163.5" y="323" width="48" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
<rect key="frame" x="20" y="20" width="39" height="30"/>
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="RpE-lI-27a" eventType="touchUpInside" id="hj3-Xv-6Gq"/>
@@ -101,35 +181,59 @@
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="IvG-yp-yzI" firstAttribute="top" secondItem="2Cd-km-qEk" secondAttribute="top" id="18k-sV-PgT"/>
<constraint firstItem="AiP-dx-mFn" firstAttribute="centerY" secondItem="JER-jz-KSq" secondAttribute="centerY" id="NUc-tM-0dN"/>
<constraint firstItem="AiP-dx-mFn" firstAttribute="centerX" secondItem="JER-jz-KSq" secondAttribute="centerX" id="hwP-mu-Vmz"/>
<constraint firstItem="IvG-yp-yzI" firstAttribute="leading" secondItem="2Cd-km-qEk" secondAttribute="leading" constant="20" id="pYt-jE-CTF"/>
<constraint firstItem="IvG-yp-yzI" firstAttribute="top" secondItem="954-Dk-zvc" secondAttribute="top" id="18k-sV-PgT"/>
<constraint firstItem="954-Dk-zvc" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="IvG-yp-yzI" secondAttribute="trailing" id="mpr-u5-MZu"/>
<constraint firstItem="IvG-yp-yzI" firstAttribute="leading" secondItem="954-Dk-zvc" secondAttribute="leading" constant="20" id="pYt-jE-CTF"/>
</constraints>
<viewLayoutGuide key="safeArea" id="2Cd-km-qEk"/>
<viewLayoutGuide key="safeArea" id="954-Dk-zvc"/>
</view>
<tabBarItem key="tabBarItem" tag="1" title="Item 2" id="qb3-RB-B28"/>
<tabBarItem key="tabBarItem" tag="1" title="Layout 2" id="qb3-RB-B28"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="NhZ-u5-Beh" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="326" y="1575"/>
<point key="canvasLocation" x="-308" y="1546"/>
</scene>
<!--Item 1-->
<!--Layout 3-->
<scene sceneID="r9h-Ql-gIv">
<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"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="pOk-Zm-vD9" eventType="touchUpInside" id="111-PD-Pop"/>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="1Rg-YG-TtU"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="0ao-SI-QZW" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="NbG-e8-HdI" secondAttribute="trailing" id="K9F-6x-KWn"/>
<constraint firstItem="NbG-e8-HdI" firstAttribute="top" secondItem="0ao-SI-QZW" secondAttribute="top" id="nsE-so-rTl"/>
<constraint firstItem="NbG-e8-HdI" firstAttribute="leading" secondItem="0ao-SI-QZW" secondAttribute="leading" constant="20" id="sF4-Dm-aoY"/>
</constraints>
<viewLayoutGuide key="safeArea" id="0ao-SI-QZW"/>
</view>
<tabBarItem key="tabBarItem" tag="2" title="Layout 3" id="RJD-TF-Sdh"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Oe3-FT-q1C" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="332" y="1546"/>
</scene>
<!--Layout 1-->
<scene sceneID="m6X-j6-yBM">
<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="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Item 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uoW-c8-9wx">
<rect key="frame" x="164.5" y="323" width="46" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
<rect key="frame" x="20" y="20" width="39" height="30"/>
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="YL4-GP-ZEZ"/>
@@ -139,18 +243,47 @@
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="leading" secondItem="f88-U8-Vja" secondAttribute="leading" constant="20" id="5BT-yZ-EKe"/>
<constraint firstItem="uoW-c8-9wx" firstAttribute="centerY" secondItem="ji9-Ez-N7i" secondAttribute="centerY" id="Nyw-Wt-78z"/>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="top" secondItem="f88-U8-Vja" secondAttribute="top" id="hUV-3a-XkY"/>
<constraint firstItem="uoW-c8-9wx" firstAttribute="centerX" secondItem="ji9-Ez-N7i" secondAttribute="centerX" id="wDv-OH-7PX"/>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="leading" secondItem="5Ns-4l-Ufg" secondAttribute="leading" constant="20" id="5BT-yZ-EKe"/>
<constraint firstItem="5Ns-4l-Ufg" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="eFN-tN-4Ct" secondAttribute="trailing" id="OzZ-Dz-RNF"/>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="top" secondItem="5Ns-4l-Ufg" secondAttribute="top" id="hUV-3a-XkY"/>
</constraints>
<viewLayoutGuide key="safeArea" id="f88-U8-Vja"/>
<viewLayoutGuide key="safeArea" id="5Ns-4l-Ufg"/>
</view>
<tabBarItem key="tabBarItem" title="Item 1" id="HEV-kf-jxH"/>
<tabBarItem key="tabBarItem" title="Layout 1" id="HEV-kf-jxH"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bkL-bc-hZC" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-380" y="1576"/>
<point key="canvasLocation" x="-962" y="1546"/>
</scene>
<!--Intrinsic View Controller-->
<scene sceneID="wtJ-qZ-aCl">
<objects>
<viewController storyboardIdentifier="IntrinsicViewController" title="Intrinsic View Controller" id="aK0-kv-mTu" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eLM-xc-d9e">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<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="24" y="24" width="327" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="ge4-RW-Gmz" secondAttribute="trailing" constant="24" id="V59-MD-Lcg"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="leading" secondItem="eLM-xc-d9e" secondAttribute="leading" constant="24" id="hAO-P0-7Kw"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="top" secondItem="eLM-xc-d9e" secondAttribute="top" constant="24" id="j0s-fd-MYj"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="ge4-RW-Gmz" secondAttribute="bottom" constant="24" id="tEn-PO-nVD"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ouu-g9-OiX"/>
</view>
<size key="freeformSize" width="375" height="778"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="DfE-fL-zy5" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2753" y="734"/>
</scene>
<!--Tab Bar View Controller-->
<scene sceneID="nQ5-PV-qFw">
@@ -164,33 +297,34 @@
<connections>
<segue destination="lto-Zc-Vtp" kind="relationship" relationship="viewControllers" id="6hP-AH-YiH"/>
<segue destination="RpE-lI-27a" kind="relationship" relationship="viewControllers" id="g6X-Sq-uSW"/>
<segue destination="pOk-Zm-vD9" kind="relationship" relationship="viewControllers" id="OPp-iO-iDK"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Z9x-EI-p2b" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-183" y="806"/>
<point key="canvasLocation" x="-706" y="749"/>
</scene>
<!--Modal View Controller-->
<scene sceneID="C9P-Ns-Qrq">
<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="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<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="667" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="744" width="375" height="34"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<rect key="frame" x="20" y="20" width="39" height="30"/>
<rect key="frame" x="20" y="44" 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="139.5" y="108" width="96" height="252"/>
<rect key="frame" x="139.66666666666666" y="132" width="96" height="252"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
@@ -225,35 +359,36 @@
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="sbF-Az-7sy" firstAttribute="top" secondItem="GBa-yx-8to" secondAttribute="top" id="3VR-hj-zeQ"/>
<constraint firstItem="9p4-06-y2T" firstAttribute="top" secondItem="GBa-yx-8to" secondAttribute="top" constant="88" id="41n-Fn-hi3"/>
<constraint firstItem="sbF-Az-7sy" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="top" id="3VR-hj-zeQ"/>
<constraint firstItem="9p4-06-y2T" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="top" constant="88" id="41n-Fn-hi3"/>
<constraint firstAttribute="bottom" secondItem="vut-mK-Y4t" secondAttribute="bottom" id="6eq-Kt-heZ"/>
<constraint firstItem="sbF-Az-7sy" firstAttribute="leading" secondItem="GBa-yx-8to" secondAttribute="leading" constant="20" id="T2G-1L-PRs"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="leading" secondItem="qwo-GK-p1U" secondAttribute="leading" id="gVC-jv-VJX"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="trailing" secondItem="GBa-yx-8to" secondAttribute="trailing" id="jkq-p2-lUm"/>
<constraint firstItem="9p4-06-y2T" firstAttribute="centerX" secondItem="qwo-GK-p1U" secondAttribute="centerX" id="l8t-p3-ETf"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="top" secondItem="GBa-yx-8to" secondAttribute="bottom" id="rMy-JT-t4B"/>
<constraint firstItem="sbF-Az-7sy" firstAttribute="leading" secondItem="kjr-TP-fcM" secondAttribute="leading" constant="20" id="T2G-1L-PRs"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="leading" secondItem="kjr-TP-fcM" secondAttribute="leading" id="gVC-jv-VJX"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="trailing" secondItem="kjr-TP-fcM" secondAttribute="trailing" id="jkq-p2-lUm"/>
<constraint firstItem="9p4-06-y2T" firstAttribute="centerX" secondItem="kjr-TP-fcM" secondAttribute="centerX" id="l8t-p3-ETf"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="bottom" id="rMy-JT-t4B"/>
</constraints>
<viewLayoutGuide key="safeArea" id="GBa-yx-8to"/>
<viewLayoutGuide key="safeArea" id="kjr-TP-fcM"/>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="safeAreaView" destination="vut-mK-Y4t" id="r9P-XF-wLd"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="fbi-LZ-M4Y" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="561" y="806"/>
<point key="canvasLocation" x="1375" y="734"/>
</scene>
<!--Nested Scroll View Controller-->
<scene sceneID="TfC-A3-4R0">
<objects>
<viewController storyboardIdentifier="NestedScrollViewController" id="LAe-jm-k6f" customClass="NestedScrollViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="414-Wy-0t1">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sBe-tN-uMi">
<rect key="frame" x="0.0" y="32" width="375" height="635"/>
<rect key="frame" x="0.0" y="32" width="375" height="746"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lFR-Sp-Sj1">
<rect key="frame" x="0.0" y="0.0" width="375" height="968"/>
@@ -329,20 +464,21 @@
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="sBe-tN-uMi" firstAttribute="leading" secondItem="414-Wy-0t1" secondAttribute="leading" id="8Qd-my-knA"/>
<constraint firstItem="sBe-tN-uMi" firstAttribute="leading" secondItem="ufS-Rf-F2F" secondAttribute="leading" id="8Qd-my-knA"/>
<constraint firstItem="sBe-tN-uMi" firstAttribute="top" secondItem="414-Wy-0t1" secondAttribute="top" constant="32" id="9Js-LU-lNr"/>
<constraint firstAttribute="bottom" secondItem="sBe-tN-uMi" secondAttribute="bottom" id="jzB-47-P7e"/>
<constraint firstAttribute="trailing" secondItem="sBe-tN-uMi" secondAttribute="trailing" id="nHG-wg-pLP"/>
<constraint firstItem="ufS-Rf-F2F" firstAttribute="trailing" secondItem="sBe-tN-uMi" secondAttribute="trailing" id="nHG-wg-pLP"/>
</constraints>
<viewLayoutGuide key="safeArea" id="sL5-d5-za2"/>
<viewLayoutGuide key="safeArea" id="ufS-Rf-F2F"/>
<connections>
<outletCollection property="gestureRecognizers" destination="tOa-bf-zGz" appends="YES" id="zle-Sz-M3U"/>
<outletCollection property="gestureRecognizers" destination="SCk-hG-weZ" appends="YES" id="OcK-FK-Lac"/>
<outletCollection property="gestureRecognizers" destination="Fvp-Z6-eVc" appends="YES" id="Fds-J5-YCg"/>
</connections>
</view>
<size key="freeformSize" width="375" height="667"/>
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="nestedScrollView" destination="xba-kG-VQ2" id="ddV-kf-37A"/>
<outlet property="scrollView" destination="sBe-tN-uMi" id="h4S-Zl-cLO"/>
</connections>
</viewController>
@@ -363,7 +499,7 @@
</connections>
</swipeGestureRecognizer>
</objects>
<point key="canvasLocation" x="1311" y="806"/>
<point key="canvasLocation" x="2097" y="734"/>
</scene>
<!--Detail View Controller-->
<scene sceneID="b6k-zi-3wn">
@@ -373,8 +509,19 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8yw-OC-Ubk">
<rect key="frame" x="0.0" y="690" width="375" height="88"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="88" id="jwV-YU-tXG"/>
</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="700"/>
<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="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="319" y="12" width="44" height="44"/>
<rect key="frame" x="319" y="44" width="44" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="0jg-5D-A1F"/>
<constraint firstAttribute="width" constant="44" id="1Cq-PA-wgW"/>
@@ -383,22 +530,8 @@
<action selector="closeWithSender:" destination="YC8-ae-15L" eventType="touchUpInside" id="Z2v-19-S5k"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8yw-OC-Ubk">
<rect key="frame" x="0.0" y="690" width="375" height="88"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="88" id="jwV-YU-tXG"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="734" width="375" height="44"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="DQJ-cY-cKx"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
<rect key="frame" x="130.5" y="108" width="114" height="82"/>
<rect key="frame" x="130.66666666666666" y="132" width="114" height="134"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
<rect key="frame" x="0.0" y="0.0" width="114" height="30"/>
@@ -415,24 +548,33 @@
<segue destination="bYI-y3-Rzb" kind="presentation" identifier="PresentModallySegue" id="3yq-HE-Tgn"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="01L-lp-oy6">
<rect key="frame" x="0.0" y="104" width="114" height="30"/>
<state key="normal" title="Update Layout"/>
<connections>
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="zTb-sq-B6f"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<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="g7l-kO-y7q" secondAttribute="top" constant="12" id="EQy-cr-F2Y"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="centerX" secondItem="g7l-kO-y7q" secondAttribute="centerX" id="EsD-Vf-dNZ"/>
<constraint firstItem="noi-1a-5bZ" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" 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="leading" secondItem="tAi-nk-rDB" secondAttribute="leading" id="RiJ-Hb-OOZ"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="trailing" secondItem="tAi-nk-rDB" secondAttribute="trailing" id="Sof-yL-mwK"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="tAi-nk-rDB" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="trailing" secondItem="tAi-nk-rDB" secondAttribute="trailing" id="kkp-Yo-FQW"/>
<constraint firstItem="tAi-nk-rDB" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="tAi-nk-rDB" secondAttribute="leading" id="oVC-i1-TwS"/>
<constraint firstItem="tAi-nk-rDB" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" id="rW2-mF-5DR"/>
<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="aOK-7l-cA6" secondAttribute="top" 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="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"/>
</constraints>
<viewLayoutGuide key="safeArea" id="tAi-nk-rDB"/>
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
<connections>
<outletCollection property="gestureRecognizers" destination="6Ca-p8-7uF" appends="YES" id="xOy-f1-NZE"/>
<outletCollection property="gestureRecognizers" destination="SPY-Vr-XDT" appends="YES" id="vgS-Am-jhQ"/>
@@ -462,7 +604,7 @@
</connections>
</pongPressGestureRecognizer>
</objects>
<point key="canvasLocation" x="1440.8" y="-23.388305847076463"/>
<point key="canvasLocation" x="655" y="734"/>
</scene>
<!--Debug Text View Controller-->
<scene sceneID="Bkq-O7-q4A">
@@ -520,24 +662,26 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="rN1-HL-YHv" firstAttribute="leading" secondItem="ix0-2W-gQN" secondAttribute="leading" id="7V3-KL-vXd"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="leading" secondItem="5ET-zC-lCb" secondAttribute="leading" id="7V3-KL-vXd"/>
<constraint firstAttribute="bottom" secondItem="rN1-HL-YHv" secondAttribute="bottom" id="efD-U5-Tet"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="top" secondItem="9YG-0j-Zzg" secondAttribute="top" constant="17" id="fiO-LL-nSC"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="trailing" secondItem="ix0-2W-gQN" secondAttribute="trailing" id="lfg-EE-euw"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="trailing" secondItem="5ET-zC-lCb" secondAttribute="trailing" id="lfg-EE-euw"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ix0-2W-gQN"/>
<viewLayoutGuide key="safeArea" id="5ET-zC-lCb"/>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="textView" destination="rN1-HL-YHv" id="gmr-Uf-jd8"/>
<outlet property="textViewTopConstraint" destination="fiO-LL-nSC" id="Rum-TN-c2e"/>
<outlet property="view" destination="9YG-0j-Zzg" id="jhb-eT-nEn"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x1h-y1-h8q" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="729" y="-23"/>
<point key="canvasLocation" x="-1" y="734"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="3yq-HE-Tgn"/>
<segue reference="r1P-2i-NDe"/>
</inferredMetricsTieBreakers>
</document>
+32 -14
View File
@@ -5,21 +5,39 @@
import UIKit
extension UIView {
var layoutInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return safeAreaInsets
} else {
return layoutMargins
}
}
protocol LayoutGuideProvider {
var topAnchor: NSLayoutYAxisAnchor { get }
var bottomAnchor: NSLayoutYAxisAnchor { get }
}
extension UILayoutGuide: LayoutGuideProvider {}
var layoutGuide: UILayoutGuide {
if #available(iOS 11.0, *) {
return safeAreaLayoutGuide
} else {
return layoutMarginsGuide
}
class CustomLayoutGuide: LayoutGuideProvider {
let topAnchor: NSLayoutYAxisAnchor
let bottomAnchor: NSLayoutYAxisAnchor
init(topAnchor: NSLayoutYAxisAnchor, bottomAnchor: NSLayoutYAxisAnchor) {
self.topAnchor = topAnchor
self.bottomAnchor = bottomAnchor
}
}
extension UIViewController {
var layoutInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
return UIEdgeInsets(top: topLayoutGuide.length,
left: 0.0,
bottom: bottomLayoutGuide.length,
right: 0.0)
}
}
var layoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
return view!.safeAreaLayoutGuide
} else {
return CustomLayoutGuide(topAnchor: topLayoutGuide.bottomAnchor,
bottomAnchor: bottomLayoutGuide.topAnchor)
}
}
}
+541 -97
View File
@@ -9,6 +9,11 @@
import UIKit
import FloatingPanel
/**
- Attention: `FloatingPanelLayout` must not be applied by the parent view
controller of a floating panel. But here `SampleListViewController` adopts it
purposely to check if the library prints an appropriate warning.
*/
class SampleListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, FloatingPanelControllerDelegate, FloatingPanelLayout {
@IBOutlet weak var tableView: UITableView!
@@ -17,9 +22,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case trackingTextView
case showDetail
case showModal
case showFloatingPanelModal
case showTabBar
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
var name: String {
switch self {
@@ -27,9 +34,11 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showFloatingPanelModal: return "Show Floating Panel Modal"
case .showTabBar: return "Show Tab Bar"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
}
}
@@ -39,16 +48,23 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showFloatingPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
}
}
}
var currentMenu: Menu = .trackingTableView
var mainPanelVC: FloatingPanelController!
var detailPanelVC: FloatingPanelController!
var currentMenu: Menu = .trackingTableView
var settingsPanelVC: FloatingPanelController!
var mainPanelObserves: [NSKeyValueObservation] = []
var settingsObserves: [NSKeyValueObservation] = []
override func viewDidLoad() {
super.viewDidLoad()
@@ -56,19 +72,42 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
let searchController = UISearchController(searchResultsController: nil)
if #available(iOS 11.0, *) {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.largeTitleDisplayMode = .automatic
} else {
// Fallback on earlier versions
}
let contentVC = DebugTableViewController()
addMainPanel(with: contentVC)
}
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()
}) {
settingsObserves.append(observation)
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
settingsObserves.removeAll()
}
func addMainPanel(with contentVC: UIViewController) {
mainPanelObserves.removeAll()
// Initialize FloatingPanelController
mainPanelVC = FloatingPanelController()
mainPanelVC.delegate = self
mainPanelVC.isRemovalInteractionEnabled = (currentMenu == .showRemovablePanel)
// Initialize FloatingPanelController and add the view
mainPanelVC.surfaceView.cornerRadius = 6.0
@@ -77,18 +116,34 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
// Set a content view controller
mainPanelVC.set(contentViewController: contentVC)
// Enable tap-to-hide and removal interaction
switch currentMenu {
case .showRemovablePanel, .showIntrinsicView:
mainPanelVC.isRemovalInteractionEnabled = true
let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
mainPanelVC.backdropView.addGestureRecognizer(backdropTapGesture)
default:
break
}
// Track a scroll view
switch contentVC {
case let consoleVC as DebugTextViewController:
mainPanelVC.track(scrollView: consoleVC.textView)
case let contentVC as DebugTableViewController:
let ob = contentVC.tableView.observe(\.isEditing) { (tableView, _) in
self.mainPanelVC.panGestureRecognizer.isEnabled = !tableView.isEditing
}
mainPanelObserves.append(ob)
mainPanelVC.track(scrollView: contentVC.tableView)
case let contentVC as NestedScrollViewController:
mainPanelVC.track(scrollView: contentVC.scrollView)
default:
break
}
// Add FloatingPanel to self.view
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
@@ -97,22 +152,72 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
detailPanelVC.removePanelFromParent(animated: true, completion: nil)
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
switch tapGesture.view {
case mainPanelVC.backdropView:
mainPanelVC.hide(animated: true, completion: nil)
case settingsPanelVC.backdropView:
settingsPanelVC.removePanelFromParent(animated: true)
settingsPanelVC = nil
default:
break
}
}
// MARK:- TableViewDatasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Menu.allCases.count
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return Menu.allCases.count + 30
} else {
return Menu.allCases.count
}
} else {
return Menu.allCases.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let menu = Menu.allCases[indexPath.row]
cell.textLabel?.text = menu.name
if Menu.allCases.count > indexPath.row {
let menu = Menu.allCases[indexPath.row]
cell.textLabel?.text = menu.name
} else {
cell.textLabel?.text = "\(indexPath.row) row"
}
return cell
}
// MARK:- Actions
@IBAction func showDebugMenu(_ sender: UIBarButtonItem) {
guard settingsPanelVC == nil else { return }
// Initialize FloatingPanelController
settingsPanelVC = FloatingPanelController()
// Initialize FloatingPanelController and add the view
settingsPanelVC.surfaceView.cornerRadius = 6.0
settingsPanelVC.surfaceView.shadowHidden = false
settingsPanelVC.isRemovalInteractionEnabled = true
let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
settingsPanelVC.backdropView.addGestureRecognizer(backdropTapGesture)
settingsPanelVC.delegate = self
let contentVC = storyboard?.instantiateViewController(withIdentifier: "SettingsViewController")
// Set a content view controller
settingsPanelVC.set(contentViewController: contentVC)
// Add FloatingPanel to self.view
settingsPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
// MARK:- TableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard Menu.allCases.count > indexPath.row else { return }
let menu = Menu.allCases[indexPath.row]
let contentVC: UIViewController = {
guard let storyboardID = menu.storyboardID else { return DebugTableViewController() }
@@ -124,7 +229,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
switch menu {
case .showDetail:
detailPanelVC?.removeFromParent()
detailPanelVC?.removePanelFromParent(animated: false)
// Initialize FloatingPanelController
detailPanelVC = FloatingPanelController()
@@ -141,6 +246,18 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .showModal, .showTabBar:
let modalVC = contentVC
present(modalVC, animated: true, completion: nil)
case .showFloatingPanelModal:
let fpc = FloatingPanelController()
let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
fpc.set(contentViewController: contentVC)
fpc.delegate = self
fpc.surfaceView.cornerRadius = 38.5
fpc.surfaceView.shadowHidden = false
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
default:
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
mainPanelVC?.removePanelFromParent(animated: true) {
@@ -150,10 +267,40 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
if currentMenu == .showRemovablePanel {
if vc == settingsPanelVC {
return IntrinsicPanelLayout()
}
switch currentMenu {
case .showRemovablePanel:
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
} else {
return self
case .showIntrinsicView:
return IntrinsicPanelLayout()
case .showFloatingPanelModal:
if vc != mainPanelVC && vc != detailPanelVC {
return ModalPanelLayout()
}
fallthrough
default:
return (newCollection.verticalSizeClass == .compact) ? nil : self
}
}
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool {
switch currentMenu {
case .showNestedScrollView:
return (vc.contentViewController as? NestedScrollViewController)?.nestedScrollView.gestureRecognizers?.contains(gestureRecognizer) ?? false
default:
return false
}
}
func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {
switch vc {
case settingsPanelVC:
settingsPanelVC = nil
default:
break
}
}
@@ -166,16 +313,22 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .full: return UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0
case .half: return 262.0
case .tip: return 69.0
case .hidden: return nil
}
}
}
class RemovablePanelLayout: FloatingPanelLayout {
class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { }
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
var topInteractionBuffer: CGFloat {
return 200.0
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
@@ -183,7 +336,24 @@ class RemovablePanelLayout: FloatingPanelLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .half: return 130.0
default: return nil
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
}
}
class RemovablePanelLandscapeLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 261.0
default: return nil
}
@@ -193,22 +363,9 @@ class RemovablePanelLayout: FloatingPanelLayout {
}
}
class RemovablePanelLandscapeLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.half]
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 261.0
default: return nil
}
class ModalPanelLayout: FloatingPanelIntrinsicLayout {
var topInteractionBuffer: CGFloat {
return 100.0
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
@@ -217,6 +374,7 @@ class RemovablePanelLandscapeLayout: FloatingPanelLayout {
class NestedScrollViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var nestedScrollView: UIScrollView!
@IBAction func longPressed(_ sender: Any) {
print("LongPressed!")
@@ -231,16 +389,32 @@ class NestedScrollViewController: UIViewController {
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, *) {
@@ -249,16 +423,68 @@ class DebugTextViewController: UIViewController, UITextViewDelegate {
}
@IBAction func close(sender: UIButton) {
// Now impossible
// dismiss(animated: true, completion: nil)
(self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}
}
class DebugTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
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)
}
}
class DebugTableViewController: InspectableViewController, UITableViewDataSource, UITableViewDelegate {
weak var tableView: UITableView!
var items: [String] = []
var itemHeight: CGFloat = 66.0
enum Menu: String, CaseIterable {
case animateScroll = "Animate Scroll"
case changeContentSize = "Change content size"
case reorder = "Reorder"
}
var reorderButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
@@ -288,17 +514,21 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable
stackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0),
])
let button = UIButton()
button.setTitle("Animate Scroll", for: .normal)
button.setTitleColor(view.tintColor, for: .normal)
button.addTarget(self, action: #selector(animateScroll), for: .touchUpInside)
stackView.addArrangedSubview(button)
let button2 = UIButton()
button2.setTitle("Change content size", for: .normal)
button2.setTitleColor(view.tintColor, for: .normal)
button2.addTarget(self, action: #selector(changeContentSize), for: .touchUpInside)
stackView.addArrangedSubview(button2)
for menu in Menu.allCases {
let button = UIButton()
button.setTitle(menu.rawValue, for: .normal)
button.setTitleColor(view.tintColor, for: .normal)
switch menu {
case .animateScroll:
button.addTarget(self, action: #selector(animateScroll), for: .touchUpInside)
case .changeContentSize:
button.addTarget(self, action: #selector(changeContentSize), for: .touchUpInside)
case .reorder:
button.addTarget(self, action: #selector(reorderItems), for: .touchUpInside)
reorderButton = button
}
stackView.addArrangedSubview(button)
}
for i in 0...100 {
items.append("Items \(i)")
@@ -339,6 +569,16 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable
self.present(actionSheet, animated: true, completion: nil)
}
@objc 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)
}
}
func changeItems(_ count: Int) {
items.removeAll()
for i in 0..<count {
@@ -352,50 +592,6 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
//print("Content View: viewWillLayoutSubviews")
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//print("Content View: viewDidLayoutSubviews")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("Content View: viewWillAppear")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("Content View: viewDidAppear", view.bounds)
}
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))")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
@@ -418,14 +614,21 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable
}),
]
}
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)
}
}
class DetailViewController: UIViewController {
class DetailViewController: InspectableViewController {
@IBOutlet weak var closeButton: UIButton!
@IBAction func close(sender: UIButton) {
// Now impossible
// dismiss(animated: true, completion: nil)
(self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}
@IBAction func buttonPressed(_ sender: UIButton) {
@@ -521,16 +724,31 @@ class ModalSecondLayout: FloatingPanelLayout {
case .full: return 18.0
case .half: return 262.0
case .tip: return 44.0
case .hidden: return nil
}
}
}
class TabBarViewController: UITabBarController {}
class TabBarContentViewController: UIViewController, FloatingPanelControllerDelegate {
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)"
}
}
}
var fpc: FloatingPanelController!
var consoleVC: DebugTextViewController!
var threeLayout: ThreeTabBarPanelLayout!
var tab3Mode: Tab3Mode = .changeAutoLayout
var switcherLabel: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Initialize FloatingPanelController
@@ -544,11 +762,47 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele
// 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 tabBarItem.tag == 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
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fpc.updateLayout()
}
override func viewWillDisappear(_ animated: Bool) {
@@ -557,19 +811,134 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele
fpc.removePanelFromParent(animated: false)
}
// MARK: - Action
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
}
// MAKR: - 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 implicity reset the offset(i.e.
// Using KVO of `scrollView.contentOffset`). Because it can lead to an
// infinit loop if a user also resets a content offset as below and,
// in the situation, a user has to modify the library.
if fpc.position != .full, fpc.surfaceView.frame.minY < fpc.originYOfSurface(for: .full) {
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 TwoTabBarPanel2Layout()
return TwoTabBarPanelLayout()
case 2:
threeLayout = ThreeTabBarPanelLayout(parent: self)
return threeLayout
default:
return nil
}
}
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
func floatingPanelDidMove(_ vc: FloatingPanelController) {
guard self.tabBarItem.tag == 2 else { return }
switch tab3Mode {
case .changeAutoLayout:
/* Good solution: Manipulate top constraint */
assert(consoleVC.textViewTopConstraint != nil)
if vc.surfaceView.frame.minY + threeLayout.topPadding < vc.layoutInsets.top {
consoleVC.textViewTopConstraint?.constant = vc.layoutInsets.top - vc.surfaceView.frame.minY
} else {
consoleVC.textViewTopConstraint?.constant = threeLayout.topPadding
}
case .changeOffset:
/*
Bad solution: Manipulate scoll 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.surfaceView.frame.minY > vc.originYOfSurface(for: .half) {
let progress = (vc.surfaceView.frame.minY - vc.originYOfSurface(for: .half)) / (vc.originYOfSurface(for: .tip) - vc.originYOfSurface(for: .half))
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
}
vc.view.layoutIfNeeded() // MUST
}
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
guard self.tabBarItem.tag == 2 else { return }
switch tab3Mode {
case .changeAutoLayout:
/* Good Solution: Manipulate top constraint */
assert(consoleVC.textViewTopConstraint != nil)
consoleVC.textViewTopConstraint?.constant = (vc.position == .full) ? vc.layoutInsets.top : 17.0
case .changeOffset:
/* Bad Solution: Manipulate scoll content inset */
guard let scrollView = consoleVC.textView else { return }
var insets = vc.adjustedContentInsets
insets.top = (vc.position == .full) ? vc.layoutInsets.top : 0.0
scrollView.contentInset = insets
if scrollView.contentOffset.y - scrollView.contentInset.top < 0.0 {
scrollView.contentOffset = CGPoint(x: 0.0,
y: 0.0 - scrollView.contentInset.top)
}
}
if vc.position == .tip {
threeLayout.leftConstraint.constant = threeLayout.sideMargin
threeLayout.rightConstraint.constant = -threeLayout.sideMargin
} else {
threeLayout.leftConstraint.constant = 0.0
threeLayout.rightConstraint.constant = 0.0
}
// Can call it, but it's not necessary because it will be also called
// by FloatingPanelController after the delegate method
vc.view.layoutIfNeeded()
}
}
@@ -606,7 +975,7 @@ class OneTabBarPanelLayout: FloatingPanelLayout {
}
}
class TwoTabBarPanel2Layout: FloatingPanelLayout {
class TwoTabBarPanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
@@ -625,3 +994,78 @@ class TwoTabBarPanel2Layout: FloatingPanelLayout {
}
}
}
class ThreeTabBarPanelLayout: FloatingPanelFullScreenLayout {
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 bottomInteractionBuffer: CGFloat = 44.0
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half, .tip]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 0.0
case .half: return 261.0 + parentVC.layoutInsets.bottom
case .tip: return 88.0 + parentVC.layoutInsets.bottom
default: return nil
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> 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 ]
}
}
class SettingsViewController: InspectableViewController {
@IBOutlet weak var largeTitlesSwicth: UISwitch!
@IBOutlet weak var translucentSwicth: 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
largeTitlesSwicth.setOn(prefersLargeTitles, animated: false)
} else {
largeTitlesSwicth.isEnabled = false
}
let isTranslucent = navigationController!.navigationBar.isTranslucent
translucentSwicth.setOn(isTranslucent, animated: false)
}
@IBAction func toggleLargeTitle(_ sender: UISwitch) {
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
}
}
@IBAction func toggleTranslucent(_ sender: UISwitch) {
navigationController?.navigationBar.isTranslucent = sender.isOn
}
}
@@ -312,7 +312,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Stocks;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -331,7 +331,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.Stocks;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -114,6 +114,7 @@ class FloatingPanelStocksLayout: FloatingPanelLayout {
case .full: return 56.0
case .half: return 262.0
case .tip: return 85.0 + 44.0 // Visible + ToolView
default: return nil
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.2.2"
s.version = "1.5.0"
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.
@@ -13,7 +13,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 => "v#{s.version}" }
s.source_files = "Framework/Sources/*.swift"
s.swift_version = "4.2"
s.swift_version = "4.0"
s.pod_target_xcconfig = { 'SWIFT_WHOLE_MODULE_OPTIMIZATION' => 'YES', 'APPLICATION_EXTENSION_API_ONLY' => 'YES' }
s.framework = "UIKit"
+131 -10
View File
@@ -7,10 +7,12 @@
objects = {
/* Begin PBXBuildFile section */
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */; };
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; };
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; };
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ViewTests.swift */; };
545DB9D22151169500CA77B8 /* FloatingPanelController.h in Headers */ = {isa = PBXBuildFile; fileRef = 545DB9C42151169500CA77B8 /* FloatingPanelController.h */; settings = {ATTRIBUTES = (Public, ); }; };
545DB9D02151169500CA77B8 /* FloatingPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* FloatingPanelTests.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 /* FloatingPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */; };
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */; };
@@ -32,12 +34,14 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTransitioning.swift; sourceTree = "<group>"; };
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = "<group>"; };
545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9C42151169500CA77B8 /* FloatingPanelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanelController.h; sourceTree = "<group>"; };
545DB9C42151169500CA77B8 /* FloatingPanel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanel.h; sourceTree = "<group>"; };
545DB9C52151169500CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FloatingPanelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9CF2151169500CA77B8 /* ViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTests.swift; sourceTree = "<group>"; };
545DB9CF2151169500CA77B8 /* FloatingPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTests.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 /* FloatingPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelController.swift; sourceTree = "<group>"; };
@@ -90,11 +94,13 @@
isa = PBXGroup;
children = (
545DB9C52151169500CA77B8 /* Info.plist */,
545DB9C42151169500CA77B8 /* FloatingPanelController.h */,
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */,
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */,
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */,
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */,
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */,
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */,
@@ -107,7 +113,7 @@
545DB9CE2151169500CA77B8 /* Tests */ = {
isa = PBXGroup;
children = (
545DB9CF2151169500CA77B8 /* ViewTests.swift */,
545DB9CF2151169500CA77B8 /* FloatingPanelTests.swift */,
545DB9D12151169500CA77B8 /* Info.plist */,
);
path = Tests;
@@ -120,7 +126,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
545DB9D22151169500CA77B8 /* FloatingPanelController.h in Headers */,
545DB9D22151169500CA77B8 /* FloatingPanel.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -225,11 +231,13 @@
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */,
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */,
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */,
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */,
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -238,7 +246,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */,
545DB9D02151169500CA77B8 /* FloatingPanelTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -398,7 +406,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -424,7 +432,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -465,6 +473,116 @@
};
name = Release;
};
54E79ADF224F6C9800717BC6 /* Test */ = {
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_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;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
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 = 12.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";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Test;
};
54E79AE0224F6C9800717BC6 /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Sources/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG __FP_LOG";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Test;
};
54E79AE1224F6C9800717BC6 /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = Tests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingModalControllerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Test;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -472,6 +590,7 @@
isa = XCConfigurationList;
buildConfigurations = (
545DB9D32151169500CA77B8 /* Debug */,
54E79ADF224F6C9800717BC6 /* Test */,
545DB9D42151169500CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -481,6 +600,7 @@
isa = XCConfigurationList;
buildConfigurations = (
545DB9D62151169500CA77B8 /* Debug */,
54E79AE0224F6C9800717BC6 /* Test */,
545DB9D72151169500CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -490,6 +610,7 @@
isa = XCConfigurationList;
buildConfigurations = (
545DB9D92151169500CA77B8 /* Debug */,
54E79AE1224F6C9800717BC6 /* Test */,
545DB9DA2151169500CA77B8 /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -23,12 +23,31 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9C92151169500CA77B8"
BuildableName = "FloatingPanelTests.xctest"
BlueprintName = "FloatingPanelTests"
ReferencedContainer = "container:FloatingPanel.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9C02151169500CA77B8"
BuildableName = "FloatingPanel.framework"
BlueprintName = "FloatingPanel"
ReferencedContainer = "container:FloatingPanel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
File diff suppressed because it is too large Load Diff
+44 -4
View File
@@ -6,7 +6,18 @@
import UIKit
public protocol FloatingPanelBehavior {
/// Returns the progress to redirect to the previous position
/// Asks the behavior object if the floating panel should project a momentum of a user interaction to move the proposed position.
///
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
/// Therfore, `proposedTargetPosition` can only be `FloatingPanelPosition.tip` or `FloatingPanelPosition.full`.
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool
/// Returns a deceleration rate to calculate a target position projected a dragging momentum.
///
/// The default implementation of this method returns the normal deceleration rate of UIScrollView.
func momentumProjectionRate(_ fpc: FloatingPanelController) -> CGFloat
/// Returns the progress to redirect to the previous position.
///
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to move to the next posiiton. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat
@@ -49,10 +60,33 @@ public protocol FloatingPanelBehavior {
}
public extension FloatingPanelBehavior {
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
switch (fpc.position, proposedTargetPosition) {
case (.full, .tip):
return false
case (.tip, .full):
return false
default:
return true
}
}
func momentumProjectionRate(_ fpc: FloatingPanelController) -> CGFloat {
#if swift(>=4.2)
return UIScrollView.DecelerationRate.normal.rawValue
#else
return UIScrollViewDecelerationRateNormal
#endif
}
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat {
return 0.5
}
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
return defaultBehavior.interactionAnimator(fpc, to: targetPosition, with: velocity)
}
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
@@ -82,10 +116,16 @@ public extension FloatingPanelBehavior {
}
}
class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
private let defaultBehavior = FloatingPanelDefaultBehavior()
public class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
public init() { }
public func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let timing = timeingCurve(with: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timing)
animator.isInterruptible = false
return animator
}
private func timeingCurve(with velocity: CGVector) -> UITimingCurveProvider {
+268 -89
View File
@@ -14,7 +14,10 @@ public protocol FloatingPanelControllerDelegate: class {
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) // changed the settled position in the model layer
func floatingPanelDidMove(_ vc: FloatingPanelController) // any offset changes
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool
func floatingPanelDidMove(_ vc: FloatingPanelController) // any surface frame changes in dragging
// called on start of dragging (may require some time and or distance to move)
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController)
@@ -27,6 +30,11 @@ public protocol FloatingPanelControllerDelegate: class {
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint)
// called when its views are removed from a parent view controller
func floatingPanelDidEndRemove(_ vc: FloatingPanelController)
/// Asks the delegate if the other gesture recognizer should be allowed to recognize the gesture in parallel.
///
/// By default, any tap and long gesture recognizers are allowed to recognize gestures simultaneously.
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
}
public extension FloatingPanelControllerDelegate {
@@ -37,6 +45,9 @@ public extension FloatingPanelControllerDelegate {
return nil
}
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {}
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
return true
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {}
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {}
@@ -45,19 +56,24 @@ public extension FloatingPanelControllerDelegate {
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint) {}
func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {}
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
public enum FloatingPanelPosition: Int, CaseIterable {
public enum FloatingPanelPosition: Int {
case full
case half
case tip
case hidden
}
///
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
///
public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
/// Constants indicating how safe area insets are added to the adjusted content inset.
public enum ContentInsetAdjustmentBehavior: Int {
case always
@@ -65,11 +81,15 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
/// The delegate of the floating panel controller object.
public weak var delegate: FloatingPanelControllerDelegate?
public weak var delegate: FloatingPanelControllerDelegate?{
didSet{
didUpdateDelegate()
}
}
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
public var surfaceView: FloatingPanelSurfaceView! {
return view as? FloatingPanelSurfaceView
return floatingPanel.surfaceView
}
/// Returns the backdrop view managed by the controller object.
@@ -84,7 +104,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
// The underlying gesture recognizer for pan gestures
public var panGestureRecognizer: UIPanGestureRecognizer {
return floatingPanel.panGesture
return floatingPanel.panGestureRecognizer
}
/// The current position of the floating panel controller's contents.
@@ -102,7 +122,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return floatingPanel.behavior
}
/// The content insets of the tracking scroll view derived from the safe area of the parent view
/// The content insets of the tracking scroll view derived from this safe area
public var adjustedContentInsets: UIEdgeInsets {
return floatingPanel.layoutAdapter.adjustedContentInsets
}
@@ -126,63 +146,92 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
private var _contentViewController: UIViewController?
private var floatingPanel: FloatingPanel!
private var layoutInsetsObservations: [NSKeyValueObservation] = []
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = FloatingPanelModalTransition()
required init?(coder aDecoder: NSCoder) {
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
setUp()
}
/// Initialize a newly created floating panel controller.
public init() {
public init(delegate: FloatingPanelControllerDelegate? = nil) {
super.init(nibName: nil, bundle: nil)
self.delegate = delegate
setUp()
}
private func setUp() {
_ = FloatingPanelController.dismissSwizzling
modalPresentationStyle = .custom
transitioningDelegate = modalTransition
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
private func didUpdateDelegate(){
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.behavior = fetchBehavior(for: self.traitCollection)
}
// MARK:- Overrides
/// Creates the view that the controller manages.
override public func loadView() {
assert(self.storyboard == nil, "Storyboard isn't supported")
let view = FloatingPanelSurfaceView()
view.backgroundColor = .white
let view = FloatingPanelPassThroughView()
view.backgroundColor = .clear
backdropView.frame = view.bounds
view.addSubview(backdropView)
surfaceView.frame = view.bounds
view.addSubview(surfaceView)
self.view = view as UIView
}
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {}
else {
// Because {top,bottom}LayoutGuide is managed as a view
if preSafeAreaInsets != layoutInsets {
self.update(safeAreaInsets: layoutInsets)
}
}
}
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if view.translatesAutoresizingMaskIntoConstraints {
view.frame.size = size
view.layoutIfNeeded()
}
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
// Change layout for a new trait collection
updateLayout(for: newCollection)
reloadLayout(for: newCollection)
setUpLayout()
floatingPanel.behavior = fetchBehavior(for: newCollection)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard previousTraitCollection != traitCollection else { return }
if let parent = parent {
self.update(safeAreaInsets: parent.layoutInsets)
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
safeAreaInsetsObservation = nil
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Need to update safeAreaInsets here to ensure that the `adjustedContentInsets` has a correct value.
// Because the parent VC does not call viewSafeAreaInsetsDidChange() expectedly and
// `view.safeAreaInsets` has a correct value of the bottom inset here.
if let parent = parent {
self.update(safeAreaInsets: parent.layoutInsets)
}
}
// MARK:- Privates
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
switch traitCollection.verticalSizeClass {
@@ -198,12 +247,17 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
private func update(safeAreaInsets: UIEdgeInsets) {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
guard
preSafeAreaInsets != safeAreaInsets,
self.floatingPanel.isDecelerating == false
else { return }
floatingPanel.safeAreaInsets = safeAreaInsets
log.debug("Update safeAreaInsets", safeAreaInsets)
scrollView?.contentOffset = contentOffset ?? .zero
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
setUpLayout()
switch contentInsetAdjustmentBehavior {
case .always:
@@ -214,17 +268,67 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
}
private func updateLayout(for traitCollection: UITraitCollection) {
private func reloadLayout(for traitCollection: UITraitCollection) {
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.layoutAdapter.prepareLayout(in: self)
guard let parent = parent else { return }
if let parent = self.parent {
if let layout = layout 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.")
}
if let behavior = behavior 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.")
}
}
}
floatingPanel.layoutAdapter.prepareLayout(toParent: parent)
private func setUpLayout() {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
floatingPanel.layoutAdapter.updateHeight()
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
scrollView?.contentOffset = contentOffset ?? .zero
}
// MARK: - Container view controller interface
/// Shows the surface view at the initial position defined by the current layout
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
reloadLayout(for: traitCollection)
setUpLayout()
if #available(iOS 11.0, *) {
// Must track the safeAreaInsets of `self.view` to update the layout.
// There are 2 reasons.
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
// That's why it needs the observation to keep `adjustedContentInsets` correct.
safeAreaInsetsObservation = self.observe(\.view.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (vc, change) in
guard change.oldValue != change.newValue else { return }
self?.update(safeAreaInsets: vc.layoutInsets)
}
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
// Instead, update(safeAreaInsets:) is called at `viewDidLayoutSubviews()`
}
move(to: floatingPanel.layoutAdapter.layout.initialPosition,
animated: animated,
completion: completion)
}
/// Hides the surface view to the hidden position
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
safeAreaInsetsObservation = nil
move(to: .hidden,
animated: animated,
completion: completion)
}
/// Adds the view managed by the controller as a child of the specified view controller.
/// - Parameters:
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
@@ -241,43 +345,34 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a floating panel doens't work well")
precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a floating panel doens't work well")
view.frame = parent.view.bounds
if let belowView = belowView {
parent.view.insertSubview(self.view, belowSubview: belowView)
} else {
parent.view.addSubview(self.view)
}
layoutInsetsObservations.removeAll()
// Must track safeAreaInsets/{top,bottom}LayoutGuide of the `parent.view`
// to update floatingPanel.safeAreaInsets`. There are 2 reasons.
// 1. The parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar.
// That's why it needs the observation to keep `adjustedContentInsets` correct.
if #available(iOS 11.0, *) {
let observaion = parent.observe(\.view.safeAreaInsets) { [weak self] (vc, chaneg) in
guard let self = self else { return }
self.update(safeAreaInsets: vc.layoutInsets)
}
layoutInsetsObservations.append(observaion)
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
// Instead, safeAreaInsets will be updated in viewDidAppear()
}
#if swift(>=4.2)
parent.addChild(self)
#else
parent.addChildViewController(self)
#endif
// Must set a layout again here because `self.traitCollection` is applied correctly once it's added to a parent VC
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.behavior = fetchBehavior(for: traitCollection)
view.frame = parent.view.bounds // Needed for a correct safe area configuration
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: parent.view.topAnchor, constant: 0.0),
self.view.leftAnchor.constraint(equalTo: parent.view.leftAnchor, constant: 0.0),
self.view.rightAnchor.constraint(equalTo: parent.view.rightAnchor, constant: 0.0),
self.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
])
floatingPanel.setUpViews(in: parent)
floatingPanel.present(animated: animated) { [weak self] in
guard let self = self else { return }
self.didMove(toParent: parent)
show(animated: animated) { [weak self] in
guard let `self` = self else { return }
#if swift(>=4.2)
self.didMove(toParent: self)
#else
self.didMove(toParentViewController: self)
#endif
}
}
@@ -291,15 +386,22 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return
}
layoutInsetsObservations.removeAll()
floatingPanel.dismiss(animated: animated) { [weak self] in
guard let self = self else { return }
hide(animated: animated) { [weak self] in
guard let `self` = self else { return }
#if swift(>=4.2)
self.willMove(toParent: nil)
#else
self.willMove(toParentViewController: nil)
#endif
self.view.removeFromSuperview()
self.backdropView.removeFromSuperview()
#if swift(>=4.2)
self.removeFromParent()
#else
self.removeFromParentViewController()
#endif
completion?()
}
}
@@ -310,22 +412,43 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter.
public func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
precondition(floatingPanel.layoutAdapter.vc != nil, "Use show(animated:completion)")
floatingPanel.move(to: to, animated: animated, completion: completion)
}
/// Sets the view controller responsible for the content portion of the floating panel..
/// Sets the view controller responsible for the content portion of the floating panel.
public func set(contentViewController: UIViewController?) {
if let vc = _contentViewController {
#if swift(>=4.2)
vc.willMove(toParent: nil)
#else
vc.willMove(toParentViewController: nil)
#endif
vc.view.removeFromSuperview()
#if swift(>=4.2)
vc.removeFromParent()
#else
vc.removeFromParentViewController()
#endif
}
if let vc = contentViewController {
let surfaceView = self.view as! FloatingPanelSurfaceView
surfaceView.add(childView: vc.view)
#if swift(>=4.2)
addChild(vc)
#else
addChildViewController(vc)
#endif
let surfaceView = floatingPanel.surfaceView
surfaceView.add(contentView: vc.view)
#if swift(>=4.2)
vc.didMove(toParent: self)
#else
vc.didMove(toParentViewController: self)
#endif
}
_contentViewController = contentViewController
@@ -349,23 +472,30 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// Tracks the specified scroll view to correspond with the scroll.
///
/// - Attention:
/// The specified scroll view must be already assigned to the delegate property because the controller intermediates between the various delegate methods.
///
public func track(scrollView: UIScrollView) {
floatingPanel.scrollView = scrollView
if scrollView.delegate !== floatingPanel {
floatingPanel.userScrollViewDelegate = scrollView.delegate
scrollView.delegate = floatingPanel
/// - Parameters:
/// - scrollView: Specify a scroll view to continuously and seamlessly work in concert with interactions of the surface view or nil to cancel it.
public func track(scrollView: UIScrollView?) {
guard let scrollView = scrollView else {
floatingPanel.scrollView = nil
return
}
floatingPanel.scrollView = scrollView
switch contentInsetAdjustmentBehavior {
case .always:
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
#if swift(>=4.2)
children.forEach { (vc) in
vc.automaticallyAdjustsScrollViewInsets = false
}
#else
childViewControllers.forEach { (vc) in
vc.automaticallyAdjustsScrollViewInsets = false
}
#endif
}
default:
break
@@ -378,14 +508,15 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// by the controller immediately.
///
/// This method updates the `FloatingPanelLayout` object from the delegate and
/// then it calls `layoutIfNeeded()` of the parent's root view to force the view
/// then it calls `layoutIfNeeded()` of the root view to force the view
/// to update the floating panel's layout immediately. It can be called in an
/// animation block.
public func updateLayout() {
updateLayout(for: view.traitCollection)
reloadLayout(for: traitCollection)
setUpLayout()
}
/// Returns the y-coordinate of the point at the origin of the surface view
/// Returns the y-coordinate of the point at the origin of the surface view.
public func originYOfSurface(for pos: FloatingPanelPosition) -> CGFloat {
switch pos {
case .full:
@@ -394,6 +525,54 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return floatingPanel.layoutAdapter.middleY
case .tip:
return floatingPanel.layoutAdapter.bottomY
case .hidden:
return floatingPanel.layoutAdapter.hiddenY
}
}
}
extension FloatingPanelController {
private static let dismissSwizzling: Any? = {
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 {
// switch implementation..
method_exchangeImplementations(originalMethod, swizzledMethod)
}
return nil
}()
}
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) {
// 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)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
}
return
}
// Call dismiss(animated:completion:) to FloatingPanelController directly
if let fpc = self as? FloatingPanelController {
if fpc.presentingViewController != nil {
self.fp_original_dismiss(animated: flag, completion: completion)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
}
return
}
// For other view controllers
self.fp_original_dismiss(animated: flag, completion: completion)
}
}
+300 -87
View File
@@ -5,11 +5,46 @@
import UIKit
/// FloatingPanelFullScreenLayout
///
/// Use the layout protocol if you configure full, half and tip insets from the superview, not the safe area.
/// It can't be used with FloatingPanelIntrinsicLayout.
public protocol FloatingPanelFullScreenLayout: FloatingPanelLayout { }
/// FloatingPanelIntrinsicLayout
///
/// Use the layout protocol if you want to layout a panel using the intrinsic height.
/// It can't be used with FloatingPanelFullScreenLayout.
///
/// - Attention:
/// `insetFor(position:)` must return `nil` for the full position. Because
/// the inset is determined automatically by the intrinsic height.
/// You can customize insets only for the half, tip and hidden positions.
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
public extension FloatingPanelIntrinsicLayout {
var initialPosition: FloatingPanelPosition {
return .full
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
return nil
}
}
public protocol FloatingPanelLayout: class {
/// Returns the initial position of a floating panel.
var initialPosition: FloatingPanelPosition { get }
/// Returns a set of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller. Default is all of them.
/// Returns a set of FloatingPanelPosition objects to tell the applicable
/// positions of the floating panel controller.
///
/// By default, it returns all position except for `hidden` position. Because
/// it's always supported by `FloatingPanelController` so you don't need to return it.
var supportedPositions: Set<FloatingPanelPosition> { get }
/// Return the interaction buffer to the top from the top position. Default is 6.0.
@@ -18,10 +53,13 @@ public protocol FloatingPanelLayout: class {
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
var bottomInteractionBuffer: CGFloat { get }
/// Returns a CGFloat value to determine a floating panel height for each position(full, half and tip).
/// A value for full position indicates a top inset from a safe area.
/// On the other hand, values for half and tip positions indicate bottom insets from a safe area.
/// If a position doesn't contain the supported positions, return nil.
/// Returns a CGFloat value to determine a Y coordinate of a floating panel for each position(full, half, tip and hidden).
///
/// Its returning value indicates a different inset for each position.
/// For full position, a top inset from a safe area in `FloatingPanelController.view`.
/// For half or tip position, a bottom inset from the safe area.
/// For hidden position, a bottom inset from `FloatingPanelController.view`.
/// If a position isn't supported or the default value is used, return nil.
func insetFor(position: FloatingPanelPosition) -> CGFloat?
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
@@ -41,9 +79,9 @@ public extension FloatingPanelLayout {
var bottomInteractionBuffer: CGFloat { return 6.0 }
var supportedPositions: Set<FloatingPanelPosition> {
return Set(FloatingPanelPosition.allCases)
return Set([.full, .half, .tip])
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
@@ -57,6 +95,8 @@ public extension FloatingPanelLayout {
}
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
public init() { }
public var initialPosition: FloatingPanelPosition {
return .half
}
@@ -66,11 +106,14 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout {
case .full: return 18.0
case .half: return 262.0
case .tip: return 69.0
case .hidden: return nil
}
}
}
public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
public init() { }
public var initialPosition: FloatingPanelPosition {
return .tip
}
@@ -89,7 +132,7 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
class FloatingPanelLayoutAdapter {
private weak var parent: UIViewController!
weak var vc: UIViewController!
private weak var surfaceView: FloatingPanelSurfaceView!
private weak var backdropView: FloatingPanelBackdropView!
@@ -99,24 +142,27 @@ class FloatingPanelLayoutAdapter {
}
}
var safeAreaInsets: UIEdgeInsets = .zero {
didSet {
updateHeight()
checkLayoutConsistance()
}
private var safeAreaInsets: UIEdgeInsets {
return vc?.layoutInsets ?? .zero
}
private var parentHeight: CGFloat = 0.0
private var heightBuffer: CGFloat = 88.0 // For bounce
private var initialConst: CGFloat = 0.0
private var fixedConstraints: [NSLayoutConstraint] = []
private var fullConstraints: [NSLayoutConstraint] = []
private var halfConstraints: [NSLayoutConstraint] = []
private var tipConstraints: [NSLayoutConstraint] = []
private var offConstraints: [NSLayoutConstraint] = []
private var heightConstraints: [NSLayoutConstraint] = []
private var interactiveTopConstraint: NSLayoutConstraint?
private var heightConstraint: NSLayoutConstraint?
private var fullInset: CGFloat {
return layout.insetFor(position: .full) ?? 0.0
if layout is FloatingPanelIntrinsicLayout {
return intrinsicHeight
} else {
return layout.insetFor(position: .full) ?? 0.0
}
}
private var halfInset: CGFloat {
return layout.insetFor(position: .half) ?? 0.0
@@ -124,29 +170,75 @@ class FloatingPanelLayoutAdapter {
private var tipInset: CGFloat {
return layout.insetFor(position: .tip) ?? 0.0
}
private var hiddenInset: CGFloat {
return layout.insetFor(position: .hidden) ?? 0.0
}
var supportedPositions: Set<FloatingPanelPosition> {
var supportedPositions = layout.supportedPositions
supportedPositions.remove(.hidden)
return supportedPositions
}
var topMostState: FloatingPanelPosition {
if supportedPositions.contains(.full) {
return .full
}
if supportedPositions.contains(.half) {
return .half
}
return .tip
}
var topY: CGFloat {
if layout.supportedPositions.contains(.full) {
return (safeAreaInsets.top + fullInset)
if supportedPositions.contains(.full) {
switch layout {
case is FloatingPanelIntrinsicLayout:
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
case is FloatingPanelFullScreenLayout:
return fullInset
default:
return (safeAreaInsets.top + fullInset)
}
} else {
return middleY
}
}
var middleY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
if layout is FloatingPanelFullScreenLayout {
return surfaceView.superview!.bounds.height - halfInset
} else{
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
}
}
var bottomY: CGFloat {
if layout.supportedPositions.contains(.tip) {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
if supportedPositions.contains(.tip) {
if layout is FloatingPanelFullScreenLayout {
return surfaceView.superview!.bounds.height - tipInset
} else{
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
}
} else {
return middleY
}
}
var safeAreaBottomY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom)
var hiddenY: CGFloat {
return surfaceView.superview!.bounds.height
}
var topMaxY: CGFloat {
return layout is FloatingPanelFullScreenLayout ? 0.0 : safeAreaInsets.top
}
var bottomMaxY: CGFloat {
if layout is FloatingPanelFullScreenLayout{
return surfaceView.superview!.bounds.height - hiddenInset
} else {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset)
}
}
var adjustedContentInsets: UIEdgeInsets {
@@ -164,136 +256,257 @@ class FloatingPanelLayoutAdapter {
return middleY
case .tip:
return bottomY
case .hidden:
return hiddenY
}
}
var intrinsicHeight: CGFloat = 0.0
init(surfaceView: FloatingPanelSurfaceView, backdropView: FloatingPanelBackdropView, layout: FloatingPanelLayout) {
self.layout = layout
self.surfaceView = surfaceView
self.backdropView = backdropView
}
func prepareLayout(toParent parent: UIViewController) {
self.parent = parent
func updateIntrinsicHeight() {
#if swift(>=4.2)
let fittingSize = UIView.layoutFittingCompressedSize
#else
let fittingSize = UILayoutFittingCompressedSize
#endif
var intrinsicHeight = surfaceView.contentView?.systemLayoutSizeFitting(fittingSize).height ?? 0.0
var safeAreaBottom: CGFloat = 0.0
if #available(iOS 11.0, *) {
safeAreaBottom = surfaceView.contentView?.safeAreaInsets.bottom ?? 0.0
if safeAreaBottom > 0 {
intrinsicHeight -= safeAreaInsets.bottom
}
}
self.intrinsicHeight = max(intrinsicHeight, 0.0)
log.debug("Update intrinsic height =", intrinsicHeight,
", surface(height) =", surfaceView.frame.height,
", content(height) =", surfaceView.contentView?.frame.height ?? 0.0,
", content safe area(bottom) =", safeAreaBottom)
}
func prepareLayout(in vc: UIViewController) {
self.vc = vc
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
surfaceView.translatesAutoresizingMaskIntoConstraints = false
backdropView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
// Fixed constraints of surface and backdrop views
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: parent.view!)
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: vc.view!)
let backdropConstraints = [
backdropView.topAnchor.constraint(equalTo: parent.view.topAnchor,
constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: parent.view.leftAnchor,
constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: parent.view.rightAnchor,
constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor,
constant: 0.0),
backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor,constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0),
]
fixedConstraints = surfaceConstraints + backdropConstraints
// Flexible surface constarints for full, half, tip and off
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.topAnchor,
constant: fullInset),
]
// Flexible surface constraints for full, half, tip and off
let topAnchor: NSLayoutYAxisAnchor = {
if layout is FloatingPanelFullScreenLayout {
return vc.view.topAnchor
} else {
return vc.layoutGuide.topAnchor
}
}()
switch layout {
case is FloatingPanelIntrinsicLayout:
// Set up on updateHeight()
break
default:
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: topAnchor,
constant: fullInset),
]
}
let bottomAnchor: NSLayoutYAxisAnchor = {
if layout is FloatingPanelFullScreenLayout {
return vc.view.bottomAnchor
} else {
return vc.layoutGuide.bottomAnchor
}
}()
halfConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
surfaceView.topAnchor.constraint(equalTo: bottomAnchor,
constant: -halfInset),
]
tipConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
surfaceView.topAnchor.constraint(equalTo: bottomAnchor,
constant: -tipInset),
]
offConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
surfaceView.topAnchor.constraint(equalTo:vc.view.bottomAnchor,
constant: -hiddenInset),
]
}
func startInteraction(at state: FloatingPanelPosition) {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
let interactiveTopConstraint: NSLayoutConstraint
switch layout {
case is FloatingPanelIntrinsicLayout,
is FloatingPanelFullScreenLayout:
initialConst = surfaceView.frame.minY
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
constant: initialConst)
default:
initialConst = surfaceView.frame.minY - safeAreaInsets.top
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
constant: initialConst)
}
NSLayoutConstraint.activate([interactiveTopConstraint])
self.interactiveTopConstraint = interactiveTopConstraint
}
func endInteraction(at state: FloatingPanelPosition) {
// Don't deactivate `interactiveTopConstraint` here because it leads to
// unsatisfiable constraints
}
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateHeight() {
defer {
UIView.performWithoutAnimation {
surfaceView.superview!.layoutIfNeeded()
}
guard let vc = vc else { return }
if let const = self.heightConstraint {
NSLayoutConstraint.deactivate([const])
}
NSLayoutConstraint.deactivate(heightConstraints)
// Must use the parent height, not the screen height because safe area insets
// of the parent are relative values. For example, a view controller in
// Navigation controller's safe area insets and frame can be changed whether
// the navigation bar is translucent or not.
let height = self.parent.view.bounds.height - (safeAreaInsets.top + fullInset)
heightConstraints = [
surfaceView.heightAnchor.constraint(equalToConstant: height)
]
NSLayoutConstraint.activate(heightConstraints)
surfaceView.set(bottomOverflow: heightBuffer)
let heightConstraint: NSLayoutConstraint
switch layout {
case is FloatingPanelIntrinsicLayout:
updateIntrinsicHeight()
heightConstraint = surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom)
default:
let const = -(positionY(for: topMostState))
heightConstraint = surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
constant: const)
}
NSLayoutConstraint.activate([heightConstraint])
self.heightConstraint = heightConstraint
surfaceView.bottomOverflow = vc.view.bounds.height + layout.topInteractionBuffer
if layout is FloatingPanelIntrinsicLayout {
NSLayoutConstraint.deactivate(fullConstraints)
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
}
}
func activateLayout(of state: FloatingPanelPosition?) {
func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool) {
defer {
surfaceView.superview!.layoutIfNeeded() // MUST call here to update `surfaceView.frame`
}
let minY: CGFloat = {
var ret: CGFloat = 0.0
switch layout {
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
ret = topY
default:
ret = topY - safeAreaInsets.top
}
if allowsTopBuffer {
ret -= layout.topInteractionBuffer
}
return max(ret, 0.0) // The top boundary is equal to the related topAnchor.
}()
let maxY: CGFloat = {
var ret: CGFloat = 0.0
switch layout {
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
ret = bottomY
default:
ret = bottomY - safeAreaInsets.top
}
ret += layout.bottomInteractionBuffer
return min(ret, bottomMaxY)
}()
let const = initialConst + diff
interactiveTopConstraint?.constant = max(minY, min(maxY, const))
}
func activateLayout(of state: FloatingPanelPosition) {
defer {
surfaceView.superview!.layoutIfNeeded()
}
var state = state
setBackdropAlpha(of: state)
// Must deactivate `interactiveTopConstraint` here
if let interactiveTopConstraint = interactiveTopConstraint {
NSLayoutConstraint.deactivate([interactiveTopConstraint])
self.interactiveTopConstraint = nil
}
NSLayoutConstraint.activate(fixedConstraints)
guard var state = state else {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints)
NSLayoutConstraint.activate(offConstraints)
return
}
if layout.supportedPositions.contains(state) == false {
if supportedPositions.union([.hidden]).contains(state) == false {
state = layout.initialPosition
}
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
switch state {
case .full:
NSLayoutConstraint.deactivate(halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.activate(fullConstraints)
case .half:
NSLayoutConstraint.deactivate(fullConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.activate(halfConstraints)
case .tip:
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + offConstraints)
NSLayoutConstraint.activate(tipConstraints)
case .hidden:
NSLayoutConstraint.activate(offConstraints)
}
}
func setBackdropAlpha(of target: FloatingPanelPosition?) {
if let target = target {
self.backdropView.alpha = layout.backdropAlphaFor(position: target)
} else {
private func setBackdropAlpha(of target: FloatingPanelPosition) {
if target == .hidden {
self.backdropView.alpha = 0.0
} else {
self.backdropView.alpha = layout.backdropAlphaFor(position: target)
}
}
func checkLayoutConsistance() {
private func checkLayoutConsistance() {
// Verify layout configurations
let supportedPositions = layout.supportedPositions
assert(supportedPositions.count > 0)
assert(supportedPositions.contains(layout.initialPosition),
"Does not include an initial potision(\(layout.initialPosition)) in supportedPositions(\(supportedPositions))")
assert(supportedPositions.union([.hidden]).contains(layout.initialPosition),
"Does not include an initial position (\(layout.initialPosition)) in supportedPositions (\(supportedPositions))")
supportedPositions.forEach { pos in
assert(layout.insetFor(position: pos) != nil,
"Undefined an inset for a pos(\(pos))")
if layout is FloatingPanelIntrinsicLayout {
assert(layout.insetFor(position: .full) == nil, "Return `nil` for full position on FloatingPanelIntrinsicLayout")
}
if halfInset > 0 {
assert(halfInset > tipInset, "Invalid half and tip insets")
}
if fullInset > 0 {
assert(middleY > topY, "Invalid insets")
assert(bottomY > topY, "Invalid insets")
}
// The verification isn't working on orientation change(portrait -> landscape)
// of a floating panel in tab bar. Because the `safeAreaInsets.bottom` is
// updated in delay so that it can be 83.0(not 53.0) even after the surface
// and the super view's frame is fit to landscape already.
/*if fullInset > 0 {
assert(middleY > topY, "Invalid insets { topY: \(topY), middleY: \(middleY) }")
assert(bottomY > topY, "Invalid insets { topY: \(topY), bottomY: \(bottomY) }")
}*/
}
}
@@ -10,7 +10,10 @@ class FloatingPanelSurfaceContentView: UIView {}
/// A view that presents a surface interface in a floating panel.
public class FloatingPanelSurfaceView: UIView {
/// A GrabberHandleView object displayed at the top of the surface view
/// A GrabberHandleView object displayed at the top of the surface view.
///
/// To use a custom grabber handle, hide this and then add the custom one
/// to the surface view at appropriate coordinates.
public var grabberHandle: GrabberHandleView!
/// The height of the grabber bar area
@@ -18,11 +21,11 @@ public class FloatingPanelSurfaceView: UIView {
return Default.grabberTopPadding * 2 + GrabberHandleView.Default.height // 17.0
}
/// A UIView object that can have the surface view added to it.
public var contentView: UIView!
/// A root view of a content view controller
public weak var contentView: UIView!
private var color: UIColor? = .white { didSet { setNeedsLayout() } }
private var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
public override var backgroundColor: UIColor? {
get { return color }
@@ -56,7 +59,8 @@ public class FloatingPanelSurfaceView: UIView {
/// The color of the surface border.
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
private var backgroundLayer: CAShapeLayer! { didSet { setNeedsLayout() } }
public var backgroundView: UIView!
private var backgroundHeightConstraint: NSLayoutConstraint!
private struct Default {
public static let grabberTopPadding: CGFloat = 6.0
@@ -67,7 +71,7 @@ public class FloatingPanelSurfaceView: UIView {
render()
}
required init?(coder aDecoder: NSCoder) {
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
}
@@ -76,22 +80,20 @@ public class FloatingPanelSurfaceView: UIView {
super.backgroundColor = .clear
self.clipsToBounds = false
let backgroundLayer = CAShapeLayer()
layer.insertSublayer(backgroundLayer, at: 0)
self.backgroundLayer = backgroundLayer
let backgroundView = UIView()
addSubview(backgroundView)
self.backgroundView = backgroundView
let contentView = FloatingPanelSurfaceContentView()
addSubview(contentView)
self.contentView = contentView as UIView
contentView.backgroundColor = color
contentView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundHeightConstraint = backgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
backgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
backgroundView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
backgroundView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
backgroundHeightConstraint,
])
let grabberHandle = GrabberHandleView()
addSubview(grabberHandle)
self.grabberHandle = grabberHandle
@@ -105,28 +107,31 @@ public class FloatingPanelSurfaceView: UIView {
])
}
public override func updateConstraints() {
super.updateConstraints()
backgroundHeightConstraint.constant = bottomOverflow
}
public override func layoutSubviews() {
super.layoutSubviews()
log.debug("surface view frame = \(frame)")
updateLayers()
updateContentViewMask()
contentView.layer.borderColor = borderColor?.cgColor
contentView.layer.borderWidth = borderWidth
contentView.backgroundColor = color
contentView?.layer.borderColor = borderColor?.cgColor
contentView?.layer.borderWidth = borderWidth
contentView?.frame = bounds
}
private func updateLayers() {
log.debug("SurfaceView bounds", bounds)
var rect = bounds
rect.size.height += bottomOverflow // Expand the height for overflow buffer
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
backgroundLayer.path = path.cgPath
backgroundLayer.fillColor = color?.cgColor
backgroundView.backgroundColor = color
if cornerRadius != 0.0, backgroundView.layer.cornerRadius != cornerRadius {
backgroundView.layer.masksToBounds = true
backgroundView.layer.cornerRadius = cornerRadius
}
if shadowHidden == false {
layer.shadowColor = shadowColor.cgColor
layer.shadowOffset = shadowOffset
@@ -136,40 +141,35 @@ public class FloatingPanelSurfaceView: UIView {
}
private func updateContentViewMask() {
guard
let contentView = contentView,
cornerRadius != 0.0,
contentView.layer.cornerRadius != cornerRadius
else { return }
if #available(iOS 11, *) {
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyborad of Example/Maps.
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
let maskLayer = CAShapeLayer()
var rect = bounds
rect.size.height += bottomOverflow
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
maskLayer.path = path.cgPath
contentView.layer.mask = maskLayer
contentView.layer.masksToBounds = true
contentView.layer.cornerRadius = cornerRadius
contentView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
} else {
// Don't use `contentView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
// Instead, a user can mask the content view manually in an application.
}
}
func set(bottomOverflow: CGFloat) {
self.bottomOverflow = bottomOverflow
updateLayers()
updateContentViewMask()
}
func add(childView: UIView) {
contentView.addSubview(childView)
childView.frame = contentView.bounds
childView.translatesAutoresizingMaskIntoConstraints = false
func add(contentView: UIView) {
insertSubview(contentView, belowSubview: grabberHandle)
self.contentView = contentView
/* contentView.frame = bounds */ // MUST NOT: Because the top safe area inset of a content VC will be incorrect.
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
childView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0),
childView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0.0),
childView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0.0),
childView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0),
contentView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
])
}
}
@@ -0,0 +1,124 @@
//
// Created by Shin Yamamoto on 2018/11/21.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
class FloatingPanelModalTransition: NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FloatingPanelModalPresentTransition()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FloatingPanelModalDismissTransition()
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return FloatingPanelPresentationController(presentedViewController: presented, presenting: presenting)
}
}
class FloatingPanelPresentationController: UIPresentationController {
override func presentationTransitionWillBegin() {
// Must call here even if duplicating on in containerViewWillLayoutSubviews()
// Because it let the floating panel present correctly with the presentation animation
addFloatingPanel()
}
override func presentationTransitionDidEnd(_ completed: Bool) {
// For non-animated presentation
if let fpc = presentedViewController as? FloatingPanelController, fpc.position == .hidden {
fpc.show(animated: false, completion: nil)
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
if let fpc = presentedViewController as? FloatingPanelController {
// For non-animated dismissal
if fpc.position != .hidden {
fpc.hide(animated: false, completion: nil)
}
fpc.view.removeFromSuperview()
}
}
override func containerViewWillLayoutSubviews() {
guard
let fpc = presentedViewController as? FloatingPanelController
else { fatalError() }
/*
* Layout the views managed by `FloatingPanelController` here for the
* sake of the presentation and dismissal modally from the controller.
*/
addFloatingPanel()
// Forward touch events to the presenting view controller
(fpc.view as? FloatingPanelPassThroughView)?.eventForwardingView = presentingViewController.view
// Set tap-to-dismiss in the backdrop view
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(tapGesture)
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true, completion: nil)
}
private func addFloatingPanel() {
guard
let containerView = self.containerView,
let fpc = presentedViewController as? FloatingPanelController
else { fatalError() }
containerView.addSubview(fpc.view)
fpc.view.frame = containerView.bounds
fpc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
else { fatalError()}
let animator = fpc.behavior.addAnimator(fpc, to: fpc.layout.initialPosition)
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
else { fatalError() }
fpc.show(animated: true) {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
class FloatingPanelModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
else { fatalError()}
let animator = fpc.behavior.removeAnimator(fpc, from: fpc.position)
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
else { fatalError() }
fpc.hide(animated: true) {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
+19
View File
@@ -0,0 +1,19 @@
//
// Created by Shin Yamamoto on 2018/11/21.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
class FloatingPanelPassThroughView: UIView {
public weak var eventForwardingView: UIView?
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
switch hitView {
case self:
return eventForwardingView?.hitTest(self.convert(point, to: eventForwardingView), with: event)
default:
return hitView
}
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ public class GrabberHandleView: UIView {
public static let barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0)
}
required init?(coder aDecoder: NSCoder) {
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.1.0</string>
<string>1.5.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+17 -27
View File
@@ -14,34 +14,22 @@ struct Logger {
private let osLog: OSLog
private let s = DispatchSemaphore(value: 1)
enum Level: Int, Comparable {
private enum Level: Int, Comparable {
case debug = 0
case info = 1
case warning = 2
case error = 3
case fault = 4
var name: String {
switch self {
case .debug: return "DEBUG"
case .info: return "INFO"
case .warning: return "WARNING"
case .error: return "ERROR"
case .fault: return "FAULT"
}
}
var shortName: String {
var displayName: String {
switch self {
case .debug:
return "D/"
case .info:
return "I/"
case .warning:
return "W/"
return "Warning:"
case .error:
return "E/"
case .fault:
return "F/"
return "Error:"
}
}
@available(iOS 10.0, *)
@@ -49,31 +37,35 @@ struct Logger {
switch self {
case .debug: return .debug
case .info: return .info
case .warning: return .info
case .warning: return .default
case .error: return .error
case .fault: return .fault
}
}
public static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool {
static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
init() {
fileprivate init() {
osLog = OSLog(subsystem: "com.scenee.FloatingPanel", category: "FloatingPanel")
}
private func log(_ level: Level, _ message: Any, _ arguments: [Any], function: String, line: UInt) {
#if __FP_LOG
_ = s.wait(timeout: .now() + 0.033)
defer { s.signal() }
let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
let log = "\(level.shortName) \(message) \(extraMessage) (\(function):\(line))"
let log: String = {
switch level {
case .debug:
return "\(level.displayName) \(message) \(extraMessage) (\(function):\(line))"
default:
return "\(level.displayName) \(message) \(extraMessage)"
}
}()
os_log("%@", log: osLog, type: level.osLogType, log)
#endif
}
private func getPrettyFunction(_ function: String, _ file: String) -> String {
@@ -85,7 +77,9 @@ struct Logger {
}
func debug(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
#if __FP_LOG
self.log(.debug, log, arguments, function: getPrettyFunction(function, file), line: line)
#endif
}
func info(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
@@ -99,8 +93,4 @@ struct Logger {
func error(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.error, log, arguments, function: getPrettyFunction(function, file), line: line)
}
func fault(_ log: Any, _ arguments: Any..., function: String = #function, file: String = #file, line: UInt = #line) {
self.log(.fault, log, arguments, function: getPrettyFunction(function, file), line: line)
}
}
+41 -6
View File
@@ -62,18 +62,46 @@ extension UIView {
}
}
extension UIView {
func disableAutoLayout() {
let frame = self.frame
translatesAutoresizingMaskIntoConstraints = true
self.frame = frame
}
func enableAutoLayout() {
translatesAutoresizingMaskIntoConstraints = false
}
}
#if __FP_LOG
#if swift(>=4.2)
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .began: return "Began"
case .changed: return "Changed"
case .failed: return "Failed"
case .cancelled: return "Cancelled"
case .ended: return "Endeded"
case .possible: return "Possible"
case .began: return "began"
case .changed: return "changed"
case .failed: return "failed"
case .cancelled: return "cancelled"
case .ended: return "endeded"
case .possible: return "possible"
}
}
}
#else
extension UIGestureRecognizerState: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .began: return "began"
case .changed: return "changed"
case .failed: return "failed"
case .cancelled: return "cancelled"
case .ended: return "endeded"
case .possible: return "possible"
}
}
}
#endif
#endif
extension UIScrollView {
var contentOffsetZero: CGPoint {
@@ -89,3 +117,10 @@ extension UISpringTimingParameters {
self.init(mass: mass, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)
}
}
extension CGPoint {
static var nan: CGPoint {
return CGPoint(x: CGFloat.nan,
y: CGFloat.nan)
}
}
+47
View File
@@ -0,0 +1,47 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import XCTest
@testable import FloatingPanel
class ViewTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_WarningRetainCycle() {
let myVC = MyZombieViewController(nibName: nil, bundle: nil)
myVC.loadViewIfNeeded()
// Check if there are memory leak warnings in console logs
}
}
class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController?
override func viewDidLoad() {
fpc = FloatingPanelController(delegate: self)
fpc?.addPanel(toParent: self)
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return self
}
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return self
}
var initialPosition: FloatingPanelPosition {
return .half
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0
case .half: return 262.0
case .tip: return 69.0
case .hidden: return nil
}
}
}
-14
View File
@@ -1,14 +0,0 @@
//
// Created by Shin Yamamoto on 2018/09/18.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import XCTest
@testable import FloatingPanelController
class ViewTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
}
+148 -72
View File
@@ -2,7 +2,9 @@
[![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)
[![Platform](https://img.shields.io/cocoapods/p/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
[![Swift 4.1](https://img.shields.io/badge/Swift-4.1-orange.svg?style=flat)](https://swift.org/)
[![Swift 4.2](https://img.shields.io/badge/Swift-4.2-orange.svg?style=flat)](https://swift.org/)
[![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg?style=flat)](https://swift.org/)
# FloatingPanel
@@ -23,10 +25,15 @@ The new interface displays the related contents and utilities in parallel as a u
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Getting Started](#getting-started)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
- [View hierarchy](#view-hierarchy)
- [Usage](#usage)
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
- [Change the initial position and height](#change-the-initial-position-and-height)
- [Support your landscape layout](#support-your-landscape-layout)
- [Use Intrinsic height layout](#use-intrinsic-height-layout)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
@@ -52,6 +59,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [x] Layout customization for all trait environments(i.e. Landscape orientation support)
- [x] Behavior customization
- [x] Free from common issues of Auto Layout and gesture handling
- [x] Modal presentation
Examples are here.
@@ -60,7 +68,7 @@ Examples are here.
## Requirements
FloatingPanel is written in Swift 4.2. Compatible with iOS 10.0+
FloatingPanel is written in Swift. It can be built by Xcode 9.4.1 or later. Compatible with iOS 10.0+.
## Installation
@@ -81,9 +89,10 @@ For [Carthage](https://github.com/Carthage/Carthage), add the following to your
github "scenee/FloatingPanel"
```
## Getting Started
### Add a floating panel as a child view controller
```swift
import UIKit
import FloatingPanel
@@ -112,15 +121,81 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove the views managed by the `FloatingPanelController` object from self.view.
fpc.removePanelFromParent()
}
...
}
```
### Present a floating panel as a modality
```swift
let fpc = FloatingPanelController()
let contentVC = ...
fpc.set(contentViewController: contentVC)
fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down
self.present(fpc, animated: true, completion: nil)
```
You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style.
NOTE: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Framework/Sources/FloatingPanelTransitioning.swift).
## View hierarchy
`FloatingPanelController` manages the views as the following view hierarchy.
```
FloatingPanelController.view (FloatingPanelPassThroughView)
├─ .backdropView (FloatingPanelBackdropView)
└─ .surfaceView (FloatingPanelSurfaceView)
├─ .contentView == FloatingPanelController.contentViewController.view
└─ .grabberHandle (GrabberHandleView)
```
## Usage
### Show/Hide a floating panel in a view with your view hierarchy
```swift
// Add the controller and the managed views to a view controller.
// From the second time, just call `show(animated:completion)`.
view.addSubview(fpc.view)
fpc.view.frame = view.bounds // MUST
// In addition, Auto Layout constraints are highly recommended.
// Because it makes the layout more robust on trait collection change.
//
// fpc.view.translatesAutoresizingMaskIntoConstraints = false
// NSLayoutConstraint.activate([...])
//
parent.addChild(fpc)
// Show a floating panel to the initial position defined in your `FloatingPanelLayout` object.
fpc.show(animated: true) {
// Only for the first time
self.didMove(toParent: self)
}
...
// Hide it
fpc.hide(animated: true) {
// Remove it if needed
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
}
```
NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app.
### Customize the layout with `FloatingPanelLayout` protocol
#### Change the initial position and height
@@ -131,7 +206,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return MyFloatingPanelLayout()
}
...
}
class MyFloatingPanelLayout: FloatingPanelLayout {
@@ -144,6 +218,7 @@ class MyFloatingPanelLayout: FloatingPanelLayout {
case .full: return 16.0 // A top inset from safe area
case .half: return 216.0 // A bottom inset from the safe area
case .tip: return 44.0 // A bottom inset from the safe area
default: return nil // Or `case .hidden: return nil`
}
}
}
@@ -157,7 +232,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returning nil indicates to use the default layout
}
...
}
class FloatingPanelLandscapeLayout: FloatingPanelLayout {
@@ -178,13 +252,40 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuid.leftAnchor, constant: 8.0),
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
}
```
#### Use Intrinsic height layout
1. Lay out your content View with the intrinsic height size. For example, see "Detail View Controller scene"/"Intrinsic View Controller scene" of [Main.storyboard](https://github.com/SCENEE/FloatingPanel/blob/master/Examples/Samples/Sources/Base.lproj/Main.storyboard). The 'Stack View.bottom' constraint determines the intrinsic height.
2. Create a layout that adopts and conforms to `FloatingPanelIntrinsicLayout` and use it.
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return RemovablePanelLayout()
}
}
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 130.0
default: return nil // Must return nil for .full
}
}
...
}
```
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
@@ -195,90 +296,67 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return FloatingPanelStocksBehavior()
}
...
}
...
class FloatingPanelStocksBehavior: FloatingPanelBehavior {
var velocityThreshold: CGFloat {
return 15.0
}
...
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let damping = self.damping(with: velocity)
let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0.5, timingParameters: springTiming)
}
...
}
```
### Use a custom grabber handle
```swift
class ViewController: UIViewController {
...
override func viewDidLoad() {
...
let myGrabberHandleView = MyGrabberHandleView()
fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
}
...
}
let myGrabberHandleView = MyGrabberHandleView()
fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
```
### Add tap gestures to the surface or backdrop views
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
override func viewDidLoad() {
...
override func viewDidLoad() {
...
surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
fpc.surfaceView.addGestureRecognizer(surfaceTapGesture)
surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
fpc.surfaceView.addGestureRecognizer(surfaceTapGesture)
backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(backdropTapGesture)
backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(backdropTapGesture)
surfaceTapGesture.isEnabled = (fpc.position == .tip)
...
}
...
// Enable `surfaceTapGesture` only at `tip` position
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
surfaceTapGesture.isEnabled = (vc.position == .tip)
}
surfaceTapGesture.isEnabled = (fpc.position == .tip)
}
// Enable `surfaceTapGesture` only at `tip` position
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
surfaceTapGesture.isEnabled = (vc.position == .tip)
}
```
### Create an additional floating panel for a detail
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
var searchPanelVC: FloatingPanelController!
var detailPanelVC: FloatingPanelController!
override func viewDidLoad() {
// Setup Search panel
self.searchPanelVC = FloatingPanelController()
override func viewDidLoad() {
// Setup Search panel
self.searchPanelVC = FloatingPanelController()
let searchVC = SearchViewController()
self.searchPanelVC.set(contentViewController: searchVC)
self.searchPanelVC.track(scrollView: contentVC.tableView)
let searchVC = SearchViewController()
self.searchPanelVC.set(contentViewController: searchVC)
self.searchPanelVC.track(scrollView: contentVC.tableView)
self.searchPanelVC.addPanel(toParent: self)
self.searchPanelVC.addPanel(toParent: self)
// Setup Detail panel
self.detailPanelVC = FloatingPanelController()
// Setup Detail panel
self.detailPanelVC = FloatingPanelController()
let contentVC = ContentViewController()
self.detailPanelVC.set(contentViewController: contentVC)
self.detailPanelVC.track(scrollView: contentVC.scrollView)
let contentVC = ContentViewController()
self.detailPanelVC.set(contentViewController: contentVC)
self.detailPanelVC.track(scrollView: contentVC.scrollView)
self.detailPanelVC.addPanel(toParent: self)
}
...
self.detailPanelVC.addPanel(toParent: self)
}
```
@@ -287,15 +365,15 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
In the following example, I move a floating panel to full or half position while opening or closing a search bar like Apple Maps.
```swift
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
```
### Work your contents together with a floating panel behavior
@@ -315,7 +393,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
searchVC.hideHeader()
}
}
...
}
```
@@ -323,7 +400,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller
'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC.
'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC(except for modality).
`FloatingPanelController` has no way to manage a stack of view controllers like `UINavigationController`. If so, it would be so complicated and the interface will become `UINavigationController`. This component should not have the responsibility to manage the stack.
@@ -346,17 +423,16 @@ class ViewController: UIViewController {
secondFpc.addPanel(toParent: self)
}
...
}
```
A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondally floating panel set the destination view controller to the content.
A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondary floating panel set the destination view controller to the content.
It's a great way to decouple between a floating panel and the content VC.
### FloatingPanelSurfaceView's issue on iOS 10
* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854.
* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of `UIVisualEffectView` issue. See https://forums.developer.apple.com/thread/50854.
So you need to draw top rounding corners of your content. Here is an example in Examples/Maps.
```swift
override func viewDidLayoutSubviews() {
@@ -367,11 +443,11 @@ override func viewDidLayoutSubviews() {
}
}
```
* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps's Auto Layout settings of UIVisualEffectView in Main.storyborad.
* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps App's Auto Layout settings of `UIVisualEffectView` in Main.storyboard.
## Author
Shin Yamamoto <shin@scenee.com>
Shin Yamamoto <shin@scenee.com> | [@scenee](https://twitter.com/scenee)
## License