You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中forEach循环仅最后一张照片可删除的问题排查

问题:仅最后一张照片的删除按钮有效,其余无效的原因及解决办法

通过ForEach循环展示照片列表,每张照片右上角设置删除按钮,但测试发现只有最后一张照片的删除按钮能正常工作,其余按钮点击无响应。相关代码如下:

@State private var cards: [Card] = []
   
@State private var card: Card = Card(id:UUID(),photoData:Data())
        
ScrollView(.vertical) {
  VStack(spacing: 15) {
    /// Parallax Carousel
    GeometryReader(content: { geometry in
      let size = geometry.size

      ScrollView(.horizontal) {
        HStack(spacing: 5) {
          ForEach(cards) { card1 in
            GeometryReader(content: { proxy in
              let cardSize = proxy.size
              let minX = min((proxy.frame(in: .scrollView).minX - 30.0) * 1.4, size.width * 1.4)

              ZStack(alignment: .topTrailing) {
                Image(uiImage: UIImage(data: card1.photoData) ?? UIImage(named: "photoForNil")!)
                  .resizable()
                  .aspectRatio(contentMode: .fill)
                  .offset(x: -minX)
                  .frame(width: proxy.size.width * 2.5)
                  .frame(width: cardSize.width, height: cardSize.height)
                  .clipShape(.rect(cornerRadius: 15))
                  .shadow(color: .black.opacity(0.25), radius: 8, x: 5, y: 10)

                Button {
                  cards.removeAll(where: {
                    $0.id == card1.id
                  })
                } label: {
                  Image(systemName: "multiply")
                    .frame(width: 20, height: 20)
                    .foregroundColor(.white)
                }
              }
            })
            .frame(width: size.width - 60, height: size.height - 50)
            .scrollTransition(.interactive, axis: .horizontal) { view, phase in
              view
                .scaleEffect(phase.isIdentity ? 1 : 0.95)
            }
          }
        }
        .padding(.horizontal, 30)
        .scrollTargetLayout()
        .frame(height: size.height, alignment: .top)
      }
      .scrollTargetBehavior(.viewAligned)
      .scrollIndicators(.hidden)
    })
    .frame(height: 500)
    .padding(.horizontal, -15)
    .padding(.top, 10)
  }
}

Card结构体定义:

import SwiftUI

///Card Model
struct Card: Identifiable, Hashable {
    var id: UUID = .init()
    var photoData: Data = Data()
    
    /// View Based Properties
    var scrollPosition: UUID?
}

原因分析

问题根源在于**ForEach内部嵌套GeometryReader并结合scrollTargetLayout使用时,SwiftUI的视图身份追踪机制被干扰**。

当使用scrollTargetLayout和scrollTargetBehavior(.viewAligned)时,滚动容器会复用视图来优化性能。而嵌套的GeometryReader作为ForEach的直接子视图,导致SwiftUI无法正确关联每个按钮对应的Card实例——闭包捕获的card1在视图复用时会指向错误的实例,最终只有最后一个未被复用的视图能正确触发删除动作。

尽管Card实现了Identifiable,但复杂的视图嵌套结构破坏了SwiftUI对每个循环项视图的唯一性识别,使得按钮的动作闭包无法正确绑定到对应的Card数据。


解决方案

将ForEach内的卡片内容提取为独立的子视图,让SwiftUI能清晰追踪每个视图的身份,避免复用导致的闭包捕获错误。

步骤1:创建独立的卡片子视图

struct CardItemView: View {
    let card: Card
    let parentSize: CGSize
    @Binding var cards: [Card]
    
    var body: some View {
        GeometryReader(content: { proxy in
            let cardSize = proxy.size
            let minX = min((proxy.frame(in: .scrollView).minX - 30.0) * 1.4, parentSize.width * 1.4)
            
            ZStack(alignment: .topTrailing) {
                Image(uiImage: UIImage(data: card.photoData) ?? UIImage(named: "photoForNil")!)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .offset(x: -minX)
                    .frame(width: proxy.size.width * 2.5)
                    .frame(width: cardSize.width, height: cardSize.height)
                    .clipShape(.rect(cornerRadius: 15))
                    .shadow(color: .black.opacity(0.25), radius: 8, x: 5, y: 10)
                
                Button {
                    cards.removeAll(where: { $0.id == card.id })
                } label: {
                    Image(systemName: "multiply")
                        .frame(width: 20, height: 20)
                        .foregroundColor(.white)
                }
            }
        })
        .frame(width: parentSize.width - 60, height: parentSize.height - 50)
        .scrollTransition(.interactive, axis: .horizontal) { view, phase in
            view.scaleEffect(phase.isIdentity ? 1 : 0.95)
        }
    }
}

步骤2:替换原视图中的ForEach代码

将原ForEach内部的嵌套代码替换为子视图调用:

// 原ForEach部分替换为:
ForEach(cards) { card1 in
    CardItemView(card: card1, parentSize: size, cards: $cards)
}

修改后的完整视图代码

@State private var cards: [Card] = []
   
@State private var card: Card = Card(id:UUID(),photoData:Data())
        
ScrollView(.vertical) {
  VStack(spacing: 15) {
    /// Parallax Carousel
    GeometryReader(content: { geometry in
      let size = geometry.size

      ScrollView(.horizontal) {
        HStack(spacing: 5) {
          ForEach(cards) { card1 in
            CardItemView(card: card1, parentSize: size, cards: $cards)
          }
        }
        .padding(.horizontal, 30)
        .scrollTargetLayout()
        .frame(height: size.height, alignment: .top)
      }
      .scrollTargetBehavior(.viewAligned)
      .scrollIndicators(.hidden)
    })
    .frame(height: 500)
    .padding(.horizontal, -15)
    .padding(.top, 10)
  }
}

// 子视图定义
struct CardItemView: View {
    let card: Card
    let parentSize: CGSize
    @Binding var cards: [Card]
    
    var body: some View {
        GeometryReader(content: { proxy in
            let cardSize = proxy.size
            let minX = min((proxy.frame(in: .scrollView).minX - 30.0) * 1.4, parentSize.width * 1.4)
            
            ZStack(alignment: .topTrailing) {
                Image(uiImage: UIImage(data: card.photoData) ?? UIImage(named: "photoForNil")!)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .offset(x: -minX)
                    .frame(width: proxy.size.width * 2.5)
                    .frame(width: cardSize.width, height: cardSize.height)
                    .clipShape(.rect(cornerRadius: 15))
                    .shadow(color: .black.opacity(0.25), radius: 8, x: 5, y: 10)
                
                Button {
                    cards.removeAll(where: { $0.id == card.id })
                } label: {
                    Image(systemName: "multiply")
                        .frame(width: 20, height: 20)
                        .foregroundColor(.white)
                }
            }
        })
        .frame(width: parentSize.width - 60, height: parentSize.height - 50)
        .scrollTransition(.interactive, axis: .horizontal) { view, phase in
            view.scaleEffect(phase.isIdentity ? 1 : 0.95)
        }
    }
}

内容的提问来源于stack exchange,提问作者Tracy9988 Xu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 04:03:12