Love/hate relationship with Cocoa Bindings

I have a love/hate relationship with Cocoa Bindings. On the surface, bindings look really cool. Write less code and get a program running faster. This is great in theory, but in reality, it can be tricky. Bindings in Interface Builder work OK (however, there seems to be annoying Interface Builder bugs that change the focus when you try to bind an object). The problem arises when you remove an object from a nib, run the app and find out that your get an exception that the object is not key/value compliant. So, you have to search through the nib looking for the bindings that are no longer relevant. If you have a lot of objects, this can be time consuming and problematic.

On the love side, I can more easily bring up a user interface and then have things update automatically when something else changes.

In order to help troubleshoot these loose bindings, I found a cool debugging technique. First, in your app’s initialize method, add the following:

+ (void) initialize
{
	[BindingDebugObject poseAsClass:[NSObject class]];
}

then add the debugging object:

@implementation BindingDebugObject
- (void)bind:(NSString *)binding toObject:(id)observable
	withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
{
	NSLog(@"Binding: %@ %@ %@ %@", binding, observable, keyPath, self);
	[super bind:binding toObject:observable withKeyPath:keyPath options:options];
}

- (void)unbind:(NSString *)binding
{
	NSLog(@"unbind: %@ %@", self, binding);
	[super unbind:binding];
}

More UI Overhauling in ReceiptWallet

Another part of my UI overhaul is to redo my preferences. They’ve completely gotten out of hand and everything has been in one window like this.

OldPrefs.jpg

I like how iTunes and other apps (including the Finder) have a tabbed toolbar interface, so I went that route. Using Matt Gemmell’s SS_PrefsController as a base, I re-worked my preferences into different panes. The source uses plugins for the preferences which I don’t need, so I modified the code to embed everything. I’m waiting to hear back from my icon guy, but things are looking better. Ignore my icons for now.

ReceiptWalletScreenSnapz001.png

Wow, what a huge difference a few tweaks can make to a program. I also turned on autohiding of the scrollbars. Due to some bugs in the OS, I had to do some magic to get things drawing correctly. Having a blank toolbar looks bad, but I didn’t want to deal with it before.

Tech Support is the bane of my existence

ReceiptWallet and DocumentWallet have done fabulously well, almost beyond my wildest dreams (OK, maybe not, I dream that I can sell 100 copies a day and then I could just sit back and relax). One of the downsides with this success is that support requests are on the rise. Some of the questions are simple and are just inquiries as the documentation could use a lot of help. Some of the requests are crashes; a majority of these crashes are due to poorly written scanner drivers and there is nothing I can do about them. A small number of requests have to do with bugs in the software. While I don’t (usually) mind some of the requests that are nicely worded and relatively short (the longer the message, the less inclined I am to read it; bulleted questions have a higher chance of getting a good response), some just drive me crazy. I answer every question myself, even if it is a canned response. I don’t like questions unanswered, so I feel the constant need to check my email and answer right away.

This, of course, causes me a great deal of stress as I’m not a very patient person. I do my best to answer questions in a professional manner, but sometimes I feel like answering questions with flippant responses.

ReceiptWallet UI Overhaul

ReceiptWalletScreenSnapz002.png
I’ve started to do some work on the ReceiptWallet UI based on some ideas I got this week as well as customer feedback. I found that I personally had a number of “recent” smart collections, so I decided to make them a standard part of the interface. Also, I used some features of Leopard to get the “Source List” view (it looks OK on Tiger, but isn’t as slick). For some reason, this was harder than it should have been; I had to verify that drag and drop still worked and lots of other things didn’t break such as contextual menus, adding/removing collections, etc. However, I think it is all working now.

I am still working on some things, but will be putting out a beta next week.

Document based vs One Main Window

