IOS 17 custom left/right swipe gesture for list item not rendering in List view
smaksymov opened this issue · 4 comments
Description
I added custom left/ right gestures to List item using ZStack and by setting offsets to views and action buttons, and all is working good until IOS 17, but in IOS 17 gestures calculations seems ok but view not renders offsets in list item set by gesture .offset(x: offset) is not rendered, when I remove introspect( .list from modifiers left/right swipe gestures are working well, so something is blocking this rendering in introspect. So Issue only arrives in IOS 17 for all before versions IOS 15, 16 all works as expected, only 17 not working.
Checklist
- I have read the README before submitting this report.
- This issue hasn't been addressed in an existing GitHub issue or discussion.
Expected behavior
Left/Right custom swipe actions correctly calculating offsets and rendering
Actual behavior
Left/Right custom swipe action correctly calculating offsets BUT list item cell not rendering updated view state according applied offsets.
Steps to reproduce
List {
Section {
ForEach(viewModel.listsViewModels, id: \.self ) { item in
ListCellView(viewModel: item)
.listRowSeparator(.hidden)
}
}
}
ListCellView subviews has .offset(x: swipeOffset) which is changed by
DragGesture(minimumDistance: 20)
.updating($isDragging, body: { value, state, _ in
if !listsViewModel.isSelectModeActivated {
state = true
}
})
.onChanged { value in
swipeOffset = value.translation.width
}
.onEnded { value in
}
connected to ListCellView very main ZStack
Version information
0.12.0
Destination operating system
IOS 17
Xcode version information
Version 15.0 beta 8 (15A5229m)
Swift Compiler version information
swift-driver version: 1.75.2 Apple Swift version 5.8.1 (swiftlang-5.8.0.124.5 clang-1403.0.22.11.100)
Target: arm64-apple-macosx13.0
Hi Stepan, thanks for the report. Unfortunately those two snippets you provided don't compile on their own. Just so I can get the full picture and get to the root of the issue faster, could you please provide a small reproducible Xcode project?
ok, just investigated a bit, left/right swipes are become blocked when you will set custom UICollectionViewDelegate to collection view got from introspect
.introspect(.list, on: .iOS(.v16, .v17)) { collectionView in
DispatchQueue.main.async {
self.collectionView = collectionView
self.collectionView?.refreshControl?.backgroundColor = UIColor.clear
self.scrollCollectionProtocol = CollectionViewProtocolImpl(scrollStateCompletion)
self.collectionView?.delegate = scrollCollectionProtocol
}
}
here we get collectionView and set custom delegate like this:
class CollectionViewProtocolImpl: NSObject, UICollectionViewDelegate {
private var scrollStateCompletion: (((offset: CGFloat, viewHeight: CGFloat, contentSize: CGFloat)) -> Void)?
init(_ completion: (((offset: CGFloat, viewHeight: CGFloat, contentSize: CGFloat)) -> Void)?){
self.scrollStateCompletion = completion
}
func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
DispatchQueue.main.async { [weak self] in
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let bottomPadding = windowScene?.windows.first?.safeAreaInsets.bottom ?? 0
self?.scrollStateCompletion?((scrollView.contentOffset.y, scrollView.bounds.size.height, scrollView.contentSize.height + bottomPadding))
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
DispatchQueue.main.async { [weak self] in
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let bottomPadding = windowScene?.windows.first?.safeAreaInsets.bottom ?? 0
self?.scrollStateCompletion?((scrollView.contentOffset.y, scrollView.bounds.size.height, scrollView.contentSize.height + bottomPadding))
}
}
}
this custom delegate is needed to get position of scrollview inside list but exact after this custom delegate is set, all horizontal custom swipes on item will stop working but this is only in IOS 17, on IOS 16 with same codes all works good.
here is sample of item
import SwiftUI
struct TestListItem: View {
@StateObject var viewModel: TestItemViewModel
@GestureState private var isDragging = false
@State private var isTapOnDelete: Bool = false
@State private var gestureMoveOffset: Double = 32
var body: some View {
let singleTapBeforeLongPress = singleTap.exclusively(before: longPress)
let tapsAndDrag = dragGesture
ZStack{
VStack{
HStack{
Spacer()
Text("buttons")
.foregroundColor(.blue)
}
}
.background(.yellow)
.frame(maxWidth: .infinity)
.animation(.linear, value: viewModel.offset)
VStack{
HStack{
Text("Main Content")
.foregroundColor(.red)
Spacer()
}
}
.gesture(singleTapBeforeLongPress)
.padding(.init(top: 20, leading: 18, bottom: 0, trailing: 18))
.background{
Color.red
.cornerRadius(10)
.shadow(color: .black, radius: 2, x: 0, y: 5)
}
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.white,
lineWidth: 2)
)
.offset(x: viewModel.offset)
.animation(.linear, value: viewModel.offset)
.zIndex(1)
}
.simultaneousGesture(tapsAndDrag)
.onAppear {
isTapOnDelete = false
viewModel.offset = 0
}
}
var longPress: some Gesture {
LongPressGesture(minimumDuration: 0.5)
.onEnded { _ in
}
}
var singleTap: some Gesture {
TapGesture().onEnded { _ in
}
}
var dragGesture: some Gesture {
DragGesture(minimumDistance: 20)
.updating($isDragging, body: { value, state, _ in
})
.onChanged { value in
withAnimation{
viewModel.offset = value.translation.width
}
}
.onEnded { value in
}
}
}
here is it's viewModel
import Foundation
import SwiftUI
class TestItemViewModel: ObservableObject, Hashable {
@Published var offset: CGFloat = 0
static func == (lhs: TestItemViewModel, rhs: TestItemViewModel) -> Bool {
lhs.id == rhs.id
}
let id = UUID().uuidString
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
and the test list, just add introspect to it:
List {
Section{
ForEach(items, id: \.self){ item in
TestListItem(
viewModel: TestItemViewModel()
)
.listRowBackground(Color.black)
.listRowSeparator(.hidden)
}
}
}
.environment(\.defaultMinListRowHeight, 1)
.background(.black)
.coordinateSpace(name: "List")
.listStyle(.plain)
.refreshable {
}
Please provide a sample that compiles without needing to fill in the gaps. I can't debug something if I don't have the full reproducible scenario in question. An Xcode project (either as a zip or a link to a repo) would be the best format.
Closing due to inactivity. Feel free to reopen with a follow-up.