Social Media
GitHub
Navigation
Powered by Squarespace
Saturday
Feb182012

What I Learned During my Mac App Store Review

Two things happened on Thursday that made it obvious to me what I should write about this week. Mountain Lion was announced, and my first Mac App was approved for the Mac App Store.

Even though iDevBlogADay is about iOS programming, more and more of us are moving from iOS to the Mac. With the announcement that GameCenter will be coming to OS X, I'm guessing that more iOS developers might be thinking about coding for the Mac now than might have been last week.

So today I'm going to talk about my experience in getting my first App on the Mac App Store and specifically the differences in the approval process between the Mac App Store and the iOS App Store.

I wasn't nearly as prepared as I thought

I first submitted my Mac App on October 17th, so it took me a day short of 4 months to get my App approved. That's at least 3 times longer than I ever took to get any of my iOS apps approved. Partly because the MAS requirements are different, and part because I wasn't paying enough attention. I learned a lot in the process, and I thought I would try to help other people avoid (at least some of) the mistakes I made. Hopefully this will help you avoid wasting both your time and the review team's time.

Note: I'm only going to focus on the general interest stuff in this post. Since I wrote a developer tool whose primary purpose is to make things happen on a remote iOS device, it doesn't fit into the expected behavior of a "normal" OS X App. So I'm going to ignore the issues I had that are specific to the type of App I wrote.

You can't require 3rd-party Apps to be installed

My App uses Dropbox and Boxcar to "push" an iOS App build to all my test devices wirelessly. And my first rejection was because my app required third party products to be installed. That was a surprise to me, because I have used several iOS apps that require 3rd party services (how many iOS Apps have you seen that do nothing until you log in with Facebook Connect? - I've seen several).

So this required me to sit back and do a redesign (or sorts). Since I had built the entire App intending to rely on those services, I didn't know what to do. So I added a new preference so that, instead of Dropbox and push notifications, you could use local Web Sharing and send an email to your device with the link to click to install your test App. It's not nearly as useful as the Dropbox/Boxcar method, and I feel there should be a different solution, but I haven't thought of a better way that could work inside the sandbox restrictions.

But the main thing is, my App is functional (even if in limited way) if you don't have Dropbox and Boxcar, so it got me past that hurdle. (Personal aside: I use both Dropbox and Boxcar on an almost daily basis and if you don't, and you have both a Mac and either an iPhone or iPad, I think you're missing out on two of the most useful tools available to you).

The first rule of Sandbox is you don't talk about Sandbox

Another rejection was because I had a description that said something like "To comply with Sandbox restrictions, if you use Dropbox, it must be installed in $HOME/Dropbox". It seemed a reasonable thing to me at the time. In order to write to the Dropbox folder (with sandboxing enabled), I have to put the Dropbox folder path in the temporary exceptions part of my entitlements file. I understand why Apple doesn't want us doing that - it means that, if they change their policy, then our descriptions would become inaccurate. Also, most users don't understand such things, but hopefully all of mine do, since all this App's users are iOS developers.

I had read the review guidelines, but I would not have thought this wouldn't be okay. The rule that this violates is: "3.3 Apps with descriptions not relevant to the application content and functionality will be rejected". So keep in mind that talking about internal Apple procedures might be considered "not relevant" and earn you a rejection.

Test on Macs with different configurations

I was also rejected because my App crashed if run on a Mac that didn't have Xcode installed. The reason for that is beyond the scope of this post (and wouldn't be relevant for most Apps), but it had never occurred to me to try to test my code on a machine without Xcode installed (all my Macs have Xcode - why wouldn't they - it's what I do all day). I uninstalled Xcode from one of my machines, and sure enough, I got the crash.

It was easy enough to fix the crash, but the important thing I learned was: I needed to have tested on configurations that were more generic than what I normally use.

They are serious about keeping the user informed

I was rejected for not including a progress bar.

Well, not explicitly - but there was a point while using my App when there was a pause, and no feedback was being provided to the user. Stuff was happening in the background, and I wasn't blocking the UI thread or anything, but I wasn't telling the user what the App was doing. It didn't seem (to me at least) that it lasted that long, but they rejected it.

So I put a progress bar on it and, you know what, it greatly improved the user experience. I'm really glad I got rejected for that, because I like it much better now.

They are serious about User Experience

I also got rejected because I didn't specify a minimum window size. Or at least, adding a minimum window size fixed the problem.

