使用 List 而不是 ScrollView 和 ForEach

Using a List instead of a ScrollView and ForEach

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

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: [=10=].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 中的两个(或至少一个)Scrollview 更改为列表,因为延迟加载以获得更好的性能。但是更改 ContentView 中的 ScrollView 会导致动画出现故障。如果我将其更改为 CardView 中的列表,则不会再显示任何卡片。

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

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

中找到

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

这是我的代码

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: [=10=].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 Whosebug22: 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: [=11=].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 Whosebug22_Previews: PreviewProvider {
    static var previews: some View {
        Whosebug22()
    }
}

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