Post

Replies

Boosts

Views

Activity

SwiftUI List produces random offset when modifying content from a modal view presented above it
Backstory - I wanted to create a SwiftUI List with a collapsible header. I came up a solution that I will show below in code In short, there's an empty section the size of the header in the List, based on which I get an offset while scrolling and can move the header accordingly And it has been working great, except the only problem so far is that if you have a screen with this component and there's another screen presented modally that causes modifications of the content of the underlying screen with a List, this solution produces a random offset value of 39.5333333333333 The value is exactly the same no matter the size of the header I put in there, and I can't see what is causing it. This ONLY happens when modification happens from a modal screen. The style of the List also doesn't matter, bug happens on all of them So far I found that it happens due to us using UIKit navigation controllers in our app. Shown below are two examples of code, one with UIKit navigation that has the bug and one fully SwiftUI that doesn't. The screen itself has no difference whatsoever UIKit: struct ScrollOffsetPreferenceKey: PreferenceKey { static var defaultValue: CGPoint = .zero static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {} } struct SizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} } class Coordinator: UINavigationController { lazy var viewModel = ContentViewModel() init() { super.init(nibName: nil, bundle: nil) let vc = UIHostingController(rootView: ContentView(viewModel: self.viewModel)) vc.navigationItem.rightBarButtonItems = [ UIBarButtonItem( title: "Show Modal", style: .plain, target: self, action: #selector(showModal) ) ] self.setViewControllers([vc], animated: false) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc func showModal() { self.present(UIHostingController(rootView: ModalView(viewModel: self.viewModel)), animated: true) } } class ContentViewModel: ObservableObject { @Published var items: [UUID] = [] init() { self.random() } func random() { self.items = (1..<100).map { _ in UUID() } } } struct ContentView: View { @ObservedObject var viewModel: ContentViewModel @State var headerSize: CGSize = .zero var body: some View { ZStack { self.listView VStack { self.header .background( GeometryReader { proxy in Color.clear .preference(key: SizePreferenceKey.self, value: proxy.size) } ) .onPreferenceChange(SizePreferenceKey.self) { self.headerSize = $0 } Spacer() } } } @ViewBuilder var listView: some View { GeometryReader { proxy in List { Section( header: Color.red .frame(height: self.headerSize.height) .listRowInsets(EdgeInsets()) .anchorPreference(key: ScrollOffsetPreferenceKey.self, value: .bounds) { proxy[$0].origin } .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: self.handleOffsetChange) ) {} Section() { ForEach(self.viewModel.items, id: \.self) { Text($0.uuidString) } } } .listStyle(GroupedListStyle()) } } @ViewBuilder var header: some View { Text("Test text, hello") .padding() .frame(maxWidth: .infinity) } func handleOffsetChange(to offset: CGPoint) { debugPrint("\(offset.y)") } } struct ModalView: View { @ObservedObject var viewModel: ContentViewModel var body: some View { Button("Do stuff") { self.viewModel.random() } } } SwiftUI (preferences and view model is the same, removed due to character limit): struct ContentView: View { @ObservedObject var viewModel: ContentViewModel @State var showingSheet = false @State var headerSize: CGSize = .zero var body: some View { NavigationView { ZStack { self.listView VStack { self.header .background( GeometryReader { proxy in Color.clear .preference(key: SizePreferenceKey.self, value: proxy.size) } ) .onPreferenceChange(SizePreferenceKey.self) { self.headerSize = $0 } Spacer() } } .sheet(isPresented: self.$showingSheet) { ModalView(viewModel: self.viewModel) } .navigationTitle("Test") .navigationBarItems( trailing: Button("Show modal") { self.showingSheet.toggle() } ) } } @ViewBuilder var listView: some View { GeometryReader { proxy in List { Section( header: Color.red .frame(height: self.headerSize.height) .listRowInsets(EdgeInsets()) .anchorPreference(key: ScrollOffsetPreferenceKey.self, value: .bounds) { proxy[$0].origin } .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: self.handleOffsetChange) ) {} Section() { ForEach(self.viewModel.items, id: \.self) { Text($0.uuidString) } } } .listStyle(GroupedListStyle()) } } @ViewBuilder var header: some View { Text("Test text, hello") .padding() .frame(maxWidth: .infinity) } func handleOffsetChange(to offset: CGPoint) { debugPrint("\(offset.y)") } } struct ModalView: View { @ObservedObject var viewModel: ContentViewModel var body: some View { Button("Do stuff") { self.viewModel.random() } } } Does anyone have an idea of how to fix this?
0
1
1.1k
Aug ’22