diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index e04c4cc021..213d39809f 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -77,6 +77,7 @@ struct UserPicker: View { .foregroundColor(theme.colors.secondary) .frame(maxWidth: 20, maxHeight: 20) .padding(.horizontal, rowPadding) + .contentShape(Rectangle()) .onTapGesture { if (colorScheme == .light) { ThemeManager.applyTheme(systemDarkThemeDefault.get()) @@ -164,19 +165,15 @@ struct UserPicker: View { } } } - + private func openSheetOnTap(_ icon: String, title: LocalizedStringKey, sheet: UserPickerSheet) -> some View { - Button { - activeSheet = sheet - } label: { - settingsRow(icon, color: theme.colors.secondary) { - Text(title).foregroundColor(.primary) - } + settingsRow(icon, color: theme.colors.secondary) { + Text(title).foregroundColor(.primary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, rowPadding) .padding(.vertical, rowVerticalPadding) - .contentShape(Rectangle()) + .modifier(ListRow { activeSheet = sheet }) } private func unreadBadge(_ u: UserInfo) -> some View { @@ -191,6 +188,64 @@ struct UserPicker: View { } } +struct ListRow: ViewModifier { + @State private var touchDown = false + let action: () -> Void + + func body(content: Content) -> some View { + ZStack { + Color(touchDown ? .systemGray4 : .clear) + content + TouchOverlay(touchDown: $touchDown, action: action) + } + } + + struct TouchOverlay: UIViewRepresentable { + @Binding var touchDown: Bool + let action: () -> Void + + func makeUIView(context: Context) -> TouchView { + let touchView = TouchView() + let gesture = UILongPressGestureRecognizer( + target: touchView, + action: #selector(touchView.longPress(gesture:)) + ) + gesture.delegate = touchView + gesture.minimumPressDuration = 0.05 + touchView.addGestureRecognizer(gesture) + return touchView + } + + func updateUIView(_ touchView: TouchView, context: Context) { + touchView.representer = self + } + + class TouchView: UIView, UIGestureRecognizerDelegate { + var representer: TouchOverlay? + + @objc + func longPress(gesture: UILongPressGestureRecognizer) { + switch gesture.state { + case .began: + representer?.touchDown = true + case .ended: + representer?.touchDown = false + if hitTest(gesture.location(in: self), with: nil) == self { + representer?.action() + } + default: break + } + } + + func gestureRecognizer( + _: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith: UIGestureRecognizer + ) -> Bool { true } + } + } +} + + struct UserPicker_Previews: PreviewProvider { static var previews: some View { @State var activeSheet: UserPickerSheet? diff --git a/apps/ios/Shared/Views/Helpers/SheetRepresentable.swift b/apps/ios/Shared/Views/Helpers/SheetRepresentable.swift index fb5bb980e1..a40f4a687a 100644 --- a/apps/ios/Shared/Views/Helpers/SheetRepresentable.swift +++ b/apps/ios/Shared/Views/Helpers/SheetRepresentable.swift @@ -42,6 +42,7 @@ struct SheetRepresentable: UIViewControllerRepresentable { private let animator = UIViewPropertyAnimator(duration: sheetAnimationDuration, curve: .easeInOut) private let representer: SheetRepresentable private var retainedFraction: CGFloat = 0 + private var sheetHeight: Double { hostingController.view.frame.height } init(content: C, representer: SheetRepresentable) { self.representer = representer @@ -83,9 +84,11 @@ struct SheetRepresentable: UIViewControllerRepresentable { animator.pausesOnCompletion = true animator.scrubsLinearly = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - self?.animator.addAnimations { - sheet.transform = CGAffineTransform(translationX: 0, y: -sheet.frame.height) - self?.view.backgroundColor = .black.withAlphaComponent(0.3) + if let self { + self.animator.addAnimations { + sheet.transform = CGAffineTransform(translationX: 0, y: -self.sheetHeight) + self.view.backgroundColor = .black.withAlphaComponent(0.3) + } } } } @@ -99,11 +102,11 @@ struct SheetRepresentable: UIViewControllerRepresentable { animator.pauseAnimation() retainedFraction = animator.fractionComplete case .changed: - animator.fractionComplete = retainedFraction - gesture.translation(in: view).y / hostingController.view.frame.height + animator.fractionComplete = retainedFraction - gesture.translation(in: view).y / sheetHeight case .ended, .cancelled: let velocity = gesture.velocity(in: view).y - animator.isReversed = velocity.sign == .plus - let defaultVelocity = hostingController.view.frame.height / sheetAnimationDuration + animator.isReversed = (velocity - (animator.fractionComplete - 0.5) * 100).sign == .plus + let defaultVelocity = sheetHeight / sheetAnimationDuration let fractionRemaining = 1 - animator.fractionComplete let durationFactor = min(fractionRemaining / (abs(velocity) / defaultVelocity), 1) animator.continueAnimation(withTimingParameters: nil, durationFactor: durationFactor) @@ -118,7 +121,9 @@ struct SheetRepresentable: UIViewControllerRepresentable { func tap(gesture: UITapGestureRecognizer) { switch gesture.state { case .ended: - representer.isPresented = false + if gesture.location(in: view).y < view.frame.height - sheetHeight { + representer.isPresented = false + } default: break } }