Search code examples
listformsswiftuiscroll

SwiftUI: Form resets its scroll position to top when popover attached to cell is dismissed


I'm writing an app with SwiftUI on macOS which uses Form view as an editor of user item. Each row in the form has a popover attached to a button inside the row that allows user to change icon of the row. The popover and changing of row icon are both working fine. But scroll position is always reset to top when the popover is dismissed.

Here's the codes:


import SwiftUI

struct ContentView: View {
    
    @State private var name: String = ""
    @State private var comment: String = ""
    
    @State private var items: [Item] = []
    
    enum ContentType: String, Identifiable {
        case typeA
        case typeB
        case typeC
        
        var id: String { rawValue }
    }
    
    var body: some View {
        Form {
            Section {
                TextField("Name", text: $name, prompt: Text("Some name"))
                
                TextField("Comment", text: $comment, prompt: Text("Some comment"))
            }
            
            Section {
                ForEach(items) { item in
                    ItemView(item: item)
                }
            } header: {
                HStack {
                    Text("\(items.count) Items")
                    Spacer()
                    Button {
                        addNewItem()
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }
        }
        .formStyle(.grouped)
    }
    
    private func addNewItem() {
        let item = Item(
            title: "Item \(Int.random(in: 1..<100))",
            image: ImageItem.icon1)
        items.append(item)
    }
}

struct ItemView: View {
    
    @ObservedObject var item: Item
    
    @State private var popoverIsPresented = false
    
    var body: some View {
        HStack {
            Button {
                popoverIsPresented.toggle()
            } label: {
                Image(systemName: item.image.rawValue)
            }
            .buttonStyle(.plain)
            .popover(isPresented: $popoverIsPresented) {
                ImageItemPicker(selectedItem: $item.image, isPresented: $popoverIsPresented)
            }
            
            Text(item.title)
        }
    }
}

struct ImageItemPicker: View {
    
    @Binding var selectedItem: ImageItem
    @Binding var isPresented: Bool
    
    var body: some View {
        List(ImageItem.allCases, selection: $selectedItem) { item in
            Button {
                selectedItem = item
                isPresented = false
            } label: {
                Label(item.rawValue, systemImage: item.rawValue)
            }
        }
    }
}

enum ImageItem: String, CaseIterable, Identifiable {
    case icon1
    case icon2
    case icon3
    case icon4
    case icon5
    
    var id: String { rawValue }
    
    var rawValue: String {
        switch self {
        case .icon1:
            return "square.dashed"
        case .icon2:
            return "bookmark.square"
        case .icon3:
            return "star.square"
        case .icon4:
            return "heart.square"
        case .icon5:
            return "bell.square"
        }
    }
}

class Item: ObservableObject, Identifiable {
    @Published var title: String
    @Published var image: ImageItem
    
    init(title: String, image: ImageItem) {
        self.title = title
        self.image = image
    }
}

I have tried using a List instead of Form. The weird scroll behavior just disappeared. Since List on macOS doesn't have a style like "InsetGrouped", I have to use Form which has a "grouped" style. I'm wondering if this is a bug of SwiftUI or there exist anything wrong in the code.


Solution

  • The form is scrolling up probably because the text fields at the top are in focus.

    In ItemView, you can reset the focus whenever the popover button is pressed:

    @Namespace var ns
    @Environment(\.resetFocus) var reset
    
    var body: some View {
        HStack {
            Button {
                popoverIsPresented.toggle()
                reset(in: ns)
            } label: {
                Image(systemName: item.image.rawValue)
            }
            ...
    

    This will cause the text fields to lose focus, and they will not be scrolled to, when the popover is dismissed.