我正试图在SwiftUI中重现苹果的节日灯光形象(来自苹果印度网站的截图).预期结果:

Apple India Diwali logo

到目前为止,我做到了以下几点:

enter image description here

到目前为止,我的理解是:图像不是形状,所以我们不能描边,但我也发现shade()修饰符可以很好地在图像边框上放置阴影.因此,我需要一种方法来定制阴影,并了解它是如何工作的.

到目前为止我try 的内容:除了上面的代码,我还try 使用Vision框架的轮廓检测将给定的SF符号转换为Shape,但没有成功,这是基于我对本文的理解:https://www.iosdevie.com/p/new-in-ios-14-vision-contour-detection

有没有人可以指导我如何go 做这件事,最好只使用科幻符号.

推荐答案

我们可以使用Vision框架和VNDetectContourRequestRevision1来获得cgPath:

func detectVisionContours(from sourceImage: UIImage) -> CGPath? {
    
    let inputImage = CIImage.init(cgImage: sourceImage.cgImage!)
    
    let contourRequest = VNDetectContoursRequest.init()
    contourRequest.revision = VNDetectContourRequestRevision1
    contourRequest.contrastAdjustment = 1.0
    contourRequest.maximumImageDimension = 512
    
    let requestHandler = VNImageRequestHandler.init(ciImage: inputImage, options: [:])
    try! requestHandler.perform([contourRequest])
    if let contoursObservation = contourRequest.results?.first {
        return contoursObservation.normalizedPath
    }
    
    return nil
}

路径将基于0,0 1.0,1.0个坐标空间,因此要使用它,我们需要将路径zoom 到所需的大小.它还使用反转的Y坐标,因此我们还需要将其翻转:

    // cgPath returned from Vision will be in rect 0,0 1.0,1.0 coordinates
    //  so we want to scale the path to our view bounds
    let scW: CGFloat = targetRect.bounds.width / cgPth.boundingBox.width
    let scH: CGFloat = targetRect.bounds.height / cgPth.boundingBox.height
    
    // we need to invert the Y-coordinate space
    var transform = CGAffineTransform.identity
        .scaledBy(x: scW, y: -scH)
        .translatedBy(x: 0.0, y: -cgPth.boundingBox.height)
    
    return cgPth.copy(using: &transform)

几张纸条.

当使用UIImage(systemName: "applelogo")时,我们得到一个具有"字体"特征的图像--即空白.请看这个https://stackoverflow.com/a/71743787/6257435和这个https://stackoverflow.com/a/66293917/6257435来进行一些讨论.

因此,我们100直接使用它,但它使得路径zoom 和转换有点复杂.

因此,与其使用这个"默认设置",不如:

enter image description here

我们可以使用一小段代码来"修剪"空间,以获得更可用的图像:

enter image description here

然后,我们可以使用从Vision开始的路径作为CAShapeLayer的路径,以及这些层属性:.lineCap = .round/.lineWidth = 8/.lineDashPattern = [2.0, 20.0](例如),以获得"虚线"笔触:

enter image description here

然后,我们可以在形状层上使用相同的路径作为渐变层上的蒙版:

enter image description here

最后,删除图像视图,以便我们只看到带有遮罩渐变层的视图:

enter image description here

以下是生成该代码的示例代码:

import UIKit
import Vision

class ViewController: UIViewController {
    
    let myOutlineView = UIView()
    let myGradientView = UIView()
    let shapeLayer = CAShapeLayer()
    let gradientLayer = CAGradientLayer()

    let defaultImageView = UIImageView()
    let trimmedImageView = UIImageView()

    var defaultImage: UIImage!
    var trimmedImage: UIImage!
    
    var visionPath: CGPath!

    // an information label
    let infoLabel: UILabel = {
        let v = UILabel()
        v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        v.textAlignment = .center
        v.numberOfLines = 0
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBlue
        
        // get the system image at 240-points (so we can get a good path from Vision)
        //  experiment with different sizes if the path doesn't appear smooth
        let cfg = UIImage.SymbolConfiguration(pointSize: 240.0)
        
        // get "applelogo" symbol
        guard let imgA = UIImage(systemName: "applelogo", withConfiguration: cfg)?.withTintColor(.black, renderingMode: .alwaysOriginal) else {
            fatalError("Could not load SF Symbol: applelogo!")
        }
        // now render it on a white background
        self.defaultImage = UIGraphicsImageRenderer(size: imgA.size).image { ctx in
            UIColor.white.setFill()
            ctx.fill(CGRect(origin: .zero, size: imgA.size))
            imgA.draw(at: .zero)
        }

        // we want to "strip" the bounding box empty space
        // get a cgRef from imgA
        guard let cgRef = imgA.cgImage else {
            fatalError("Could not get cgImage!")
        }
        // create imgB from the cgRef
        let imgB = UIImage(cgImage: cgRef, scale: imgA.scale, orientation: imgA.imageOrientation)
            .withTintColor(.black, renderingMode: .alwaysOriginal)
        
        // now render it on a white background
        self.trimmedImage = UIGraphicsImageRenderer(size: imgB.size).image { ctx in
            UIColor.white.setFill()
            ctx.fill(CGRect(origin: .zero, size: imgB.size))
            imgB.draw(at: .zero)
        }

        defaultImageView.image = defaultImage
        defaultImageView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(defaultImageView)

        trimmedImageView.image = trimmedImage
        trimmedImageView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(trimmedImageView)
        
        myOutlineView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myOutlineView)
        
