How to update interface when main thread is busy ? - xcode

I have an NSView who draw something every second. Also, i have a setting window with some controls. The problem is that: When i want change settings, my view blocked.
What can i do?
For update the view I use NSTimer :
NSTimer.scheduledTimerWithTimeInterval(refreshInterval, target: self, selector:"updateView", userInfo: nil, repeats: true)
and, in updateView function I call myView.needsDisplay = true ;
Is very annoying to try open an popUpButton and myView to freeze.
Have you any idea what can I do?
Thanks!

Related

MAC Cocoa - Programmatically set window size

I have a one window application that has some checkboxes on the screen.
I use NSUserDefaults to store not only the state of the checkboxes but also the main window width, height, and position (x/y).
My issue is to find the right event to read and set the window properties.
Currently I do it at:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// read preferences
UserPreferences *userPrefs = [[UserPreferences alloc] init];
NSRect oldFrame = [window frame];
if( [userPrefs MainWindowWidth] > 0)
oldFrame.size.width = [userPrefs MainWindowWidth];
if( [userPrefs MainWindowHeight] > 0)
oldFrame.size.height = [userPrefs MainWindowHeight];
if( [userPrefs MainWindowTop] > 0)
oldFrame.origin.y = [userPrefs MainWindowTop];
if( [userPrefs MainWindowLeft] > 0)
oldFrame.origin.x = [userPrefs MainWindowLeft];
// set windows properties
[window setFrame:oldFrame display:YES animate:NO];
}
It works but the screen first shows default size and then changes to the stored size so visually a hiccup. This tells me that its too late in the event chain to set these parameters.
I also tried awakefromnib but that seems too early in the chain since setting width and height is simply ignored.
Which event would be the right one to plug this code in to reset the window right before it is show on screen?
Any advise would be appreciated. Every beginning is hard.
thank you.
This is because window's frame is first loaded from nib, and then window is shown (once finished loading from nib).
You can disable 'show window on start' checkbox in interface builder, and show it manually in applicationDidFinishLaunching.
The applicationDidFinishLaunching function is a place to do things, well... as soon as the app has finished launching. But what you really want is to catch the window at the time when it has just been loaded from the nib, but before it has shown. IOW, you're trying to do this in the wrong place.
You need more control over your window, so... create your own window controller! Create your own class which inherits from NSWindowController, say MyWindTrol. In the implementation file, add the awakeFromNib function, and put your efforts to control your window's size and location in there.
In your nib file, drag an NSObject from the library, declare it to be of class MyWindTrol, and control-drag the connections so that your MyWindTrol object's window property points to the window object.

NSButton setAction selector

