Problems using long hold & single tap in game together - objective-c

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.

Related

UITapGestureRecognizer won't execute properly

In my current project, I am employing a UITapGestureRecognizer. I have coded it so that when the left side of the screen is tapped, an object moves, and when the right side of the screen is tapped, another object moves. However, the gesture recognizer is only working when the right side is tapped. Also, I would like to make it so that the SKActions can be executed if both sides are tapped simultaneously. I don't understand what I am doing wrong. My code is below. Thanks in advance
In ViewDidLoad
UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tap1:)];
[tap1 setNumberOfTapsRequired:1];
[self.view addGestureRecognizer:tap1];
view.multipleTouchEnabled = YES;
-(void)tap1:(UIGestureRecognizer *)gestureRecognizer {
SKNode *person11 = [self childNodeWithName:#"person1"];
SKNode *person2 = [self childNodeWithName:#"person2"];
CGPoint pt = [gestureRecognizer locationInView:self.view];
if (pt.x > (self.view.bounds.size.width/2))
{
if (person2.position.x == CGRectGetMidX(self.frame) + 400 ) {
SKAction *moveLeft2 = [SKAction moveTo:CGPointMake(CGRectGetMidX(self.frame) + 90, CGRectGetMidY(self.frame) + 200) duration:0.1f];
[person2 runAction:moveLeft2];
}
if (person2.position.x == CGRectGetMidX(self.frame) + 90 ) {
SKAction *moveRight2 = [SKAction moveTo:CGPointMake(CGRectGetMidX(self.frame) + 400, CGRectGetMidY(self.frame) + 200) duration:0.1f];
[person2 runAction:moveRight2];
}
}
if (pt.x < (self.view.bounds.size.width/2)){
if (person11.position.x == CGRectGetMidX(self.frame) - 90 ) {
SKAction *moveLeft2 = [SKAction moveTo:CGPointMake(CGRectGetMidX(self.frame) - 400, CGRectGetMidY(self.frame) + 200) duration:0.1f];
[person11 runAction:moveLeft2];
}
if (person11.position.x == CGRectGetMidX(self.frame) - 400 ) {
SKAction *moveRight2 = [SKAction moveTo:CGPointMake(CGRectGetMidX(self.frame) - 90, CGRectGetMidY(self.frame) + 200) duration:0.1f];
[person11 runAction:moveRight2];
}
}
}
Instead of a gesture recognizer, do the work in touchesEnded. Here's an example:
Swift
override func viewDidLoad() {
super.viewDidLoad()
self.view.multipleTouchEnabled = true
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
if(touch.locationInView(self.view).x<self.view.frame.midX) {
print("On the left half") // run the respective SKAction
} else {
print("On the right half") // run the respective SKAction
}
}
}
Objective C (should work, but I use Swift and translated on my phone, so this might not work perfectly)
-(void)viewDidLoad {
self.view.multipleTouchEnabled = YES
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (UITouch* touch in touches) {
if(touch.locationInView(self.view).x<self.view.frame.midX) {
printf("On the left half\n"); // run the respective SKAction
} else {
printf("On the right half\n"); // run the respective SKAction
}
}
}
This will do exactly what you want.
Pretty self explanatory: allows multiple touches, then, when the touch is released, it checks its relative horizontal location in the view (.x), and sees if it's on the left or right half of the view. No gesture recognizers needed!
I'll try to double check the Objective C code later, but the Swift definitely works.
If you have any questions, please tell me!
Try to put this (in Swift):
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Sorry I used Swift frequently... I guess you can simply find an built-in gestureRecognizer() on Objective-C too.

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

how to add new SKScene With in SKScene in SpritKit Swift

I have to add next SKScene in SpritKit in swift
override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!)
{
var objgamescen = GameScene()
//objgamescen.initWithSize(self.size, playerWon: true)
self.view .presentScene(objgamescen)
}
Then GameScene is Blanked and Self.frame is (0.0,0.0,1.0,1.0)
Multiple Scene Added in SKScene By This:
let skView = self.view as SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
var scene: HomeScene!
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
if UIScreen.mainScreen().bounds.size.width < 568 {
scene = HomeScene(size: CGSizeMake(480, 320))
}else {
scene = HomeScene(size: CGSizeMake(568, 320))
}
}else {
scene = HomeScene(size: CGSizeMake(1024, 768))
}
scene.scaleMode = .AspectFill
skView.presentScene(scene)
It sounds like you are just trying to restart the scene, you can do that by creating a function like this
func goToGameScene(){
let gameScene:GameScene = GameScene(size: self.view!.bounds.size) // create your new scene
let transition = SKTransition.fadeWithDuration(1.0) // create type of transition (you can check in documentation for more transtions)
gameScene.scaleMode = SKSceneScaleMode.Fill
self.view!.presentScene(gameScene, transition: transition)
}
Then you just call it where you want it to take place
goToGameScene()

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

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

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