Rendering NSView in another context - swift

Im looking for a way to have one NSView draw inside another NSView, to make a preview of that view. To be exact Im using VZVirtualMachineView and want to have a preview only in a grid. I would just use VZVirtualMachineView but there is one issue with it - it captures mouse cursor (can hide it) and reacts to clicks and mouse movement over it. And in this case I want to have only the preview with no interaction and no cursor hiding.
So I thought I might try rendering it inside another NSView using vmView.layer?.render(in: context.cgContext), or some other similar mechanism.
The question is how to do this properly? And how to make it refresh with correct time? Should I use something like CADisplayLink? Do I need to add this view first somewhere or can I draw it without adding? Also I would like to not use something like creating image (unless this would be efficient method)
Here is very simple first try, but Im not happy with that code, so
Please suggest some approaches that I should try.
class VMPreview: NSView {
let vmView: VZVirtualMachineView
var timer: Timer?
init(virtualMachine: VZVirtualMachine) {
self.vmView = .init()
self.vmView.virtualMachine = virtualMachine
super.init(frame: .zero)
addSubview(self.vmView)
dr()
}
func dr() {
self.setNeedsDisplay(bounds)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.dr()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var frame: NSRect {
didSet {
self.vmView.frame = bounds.offsetBy(dx: 0, dy: bounds.height)
}
}
override func draw(_ dirtyRect: NSRect) {
guard let context = NSGraphicsContext.current else { return }
vmView.layer?.render(in: context.cgContext)
self.setNeedsDisplay(bounds)
}
}

Related

How, exactly, do I render Metal on a background thread?

This problem is caused by user interface interactions such as showing the titlebar while in fullsreen. That question's answer provides a solution, but not how to implement that solution.
The solution is to render on a background thread. The issue is, the code provided in Apple's is made to cover a lot of content so most of it will extraneous code, so even if I could understand it, it isn't feasible to use Apple's code. And I can't understand it so it just plain isn't an option. How would I make a simple Swift Metal game use a background thread being as concise as possible?
Take this, for example:
class ViewController: NSViewController {
var MetalView: MTKView {
return view as! MTKView
}
var Device: MTLDevice = MTLCreateSystemDefaultDevice()!
override func viewDidLoad() {
super.viewDidLoad()
MetalView.delegate = self
MetalView.device = Device
MetalView.colorPixelFormat = .bgra8Unorm_srgb
Device = MetalView.device
//setup code
}
}
extension ViewController: MTKViewDelegate {
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
//drawing code
}
}
That is the start of a basic Metal game. What would that code look like, if it were rendering on a background thread?
To fix that bug when showing the titlebar in Metal, I need to render it on a background thread. Well, how do I render it on a background thread?
I've noticed this answer suggests to manually redraw it 60 times a second. Presumably using a loop that is on a background thread? But that seems... not a clean way to fix it. Is there a cleaner way?
The main trick in getting this to work seems to be setting up the CVDisplayLink. This is awkward in Swift, but doable. After some work I was able to modify the "Game" template in Xcode to use a custom view backed by CAMetalLayer instead of MTKView, and a CVDisplayLink to render in the background, as suggested in the sample code you linked — see below.
Edit Oct 22:
The approach mentioned in this thread seems to work just fine: still using an MTKView, but drawing it manually from the display link callback. Specifically I was able to follow these steps:
Create a new macOS Game project in Xcode.
Modify GameViewController to add a CVDisplayLink, similar to below (see this question for more on using CVDisplayLink from Swift). Start the display link in viewWillAppear and stop it in viewWillDisappear.
Set mtkView.isPaused = true in viewDidLoad to disable automatic rendering, and instead explicitly call mtkView.draw() from the display link callback.
The full content of my modified GameViewController.swift is available here.
I didn't review the Renderer class for thread safety, so I can't be sure no more changes are required, but this should get you up and running.
Older implementation with CAMetalLayer instead of MTKView:
This is just a proof of concept and I can't guarantee it's the best way to do everything. You might find these articles helpful too:
I didn't try this idea, but given how much convenience MTKView generally provides over CAMetalLayer, it might be worth giving it a shot:
https://developer.apple.com/forums/thread/89241?answerId=268384022#268384022
Is drawing to an MTKView or CAMetalLayer required to take place on the main thread? and https://developer.apple.com/documentation/quartzcore/cametallayer/1478157-presentswithtransaction
class MyMetalView: NSView {
var displayLink: CVDisplayLink?
var metalLayer: CAMetalLayer!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
setupMetalLayer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupMetalLayer()
}
override func makeBackingLayer() -> CALayer {
return CAMetalLayer()
}
func setupMetalLayer() {
wantsLayer = true
metalLayer = layer as! CAMetalLayer?
metalLayer.device = MTLCreateSystemDefaultDevice()!
// ...other configuration of the metalLayer...
}
// handle display link callback at 60fps
static let _outputCallback: CVDisplayLinkOutputCallback = { (displayLink, inNow, inOutputTime, flagsIn, flagsOut, context) -> CVReturn in
// convert opaque context pointer back into a reference to our view
let view = Unmanaged<MyMetalView>.fromOpaque(context!).takeUnretainedValue()
/*** render something into view.metalLayer here! ***/
return kCVReturnSuccess
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
guard CVDisplayLinkCreateWithActiveCGDisplays(&displayLink) == kCVReturnSuccess,
let displayLink = displayLink
else {
fatalError("unable to create display link")
}
// pass a reference to this view as an opaque pointer
guard CVDisplayLinkSetOutputCallback(displayLink, MyMetalView._outputCallback, Unmanaged<MyMetalView>.passUnretained(self).toOpaque()) == kCVReturnSuccess else {
fatalError("unable to configure output callback")
}
guard CVDisplayLinkStart(displayLink) == kCVReturnSuccess else {
fatalError("unable to start display link")
}
}
deinit {
if let displayLink = displayLink {
CVDisplayLinkStop(displayLink)
}
}
}