The rejection said that the user could resize the window so small that the app could no longer be used. They were correct - that window exists to be a drag-and-drop target, and if you make the window small enough, then you can't drop your .app bundle onto it anymore. My initial reaction was "well, the user shouldn't do that and still expect it to work". But I was wrong.

The bottom line is that I hadn't even considered what I wanted the App to do if the user resized the window - windows don't get resized on mobile devices, so it hadn't entered my thought process. After that rejection, I thought about what I wanted the App to do when a resize happened. And it's a much better App for having thought that through.

So, in conclusion: It's as much about design as code

I would say that, more than anything else, I learned that the difference between the User Experience on OS X and iOS is at least as great, if not greater, than the difference between writing code for OS X and iOS. When I was reading books and teaching myself about programming for the Mac, I tended to focus on the framework changes (e.g. NSView vs. UIView) and the new elements (NSMenu, NSDraggingDestination, NSTask, etc) and I mostly skimmed the design parts. That was a mistake.

Over the months, I got very frustrated with the review process. There were times I thought that I was never going to get the App approved. But even though people kept telling me that I should just withdraw it and sell it outside the App Store, I'm glad I didn't give up. It's a much better App than it would have been if I hadn't done what it took to get it approved for the store.

WARNING: Blatant Self Promotion Ahead

Don't say I didn't warn you.

If you've read this far, and are an iOS developer, you might be interested in my new App, AppDropOTA. Which you can get for about the cost of the proverbial cup of Starbucks coffee here (App Store link).

After installing it, you configure it with your Boxcar email address and copy and paste a Dropbox Public folder URL into the Preferences panel. (Also, if you don't have it, you should go install the Boxcar App and log in to it with your email address).

Now use Xcode to build an iOS App for the device, grab the .app bundle and drop it onto the AppDropOTA window. Your .app bundle will be turned into an .ipa file and placed into your Dropbox Public folder so it's accessible via HTTP. Then you'll get a push notification on the Boxcar app on your device(s). Launch Boxcar (via Notification Center is the quickest way), open the notification, and tap the "View Original" link on the item detail screen. You'll get an alert pop-up prompting you to install your App. Click "Install" and you're done.

When I'm testing, I do this several times a day. I have 9 iOS devices I test with: an iPad 2, an iPhone 4S and an iPhone 4 running 5.0, an iPod Touch running 5.1 beta, an iPad 1, an iPhone 4 and an iPod touch running 4.3 and an iPhone 3G and a iPod Touch running 4.2. (Soon, I hope to convince my customers to drop 4.x support, but haven't, yet). I learned the hard way that I needed to test on different hardware when I shipped an App update for a customer that immediately crashed on all armv6 devices because of this Xcode bug. I wrote AppDropOTA to speed up the process of getting Apps on all my test devices.

