Is there any way to add mouseOver events to a tableView in cocoa? - objective-c

I want to make my tableView act like this:
When the mouse swipe over a certain row, the row will be highlighted, just like the mouseOver event of a button

It took me some time to work on it based on this hint.
It works for me, correct me if I'm wrong.
Tested with Swift 3.0.2 on macOS 10.12.2 and Xcode 8.2.1
//
// Created by longkai on 30/12/2016.
// Copyright (c) 2016 xiaolongtongxue.com. All rights reserved.
//
import Cocoa
class InboxTableCellView: NSTableCellView {
// MARK: - Outlets
#IBOutlet weak var title: NSTextField!
#IBOutlet weak var sender: NSTextField!
#IBOutlet weak var time: NSTextField!
#IBOutlet weak var snippet: NSTextField!
// MARK: - Mouse hover
deinit {
removeTrackingArea(trackingArea)
}
private var trackingArea: NSTrackingArea!
override func awakeFromNib() {
super.awakeFromNib()
self.trackingArea = NSTrackingArea(
rect: bounds,
options: [NSTrackingAreaOptions.activeAlways, NSTrackingAreaOptions.mouseEnteredAndExited,/* NSTrackingAreaOptions.mouseMoved */],
owner: self,
userInfo: nil
)
addTrackingArea(trackingArea)
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSColor(red: 0.96, green: 0.96, blue: 0.96, alpha: 1.00).set()
// mouse hover
if highlight {
let path = NSBezierPath(rect: bounds)
path.fill()
}
// draw divider
let rect = NSRect(x: 0, y: bounds.height - 2, width: bounds.width, height: bounds.height)
let path = NSBezierPath(rect: rect)
path.fill()
}
private var highlight = false {
didSet {
setNeedsDisplay(bounds)
}
}
override func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event)
if !highlight {
highlight = true
}
}
override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
if highlight {
highlight = false
}
}
}

You need to create a subclass and use tracking areas. That's what buttons do to track mouse hovering.
There's an Apple Sample Code that does exactly what you need - highlighting rows on hover:
HoverTableDemo
It's been thoroughly discussed in WWDC 2011 Session 120

(Ignoring the "Mouse Over is bad GUI" sermon (which you'll ignore anyway… ;-))
#import "MoTableView.h"
#implementation MoTableView
{
NSUInteger mouseRow;
NSRect mouseRowFrame;
}
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
mouseRow = -1;
}
return self;
}
- (void)awakeFromNib
{
[self.window setAcceptsMouseMovedEvents:YES];
}
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
// Drawing code here.
[[NSColor redColor] set];
NSLogDebug(#"mouseRowFrame: %#", NSStringFromRect(mouseRowFrame));
NSFrameRectWithWidth(mouseRowFrame, 2.);
}
- (void)mouseMoved:(NSEvent *)theEvent
{
NSPoint mouseLocation = [theEvent locationInWindow];
NSPoint viewLocation = [self convertPoint:mouseLocation fromView:nil] ;
NSInteger row = [self rowAtPoint:viewLocation];
if (row != mouseRow) {
mouseRowFrame = [self rectOfRow:row];
[self setNeedsDisplay];
mouseRow = row;
}
}
#end

Related

Problems using long hold & single tap in game together