Drag and drop animated GIFs rendered in NSImageView Swift MacOS application

I am trying to build a Mac OSX application that renders several gifs and allows the users to drag and drop them for copying the gifs into some other app. I am using a DragDropImageView (code below) that conforms to an NSImageView to render a gif that is drag-n-drop enabled.
It works fine, except that when I drag and drop the gif into another application, it copies only a single image frame of the gif. My intention is to copy the entire gif file and not just a single image.
I am pretty new to iOS/MacOS development in general, and I am not sure if my approach to building this draggable gif component is correct.
I am building the app using swiftUI, and I use a custom view called GifImageView that converts the DragDropImageView to a swiftUI view.
DragDropImageView.swift
import Cocoa
class DragDropImageView: NSImageView, NSDraggingSource {
/// Holds the last mouse down event, to track the drag distance.
var mouseDownEvent: NSEvent?
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
isEditable = false
}
required init?(coder: NSCoder) {
super.init(coder: coder)
// Assure editable is set to true, to enable drop capabilities.
isEditable = true
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
// MARK: - NSDraggingSource
// Since we only want to copy the current image we register
// for .Copy operation.
func draggingSession(_: NSDraggingSession,
sourceOperationMaskFor _: NSDraggingContext) -> NSDragOperation {
return NSDragOperation.copy
}
// // Clear the ImageView on delete operation; e.g. the image gets
// // dropped on the trash can in the dock.
// func draggingSession(_: NSDraggingSession, endedAt _: NSPoint,
// operation: NSDragOperation) {
// if operation == .delete {
// image = nil
// }
// }
// Track mouse down events and safe the to the poperty.
override func mouseDown(with theEvent: NSEvent) {
mouseDownEvent = theEvent
}
// Track mouse dragged events to handle dragging sessions.
override func mouseDragged(with event: NSEvent) {
// Calculate the dragging distance...
let mouseDown = mouseDownEvent!.locationInWindow
let dragPoint = event.locationInWindow
let dragDistance = hypot(mouseDown.x - dragPoint.x, mouseDown.y - dragPoint.y)
// Cancel the dragging session in case of an accidental drag.
if dragDistance < 3 {
return
}
guard let image = self.image else {
return
}
let draggingImage = image
// Create a new NSDraggingItem with the image as content.
let draggingItem = NSDraggingItem(pasteboardWriter: image)
// Calculate the mouseDown location from the window's coordinate system to the
// ImageView's coordinate system, to use it as origin for the dragging frame.
let draggingFrameOrigin = convert(mouseDown, from: nil)
// Build the dragging frame and offset it by half the image size on each axis
// to center the mouse cursor within the dragging frame.
let draggingFrame = NSRect(origin: draggingFrameOrigin, size: draggingImage.size)
.offsetBy(dx: -draggingImage.size.width / 2, dy: -draggingImage.size.height / 2)
// Assign the dragging frame to the draggingFrame property of our dragging item.
draggingItem.draggingFrame = draggingFrame
// Provide the components of the dragging image.
draggingItem.imageComponentsProvider = {
let component = NSDraggingImageComponent(key: NSDraggingItem.ImageComponentKey.icon)
component.contents = image
component.frame = NSRect(origin: NSPoint(), size: draggingFrame.size)
return [component]
}
// Begin actual dragging session. Woohow!
beginDraggingSession(with: [draggingItem], event: mouseDownEvent!, source: self)
}
}
GifImageView.swift
import AppKit;
import SwiftUI;
struct GifImageView: NSViewRepresentable {
var image: NSImage
func makeNSView(context: Context) -> DragDropImageView {
let view = DragDropImageView()
view.image = self.image
// view.allowsCutCopyPaste = true
return view
}
func updateNSView(_ view: DragDropImageView, context: Context) {
}
}
struct GifImageView_Previews: PreviewProvider {
static var previews: some View {
GifImageView(image: NSImage(data: (NSDataAsset(name: "tenor")?.data)!)!)
}
}
in my ContentView.swift, I use my GifImageView something like this:
GifImageView(image: *some NSImage*)

