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

SwiftUI中如何获取遮罩的可见量或可见百分比?

解决方案

SwiftUI的mask修饰符本身没有直接提供获取可见百分比的API,不过我们可以通过计算刮擦路径的覆盖面积来实现需求。核心思路是:计算用户刮擦路径的总面积,和卡片总面积对比,当比例达到80%时,直接显示完整的内容视图。

具体实现步骤:

  1. 添加状态变量,控制内容显示状态、记录刮擦面积:
@State private var isRevealed = false
@State private var scratchArea: CGFloat = 0
private let totalCardArea = 250 * 250 // 卡片固定总面积
private let revealThreshold: CGFloat = 0.8 // 80%显示阈值
  1. 实现刮擦面积计算方法:
    将用户拖拽生成的路径,转换成stroke对应的填充区域,通过CoreGraphics计算该区域的实际面积:
private func calculateScratchArea() -> CGFloat {
    guard !points.isEmpty else { return 0 }
    
    // 创建用户拖拽路径
    let dragPath = Path { path in
        path.addLines(points)
    }
    
    // 将stroke路径转换为可填充的轮廓路径
    guard let strokeContourPath = dragPath.stroke(
        StrokeStyle(lineWidth: 50, lineCap: .round, lineJoin: .round),
        fill: .color(.black)
    ).cgPath else {
        return 0
    }
    
    let boundingBox = strokeContourPath.boundingBoxOfPath
    // 创建临时上下文统计填充像素
    guard let context = CGContext(
        data: nil,
        width: Int(boundingBox.width),
        height: Int(boundingBox.height),
        bitsPerComponent: 8,
        bytesPerRow: Int(boundingBox.width) * 4,
        space: CGColorSpaceCreateDeviceRGB(),
        bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
    ) else {
        return 0
    }
    
    context.setFillColor(UIColor.black.cgColor)
    context.addPath(strokeContourPath)
    context.fillPath()
    
    // 统计填充像素数量,转换为实际面积
    let pixelData = context.data!.bindMemory(to: UInt8.self, capacity: Int(boundingBox.width) * Int(boundingBox.height) * 4)
    var filledPixels = 0
    for i in 0..<Int(boundingBox.width) * Int(boundingBox.height) {
        if pixelData[i * 4] == 255 {
            filledPixels += 1
        }
    }
    
    let pixelScaleX = boundingBox.width / CGFloat(Int(boundingBox.width))
    let pixelScaleY = boundingBox.height / CGFloat(Int(boundingBox.height))
    return CGFloat(filledPixels) * pixelScaleX * pixelScaleY
}
  1. 修改视图结构,添加阈值判断:
    当刮擦面积占比达到阈值时,直接显示完整的黄色内容;否则保持刮擦交互逻辑:
struct ScratchCardView: View {
    @State var points = [CGPoint]()
    @State private var isRevealed = false
    @State private var scratchArea: CGFloat = 0
    private let totalCardArea = 250 * 250
    private let revealThreshold: CGFloat = 0.8

    var body: some View {
        ZStack {
            if isRevealed {
                // 完全显示内容
                RoundedRectangle(cornerRadius: 20)
                    .fill(.yellow)
                    .frame(width: 250, height: 250)
            } else {
                // 刮擦覆盖层
                RoundedRectangle(cornerRadius: 20)
                    .fill(.red)
                    .frame(width: 250, height: 250)

                // 隐藏的内容视图
                RoundedRectangle(cornerRadius: 20)
                    .fill(.yellow)
                    .frame(width: 250, height: 250)
                    .mask(
                        Path { path in
                            path.addLines(points)
                        }.stroke(style: StrokeStyle(lineWidth: 50, lineCap: .round, lineJoin: .round))
                    )
                    .gesture(
                        DragGesture(minimumDistance: 0, coordinateSpace: .local)
                            .onChanged({ value in
                                points.append(value.location)
                            })
                            .onEnded({ _ in
                                scratchArea = calculateScratchArea()
                                if scratchArea / CGFloat(totalCardArea) >= revealThreshold {
                                    isRevealed = true
                                }
                            })
                    )
            }
        }
    }

    // 面积计算方法
    private func calculateScratchArea() -> CGFloat {
        // 实现代码同上
        guard !points.isEmpty else { return 0 }
        
        let dragPath = Path { path in
            path.addLines(points)
        }
        
        guard let strokeContourPath = dragPath.stroke(
            StrokeStyle(lineWidth: 50, lineCap: .round, lineJoin: .round),
            fill: .color(.black)
        ).cgPath else {
            return 0
        }
        
        let boundingBox = strokeContourPath.boundingBoxOfPath
        guard let context = CGContext(
            data: nil,
            width: Int(boundingBox.width),
            height: Int(boundingBox.height),
            bitsPerComponent: 8,
            bytesPerRow: Int(boundingBox.width) * 4,
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
        ) else {
            return 0
        }
        
        context.setFillColor(UIColor.black.cgColor)
        context.addPath(strokeContourPath)
        context.fillPath()
        
        let pixelData = context.data!.bindMemory(to: UInt8.self, capacity: Int(boundingBox.width) * Int(boundingBox.height) * 4)
        var filledPixels = 0
        for i in 0..<Int(boundingBox.width) * Int(boundingBox.height) {
            if pixelData[i * 4] == 255 {
                filledPixels += 1
            }
        }
        
        let pixelScaleX = boundingBox.width / CGFloat(Int(boundingBox.width))
        let pixelScaleY = boundingBox.height / CGFloat(Int(boundingBox.height))
        return CGFloat(filledPixels) * pixelScaleX * pixelScaleY
    }
}

注意事项:

  • 面积计算有一定性能开销,建议只在拖拽结束时计算;若需实时反馈,可每隔N次拖拽事件计算一次。
  • 若卡片尺寸为动态值,需通过GeometryReader获取实际尺寸,替换固定的totalCardArea。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 00:01:05