CustomView выглядит странно в моем проекте, но хорош на игровой площадке

Итак, я создал пользовательский NSButton, чтобы иметь красивый переключатель, но я испытываю очень странную ошибку.

Моя радиокнопка хорошо выглядит на игровой площадке, но когда я добавляю ее в свой проект, она выглядит странно.

Вот скриншоты:

radio button radio button x400

Левая= на игровой площадке.
Правильно= в моем проекте.

Как вы можете видеть, в вправо (в моем проекте) синяя точка выглядит ужасно, она не гладкая, то же самое для белого круга (он менее заметен с темным фоном).

В моем проекте NSShadow на моем CALayer также перевернуто, даже если свойство geometryFlipped на моем основном (_containerLayer_) CALayer установлено на true. → ФИКСИРОВАННЫЙ: см. ответ @Bannings.

import AppKit

extension NSColor {
    static func colorWithDecimal(deviceRed deviceRed: Int, deviceGreen: Int, deviceBlue: Int, alpha: Float) -> NSColor {
        return NSColor(
            deviceRed: CGFloat(Double(deviceRed)/255.0),
            green: CGFloat(Double(deviceGreen)/255.0),
            blue: CGFloat(Double(deviceBlue)/255.0),
            alpha: CGFloat(alpha)
        )
    }
}

extension NSBezierPath {

    var CGPath: CGPathRef {
        return self.toCGPath()
    }

    /// Transforms the NSBezierPath into a CGPathRef
    ///
    /// :returns: The transformed NSBezierPath
    private func toCGPath() -> CGPathRef {

        // Create path
        let path = CGPathCreateMutable()
        var points = UnsafeMutablePointer<NSPoint>.alloc(3)
        let numElements = self.elementCount

        if numElements > 0 {

            var didClosePath = true

            for index in 0..<numElements {

                let pathType = self.elementAtIndex(index, associatedPoints: points)

                switch pathType {

                case .MoveToBezierPathElement:
                    CGPathMoveToPoint(path, nil, points[0].x, points[0].y)
                case .LineToBezierPathElement:
                    CGPathAddLineToPoint(path, nil, points[0].x, points[0].y)
                    didClosePath = false
                case .CurveToBezierPathElement:
                    CGPathAddCurveToPoint(path, nil, points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y)
                    didClosePath = false
                case .ClosePathBezierPathElement:
                    CGPathCloseSubpath(path)
                    didClosePath = true
                }
            }

            if !didClosePath { CGPathCloseSubpath(path) }
        }

        points.dealloc(3)
        return path
    }
}

class RadioButton: NSButton {

    private var containerLayer: CALayer!
    private var backgroundLayer: CALayer!
    private var dotLayer: CALayer!
    private var hoverLayer: CALayer!

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.setupLayers(radioButtonFrame: CGRectZero)
    }

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        let radioButtonFrame = CGRect(
            x: 0,
            y: 0,
            width: frameRect.height,
            height: frameRect.height
        )
        self.setupLayers(radioButtonFrame: radioButtonFrame)
    }

    override func drawRect(dirtyRect: NSRect) {
    }

    private func setupLayers(radioButtonFrame radioButtonFrame: CGRect) {
        //// Enable view layer
        self.wantsLayer = true

        self.setupBackgroundLayer(radioButtonFrame)
        self.setupDotLayer(radioButtonFrame)
        self.setupHoverLayer(radioButtonFrame)
        self.setupContainerLayer(radioButtonFrame)
    }

    private func setupContainerLayer(frame: CGRect) {

        self.containerLayer = CALayer()
        self.containerLayer.frame = frame
        self.containerLayer.geometryFlipped = true

        //// Mask
        let mask = CAShapeLayer()
        mask.path = NSBezierPath(ovalInRect: frame).CGPath
        mask.fillColor = NSColor.blackColor().CGColor
        self.containerLayer.mask = mask

        self.containerLayer.addSublayer(self.backgroundLayer)
        self.containerLayer.addSublayer(self.dotLayer)
        self.containerLayer.addSublayer(self.hoverLayer)

        self.layer!.addSublayer(self.containerLayer)
    }

    private func setupBackgroundLayer(frame: CGRect) {

        self.backgroundLayer = CALayer()
        self.backgroundLayer.frame = frame
        self.backgroundLayer.backgroundColor = NSColor.whiteColor().CGColor
    }

    private func setupDotLayer(frame: CGRect) {

        let dotFrame = frame.rectByInsetting(dx: 6, dy: 6)
        let maskFrame = CGRect(origin: CGPointZero, size: dotFrame.size)

        self.dotLayer = CALayer()
        self.dotLayer.frame = dotFrame
        self.dotLayer.shadowColor = NSColor.colorWithDecimal(deviceRed: 46, deviceGreen: 146, deviceBlue: 255, alpha: 1.0).CGColor
        self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)
        self.dotLayer.shadowOpacity = 0.4
        self.dotLayer.shadowRadius = 2.0

        //// Mask
        let maskLayer = CAShapeLayer()
        maskLayer.path = NSBezierPath(ovalInRect: maskFrame).CGPath
        maskLayer.fillColor = NSColor.blackColor().CGColor

        //// Gradient
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = CGRect(origin: CGPointZero, size: dotFrame.size)
        gradientLayer.colors = [
            NSColor.colorWithDecimal(deviceRed: 29, deviceGreen: 114, deviceBlue: 253, alpha: 1.0).CGColor,
            NSColor.colorWithDecimal(deviceRed: 59, deviceGreen: 154, deviceBlue: 255, alpha: 1.0).CGColor
        ]
        gradientLayer.mask = maskLayer

        //// Inner Stroke
        let strokeLayer = CAShapeLayer()
        strokeLayer.path = NSBezierPath(ovalInRect: maskFrame.rectByInsetting(dx: 0.5, dy: 0.5)).CGPath
        strokeLayer.fillColor = NSColor.clearColor().CGColor
        strokeLayer.strokeColor = NSColor.blackColor().colorWithAlphaComponent(0.12).CGColor
        strokeLayer.lineWidth = 1.0

        self.dotLayer.addSublayer(gradientLayer)
        self.dotLayer.addSublayer(strokeLayer)
    }

    private func setupHoverLayer(frame: CGRect) {

        self.hoverLayer = CALayer()
        self.hoverLayer.frame = frame

        //// Inner Shadow
        let innerShadowLayer = CAShapeLayer()
        let ovalPath = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -10, dy: -10))
        let cutout = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -1, dy: -1)).bezierPathByReversingPath
        ovalPath.appendBezierPath(cutout)
        innerShadowLayer.path = ovalPath.CGPath
        innerShadowLayer.shadowColor = NSColor.blackColor().CGColor
        innerShadowLayer.shadowOpacity = 0.2
        innerShadowLayer.shadowRadius = 2.0
        innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2)

        self.hoverLayer.addSublayer(innerShadowLayer)

        //// Inner Stroke
        let strokeLayer = CAShapeLayer()
        strokeLayer.path = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -0.5, dy: -0.5)).CGPath
        strokeLayer.fillColor = NSColor.clearColor().CGColor
        strokeLayer.strokeColor = NSColor.blackColor().colorWithAlphaComponent(0.22).CGColor
        strokeLayer.lineWidth = 2.0

        self.hoverLayer.addSublayer(strokeLayer)
    }
}

