@@ -37,8 +37,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
3737 private( set) var state : FloatingPanelState = . hidden {
3838 didSet {
3939 os_log ( msg, log: devLog, type: . debug, " state changed: \( oldValue) -> \( state) " )
40- if let vc = ownerVC {
41- vc . delegate? . floatingPanelDidChangeState ? ( vc )
40+ if let fpc = ownerVC {
41+ fpc . delegate? . floatingPanelDidChangeState ? ( fpc )
4242 }
4343 }
4444 }
@@ -120,7 +120,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
120120 completion ? ( )
121121 return
122122 }
123- if state != layoutAdapter . mostExpandedState {
123+ if !isScrollable ( state: state ) {
124124 lockScrollView ( )
125125 }
126126 tearDownActiveInteraction ( )
@@ -130,7 +130,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
130130 if animated {
131131 let updateScrollView : ( ) -> Void = { [ weak self] in
132132 guard let self = self else { return }
133- if self . state == self . layoutAdapter . mostExpandedState , 0 == self . layoutAdapter. offsetFromMostExpandedAnchor {
133+ if self . isScrollable ( state: self . state ) , 0 == self . layoutAdapter. offset ( from : self . state ) {
134134 self . unlockScrollView ( )
135135 } else {
136136 self . lockScrollView ( )
@@ -184,7 +184,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
184184 } else {
185185 self . state = to
186186 self . updateLayout ( to: to)
187- if self . state == self . layoutAdapter . mostExpandedState {
187+ if isScrollable ( state: state ) {
188188 self . unlockScrollView ( )
189189 } else {
190190 self . lockScrollView ( )
@@ -221,11 +221,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
221221 if let contentOffset = contentOffset {
222222 scrollView? . contentOffset = contentOffset
223223 }
224+
225+ adjustScrollContentInsetIfNeeded ( )
224226 }
225227
226228 private func updateLayout( to target: FloatingPanelState ) {
227- self . layoutAdapter. activateLayout ( for: target, forceLayout: true )
228- self . backdropView. alpha = self . getBackdropAlpha ( for: target)
229+ layoutAdapter. activateLayout ( for: target, forceLayout: true )
230+ backdropView. alpha = getBackdropAlpha ( for: target)
231+ adjustScrollContentInsetIfNeeded ( )
229232 }
230233
231234 private func getBackdropAlpha( for target: FloatingPanelState ) -> CGFloat {
@@ -326,7 +329,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
326329 if surfaceView. grabberAreaContains ( gestureRecognizer. location ( in: surfaceView) ) {
327330 return false
328331 }
329- guard state == layoutAdapter. mostExpandedState else { return false }
332+
333+ guard isScrollable ( state: state) else { return false }
334+
330335 // The condition where offset > 0 must not be included here. Because it will stop recognizing
331336 // the panel pan gesture if a user starts scrolling content from an offset greater than 0.
332337 return allowScrollPanGesture ( of: scrollView) { offset in offset <= scrollBounceThreshold }
@@ -390,29 +395,41 @@ class Core: NSObject, UIGestureRecognizerDelegate {
390395 """
391396 )
392397
393- let offsetDiff = value ( of: scrollView. contentOffset - contentOffsetForPinning( of: scrollView) )
398+ let baseOffset = contentOffsetForPinning ( of: scrollView)
399+ let offsetDiff = value ( of: scrollView. contentOffset - baseOffset)
394400
395401 if insideMostExpandedAnchor {
396- // Scroll offset pinning
397- if state == layoutAdapter . mostExpandedState {
402+ // Prevent scrolling if needed
403+ if isScrollable ( state: state ) {
398404 if interactionInProgress {
399405 os_log ( msg, log: devLog, type: . debug, " settle offset -- \( value ( of: initialScrollOffset) ) " )
406+ // Return content offset to initial offset to prevent scrolling
400407 stopScrolling ( at: initialScrollOffset)
401408 } else {
402- if surfaceView. grabberAreaContains ( location ) {
409+ if surfaceView. grabberAreaContains ( initialLocation ) {
403410 // Preserve the current content offset in moving from full.
404411 stopScrolling ( at: initialScrollOffset)
405412 }
413+ /// When the scroll offset is at the pinned offset and a panel is moved, the content
414+ /// must be fixed at the pinned position without scrolling. According to the scroll
415+ /// pan gesture behavior, the content might have already scrolled a bit by the time
416+ /// this handler is called. Thus `initialScrollOffset` property is used here.
417+ if value ( of: initialScrollOffset - baseOffset) == 0.0 {
418+ stopScrolling ( at: initialScrollOffset)
419+ }
406420 }
407421 } else {
422+ // Return content offset to initial offset to prevent scrolling
408423 stopScrolling ( at: initialScrollOffset)
409424 }
410425
411426 // Hide a scroll indicator at the non-top in dragging.
412427 if interactionInProgress {
413428 lockScrollView ( )
414429 } else {
415- if state == layoutAdapter. mostExpandedState, self . transitionAnimator == nil {
430+ // Put back the scroll indicator and bounce of tracking scroll view
431+ // for scrollable states, not most expanded state.
432+ if isScrollable ( state: state) , self . transitionAnimator == nil {
416433 switch layoutAdapter. position {
417434 case . top, . left:
418435 if offsetDiff < 0 && velocity > 0 {
@@ -426,6 +443,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
426443 }
427444 }
428445 } else {
446+ // Here handles seamless scrolling at the most expanded position
429447 if interactionInProgress {
430448 // Show a scroll indicator at the top in dragging.
431449 switch layoutAdapter. position {
@@ -440,14 +458,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
440458 return
441459 }
442460 }
443- if state == layoutAdapter . mostExpandedState {
461+ if isScrollable ( state: state ) {
444462 // Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
445463 if surfaceView. grabberAreaContains ( location) , surfaceView. grabberAreaContains ( initialLocation) {
446464 stopScrolling ( at: initialScrollOffset)
447465 }
448466 }
449467 } else {
450- if state == layoutAdapter . mostExpandedState {
468+ if isScrollable ( state: state ) {
451469 let allowScroll = allowScrollPanGesture ( of: scrollView) { offset in
452470 offset <= scrollBounceThreshold || 0 < offset
453471 }
@@ -570,9 +588,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
570588 }
571589
572590 guard
573- state == layoutAdapter . mostExpandedState , // When not top most(i.e. .full), don't scroll.
574- interactionInProgress == false , // When interaction already in progress, don't scroll.
575- 0 == layoutAdapter. offsetFromMostExpandedAnchor
591+ isScrollable ( state: state ) , // When not top most(i.e. .full), don't scroll.
592+ interactionInProgress == false , // When interaction already in progress, don't scroll.
593+ 0 == layoutAdapter. offset ( from : state )
576594 else {
577595 return false
578596 }
@@ -603,14 +621,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
603621 if offset < 0.0 {
604622 return true
605623 }
606- if velocity >= 0 {
624+ if velocity >= 0 , offset > 0.0 {
607625 return true
608626 }
609627 case . bottom, . right:
610628 if offset > 0.0 {
611629 return true
612630 }
613- if velocity <= 0 {
631+ if velocity <= 0 , offset < 0.0 {
614632 return true
615633 }
616634 }
@@ -632,13 +650,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
632650 os_log ( msg, log: devLog, type: . debug, " panningBegan -- location = \( value ( of: location) ) " )
633651
634652 guard let scrollView = scrollView else { return }
635- if state == layoutAdapter. mostExpandedState {
636- if surfaceView. grabberAreaContains ( location) {
637- initialScrollOffset = scrollView. contentOffset
638- }
639- } else {
640- initialScrollOffset = scrollView. contentOffset
641- }
653+
654+ initialScrollOffset = scrollView. contentOffset
642655 }
643656
644657 private func panningChange( with translation: CGPoint ) {
@@ -775,7 +788,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
775788 var offset : CGPoint = . zero
776789
777790 initialSurfaceLocation = layoutAdapter. surfaceLocation
778- if state == layoutAdapter . mostExpandedState , let scrollView = scrollView {
791+ if isScrollable ( state: state ) , let scrollView = scrollView {
779792 let scrollFrame = scrollView. convert ( scrollView. bounds, to: nil )
780793 let touchStartingPoint = surfaceView. convert ( initialLocation, to: nil )
781794
@@ -859,7 +872,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
859872 interactionInProgress = false
860873
861874 // Prevent to keep a scroll view indicator visible at the half/tip position
862- if state != layoutAdapter . mostExpandedState {
875+ if !isScrollable ( state: state ) {
863876 lockScrollView ( )
864877 }
865878
@@ -949,7 +962,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
949962 """ )
950963
951964 if tryUnlockScroll {
952- if ( state == layoutAdapter . mostExpandedState && 0 == layoutAdapter. offsetFromMostExpandedAnchor )
965+ if ( isScrollable ( state: state ) && 0 == layoutAdapter. offset ( from : state ) )
953966 || shouldLooselyLockScrollView {
954967 unlockScrollView ( )
955968 }
@@ -1040,7 +1053,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
10401053 return
10411054 }
10421055 let contentOffset = scrollView. contentOffset. y
1043- guard contentOffset < 0 , layoutAdapter. position == . bottom, state == layoutAdapter . mostExpandedState else {
1056+ guard contentOffset < 0 , layoutAdapter. position == . bottom, isScrollable ( state: state ) else {
10441057 if surfaceView. transform != . identity {
10451058 surfaceView. transform = . identity
10461059 scrollView. transform = . identity
@@ -1149,6 +1162,48 @@ class Core: NSObject, UIGestureRecognizerDelegate {
11491162 }
11501163 return condition ( offset)
11511164 }
1165+
1166+ func isScrollable( state: FloatingPanelState ) -> Bool {
1167+ guard let scrollView = scrollView else { return false }
1168+ if let fpc = ownerVC, let result = fpc. delegate? . floatingPanel ? ( fpc, shouldAllowToScroll: scrollView) {
1169+ return result
1170+ }
1171+ return state == layoutAdapter. mostExpandedState
1172+ }
1173+
1174+ /// Adjust content inset of the tracking scroll view if the controller's
1175+ /// `contentInsetAdjustmentBehavior` is `.always` and its `contentMode` is `.static`.
1176+ /// if its content is scrollable, the content might not be fully visible on `.half`
1177+ /// state, for example. Therefore the content inset needs to adjust to display the
1178+ /// full content.
1179+ func adjustScrollContentInsetIfNeeded( ) {
1180+ guard
1181+ let fpc = ownerVC,
1182+ let scrollView = scrollView,
1183+ fpc. contentInsetAdjustmentBehavior == . always
1184+ else { return }
1185+
1186+ switch fpc. contentMode {
1187+ case . static:
1188+ var inset = scrollView. safeAreaInsets
1189+ let offset = layoutAdapter. offsetFromMostExpandedAnchor
1190+ if offset < 0 {
1191+ switch layoutAdapter. position {
1192+ case . top:
1193+ inset. top = - offset + scrollView. safeAreaInsets. top
1194+ case . bottom:
1195+ inset. bottom = - offset + scrollView. safeAreaInsets. bottom
1196+ case . left:
1197+ inset. left = - offset + scrollView. safeAreaInsets. left
1198+ case . right:
1199+ inset. left = - offset + scrollView. safeAreaInsets. right
1200+ }
1201+ }
1202+ scrollView. contentInset = inset
1203+ case . fitToBounds:
1204+ scrollView. contentInset = scrollView. safeAreaInsets
1205+ }
1206+ }
11521207}
11531208
11541209/// A gesture recognizer that looks for panning (dragging) gestures in a panel.
0 commit comments