After looking at NEAT Receipts yesterday and a few other programs, I’m beginning to think that my decision to make ReceiptWallet a one main window application may not have been the best one. A huge advantage to multiple windows is that people can have data files for each year or one for work and one for home. Unfortunately, changing from the main window to a document based model is not easy. Unfortunately NSPersistentDocument doesn’t support packages, but a few developers have figured out how to get around this. What does this mean? This would mean that all the data is stored in one package and it would appear as one file, but would actually be separated inside. This is exactly what I’d want with the database file separate from the actual PDFs.

Hmmmm…

Crappy coding bites me again

I write a lot of code for ReceiptWallet as well as my day job doing contract work, so it goes without saying that I write some good code and some bad code. I like to think that I mostly write good code with a low bug count. I’ve been getting some really odd crash reports with ReceiptWallet and no one has actually sent a description of how they managed to crash it. I took a look today based on some guesses I had about how a customer was using the product and managed to track down the crash to one line of code; granted the crashes didn’t point to the line of code and were all across the board, but when I commented out the line, the funky behavior stopped. Turns out it was a really stupid bug; I use Cocoa Bindings in ReceiptWallet which saves code and is, in general, pretty neat. Basically in the UI, I specify that a particular field is bound to a variable. Then in the code, I set the variable. The trick is that the variable has to be set using Key Value Coding (KVC) such as:

[self setValue:[sender stringValue] forKey:@"variableName];

However, I found in my code a line that looked like this:

variableName = [sender stringValue];

When I first looked at it and started thinking about it, I thought I hadn’t retained the value and that the results were undefined when it exited the function, but after thinking about it all day, I realized my mistake, changed the code and presto, the crash was gone. What really threw me were crash reports like:

Thread 0 Crashed:
0   libobjc.A.dylib               	0x94cc56e8 objc_msgSend + 24
1   com.apple.Foundation          	0x95f4a180 -[NSComparisonPredicate evaluateWithObject:substitutionVariables:] + 240
2   com.apple.Foundation          	0x95f4a081 -[NSPredicate evaluateWithObject:] + 49
3   com.apple.CoreData            	0x90c4520a -[NSManagedObjectContext executeFetchRequest:error:] + 2266

and

Thread 0 Crashed:
0   libobjc.A.dylib               	0x94cc56e8 objc_msgSend + 24
1   com.apple.AppKit              	0x936c3732 -[NSApplication run] + 892
2   com.apple.AppKit              	0x936909ba NSApplicationMain + 574

Now I can sleep better knowing that I fixed a long standing bug that may have solved a number of crashes that I couldn’t explain.

Lovely Core Data Issue; is it a bug or a feature?

I’ve had a few users report that they lost all their data when upgrading to the latest ReceiptWallet or DocumentWallet. This really concerns me as I don’t like data loss (who does?). The latest version of both programs switched to using an SQLite database when users were running under Leopard as it is much faster. At the same time, I changed my object model as I was including lots of models that I didn’t use (damn Apple sample code included all models in all frameworks). So the changes weren’t minor, bug I did my testing and things were working fine. It was in beta for awhile and I had no reports of issues.

Today I decided to revisit the issue and try to solve the problem once and for all. In order to determine when object model I’m using, I add a model version to the metadata which gets written out to the database. I had code like this when I was setting up the data, at the end of the conversion.

[self generateMetadata]

if (managedObjectContext != nil && [managedObjectContext hasChanges])
{	
	[managedObjectContext commitEditing];
	[managedObjectContext save:&error];
}

That should be fine as I don’t want to save if there aren’t any changes and there has been a report that saving when there aren’t changes can cause data loss under Leopard. My code should be nice and fine. Well, here’s the rub. When I do the upgrade, I modify the metadata (in generateMetadata) and if there are no changes (like when the app initially starts), the data file doesn’t get saved. So the next time the app is launched, it thinks it is using the old data model, attempts to do an upgrade, hoses all the data, and continues. If the user modified something prior to quitting the app, all is fine.

So, it looks like people launched the app, let it do the update and then immediately quit it. Ouch.

My modified code is:

	BOOL metadataChanged = [self generateMetadata];
	if (managedObjectContext != nil && ([managedObjectContext hasChanges] || metadataChanged))
	{	
		if ([managedObjectContext hasChanges])
		{
			[managedObjectContext commitEditing];
		}
		[managedObjectContext save:&error];
	}

where generateMetadata returns true if the metadata is different than what already exists.

This brings up another question. Why did I restrict this to Leopard. Well, I was having problems in Tiger where the data got hosed. In my testing this evening, the problem was the above issue. I guess I changed something in the app during Leopard testing which caused the metadata to be written out, but didn’t change anything under Tiger. OK, so now Tiger users get the benefit of the SQLite database format.

Of course, the users that had this problem said that they installed the update and their data was gone. What they didn’t say is that they quit the program right after the update without changing anything. Well, I don’t expect people to take this much notice to what they are doing, but it would have helped track this down faster.

The good news about this mess is that during testing I saw how this could be an issue and made a backup of the old database called ReceiptWallet.receiptwallet.old or DocumentWallet.documentwallet.old. This is in addition to the daily backups I automatically make of the database (up to 5 copies).

Indie Software Developers

For years, I’ve seen references to “indie” (independent) software developers, but never considered myself one. I’ve never had to rely on sales of my own software as I’ve been an independent contractor without my own products (that brought in any significant amount of income) for almost five years. However, this past year, ReceiptWallet has started to change that; I’m not about to give up my day job, but in an exchange with another developer today, he referred to both of us as “indie” developers. I guess I am an independent developer.

Broken Idle Time in 10.4/10.5

After fighting with trying to get the amount of time since a user has done something with the system, I’ve determined that CGEventSourceSecondsSinceLastEventType is broken. The documentation indicates that calling it like:

CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType)

