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!
Related
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*)
I am doing an app that does background job that can take some time
I want to show a loader in that time
I want a black screen with a simple loader in the front of it
and show it \ hide it,
when I do actions in the background
I want to do a simple half black square with loader circle
that also blocks presses to the screen
Like in this picture:
How can I achieve that and that ?
First create one UIView which you will put in front of your LogIn view. Then add UIActivityIndicatorView to the created UIView.
let loadingIndicatorView = UIView()
let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
Now the loadingIndicatorView should have same frame size as your LogIN view. For color you can set your own color with alpha as you want to show LogIn content too. Initially keep it hidden and whenever you want to show it unhide it.
loadingIndicatorView.frame = view.frame
loadingIndicatorView.backgroundColor = .gray
loadingIndicatorView.isHidden = true
Now setup activityIndicatorView, it should be shown at centre,
activityIndicatorView.center = CGPoint(
x: UIScreen.main.bounds.size.width / 2,
y: UIScreen.main.bounds.size.height / 2
)
You can set some color to the indicator,
activityIndicatorView.color = .white
activityIndicatorView.hidesWhenStopped = true
Now add this activityIndicatorView to loadingIndicatorView and loadingIndicatorView to LogIn View.
loadingIndicatorView.addSubview(activityIndicatorView)
view.addSubview(loadingIndicatorView)
Lastly for showing do,
loadingIndicator.startAnimating()
loadingIndicatorView.isHidden = false
And for hiding,
loadingIndicator.stopAnimating()
loadingIndicatorView.isHidden = true
Updated Answer
Since the OP wanted an example code. Hence the updated answer. Hope everyone gets to learn something or the other out of it.
To start with, I created a subclass of UIView and named it PSOverlaySpinner and it looks something like below:
import UIKit
class PSOverlaySpinner: UIView {
//MARK: - Variables
private var isSpinning: Bool = false
private lazy var spinner : UIActivityIndicatorView = {
var spinner = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.white)
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.hidesWhenStopped = true
return spinner
}()
// MARK: - View Lifecycle Functions
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init() {
super.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.init(white: 0.0, alpha: 0.8)
self.isSpinning = false
self.isHidden = true
createSubviews()
}
deinit {
self.removeFromSuperview()
}
func createSubviews() -> Void {
self.addSubview(spinner)
setupAutoLayout()
}
// MARK: - Private Methods
private func setupAutoLayout() {
if #available(iOS 11.0, *) {
spinner.safeAreaLayoutGuide.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor).isActive = true
spinner.safeAreaLayoutGuide.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor).isActive = true
} else {
// Fallback on earlier versions
spinner.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
spinner.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
}
// MARK: - Public Methods
public func show() -> Void {
DispatchQueue.main.async {
if !self.spinner.isAnimating {
self.spinner.startAnimating()
}
self.isHidden = false
}
isSpinning = true
}
public func hide() -> Void {
DispatchQueue.main.async {
if self.spinner.isAnimating {
self.spinner.stopAnimating()
}
self.isHidden = true
}
isSpinning = false
}
}
Now move onto the ViewController that you want to add this overlay view to. Since I create my views programmatically, I will show how to do it the same way, but you can easily do it via storyboard or xibs.
Step 1 : Initialize
public lazy var spinnerView : PSOverlaySpinner = {
let loadingView : PSOverlaySpinner = PSOverlaySpinner()
return loadingView
}()
Step 2 : Add as a subview
self.view.addSubview(spinnerView)
Step 3 : Set constraints
spinnerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
spinnerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
spinnerView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
spinnerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
Step 4 : To show PSOverlaySpinner
spinnerView.show()
Step 5 : To hide PSOverlaySpinner
spinnerView.hide()
That is it!!
If you want you can go ahead and modify the PSOverlaySpinner as per your needs. For example, you might want to add a UILabel below the spinner indicating him of the type of action taking place and so on.
Before
After
Old Answer
If you wish to do it manually then create a UIView with the its frame matching self.view.bounds, with 0.5-0.7 alpha and black background color. Add UIActivityIndicator as its subview constrained to its center. For a spinner specific to the image you will have to use the open sourced spinners made available. A couple of them can be found here. Once done add this view as the topmost subview in self.view.
You need to import this library SVProgressHUD and then set few properties like as follows:
SVProgressHUD.setDefaultStyle(SVProgressHUDStyle.dark)
SVProgressHUD.setBackgroundColor(.clear)
SVProgressHUD.setForegroundColor(.white)
SVProgressHUD.setDefaultMaskType(.black)
SVProgressHUD.show()
//SVProgressHUD.show(withStatus: "Loading something, Loading something,Loading something ...")
This will produce same UI output as needed by you in OP. You can find a running sample at my repository (TestPreLoader)
I made a custom NSControl to use it as a custom layer backed button.
When the MyButton instance receives mouseDown and mouseUp events it changes its backgroundLayer's backgroundColorand its textLayer's foregroundColor.
It's using layers so the changes are implicitly animated.
But on mouseUp if the mouse is inside the MyButton's instance frame I call the linked action through sendAction(_:to:) method.
I linked the action to a method closing the current window and opening another one but sometimes the app freeze after the second window shows up.
I tried several things and it seems related to the layer animations, maybe something to do before closing the window that I'm not aware of.
UPDATE: It's Tweetbot ! If Tweetbot is running it's freezing the app! As soon as I quit it the app becomes responsive again.
You can find an example project here :
https://dl.dropboxusercontent.com/u/378166/CALayerFreeze.zip
(note that you'll sometimes have to try several times before the bug occurs)
Here's the code for MyButton.
class MyButton: NSControl {
let title = "Click me!"
// Init
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
deinit {
trackingAreas.forEach { self.removeTrackingArea($0) }
}
// Layer + Tracking Area configuration
var backgroundLayer = CALayer()
var textLayer = CATextLayer()
func setup() {
wantsLayer = true
backgroundLayer.frame = NSRect(origin: .zero, size: frame.size)
backgroundLayer.backgroundColor = NSColor.whiteColor().CGColor
layer?.addSublayer(backgroundLayer)
textLayer.frame = NSRect(origin: .zero, size: frame.size)
textLayer.string = title
textLayer.foregroundColor = NSColor.blackColor().colorWithAlphaComponent(0.64).CGColor
layer?.addSublayer(textLayer)
addTrackingArea(
NSTrackingArea(
rect: bounds,
options: [.MouseEnteredAndExited, .EnabledDuringMouseDrag, .ActiveInKeyWindow],
owner: self,
userInfo: nil
)
)
}
// States
private func normal() {
// ——— COMMENTING THIS MAKES THE BEACHBALL GO AWAY
backgroundLayer.backgroundColor = NSColor.whiteColor().CGColor
textLayer.foregroundColor = NSColor.blackColor().colorWithAlphaComponent(0.64).CGColor
}
private func highlight() {
// ——— COMMENTING THIS MAKES THE BEACHBALL GO AWAY
backgroundLayer.backgroundColor = NSColor.grayColor().CGColor
textLayer.foregroundColor = NSColor.whiteColor().colorWithAlphaComponent(0.64).CGColor
}
// Tracking events
var isMouseDown = false
override func mouseDown(theEvent: NSEvent) {
super.mouseDown(theEvent)
isMouseDown = true
highlight()
}
override func mouseUp(theEvent: NSEvent) {
super.mouseUp(theEvent)
isMouseDown = false
normal()
if frame.contains(convertPoint(theEvent.locationInWindow, toView: self)) {
sendAction(action, to: target)
}
}
}
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.
What is the process of drawing to NSView using storyboards for osx? I have added a NSView to the NSViewController. Then, I added a few constraints and an outlet.
Next, I added some code to change the color:
import Cocoa
class ViewController: NSViewController {
#IBOutlet var box: NSView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear() {
box.layer?.backgroundColor = NSColor.blueColor().CGColor
//box.layer?.setNeedsDisplay()
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
}
I would like to do custom drawing and changing colors of the NSView. I have
performed sophisticated drawing on iOS in the past, but am totally stuck here.
Does anyone see what I'm doing wrong?
The correct way is
class ViewController: NSViewController {
#IBOutlet var box: NSView!
override func viewDidLoad() {
super.viewDidLoad()
self.view.wantsLayer = true
}
override func viewWillAppear() {
box.layer?.backgroundColor = NSColor.blue.cgColor
//box.layer?.setNeedsDisplay()
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}}
Swift via property
extension NSView {
var backgroundColor: NSColor? {
get {
if let colorRef = self.layer?.backgroundColor {
return NSColor(CGColor: colorRef)
} else {
return nil
}
}
set {
self.wantsLayer = true
self.layer?.backgroundColor = newValue?.CGColor
}
}
}
Usage:
yourView.backgroundColor = NSColor.greenColor()
Where yourView is NSView or any of its subclasses
Updated for Swift 3
extension NSView {
var backgroundColor: NSColor? {
get {
if let colorRef = self.layer?.backgroundColor {
return NSColor(cgColor: colorRef)
} else {
return nil
}
}
set {
self.wantsLayer = true
self.layer?.backgroundColor = newValue?.cgColor
}
}
}
edit/update:
Another option is to design your own colored view:
import Cocoa
#IBDesignable class ColoredView: NSView {
#IBInspectable var backgroundColor: NSColor = .clear
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
backgroundColor.set()
dirtyRect.fill()
}
}
Then you just need to add a Custom View NSView and set the custom class in the inspector:
Original Answer
Swift 3.0 or later
extension NSView {
var backgroundColor: NSColor? {
get {
guard let color = layer?.backgroundColor else { return nil }
return NSColor(cgColor: color)
}
set {
wantsLayer = true
layer?.backgroundColor = newValue?.cgColor
}
}
}
let myView = NSView(frame: NSRect(x: 0, y: 0, width: 100, height: 100))
myView.backgroundColor = .red
This works a lot better:
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
NSColor.blueColor().setFill()
NSRectFill(dirtyRect);
}
Best way to set a NSView background colour in MacOS 10.14 with dark mode support :
1/ Create your colour in Assets.xcassets
2/ Subclass your NSView and add this :
class myView: NSView {
override func updateLayer() {
self.layer?.backgroundColor = NSColor(named: "customControlColor")?.cgColor
}
}
Very simple and dark mode supported with the colour of your choice !
Full guide : https://developer.apple.com/documentation/appkit/supporting_dark_mode_in_your_interface
Just one line of code is enough to change the background of any NSView object:
myView.setValue(NSColor.blue, forKey: "backgroundColor")
Instead of this, you can also add an user defined attribute in the interface designer of type Color with keyPath backgroundColor.
Update to Swift 3 solution by #CryingHippo (It showed colors not on every run in my case). I've added DispatchQueue.main.async and now it shows colors on every run of the app.
extension NSView {
var backgroundColor: NSColor? {
get {
if let colorRef = self.layer?.backgroundColor {
return NSColor(cgColor: colorRef)
} else {
return nil
}
}
set {
DispatchQueue.main.async {
self.wantsLayer = true
self.layer?.backgroundColor = newValue?.cgColor
}
}
}
}
None of the solutions is using pure power of Cocoa framework.
The correct solution is to use NSBox instead of NSView. It has always supported fillColor, borderColor etc.
Set titlePosition to None
Set boxType to Custom
Set borderType to None
Set your desired fillColor from asset catalogue (IMPORTANT for dark mode)
Set borderWidth and borderRadius to 0
Bonus:
it dynamically reacts to sudden change of appearance (light to dark)
it supports animations ( no need for dynamic + no need to override animation(forKey key:NSAnimatablePropertyKey) ->)
future macOS support automatically
WARNING:
Using NSBox + system colors in dark mode will apply tinting corectly. If you do not want tinting use catalogue color.
Alternative is to provide subclass of NSView and do the drawing updates in updateLayer
import Cocoa
#IBDesignable
class CustomView: NSView {
#IBInspectable var backgroundColor : NSColor? {
didSet { needsDisplay = true }
}
override var wantsUpdateLayer: Bool {
return true
}
override func updateLayer() {
guard let layer = layer else { return }
layer.backgroundColor = backgroundColor?.cgColor
}
}
Tinting:
Since macOS 10.13 (High Sierra) NSView responds to selector backgroundColor although it is not documented!