Some people use TestFlight for this. For me, TestFlight is too public to use for every test build. I use TestFlight for builds I'm sharing with my customers, but I never push something to TestFlight until I've run it on my own device first. AppDropOTA is what I use for that (and it's faster that TestFlight, too).

Wednesday
Feb082012

Hey, iOS Devs, Nice AppStore. Would be a shame if something were to happen to it…

So, breaking iOS AppStore news this afternoon: Path uploads your entire iPhone address book to its servers.

This sucks, and I'm pissed off, but not because my data is so precious, but because I'm scared of where this could end up.

Danger!!! Will Robinson! Danger!!!

Many of us in the iOS development community make our living off writing mobile apps.  Even more developers dream of one day being able to write apps as a profession.  One (probably overstated) estimate out this week says half a million jobs have been created. For those of us that can write code (or are wanting to learn).  Even if that number is inflated, this is still a great thing.

I just got back from the 360MacDev conference in Denver (a spin-off of the 360iDev conference I've attended and loved the last two years).  It was fantastic, and I had a great time.  There's a great community of developers out there who are not just willing but genuinely happy to share what they know with anyone who will listen (although there is the occasional exception).

There's exactly one reason why this is possible - because the ecosystem is still growing, and there are plenty of people who want to pay to get an App written, which is, of course, because Apps are generating a lot of money.

But all is not well.  As the app stores grow more crowded, people are cheating to try to stand out, and worse, users are discovering that their iPhones are tracking them everywhere they gotheir Android Phones are logging all their keystrokes, malware has invaded the Android market and even the iPhone isn't safe from malware.

And when the size of the market starts shrinking, then everyone starts to panic, and before long people don't want to hire you and then the knives come out.

There is one thing that we, as a development community, cannot afford - and that is for the people that download and use our software to be scared of that software.  Because then the need for apps starts to dry up, and the need for mobile programmers will start to drop.

Some of us are old enough that we remember trying to sell shareware on the PC back when viruses were rampant.  It was a tough sell.  And some of us remember how hard it was to sell mobile software (for PalmOS or Windows Mobile) in the pre-AppStore world.  Back then, it was a real undertaking just convincing a user that downloading any third-party program was worth their time.  We don't want to go back to that.

This is the golden age of the indie developer (or at least the most golden age so far).  When else in history could  a single person make half a million dollars in a month, or  make a world-wide hit while commuting or  could 3 people beat a company with almost 1000x more employees?  It's certainly not something I expected a "geek" to be able to do while I was growing up.

And for that to continue, we need our users to trust us, and the easiest and best way to do that is to be worthy of their trust.  There will alway be rogue actors and  assholes that try to scam users.  But those apps are short-lived (although not short enough), poorly reviewed, and there's very little we can do about individual fraudsters, or users who don't pay attention to which developer an app came from.

But when a respectable, well-written App like Path can't be trusted, then we all have a problem.  The app is beautiful, obviously professionally designed and written, and well reviewed.  It didn't need to be slimy to be successful.

And what we, as professional mobile app developers, of whatever flavor, need to take to heart, in my opinion, is this: Don' t Be Scary.

If the professional, successful App Designers and Programmers agree to put what's best for the ecosystem first, then companies will stop risking all of our long-term success, for a short-term advantage.

Yes, it's less work to store your users' pictures unencrypted and unprotected on a server or store plain-text passwords in your app, and if you want your app to be more popular, it's tempting to harvest your users' contact info, or jack up your App ranking,  but, even if you have a legitimate non-spammy reason for uploading your customers' info, just don't.  Please, please, please always err on the side of earning your users' trust.  Yes, it might be more work for your current app, but remember, your current users are the potential users for all our apps, and whatever app you're working on next.

And yes, some of us will piss off some people who want us to make apps for them by refusing to scare our users, but there will be other people who want us to build apps.

We will hopefully all be writing apps for years… Unless our users stop being our users.

Don't forget that very few mobile apps are "necessary."  They're a luxury - and many (if not most) app purchases are quick impulse buys.  It probably won't take much fear on the part of an individual user before he or she decides that, even if the App is worth 99 cents, it isn't worth the risk that it might steal their data.

Then we all lose.

(Path Story link Via @mattgemmell.)

Saturday
Oct292011

HTTP Testing to the edge on iOS: The School of Hard Mocks

I'm a big fan of Automated Testing, even on iOS projects, but even when I was doing mostly Ruby, Java and C# work, I was never a big user of mock objects.

Now, I'll admit that Mock objects can be useful under some circumstances, but I've seen them used too often in cases where a bunch of different developers each build their own little fiefdoms of their own code surrounded by Mock Objects where they interact with anything else. Each developer's code runs perfectly when being tested against Mock input, but when you put all that code together, you can't get a transaction working end-to-end, because the real world doesn't obey the assumptions inherent in the Mocks.

So the Automated tests I write have more of a tendency to be more like what are called "Integration Tests", even though I use "Unit Testing Frameworks" to write them.

When you combine that with the fact that I write a lot of iOS apps that talk to web servers, I want a way to test how my code talks to HTTP. And I want to know that my code handles things even when any library I happen to be using hands me errors to deal with.

When I first solved this problem years ago, we were using the Google Toolbox for Mac as our unit test framework, because SenTest didn't work well back then. So, at the time, I built a solution based on GTM, but I went recently to try to recreate that solution for a new customer, and I realized that the files I had used had been deleted from GTM.

So I went spelunking back in the history of GTM, and pulled out the last version of the the files I needed before they were removed, and put them up on github in a new repository I'm calling SyntheticServerTest.

The way you use it is to look at the example test file.

You just have to run:

[testServer startServerWithDocumentRoot:]

With a document root directory that contains web content (I usually use 'curl -O' to pull known pages (either good or errors) from the server and write them to that directory), and have a mechanism so, that, while your test is running, the host part of the URL gets replaced with

@"localhost:%d",[testServer port]

and the test HTTP server, running in the background (in the simulator or on the device), will get the HTTP request and hand you back the test data file in response.

This way, you can test your App's entire HTTP stack, including any parsing libraries you may be using, with both real data (that doesn't require you to be on line) and with bad data, to make sure you are correctly handling whatever errors your parsing or network library is throwing off.

Saturday
Oct152011

Using Regular Expressions Part 2 - The Cocoa Connection

Last time, in Part 1 of this series, I wrote about the basics of regular expressions, and the phrases I tend to use. Today, I'm going to talk about the mechanics of how I use Regular Expressions in Cocoa.

But first, an historical diversion

In my opinion there are, two different ways that programming languages implement Regular Expressions: The perl/ruby way, and the Java/C#/Python/Cocoa way.

In ruby and perl, regexes are implemented directly on the String type, whereas in the other languages, there a separate object that contains the functionality. Here's what you need to know to do a regex substitution on a string in ruby:

myString.sub('pattern','replacement')

clean, easy, and immediately useable if you know what pattern you want to use.

Here's what you need to know to do the same thing in Cocoa:

+[NSRegularExpression regularExpressionWithPattern:(NSString *) pattern 
    options:(NSRegularExpressionOptions)options error:(NSError **) error]

-[NSRegularExpression replaceMatchesInString:(NSMutableString *) string 
    options:(NSMatchingOptions)options range:(NSRange)range 
    withTemplate:(NSString *)template]

which is not clean, not easy and contains a bunch of stuff you have to go look up to be able to get started. What are NSRegularExpressionOptions and NSMatchingOptions? What's a template? Do I really have to create an NSRange for this? And that leads to the obvious question: Is all this effort really worth it?

Now I don't know about you, but I don't want to spend any effort remembering any of those option parameters, and I don't want to take the time to look them up any time I want to use a regular expression. To me, the beauty of Objective-C is that it gives us the ability to build most of what you need to know directly into the method signatures.

Let's simplify things a little

So that's what I did. For the rest of this post, I'll be using the categories on NSString found in a repo I wrote on github called RegexOnNString.

There are three basic methods I wrote:

-(NSString *) [NSString stringByReplacingRegexPattern:(NSString *)regex 
    withString:(NSString *) replacement]

which takes a string, finds the occurrences of the regex pattern and replaces them with the string replacement.

-(NSArray *) [NSString stringsByExtractingGroupsUsingRegexPattern:(NSString *)regex]

which gives you an array of all the pattern groups (things in parentheses) it found in your string, and

-(BOOL) [NSString matchesPatternRegexPattern:(NSString *)regex]

which just tells you whether a pattern is present in your string or not.

There are two additional, optional parameters that you can add, caseInsensitive:(BOOL) ignoreCase and treatAsOneLine:(BOOL) assumeMultiLine.

caseInsensitive is hopefully self explanatory, and treatAsOneLine just means that you expect that your string has (or might have) newline (\n) characters in it, and you want them to be treated like any other character.

To get them, you just need to grab the MIT-licensed code from github, include NSString+PDRegex.h and NSString+PDRegex.m in your project, and put

#import "NSString+PDRegex.h"

in the top of your source file.

How about some examples?

The simplest of these is the one that returns the boolean, like so:

if(![emailAddress matchesPatternRegexPattern:@"@.*\\\\..*"]) {
    NSLog(@"If the user is going to give us a fake email address" \
          @" they could at least try and make it look like one" \
          @" by making sure it has an at-sign and a dot in it.");
}

I use this a lot for string validation. No sense in trying to send an email if there isn't an at-sign in it, and no use trying to convert an NSString to an NSURL if the string doesn't at least contain '://'. (Note that I have to use two backslashes there because a @"@.*\\." will cause Xcode to generate a: Lexical or Preprocessor Issue: Unknown escape sequence '\\.' warning).

The one I use next most often is the one that returns an NSString. I use this one for extracting substrings. For example, in:

<Warning: Shamless Plug> a Mac application I recently released as an Open Beta that helps iOS developers deploy Apps to their test devices without having to use a USB cable <End Shamless Plug>

I'm getting a string that is the path of the .app file that the user dragged-and-dropped onto my App (and it's either a path if they dropped it onto the App Icon in the Dock, or a URL if they dropped it onto the Window). From that string, I need to figure out what their iOS App is named (so I can use that name in the notification). I use the stringByReplacingRegexPattern method for that. I could use [[NSString lastPathComponent] stringByDeletingPathExtension] for that, but by using regexes, I don't have to go look up the path component methods, like I just did to put them in this post. But an even better example from that app is:

