siteline/swiftui-introspect

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

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.