使用列表而不是ScrollView和ForEach

时间:2020-07-05 16:29:56

标签: foreach swiftui lazy-loading swiftui-list

我有以下代码来显示一些卡片,如果用户点击其中之一,它们会扩展到全屏显示:

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()
    }
}

此代码产生了effect

现在,由于延迟加载以获得更好的性能,我想将ContentView和CardView中的两个(或至少一个)滚动视图更改为列表。但是在ContentView中更改ScrollView会导致glitched animations。而且,如果我将其更改为CardView中的列表,则什至没有卡片出现。

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

2 个答案:

答案 0 :(得分:1)

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

中找到

由于时间的限制,我能够解决奇怪的滚动行为,即通过更改动画顺序,第二张/第三张/等...卡会跳到顶部。我希望今晚能回到这篇文章,并通过确定动画首先发生的顺序来解决闪烁问题。它们必须被锁住。

这是我的代码,

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 StackOverflow22: 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 StackOverflow22_Previews: PreviewProvider {
    static var previews: some View {
        StackOverflow22()
    }
}

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

答案 1 :(得分:0)

它不起作用可能是因为要在ScrollView中添加List。尝试避免这种情况,只添加不带ScrollView的列表。