NSString *dSYMPath = [droppedPath stringByReplacingRegexPattern:@"\\\\.app$" withString:@".dSYM"];

So that I can save off the dSYMs so that the user can get to the symbol data for that build if they need it later.

I also use it so that, in order for my App to get the user's Dropbox's public URL, I can let the user drop any Public URL that Dropbox gives them into the preferences panel, and I can use:

 NSString *dbPublicRoot=[pastedLink stringByReplacingRegexPattern:
    @"^(http://dl.dropbox.com/u/[0-9][0-9]*)[^0-9]*.*$" withString:@"$1" caseInsensitive:NO];

so that I don't have to rely on the user to correctly truncate the URL at the right place, and the user doesn't have to think about it.

The last method, the one that returns the NSArray, I don't use as often, but when I do, it can save me a lot of effort. For example, recently I was implementing a Tic-Tac-Toe game as a programming exercise as part of an interview process. So when I was shipping turns between the two players, I was actually sending one string:

NSString *stringForThisMove = [NSString stringWithFormat:
      @"Move %@=Player %@ to Square %@\n",
      [move TurnNumber],
      playerThatMoved,
      [move SquarePlayed]];

and then on the receiving end, I used:

NSArray *extractedStrings=[moveString
       stringsByExtractingGroupsUsingRegexPattern:
       @" *^Move  *([0-9]) *= *Player  *([X|O])  *to  *Square  *([0-9]) *$"
       caseInsensitive:YES treatAsOneLine:YES];

