使用 List 而不是 ScrollView 和 ForEach

Posted

技术标签:

【中文标题】使用 List 而不是 ScrollView 和 ForEach【英文标题】:Using a List instead of a ScrollView and ForEach 【发布时间】:2020-07-05 16:29:56 【问题描述】:

我有以下代码来显示一些卡片,如果用户点击其中一个卡片会展开为全屏:

import SwiftUI

struct ContentView: View 

    @State private var cards = [
        Card(title: "testA", subtitle: "subtitleA"),
        Card(title: "testB", subtitle: "subtitleB"),
        Card(title: "testC", subtitle: "subtitleC"),
        Card(title: "testD", subtitle: "subtitleD"),
        Card(title: "testE", subtitle: "subtitleE")
    ]

    @State private var showDetails: Bool = false
    @State private var heights = [Int: CGFloat]()

    var body: some View 
        ScrollView 
            VStack 
                if(!cards.isEmpty) 
                    ForEach(self.cards.indices, id: \.self)  index in
                        GeometryReader  reader in
                            CardView(card: self.$cards[index], isDetailed: self.$showDetails)
                            .offset(y: self.cards[index].showDetails ? -reader.frame(in: .global).minY : 0)
                            .fixedSize(horizontal: false, vertical: !self.cards[index].showDetails)
                            .background(GeometryReader 
                                Color.clear
                                    .preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
                            )
                            .onTapGesture 
                                withAnimation(.spring()) 
                                    self.cards[index].showDetails.toggle()
                                    self.showDetails.toggle()
                                
                            
                        
                        .frame(height: self.cards[index].showDetails ? UIScreen.main.bounds.height : self.heights[index], alignment: .center)
                        .onPreferenceChange(ViewHeightKey.self)  value in
                            self.heights[index] = value
                        
                        
                 else 
                    ActivityIndicator(style: UIActivityIndicatorView.Style.medium).frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 2)
                
            
        
        .onAppear() 
            // load data
        
    



struct CardView : View 
    @Binding var card : Card
    @Binding var isDetailed : Bool

    var body: some View 
        VStack(alignment: .leading)
            ScrollView(showsIndicators: isDetailed && card.showDetails) 
                HStack (alignment: .center)
                    VStack (alignment: .leading)
                        HStack(alignment: .top)
                            Text(card.subtitle).foregroundColor(Color.gray)
                            Spacer()
                        
                        Text(card.title).fontWeight(Font.Weight.bold).fixedSize(horizontal: false, vertical: true)
                    
                
                .padding([.top, .horizontal]).padding(isDetailed && card.showDetails ? [.top] : [] , 34)
            
                Image("placeholder-image").resizable().scaledToFit().frame(width: UIScreen.main.bounds.width - 60).padding(.bottom)
            
                if isDetailed && card.showDetails 
                    Text("Lorem ipsum ... ")
                
            
        
        .background(Color.green)
        .cornerRadius(16)
        .shadow(radius: 12)
        .padding(isDetailed && card.showDetails ? [] : [.top, .horizontal])
        .opacity(isDetailed && card.showDetails ? 1 : (!isDetailed ? 1 : 0))
        .edgesIgnoringSafeArea(.all)
    


struct Card  : Identifiable 
    public var id = UUID()
    public var title: String
    public var subtitle : String
    public var showDetails : Bool = false

struct ViewHeightKey: PreferenceKey 
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) 
    value += nextValue()
    

现在我想将 ContentView 和 CardView 中的两个(或至少一个)滚动视图更改为列表,因为延迟加载以获得更好的性能。但是更改 ContentView 中的 ScrollView 会导致动画出现故障。如果我将其更改为 CardView 中的列表,则不再显示任何卡片。

知道如何更改代码以使用列表吗?

【问题讨论】:

你能发帖ViewHeightKey,这样我就能得到你的代码。不过,如果您使用列表并枚举卡片,解决方案应该很简单,但请记住使用自定义绑定。 测试了将 ScrollView 更改为 List 并再次返回,我仍然没有看到任何故障,您能否在故障动画旁边重新编码预期的动画 你指的是像这样的缩放卡片imgur.com/a/f2T41Uy @cyden 您是否将闪烁称为故障?还是当您点击一个项目时它会比前一个项目增长时,列表类型会滚动?我只需将ScrollView 替换为List 并将.listStyle(PlainListStyle).listRowInsets(.init(top:0,leading:0,bottom:0,trailing:0) 添加到第一个vstack 即可将其转换为列表。此外,您不需要将卡片的滚动视图更改为列表,因为它没有加载任何内容。 .delay(1) 添加到.spring() 动画时可以看到故障。如果我错了,请纠正我,如果那不是故障。 【参考方案1】:

因此,正如 cmets 中所述,闪烁是 Apple 的错误。问题是您要先调整大小然后更改偏移量,因为如果您更改偏移量然后调整大小,当单元格重叠时会出现闪烁问题。我写了一个简单的测试,可以在这个pastebin

由于我的时间限制,我能够通过更改动画顺序来解决第 2/3/等卡会跳到顶部的奇怪滚动行为。希望今晚我会回到这篇文章,并通过修复动画首先发生的顺序来修复闪烁。他们必须被锁起来。

这是我的代码

init() 
        UITableView.appearance().separatorStyle = .none
        UITableView.appearance().backgroundColor = .clear
        UITableView.appearance().layoutMargins = .zero
        
        UITableViewCell.appearance().backgroundColor = .clear
        UITableViewCell.appearance().layoutMargins = .zero

...
        List 
                VStack 
                    if(!cards.isEmpty) 
                        ForEach(self.cards.indices, id: \.self)  index in
                            GeometryReader  reader in
                                CardView(card: self.$cards[index], isDetailed: self.$showDetails)
                                    // 1. Note I switched the order of offset and fixedsize (not that it will make difference in this step but its good practice
                                    // 2. I added animation in between
                                    .fixedSize(horizontal: false, vertical: !self.cards[index].showDetails)
                                    .animation(Animation.spring())
                                    .offset(y: self.cards[index].showDetails ? -reader.frame(in: .global).minY : 0)
                                    .animation(Animation.spring())

                                    .background(GeometryReader 
                                        Color.clear
                                            .preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
                                    )
                                    .onTapGesture 
                                       // Note I commented this out, it's the source of the bug.
//                                        withAnimation(Animation.spring().delay()) 
                                            self.cards[index].showDetails.toggle()
                                            self.showDetails.toggle()
//                                        
                                    
                                .zIndex(self.cards[index].showDetails ? 100 : -1)
                            
                            .frame(height: self.cards[index].showDetails ? UIScreen.main.bounds.height : self.heights[index], alignment: .center)
                            .onPreferenceChange(ViewHeightKey.self)  value in
                                self.heights[index] = value
                            
                        
                     else 
                        Text("Loading")
                    
                
                 // I added this for styling
                .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
            
             // I added this for styling
            .listStyle(PlainListStyle())
            .onAppear() 
                // load data
            

注意:但是,如果您在打开和关闭之间等待一两秒钟,您就不会注意到闪烁的行为。

注意2:这种链接行为也可以在App Store 中观察到,但它们完美地融合了动画。如果你注意你会发现一些延迟

编辑 1 在本次编辑中,我想指出之前的解决方案确实增强了动画效果,但它仍然存在闪烁问题,不幸的是,我们对此无能为力,除非我们可以在呈现和隐藏​​之间有延迟。甚至苹果公司也这样做了。如果您注意 ios 上的 App Store,您会注意到,当您尝试打开他们的一张卡片时,您将无法立即按“X”,您首先会注意到它没有响应,那是因为即使 Apple 也有在呈现和能够隐藏之间添加一些延迟。延迟基本上是一种解决方法。此外,您还可以注意到,当您点击卡片时,它不会立即出现。执行动画实际上需要一些时间,这又是一个增加延迟的技巧。

因此,为了解决闪烁问题,您可以添加一个简单的计时器,在允许用户关闭已打开的卡片或再次打开已关闭的卡片之前进行倒计时。此延迟可能是您的某些动画的持续时间,因此用户不会在您的应用中感到任何延迟。

在这里,我编写了一个简单的代码来演示如何做到这一点。但请注意,动画或延迟的混合并不完美,可以进一步调整。我只是想证明有可能摆脱闪烁或至少将其减少很多。可能有比这更好的解决方案,但这就是我想出的:)

编辑 2: 在第二次测试中,我能够指出这种闪烁实际上正在发生,因为您的列表没有动画。因此,可以通过对List 应用动画来完成快速解决方案;请记住,Edit 1: 中的上述解决方案同样有效,但需要大量试验和错误,无需进一步等待,这是我的解决方案。

