SwiftUI - 防止列表在项目选择时将滚动位置重置为顶部

Posted

技术标签:

【中文标题】SwiftUI - 防止列表在项目选择时将滚动位置重置为顶部【英文标题】:SwiftUI - Prevent List from resetting scroll position to top on item selection 【发布时间】:2019-06-26 09:30:23 【问题描述】:

所以,一直在搞乱 SwiftUI,除了在选择项目时列表会将其滚动位置重置到顶部之外,它都可以正常工作。我做任何事情都无法解决这个问题。代码 -

struct FontSettingsView : View 

    @ObjectBinding var settings: FontSettings

    var body: some View 
        NavigationView 
            ContentView(settings: settings)
                .navigationBarTitle(Text("Font"), displayMode: .inline)
        
    

    struct ContentView: View 

        @State var settings: FontSettings

        var body: some View 
            VStack 

                HeaderView(settings: $settings)

                FontListView(excludedFontNames: settings.excludedFontNames)  fontName in
                    self.$settings.name.value = fontName
                
            
        

        struct HeaderView: View 

            @Binding var settings: FontSettings

            var body: some View 
                VStack 

                    HStack 
                        VStack (alignment: .leading) 
                            Text("Custom font size:")
                                .font(.body)
                            Text("If disabled, will use system settings")
                                .font(.caption)
                        
                        Toggle(isOn: $settings.isCustomSize) 
                            Text("")
                        
                    

                    Slider(value: $settings.size, from: 10, through: 30, by: 0.1)
                        .disabled($settings.isCustomSize.value == false)

                    Text("Current font: \($settings.name.value)")
                        .customFont(using: self.settings)
                        .padding()
                        .animation(.default)

                .padding()
            
        

        struct FontListView: View 

            struct FontName: Identifiable 
                var id = UUID()
                let value: String
            

            private let fontNames: [FontName]
            private let tapped: ((String) -> ())?

            init(excludedFontNames: [String] = [], tapped: ((String) -> ())?) 
                self.fontNames = UIFont.familyNames.sorted().filter( excludedFontNames.contains($0) == false ).map  FontName(value: $0) 
                self.tapped = tapped
            

            var body: some View 
                List(fontNames)  fontName in
                    Button(action: 
                        self.tapped?(fontName.value)
                    ) 
                        Text(fontName.value).customFont(named: fontName.value)
                    
                
            
        
    

不幸的是,所有文档和示例似乎都与选择项目时导航到新屏幕有关,这不是我的意图。

为想要运行整个程序的任何人提供一些额外的代码 -

class FontSettings: BindableObject 

    private static let defaultSize: CGFloat = 20

    var didChange = PassthroughSubject<Void, Never>()

    var excludedFontNames: [String] = []

    var name: String = "Helvetica" 
        didSet 
            self.didChange.send()
        
    
    var size: CGFloat = FontSettings.defaultSize 
        didSet 
            self.didChange.send()
        
    
    var isCustomSize: Bool = false 
        didSet 
            if self.isCustomSize == false 
                self.size = FontSettings.defaultSize
            
            self.didChange.send()
        
    



extension View 

    func customFont(named fontName: String, style: UIFont.TextStyle = .body) -> Self.Modified<CustomFont> 
        return self.modifier(CustomFont(fontName: fontName, textStyle: style))
    

    func customFont(named fontName: String, size: CGFloat) -> Self.Modified<CustomFont> 
        return self.modifier(CustomFont(fontName: fontName, size: size))
    

    func customFont(using settings: FontSettings) -> Self.Modified<CustomFont> 
        if settings.isCustomSize 
            return self.modifier(CustomFont(fontName: settings.name, size: settings.size))
        
        return self.modifier(CustomFont(fontName: settings.name, textStyle: .body))
    