        myGradientView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myGradientView)
        
        // next step button
        let btn = UIButton()
        btn.setTitle("Next Step", for: [])
        btn.setTitleColor(.white, for: .normal)
        btn.setTitleColor(.lightGray, for: .highlighted)
        btn.backgroundColor = .systemRed
        btn.layer.cornerRadius = 8
        
        btn.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(btn)
        
        infoLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(infoLabel)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            // inset default image view 20-points on each side
            //  height proportional to the image
            //  near the top
            defaultImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            defaultImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            defaultImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            defaultImageView.heightAnchor.constraint(equalTo: defaultImageView.widthAnchor, multiplier: defaultImage.size.height / defaultImage.size.width),
            
            // inset trimmed image view 40-points on each side
            //  height proportional to the image
            //  centered vertically
            trimmedImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            trimmedImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            trimmedImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            trimmedImageView.heightAnchor.constraint(equalTo: trimmedImageView.widthAnchor, multiplier: self.trimmedImage.size.height / self.trimmedImage.size.width),
            
            // add outline view on top of trimmed image view
            myOutlineView.topAnchor.constraint(equalTo: trimmedImageView.topAnchor, constant: 0.0),
            myOutlineView.leadingAnchor.constraint(equalTo: trimmedImageView.leadingAnchor, constant: 0.0),
            myOutlineView.trailingAnchor.constraint(equalTo: trimmedImageView.trailingAnchor, constant: 0.0),
            myOutlineView.bottomAnchor.constraint(equalTo: trimmedImageView.bottomAnchor, constant: 0.0),
            
            // add gradient view on top of trimmed image view
            myGradientView.topAnchor.constraint(equalTo: trimmedImageView.topAnchor, constant: 0.0),
            myGradientView.leadingAnchor.constraint(equalTo: trimmedImageView.leadingAnchor, constant: 0.0),
            myGradientView.trailingAnchor.constraint(equalTo: trimmedImageView.trailingAnchor, constant: 0.0),
            myGradientView.bottomAnchor.constraint(equalTo: trimmedImageView.bottomAnchor, constant: 0.0),
            
            // button and info label below
            btn.topAnchor.constraint(equalTo: defaultImageView.bottomAnchor, constant: 20.0),
            btn.leadingAnchor.constraint(equalTo: trimmedImageView.leadingAnchor, constant: 0.0),
            btn.trailingAnchor.constraint(equalTo: trimmedImageView.trailingAnchor, constant: 0.0),

            infoLabel.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0),
            infoLabel.leadingAnchor.constraint(equalTo: trimmedImageView.leadingAnchor, constant: 0.0),
            infoLabel.trailingAnchor.constraint(equalTo: trimmedImageView.trailingAnchor, constant: 0.0),
            infoLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 60.0),
            
        ])
        
        // setup the shape layer
        shapeLayer.strokeColor = UIColor.red.cgColor
        shapeLayer.fillColor = UIColor.clear.cgColor
        
        // this will give use round dots for the shape layer's stroke
        shapeLayer.lineCap = .round
        shapeLayer.lineWidth = 8
        shapeLayer.lineDashPattern = [2.0, 20.0]
        
        // setup the gradient layer
        let c1: UIColor = .init(red: 0.95, green: 0.73, blue: 0.32, alpha: 1.0)
        let c2: UIColor = .init(red: 0.95, green: 0.25, blue: 0.45, alpha: 1.0)
        gradientLayer.colors = [c1.cgColor, c2.cgColor]
        
        myOutlineView.layer.addSublayer(shapeLayer)
        myGradientView.layer.addSublayer(gradientLayer)

        btn.addTarget(self, action: #selector(nextStep), for: .touchUpInside)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    
        guard let pth = pathSetup()
        else {
            fatalError("Vision could not create path")
        }
        self.visionPath = pth
        
        shapeLayer.path = pth
        
        gradientLayer.frame = myGradientView.bounds.insetBy(dx: -8.0, dy: -8.0)
        let gradMask = CAShapeLayer()
        gradMask.strokeColor = UIColor.red.cgColor
        gradMask.fillColor = UIColor.clear.cgColor
        gradMask.lineCap = .round
        gradMask.lineWidth = 8
        gradMask.lineDashPattern = [2.0, 20.0]
        
        gradMask.path = pth
        gradMask.position.x += 8.0
        gradMask.position.y += 8.0
        gradientLayer.mask = gradMask
        
        nextStep()
    }
    
    var idx: Int = -1
    
    @objc func nextStep() {
        idx += 1
        switch idx % 5 {
        case 1:
            defaultImageView.isHidden = true
            trimmedImageView.isHidden = false
            infoLabel.text = "\"applelogo\" system image - with trimmed empty-space bounding-box."
        case 2:
            myOutlineView.isHidden = false
            shapeLayer.opacity = 1.0
            infoLabel.text = "Dotted outline shape using Vision detected path."
        case 3:
            myOutlineView.isHidden = true
            myGradientView.isHidden = false
            infoLabel.text = "Use Dotted outline shape as a gradient layer mask."
        case 4:
            trimmedImageView.isHidden = true
            view.backgroundColor = .black
            infoLabel.text = "View by itself with Dotted outline shape as a gradient layer mask."
        default:
            view.backgroundColor = .systemBlue
            defaultImageView.isHidden = false
            trimmedImageView.isHidden = true
            myOutlineView.isHidden = true
            myGradientView.isHidden = true
            shapeLayer.opacity = 0.0
            infoLabel.text = "Default \"applelogo\" system image - note empty-space bounding-box."
        }
    }

    func pathSetup() -> CGPath? {
        // get the cgPath from the image
        guard let cgPth = detectVisionContours(from: self.trimmedImage)
        else {
            print("Failed to get path!")
            return nil
        }
        
        // cgPath returned from Vision will be in rect 0,0 1.0,1.0 coordinates
        //  so we want to scale the path to our view bounds
        let scW: CGFloat = myOutlineView.bounds.width / cgPth.boundingBox.width
        let scH: CGFloat = myOutlineView.bounds.height / cgPth.boundingBox.height
        
        // we need to invert the Y-coordinate space
        var transform = CGAffineTransform.identity
            .scaledBy(x: scW, y: -scH)
            .translatedBy(x: 0.0, y: -cgPth.boundingBox.height)
        
        return cgPth.copy(using: &transform)
    }
    
    func detectVisionContours(from sourceImage: UIImage) -> CGPath? {
        
        let inputImage = CIImage.init(cgImage: sourceImage.cgImage!)
        
        let contourRequest = VNDetectContoursRequest.init()
        contourRequest.revision = VNDetectContourRequestRevision1
        contourRequest.contrastAdjustment = 1.0
        contourRequest.maximumImageDimension = 512
        
        let requestHandler = VNImageRequestHandler.init(ciImage: inputImage, options: [:])
        try! requestHandler.perform([contourRequest])
        if let contoursObservation = contourRequest.results?.first {
            return contoursObservation.normalizedPath
        }
        
        return nil
    }
}