I'm making a game with two methods of moving. When tapping the screen the player moves up. The game world also moves towards the player looking like the bird is flying forward. When the user holds down the on the screen the player will start flying forward in its position. When the player doesn't touch the screen at all, the player falls (almost like flappy bird).
The game worked fine and smooth until I added the long press method. Because one second is too short, it gets detected as a tap. So when I tap the screen fast 2 times the long press method gets called as well. So I'm thinking there must be another way instead of using long press gesture?
Is there any way to programmatically detect touch up inside and touch and hold in spritekit? I know you could do this in UIKit singleview applications on buttons.
But I need to make it work anywhere on the screen and not just from tapping a button.
I'm using Spritekit as game technology and Xcode as Platform.
Any ideas?
Here is a full working solution for you. Tap to jump the block, hold the screen to move forward. when you touch a wall you will reset position.
Swift3:
class Bird: SKSpriteNode {
private let flyUpImpulse = CGVector(dx: 0, dy: 20)
private let flyForwardForce = CGVector(dx: 100, dy: 0)
// var pb: SKPhysicsBody { return self.physicsBody! }
func initialize() {
color = .blue
size = CGSize(width: 50, height: 50)
physicsBody = SKPhysicsBody(rectangleOf: size)
physicsBody!.categoryBitMask = UInt32(1)
}
func flyUp() {
physicsBody!.applyImpulse(flyUpImpulse)
}
func flyForward() {
physicsBody!.applyForce(flyForwardForce)
}
func resetPosition() {
guard let scene = self.scene else {
print("reset position failed: bird has not been added to scene yet!")
return
}
run(.move(to: CGPoint(x: scene.frame.minX + 50, y: scene.frame.midY), duration: 0))
physicsBody!.velocity = CGVector.zero
}
}
class GameScene: SKScene, SKPhysicsContactDelegate {
enum ControlToDo { case tap, longPress, none }
var controlToDo = ControlToDo.none
// How many frames we have to release screen in order to recognize a "tap".
// Less frames will give a faster response time, but may also give incorrect input:
let tapThreshold = 10
var isTouchingScreen = false
var frameCountSinceFirstTouch = 0
let bird = Bird()
// Scene setup:
override func didMove(to view: SKView) {
removeAllChildren() // Remove this from your actual project.
anchorPoint = CGPoint(x: 0.5, y: 0.5)
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsBody!.categoryBitMask = UInt32(2)
physicsBody!.contactTestBitMask = UInt32(1)
physicsWorld.gravity = CGVector(dx: 0, dy: -2)
physicsWorld.contactDelegate = self
bird.initialize()
addChild(bird)
}
// Touch handling stuff:
func tap() {
bird.flyUp()
controlToDo = .none
}
func longPress() {
bird.flyForward()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
isTouchingScreen = true
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
isTouchingScreen = false
if frameCountSinceFirstTouch > tapThreshold {
controlToDo = .none
frameCountSinceFirstTouch = 0
}
else if frameCountSinceFirstTouch < tapThreshold {
controlToDo = .tap
frameCountSinceFirstTouch = 0
}
}
override func update(_ currentTime: TimeInterval) {
// Increase counter if touching the screen:
if isTouchingScreen {
frameCountSinceFirstTouch += 1
}
// If we have held the screen for long enough, do a longPress:
if frameCountSinceFirstTouch > tapThreshold {
controlToDo = .longPress
}
switch controlToDo {
case .tap: tap()
case .longPress: longPress()
case .none: break
}
}
// Reset positon on touch of wall:
func didBegin(_ contact: SKPhysicsContact) {
bird.resetPosition()
}
}
Objective C:
// MAKE SURE YOU HAVE <SKPhysicsContactDelegate> in your .h file!
#import "GameScene.h"
#implementation GameScene {
SKSpriteNode *_bird;
/// 1 is tap, 2 is longpress (i couldn't figure out how to compare strings, or do an enum)
int _controlToDo;
CGFloat _tapThreshold;
CGFloat _frameCountSinceFirstTouch;
Boolean _isTouchingScreen;
}
- (void) setupProperties {
_controlToDo = 0;
_tapThreshold = 10;
_isTouchingScreen = false;
_frameCountSinceFirstTouch = 0;
}
- (void) setupScene {
self.anchorPoint = CGPointMake(0.5, 0.5);
[self removeAllChildren];
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
self.physicsBody.categoryBitMask = 2;
self.physicsBody.contactTestBitMask = 1;
self.physicsWorld.gravity = CGVectorMake(0, -2);
self.physicsWorld.contactDelegate = self;
_bird = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:CGSizeMake(50, 50)];
_bird.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_bird.size];
_bird.physicsBody.categoryBitMask = 1;
[self addChild:_bird];
}
- (void) birdResetPosition {
CGFloat min = 0 - (self.size.width / 2) + (_bird.size.width / 2) + 5;
CGPoint center = CGPointMake(min, 0);
SKAction *movement = [SKAction moveTo:center duration:0];
[_bird runAction:movement];
}
- (void)didMoveToView:(SKView *)view {
[self setupProperties];
[self setupScene];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
_isTouchingScreen = true;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
_isTouchingScreen = false;
if (_frameCountSinceFirstTouch > _tapThreshold) {
_controlToDo = 0;
}
else if (_frameCountSinceFirstTouch < _tapThreshold) {
_controlToDo = 1;
}
_frameCountSinceFirstTouch = 0;
}
-(void)update:(CFTimeInterval)currentTime {
// Increase counter if touching the screen:
if (_isTouchingScreen == true) {
_frameCountSinceFirstTouch += 1;
}
// If we have held the screen for long enough, do a longPress:
if (_frameCountSinceFirstTouch > _tapThreshold) {
_controlToDo = 2;
}
// Bird fly up:
if (_controlToDo == 1) {
[_bird.physicsBody applyImpulse:CGVectorMake(0, 20)];
_controlToDo = 0;
}
// Bird fly forward:
else if (_controlToDo == 2) {
[_bird.physicsBody applyForce:CGVectorMake(100, 0)];
}
}
- (void) didBeginContact:(SKPhysicsContact *)contact {
[self birdResetPosition];
_bird.physicsBody.velocity = CGVectorMake(0, 0);
}
#end
All SKNodes have access to touch events, you just need to enable userInteractionEnabled
By default, this is set to false, but if you are using an SKS file, then the SKS default is set to true
As long as you do not have any other node enabled, when you touch the scene, it will fire for the scene.
To get a hold event going, I would recommend using SKAction's on your scene.
Basically, we want to wait for a specific time period, then fire the event
If at any point the finger is removed, then we remove the action, not firing your event.
Swift:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let wait = SKAction.wait(forDuration:1)
let beginHoldEvent = SKAction(run:{//function to start hold event})
run(SKAction.sequence([wait,beginHoldEvent],withKey:"holding")
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
guard let _ = action(forKey:"holding") else {return}
removeAction(forKey:"holding")
//do single click event
}
Objective C:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
void (^holdBlock)(void) = ^{
//do holding code here
};
SKAction* wait = [SKAction waitForDuration:1];
SKAction* beginHoldEvent = [SKAction runBlock:holdBlock];
SKAction* seq = [SKAction sequence:#[wait,beghinHoldEvent]];
[self runAction:seq withKey:#"holding"];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if([self actionForKey:#"holding"] == nil){
//we are on a long touch do nothing
return;
}
[self removeActionForKey:#"holding")];
//do tap event
}
Now this code does not handle multiple touches, you need to handle that.

How can I create a button with a background color for tvOS while still showing focus?

All I want to do is add a background color to a button for all states. But I want to maintain the automatic focus shadow that you get "for free" when using a system button in the tvOS storyboard. So far I haven't been able to find a combination that allows this.
Alternatively, I would also be interested in a way to programmatically add the shadow when the button is focused, but short of subclassing the button (which I haven't yet tried), I don't know how to do that either.
You can add a shadow for your custom button like this:
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
context.nextFocusedView.layer.shadowOffset = CGSizeMake(0, 10);
context.nextFocusedView.layer.shadowOpacity = 0.6;
context.nextFocusedView.layer.shadowRadius = 15;
context.nextFocusedView.layer.shadowColor = [UIColor blackColor].CGColor;
context.previouslyFocusedView.layer.shadowOpacity = 0;
}
Wasn't happy with a simple colour change, so I made a custom button subclass to look more like the default animation you get with system buttons -
class CustomButton: UIButton
{
private var initialBackgroundColour: UIColor!
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
initialBackgroundColour = backgroundColor
}
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)
{
coordinator.addCoordinatedAnimations(
{
if self.focused
{
self.backgroundColor = UIColor.whiteColor()
UIView.animateWithDuration(0.2, animations:
{
self.transform = CGAffineTransformMakeScale(1.1, 1.1)
},
completion:
{
finished in
UIView.animateWithDuration(0.2, animations:
{
self.transform = CGAffineTransformIdentity
},
completion: nil)
})
}
else
{
self.backgroundColor = self.initialBackgroundColour
}
},
completion: nil)
}
}
Nothing too complicated, but gets the job done
Override didUpdateFocusInContext method and check if next focus view is button, if yes then customize its UI, and to set it back to orignal state check context.previousFocusedView was that button, something like below
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
if (context.nextFocusedView == _button)
{
// set background color
}
else if (context.previousFocusedView == _button)
{
// set background color to background
}
}
The Ultimate Solution with inspiration from SomaMan. Just subclass all your custom buttons and you Get this:
includes: on tap animation and release and drag away.
//
// CustomFocusButton.swift
//
import UIKit
class CustomFocusButton: UIButton {
let focusedScaleFactor : CGFloat = 1.2
let focusedShadowRadius : CGFloat = 10
let focusedShadowOpacity : Float = 0.25
let shadowColor = UIColor.blackColor().CGColor
let shadowOffSetFocused = CGSizeMake(0, 27)
let animationDuration = 0.2
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)
{
coordinator.addCoordinatedAnimations({
if self.focused{
UIView.animateWithDuration(self.animationDuration, animations:{ [weak self] () -> Void in
guard let weakSelf = self else {return}
weakSelf.transform = CGAffineTransformMakeScale(weakSelf.focusedScaleFactor, weakSelf.focusedScaleFactor)
weakSelf.clipsToBounds = false
weakSelf.layer.shadowOpacity = weakSelf.focusedShadowOpacity
weakSelf.layer.shadowRadius = weakSelf.focusedShadowRadius
weakSelf.layer.shadowColor = weakSelf.shadowColor
weakSelf.layer.shadowOffset = weakSelf.shadowOffSetFocused
},completion:{ [weak self] finished in
guard let weakSelf = self else {return}
if !finished{
weakSelf.transform = CGAffineTransformMakeScale(weakSelf.focusedScaleFactor, weakSelf.focusedScaleFactor)
weakSelf.clipsToBounds = false
weakSelf.layer.shadowOpacity = weakSelf.focusedShadowOpacity
weakSelf.layer.shadowRadius = weakSelf.focusedShadowRadius
weakSelf.layer.shadowColor = weakSelf.shadowColor
weakSelf.layer.shadowOffset = weakSelf.shadowOffSetFocused
}
})
} else {
UIView.animateWithDuration(self.animationDuration, animations:{ [weak self] () -> Void in
guard let weakSelf = self else {return}
weakSelf.clipsToBounds = true
weakSelf.transform = CGAffineTransformIdentity
}, completion: {[weak self] finished in
guard let weakSelf = self else {return}
if !finished{
weakSelf.clipsToBounds = true
weakSelf.transform = CGAffineTransformIdentity
}
})
}
}, completion: nil)
}
override func pressesBegan(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
UIView.animateWithDuration(animationDuration, animations: { [weak self] () -> Void in
guard let weakSelf = self else {return}
weakSelf.transform = CGAffineTransformIdentity
weakSelf.layer.shadowOffset = CGSizeMake(0, 10);
})
}
override func pressesCancelled(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
if focused{
UIView.animateWithDuration(animationDuration, animations: { [weak self] () -> Void in
guard let weakSelf = self else {return}
weakSelf.transform = CGAffineTransformMakeScale(weakSelf.focusedScaleFactor, weakSelf.focusedScaleFactor)
weakSelf.layer.shadowOffset = weakSelf.shadowOffSetFocused
})
}
}
override func pressesEnded(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
if focused{
UIView.animateWithDuration(animationDuration, animations: {[weak self] () -> Void in
guard let weakSelf = self else {return}
weakSelf.transform = CGAffineTransformMakeScale(weakSelf.focusedScaleFactor, weakSelf.focusedScaleFactor)
weakSelf.layer.shadowOffset = weakSelf.shadowOffSetFocused
})
}
}
}
I've found something better:
-(void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
[coordinator addCoordinatedAnimations:^{
if (self.focused) {
self.backgroundColor = [UIColor whiteColor];
}
else {
self.backgroundColor = [UIColor clearColor];
}
} completion:nil];
}
Swift 4 /tvOS11 and better:
Set the ButtonType in the Interface Builder button properties to "Plain".
Add this private extension to your class:
private extension UIImage {
static func imageWithColor(color: UIColor, size: CGSize) -> UIImage? {
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(rect)
let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
Then in your connected IBOutlet set the background image for the focused state of the button:
#IBOutlet weak var myButton: UIButton! {
didSet {
let backgroundImageSelected = UIImage.imageWithColor(color: .red, size: myButton.bounds.size)
myButton.setBackgroundImage(backgroundImageSelected, for: .focused)
}
}
You can use the UIButton method setBackgroundImage(image: UIImage?, forState state: UIControlState) and pass through an image that is a flat color and the state .Normal.
This image can easily be created programatically from a UIColor and a size of 1x1:
func getImageWithColor(color: UIColor, size: CGSize) -> UIImage {
let rect = CGRectMake(0, 0, size.width, size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
You can set the background image in storyboard to an image that contains the color you would like

iTunes Song Title Scrolling in Cocoa

I have searched extensively and cannot for the life of me find any information about how to achieve a similar effect to that of the iTunes song title scrolling if the text is too large in Cocoa. I have tried setting the bounds on a NSTextField to no avail. I have tried using NSTextView as well as various attempts at using NSScrollView. I am sure I am missing something simple but any help would be greatly appreciated. I am also hoping to not have to use CoreGraphics if at all possible.
Example, notice the "Base.FM http://www." text has been scrolled. If you need a better example open iTunes with a song with a rather large title and watch it scroll back and forth.
I would think surely there is a simple way to just create a marquee type effect with an NSTextField and an NSTimer, but alas.
I can see how this would be difficult if you're trying to shoehorn the functionality into an exist control. However, if you just start with a plain NSView, it's not that bad. I whipped this up in about 10 minutes...
//ScrollingTextView.h:
#import <Cocoa/Cocoa.h>
#interface ScrollingTextView : NSView {
NSTimer * scroller;
NSPoint point;
NSString * text;
NSTimeInterval speed;
CGFloat stringWidth;
}
#property (nonatomic, copy) NSString * text;
#property (nonatomic) NSTimeInterval speed;
#end
//ScrollingTextView.m
#import "ScrollingTextView.h"
#implementation ScrollingTextView
#synthesize text;
#synthesize speed;
- (void) dealloc {
[text release];
[scroller invalidate];
[super dealloc];
}
- (void) setText:(NSString *)newText {
[text release];
text = [newText copy];
point = NSZeroPoint;
stringWidth = [newText sizeWithAttributes:nil].width;
if (scroller == nil && speed > 0 && text != nil) {
scroller = [NSTimer scheduledTimerWithTimeInterval:speed target:self selector:#selector(moveText:) userInfo:nil repeats:YES];
}
}
- (void) setSpeed:(NSTimeInterval)newSpeed {
if (newSpeed != speed) {
speed = newSpeed;
[scroller invalidate];
scroller == nil;
if (speed > 0 && text != nil) {
scroller = [NSTimer scheduledTimerWithTimeInterval:speed target:self selector:#selector(moveText:) userInfo:nil repeats:YES];
}
}
}
- (void) moveText:(NSTimer *)timer {
point.x = point.x - 1.0f;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect {
// Drawing code here.
if (point.x + stringWidth < 0) {
point.x += dirtyRect.size.width;
}
[text drawAtPoint:point withAttributes:nil];
if (point.x < 0) {
NSPoint otherPoint = point;
otherPoint.x += dirtyRect.size.width;
[text drawAtPoint:otherPoint withAttributes:nil];
}
}
#end
Just drag an NSView onto your window in Interface Builder and change its class to "ScrollingTextView". Then (in code), you do:
[myScrollingTextView setText:#"This is the text I want to scroll"];
[myScrollingTextView setSpeed:0.01]; //redraws every 1/100th of a second
This is obviously pretty rudimentary, but it does the wrap around stuff that you're looking for and is a decent place to start.
For anyone looking for this in Swift 4, I have converted Dave's answer and added some more functionality.
import Cocoa
open class ScrollingTextView: NSView {
/// Text to scroll
open var text: NSString?
/// Font for scrolling text
open var font: NSFont?
/// Scrolling text color
open var textColor: NSColor = .headerTextColor
/// Determines if the text should be delayed before starting scroll
open var isDelayed: Bool = true
/// Spacing between the tail and head of the scrolling text
open var spacing: CGFloat = 20
/// Amount of time the text is delayed before scrolling
open var delay: TimeInterval = 2 {
didSet {
updateTraits()
}
}
/// Length of the scrolling text view
open var length: CGFloat = 0 {
didSet {
updateTraits()
}
}
/// Speed at which the text scrolls. This number is divided by 100.
open var speed: Double = 4 {
didSet {
updateTraits()
}
}
private var timer: Timer?
private var point = NSPoint(x: 0, y: 0)
private var timeInterval: TimeInterval?
private(set) var stringSize = NSSize(width: 0, height: 0) {
didSet {
point.x = 0
}
}
private var timerSpeed: Double? {
return speed / 100
}
private lazy var textFontAttributes: [NSAttributedString.Key: Any] = {
return [NSAttributedString.Key.font: font ?? NSFont.systemFont(ofSize: 14)]
}()
/**
Sets up the scrolling text view
- Parameters:
- string: The string that will be used as the text in the view
*/
open func setup(string: String) {
text = string as NSString
stringSize = text?.size(withAttributes: textFontAttributes) ?? NSSize(width: 0, height: 0)
setNeedsDisplay(NSRect(x: 0, y: 0, width: frame.width, height: frame.height))
updateTraits()
}
}
private extension ScrollingTextView {
func setSpeed(newInterval: TimeInterval) {
clearTimer()
timeInterval = newInterval
guard let timeInterval = timeInterval else { return }
if timer == nil, timeInterval > 0.0, text != nil {
if #available(OSX 10.12, *) {
timer = Timer.scheduledTimer(timeInterval: newInterval, target: self, selector: #selector(update(_:)), userInfo: nil, repeats: true)
guard let timer = timer else { return }
RunLoop.main.add(timer, forMode: .commonModes)
} else {
// Fallback on earlier versions
}
} else {
clearTimer()
point.x = 0
}
}
func updateTraits() {
clearTimer()
if stringSize.width > length {
guard let speed = timerSpeed else { return }
if #available(OSX 10.12, *), isDelayed {
timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { [weak self] timer in
self?.setSpeed(newInterval: speed)
})
} else {
setSpeed(newInterval: speed)
}
} else {
setSpeed(newInterval: 0.0)
}
}
func clearTimer() {
timer?.invalidate()
timer = nil
}
#objc
func update(_ sender: Timer) {
point.x = point.x - 1
setNeedsDisplay(NSRect(x: 0, y: 0, width: frame.width, height: frame.height))
}
}
extension ScrollingTextView {
override open func draw(_ dirtyRect: NSRect) {
if point.x + stringSize.width < 0 {
point.x += stringSize.width + spacing
}
textFontAttributes[NSAttributedString.Key.foregroundColor] = textColor
text?.draw(at: point, withAttributes: textFontAttributes)
if point.x < 0 {
var otherPoint = point
otherPoint.x += stringSize.width + spacing
text?.draw(at: otherPoint, withAttributes: textFontAttributes)
}
}
override open func layout() {
super.layout()
point.y = (frame.height - stringSize.height) / 2
}
}
Reference gist:
https://gist.github.com/NicholasBellucci/b5e9d31c47f335c36aa043f5f39eedb2

Best way to change the background color for an NSView

I'm looking for the best way to change the backgroundColor of an NSView. I'd also like to be able to set the appropriate alpha mask for the NSView. Something like:
myView.backgroundColor = [NSColor colorWithCalibratedRed:0.227f
green:0.251f
blue:0.337
alpha:0.8];
I notice that NSWindow has this method, and I'm not a big fan of the NSColorWheel, or NSImage background options, but if they are the best, willing to use.
Yeah, your own answer was right. You could also use Cocoa methods:
- (void)drawRect:(NSRect)dirtyRect {
// set any NSColor for filling, say white:
[[NSColor whiteColor] setFill];
NSRectFill(dirtyRect);
[super drawRect:dirtyRect];
}
In Swift:
class MyView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// #1d161d
NSColor(red: 0x1d/255, green: 0x16/255, blue: 0x1d/255, alpha: 1).setFill()
dirtyRect.fill()
}
}
An easy, efficient solution is to configure the view to use a Core Animation layer as its backing store. Then you can use -[CALayer setBackgroundColor:] to set the background color of the layer.
- (void)awakeFromNib {
self.wantsLayer = YES; // NSView will create a CALayer automatically
}
- (BOOL)wantsUpdateLayer {
return YES; // Tells NSView to call `updateLayer` instead of `drawRect:`
}
- (void)updateLayer {
self.layer.backgroundColor = [NSColor colorWithCalibratedRed:0.227f
green:0.251f
blue:0.337
alpha:0.8].CGColor;
}
That’s it!
If you are a storyboard lover, here is a way that you don't need any line of code.
Add NSBox as a subview to NSView and adjust NSBox's frame as the same with NSView.
In Storyboard or XIB change Title position to None, Box type to Custom, Border Type to "None", and Border color to whatever you like.
Here is a screenshot:
This is the result:
If you setWantsLayer to YES first, you can directly manipulate the layer background.
[self.view setWantsLayer:YES];
[self.view.layer setBackgroundColor:[[NSColor whiteColor] CGColor]];
Think I figured out how to do it:
- (void)drawRect:(NSRect)dirtyRect {
// Fill in background Color
CGContextRef context = (CGContextRef) [[NSGraphicsContext currentContext] graphicsPort];
CGContextSetRGBFillColor(context, 0.227,0.251,0.337,0.8);
CGContextFillRect(context, NSRectToCGRect(dirtyRect));
}
edit/update: Xcode 8.3.1 • Swift 3.1
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
}
}
}
usage:
let myView = NSView(frame: NSRect(x: 0, y: 0, width: 100, height: 100))
print(myView.backgroundColor ?? "none") // NSView's background hasn't been set yet = nil
myView.backgroundColor = .red // set NSView's background color to red color
print(myView.backgroundColor ?? "none")
view.addSubview(myView)
I went through all of these answers and none of them worked for me unfortunately. However, I found this extremely simple way, after about an hour of searching : )
myView.layer.backgroundColor = CGColorCreateGenericRGB(0, 0, 0, 0.9);
Best Solution :
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.wantsLayer = YES;
}
return self;
}
- (void)awakeFromNib
{
float r = (rand() % 255) / 255.0f;
float g = (rand() % 255) / 255.0f;
float b = (rand() % 255) / 255.0f;
if(self.layer)
{
CGColorRef color = CGColorCreateGenericRGB(r, g, b, 1.0f);
self.layer.backgroundColor = color;
CGColorRelease(color);
}
}
In Swift:
override func drawRect(dirtyRect: NSRect) {
NSColor.greenColor().setFill()
NSRectFill(dirtyRect)
super.drawRect(dirtyRect)
}
Use NSBox, which is a subclass of NSView, allowing us to easily style
Swift 3
let box = NSBox()
box.boxType = .custom
box.fillColor = NSColor.red
box.cornerRadius = 5
Without doubt the easiest way, also compatible with Color Set Assets:
Swift:
view.setValue(NSColor.white, forKey: "backgroundColor")
Objective-C:
[view setValue: NSColor.whiteColor forKey: "backgroundColor"];
Interface Builder:
Add a user defined attribute backgroundColor in the interface builder, of type NSColor.
Just set backgroundColor on the layer (after making the view layer backed).
view.wantsLayer = true
view.layer?.backgroundColor = CGColor.white
I tested the following and it worked for me (in Swift):
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.blackColor().colorWithAlphaComponent(0.5).CGColor
In Swift 3, you can create an extension to do it:
extension NSView {
func setBackgroundColor(_ color: NSColor) {
wantsLayer = true
layer?.backgroundColor = color.cgColor
}
}
// how to use
btn.setBackgroundColor(NSColor.gray)
In swift you can subclass NSView and do this
class MyView:NSView {
required init?(coder: NSCoder) {
super.init(coder: coder);
self.wantsLayer = true;
self.layer?.backgroundColor = NSColor.redColor().CGColor;
}
}
This supports changing systemwide appearance (turning dark mode on or off) while the application is running. You can also set the background colour in Interface Builder, if you set the class of the view to BackgroundColorView first.
class BackgroundColorView: NSView {
#IBInspectable var backgroundColor: NSColor? {
didSet { needsDisplay = true }
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
wantsLayer = true
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
wantsLayer = true
}
override var wantsUpdateLayer: Bool { return true }
override func updateLayer() {
layer?.backgroundColor = backgroundColor?.cgColor
}
}
Have a look at RMSkinnedView. You can set the NSView's background color from within Interface Builder.
Just small reusable class (Swift 4.1)
class View: NSView {
var backgroundColor: NSColor?
convenience init() {
self.init(frame: NSRect())
}
override func draw(_ dirtyRect: NSRect) {
if let backgroundColor = backgroundColor {
backgroundColor.setFill()
dirtyRect.fill()
} else {
super.draw(dirtyRect)
}
}
}
// Usage
let view = View()
view.backgroundColor = .white

