togo

= ひとりごと to go

zumuya の人による机の上系情報サイト

NSVisualEffectView の角を丸くする 2 つの方法

  • Apple
  • 開発

画面の端にぴったりくっつけることが多い iOS と違い、macOS だと余白を設けてビューを配置することが多い。そうすると角を丸くしたくなる。木工家具でやすりがけや面取りをするような基本的な話だ。

AppKit の標準コントロールのほとんどはそういう仕上げがしてあるから意識する必要はないのだけど、先日 NSVisualEffectView の角を丸くしたい状況があったのでやり方を考えてみた。

方法 1(シンプルだけど邪道?)

ビューを丸くするんでしょう、layer.cornerRadius を指定すればいいじゃない。iOS の人だったらまずやりそうな方法だ。

import Cocoa

let view = NSVisualEffectView()

//adjust for Playground
view.material = .dark
view.blendingMode = .withinWindow
view.state = .inactive

//make it round!
view.layer?.cornerRadius = 40

view.setFrameSize(NSSize(width: 320, height: 240))
view //Size 1

view.setFrameSize(NSSize(width: 240, height: 240))
view //Size 2
Size 1
Size 2

ほら普通に丸くなる。

...ところが自分で用意していない layer には触れるなというのが NSView の CALayer 対応の基本なので、若干気持ち悪い。frame などと違って cornerRadius が原因で動作がおかしくなることなんてまずなさそうではあるけれども。

あと NSVisualEffect の実装が変わって例えばサブレイヤーで描画するようになったりしたら四角い角に戻ってしまうわけで、不安は残る。

方法 2(正攻法?)

次の方法は maskImage を指定する方法だ。NSVisualEffectView 自身が用意している正攻法。でも普通のイメージだとビューのサイズが変わるたびにセットし直さないと角丸が引き伸ばされてしまう。

そこで活躍するのが NSImage.capInsets だ。UIImage でおなじみの四辺と角を保護しながらリサイズして描画してくれる機能。OS X 10.10 Yosemite 以降から AppKit でも使用できるようになった1

import Cocoa

let view = NSVisualEffectView()

//adjust for Playground
view.material = .dark
view.blendingMode = .withinWindow
view.state = .inactive

func maskImageForRoundedRect(cornerRadius: CGFloat) -> NSImage
{
    let imageBounds = NSRect(x: 0, y: 0, width: ((cornerRadius * 2) + 2), height: ((cornerRadius * 2) + 2))
    let image = NSImage(size: imageBounds.size, flipped: false) { destinationRect in
        NSColor.black.setFill()
        NSBezierPath(roundedRect: imageBounds, xRadius: cornerRadius, yRadius: cornerRadius).fill()
        return true
    }
    image.capInsets = NSEdgeInsets(top: cornerRadius, left: cornerRadius, bottom: cornerRadius, right: cornerRadius)
    return image
}
//make it round!
view.maskImage = maskImageForRoundedRect(cornerRadius: 40)

view.setFrameSize(NSSize(width: 320, height: 240))
view //Size 1

view.setFrameSize(NSSize(width: 240, height: 240))
view //Size 2
Size 1
Size 2

イメージを生成してメモリや GPU のリソースを消費することになるので方法 1 の方が優れているように見えるけど、maskImage の内容は自由なので、例えば Dock っぽく上の角だけ丸くしたいみたいな凝ったことにも対応できる。どの方法を選ぶかはあなた次第だ2

記事完成直後の追記

方法 2 は Mojave の Playground だと capInsets が反映されず正しく表示されないことが判明。

Size 1
Size 2

自分の環境では今のところ実際の App 上では問題なく動いているけど「正攻法」と呼べるかは怪しい。わざわざ記事にしたのに...

2018.9.27 追記

maskImage に capInsets が使用できることは WWDC で明言されているようだ:

正攻法!正攻法!


  1. 本音を言えば、フラット風味になる前の時代にこそ欲しかった! ↩︎

  2. = 責任を負いたくない = 結論を出すことから逃げた ↩︎

Share

リンクも共有もお気軽に。記事を書くモチベーションの向上に役立てます。