from which [extractedStrings objectAtIndex:0] was the move number, [extractedStrings objectAtIndex:1] was the player (@"X" or @"O") and [extractedStrings objectAtIndex:2] was the number of the square they moved to (where the first row of the board was 1-2-3 and the last row was 7-8-9).

Now, there are many other ways I could have encoded that, but the nice thing about using strings for it was that anyone looking at the intermediate value (in the debugger or logs) could easily tell what move was being talked about at that point, and if I were ever to need to come back to this code later, @"Move 1=Player X to Square 1" will make sense to me (after all, that kind of notation has been of use in the Chess world for hundreds, if not thousands of years).

But aren't Regular Expressions slower?

Well, define slow :-).

In the test suite for my RegexOnNString category, I have a test that does 1000 string replaces:

for (uint i=0; i&lt; 1000; i++) {
    if (lastTimeString) {
        NSString *currentNumberString=[NSString stringWithFormat:@"%u",i];
        NSString *replacementString=[lastTimeString stringByReplacingRegexPattern:@"[0-9][0-9]*" withString:currentNumberString caseInsensitive:NO];
        STAssertEqualObjects(replacementString, currentNumberString, @"regex replace failed");
    }
    lastTimeString=[NSString stringWithFormat:@"%u",i];
    i++;
}

Now each of those regex's is different (by design), so I can't compile them, and I'm creating and throwing away my NSRegularExpression object and a temporary NSString every run. So it's near a worst-case scenario. By way of comparison, I do another loop of 1000 replaces, using [NSString stringByReplacingOccurrencesOfString: withString:] to see how much slower the regex makes the task.

The output from running the test on my 4thGen iPod touch is:

2011-10-15 17:23:00.748 RegexOnNSStringIOSExample[224:607] 1000 regex replaces took 0.167364 seconds
2011-10-15 17:23:00.776 RegexOnNSStringIOSExample[224:607] 1000 String replaces took 0.025167 seconds
2011-10-15 17:23:00.777 RegexOnNSStringIOSExample[224:607] Simple String substitution 6.650140 times faster

and on my daughter's 2nd Gen iPod touch, the output is:

2011-10-15 17:33:37.631 RegexOnNSStringIOSExample[183:307] 1000 regex replaces took 0.641442 seconds
2011-10-15 17:33:37.756 RegexOnNSStringIOSExample[183:307] 1000 String replaces took 0.119230 seconds
2011-10-15 17:33:37.768 RegexOnNSStringIOSExample[183:307] Simple String substitution 5.379869 times faster

So yes, it's slower. It takes 0.17 milliseconds on a 4th-gen touch and 0.64 milliseconds on a 2nd-gen touch. And it's between 5 and 7 times slower than stringByReplacingOccurrencesOfString:withString. If 0.52 ms really matters in your code when running on a 2nd-gen touch, then you should use stringByReplacingOccurrencesOfString:withString instead.

So, in conclusion,

I hope you found this post useful. If you need to do string manipulation, Regular Expressions are a time-tested way to do that, and I hope the extra methods I've talked about here will simplify things if you want to do string manipulation in your Cocoa code.

Sunday
Oct022011

Using Regular Expressions and Retaining your Sanity

