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

SwiftUI动态检测子视图位置变化:判断矩形何时越过lineView

无需绝对位置,实时检测SwiftUI视图交叉的方案

要实现不依赖绝对位置、实时检测移动矩形越过lineView的需求,可以通过PreferenceKey传递视图几何信息结合GeometryReader来实现,核心思路是获取两个视图在同一坐标系下的Frame并实时对比。

实现步骤

  1. 定义两个PreferenceKey,分别用于传递矩形和lineView的Frame信息
  2. 给目标视图添加背景用的GeometryReader,将自身Frame通过PreferenceKey传递出去
  3. 在父视图中监听Frame变化,实时对比两个视图的位置关系,判断是否越过

修改后的完整代码

import SwiftUI

// 用于传递lineView的Frame
struct LineFrameKey: PreferenceKey {
    static var defaultValue: CGRect = .zero
    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}

// 用于传递移动矩形的Frame
struct BlockFrameKey: PreferenceKey {
    static var defaultValue: CGRect = .zero
    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}

struct ColorView: View {
    private let blockWidth = 40.0
    private let blockHeight = 40.0
    @State private var blockYPos = 0.0
    private let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
    
    // 存储两个视图的Frame,用于实时对比
    @State private var lineFrame: CGRect = .zero
    @State private var blockFrame: CGRect = .zero
    // 标记是否已经越过lineView,避免重复触发逻辑
    @State private var hasCrossedLine = false
    
    var body: some View {
        GeometryReader { geometry in
            ZStack {
                // 移动的矩形:添加GeometryReader传递自身Frame
                Rectangle()
                    .frame(width: blockWidth, height: blockHeight)
                    .position(x: geometry.size.width/2, y: blockYPos)
                    .background(
                        GeometryReader { geo in
                            Color.clear
                                // 获取相对于ZStack的本地坐标Frame
                                .preference(key: BlockFrameKey.self, value: geo.frame(in: .local))
                        }
                    )
                
                VStack {
                    HStack {
                        scribble
                        Spacer()
                        close
                    }
                    .font(.title)
                    .frame(height: 40)
                    
                    palleteView
                    
                    // lineView:添加GeometryReader传递自身Frame
                    lineView
                    
                    Spacer()
                }
            }
            .background {
                Color.yellow.ignoresSafeArea()
            }
            .onAppear {
                blockYPos = geometry.size.height - blockHeight
            }
            .onReceive(timer) { _ in
                // 未越过时才继续移动
                if !hasCrossedLine {
                    blockYPos -= 5
                }
            }
            // 监听lineView的Frame变化
            .onPreferenceChange(LineFrameKey.self) { newFrame in
                lineFrame = newFrame
            }
            // 监听矩形Frame变化,实时判断是否越过
            .onPreferenceChange(BlockFrameKey.self) { newFrame in
                blockFrame = newFrame
                
                // 逻辑说明:矩形向上移动,当矩形的顶部(minY)小于等于lineView的底部(maxY)时,视为越过
                // 可根据实际需求调整判断条件(比如判断矩形底部与line顶部的关系)
                let blockTop = blockFrame.minY
                let lineBottom = lineFrame.maxY
                
                if blockTop <= lineBottom && !hasCrossedLine {
                    hasCrossedLine = true
                    print("矩形已越过lineView")
                    // 这里可以添加越过触发后的逻辑,比如停止定时器
                    timer.upstream.connect().cancel()
                }
            }
        }
    }
    
    private var lineView: some View {
        Capsule()
            .frame(height: 2)
            .foregroundStyle(Color.red)
            .padding(.horizontal)
            .background(
                GeometryReader { geo in
                    Color.clear
                        // 获取相对于ZStack的本地坐标Frame
                        .preference(key: LineFrameKey.self, value: geo.frame(in: .local))
                }
            )
    }
    
    private var close: some View {
        Image(systemName: "xmark.circle.fill")
            .padding(.trailing, 8)
    }
    
    private var scribble: some View {
        Image(systemName: "scribble.variable")
            .padding(.leading, 8)
    }
    
    private var palleteView: some View {
        HStack(alignment: .center) {
            Color.green.frame(width: blockWidth, height: blockHeight)
            Color.blue.frame(width: blockWidth, height: blockHeight)
            Color.orange.frame(width: blockWidth, height: blockHeight)
            Color.indigo.frame(width: blockWidth, height: blockHeight)
            Color.mint.frame(width: blockWidth, height: blockHeight)
        }
    }
}

关键说明

  • 使用.local坐标系:确保两个视图的Frame基于同一个父容器(ZStack)计算,避免全局坐标系带来的安全区、屏幕尺寸等干扰
  • PreferenceKey的作用:在SwiftUI视图层级中安全传递几何数据,无需硬编码绝对位置
  • 状态标记hasCrossedLine:防止重复触发越过逻辑,可根据需求调整重置逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 05:34:51