Ios相关问答推荐

Flutter应用程序无法使用IOS/Swift的蓝牙核心库发现某些外围设备

preferredCompactColumn作为NavigationSplitViewColumn.sidebar传递,但仍能在iOS上看到详细信息页面

不使用iOS 17修改器修改SWIFT用户界面视图

在相机动作之前快速防止场景视点重置

如何根据SWIFT中的按钮点击更新文本值

在flutter中使用flatter_screenutil这样的软件包的目的是什么?

当虚拟对象附加到其网格时,如何刷新创建 ARView 的 SwiftUI UIViewRepresentable?

如何在用户点击的位置使用SceneKit渲染球体?

Toast 不显示 Flutter

如何在 SwiftUI 中同时使用 LongPressGesture 和 ScrollView 中的滚动?

为什么 Swift 不在启动的同一线程上恢复异步函数?

在 Swift 中 @MainActor 似乎被 withCheckedContinuation 而不是其他异步调用继承是巧合吗?

Apple Push Service 证书不受信任

setNeedsLayout 和 setNeedsDisplay

每个版本的 iOS 都附带什么版本的移动 Safari?

使用 swift 从应用程序委托打开视图控制器

用 NavigationViewController 快速呈现 ViewController

以编程方式确定 iPhone 是否越狱

更改标签栏项目图像和文本 colored颜色 iOS

错误 itms-90035 - Xcode