At a recent Austin, Texas Cocoacoder meeting, I made an offhand comment giving someone a regular expression that would help with a problem they were having. That led to two things. First, I was asked to put together a presentation (which I’ve been working on) on using regular expressions to give at an upcoming CocoaCoder meeting, and second, I was asked why on Earth anyone would use something as opaque and unmaintainable as a regular expression in this day and age.

Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems.

Or so goes the old saying. Personally, I rarely find this to be true. On the occasions when I can identify with that quote, it’s because I’m trying to deal with a regex written by someone who, in my opinion, tried to do far too much at once.

I have a certain philosophy that I try use regular expressions that seems to keep me out of trouble. In this post, I’m going to try to make a set of rules out of that philosophy. They are:

  1. Limit yourself to only the basic meta-characters.
  2. Favor clarity over brevity.
  3. Take more smaller bites.
  4. Beware of greedy matching

Let’s break these down.

1. Limit yourself to only the basic meta-characters.

Pretty much every regex tutorial or man page has a giant laundry list of what characters in a regular expression match what characters in the string you’re trying to match. I ignore most of these and look them up when I have to (which is only when I’m looking at other people’s code). I use a few “phrases” over and over, so let me go through some of those to try to give you some examples:

^.* means “the junk to the left of what I want”

This breaks down as ^ (the beginning of the string) followed by .* any number of any character. Likewise:

.*$ means “the junk to the right of what I want”

This breaks down as any number of any character .* followed by $ (the end of the string)

[0–9][0–9]* means “a number with at least one digit”

The brackets ([ and ]) mean “any of the characters contained within the brackets”. So this means 1 character of 0–9 (so 0 1 2 3 4 5 6 7 8 or 9) followed by zero or more of the same character.

[^A-Za-z] means “any character that’s not a letter”

The ^ as the first character inside the brackets reverses means “not” so instead of meaning “any letter” it means “anything that isn’t a letter”. Likewise, [^0-9] means “anything that isn’t a number”.

\. a literal dot

So this is what you’d use to match the . in .com

\( …stuff… \) stuff I want to refer to later

This causes the string matched inside the parenthesis to be retrieved later.

2. Favor Clarity Over Brevity

There are a lot of shortcuts you can take, like \w means “any character that appears in a word” (I know because I just looked it up). Don’t use it or things like it. Because \W (that’s a capital ‘W’) means “not a word” and they look too much alike. Also, the mnemonic is rubbish, does “W” stand for “Word” or “Whitespace”? Use [A-Za-z] instead. It’s clearer when you’re writing it, and clearer when you look at it later. It’s more keystrokes, but it’s worth it. Likewise there’s a ‘+’ which means “one or more” so [0–9]+ is equivalent of [0–9][0–9], but if you always do it the second way, there’s one less special character to remember and you won’t ever accidentally type [0–9] when you meant [0–9]+ and accidentally match nothing.

3. Take more smaller bites.

This is really the core of it. I often use several different regular expressions to get one string I want. It’s more typing and it may potentially create some intermediate strings that have to be thrown away, but it’s much easier to read. So if I were trying to extract an HREF link from an HTML document, for example, I’d use two regular expressions:

^.*href=["’]

and

["’].*$

The first one of those matches everything up to the single or double quote before the URL, and the second one matches from the quote after the URL through the rest of the string. I use a substitute mechanism to throw both of those parts of the string away, and I’m left with the URL I want. *(Note - Those regex’s are simplified for illustration purposes. For example, the = sign might have whitespace around it, etc.)

4. Beware greedy matching

People get themselves in trouble by forgetting about greedy matching which is the requirement that a * in a pattern matches as much as it can. So let’s say we were doing URL extraction again, and you used the pattern:

^.href=“\(.*\)”.$

In theory, that should say (out of the whole string, from beginning (^) to end ($) grab the thing in between the quotes right after the string href=" and remember it. However, if I gave you the string <a href=“http://1.example.com/”>This is a link</a> but <a href=“http://2.example.com/”>This is a link, too.</a> and you ran your regex against it, you’d get http://1.example.com/“>This is a link</a> but <a href=”http://2.example.com/ as what your regex remembered, because the “.*” grabbed everything between the very first quote and the very last quote. In this case, either use ^.href=“\([^"]*\)”.$ (replacing the .* with [^"] so that the .* doesn’t match quotes) or use more regex’s, and take a smaller bite with each.

That’s a long enough post for now. Next time, I’ll introduce an Objective C category I’m working on and will hopefully have done by then which will simplify using NSRegularExpression, and we'll work through more examples.