@swmansion/react-native-bottom-sheet 0.15.0-next.2 → 0.15.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,9 +68,8 @@ public final class BottomSheetHostingView: UIView {
68
68
  public let sheetContainer = UIView()
69
69
  private let scrimView = UIControl()
70
70
  private var panGesture: UIPanGestureRecognizer!
71
- private var activeSpring: CriticalSpring?
72
- private var activeSpringTargetIndex: Int = 0
73
- private var activeSpringEmitsSettle = false
71
+ private var activeAnimator: UIViewPropertyAnimator?
72
+ private var activeAnimatorEmitsSettle = false
74
73
  private var scrimPinnedFull = false
75
74
  private var displayLink: CADisplayLink?
76
75
  private var pendingIndex: Int?
@@ -182,13 +181,20 @@ public final class BottomSheetHostingView: UIView {
182
181
  return
183
182
  }
184
183
 
185
- if activeSpring != nil || isPanning { return }
184
+ if activeAnimator != nil || isPanning { return }
186
185
  sheetContainer.transform = CGAffineTransform(translationX: 0, y: translationY(for: targetIndex))
187
186
  updateScrim()
188
187
  }
189
188
 
189
+ private var presentedSheetFrame: CGRect {
190
+ if activeAnimator != nil, let presentation = sheetContainer.layer.presentation() {
191
+ return presentation.frame
192
+ }
193
+ return sheetContainer.frame
194
+ }
195
+
190
196
  override public func point(inside point: CGPoint, with _: UIEvent?) -> Bool {
191
- if sheetContainer.frame.contains(point) {
197
+ if presentedSheetFrame.contains(point) {
192
198
  return true
193
199
  }
194
200
 
@@ -198,7 +204,7 @@ public final class BottomSheetHostingView: UIView {
198
204
  override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
199
205
  guard self.point(inside: point, with: event) else { return nil }
200
206
 
201
- if isScrimVisible, !sheetContainer.frame.contains(point) {
207
+ if isScrimVisible, !presentedSheetFrame.contains(point) {
202
208
  let scrimPoint = convert(point, to: scrimView)
203
209
  return scrimView.hitTest(scrimPoint, with: event)
204
210
  }
@@ -263,8 +269,8 @@ public final class BottomSheetHostingView: UIView {
263
269
  }
264
270
 
265
271
  public func resetSheetState() {
266
- activeSpring = nil
267
- activeSpringEmitsSettle = false
272
+ activeAnimator?.stopAnimation(true)
273
+ activeAnimator = nil
268
274
  stopDisplayLink()
269
275
  rawDetentSpecs = []
270
276
  detentSpecs = []
@@ -358,7 +364,7 @@ public final class BottomSheetHostingView: UIView {
358
364
 
359
365
  private func startDisplayLink() {
360
366
  guard displayLink == nil else { return }
361
- let link = CADisplayLink(target: self, selector: #selector(displayLinkFired(_:)))
367
+ let link = CADisplayLink(target: self, selector: #selector(displayLinkFired))
362
368
  link.add(to: .main, forMode: .common)
363
369
  displayLink = link
364
370
  }
@@ -379,9 +385,8 @@ public final class BottomSheetHostingView: UIView {
379
385
  isContentInteractionDisabled = !isEnabled
380
386
  }
381
387
 
382
- @objc private func displayLinkFired(_ link: CADisplayLink) {
383
- // `targetTimestamp` is predicted when the NEXT frame will be shown
384
- stepSpring(targetTime: link.targetTimestamp)
388
+ @objc private func displayLinkFired() {
389
+ emitPosition()
385
390
  }
386
391
 
387
392
  @objc private func handleScrimPress() {
@@ -389,7 +394,7 @@ public final class BottomSheetHostingView: UIView {
389
394
  modal,
390
395
  let closedIndex,
391
396
  targetIndex != closedIndex,
392
- activeSpring == nil || currentSheetHeight > 0.5
397
+ activeAnimator == nil || currentSheetHeight > 0.5
393
398
  else {
394
399
  return
395
400
  }
@@ -413,71 +418,43 @@ public final class BottomSheetHostingView: UIView {
413
418
  let currentTy = sheetContainer.transform.ty
414
419
  let targetTy = translationY(for: index)
415
420
  let distance = targetTy - currentTy
416
-
417
421
  let velocityRatio = distance != 0 ? velocity / distance : 0
418
422
  let clampedRatio = min(max(velocityRatio, -5), 5)
419
- let v0 = clampedRatio * distance
420
-
421
- let duration: CFTimeInterval = 0.45
422
- // Pick the stiffness so the sheet looks settled (within ~0.5% of target)
423
- // right at `duration`. For a critically-damped spring that point is
424
- // ω·t ≈ 8, so ω = 8 / duration. Exact agreement with UIKit's spring doesn't
425
- // matter here we drive the modal from this curve.
426
- //
427
- // NOTE: This solution may affect the animation performance when the main thread is busy.
428
- // If that becomes a problem, consider adopting this solution: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/412
429
- let omega = 8.0 / CGFloat(duration)
430
- activeSpringEmitsSettle = emitSettle
431
- activeSpringTargetIndex = index
432
-
433
- activeSpring = CriticalSpring(
434
- from: currentTy,
435
- target: targetTy,
436
- v0: v0,
437
- omega: omega,
438
- startTime: CACurrentMediaTime(),
439
- duration: duration
440
- )
441
-
423
+ let initialVelocity = CGVector(dx: 0, dy: clampedRatio)
424
+
425
+ activeAnimatorEmitsSettle = emitSettle
426
+ activeAnimator?.stopAnimation(true)
427
+
428
+ let spring = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: initialVelocity)
429
+ let animator = UIViewPropertyAnimator(duration: 0.45, timingParameters: spring)
430
+
431
+ animator.addAnimations {
432
+ self.sheetContainer.transform = CGAffineTransform(translationX: 0, y: targetTy)
433
+ }
434
+ animator.addCompletion { [weak self] position in
435
+ guard let self, position == .end else { return }
436
+ self.stopDisplayLink()
437
+ self.emitPosition()
438
+ self.activeAnimator = nil
439
+ self.activeAnimatorEmitsSettle = false
440
+ self.scrimPinnedFull = false
441
+ self.setContentInteractionEnabled(true)
442
+ self.updateInteractionState()
443
+ if emitSettle {
444
+ self.eventDelegate?.bottomSheetHostingView(self, didSettle: index)
445
+ }
446
+ }
442
447
  // Report the index change as soon as the snap is committed, not when it
443
448
  // finishes: `targetIndex` is already set, and a programmatic snap's start is
444
449
  // known to the caller. `onSettle` remains the signal for movement end.
445
450
  if emitIndexChange {
446
451
  eventDelegate?.bottomSheetHostingView(self, didChangeIndex: index)
447
452
  }
453
+ animator.startAnimation()
454
+ activeAnimator = animator
448
455
  startDisplayLink()
449
456
  }
450
457
 
451
- private func stepSpring(targetTime: CFTimeInterval) {
452
- guard let spring = activeSpring else { return }
453
- if spring.isFinished(at: targetTime) {
454
- finishSpring()
455
- return
456
- }
457
- let ty = spring.value(at: targetTime)
458
- sheetContainer.transform = CGAffineTransform(translationX: 0, y: ty)
459
- emitPosition()
460
- }
461
-
462
- private func finishSpring() {
463
- let index = activeSpringTargetIndex
464
- let emitSettle = activeSpringEmitsSettle
465
- let targetTy = translationY(for: index)
466
-
467
- activeSpring = nil
468
- activeSpringEmitsSettle = false
469
- stopDisplayLink()
470
-
471
- sheetContainer.transform = CGAffineTransform(translationX: 0, y: targetTy)
472
- emitPosition()
473
- scrimPinnedFull = false
474
- setContentInteractionEnabled(true)
475
- updateInteractionState()
476
- if emitSettle {
477
- eventDelegate?.bottomSheetHostingView(self, didSettle: index)
478
- }
479
- }
480
-
481
458
  @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
482
459
  let maxHeight = sheetContainerHeight
483
460
 
@@ -493,10 +470,12 @@ public final class BottomSheetHostingView: UIView {
493
470
  handler.isEnabled = true
494
471
  }
495
472
  gesture.setTranslation(.zero, in: self)
496
- if activeSpring != nil {
473
+ if let animator = activeAnimator {
497
474
  stopDisplayLink()
498
- activeSpring = nil
499
- activeSpringEmitsSettle = false
475
+ let visual = sheetContainer.layer.presentation()?.affineTransform() ?? sheetContainer.transform
476
+ animator.stopAnimation(true)
477
+ sheetContainer.transform = visual
478
+ activeAnimator = nil
500
479
  }
501
480
 
502
481
  case .changed:
@@ -714,13 +693,13 @@ public final class BottomSheetHostingView: UIView {
714
693
  let newMaxHeight = sheetContainerHeight
715
694
  let targetTy = translationY(for: targetIndex)
716
695
 
717
- if activeSpring != nil {
696
+ if let animator = activeAnimator {
718
697
  stopDisplayLink()
719
- // `transform.ty` is the live on-screen value (set each frame).
720
- let visualTy = sheetContainer.transform.ty
721
- let shouldEmitSettle = activeSpringEmitsSettle
722
- activeSpring = nil
723
- activeSpringEmitsSettle = false
698
+ let visualTy = sheetContainer.layer.presentation()?.affineTransform().ty ?? sheetContainer.transform.ty
699
+ let shouldEmitSettle = activeAnimatorEmitsSettle
700
+ animator.stopAnimation(true)
701
+ activeAnimator = nil
702
+ activeAnimatorEmitsSettle = false
724
703
  // Re-anchor the in-flight position to the new container height so the
725
704
  // sheet surface keeps the same on-screen height across the resize.
726
705
  let visibleHeight = previousMaxHeight - visualTy
@@ -861,10 +840,9 @@ extension BottomSheetHostingView: UIGestureRecognizerDelegate {
861
840
 
862
841
  private extension BottomSheetHostingView {
863
842
  var currentTranslationY: CGFloat {
864
- // Both the drag and the settle assign `transform` directly every frame, so
865
- // it always holds the live on-screen value. (If the settle were run by a
866
- // UIViewPropertyAnimator instead, the in-flight value would live on the
867
- // render server and we'd have to read `layer.presentation()` here.)
843
+ if activeAnimator != nil, let presentation = sheetContainer.layer.presentation() {
844
+ return presentation.affineTransform().ty
845
+ }
868
846
  return sheetContainer.transform.ty
869
847
  }
870
848
 
@@ -890,7 +868,7 @@ private extension BottomSheetHostingView {
890
868
  if
891
869
  let closedIndex,
892
870
  targetIndex == closedIndex,
893
- activeSpring == nil,
871
+ activeAnimator == nil,
894
872
  !isPanning
895
873
  {
896
874
  scrimView.alpha = 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/react-native-bottom-sheet",
3
- "version": "0.15.0-next.2",
3
+ "version": "0.15.0-next.3",
4
4
  "description": "Provides bottom-sheet components for React Native.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -1,39 +0,0 @@
1
- import QuartzCore
2
-
3
- /// A spring we evaluate ourselves each frame instead of letting
4
- /// `UIViewPropertyAnimator` run the settle. The animator runs on the "render
5
- /// server", so its value is only readable one frame late via `presentation()` —
6
- /// the cause of the follower view (fed by `onPositionChange`) trailing the
7
- /// modal. Computing it in-process lets us emit the exact value we set.
8
- /// Critically damped (ζ = 1): reaches the target as fast as possible without
9
- /// overshooting.
10
- struct CriticalSpring {
11
- let from: CGFloat
12
- let target: CGFloat
13
- /// Initial velocity (points/sec) — e.g. carried over from a finger flick.
14
- let v0: CGFloat
15
- /// Angular frequency (rad/sec) — the spring's stiffness/speed. Higher ω snaps faster;
16
- let omega: CGFloat
17
- let startTime: CFTimeInterval
18
- let duration: CFTimeInterval
19
-
20
- /// Position at an absolute `time`, from the closed-form solution of a
21
- /// critically-damped spring (ζ = 1):
22
- /// x(t) = target + e^(−ω·t)·[A + (v0 + ω·A)·t]
23
- /// where A is the starting offset from the target. The `e^(−ω·t)` term decays
24
- /// the offset toward 0 (so x → target), and the linear `…·t` factor is what
25
- /// lets a critically-damped spring carry initial velocity without oscillating.
26
- func value(at time: CFTimeInterval) -> CGFloat {
27
- // Seconds since the spring started (clamped so a past `time` reads as t = 0).
28
- let t = CGFloat(max(0, time - startTime))
29
- // A: how far `from` is from `target` — the offset the spring must close.
30
- let a = from - target
31
- // Exponential envelope: 1 at t = 0, shrinking toward 0 as time passes.
32
- let decay = exp(-omega * t)
33
- return target + decay * (a + (v0 + omega * a) * t)
34
- }
35
-
36
- func isFinished(at time: CFTimeInterval) -> Bool {
37
- time - startTime >= duration
38
- }
39
- }