0

I'm trying for hours to draw a gradient border on an NSView, like this.

I've adjusted to code to NSView:

class BlurView: NSView {
  
  override init(frame: CGRect) {
    super.init(frame: frame)

    let gradient = CAGradientLayer()
    gradient.frame =  CGRect(origin: CGPoint.zero, size: frame.size)
    gradient.colors = [NSColor.blue.cgColor, NSColor.green.cgColor, NSColor.blue.cgColor]

    let shape = CAShapeLayer()
    shape.lineWidth = 2
    shape.path = NSBezierPath(rect: self.bounds).cgPath
    shape.strokeColor = NSColor.black.cgColor
    shape.fillColor = NSColor.clear.cgColor
    gradient.mask = shape
    
    self.wantsLayer = true
    self.layer?.insertSublayer(gradient, at: 0)
  }
}

However it doesn't work, I cannot find more info online, so maybe this is only working for iOS. A couple of things of interest:

  • self.bounds prints (0.0, 0.0, 0.0, 0.0) so I don't think that is working properly
  • I added the cgPath as an extension on NSBeziertPath with the following code
import Foundation

extension NSBezierPath {

    public var cgPath: CGPath {
        let path = CGMutablePath()
        var points = [CGPoint](repeating: .zero, count: 3)

        for i in 0 ..< elementCount {
            let type = element(at: i, associatedPoints: &points)
            switch type {
            case .moveTo:
                path.move(to: points[0])
            case .lineTo:
                path.addLine(to: points[0])
            case .curveTo:
                path.addCurve(to: points[2], control1: points[0], control2: points[1])
            case .closePath:
                path.closeSubpath()
            @unknown default:
                continue
            }
        }

        return path
    }
}

Any idea how to get this working?

HangarRash
  • 7,314
  • 5
  • 5
  • 32
Oscar Franco
  • 5,691
  • 5
  • 34
  • 56
  • I tried your code and it works for me. How is the `BlurView` instantiated and does the frame change later on? – Willeke Apr 23 '23 at 12:49

1 Answers1

0

I got this working. The problem was not how I rendered the border but how the view was managed. This is a component embedded in react-native, therefore it is initialized with a 0 size and only gets updated later on. So the callback to render the border needed to be in an update method:

import Cocoa

let blue = CGColor(red: 0, green: 98.0/255.0, blue: 255.0/255.0, alpha: 0.2)
let blue2 = CGColor(red: 0, green: 98.0/255.0, blue: 255.0/255.0, alpha: 0.1)
let green = CGColor(red: 63.0/255.0, green: 255.0/255.0, blue: 128.0/255.0, alpha: 0.2)
let blueBg1 = CGColor(red: 0, green: 98.0/255.0, blue: 255.0/255.0, alpha: 0.03)
let blueBg2 = CGColor(red: 0, green: 98.0/255.0, blue: 255.0/255.0, alpha: 0.08)

class BlurView: NSVisualEffectView {
  
  var borderGradient: CAGradientLayer!
  var bgGradient: CAGradientLayer!
  
  override var isFlipped: Bool {
    return true
  }
  
  
  override var frame: CGRect {
      didSet {
        let gradient = CAGradientLayer()
        gradient.frame =  CGRect(origin: CGPoint.zero, size: self.frame.size)
        gradient.colors = [blue2, blue, blue2]
        gradient.apply(angle: 90)

        let shape = CAShapeLayer()
        shape.lineWidth = 2
        
        shape.path = NSBezierPath(roundedRect: self.bounds, xRadius: 10, yRadius: 10).cgPath
        shape.strokeColor = NSColor.black.cgColor
        shape.fillColor = NSColor.clear.cgColor
        gradient.mask = shape
        
        self.layer?.replaceSublayer(borderGradient, with: gradient)
        borderGradient = gradient
        
        let gradient2 = CAGradientLayer()
        gradient2.frame =  CGRect(origin: CGPoint.zero, size: self.frame.size)
        gradient2.colors = [blueBg1, blueBg2]
        gradient2.apply(angle: 135)
        
        self.layer?.replaceSublayer(bgGradient, with: gradient2)
        bgGradient = gradient2
        
        
      }
  }
  
  
  override init(frame: CGRect) {
    super.init(frame: frame)

    self.material = .sidebar
    self.wantsLayer = true
    self.layer?.cornerRadius = 10.0
    self.layer?.masksToBounds = true
    
    let gradient = CAGradientLayer()
    gradient.frame =  CGRect(origin: CGPoint.zero, size: self.frame.size)
    gradient.colors = [blue, green, blue]
    gradient.apply(angle: 90)

    let shape = CAShapeLayer()
    shape.lineWidth = 2
    
    shape.path = NSBezierPath(roundedRect: self.bounds, xRadius: 10, yRadius: 10).cgPath
    shape.strokeColor = NSColor.black.cgColor
    shape.fillColor = NSColor.clear.cgColor
    gradient.mask = shape
    
    self.layer?.addSublayer(gradient)
    borderGradient = gradient
    
    let gradient2 = CAGradientLayer()
    gradient2.frame =  CGRect(origin: CGPoint.zero, size: self.frame.size)
    gradient2.colors = [blueBg1, blueBg2]
    gradient2.apply(angle: 135)
    
    self.layer?.addSublayer(gradient2)
    bgGradient = gradient2
  }
  
  

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
}

@objc (BlurViewManager)
class BlurViewManager: RCTViewManager {

  override static func requiresMainQueueSetup() -> Bool {
    return true
  }

  override func view() -> NSView! {
    return BlurView()
  }

}
Oscar Franco
  • 5,691
  • 5
  • 34
  • 56