Skip to content

Commit 0926cc2

Browse files
committed
Fix scrolling in the message list when presented with a sheet on iOS 26
1 parent a5ae622 commit 0926cc2

File tree

3 files changed

+57
-36
lines changed

3 files changed

+57
-36
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33

44
# Upcoming
55

6-
### 🔄 Changed
6+
### 🐞 Fixed
7+
- Fix scrolling in the message list when presented with a sheet on iOS 26 [#1065](https://github.com/GetStream/stream-chat-swiftui/pull/1065)
78

89
# [4.94.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.94.0)
910
_December 02, 2025_

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
1010
@StateObject var messageViewModel: MessageViewModel
1111
@Environment(\.channelTranslationLanguage) var translationLanguage
1212
@Environment(\.highlightedMessageId) var highlightedMessageId
13+
@Environment(\.messageListSwipe) var messageListSwipe
1314

1415
@Injected(\.fonts) private var fonts
1516
@Injected(\.colors) private var colors
@@ -32,7 +33,6 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
3233
@State private var computeFrame = false
3334
@State private var offsetX: CGFloat = 0
3435
@State private var offsetYAvatar: CGFloat = 0
35-
@GestureState private var offset: CGSize = .zero
3636

3737
private let replyThreshold: CGFloat = 60
3838
private var paddingValue: CGFloat {
@@ -129,6 +129,9 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
129129
.onChange(of: computeFrame, perform: { _ in
130130
frame = proxy.frame(in: .global)
131131
})
132+
.onChange(of: messageListSwipe, perform: { messageListSwipe in
133+
handleMessageListSwipe(messageListSwipe, geometry: proxy)
134+
})
132135
}
133136
)
134137
.onTapGesture(count: 2) {
@@ -140,40 +143,6 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
140143
handleGestureForMessage(showsMessageActions: true)
141144
})
142145
.offset(x: min(self.offsetX, maximumHorizontalSwipeDisplacement))
143-
.simultaneousGesture(
144-
DragGesture(
145-
minimumDistance: minimumSwipeDistance,
146-
coordinateSpace: .local
147-
)
148-
.updating($offset) { (value, gestureState, _) in
149-
guard messageViewModel.isSwipeToQuoteReplyPossible else {
150-
return
151-
}
152-
// Using updating since onEnded is not called if the gesture is canceled.
153-
let diff = CGSize(
154-
width: value.location.x - value.startLocation.x,
155-
height: value.location.y - value.startLocation.y
156-
)
157-
158-
if diff == .zero {
159-
gestureState = .zero
160-
} else {
161-
gestureState = value.translation
162-
}
163-
}
164-
)
165-
.onChange(of: offset, perform: { _ in
166-
if !channel.config.quotesEnabled {
167-
return
168-
}
169-
170-
if offset == .zero {
171-
// gesture ended or cancelled
172-
setOffsetX(value: 0)
173-
} else {
174-
dragChanged(to: offset.width)
175-
}
176-
})
177146
.accessibilityElement(children: .contain)
178147
.accessibilityIdentifier("MessageView")
179148

@@ -351,6 +320,18 @@ public struct MessageContainerView<Factory: ViewFactory>: View {
351320
private var messageListConfig: MessageListConfig {
352321
utils.messageListConfig
353322
}
323+
324+
private func handleMessageListSwipe(_ messageListSwipe: MessageListSwipe?, geometry: GeometryProxy) {
325+
guard messageViewModel.isSwipeToQuoteReplyPossible else { return }
326+
guard let messageListSwipe else { return }
327+
// The view is moving during the swipe handling, therefore we skip the contains check if it is in progress
328+
guard offsetX > 0 || geometry.frame(in: .global).contains(messageListSwipe.startLocation) else { return }
329+
if messageListSwipe.horizontalOffset == 0 {
330+
setOffsetX(value: 0)
331+
} else {
332+
dragChanged(to: messageListSwipe.horizontalOffset)
333+
}
334+
}
354335

355336
private func dragChanged(to value: CGFloat) {
356337
let horizontalTranslation = value

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public struct MessageListView<Factory: ViewFactory>: View, KeyboardReadable {
3636
@State private var scrollDirection = ScrollDirection.up
3737
@State private var unreadMessagesBannerShown = false
3838
@State private var unreadButtonDismissed = false
39+
@State private var messageListSwipe: MessageListSwipe?
3940

4041
private var messageRenderingUtil = MessageRenderingUtil.shared
4142
private var skipRenderingMessageIds = [String]()
@@ -191,6 +192,7 @@ public struct MessageListView<Factory: ViewFactory>: View, KeyboardReadable {
191192
isLast: !showsLastInGroupInfo && message == messages.last
192193
)
193194
.environment(\.channelTranslationLanguage, channel.membership?.language)
195+
.environment(\.messageListSwipe, messageListSwipe)
194196
.onAppear {
195197
if index == nil {
196198
index = messageListDateUtils.index(for: message, in: messages)
@@ -310,6 +312,20 @@ public struct MessageListView<Factory: ViewFactory>: View, KeyboardReadable {
310312
}
311313
}
312314
}
315+
.if(channel.config.quotesEnabled, transform: { view in
316+
view.simultaneousGesture(
317+
DragGesture(
318+
minimumDistance: utils.messageListConfig.messageDisplayOptions.minimumSwipeGestureDistance,
319+
coordinateSpace: .global
320+
)
321+
.onChanged { value in
322+
messageListSwipe = MessageListSwipe(startLocation: value.startLocation, horizontalOffset: value.translation.width)
323+
}
324+
.onEnded { value in
325+
messageListSwipe = MessageListSwipe(startLocation: value.startLocation, horizontalOffset: 0)
326+
}
327+
)
328+
})
313329
.accessibilityIdentifier("MessageListScrollView")
314330
}
315331

@@ -651,6 +667,15 @@ private struct MessageViewModelKey: EnvironmentKey {
651667
static let defaultValue: MessageViewModel? = nil
652668
}
653669

670+
private struct MessageListSwipeKey: EnvironmentKey {
671+
static let defaultValue: MessageListSwipe? = nil
672+
}
673+
674+
struct MessageListSwipe: Equatable {
675+
let startLocation: CGPoint
676+
let horizontalOffset: CGFloat
677+
}
678+
654679
extension EnvironmentValues {
655680
var channelTranslationLanguage: TranslationLanguage? {
656681
get {
@@ -669,4 +694,18 @@ extension EnvironmentValues {
669694
self[MessageViewModelKey.self] = newValue
670695
}
671696
}
697+
698+
/// Propagates the drag state to message items.
699+
///
700+
/// - Important: Since iOS 26 simultaneous gestures do not update ancestors.
701+
/// The gesture handler should be attached to the ScrollView and then propagating
702+
/// the state to items which decide if the drag should be handled.
703+
var messageListSwipe: MessageListSwipe? {
704+
get {
705+
self[MessageListSwipeKey.self]
706+
}
707+
set {
708+
self[MessageListSwipeKey.self] = newValue
709+
}
710+
}
672711
}

0 commit comments

Comments
 (0)