This has been a very productive week for me as a programmer, and I mean that in the gestaltist sense of the word. Java skills? Level up! Applescript skills? Level up! Program design skills? Level up! Cocoa skills? BIG level up! Perhaps the aspect of programming that I enjoy the most is the iterative process of:
- Have an idea.
- Can’t figure out how to execute the idea.
- Study the concepts behind the idea.
- Test my understanding.
- Implement the idea.
It really is a constant learning experience, and that is what I find enjoyable. The other day I was asked by someone new to Cocoa, “How do you remember all of those methods? There’s thousands of them!” He seemed frustrated by the perceived vertical wall of learning ahead of him. The truth of the matter is, I DON’T remember all of the Cocoa API calls, nor the Core Foundation calls, nor the Core Image calls. What I enjoy about programming is that I oftentimes don’t need to remember all of these things nor do I feel that a good programmer should be expected to recall arcane API calls at the drop of a hat.
What is more important, in my opinion, is learning how to discover what I need to solve a problem. In other words, learning how to learn. In this way, it is more important to think about my programs in terms of objects, to consider Apple’s use of design patterns, to read sample code, and to develop one’s own style. By internalizing these concepts I begin to understand the shared language that programmers use to discuss programming and subsequently learn how to convert my thoughts into the appropriate question. I don’t mean Java vs. Objective-C vs. C#, when I say language. I mean using terms like “notification” when talking about “reducing coupling” within a Cocoa application. Consider that I first need to know about the existence of the concept of coupling. Then I need to know that “coupling is bad” from an object-oriented design sense (please, allow me that over-simplification for a moment). Then I need to know that the “observer” design pattern addresses the problem of loose coupling through its publish-and-subscribe methodology. Then I need to know that Apple implements the NSNotification related classes as a robust Cocoa implementation of this pattern.
Of course, that is not typically the order we learn about such things, is it? These days, many will read Aaron Hillegass’ book because they want to cash in on iPod/iPad mania. They learn about NSNotificationCenter and NSNotification objects, while getting a nice overview of Apple’s design philosophy behind Cocoa, but Aaron very much makes it clear that his book teaches Cocoa, not programming. Then vaguely-worded questions are posted to the newsgroups or Stack Overflow while the beginner attempts to formulate his questions, which are then clarified and re-clarified and answered and re-answered and, well, I think we all understand the learning process.
I was reminded this week, while working on file_wrangler_2 just how much there is to know and understand. Not just about the Cocoa APIs but about the process of program design. In developing the base and subclasses for the FWFilter objects in the program, I realized that suppositions I had about how it would work in the interface were challenged at every design decision. Ultimately I came up with a new programming model that I feel closely mirrors the user’s mental model of what would happen, but the trip to get to that point required a lot of research, a lot of consideration, and a lot of mistakes.
The FWFilter class allows one to target a subset of files and folders that have been added to the interface. So, one may drag in a giant folder of stuff, but is only interested in all Microsoft Word documents created after April 1, 2009. This seemed to me to have a fairly obvious solution whereby the user would add an “extension filter”, and a “date filter” to the filter well. After doing research on robust filtering in Cocoa, NSPredicate seemed the way to go. Here’s a case where I understood enough about Cocoa to ask the right question and found this class to be perfect. Each filter would create an NSPredicate representing user choices in the interface. The full chain of filters would be combined into an NSCompoundPredicate and “Bob’s your uncle” as they say.
NSCompoundPredicate allows us to create AND, OR, and NOT compound predicates. It seemed fairly obvious that the user would want those files that intersected the “.doc” extension and the modification date and so I created an AND predicate. All was right with the file_wrangler_2 world until I decided to add two date filters to the interface.
Suddenly the assumptions I had made about the filter class were challenged and even upended.
If I have a date filter for everything created April 10 and another for April 15, but also only want to see Microsoft Word documents, from a predicate boolean point of view this means we want everything in ((modification date = April 10 OR = April 15) AND = .doc). What if I put in two date filters, one set to creation date later than April 1 and another set to earlier than May 1? Clearly we’re trying to define a range, so now I want ((creation date > April 1 AND creation date < May 1) AND extension = .doc). The logic behind AND’ing at certain times and OR’ing at other times were getting very confusing and the more I tried to code my way around the matter, the more confusing the whole process became.
This is where being a scrum of ONE has some drawbacks. On paper it all made so much sense, and the code worked beautifully (including the notification system in place to keep the filtered file list accurate) but in practice it made little sense at all. Luckily, I am nothing if not tenacious and set about tackling the problem at a deeper level. First I had to learn that a search for a date is not a specific thing. If someone wants files created on February 1, what she REALLY wants is everything created from the time 00:00:00 to 23:59:59 of February 1. In other words, she’s looking for a range, even in the cases where she specifies an “exact date.” Less than just means from [NSDate distantPast] to the target date. Greater than means the target date to [NSDate distantFuture].
Second, I had to redesign the DateFilter.xib file to accommodate a range. I thought I would be able to handle this by simply adding two filters, but it proved not to be true. By designing the interface to handle ranges, I didn’t have to worry about trying to “guess” whether a user intended a range or not.
Then, suddenly, the mechanism fell into place. When we consider user intent on the question, “Why is she adding two date ranges?” the intention, it seems to me, is because she wants to be inclusive. I want everything made in February and and I want everything made in April and everything made yesterday. (We must be very careful here not to confuse the boolean AND with the user model of “and”). It assumes a list that has nothing, but gradually increases with each filter of the same type.
When adding filters of differing types, the intention is to be exclusive. In other words, “Within that group of files from my date choices, ONLY show me those things that are .doc files and also show me .psd files.” In some of these cases, the boolean OR applies and so we can write her intentions thusly:
((creationDate >= Feb 1 AND creationDate =< Feb 28) OR (creationDate > March 31 AND creationDate < May 1) OR (creationDate >= midnight today AND creationDate <= 23:59:59 today)) AND ((extension == .doc) OR (extension == .psd))
I know that looks confusing, but it boils down to this. Every date filter does an internal AND for its individual predicate. Every date filter in the user interface is then OR’ed against one another and combined into a total, composite date filter compound. This is done for each type of filter in the well where every filter of a like type is OR’ed against one another to create their individual compound predicates. For the full chain of filtering we AND every unique compound.
So, the new filtering mechanism works like a champ and I believe accurately reflects user expectations. However, this was a prime example of how my own understanding of these concepts and API usages had to evolve and grow. I also had to throw away a lot of code as I went off on more than one dead-end path of exploration. But, as in life, it is only through implementation that the truth of a problem can be revealed. What is the real problem I’m trying to solve?
Its been said before, but I’ll say again, there is really only one way to learn these things: write code. Over time you’ll find that there are many APIs you fall back on time and again and become loyal companions that you know intimately. Then, you’ll start to see patterns in how a class you’ve never used before feels comfortable because the method names and objects so closely mimic another class (like in my earlier post on QuickLook). Then you’ll start to expect those similarities and grow to rely on using this knowledge to minimize your time of discovery on new problems. But even a program as conceptually simple as file_wrangler_2 reveals the need to continually research, iterate, and try new things every single day.