Using a CALayer in an NSStatusBarButton

I'm interested in drawing custom content in a NSStatusBarButton similar to the built-in battery menu bar app:
I'd like to use a CALayer due to the flexibility of drawing custom content. I've managed to add a layer-backed NSView to the button using this code:
// StatusView
class StatusView: NSView {
required init?(coder: NSCoder) {
super.init(coder: coder)
self.wantsLayer = true
}
override var wantsUpdateLayer: Bool {
return true
}
override func updateLayer() {
if let layer = self.layer {
layer.backgroundColor = NSColor.clear.cgColor
// Code adding text layer...
}
}
}
// MenuController
class StatusMenuController: NSObject {
override func awakeFromNib() {
if let button = statusItem.button {
layerView.frame = NSRect(x: 0.0, y: 0.0, width: len / 2, height: button.bounds.height)
button.addSubview(layerView)
}
}
}
Basically I'm adding a CALayer to the button of the NSStatusBarButton, which looks fine normally, but seems to have an opaque background when you click the menu item:
I've set the background color of the CALayer to transparent and the isOpaque property of the layer's view is false. Is there a way to get the view to actually blend with the background, similar to the battery menu bar app?
Thanks!

How to control one IB custom view with mouseEntered/-Exited in another IB custom view

I'm sorry for my incompetence, but I'm new to Cocoa, Swift, and object-oriented programming, in general. My primary sources have been Cocoa Programming for OS X (5th ed.), and Apple's jargon- and Objective-C-riddled Developer pages. But I'm here because I haven't seen (or didn't realize that I saw) anything that speaks to this problem.
I want to change the contents of one IB-created custom view, LeftView, by mouseEntered/-Exited actions in another IB-created custom view, RightView. Both are in the same window. I created a toy program to try to figure things out, but to no avail.
Here's the class definition for RightView (which is supposed to change LeftView):
import Cocoa
class RightView: NSView {
override func drawRect(dirtyRect: NSRect) {
// Nothing here, for now.
}
override func viewDidMoveToWindow() {
window?.acceptsMouseMovedEvents = true
let options: NSTrackingAreaOptions =
[.MouseEnteredAndExited, .ActiveAlways, .InVisibleRect]
let trackingArea = NSTrackingArea(rect: NSRect(),
options: options,
owner: self,
userInfo: nil)
addTrackingArea(trackingArea)
}
override func mouseEntered(theEvent: NSEvent) {
Swift.print("Mouse entered!")
LeftView().showStuff(true)
}
override func mouseExited(theEvent: NSEvent) {
Swift.print("Mouse exited!")
LeftView().showStuff(false)
}
}
And here's the class definition for LeftView (which is supposed to be changed by RightView):
import Cocoa
class LeftView: NSView {
var value: Bool = false {
didSet {
needsDisplay = true
Swift.print("didSet happened and needsDisplay was \(needsDisplay)")
}
}
override func mouseUp(theEvent: NSEvent) {
showStuff(true)
}
override func drawRect(dirtyRect: NSRect) {
let backgroundColor = NSColor.blackColor()
backgroundColor.set()
NSBezierPath.fillRect(bounds)
Swift.print("drawRect was called when needsDisplay was \(needsDisplay)")
switch value {
case true: NSColor.greenColor().set()
case false: NSColor.redColor().set()
}
NSBezierPath.fillRect(NSRect(x: 40, y: 40,
width: bounds.width - 80, height: bounds.height - 80))
}
func showStuff(showing: Bool) {
Swift.print("Trying to show? \(showing)")
value = showing
}
}
I'm sure I'm missing something "completely obvious," but I'm a little dense. If you could tell me how to fix the code/xib files, I would very much appreciate it. If you could explain things like when talking to a child, I would be even more appreciative. When I take over the world (I'm not incompetent at everything), I will remember your kindness.
I came up with a workaround that is much simpler than my prior strategy. Instead of having mouseEntered/-Exited actions in one custom view try to control what is displayed in another custom view, I simply put the mouseEntered/-Exited code into the view that I wanted to control, and then I altered the location of rect: in NSTrackingArea.
Before moving code over by for that approach, I had tried changing the owner: in NSTrackingArea to LeftView() and just moving over the mouseEntered/-Exited code. That generated lots of scary error messages (naive newbie talking here), so I gave up on it. It would be nice to know, though, how one can correctly assign an owner other than self.
In any case, any further thoughts or insights would be appreciated.