I just want to add a NSButton with setAction Arguments.
NSRect frame = NSMakeRect(10, 40, 90, 40);
NSButton* pushButton = [[NSButton alloc] initWithFrame: frame];
[pushButton setTarget:self];
[pushButton setAction:#selector(myAction:)];
But I want to put an argument to the function myAction...
How ?
Thanks.
But I want to put an argument to the function myAction...
How ?
You can't.
… if there is more than one button that uses this method, we can not differentiate the sender (only with title)...
There are three ways to tell which button (or other control) is talking to you:
Assign each button (or other control) a tag, and compare the tags in your action method. When you create controls in a nib, this has the downside that you have to write the tag twice (once in the code, once in the nib). Since you're writing out the button by hand from scratch, you don't have that problem.
Have an outlet to every control that you expect to send you this message, and compare the sender to each outlet.
Have different action methods, with each control being the only one wired up to each action. Each action method then does not need to determine which control sent you that message, because you already know that by which method it is.
The problem with tags is the aforementioned repetitiveness. It's also very easy to neglect to name each tag, so you end up looking at code like if ([sender tag] == 42) and not knowing/having to look up which control is #42.
The problem with outlets is that your action method may get very long, and anyway is probably doing multiple different things that have no business being in the same method. (Which is also a problem with tags.)
So, I generally prefer the third solution. Create an action method for every button (or other control) that will have you as its target. You'll typically name the method and the button the same (like save: and “Save”) or something very similar (like terminate: and “Quit”), so you'll know just by reading each method which button it belongs to.
I never programatically created an NSButton, but I think that you just need to create a method like this:
- (void) myAction: (NSButton*)button{
//your code
}
And that's it !!
You can use associated Objects for passing arguments.
You can refer : http://labs.vectorform.com/2011/07/objective-c-associated-objects/
http://www.cocoanetics.com/2012/06/associated-objects/
.tag should be sufficient if your object have any integer uniqueID.
I use .identifier instead, since it support string based uniqueID.
Example:
...
for (index, app) in apps.enumerated() {
let appButton = NSButton(title: app.title, target: self, action: #selector(appButtonPressed))
appButton.identifier = NSUserInterfaceItemIdentifier(rawValue: app.guid)
}
...
#objc func appButtonPressed(sender: NSButton) {
print(sender.identifier?.rawValue)
}

Show number on label, wait and show another. Xcode tools

I need some help with xcode...
I need to show a value on label with [label1 setIntValue: someInt] wait some secs and do again [label1 setIntValue: otherInt]. I tried with sleep() but the ui stucks and only the second int is shown. What I need to do? Thank you very much!
What you need to do is to set up a timer. After setting the first value on the label, use this:
NSTimer* t = [NSTimer scheduledTimerWithTimeInterval:1 target: self selector:#selector(timerEnded) userInfo: nil repeats:NO];
In this case you're waiting 1 second before triggering timerEnded. So, after this, create the timerEnded method. This is the method that gets called after 1 second.
-(void)timerEnded{
//set value to label
}

NSTimer - set up plain vanilla -- doesn't fire

Compiling in XCode 3.1.1 for OSX 10.5.8 target, 32-bit and i386 build.
I have a modal run loop, running in NSWindow wloop and NSView vloop. The modal loop is started first. It starts, runs and stops as expected. Here's the start:
[NSApp runModalForWindow: wloop];
Then, when the user presses the left mouse button, I do this:
if (ticking == 0) // ticking is set to zero in its definition, so starts that way
{
ticking = 1; // don't want to do this more than once per loop
tickCounter = 0;
cuckCoo = [NSTimer scheduledTimerWithTimeInterval: 1.0f / 10.0f // 10x per second
target: self // method is here in masterView
selector: #selector(onTick:) // method
userInfo: nil // not used
repeats: YES]; // should repeat
}
Checking the return of the call, I do get a timer object, and can confirm that the timer call is made when I expect it to be.
Now, according to the docs, the resulting NSTimer, stored globally as "cuckCoo", should be added to the current run loop automagically. The current run loop is definitely the modal one - at this time other windows are locked out and only the window with the intended mouse action is taking messages.
The method that this calls, "onTick", is very simple (because I can't get it to fire), located in the vloop NSView code, which is where all of this is going on:
- (void) onTick:(NSTimer*)theTimer
{
tickCounter += 1;
NSLog(#"Timer started");
}
Then when it's time to stop the modal loop (which works fine, btw), I do this:
[cuckCoo invalidate];
[NSApp stop: nil];
ticking=0;
cuckCoo = NULL;
NSLog(#"tickCounter=%ld",tickCounter);
ticking and tickCounter are both global longs.
I don't get the NSLog message from within onTick, and tickCounter remains at zero as reported by the NSLog at the close of the runloop.
All this compiles and runs fine. I just never get any ticks. I'm at a complete loss. Any ideas, anyone?
The problem is related to this statement "The current run loop is definitely the modal one". In Cocoa, each thread has at most one runloop, and each runloop can be run in a variety of "modes". Typical modes are default, event tracking, and modal. Default is the mode the loop normally runs in, while event tracking is typically used to track a drag session of the mouse, and modal is used for things like modal panels.
When you invoke -[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:] it does schedule the timer immediately, but it only schedules it for the default runloop mode, not the modal runloop mode. The idea behind this is that the app generally shouldn't continue to run behind a modal panel.
To create a timer that fires during a modal runloop, you can use -[NSTimer initWithFireDate:interval:target:selector:userInfo:repeats:] and then -[NSRunLoop addTimer:forMode:].
The answer specific to...
[NSApp runModalForWindow: wloop];
...is, after the modal run loop has been entered:
NSRunLoop *crl;
cuckCoo = [NSTimer timerWithTimeInterval: 1.0 / 10
target: self
selector: #selector(onTick:)
userInfo: nil
repeats:YES];
crl = [NSRunLoop currentRunLoop];
[crl addTimer: cuckCoo forMode: NSModalPanelRunLoopMode];
(crl obtained separately for clarity) Where the onTick method has the form:
- (void) onTick:(NSTimer*)theTimer
{
// do something tick-tocky
}

Distinguishing a single click from a double click in Cocoa on the Mac

I have a custom NSView (it's one of many and they all live inside an NSCollectionView — I don't think that's relevant, but who knows). When I click the view, I want it to change its selection state (and redraw itself accordingly); when I double-click the view, I want it to pop up a larger preview window for the object that was just double-clicked.
My first looked like this:
- (void)mouseUp: (NSEvent *)theEvent {
if ([theEvent clickCount] == 1) [model setIsSelected: ![model isSelected]];
else if ([theEvent clickCount] == 2) if ([model hasBeenDownloaded]) [mainWindowController showPreviewWindowForPicture:model];
}
which mostly worked fine. Except, when I double-click the view, the selection state changes and the window pops up. This is not exactly what I want.
It seems like I have two options. I can either revert the selection state when responding to a double-click (undoing the errant single-click) or I can finagle some sort of NSTimer solution to build in a delay before responding to the single click. In other words, I can make sure that a second click is not forthcoming before changing the selection state.
This seemed more elegant, so it was the approach I took at first. The only real guidance I found from Google was on an unnamed site with a hyphen in its name. This approach mostly works with one big caveat.
The outstanding question is "How long should my NSTimer wait?". The unnamed site suggests using the Carbon function GetDblTime(). Aside from being unusable in 64-bit apps, the only documentation I can find for it says that it's returning clock-ticks. And I don't know how to convert those into seconds for NSTimer.
So what's the "correct" answer here? Fumble around with GetDblTime()? "Undo" the selection on a double-click? I can't figure out the Cocoa-idiomatic approach.
Delaying the changing of the selection state is (from what I've seen) the recommended way of doing this.
It's pretty simple to implement:
- (void)mouseUp:(NSEvent *)theEvent
{
if([theEvent clickCount] == 1) {
[model performSelector:#selector(toggleSelectedState) afterDelay:[NSEvent doubleClickInterval]];
}
else if([theEvent clickCount] == 2)
{
if([model hasBeenDownloaded])
{
[NSRunLoop cancelPreviousPerformRequestsWithTarget: model];
[mainWindowController showPreviewWindowForPicture:model];
}
}
}
(Notice that in 10.6, the double click interval is accessible as a class method on NSEvent)
If your single-click and double-click operations are really separate and unrelated, you need to use a timer on the first click and wait to see if a double-click is going to happen. That is true on any platform.
But that introduces an awkward delay in your single-click operation that users typically don't like. So you don't see that approach used very often.
A better approach is to have your single-click and double-click operations be related and complementary. For example, if you single-click an icon in Finder it is selected (immediately), and if you double-click an icon it is selected and opened (immediately). That is the behavior you should aim for.
In other words, the consequences of a single-click should be related to your double-click command. That way, you can deal with the effects of the single-click in your double-click handler without having to resort to using a timer.
Personally, I think you need to ask yourself why you want this non-standard behaviour.
Can you point to any other application which treats the first click in a double-click as being different from a single-click? I can't think of any...
Add two properties to your custom view.
// CustomView.h
#interface CustomView : NSView {
#protected
id m_target;
SEL m_doubleAction;
}
#property (readwrite) id target;
#property (readwrite) SEL doubleAction;
#end
Overwrite the mouseUp: method in your custom view.
// CustomView.m
#pragma mark - MouseEvents
- (void)mouseUp:(NSEvent*)event {
if (event.clickCount == 2) {
if (m_target && m_doubleAction && [m_target respondsToSelector:m_doubleAction]) {
[m_target performSelector:m_doubleAction];
}
}
}
Register your controller as the target with an doubleAction.
// CustomController.m
- (id)init {
self = [super init];
if (self) {
// Register self for double click events.
[(CustomView*)m_myView setTarget:self];
[(CustomView*)m_myView setDoubleAction:#selector(doubleClicked:)];
}
return self;
}
Implement what should be done when a double click happens.
// CustomController.m
- (void)doubleClicked:(id)sender {
// DO SOMETHING.
}
#Dave DeLong's solution in Swift 4.2 (Xcode 10, macOS 10.13), amended for use with event.location(in: view)
var singleClickPoint: CGPoint?
override func mouseDown(with event: NSEvent) {
singleClickPoint = event.location(in: self)
perform(#selector(GameScene.singleClickAction), with: nil, afterDelay: NSEvent.doubleClickInterval)
if event.clickCount == 2 {
RunLoop.cancelPreviousPerformRequests(withTarget: self)
singleClickPoint = nil
//do whatever you want on double-click
}
}
#objc func singleClickAction(){
guard let singleClickPoint = singleClickPoint else {return}
//do whatever you want on single-click
}
The reason I'm not using singleClickAction(at point: CGPoint) and calling it with: event.location(in: self) is that any point I pass in - including CGPoint.zero - ends up arriving in the singleClick Action as (0.0, 9.223372036854776e+18). I will be filing a radar for that, but for now, bypassing perform is the way to go. (Other objects seem to work just fine, but CGPoints do not.)

Resources