How to have an event-loop on non-main thread in macOS? - objective-c

Related to this other question: I have the need to gather information about what is the current active application, on macOS.
The linked QA answer provide a mechanism to get alerted (event) when the active application changes, but it crashes when run on a separated thread:
FocusDetector::AppFocus focus;
focus.run();
//std::thread threadListener(&FocusDetector::AppFocus::run, &focus); //Does not works
//if (threadListener.joinable())
//{
// threadListener.join();
//}
.
*** Assertion failure in +[NSUndoManager _endTopLevelGroupings], /xxxxxxx/NSUndoManager.m:363
2020-11-24 08:54:41.758 focus_detection[81935:18248374] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '+[NSUndoManager(NSInternal) _endTopLevelGroupings] is only safe to invoke on the main thread.'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff3006cb57 __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00007fff68eb35bf objc_exception_throw + 48
2 CoreFoundation 0x00007fff30095d08 +[NSException raise:format:arguments:] + 88
3 Foundation 0x00007fff32787e9d -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
4 Foundation 0x00007fff326c45ee +[NSUndoManager(NSPrivate) _endTopLevelGroupings] + 440
5 AppKit 0x00007fff2d25165c -[NSApplication run] + 864
6 focus_detection 0x0000000104b1a010 _ZN13FocusDetector8AppFocus3runEv + 128
7 focus_detection 0x0000000104b19547 _ZNSt3__1L8__invokeIMN13FocusDetector8AppFocusEFvvEPS2_JEvEEDTcldsdeclsr3std3__1E7forwardIT0_Efp0_Efp_spclsr3std3__1E7forwardIT1_Efp1_EEEOT_OS6_DpOS7_ + 119
8 focus_detection 0x0000000104b1944e _ZNSt3__1L16__thread_executeINS_10unique_ptrINS_15__thread_structENS_14default_deleteIS2_EEEEMN13FocusDetector8AppFocusEFvvEJPS7_EJLm2EEEEvRNS_5tupleIJT_T0_DpT1_EEENS_15__tuple_indicesIJXspT2_EEEE + 62
9 focus_detection 0x0000000104b18c66 _ZNSt3__114__thread_proxyINS_5tupleIJNS_10unique_ptrINS_15__thread_structENS_14default_deleteIS3_EEEEMN13FocusDetector8AppFocusEFvvEPS8_EEEEEPvSD_ + 118
10 libsystem_pthread.dylib 0x00007fff6a260109 _pthread_start + 148
11 libsystem_pthread.dylib 0x00007fff6a25bb8b thread_start + 15
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Abort trap: 6
This is obviously related with NSApplication, for which the documentation state:
Every app uses a single instance of NSApplication to control the main event loop
In consequence, I am looking for another way to listen on events, which is not restricted to the main event-loop ( or main thread.
Intuitively, it should be possible to get information about the current application with focus, in a separated thread.
I have no idea how to approach this problem, sorry for not providing much research. I did researched on internet for "NSNotification not in main thread" and other similar sentences, but without success.
Question:
How to listen on activeAppDidChange NSNotification outside the main thread?

Place Observers to the following Notifications below and let them invoke your methods. NotificationCenter are singleton for good reason, while you can of course, create your own. [NSWorkspace sharedWorkspace] is a singleton i guess, and [[NSWorkspace sharedWorkspace] notificationCenter] as named - the notification center to observe. The sending object to expect is nil in your case, because you don't know which object to observe more specific.
#import <AppKit/AppKit.h>
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
[[[NSWorkspace sharedWorkspace] notificationCenter]
addObserver:self
selector:#selector(activeAppDidChange:)
name:NSWorkspaceDidActivateApplicationNotification
object:nil
];
[[[NSWorkspace sharedWorkspace] notificationCenter]
addObserver:self
selector:#selector(activeAppDidTerminate:)
name:NSWorkspaceDidTerminateApplicationNotification
object:nil
];
// id<NSObject> myObserver;
_myObserver = [[[NSWorkspace sharedWorkspace] notificationCenter]
addObserverForName:NSWorkspaceDidHideApplicationNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
// do stuff in block
NSRunningApplication *app = note.userInfo[NSWorkspaceApplicationKey];
NSLog(#"%u %# %ld",
app.processIdentifier,
app.bundleIdentifier,
(long)app.activationPolicy
);
}
];
}
-(void)activeAppDidChange:(NSNotification *)note {
NSLog(#"%#",note.userInfo.debugDescription);
}
-(void)activeAppDidTerminate:(NSNotification *)note {
NSLog(#"%#",note.userInfo.debugDescription);
}

Just proof you can receive Notifications on Threads that are not mainThread.
// ThreadManagerExample.h
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#interface ThreadManagerExample : NSObject
#property (nonatomic, readonly) BOOL started;
#property (nonatomic, readonly) uint64_t looptime;
-(void)start;
-(void)stop;
#end
NS_ASSUME_NONNULL_END
// ThreadManagerExample.m
#import "ThreadManagerExample.h"
static const double kThreadPriority = 1.0;
#interface ReceivingThread : NSThread
#property (nonatomic, weak) ThreadManagerExample * threadManager;
#end
#interface ThreadManagerExample ()
#property (nonatomic, strong) ReceivingThread *thread;
#property (nonatomic, readwrite) BOOL started;
#property (nonatomic, readwrite) uint64_t looptime;
#end
#implementation ThreadManagerExample
-(void)dealloc {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(startThread) object:nil];
if ( _thread ) {
[_thread cancel];
while ( !_thread.isFinished ) {
[NSThread sleepForTimeInterval:0.01];
}
}
}
-(instancetype)init {
if ( !(self = [super init]) ) return nil;
_looptime = 1000000000; // 1 sec
return self;
}
-(void)startThread {
if ( !_thread) {
self.thread = [ReceivingThread new];
_thread.threadManager = self;
_thread.name=#"ReceivingThread";
[_thread setThreadPriority:kThreadPriority];
[_thread start];
}
}
-(void)start {
if ( !_thread ) {
#synchronized ( self ) {
self.started = YES;
}
[self performSelector:#selector(startThread) withObject:nil afterDelay:0.0];
}
}
-(void)stop {
#synchronized ( self ) {
self.started = NO;
}
if ( _thread ) {
[_thread cancel];
self.thread = nil;
}
}
#end
#implementation ReceivingThread {
BOOL somethinghappend;
NSString *oldBundleIdentifier;
NSString *lastbundleIdentifier;
pid_t oldPID;
pid_t focusedPID;
}
-(instancetype)init {
if (!(self=[super init])) return nil;
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:#selector(mimi:) name:NSWorkspaceDidActivateApplicationNotification object:nil];
return self;
}
-(void)mimi:(NSNotification*)note {
NSRunningApplication *app = note.userInfo[NSWorkspaceApplicationKey];
lastbundleIdentifier = app.bundleIdentifier;
focusedPID = app.processIdentifier;
if (![oldBundleIdentifier isEqualToString:lastbundleIdentifier]) {
somethinghappend = YES;
}
oldBundleIdentifier = lastbundleIdentifier;
oldPID = focusedPID;
}
-(void)main {
[NSThread setThreadPriority:kThreadPriority];
while ( !self.isCancelled ) {
uint64_t nextLoop = 0;
#synchronized ( _threadManager ) {
if (somethinghappend) {
NSLog(#"%# %u",lastbundleIdentifier, focusedPID);
somethinghappend = NO;
}
uint64_t now = mach_absolute_time();
nextLoop = now + _threadManager.looptime;
}
mach_wait_until(nextLoop);
}
}
#end
and instance and starting is done like..
if (!_manager) _manager = [[ThreadManagerExample alloc] init];
[_manager start];
stopping
[_manager stop];
_manager = nil; // if needed
So as looptime is set to 1 sec it will check each second if the last received bundleIdentifier was changed. You could extend the -(void)mimi: method by checking if the bundleIdentifier differs from [[NSBundle mainBundle] bundleIdentifier] to know if you became active yourself and ignore that when needed.
Edit from Apple Docs
NSRunningApplication is thread safe, in that its properties are returned atomically. However, it is still subject to the main run loop policy described above. If you access an instance of NSRunningApplication from a background thread, be aware that its time-varying properties may change from under you as the main run loop runs (or not).