struct CustomFont: ViewModifier 

    let fontName: String
    let fontSize: CGFloat?
    let textStyle: UIFont.TextStyle?

    init(fontName: String, textStyle: UIFont.TextStyle) 
        self.fontName = fontName
        self.textStyle = textStyle
        self.fontSize = nil
    

    init(fontName: String, size: CGFloat) 
        self.fontName = fontName
        self.fontSize = size
        self.textStyle = nil
    

    // trigger view refresh when the ContentSizeCategory changes
    @Environment(\.sizeCategory) var sizeCategory: ContentSizeCategory

    func body(content: Content) -> some View 

        if let fontSize = self.fontSize 
            return content.font(.custom(self.fontName, size: fontSize))
        

        guard let textStyle = self.textStyle, let size = self.fontSizes[textStyle] else 
            fatalError("Unrecognized textStyle")
        

        let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
        let fontSize = fontMetrics.scaledValue(for: size)

        return content.font(.custom(self.fontName, size: fontSize))
    

    // normal font sizes per style, as defined by Apple
    private let fontSizes: [UIFont.TextStyle: CGFloat] = [
        .largeTitle: 34,
        .title1: 28,
        .title2: 22,
        .title3: 20,
        .headline: 17,
        .body: 17,
        .callout: 16,
        .subheadline: 15,
        .footnote: 13,
        .caption1: 12,
        .caption2: 11
    ]

【问题讨论】:

你有没有找到解决办法? @Jordy 不,还没有 【参考方案1】:

我确信有更好的方法,但这是我用于基本选择的方法

基本上存储一组id,并将每个单元格绑定到该组

然后在单元格选择时从集合中添加/删除。

struct Person: Identifiable 
    var name: String
    var id = UUID().uuidString


struct ContentView: View 

    @State private var selectedItems = Set<String>([])
    @State private var items = [
        Person(name: "A"),
        Person(name: "B"),
        Person(name: "C"),
        Person(name: "D"),
        Person(name: "E"),
        Person(name: "F"),
        Person(name: "G"),
        Person(name: "H"),
        Person(name: "I"),
        Person(name: "J"),
        Person(name: "K"),
        Person(name: "L")
    ]

    var body: some View 
        List 
            ForEach(self.items)  item in
                Row(item: item, selectedItems: self.$selectedItems)
            .listRowInsets(EdgeInsets.appDefault())
        
    


struct Row: View 

    var item: Person
    @State private var isSelected: Bool = false
    // ref to list of current selected items
    @Binding var selectedItems: Set<String>

    private func isInSelectedList(_ item: Person) -> Bool 
        selectedItems.contains(item.id)
    

    var body: some View 
        HStack 
            VStack(alignment: .leading) 
                Text(item.name)
                    .font(.headline)
            

            Spacer()

            Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
                .font(Font.system(size: 40, weight: .light))
                .foregroundColor(isSelected ? Color(UIColor.systemGreen) : Color.secondary.opacity(0.6))
                .onTapGesture 
                    // use the main view as the view model
                    self.addOrRemoveFromSelectedList(self.item)
                    // tap circle to add - then re check if should show tick or not
                    self.isSelected = self.isInSelectedList(self.item)
                

        
        .frame(height: 80)
        .padding([.top, .bottom])
        .onAppear 
            // after scrolling away the selection is set when cell shown again
            self.isSelected = self.isInSelectedList(self.item)
        
    

    private func addOrRemoveFromSelectedList(_ item: Person) 
        if selectedItems.contains(item.id) 
            selectedItems.remove(item.id)
         else 
            selectedItems.insert(item.id)
        
    


// stops the annoying inseting of cell when selected
extension EdgeInsets 
    static func appDefault() -> EdgeInsets 
        return .init(top: 0, leading: 16, bottom: 0, trailing: 16)
    

【讨论】:

谢谢你,我会试一试的——我非常喜欢 SwiftUI 的某些部分,但是我们有时不得不跳过一些障碍来做最简单的事情,这让人感觉像是一场持久战 【参考方案2】:

嘿,我发现如果你在 ScrollView 中使用 ForEach,当你使用 onTap 手势识别器时它不会重置滚动位置

ScrollView 
            ForEach(podcast, id: \.id)  item in
                PodcastRow(item: item, audioPlayer: audioPlayer)
                    .padding(.leading)
            
            .navigationBarTitle(Text("\(podcastTitle)"))
            .onAppear 
                self.loadPodcasts()
            
        

希望这就是你要找的东西

【讨论】:

以上是关于SwiftUI - 防止列表在项目选择时将滚动位置重置为顶部的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 在列表滚动期间停止更新

如何使用滚动视图单击列表部分内的项目以导航到详细信息页面 SwiftUI

当 Swiftui Picker 滚动其列表时触发的事件

SwiftUI 中的底部优先滚动

在 SwiftUI 中滚动时,许多项目列表崩溃,我该如何解决?

滚动列表中的项目时丢弃 SwiftUI 状态