will tell me how many seconds since the user last moved the mouse, touched the keyboard, etc. The docs say:

The various system and app defined events do not contribute to this event type’s time.

Unfortunately this just isn’t true. In one app I’m working on, I have a timer that fires every 5 seconds and prints the idle time. It starts going up, but then a notification comes in and it resets back to zero without me touching the keyboard. So, this call is almost useless as I need to know when things are idle in order to perform some tasks. While some of you are saying that I can use a Carbon Event Idle Timer, it turns out that they don’t work in background only apps. My only solution is to make the above call using something like:

+ (double) idleTimeInSeconds
{
	double idleTime = 0;
	double tempIdleTime = 0;
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventLeftMouseDown);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventLeftMouseUp);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}

	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventRightMouseDown);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}

	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventRightMouseUp);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventMouseMoved);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventLeftMouseDragged);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventRightMouseDragged);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventKeyDown);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventKeyUp);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventFlagsChanged);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventScrollWheel);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventTabletPointer);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventTabletProximity);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventOtherMouseDown);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventOtherMouseUp);
	if (idleTime == 0 || tempIdleTime < idleTime)
	{
		idleTime = tempIdleTime;
	}
	
	tempIdleTime = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGEventOtherMouseDragged);
	
	return idleTime;
}

Wow, that is freaking ugly, but at least I know exactly when a user event occurred. Feel free to use my code in any way you see fit. I've searched the web and found answers to a lot of my problems, so here's a small contribution back to the community.

Lets’ spy on the competition

Today I saw that NEAT Receipts has provided more information on their Mac version, and here. Just about a month ago, the founder of NEAT Receipts (or one of his employees) purchased a copy of ReceiptWallet and DocumentWallet. At first I thought maybe they wanted to talk to me about the products and I contacted them about it, but now it seems that the goal was simply to either look at the competition or learn from it. So it looks like NEAT Receipts has 3 developers working on the program; I wrote ReceiptWallet initially in about 2 weeks part time and have had over a year to perfect it.I just hope that they don’t copy my ideas and interface as it has taken me a long time to get things quite usable. They’re exhibiting at Macworld, so I’ll have to take a look at what they have and maybe change my marketing message, “The original receipt management program for Mac OS X”.