Related

How to properly stub methods on a different thread using OCMock?

I have the following unit test that is failing. I think its because OCMock does not work well across multiple threads but I could be wrong. mockTestMethodA never gets called. If I modified the code to call the testMethodA on the same thread (without NSThread), stubbing seems to work. Is this a known limitation of OCMock or am I missing something?
Sample code:
- (void) testCallMethodUsingNSThreadFromADifferentClass
{
mockTestClassA = [OCMockObject partialMockForObject:testClassA];
[[[mockTestClassA expect] andCall:#selector(mockTestMethodA) onObject:self] testMethodA];
[testClassC threadedRequestToCallMethodA];
[self waitForCompletion:5.0];
[mockTestClassA verify];
}
threadedRequestToCallMethodA and callMethodAFromTestClassC in TestClassC are defined as follows:
- (void) threadedRequestToCallMethodA
{
[NSThread detachNewThreadSelector:#selector(callMethodAFromTestClassC) toTarget:self withObject:nil];
}
- (void) callMethodAFromTestClassC
{
[[[TestClassA alloc] init] testMethodA];
}
testMethodA in TestClassA is defined as:
- (void) testMethodA
{
NSLog(#"testMethodA");
}
Stub method is defined as follows:
- (void) mockTestMethodA
{
NSLog(#"mockTestMethodA");
}
And finally waitForCompletion:
- (BOOL) waitForCompletion:(NSTimeInterval)timeoutSecs
{
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
if([timeoutDate timeIntervalSinceNow] < 0.0)
break;
} while (!done);
return done;
}
Your help is appreciated.
Thanks.
Your issue is that you aren't calling the test method on the object you partially mocked, but are instead alloc'ing a new one in your callMethodAFromTestClassC. If you call it on the proper object, it works fine. See below (note I don't actually create a separate class C, but the effect is the same). As a side note, I think blocks and GCD make life easier, but everyone has their own style.
//
// TestClassTest.m
// TestApp
//
// Created by Benjamin Flynn on 11/20/12.
//
#import <SenTestingKit/SenTestingKit.h>
#import "OCMock.h"
#import "TestClassA.h"
#interface TestClassTest : SenTestCase
#property (nonatomic, retain) TestClassA *testClassA;
#property (atomic, assign) BOOL done;
#end
#implementation TestClassTest
- (void) testCallMethodUsingNSThreadFromADifferentClass
{
self.testClassA = [[TestClassA alloc] init];
id mockTestClassA = [OCMockObject partialMockForObject:self.testClassA];
[[[mockTestClassA expect] andCall:#selector(mockTestMethodA) onObject:self] testMethodA];
[self threadedRequestToCallMethodA];
[self waitForCompletion:5.0];
[mockTestClassA verify];
}
- (void)threadedRequestToCallMethodA
{
[NSThread detachNewThreadSelector:#selector(callMethodAFromTestClassC) toTarget:self withObject:nil];
}
- (void)callMethodAFromTestClassC
{
[self.testClassA testMethodA];
}
- (void)mockTestMethodA
{
NSLog(#"Mock test method A");
self.done = YES;
}
- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs
{
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];
NSLog(#"Starting timer");
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
if([timeoutDate timeIntervalSinceNow] < 0.0)
break;
} while (!self.done);
NSLog(#"Ending timer");
return self.done;
}
#end

KVO with Run-to-Completion semantics - Is it possible?

I recently ran into reentrancy issues with KVO. To visualize the problem, I would like to show a minimal example. Consider the interface of an AppDelegate class
#interface AppDelegate : UIResponder <UIApplicationDelegate>
#property (strong, nonatomic) UIWindow *window;
#property (nonatomic) int x;
#end
as well as its implementation
#implementation AppDelegate
- (BOOL) application:(__unused UIApplication *)application
didFinishLaunchingWithOptions:(__unused NSDictionary *)launchOptions
{
__unused BigBugSource *b = [[BigBugSource alloc] initWithAppDelegate:self];
self.x = 42;
NSLog(#"%d", self.x);
return YES;
}
#end
Unexpectedly, this program prints 43 to the console.
Here's why:
#interface BigBugSource : NSObject {
AppDelegate *appDelegate;
}
#end
#implementation BigBugSource
- (id)initWithAppDelegate:(AppDelegate *)anAppDelegate
{
self = [super init];
if (self) {
appDelegate = anAppDelegate;
[anAppDelegate addObserver:self
forKeyPath:#"x"
options:NSKeyValueObservingOptionNew
context:nil];
}
return self;
}
- (void)dealloc
{
[appDelegate removeObserver:self forKeyPath:#"x"];
}
- (void)observeValueForKeyPath:(__unused NSString *)keyPath
ofObject:(__unused id)object
change:(__unused NSDictionary *)change
context:(__unused void *)context
{
if (appDelegate.x == 42) {
appDelegate.x++;
}
}
#end
As you see, some different class (that may be in third-party code you do not have access to) may register an invisible observer to a property. This observer is then called synchronously, whenever the property's value has changed.
Because the call happens during the execution of another function, this introduces all sort of concurrency / multithreading bugs although the program runs on a single thread. Worse, the change happens without an explicit notice in the client-code (OK, you could expect that concurrency issues arise whenever you set a property...).
What is the best practice to solve this problem in Objective-C?
Is there some common solution to regain run-to-completion semantics automatically, meaning that KVO-Observation messages go through an event-queue, AFTER the current method finishes executing and invariants / postconditions are restored?
Not exposing any properties?
Guarding every critical function of an object with a boolean variable to ensure that reentrancy is not possible?
For example: assert(!opInProgress); opInProgress = YES; at the beginning of the methods, and opInProgress = NO; at the end of the methods. This would at least reveal those kind of bugs directly during runtime.
Or is it possible to opt out of KVO somehow?
Update
Based on the answer by CRD, here is the updated code:
BigBugSource
- (void)observeValueForKeyPath:(__unused NSString *)keyPath
ofObject:(__unused id)object
change:(__unused NSDictionary *)change
context:(__unused void *)context
{
if (appDelegate.x == 42) {
[appDelegate willChangeValueForKey:#"x"]; // << Easily forgotten
appDelegate.x++; // Also requires knowledge of
[appDelegate didChangeValueForKey:#"x"]; // whether or not appDelegate
} // has automatic notifications
}
AppDelegate
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:#"x"]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
- (BOOL) application:(__unused UIApplication *)application
didFinishLaunchingWithOptions:(__unused NSDictionary *)launchOptions
{
__unused BigBugSource *b = [[BigBugSource alloc] initWithAppDelegate:self];
[self willChangeValueForKey:#"x"];
self.x = 42;
NSLog(#"%d", self.x); // now prints 42 correctly
[self didChangeValueForKey:#"x"];
NSLog(#"%d", self.x); // prints 43, that's ok because one can assume that
// state changes after a "didChangeValueForKey"
return YES;
}
What you are asking for is manual change notification and is supported by KVO. It is a three stage process:
Your class overrides + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey returning NO for any property you wish to defer notifications for and deferring to super otherwise;
Before changing a property you call [self willChangeValueForKey:key]; and
When you are ready for the notification to occur you call [self didChangeValueForKey:key]
You can build on this protocol quite easily, e.g. it is easy to keep a record of keys you have changed and trigger them all before you exit.
You can also use willChangeValueForKey: and didChangeValueForKey with automatic notifications turned on if you directly alter the backing variable of a property and need to trigger KVO.
The process along with an examples is described in Apple's documentation.

NSOperation with repeat option

I've developed a Category that gives the NSOperation the ability to be executed in the background at timed intervals. I would really appreciate getting some feedback on this, especially any potential problems with this approach that I'm not thinking of.
Thank you!
Here's the code:
NSOperation+Repeat.h
#import <Foundation/Foundation.h>
#interface NSOperation (repeat)
#property (readonly, nonatomic) NSTimeInterval repeatInterval;
#property (readonly, nonatomic) NSOperationQueue *repeatOperationQueue;
- (void)performUsingOperationQueue:(NSOperationQueue *)operationQueue;
- (void)performAtRepeatingInterval:(NSTimeInterval)interval usingOperationQueue:(NSOperationQueue *)operationQueue;
#end
NSOperation+Repeat.m
#import "NSOperation+repeat.h"
#import <objc/runtime.h>
static char const * const RepeatPropertiesKey = "RepeatProperties";
#implementation NSOperation (repeat)
#dynamic repeatInterval;
#dynamic repeatOperationQueue;
static NSString * RepeatIntervalKey = #"interval";
static NSString * RepeatOperationQueueKey = #"operationQueue";
static NSString * RepeatTimerKey = #"timer";
- (NSMutableDictionary *)repeatProperties {
NSMutableDictionary * properties = objc_getAssociatedObject(self, RepeatPropertiesKey);
if (properties == nil) {
properties = [NSMutableDictionary new];
objc_setAssociatedObject(self, RepeatPropertiesKey, properties, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return properties;
}
- (NSTimeInterval)interval {
NSNumber * interval = [[self repeatProperties] objectForKey:RepeatIntervalKey];
return [interval doubleValue];
}
- (NSOperationQueue *)repeatOperationQueue {
NSOperationQueue * operationQueue = [[self repeatProperties] objectForKey:RepeatOperationQueueKey];
return operationQueue;
}
- (void)performUsingOperationQueue:(NSOperationQueue *)operationQueue {
[operationQueue addOperation:[self copy]];
}
- (void)performAtInterval:(NSTimer *)timer {
[self performUsingOperationQueue:self.repeatOperationQueue];
}
- (void)performAtRepeatingInterval:(NSTimeInterval)interval usingOperationQueue:(NSOperationQueue *)operationQueue {
// Save interval and operationQueue in repeatProperties
[self.repeatProperties setValue:[NSNumber numberWithDouble:interval] forKey:RepeatIntervalKey];
[self.repeatProperties setValue:operationQueue forKey:RepeatOperationQueueKey];
// Create timer to call performAtInterval on self
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:(interval*60)
target:self
selector:#selector(performAtInterval:)
userInfo:nil
repeats:YES];
// Save the timer in repeatProperties
[self.repeatProperties setValue:timer forKey:RepeatTimerKey];
[self performUsingOperationQueue:operationQueue];
}
#end
Here's an example of a NSOperation subclass that can repeat:
MSScheduleImportOperation.h
#import <Foundation/Foundation.h>
#import "NSOperation+Repeat.h"
#interface MSScheduleImportOperation : NSOperation <NSCopying>
#property (readonly, strong, nonatomic) NSString* employeeId;
- (id)initWithEmployeeId:(NSString *)employeeId;
#end
MSScheduleImportOperation.m
#import "MSScheduleImportOperation.h"
#implementation MSScheduleImportOperation
#synthesize employeeId = __employeeId;
- (id)initWithEmployeeId:(NSString *)employeeId {
self = [super init];
__employeeId = [employeeId copy];
return self;
}
- (id)copyWithZone:(NSZone *)zone {
MSScheduleImportOperation* copy = [[MSScheduleImportOperation alloc] initWithEmployeeId:self.employeeId];
return copy;
}
- (void)main
{
...
}
#end
The Apple documentation says:
An operation object is a single-shot object—that is, it executes its task once and cannot be used to execute it again.
So the first problem is that there might be internals that stop it from working. Although, I see you try to get around the problem by making a copy.
This leads us to the other problem is that NSOperation is not advertised to conform to NSCopying.
[operationQueue addOperation:[self copy]];
This line should throw an exception.
Instead of a category on NSOperation where the object copies itself and adds the copy to an NSOperationQueue - it would be simpler to manage this at a higher level. For example:
+[RepeatingOperation operationBlock:(InitOperationBlock)operationBlock
queue:(NSOperationQueue*)queue
interval:(NSTimeInterval)interval];
where InitOperationBlock would be a block where the operation was created and configured.
The main benefit is the API would be harder to mess up. For example in category in the original post, performUsingOperationQueue: will fail silently if you forget to set repeatOperationQueue.

NSWorkspace Notifications in CFNotificationCenter

We are working on a Qt project, and there is some Mac specific code that we need to add. We need to register for an event, in a sample program we did that by using:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(notificationHandler:)
name:NSWorkspaceDidDeactivateApplicationNotification
object:nil];
Since we can use that directly in our mm file on Qt, we are taking the approach of doing something like:
MyClass::MyClass() : {
// do other setup ...
CFNotificationCenterAddObserver
(
CFNotificationCenterGetLocalCenter(),
this,
&notificationHandler,
CFSTR("???"),
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately
);
}
whats the string for "NSWorkspaceDidDeactivateApplicationNotification"?? Or how do we attatch ourselves to this particular notification?
We tried NSGod's approach, but since no Objective-C code can be added in a .h with Qt, then we added a private member which its class is defined in the mm file, that containes the actual logic. like this:
SelectedStuffManager.h
class MacWrap;
class SelectedStuffManager
{
public:
....
doSomething();
MacWrap* d;
private:
....
};
SelectedStuffManager.mm
#class MDWorkspaceWatcher;
class MacWrap
{
public:
MacWrap();
~MacWrap();
void applicationDeactivated(NSNotification * notification);
SystemEventsApplication *systemApplication;
NSRunningApplication *runApp;
private:
MDWorkspaceWatcher *workspaceWatcher;
};
MacWrap::MacWrap() {
this->workspaceWatcher = [[MDWorkspaceWatcher alloc] initWithMyClass:this];
}
MacWrap::~MacWrap() {
[this->workspaceWatcher release];
}
void MacWrap::applicationDeactivated(NSNotification* notification)
{
// guardar el id del proceso para utilizarlo luego
runApp = [[notification userInfo] valueForKey:#"NSWorkspaceApplicationKey"];
NSString *systemEventsASppName = [runApp bundleIdentifier];
if( [ systemEventsASppName isNotEqualTo:#"com.yo.SelectedText"])
{
systemApplication = [SBApplication applicationWithBundleIdentifier:systemEventsASppName];
NSLog(#"Launched. %#",systemEventsASppName);
}
}
#interface MDWorkspaceWatcher : NSObject {
MacWrap *manager;
}
- (id)initWithMyClass:(MacWrap*)obj;
- (void)didDeactivateApp:(NSNotification *)notification; #end
#implementation MDWorkspaceWatcher
- (id)initWithMyClass:(MacWrap*)obj {
if ((self = [super init])) {
manager = obj;
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
selector:#selector(didDeactivateApp:)
name:NSWorkspaceDidDeactivateApplicationNotification
object:nil];
}
return self;
}
- (void)dealloc {
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
[super dealloc];
}
- (void)didDeactivateApp:(NSNotification *)notification {
manager->applicationDeactivated(notification);
}
#end
SelectedStuffManager::SelectedStuffManager()
{
d = new MacWrap();
}
SelectedStuffManager::doSomething()
{
if ([[d->runApp localizedName] isEqualTo: #"something"]) --> here it fails, bad memory access
{
...
}
}
It seems like someone is freeing both runApp and systemApplication, so we get a null pointer or bad memory. How or why could this be happening?
I don't believe you can just do like you're hoping to. First of all, NSWorkspace uses its own NSNotificationCenter, which is different than the default NSNotificationCenter returned with +defaultCenter.
I don't believe there's a strict CF-equivalent to those NSWorkspace calls. There are likely high-level Carbon-based equivalents, but they're not available in 64-bit, and should likely be avoided.
You should be able to accomplish what you want using a small Objective-C helper class to receive the notifications and forward them to your C++ class like in the following code:
EDIT: updated to remove any Objective-C from header files. Just use generic void * pointers, which you can cast inside the .mm file.
.h:
//#class MDWorkspaceWatcher;
class MyClass {
private:
// MDWorkspaceWatcher *workspaceWatcher;
void *workspaceWatcher;
public:
MyClass();
~MyClass();
// const void didActivateApp(NSNotification *notification) const;
// const void didDeactivateApp(NSNotification *notification) const;
const void didActivateApp(void *anNSnotification) const;
const void didDeactivateApp(void *anNSnotification) const;
};
.mm:
Objective-C part:
#interface MDWorkspaceWatcher : NSObject {
MyClass *myClass;
}
- (id)initWithMyClass:(MyClass *)aMyClass;
#end
#implementation MDWorkspaceWatcher
- (id)initWithMyClass:(MyClass *)aMyClass {
if ((self = [super init])) {
myClass = aMyClass;
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
selector:#selector(didActivateApp:)
name:NSWorkspaceDidActivateApplicationNotification
object:nil];
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
selector:#selector(didDeactivateApp:)
name:NSWorkspaceDidDeactivateApplicationNotification
object:nil];
}
return self;
}
// very important:
- (void)dealloc {
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
[super dealloc];
}
- (void)didActivateApp:(NSNotification *)notification {
myClass->didActivateApp(notification);
}
- (void)didDeactivateApp:(NSNotification *)notification {
myClass->didDeactivateApp(notification);
}
#end
C++ part:
MyClass::MyClass() {
this->workspaceWatcher = [[MDWorkspaceWatcher alloc] initWithMyClass:this];
}
MyClass::~MyClass() {
[(MDWorkspaceWatcher *)this->workspaceWatcher release];
}
MyClass::didActivateApp(void *anNSnotification) {
NSDictionary *appInfo = [(NSNotification *)anNSnotification userInfo];
NSLog(#"appInfo == %#", appInfo);
}
MyClass::didDeactivateApp(void *anNSnotification) {
NSDictionary *appInfo = [(NSNotification *)anNSnotification userInfo];
NSLog(#"appInfo == %#", appInfo);
}
Note that an NSDictionary is toll-free-bridged with CFDictionaryRef, so you can simply cast the appInfo NSDictionary to a CFDictionaryRef and then call the CF functions to get at the contents of the dictionary if you prefer C over Objective-C.
Note that the notification center owns the appInfo dictionary (in other words, it will be autoreleased), so you shouldn't call CFRelease() on it like you might with CFCreate*/CFCopy*-related code.

Cocoa Touch - iPhone App crashes when button is pressed?

This is my first app. It doesn't crash the simulator just brings you back to the home screen. This isn't my first cocoa program (I'm not a pro at cocoa either)just my first iPhone.
It crashes when you press the letsPlayButton, which runs the -(IBAction)letsPlay:(id)sender; method.
Help me! Im so lost with what's wrong!
.H
#import <UIKit/UIKit.h>
#interface BrainiacViewController : UIViewController {
IBOutlet UILabel *theQuestion;
IBOutlet UILabel *theScore;
IBOutlet UILabel *theLives;
IBOutlet UIButton *answerOne;
IBOutlet UIButton *answerTwo;
IBOutlet UIButton *answerThree;
IBOutlet UIButton *answerFour;
IBOutlet UIButton *letsPlayButton;
NSString *questionNumber;
NSInteger myScore;
NSInteger myLives;
NSInteger usersAnswer;
NSArray *theQuiz;
//quiz dictionarys
NSMutableDictionary *question, *rightAnswer, *wrongOne, *wrongTwo, *wrongThree;
}
-(IBAction)buttonOne:(id)sender;
-(IBAction)buttonTwo:(id)sender;
-(IBAction)buttonThree:(id)sender;
-(IBAction)buttonFour:(id)sender;
-(IBAction)letsPlay:(id)sender;
-(void)checkAnswer;
-(void)askQuestion;
-(void)updateScore;
-(void)loadQuiz;
#end
.M
#import "BrainiacViewController.h"
#implementation BrainiacViewController
-(void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
- (void)viewDidUnload {
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (void)dealloc {
[super dealloc];
}
-(IBAction)buttonOne:(id)sender{
usersAnswer = 1;
}
-(IBAction)buttonTwo:(id)sender{
usersAnswer = 2;
}
-(IBAction)buttonThree:(id)sender{
usersAnswer = 3;
}
-(IBAction)buttonFour:(id)sender{
usersAnswer = 4;
}
-(IBAction)letsPlay:(id)sender{
//un hide answer buttons
[answerOne setHidden:NO];
[answerTwo setHidden:NO];
[answerThree setHidden:NO];
[answerFour setHidden:NO];
//hide the lets play button
[letsPlayButton setHidden:YES];
//assign values to variables
myScore = 0;
myLives = 3;
questionNumber = 0;
//ask question and start game
[self askQuestion];
}
-(void)checkAnswer{
[question objectForKey:questionNumber];
[rightAnswer objectForKey:questionNumber];
[wrongOne objectForKey:questionNumber];
[wrongTwo objectForKey:questionNumber];
[wrongThree objectForKey:questionNumber];
}
-(void)askQuestion{
//create array to mix up the possible answers
NSMutableArray *possibleAnswers = [[NSMutableArray alloc] init];
[possibleAnswers addObject:[rightAnswer objectForKey:questionNumber] ];
[possibleAnswers addObject:[wrongOne objectForKey:questionNumber] ];
[possibleAnswers addObject:[wrongTwo objectForKey:questionNumber] ];
[possibleAnswers addObject:[wrongThree objectForKey:questionNumber] ];
//set labels/buttons for question
theQuestion.text = [question objectForKey:questionNumber];
//user picks a answer button and sets a value to usersAnswer
questionNumber = questionNumber + 1;
[self checkAnswer];
}
-(void)updateScore{
}
-(void)loadQuiz{ //assigns dictionary values
//*KEY* defines question number
[question setObject:#"Which is NOT a starwars movie?" forKey:#"1"];
[rightAnswer setObject:#"Attack of the Jedi" forKey:#"1"];
[wrongOne setObject:#"A New Hope" forKey:#"1"];
[wrongTwo setObject:#"The Phantom Menace" forKey:#"1"];
[wrongThree setObject:#"Attack of the Clones" forKey:#"1"];
[question setObject:#"In what state is the majority of Yellow Stone National Park in?" forKey:#"2"];
[rightAnswer setObject:#"Wyoming" forKey:#"2"];
[wrongOne setObject:#"California" forKey:#"2"];
[wrongTwo setObject:#"Ohio" forKey:#"2"];
[wrongThree setObject:#"Nebraska" forKey:#"2"];
[question setObject:#"In the US, what household pet is the most common?" forKey:#"2"];
[rightAnswer setObject:#"Dogs" forKey:#"2"];
[wrongOne setObject:#"Cats" forKey:#"2"];
[wrongTwo setObject:#"Hamsters" forKey:#"2"];
[wrongThree setObject:#"Komodo Dragons" forKey:#"2"];
[question setObject:#"A plane is traveling 2675 miles every 5 hours. How fast is the plane going in Miles per Hour?" forKey:#"3"];
[rightAnswer setObject:#"535 Mph" forKey:#"3"];
[wrongOne setObject:#"325 Mph" forKey:#"3"];
[wrongTwo setObject:#"535 Mph" forKey:#"3"];
[wrongThree setObject:#"420 Mph" forKey:#"3"];
}
#end
appdelagate.h
#import <UIKit/UIKit.h>
#class BrainiacViewController;
#interface BrainiacAppDelegate : NSObject <UIApplicationDelegate> {
UIWindow *window;
BrainiacViewController *viewController;
}
#property (nonatomic, retain) IBOutlet UIWindow *window;
#property (nonatomic, retain) IBOutlet BrainiacViewController *viewController;
#end
appdelegate.m
#import "BrainiacAppDelegate.h"
#import "BrainiacViewController.h"
#implementation BrainiacAppDelegate
#synthesize window;
#synthesize viewController;
#pragma mark -
#pragma mark Application lifecycle
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
// Add the view controller's view to the window and display.
[window addSubview:viewController.view];
[window makeKeyAndVisible];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
/*
Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
*/
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
/*
Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
If your application supports background execution, called instead of applicationWillTerminate: when the user quits.
*/
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
/*
Called as part of transition from the background to the inactive state: here you can undo many of the changes made on entering the background.
*/
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
/*
Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
*/
}
- (void)applicationWillTerminate:(UIApplication *)application {
/*
Called when the application is about to terminate.
See also applicationDidEnterBackground:.
*/
}
#pragma mark -
#pragma mark Memory management
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
/*
Free up as much memory as possible by purging cached data objects that can be recreated (or reloaded from disk) later.
*/
}
- (void)dealloc {
[viewController release];
[window release];
[super dealloc];
}
#end
I tried it again and checked the console and the error was this..I dont know what that means.
[Session started at 2010-07-11 12:24:40 -0400.]
2010-07-11 12:24:45.729 Brainiac[46764:207] -[NSCFString letsPlay:]: unrecognized selector sent to instance 0x5944f00
2010-07-11 12:24:45.733 Brainiac[46764:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSCFString letsPlay:]: unrecognized selector sent to instance 0x5944f00'
*** Call stack at first throw:
(
0 CoreFoundation 0x02395919 __exceptionPreprocess + 185
1 libobjc.A.dylib 0x024e35de objc_exception_throw + 47
2 CoreFoundation 0x0239742b -[NSObject(NSObject) doesNotRecognizeSelector:] + 187
3 CoreFoundation 0x02307116 ___forwarding___ + 966
4 CoreFoundation 0x02306cd2 _CF_forwarding_prep_0 + 50
5 UIKit 0x002b9e14 -[UIApplication sendAction:to:from:forEvent:] + 119
6 UIKit 0x003436c8 -[UIControl sendAction:to:forEvent:] + 67
7 UIKit 0x00345b4a -[UIControl(Internal) _sendActionsForEvents:withEvent:] + 527
8 UIKit 0x003446f7 -[UIControl touchesEnded:withEvent:] + 458
9 UIKit 0x002dd2ff -[UIWindow _sendTouchesForEvent:] + 567
10 UIKit 0x002bf1ec -[UIApplication sendEvent:] + 447
11 UIKit 0x002c3ac4 _UIApplicationHandleEvent + 7495
12 GraphicsServices 0x02bfbafa PurpleEventCallback + 1578
13 CoreFoundation 0x02376dc4 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 52
14 CoreFoundation 0x022d7737 __CFRunLoopDoSource1 + 215
15 CoreFoundation 0x022d49c3 __CFRunLoopRun + 979
16 CoreFoundation 0x022d4280 CFRunLoopRunSpecific + 208
17 CoreFoundation 0x022d41a1 CFRunLoopRunInMode + 97
18 GraphicsServices 0x02bfa2c8 GSEventRunModal + 217
19 GraphicsServices 0x02bfa38d GSEventRun + 115
20 UIKit 0x002c7b58 UIApplicationMain + 1160
21 Brainiac 0x00002790 main + 102
22 Brainiac 0x00002721 start + 53
)
terminate called after throwing an instance of 'NSException'
I tried it again and checked the console and the error was this..I dont know what that means.
2010-07-11 12:24:45.729 Brainiac[46764:207] -[NSCFString letsPlay:]: unrecognized selector sent to instance 0x5944f00
You sent a message (letsPlay:) to an object that does not respond to that message (an NSString, shown here as NSCFString).
This sort of thing usually happens because you didn't properly hold on to the object that you meant to send letsPlay: to, so it got deallocated. An NSString later got allocated at the same address, so everything that had been holding the pointer to the old object (and thinks it still is) is now holding the pointer to the NSString. Same pointer, different object. Then, when one of those would-be owners tries to send the message to the object it thinks it still has… boom.
The fix is to check your ownerships and make sure you're retaining (or creating using alloc and not autoreleasing) what you mean to own.
On a related note:
NSMutableArray *possibleAnswers = [[NSMutableArray alloc] init];
[possibleAnswers addObject:[rightAnswer objectForKey:questionNumber] ];
[possibleAnswers addObject:[wrongOne objectForKey:questionNumber] ];
[possibleAnswers addObject:[wrongTwo objectForKey:questionNumber] ];
[possibleAnswers addObject:[wrongThree objectForKey:questionNumber] ];
If you mean for this to be a local variable, you must release it by the end of the method, or you will leak this array.
If you mean to own this object, you should move it to an instance variable, and drop the local variable, and continue creating the object as you are doing now.
Required reading: The Memory Management Programming Guide for Cocoa.
You need to 'box' the integer questionNumber in a NSNumber object, like this:
theQuestion.text = [question objectForKey:[NSNumber numberWithInt:questionNumber]];
Or, you can use the more appropriate NSArray indexer:
theQuestion.text = [question objectAtIndex:questionNumber];