From df86081d19c8e60e90f2adfd5ae44e268cacfacd Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 9 Oct 2024 23:03:17 -0700 Subject: [PATCH] basic scrollview on the native side --- .../bottom-sheet/ios/BottomSheetModule.swift | 1 - .../ios/BottomSheetScrollView.swift | 79 +++++++++++++++++-- .../UIView+findFirstResponder.swift | 20 +++++ modules/bottom-sheet/ios/SheetView.swift | 29 ++----- .../ios/SheetViewController.swift | 11 ++- .../src/BottomSheetNativeComponent.tsx | 16 ---- src/components/Dialog/index.tsx | 71 ++--------------- src/components/ReportDialog/index.tsx | 5 +- 8 files changed, 113 insertions(+), 119 deletions(-) create mode 100644 modules/bottom-sheet/ios/Extensions/UIView+findFirstResponder.swift diff --git a/modules/bottom-sheet/ios/BottomSheetModule.swift b/modules/bottom-sheet/ios/BottomSheetModule.swift index 4866c41918..488349d2de 100644 --- a/modules/bottom-sheet/ios/BottomSheetModule.swift +++ b/modules/bottom-sheet/ios/BottomSheetModule.swift @@ -20,7 +20,6 @@ public class BottomSheetModule: Module { } AsyncFunction("updateLayout") { (view: SheetView) in - view.updateLayout() } Prop("containerBackgroundColor") { (view: SheetView, prop: UIColor) in diff --git a/modules/bottom-sheet/ios/BottomSheetScrollView.swift b/modules/bottom-sheet/ios/BottomSheetScrollView.swift index 9c0fe0f29f..148f975dc0 100644 --- a/modules/bottom-sheet/ios/BottomSheetScrollView.swift +++ b/modules/bottom-sheet/ios/BottomSheetScrollView.swift @@ -6,30 +6,97 @@ // class BottomSheetScrollView: UIScrollView, UIScrollViewDelegate { + private let sheetView: SheetView private var previousHeight: CGFloat? + private var beganDraggingY: CGFloat? - init() { + init(sheetView: SheetView) { + self.sheetView = sheetView super.init(frame: .zero) if let window = Util.getWindow() { self.frame = window.bounds - self.contentInset = UIEdgeInsets(top: 0, - left: 0, - bottom: window.safeAreaInsets.bottom, - right: 0) + let offset = window.safeAreaInsets.bottom * 3 + self.contentInset.bottom = offset + self.verticalScrollIndicatorInsets.top = 20 + self.verticalScrollIndicatorInsets.bottom = offset } + + self.delegate = self + + NotificationCenter.default.addObserver(self, + selector: #selector(keyboardWillShow(notification:)), + name: UIResponder.keyboardWillShowNotification, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(keyboardWillHide(notification:)), + name: UIResponder.keyboardWillHideNotification, + object: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + NotificationCenter.default.removeObserver(self) + } + override func layoutSubviews() { super.layoutSubviews() if let height = self.subviews.first?.frame.size.height, self.previousHeight != height, let window = self.window { - self.contentSize = CGSize(width: window.bounds.width, height: height + window.safeAreaInsets.bottom) + let newDetent = self.sheetView.sheetVc.updateDetents(contentHeight: self.sheetView.clampHeight(height), + preventExpansion: self.sheetView.preventExpansion) + self.sheetView.selectedDetentIdentifier = newDetent + self.contentSize = CGSize(width: window.bounds.width, height: height) + self.previousHeight = height + } + } + + @objc func keyboardWillShow(notification: Notification) { + if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { + let keyboardHeight = keyboardFrame.height + let newOffset = self.contentInset.bottom + keyboardHeight + + // We purposefully add keyboard height to content inset and only update + // the scroll indicator inset + self.contentInset.bottom = newOffset + self.verticalScrollIndicatorInsets.bottom = keyboardHeight + + if let firstResponder = self.findFirstResponder(), + let window = self.window { + let position = firstResponder.convert(firstResponder.bounds, + to: window) + + if position.minY > window.bounds.height - keyboardHeight { + self.setContentOffset(CGPoint(x: 0, y: self.contentOffset.y + keyboardHeight), + animated: true) + } + } + } + } + + @objc func keyboardWillHide(notification: Notification) { + if let window = Util.getWindow() { + let offset = window.safeAreaInsets.bottom * 3 + self.contentInset.bottom = offset + self.verticalScrollIndicatorInsets.bottom = offset + } + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.beganDraggingY = self.contentOffset.y + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let beganDraggingY = self.beganDraggingY, + self.contentOffset.y < beganDraggingY { + if let firstResponder = self.findFirstResponder() { + firstResponder.resignFirstResponder() + } + self.beganDraggingY = nil } } } diff --git a/modules/bottom-sheet/ios/Extensions/UIView+findFirstResponder.swift b/modules/bottom-sheet/ios/Extensions/UIView+findFirstResponder.swift new file mode 100644 index 0000000000..c8a3b53ed8 --- /dev/null +++ b/modules/bottom-sheet/ios/Extensions/UIView+findFirstResponder.swift @@ -0,0 +1,20 @@ +// +// UIView+findFirstResponder.swift +// Pods +// +// Created by Hailey on 10/9/24. +// + +extension UIView { + func findFirstResponder() -> UIView? { + if self.isFirstResponder { + return self + } + for subview in subviews { + if let responder = subview.findFirstResponder() { + return responder + } + } + return nil + } +} diff --git a/modules/bottom-sheet/ios/SheetView.swift b/modules/bottom-sheet/ios/SheetView.swift index 6c37848958..9234b8db6c 100644 --- a/modules/bottom-sheet/ios/SheetView.swift +++ b/modules/bottom-sheet/ios/SheetView.swift @@ -3,13 +3,8 @@ import UIKit class SheetView: ExpoView, UISheetPresentationControllerDelegate { // Views - private var sheetVc: SheetViewController! + var sheetVc: SheetViewController! private var innerView: UIView? - private var contentHeight: CGFloat? { - get { - self.innerView?.frame.height - } - } // Scroll view private var scrollView: BottomSheetScrollView! @@ -59,6 +54,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate { } } } + private var isClosing = false { didSet { if isClosing { @@ -68,7 +64,8 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate { } } } - private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? { + + var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? { didSet { if selectedDetentIdentifier == .large { onSnapPointChange([ @@ -89,12 +86,12 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate { required init (appContext: AppContext? = nil) { super.init(appContext: appContext) - self.scrollView = BottomSheetScrollView() - self.sheetVc = SheetViewController() if let sheet = self.sheetVc.sheetPresentationController { sheet.delegate = self } + + self.scrollView = BottomSheetScrollView(sheetView: self) self.sheetVc.view.addSubview(self.scrollView) self.touchHandler = RCTTouchHandler(bridge: appContext?.reactBridge) @@ -132,12 +129,10 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate { guard !self.isOpen, !self.isOpening, !self.isClosing, - let contentHeight = self.contentHeight, let rvc = self.reactViewController() else { return } - self.sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion) if let sheet = sheetVc.sheetPresentationController { sheet.preferredCornerRadius = self.cornerRadius self.selectedDetentIdentifier = sheet.selectedDetentIdentifier @@ -151,16 +146,6 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate { } } - func updateLayout() { - if self.prevLayoutDetentIdentifier == self.selectedDetentIdentifier, - let contentHeight = self.contentHeight { - self.sheetVc.updateDetents(contentHeight: self.clampHeight(contentHeight), - preventExpansion: self.preventExpansion) - self.selectedDetentIdentifier = self.sheetVc.getCurrentDetentIdentifier() - } - self.prevLayoutDetentIdentifier = self.selectedDetentIdentifier - } - func dismiss() { self.isClosing = true self.sheetVc.dismiss(animated: true) { [weak self] in @@ -170,7 +155,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate { // MARK: - Utils - private func clampHeight(_ height: CGFloat) -> CGFloat { + func clampHeight(_ height: CGFloat) -> CGFloat { if height < self.minHeight { return self.minHeight } else if height > self.maxHeight { diff --git a/modules/bottom-sheet/ios/SheetViewController.swift b/modules/bottom-sheet/ios/SheetViewController.swift index f79f358c42..04cdec23b0 100644 --- a/modules/bottom-sheet/ios/SheetViewController.swift +++ b/modules/bottom-sheet/ios/SheetViewController.swift @@ -20,9 +20,9 @@ class SheetViewController: UIViewController { } } - func setDetents(contentHeight: CGFloat, preventExpansion: Bool) { + func setDetents(contentHeight: CGFloat, preventExpansion: Bool) -> UISheetPresentationController.Detent.Identifier? { guard let sheet = self.sheetPresentationController else { - return + return nil } if contentHeight > Util.getScreenHeight() - 100 { @@ -48,17 +48,20 @@ class SheetViewController: UIViewController { } sheet.selectedDetentIdentifier = .medium } + return sheet.selectedDetentIdentifier } - func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) { + func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) -> UISheetPresentationController.Detent.Identifier? { + var newDetent: UISheetPresentationController.Detent.Identifier? if let sheet = self.sheetPresentationController { sheet.animateChanges { - self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion) + newDetent = self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion) if #available(iOS 16.0, *) { sheet.invalidateDetents() } } } + return newDetent } func getCurrentDetentIdentifier() -> UISheetPresentationController.Detent.Identifier? { diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx index 0d0afe3536..a43abc50d8 100644 --- a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx @@ -2,10 +2,7 @@ import * as React from 'react' import { Dimensions, NativeSyntheticEvent, - Platform, - ScrollView, StyleProp, - View, ViewStyle, } from 'react-native' import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' @@ -66,7 +63,6 @@ export class BottomSheetNativeComponent extends React.Component< render() { const {children, ...rest} = this.props - const cornerRadius = rest.cornerRadius ?? 0 if (!this.state.open) { return null @@ -82,19 +78,7 @@ export class BottomSheetNativeComponent extends React.Component< height: screenHeight, width: '100%', }}> - {/**/} {children} - {/**/} ) } diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 659e4e31d5..b844e9eddc 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -1,26 +1,17 @@ import React, {useImperativeHandle} from 'react' import { - NativeScrollEvent, - NativeSyntheticEvent, Pressable, processColor, - ScrollView, StyleProp, TextInput, View, ViewStyle, } from 'react-native' -import { - KeyboardAwareScrollView, - useKeyboardHandler, -} from 'react-native-keyboard-controller' -import {runOnJS} from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {logger} from '#/logger' -import {isAndroid, isIOS} from '#/platform/detection' import {useA11y} from '#/state/a11y' import {useDialogStateControlContext} from '#/state/dialogs' import {List, ListMethods, ListProps} from '#/view/com/util/List' @@ -161,6 +152,7 @@ export function Outer({ onSnapPointChange={onSnapPointChange} onStateChange={onStateChange} disableDrag={disableDrag} + // @ts-expect-error @TODO containerBackgroundColor={processColor(t.atoms.bg.backgroundColor)}> {children} @@ -169,69 +161,16 @@ export function Outer({ ) } -export function Inner({children, style}: DialogInnerProps) { - const insets = useSafeAreaInsets() +export function Inner({children, style, ...props}: DialogInnerProps) { return ( - + {children} ) } -export const ScrollableInner = React.forwardRef( - function ScrollableInner({children, style, ...props}, ref) { - const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() - const insets = useSafeAreaInsets() - const [keyboardHeight, setKeyboardHeight] = React.useState(0) - useKeyboardHandler({ - onEnd: e => { - 'worklet' - runOnJS(setKeyboardHeight)(e.height) - }, - }) - - const basePading = - (isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight) - const fullPaddingBase = insets.bottom + insets.top + basePading - const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50 - - const paddingBottom = - nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading - - const onScroll = (e: NativeSyntheticEvent) => { - const {contentOffset} = e.nativeEvent - if (contentOffset.y > 0 && !disableDrag) { - setDisableDrag(true) - } else if (contentOffset.y <= 1 && disableDrag) { - setDisableDrag(false) - } - } - - return children - - return ( - - {children} - - ) - }, -) +// @TODO remove this later +export const ScrollableInner = Inner export const InnerFlatList = React.forwardRef< ListMethods, diff --git a/src/components/ReportDialog/index.tsx b/src/components/ReportDialog/index.tsx index 5bf8aa5b4b..ed75488a9d 100644 --- a/src/components/ReportDialog/index.tsx +++ b/src/components/ReportDialog/index.tsx @@ -1,6 +1,5 @@ import React from 'react' import {Pressable, View} from 'react-native' -import {ScrollView} from 'react-native-gesture-handler' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -38,10 +37,8 @@ function ReportDialogInner(props: ReportDialogProps) { } = useMyLabelersQuery() const isLoading = useDelayedLoading(500, isLabelerLoading) - const ref = React.useRef(null) - return ( - + {isLoading ? (