Custom Activity Indicator with Swift

I am trying to create a custom activity indicator like one of these. There seems to be many approaches to animating the image, so I'm trying to figure out the best approach.
I found this tutorial with Photoshop + Aftereffects animation loop, but as the comment points out, it seems overly complex (and I don't own After Effects).
tldr: how can I take my existing image and animate it as a activity indicator (using either rotating/spinning animation or looping through an animation)
It's been a while since you posted this question, so I'm not sure if you've found the answer you're looking for.
You are right there are many ways to do what you are asking, and I don't think there's a "right way". I would make a UIView object which have a start and a stop method as the only public methods. I would also make it a singleton, so it's a shared object and not possible to put multiple instances on a UIViewController (imagine the mess it could make).
You can add a dataSource protocol which returns a UIViewController so the custom activity indicator could add itself to it's parent.
In the start method I would do whatever animations is needed (transform, rotate, GIF, etc.).
Here's an example:
import UIKit
protocol CustomActivityIndicatorDataSource {
func activityIndicatorParent() -> UIViewController
}
class CustomActivityIndicator: UIView {
private var activityIndicatorImageView: UIImageView
var dataSource: CustomActivityIndicatorDataSource? {
didSet {
self.setupView()
}
}
// Singleton
statice let sharedInstance = CustomActivityIndicator()
// MARK:- Initialiser
private init() {
var frame = CGRectMake(0,0,200,200)
self.activityIndicatorImageView = UIImageView(frame: frame)
self.activityIndicatorImageView.image = UIImage(named: "myImage")
super.init(frame: frame)
self.addSubview(self.activityIndicatorImageView)
self.hidden = true
}
internal required init?(coder aDecoder: NSCoder) {
self.activityIndicatorImageView = UIImageView(frame: CGRectMake(0, 0, 200, 200))
self.activityIndicatorImageView = UIImage(named: "myImage")
super.init(coder: aDecoder)
}
// MARK:- Helper methods
private func setupView() {
if self.dataSource != nil {
self.removeFromSuperview()
self.dataSource!.activityIndicatorParent().addSubview(self)
self.center = self.dataSource!.activityIndicatorParent().center
}
}
// MARK:- Animation methods
/**
Set active to true, and starts the animation
*/
func startAnimation() {
// A custom class which does thread handling. But you can use the dispatch methods.
ThreadController.performBlockOnMainQueue{
self.active = true
self.myAnimation
self.hidden = false
}
}
/**
This will set the 'active' boolean to false.
Remeber to remove the view from the superview manually
*/
func stopAnimation() {
ThreadController.performBlockOnMainQueue{
self.active = false
self.hidden = true
}
}
}
Mind that this is not fully tested and may need some tweaks to work 100%, but this is how I basically would handle it.