Files
bsky-social-app/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift
T
hailey 544f7befe0 bump it bop it upgrade it (rn 79/expo 53) (#8281)
* basic bumps

* more tweaking

* fix rn patch

* fix crop picker patch

* fix media library patch

* rm unnecessary patch

* fix notifications patch

* update bottomsheet

* Update withAppDelegateReferrer.js

* Delete withNoBundleCompression.js

* rm withNoBundleCompression plugin

* rm findLast shim

* metro package exports is enabled by default

* update react/react-dom/react-compiler

* fix reanimated issue

* vendor expo-ized emoji popup

* fix types

* hackfix view full thread

* Update EmojiPickerModule.podspec

* more upgrades

* fix multiformats package version

* add baseurl

* bump mmkv

* bumps

* update react-keyed-flatten-children

* bump locale packages

* fix emoji picker dark mode

* rn upgrades

* Revert "bump locale packages"

This reverts commit fc82f0f173.

* upgrade testing-library

* rm test renderer

* update patch name minors

* rm findNodeHandle from tabbar

* only do scrollview tag thing on ios

* disable package exports

* update expo notifications handler

* memoize emoji picker styles

* fix tests, mock multiformats

* bump some dev deps with RC versions

* completely rearchitect toasts

* rm logs

* layout animation config for composer footer

* disable autolinking for patched libs

* undo lingui changes

* version bump from release candidate to 0.1

* update atproto deps

* rm @did-plc/server

* fix key issue (maybe)

* move URL polyfill to the polyfill file

* fix yarn lock

* upgrade to 53.0.3

* reanimated layout anim bug patch

* workletize a function that wasn't getting autoworkletized anymore (#8309)

* bump to expo 53.0.4

* bump RN to 0.79.2

* fix yarn lock ci

* Revert "completely rearchitect toasts"

This reverts commit 2e2fcaeeed.

* final upgrades

* chore: cleanup yarn lock

* prettier

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2025-05-02 13:23:39 -07:00

231 lines
6.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ExpoModulesCore
// This view will be used as a native component. Make sure to inherit from `ExpoView`
// to apply the proper styling (e.g. border radius and shadows).
class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
var scrollViewTag: Int? {
didSet {
self.tryFindScrollView()
}
}
private var scrollView: UIScrollView?
private var rctRefreshCtrl: RCTRefreshControl?
private var cancelGestureRecognizers: [UIGestureRecognizer]?
private var animTimer: Timer?
private var initialOffset: CGFloat = 0.0
private var didImpact: Bool = false
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:)))
pg.delegate = self
self.addGestureRecognizer(pg)
let tg = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
tg.isEnabled = false
tg.delegate = self
let lpg = UILongPressGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
lpg.minimumPressDuration = 0.01
lpg.isEnabled = false
lpg.delegate = self
self.cancelGestureRecognizers = [lpg, tg]
}
// We don't want to recognize the scroll pan gesture and the swipe back gesture together
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer {
return false
}
return true
}
// We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will
// interfere with the native swipe back gesture.
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else {
return true
}
let velocity = gestureRecognizer.velocity(in: self)
return abs(velocity.y) > abs(velocity.x)
}
// This will be used to cancel the scroll animation whenever we tap inside of the header. We don't need another
// recognizer for this one.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.stopTimer()
}
// This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change
// the scroll view gesture's delegate, so we add an additional recognizer to detect this.
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) {
self.stopTimer()
}
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
guard let sv = self.scrollView else {
return
}
let translation = sender.translation(in: self).y
if sender.state == .began {
if sv.contentOffset.y < 0 {
sv.contentOffset.y = 0
}
self.initialOffset = sv.contentOffset.y
}
if sender.state == .changed {
sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset)
if sv.contentOffset.y <= -130, !didImpact {
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
self.didImpact = true
}
}
if sender.state == .ended {
let velocity = sender.velocity(in: self).y
self.didImpact = false
if sv.contentOffset.y <= -130 {
self.rctRefreshCtrl?.forwarderBeginRefreshing()
return
}
// A check for a velocity under 250 prevents animations from occurring when they wouldn't in a normal
// scroll view
if abs(velocity) < 250, sv.contentOffset.y >= 0 {
return
}
self.startDecayAnimation(translation, velocity)
}
}
func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) {
guard let sv = self.scrollView else {
return
}
var velocity = velocity
self.enableCancelGestureRecognizers()
if velocity > 0 {
velocity = min(velocity, 5000)
} else {
velocity = max(velocity, -5000)
}
var animTranslation = -translation
self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { _ in
velocity *= 0.9875
animTranslation = (-velocity / 120) + animTranslation
let nextOffset = self.dampenOffset(animTranslation + self.initialOffset)
if nextOffset <= 0 {
if self.initialOffset <= 1 {
self.scrollToOffset(0)
} else {
sv.contentOffset.y = 0
}
self.stopTimer()
return
} else {
sv.contentOffset.y = nextOffset
}
if abs(velocity) < 5 {
self.stopTimer()
}
}
}
func dampenOffset(_ offset: CGFloat) -> CGFloat {
if offset < 0 {
return offset - (offset * 0.55)
}
return offset
}
private func findScrollView(in view: UIView, foundCount: Int) -> UIScrollView? {
var foundCount = foundCount
if let sv = view as? UIScrollView { return sv }
for child in view.subviews {
if let found = findScrollView(in: child, foundCount: foundCount) {
if foundCount == 1 {
print("found sv: \(found)")
// return found
} else {
print("found sv: \(found)")
foundCount += 1
}
}
}
return nil
}
func tryFindScrollView() {
// Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer.
// Otherwise we might end up with duplicates when we switch back to that scrollview.
self.removeCancelGestureRecognizers()
guard let sv = self.findScrollView(in: self.superview!.superview!.superview!, foundCount: 0) else {
print("⚠️ ExpoScrollForwarder: couldnt find UIScrollView under tag \(tag)")
return
}
self.scrollView = sv
self.rctRefreshCtrl = sv.refreshControl as? RCTRefreshControl
self.addCancelGestureRecognizers()
}
func addCancelGestureRecognizers() {
self.cancelGestureRecognizers?.forEach { r in
self.scrollView?.addGestureRecognizer(r)
}
}
func removeCancelGestureRecognizers() {
self.cancelGestureRecognizers?.forEach { r in
self.scrollView?.removeGestureRecognizer(r)
}
}
func enableCancelGestureRecognizers() {
self.cancelGestureRecognizers?.forEach { r in
r.isEnabled = true
}
}
func disableCancelGestureRecognizers() {
self.cancelGestureRecognizers?.forEach { r in
r.isEnabled = false
}
}
func scrollToOffset(_ offset: Int, animated: Bool = true) {
self.scrollView?.scrollRectToVisible(CGRect(x: 0, y: offset, width: 0, height: 0), animated: animated)
}
func stopTimer() {
self.disableCancelGestureRecognizers()
self.animTimer?.invalidate()
self.animTimer = nil
}
}