Ответ 1
Обновление для OS X El Capitan
Хак, описанный в моем первоначальном ответе ниже, больше не нужен для OS X El Capitan. NSVisualEffectView
s maskImage
должен корректно работать там, если для параметра NSWindow
s contentView
установлено значение NSVisualEffectView
(его недостаточно, если он является подзоном contentView
).
Вот пример проекта: https://github.com/marcomasser/OverlayTest
Оригинальный ответ - только для OS X Yosemite
Я нашел способ сделать это, переопределив частный метод NSWindow: - (NSImage *)_cornerMask
. Просто верните изображение, созданное путем рисования NSBezierPath с закругленным прямоугольником в нем, чтобы получить вид, похожий на окно тома OS Xs.
В моем тестировании я обнаружил, что вам нужно использовать изображение маски для NSVisualEffectView и NSWindow. В вашем коде вы используете свойство слоев cornerRadius
, чтобы получить закругленные углы, но вы можете добиться того же, используя изображение маски. В моем коде я генерирую NSImage, который используется как NSVisualEffectView, так и NSWindow:
func maskImage(#cornerRadius: CGFloat) -> NSImage {
let edgeLength = 2.0 * cornerRadius + 1.0
let maskImage = NSImage(size: NSSize(width: edgeLength, height: edgeLength), flipped: false) { rect in
let bezierPath = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
NSColor.blackColor().set()
bezierPath.fill()
return true
}
maskImage.capInsets = NSEdgeInsets(top: cornerRadius, left: cornerRadius, bottom: cornerRadius, right: cornerRadius)
maskImage.resizingMode = .Stretch
return maskImage
}
Затем я создал подкласс NSWindow, у которого есть сеттер для изображения маски:
class MaskedWindow : NSWindow {
/// Just in case Apple decides to make `_cornerMask` public and remove the underscore prefix,
/// we name the property `cornerMask`.
@objc dynamic var cornerMask: NSImage?
/// This private method is called by AppKit and should return a mask image that is used to
/// specify which parts of the window are transparent. This works much better than letting
/// the window figure it out by itself using the content view shape because the latter
/// method makes rounded corners appear jagged while using `_cornerMask` respects any
/// anti-aliasing in the mask image.
@objc dynamic func _cornerMask() -> NSImage? {
return cornerMask
}
}
Затем в моем подклассе NSWindowController я установил изображение маски для представления и окна:
class OverlayWindowController : NSWindowController {
@IBOutlet weak var visualEffectView: NSVisualEffectView!
override func windowDidLoad() {
super.windowDidLoad()
let maskImage = maskImage(cornerRadius: 18.0)
visualEffectView.maskImage = maskImage
if let window = window as? MaskedWindow {
window.cornerMask = maskImage
}
}
}
Я не знаю, что сделает Apple, если вы отправите приложение с этим кодом в App Store. Вы на самом деле не называете какой-либо частный API, вы просто переопределяете метод, который имеет то же имя, что и частный метод в AppKit. Как вы должны знать, что это конфликт имен? 😉
Кроме того, это невозможно изящно, без необходимости делать что-либо. Если Apple изменит способ, которым это работает внутренне, и метод просто не вызван, ваше окно не получит приятные округлые углы, но все еще работает и выглядит почти так же.
Если вам интересно узнать, как я узнал об этом методе:
Я знал, что индикация громкости OS X сделала то, что я хочу сделать, и я надеялся, что изменение громкости, как сумасшедшего, привело к заметному использованию ЦП процессом, который помещает эту индикацию громкости на экран. Поэтому я открыл Activity Monitor, отсортированный по использованию ЦП, активировал фильтр, чтобы показывать только "Мои процессы" и забил мои клавиши увеличения/уменьшения громкости.
Стало ясно, что coreaudiod
и что-то, что называется BezelUIServer
в /System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/BezelUI/BezelUIServer
, что-то сделал. От взгляда на ресурсы пучка для последнего было очевидно, что он отвечает за вывод указателя объема. (Примечание: этот процесс выполняется только в течение короткого времени после его отображения.)
Затем я использовал Xcode для присоединения к этому процессу сразу после его запуска (Debug > Attach to Process > By Process Identifier (PID) или Name..., затем введите "BezelUIServer" ) и снова изменил громкость. После того как отладчик был подключен, отладчик просмотра позволяет мне взглянуть на иерархию представления и увидеть, что это окно является экземпляром подкласса NSWindow под названием BSUIRoundWindow
.
Использование class-dump
в двоичном выражении показало, что этот класс является прямым потомком NSWindow и реализует только три метода, тогда как один - (id)_cornerMask
, который звучал многообещающе.
Вернувшись в Xcode, я использовал Object Inspector (правая сторона, третья вкладка), чтобы получить адрес для объекта window. Используя этот указатель, я проверил, что это действительно возвращает _cornerMask
, напечатав его описание в lldb:
(lldb) po [0x108500110 _cornerMask]
<NSImage 0x608000070300 Size={37, 37} Reps=(
"NSCustomImageRep 0x608000082d50 Size={37, 37} ColorSpace=NSCalibratedRGBColorSpace BPS=0 Pixels=0x0 Alpha=NO"
)>
Это показывает, что возвращаемое значение на самом деле является NSImage, которое является информацией, необходимой мне для реализации _cornerMask
.
Если вы хотите взглянуть на это изображение, вы можете записать его в файл:
(lldb) e (BOOL)[[[0x108500110 _cornerMask] TIFFRepresentation] writeToFile:(id)[@"~/Desktop/maskImage.tiff" stringByExpandingTildeInPath] atomically:YES]
Чтобы выровнять немного глубже, вы можете использовать Hopper Disassembler, чтобы разобрать BezelUIServer
и AppKit
и сгенерировать псевдокод, чтобы увидеть, как _cornerMask
реализуется и используется, чтобы получить более четкое представление о том, как работают внутренние элементы. К сожалению, все, что касается этого механизма, - это частный API.