How to detect fullscreen mode of AVPlayerViewController - ios8

How can I detect when the user press the expand icon of the AVPlayerViewController?
I want to know when the movie playing is entering the fullscreen mode.

It is also possible to observe bounds of playerViewController.contentOverlayView and compare that to [UIScreen mainScreen].bounds, e.g.:
self.playerViewController = [AVPlayerViewController new];
// do this after adding player VC as a child VC or in completion block of -presentViewController:animated:completion:
[self.playerViewController.contentOverlayView addObserver:self forKeyPath:#"bounds" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
...
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *, id> *)change context:(void *)context {
if (object == self.playerViewController.contentOverlayView) {
if ([keyPath isEqualToString:#"bounds"]) {
CGRect oldBounds = [change[NSKeyValueChangeOldKey] CGRectValue], newBounds = [change[NSKeyValueChangeNewKey] CGRectValue];
BOOL wasFullscreen = CGRectEqualToRect(oldBounds, [UIScreen mainScreen].bounds), isFullscreen = CGRectEqualToRect(newBounds, [UIScreen mainScreen].bounds);
if (isFullscreen && !wasFullscreen) {
if (CGRectEqualToRect(oldBounds, CGRectMake(0, 0, newBounds.size.height, newBounds.size.width))) {
NSLog(#"rotated fullscreen");
}
else {
NSLog(#"entered fullscreen");
}
}
else if (!isFullscreen && wasFullscreen) {
NSLog(#"exited fullscreen");
}
}
}
}

You can use KVO to observe the videoBounds property of your AVPlayerViewController instance.
Edit
The most basic example being
[_myPlayerViewController addObserver:self forKeyPath:#"videoBounds" options:0 context:nil];

In swift, both for audio and video:
Add this observer in a initialization function such as viewDidLoad, or didMoveToSuperview:
//Observe if player changed bounds and maybe went to fullscreen
playerController.contentOverlayView!.addObserver(self, forKeyPath: "bounds", options: NSKeyValueObservingOptions.New, context: nil)
and this function on the class:
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "bounds" {
let rect = change!["new"] as! NSValue
if let playerRect: CGRect = rect.CGRectValue() as CGRect {
if playerRect.size == UIScreen.mainScreen().bounds.size {
print("Player in full screen")
isVideoInFullScreen = true
} else {
print("Player not in full screen")
}
}
}
}

Swift 2.2
Create and use subclass of AVPlayerViewController.
class YouVideoPlayer: AVPlayerViewController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if view.bounds == contentOverlayView?.bounds {
//code
}
}

self.frameChecker = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(checkContetnOverlay) userInfo:nil repeats:YES];
-(void)checkContetnOverlay{
BOOL currentPlayerIsFullscreen = (self.avVideoPlayer.contentOverlayView.frame.size.width>1000 || self.avVideoPlayer.contentOverlayView.frame.size.height>1000);
//works with these values only for iPad
if (((PlayerViewController*)self.parentViewController).playerIsInfullScreen != currentPlayerIsFullscreen) {
if (currentPlayerIsFullscreen) {
NSLog(#"CUSTOM PLAYER (av) : changed to fullscreen");
self.avVideoPlayer.showsPlaybackControls = YES;
}else{
NSLog(#"CUSTOM PLAYER (av) : changed to minimised");
self.avVideoPlayer.showsPlaybackControls = YES;
}
}
}

Related

Detect if SKStoreReviewController has displayed app rating window on macOS

The way to ask for review
SKStoreReviewController.requestReview()
On iOS folks found pretty easy way how to find if rating window has been presented link (just by counting number of windows within the app)
How to reliably detect this on macOS? (Counting windows alternatives?)
Found that CGWindowListCopyWindowInfo + NSRunningApplication are the right things to inspect:
dispatch_time_t twoSecondsFromNow = DISPATCH_TIME_NOW + 2.0;
dispatch_after(twoSecondsFromNow, dispatch_get_main_queue(), ^{
//HERE REQUEST REVIEW
[SKStoreReviewController requestReview];
dispatch_time_t secondFromNow = DISPATCH_TIME_NOW + 1.0;
dispatch_after(secondFromNow, dispatch_get_main_queue(), ^{
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSArray <NSString*>*windowNames = [(__bridge NSArray *)windowList valueForKey:#"kCGWindowOwnerName"];
CFRelease(windowList);
if ([windowNames containsObject:#"storeuid"]) {
//REPORT TO PREFERENCES THAT WE SUCCESSFULLY ASKED FOR REVIEW
NSLog(#"Rating Window was presented");
}
});
});
Possible alternative (as of 10.14.2 with significant delays for deactivations) :
- (void)findRunningApplication
{
NSArray<NSRunningApplication *> *runningApplications = [[NSWorkspace sharedWorkspace] runningApplications];
for (NSRunningApplication *app in runningApplications) {
if ([[app bundleIdentifier] isEqualToString:#"com.apple.storeuid"]) {
[self setRunningApplication:app];
}
}
}
- (void)setRunningApplication:(NSRunningApplication *)runningApplication
{
if (runningApplication != _runningApplication) {
if (runningApplication == nil) {
[_runningApplication removeObserver:self forKeyPath:#"active"];
} else {
[runningApplication addObserver:self forKeyPath:#"active" options:NSKeyValueObservingOptionNew context:StoreInspectorKVOContext];
}
_runningApplication = runningApplication;
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if (context == StoreInspectorKVOContext) {
if (object == [self runningApplication] && [keyPath isEqualToString:#"active"]) {
[self runningApplicationActiveHasChanged];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
One can even get what rating has user clicked with global monitor event + position from cgwindow
NSEvent *monitor = [NSEvent addGlobalMonitorForEventsMatchingMask:(NSEventMaskLeftMouseDown)
handler:^(NSEvent *event) {
NSLog(#"%#",event);
}];
2019-01-13 23:58:12.727979+0100 testWindows2[11466:620853] NSEvent: type=LMouseDown loc=(994.746,172.551) time=103867.3 flags=0 win=0x0 winNum=4852 ctxt=0x0 evNum=9320 click=1 buttonNumber=0 pressure=1 deviceID:0x300000014400000 subtype=NSEventSubtypeTouch
2019-01-13 23:58:12.729521+0100 testWindows2[11466:620853] {
kCGWindowAlpha = 1;
kCGWindowBounds = {
Height = 157;
Width = 420;
X = 786;
Y = 23;
};
kCGWindowIsOnscreen = 1;
kCGWindowLayer = 0;
kCGWindowMemoryUsage = 1248;
kCGWindowNumber = 4590;
kCGWindowOwnerName = storeuid;
kCGWindowOwnerPID = 7017;
kCGWindowSharingState = 1;
kCGWindowStoreType = 1;
}

UICollectionView Auto Scroll Paging

my project have a UIcollectionView. This Horizontal paging and have four object. I want my collectionView Auto Scroll Paging. which use methods?
As for theory, I just explain the key point. For example, if you want to auto-cycle display 5 images in somewhere, just as ads bar. You can create a UICollectionView with 100 sections, and there're 5 items in every section. After creating UICollectionView, set its original position is equal to the 50th section 0th item(middle of MaxSections
) so that you can scroll to left or right. Don't worry it will ends, because I have reset the position equal to middle of MaxSections when the Timer runs. Never argue with me :" it also will ends when the user keep scrolling by himself" If one find there're only 5 images, will he keep scrolling!? I believe there is few such silly user in this world!
Note: In the following codes,the headerView is UICollectionView,headerNews is data. And I suggest set MaxSections = 100.
Add a NSTimer after custom collectionView(Ensure the UICollectionViewFlowLayout is properly set at first):
- (void)addTimer
{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3.0f target:self selector:#selector(nextPage) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer;
}
then implement #selector(nextPage):
- (void)nextPage
{
// 1.back to the middle of sections
NSIndexPath *currentIndexPathReset = [self resetIndexPath];
// 2.next position
NSInteger nextItem = currentIndexPathReset.item + 1;
NSInteger nextSection = currentIndexPathReset.section;
if (nextItem == self.headerNews.count) {
nextItem = 0;
nextSection++;
}
NSIndexPath *nextIndexPath = [NSIndexPath indexPathForItem:nextItem inSection:nextSection];
// 3.scroll to next position
[self.headerView scrollToItemAtIndexPath:nextIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}
last implement resetIndexPath method:
- (NSIndexPath *)resetIndexPath
{
// currentIndexPath
NSIndexPath *currentIndexPath = [[self.headerView indexPathsForVisibleItems] lastObject];
// back to the middle of sections
NSIndexPath *currentIndexPathReset = [NSIndexPath indexPathForItem:currentIndexPath.item inSection:MaxSections / 2];
[self.headerView scrollToItemAtIndexPath:currentIndexPathReset atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
return currentIndexPathReset;
}
In order to control NSTimer , you have to implement some other methods and UIScrollView's delegate methods:
- (void)removeTimer
{
// stop NSTimer
[self.timer invalidate];
// clear NSTimer
self.timer = nil;
}
// UIScrollView' delegate method
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[self removeTimer];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
[self addTimer];
}
Swift 2.0 version for #Elijah_Lam 's answer:
func addTimer()
{
timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "nextPage", userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
}
func nextPage()
{
var ar = cvImg.indexPathsForVisibleItems()
///indexPathsForVisibleItems()'s result is not sorted. Sort it by myself...
ar = ar.sort{if($0.section < $1.section)
{
return true
}
else if($0.section > $1.section)
{
return false
}
///in same section, compare the item
else if($0.item < $1.item)
{
return true
}
return false
}
var currentIndexPathReset = ar[1]
if(currentIndexPathReset.section > MaxSections)
{
currentIndexPathReset = NSIndexPath(forItem:0, inSection:0)
}
cvImg.scrollToItemAtIndexPath(currentIndexPathReset, atScrollPosition: UICollectionViewScrollPosition.Left, animated: true)
}
func removeTimer()
{
// stop NSTimer
timer.invalidate()
}
// UIScrollView' delegate method
func scrollViewWillBeginDragging(scrollView: UIScrollView)
{
removeTimer()
}
func scrollViewDidEndDragging(scrollView: UIScrollView,
willDecelerate decelerate: Bool)
{
addTimer()
}

Cocoa: Making NSTextField editable after a click and short delay (like renaming in Finder)

I cannot find a simple example of how to use an NSTextField to edit it's contents in place.
Exactly like in the Finder - you're able to click, and with a short delay the text field becomes editable.
It seems like it's some combination of the textField, it's cell, and the fieldEditor? Problem is I can't find the most basic example of how to do it.
I've tried subclassing NSTextField with a couple different tests but it hasn't worked:
#import "GWTextField.h"
#implementation GWTextField
- (id) initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
return self;
}
- (void) mouseDown:(NSEvent *)theEvent {
[super mouseDown:theEvent];
[self.cell editWithFrame:self.frame inView:self.superview editor:[self.cell fieldEditorForView:self] delegate:self event:theEvent];
//[self setEditable:TRUE];
//[self setSelectable:TRUE];
//[self selectText:nil];
[NSTimer scheduledTimerWithTimeInterval:.3 target:self selector:#selector(edit:) userInfo:nil repeats:FALSE];
}
- (void) edit:(id) sende {
NSLog(#"edit");
[[NSApplication sharedApplication].mainWindow makeFirstResponder:self];
[self selectText:nil];
}
#end
Any ideas?
Here's another solution with no NSCell - one user pointed out that NSCell is deprecated and will at some point be gone.
#import <Cocoa/Cocoa.h>
#interface EditTextField : NSTextField <NSTextDelegate,NSTextViewDelegate,NSTextFieldDelegate>
#property BOOL isEditing;
#property BOOL commitChangesOnEscapeKey;
#property BOOL editAfterDelay;
#property CGFloat delay;
#end
----
#import "EditTextField.h"
#interface EditTextField ()
#property NSObject <NSTextFieldDelegate,NSTextViewDelegate> * userDelegate;
#property NSString * originalStringValue;
#property NSTimer * editTimer;
#property NSTrackingArea * editTrackingArea;
#end
#implementation EditTextField
- (id) initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
[self defaultInit];
return self;
}
- (id) initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
[self defaultInit];
return self;
}
- (id) init {
self = [super init];
[self defaultInit];
return self;
}
- (void) defaultInit {
self.delay = .8;
}
- (void) mouseDown:(NSEvent *) theEvent {
if(theEvent.clickCount == 2) {
[self startEditing];
} else {
[super mouseDown:theEvent];
if(self.editAfterDelay) {
[self startTracking];
self.editTimer = [NSTimer scheduledTimerWithTimeInterval:.8 target:self selector:#selector(startEditing) userInfo:nil repeats:FALSE];
}
}
}
- (void) startTracking {
if(!self.editTrackingArea) {
self.editTrackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds options:NSTrackingMouseEnteredAndExited|NSTrackingMouseMoved|NSTrackingActiveInActiveApp|NSTrackingAssumeInside|NSTrackingInVisibleRect owner:self userInfo:nil];
}
[self addTrackingArea:self.editTrackingArea];
}
- (void) mouseExited:(NSEvent *)theEvent {
[self.editTimer invalidate];
self.editTimer = nil;
}
- (void) mouseMoved:(NSEvent *) theEvent {
[self.editTimer invalidate];
self.editTimer = nil;
}
- (void) startEditing {
id firstResponder = self.window.firstResponder;
if([firstResponder isKindOfClass:[NSTextView class]]) {
NSTextView * tv = (NSTextView *)firstResponder;
if(tv.delegate && [tv.delegate isKindOfClass:[EditTextField class]]) {
EditTextField * fr = (EditTextField *)tv.delegate;
[fr stopEditingCommitChanges:FALSE clearFirstResponder:FALSE];
}
}
if(self.delegate != self) {
self.userDelegate = (NSObject <NSTextFieldDelegate,NSTextViewDelegate> *)self.delegate;
}
self.isEditing = TRUE;
self.delegate = self;
self.editable = TRUE;
self.originalStringValue = self.stringValue;
[self.window makeFirstResponder:self];
}
- (void) stopEditingCommitChanges:(BOOL) commitChanges clearFirstResponder:(BOOL) clearFirstResponder {
self.editable = FALSE;
self.isEditing = FALSE;
self.delegate = nil;
[self removeTrackingArea:self.editTrackingArea];
if(!commitChanges) {
self.stringValue = self.originalStringValue;
}
if(clearFirstResponder) {
[self.window makeFirstResponder:nil];
}
}
- (void) cancelOperation:(id) sender {
if(self.commitChangesOnEscapeKey) {
[self stopEditingCommitChanges:TRUE clearFirstResponder:TRUE];
} else {
[self stopEditingCommitChanges:FALSE clearFirstResponder:TRUE];
}
}
- (BOOL) textView:(NSTextView *) textView doCommandBySelector:(SEL) commandSelector {
BOOL handlesCommand = FALSE;
NSString * selector = NSStringFromSelector(commandSelector);
if(self.userDelegate) {
if([self.userDelegate respondsToSelector:#selector(control:textView:doCommandBySelector:)]) {
handlesCommand = [self.userDelegate control:self textView:textView doCommandBySelector:commandSelector];
} else if([self.userDelegate respondsToSelector:#selector(textView:doCommandBySelector:)]) {
handlesCommand = [self.userDelegate textView:textView doCommandBySelector:commandSelector];
}
if(!handlesCommand) {
if([selector isEqualToString:#"insertNewline:"]) {
[self stopEditingCommitChanges:TRUE clearFirstResponder:TRUE];
handlesCommand = TRUE;
}
if([selector isEqualToString:#"insertTab:"]) {
[self stopEditingCommitChanges:TRUE clearFirstResponder:FALSE];
handlesCommand = FALSE;
}
}
} else {
if([selector isEqualToString:#"insertNewline:"]) {
[self stopEditingCommitChanges:TRUE clearFirstResponder:TRUE];
handlesCommand = TRUE;
}
if([selector isEqualToString:#"insertTab:"]) {
[self stopEditingCommitChanges:TRUE clearFirstResponder:FALSE];
handlesCommand = FALSE;
}
}
return handlesCommand;
}
#end
I built a re-usable NSTextField subclass you can use for edit in place functionality. http://pastebin.com/QymunMYB
I came up with a better solution to the edit in place problem. I believe this is how to properly do edit in place with NSCell. Please show and tell if this is wrong.
#import <Cocoa/Cocoa.h>
#interface EditTextField : NSTextField <NSTextDelegate>
#end
---
#import "EditTextField.h"
#implementation EditTextField
- (void) mouseDown:(NSEvent *)theEvent {
if(theEvent.clickCount == 2) {
self.editable = TRUE;
NSText * fieldEditor = [self.window fieldEditor:TRUE forObject:self];
[self.cell editWithFrame:self.bounds inView:self editor:fieldEditor delegate:self event:theEvent];
} else {
[super mouseDown:theEvent];
}
}
- (void) cancelOperation:(id)sender {
[self.cell endEditing:nil];
self.editable = FALSE;
}
- (BOOL) textView:(NSTextView *) textView doCommandBySelector:(SEL) commandSelector {
NSString * selector = NSStringFromSelector(commandSelector);
if([selector isEqualToString:#"insertNewline:"]) {
NSText * fieldEditor = [self.window fieldEditor:TRUE forObject:self];
[self.cell endEditing:fieldEditor];
self.editable = FALSE;
return TRUE;
}
return FALSE;
}
#end
In my application I have two text fields - one non editable, and second, hidden, editable, and activates title editing by calling:
[self addSubview:windowTitle];
[windowTitleLabel removeFromSuperview];
[self.window makeFirstResponder:windowTitle];
This is called from mouseUp: on view behind the label.
I don't remember why I needed to have two text fields (i didn't know Cocoa good that time), probably it will work even without label swapping.

Moving borderless NSWindow fully covered with Web View

In my COCOA application I have implemented a custom borderless window. The content area of the Window is fully covered by a WebView. I want this borderless window to move when user clicks and drag mouse anywhere in the content area. I tried by overriding isMovableByWindowBackground but no use. How can I fix this problem?
Calling -setMovableByWindowBackround:YES on the WebView and making the window textured might work.
This is how I did it.
#import "BorderlessWindow.h"
#implementation BorderlessWindow
#synthesize initialLocation;
- (id)initWithContentRect:(NSRect)contentRect
styleMask:(NSUInteger)windowStyle
backing:(NSBackingStoreType)bufferingType
defer:(BOOL)deferCreation
{
if((self = [super initWithContentRect:contentRect
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO]))
{
return self;
}
return nil;
}
- (BOOL) canBecomeKeyWindow
{
return YES;
}
- (BOOL) acceptsFirstResponder
{
return YES;
}
- (NSTimeInterval)animationResizeTime:(NSRect)newWindowFrame
{
return 0.1;
}
- (void)sendEvent:(NSEvent *)theEvent
{
if([theEvent type] == NSKeyDown)
{
if([theEvent keyCode] == 36)
return;
}
if([theEvent type] == NSLeftMouseDown)
[self mouseDown:theEvent];
else if([theEvent type] == NSLeftMouseDragged)
[self mouseDragged:theEvent];
[super sendEvent:theEvent];
}
- (void)mouseDown:(NSEvent *)theEvent
{
self.initialLocation = [theEvent locationInWindow];
}
- (void)mouseDragged:(NSEvent *)theEvent
{
NSRect screenVisibleFrame = [[NSScreen mainScreen] visibleFrame];
NSRect windowFrame = [self frame];
NSPoint newOrigin = windowFrame.origin;
NSPoint currentLocation = [theEvent locationInWindow];
if(initialLocation.y > windowFrame.size.height - 40)
{
newOrigin.x += (currentLocation.x - initialLocation.x);
newOrigin.y += (currentLocation.y - initialLocation.y);
if ((newOrigin.y + windowFrame.size.height) > (screenVisibleFrame.origin.y + screenVisibleFrame.size.height))
{
newOrigin.y = screenVisibleFrame.origin.y + (screenVisibleFrame.size.height - windowFrame.size.height);
}
[self setFrameOrigin:newOrigin];
}
}
#end
And .h file:
#import <Cocoa/Cocoa.h>
#interface BorderlessWindow : NSWindow {
NSPoint initialLocation;
}
- (id)initWithContentRect:(NSRect)contentRect
styleMask:(NSUInteger)windowStyle
backing:(NSBackingStoreType)bufferingType
defer:(BOOL)deferCreation;
#property (assign) NSPoint initialLocation;
#end
Since this is the top hit on Google...the provided approach didn't work for me as WKWebView intercepts the mouse events before they reach the window. I had to instead create a subclass of WKWebView and do the work there (h/t to Apple's Photo Editor/WindowDraggableButton.swift example).
I use Xamarin, but the code is pretty simple...here are the important bits:
// How far from the top of the window you are allowed to grab the window
// to begin the drag...the title bar height, basically
public Int32 DraggableAreaHeight { get; set; } = 28;
public override void MouseDown(NSEvent theEvent)
{
base.MouseDown(theEvent);
var clickLocation = theEvent.LocationInWindow;
var windowHeight = Window.Frame.Height;
if (clickLocation.Y > (windowHeight - DraggableAreaHeight))
_dragShouldRepositionWindow = true;
}
public override void MouseUp(NSEvent theEvent)
{
base.MouseUp(theEvent);
_dragShouldRepositionWindow = false;
}
public override void MouseDragged(NSEvent theEvent)
{
base.MouseDragged(theEvent);
if (_dragShouldRepositionWindow)
{
this.Window.PerformWindowDrag(theEvent);
}
}
#starkos porvided the correct answer at https://stackoverflow.com/a/54987061/140927 The following is just the ObjC implementation in a subclass of WKWebView:
BOOL _dragShouldRepositionWindow = NO;
- (void)mouseDown:(NSEvent *)event {
[super mouseDown:event];
NSPoint loc = event.locationInWindow;
CGFloat height = self.window.frame.size.height;
if (loc.y > height - 28) {
_dragShouldRepositionWindow = YES;
}
}
- (void)mouseUp:(NSEvent *)event {
[super mouseUp:event];
_dragShouldRepositionWindow = NO;
}
- (void)mouseDragged:(NSEvent *)event {
[super mouseDragged:event];
if (_dragShouldRepositionWindow) {
[self.window performWindowDragWithEvent:event];
}
}
For further info about how to manipulate the title bar, see https://github.com/lukakerr/NSWindowStyles

How to add animated icon to OS X status bar?

I want to put an icon in Mac OS status bar as part of my cocoa application. What I do right now is:
NSStatusBar *bar = [NSStatusBar systemStatusBar];
sbItem = [bar statusItemWithLength:NSVariableStatusItemLength];
[sbItem retain];
[sbItem setImage:[NSImage imageNamed:#"Taski_bar_icon.png"]];
[sbItem setHighlightMode:YES];
[sbItem setAction:#selector(stopStart)];
but if I want the icon to be animated (3-4 frames), how do I do it?
You'll need to repeatedly call -setImage: on your NSStatusItem, passing in a different image each time. The easiest way to do this would be with an NSTimer and an instance variable to store the current frame of the animation.
Something like this:
/*
assume these instance variables are defined:
NSInteger currentFrame;
NSTimer* animTimer;
*/
- (void)startAnimating
{
currentFrame = 0;
animTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/30.0 target:self selector:#selector(updateImage:) userInfo:nil repeats:YES];
}
- (void)stopAnimating
{
[animTimer invalidate];
}
- (void)updateImage:(NSTimer*)timer
{
//get the image for the current frame
NSImage* image = [NSImage imageNamed:[NSString stringWithFormat:#"image%d",currentFrame]];
[statusBarItem setImage:image];
currentFrame++;
if (currentFrame % 4 == 0) {
currentFrame = 0;
}
}
I re-wrote Rob's solution so that I can reuse it:
I have number of frames 9 and all the images name has last digit as frame number so that I can reset the image each time to animate the icon.
//IntervalAnimator.h
#import <Foundation/Foundation.h>
#protocol IntervalAnimatorDelegate <NSObject>
- (void)onUpdate;
#end
#interface IntervalAnimator : NSObject
{
NSInteger numberOfFrames;
NSInteger currentFrame;
__unsafe_unretained id <IntervalAnimatorDelegate> delegate;
}
#property(assign) id <IntervalAnimatorDelegate> delegate;
#property (nonatomic) NSInteger numberOfFrames;
#property (nonatomic) NSInteger currentFrame;
- (void)startAnimating;
- (void)stopAnimating;
#end
#import "IntervalAnimator.h"
#interface IntervalAnimator()
{
NSTimer* animTimer;
}
#end
#implementation IntervalAnimator
#synthesize numberOfFrames;
#synthesize delegate;
#synthesize currentFrame;
- (void)startAnimating
{
currentFrame = -1;
animTimer = [NSTimer scheduledTimerWithTimeInterval:0.50 target:delegate selector:#selector(onUpdate) userInfo:nil repeats:YES];
}
- (void)stopAnimating
{
[animTimer invalidate];
}
#end
How to use:
Conform to protocol in your StatusMenu class
//create IntervalAnimator object
animator = [[IntervalAnimator alloc] init];
[animator setDelegate:self];
[animator setNumberOfFrames:9];
[animator startAnimating];
Implement protocol method
-(void)onUpdate {
[animator setCurrentFrame:([animator currentFrame] + 1)%[animator numberOfFrames]];
NSImage* image = [NSImage imageNamed:[NSString stringWithFormat:#"icon%ld", (long)[animator currentFrame]]];
[statusItem setImage:image];
}
Just had to do something similar recently in a simple project, so I'm posting my personal version written in Swift:
class StatusBarIconAnimationUtils: NSObject {
private var currentFrame = 0
private var animTimer : Timer
private var statusBarItem: NSStatusItem!
private var imageNamePattern: String!
private var imageCount : Int!
init(statusBarItem: NSStatusItem!, imageNamePattern: String, imageCount: Int) {
self.animTimer = Timer.init()
self.statusBarItem = statusBarItem
self.imageNamePattern = imageNamePattern
self.imageCount = imageCount
super.init()
}
func startAnimating() {
stopAnimating()
currentFrame = 0
animTimer = Timer.scheduledTimer(timeInterval: 5.0 / 30.0, target: self, selector: #selector(self.updateImage(_:)), userInfo: nil, repeats: true)
}
func stopAnimating() {
animTimer.invalidate()
setImage(frameCount: 0)
}
#objc private func updateImage(_ timer: Timer?) {
setImage(frameCount: currentFrame)
currentFrame += 1
if currentFrame % imageCount == 0 {
currentFrame = 0
}
}
private func setImage(frameCount: Int) {
let imagePath = "\(imageNamePattern!)\(frameCount)"
print("Switching image to: \(imagePath)")
let image = NSImage(named: NSImage.Name(imagePath))
image?.isTemplate = true // best for dark mode
DispatchQueue.main.async {
self.statusBarItem.button?.image = image
}
}
}
Usage:
private let statusItem =
NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
// Assuming we have a set of images with names: statusAnimatedIcon0, ..., statusAnimatedIcon6
private lazy var iconAnimation =
StatusBarIconAnimationUtils.init(statusBarItem: statusItem, imageNamePattern: "statusAnimatedIcon",
imageCount: 7)
private func startAnimation() {
iconAnimation.startAnimating()
}
private func stopAnimating() {
iconAnimation.stopAnimating()
}

Resources