import SwiftUI

struct ***22: View 
    
    @State private var cards = [
        Card(title: "testA", subtitle: "subtitleA"),
        Card(title: "testB", subtitle: "subtitleB"),
        Card(title: "testC", subtitle: "subtitleC"),
        Card(title: "testD", subtitle: "subtitleD"),
        Card(title: "testE", subtitle: "subtitleE")
    ]
    
    @State private var showDetails: Bool = false
    @State private var heights = [Int: CGFloat]()
    
    init() 
        UITableView.appearance().separatorStyle = .none
        UITableView.appearance().backgroundColor = .clear
        UITableView.appearance().layoutMargins = .zero
        
        UITableViewCell.appearance().backgroundColor = .clear
        UITableViewCell.appearance().layoutMargins = .zero
    
    
    var body: some View 
        
        List 
            VStack 
                if(!cards.isEmpty) 
                    ForEach(self.cards.indices, id: \.self)  index in
                        GeometryReader  reader in
                            CardView(card: self.$cards[index], isDetailed: self.$showDetails)
                                .fixedSize(horizontal: false, vertical: !self.cards[index].showDetails)
                                .offset(y: self.cards[index].showDetails ? -reader.frame(in: .global).minY : 0)
                                .background(GeometryReader 
                                    Color.clear
                                        .preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
                                )
                                .onTapGesture 
                                    withAnimation(.spring()) 
                                        self.cards[index].showDetails.toggle()
                                        self.showDetails.toggle()
                                    
                            
                        
                        .frame(height: self.cards[index].showDetails ? UIScreen.main.bounds.height : self.heights[index], alignment: .center)
                            
                        .onPreferenceChange(ViewHeightKey.self)  value in
                            self.heights[index] = value
                        
                    
                 else 
                    Text("Loading")
                
            
            .animation(Animation.linear)
            .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
        
        .listStyle(PlainListStyle())
        .onAppear() 
            // load data
        
    



struct CardView : View 
    @Binding var card : Card
    @Binding var isDetailed : Bool
    var body: some View 
        VStack(alignment: .leading)
            ScrollView(showsIndicators: isDetailed && card.showDetails) 
                HStack (alignment: .center)
                    VStack (alignment: .leading)
                        HStack(alignment: .top)
                            Text(card.subtitle).foregroundColor(Color.gray)
                            Spacer()
                        
                        Text(card.title).fontWeight(Font.Weight.bold).fixedSize(horizontal: false, vertical: true)
                    
                
                .padding([.top, .horizontal]).padding(isDetailed && card.showDetails ? [.top] : [] , 34)
                
                Image("placeholder-image").resizable().scaledToFit().frame(width: UIScreen.main.bounds.width - 60).padding(.bottom)
                
                if isDetailed && card.showDetails 
                    Text("Lorem ipsum ... ")
                
            
        
        .background(Color.green)
        .cornerRadius(16)
        .shadow(radius: 12)
        .padding(isDetailed && card.showDetails ? [] : [.top, .horizontal])
        .opacity(isDetailed && card.showDetails ? 1 : (!isDetailed ? 1 : 0))
        .edgesIgnoringSafeArea(.all)
    


struct Card  : Identifiable, Hashable 
    public var id = UUID()
    public var title: String
    public var subtitle : String
    public var showDetails : Bool = false


struct ***22_Previews: PreviewProvider 
    static var previews: some View 
        ***22()
    


struct ViewHeightKey: PreferenceKey 
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) 
        value += nextValue()
    


【讨论】:

我很抱歉这是我的错误。我编辑了答案,现在它也包含了闪烁的解决方案。如果有兴趣,我希望能解决您的问题以及详细的解释!祝你好运!

以上是关于使用 List 而不是 ScrollView 和 ForEach的主要内容,如果未能解决你的问题,请参考以下文章

android如何使用listview而不是scrollview

使用外部 Scrollview 滚动 Tableview Cell 而不是它自己的滚动?

SwiftUI NavigationView 背景颜色

ScrollView 内容未填写 SwiftUI

如何为 ScrollView 设置图像而不是淡化边缘?

远程图像使用 List 正确加载,但在使用带有嵌入式 VStack 的 ScrollView 和 SwiftUI 时不加载