let rbFrame = NSRect(
    x: 87,
    y: 37,
    width: 26,
    height: 26
)

let viewFrame = CGRect(
    x: 0,
    y: 0,
    width: 200,
    height: 100
)

let view = NSView(frame: viewFrame)
view.wantsLayer = true
view.layer!.backgroundColor = NSColor.colorWithDecimal(deviceRed: 40, deviceGreen: 40, deviceBlue: 40, alpha: 1.0).CGColor

let rb = RadioButton(frame: rbFrame)
view.addSubview(rb)

Я использую тот же самый код как для моего проекта, так и на игровой площадке.
Вот zip, содержащий игровую площадку и проект.

Просто для того, чтобы быть ясным: я хочу знать, почему рисунки кругов гладкие на игровой площадке, но не в проектах. (См. @Bannings ответ, это более очевидно с его скриншотами)

Ответы

Ответ 1

Пришло время, но я думаю, что, наконец, понял все или почти все.

  • Сначала наука: Круги или дуги не могут быть представлены через кривые Безье. Это свойство кривых Безье, как указано здесь: https://en.wikipedia.org/wiki/Bézier_curve

    Таким образом, при использовании NSBezierPath(ovalInRect:) вы на самом деле генерируете кривую Безье , приближающую круг. Это может привести к различию внешнего вида и способа формирования фигуры. Тем не менее это не должно быть проблемой в нашем случае, потому что разница между двумя кривыми Безье (одна в Playground и одна в реальном проекте OS X), но все же мне интересно отметить, если вы считаете, что круг не достаточно хорошо.

  • Во-вторых, как указано в этом вопросе (Как нарисовать гладкий круг с CAShapeLayer и UIBezierPath) существуют различия в том, как сглаживание будет применяться к вашему путь в зависимости от того, где используется путь. NSView's drawRect: является местом, где сглаживание пути будет лучшим, а CAShapeLayer - наихудшим.

    Также я обнаружил, что документация CAShapeLayer имеет примечание, в котором говорится следующее:

    Растеризация формы может способствовать ускорению по точности. Например, пиксели с несколькими пересекающимися сегментами пути могут не давать точных результатов.

    Глен Низкий ответ на вопрос, который я ранее упомянул, кажется, отлично работает в нашем случае:

    layer.rasterizationScale = 2.0 * self.window!.screen!.backingScaleFactor;
    layer.shouldRasterize = true;
    

    См. различия здесь:

    Простой путь Безье

    Путь Bézier с ранее упомянутым исправлением

    Другим решением является использование угловых радиусов вместо путей Безье для имитации круга, и на этот раз это довольно точно:

    Использование радиусов

  • Наконец, мое предположение о различии между Playground и реальным проектом OS X заключается в том, что Apple настроил Игровая площадка, чтобы некоторые оптимизации были отключены, поэтому путь даже мысль, проведенная с использованием CAShapeLayer, дает лучшее сглаживание. В конце концов вы прототипируете, выступления не очень важны, особенно при рисовании.

    Я не уверен, что я прав, но думаю, что это не удивительно. Если у кого-то есть источник, я бы с радостью добавил его.

Для меня лучшим решением, если вам действительно нужен лучший круг, является использование угловых радиусов.

Также как указано @Bannings в другом ответе на этот пост. Тени меняются вспять из-за того, что игровая площадка воспроизводится в другой системе координат. См. Его ответ, чтобы исправить это.

Ответ 2

Я просто исправил его, заменив этот код строки:

self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)

с:

self.dotLayer.shadowOffset = CGSize(width: 0, height: -2)

и замените innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2) на:

innerShadowLayer.shadowOffset = CGSize(width: 0, height: -2)

Думаю, вы получите тот же результат:

введите описание изображения здесь введите описание изображения здесь

Левая= на игровой площадке.
Правильно= в моем проекте.

Кажется, что Playground показывает путь безье в системе координат LLO, вы можете перейти по ссылке:  https://forums.developer.apple.com/message/39277#39277