From 0926cc2de20569c399e55bcadf2c8accdd89f1f2 Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Fri, 5 Dec 2025 13:40:16 +0200 Subject: [PATCH] Fix scrolling in the message list when presented with a sheet on iOS 26 --- CHANGELOG.md | 3 +- .../MessageList/MessageContainerView.swift | 51 ++++++------------- .../MessageList/MessageListView.swift | 39 ++++++++++++++ 3 files changed, 57 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eca98c61..689cdb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming -### 🔄 Changed +### 🐞 Fixed +- Fix scrolling in the message list when presented with a sheet on iOS 26 [#1065](https://github.com/GetStream/stream-chat-swiftui/pull/1065) # [4.94.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.94.0) _December 02, 2025_ diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift index cf9324c9..048db492 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift @@ -10,6 +10,7 @@ public struct MessageContainerView: View { @StateObject var messageViewModel: MessageViewModel @Environment(\.channelTranslationLanguage) var translationLanguage @Environment(\.highlightedMessageId) var highlightedMessageId + @Environment(\.messageListSwipe) var messageListSwipe @Injected(\.fonts) private var fonts @Injected(\.colors) private var colors @@ -32,7 +33,6 @@ public struct MessageContainerView: View { @State private var computeFrame = false @State private var offsetX: CGFloat = 0 @State private var offsetYAvatar: CGFloat = 0 - @GestureState private var offset: CGSize = .zero private let replyThreshold: CGFloat = 60 private var paddingValue: CGFloat { @@ -129,6 +129,9 @@ public struct MessageContainerView: View { .onChange(of: computeFrame, perform: { _ in frame = proxy.frame(in: .global) }) + .onChange(of: messageListSwipe, perform: { messageListSwipe in + handleMessageListSwipe(messageListSwipe, geometry: proxy) + }) } ) .onTapGesture(count: 2) { @@ -140,40 +143,6 @@ public struct MessageContainerView: View { handleGestureForMessage(showsMessageActions: true) }) .offset(x: min(self.offsetX, maximumHorizontalSwipeDisplacement)) - .simultaneousGesture( - DragGesture( - minimumDistance: minimumSwipeDistance, - coordinateSpace: .local - ) - .updating($offset) { (value, gestureState, _) in - guard messageViewModel.isSwipeToQuoteReplyPossible else { - return - } - // Using updating since onEnded is not called if the gesture is canceled. - let diff = CGSize( - width: value.location.x - value.startLocation.x, - height: value.location.y - value.startLocation.y - ) - - if diff == .zero { - gestureState = .zero - } else { - gestureState = value.translation - } - } - ) - .onChange(of: offset, perform: { _ in - if !channel.config.quotesEnabled { - return - } - - if offset == .zero { - // gesture ended or cancelled - setOffsetX(value: 0) - } else { - dragChanged(to: offset.width) - } - }) .accessibilityElement(children: .contain) .accessibilityIdentifier("MessageView") @@ -351,6 +320,18 @@ public struct MessageContainerView: View { private var messageListConfig: MessageListConfig { utils.messageListConfig } + + private func handleMessageListSwipe(_ messageListSwipe: MessageListSwipe?, geometry: GeometryProxy) { + guard messageViewModel.isSwipeToQuoteReplyPossible else { return } + guard let messageListSwipe else { return } + // The view is moving during the swipe handling, therefore we skip the contains check if it is in progress + guard offsetX > 0 || geometry.frame(in: .global).contains(messageListSwipe.startLocation) else { return } + if messageListSwipe.horizontalOffset == 0 { + setOffsetX(value: 0) + } else { + dragChanged(to: messageListSwipe.horizontalOffset) + } + } private func dragChanged(to value: CGFloat) { let horizontalTranslation = value diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift index c2c0e6d0..fe208e1b 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift @@ -36,6 +36,7 @@ public struct MessageListView: View, KeyboardReadable { @State private var scrollDirection = ScrollDirection.up @State private var unreadMessagesBannerShown = false @State private var unreadButtonDismissed = false + @State private var messageListSwipe: MessageListSwipe? private var messageRenderingUtil = MessageRenderingUtil.shared private var skipRenderingMessageIds = [String]() @@ -191,6 +192,7 @@ public struct MessageListView: View, KeyboardReadable { isLast: !showsLastInGroupInfo && message == messages.last ) .environment(\.channelTranslationLanguage, channel.membership?.language) + .environment(\.messageListSwipe, messageListSwipe) .onAppear { if index == nil { index = messageListDateUtils.index(for: message, in: messages) @@ -310,6 +312,20 @@ public struct MessageListView: View, KeyboardReadable { } } } + .if(channel.config.quotesEnabled, transform: { view in + view.simultaneousGesture( + DragGesture( + minimumDistance: utils.messageListConfig.messageDisplayOptions.minimumSwipeGestureDistance, + coordinateSpace: .global + ) + .onChanged { value in + messageListSwipe = MessageListSwipe(startLocation: value.startLocation, horizontalOffset: value.translation.width) + } + .onEnded { value in + messageListSwipe = MessageListSwipe(startLocation: value.startLocation, horizontalOffset: 0) + } + ) + }) .accessibilityIdentifier("MessageListScrollView") } @@ -651,6 +667,15 @@ private struct MessageViewModelKey: EnvironmentKey { static let defaultValue: MessageViewModel? = nil } +private struct MessageListSwipeKey: EnvironmentKey { + static let defaultValue: MessageListSwipe? = nil +} + +struct MessageListSwipe: Equatable { + let startLocation: CGPoint + let horizontalOffset: CGFloat +} + extension EnvironmentValues { var channelTranslationLanguage: TranslationLanguage? { get { @@ -669,4 +694,18 @@ extension EnvironmentValues { self[MessageViewModelKey.self] = newValue } } + + /// Propagates the drag state to message items. + /// + /// - Important: Since iOS 26 simultaneous gestures do not update ancestors. + /// The gesture handler should be attached to the ScrollView and then propagating + /// the state to items which decide if the drag should be handled. + var messageListSwipe: MessageListSwipe? { + get { + self[MessageListSwipeKey.self] + } + set { + self[MessageListSwipeKey.self] = newValue + } + } }