Hide buttons from titlebar in Cocoa

The Apples Human Interface Guidelines say:
macOS Human Interface Guidelines: Panels
How do I make the very first titlebar in that image (with only a close button). Disabling both Resize and Minimize in IB only make the resize/minimize buttons get disabled. But I want them to disappear. How can I do that?
I believe this should work:
[[window standardWindowButton:NSWindowCloseButton] setHidden:YES];
[[window standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES];
[[window standardWindowButton:NSWindowZoomButton] setHidden:YES];
Swift code for accepted answer
window!.standardWindowButton(.miniaturizeButton)!.isHidden = true
window!.standardWindowButton(.zoomButton)!.isHidden = true
window!.standardWindowButton(.closeButton)!.isHidden = true
I also needed this but visible for mouse overs - Swift:
var trackingTag: NSTrackingRectTag?
override func mouseEntered(with theEvent: NSEvent) {
if trackingTag == theEvent.trackingNumber {
window!.standardWindowButton(.closeButton)!.alphaValue = 1.00
}
}
override func mouseExited(with theEvent: NSEvent) {
if trackingTag == theEvent.trackingNumber {
window!.standardWindowButton(.closeButton)!.alphaValue = 0.01
}
}
func updateTrackingAreas(_ establish : Bool) {
if let tag = trackingTag {
window!.standardWindowButton(.closeButton)!.removeTrackingRect(tag)
}
if establish, let closeButton = window!.standardWindowButton(.closeButton) {
trackingTag = closeButton.addTrackingRect(closeButton.bounds, owner: self, userData: nil, assumeInside: false)
}
}
override func windowDidLoad() {
window!.ignoresMouseEvents = false
updateTrackingAreas(true)
window!.standardWindowButton(.closeButton)!.alphaValue = 0.01
}
func windowShouldClose(_ sender: Any) -> Bool {
window!.ignoresMouseEvents = true
updateTrackingAreas(false)
return true
}
Visibility is needed but just slightly - 0.01 opacity, to have the tracking area effective.
another way is...
for (id subview in [self window].contentView.superview.subviews) {
if ([subview isKindOfClass:NSClassFromString(#"NSTitlebarContainerView")]) {
NSView *titlebarView = [subview subviews][0];
for (id button in titlebarView.subviews) {
if ([button isKindOfClass:[NSButton class]]) {
[button setHidden:YES];
}
}
}
}