In some of my iOS projects I had to display PDFs of documents originally intended for print format. Some of those PDFs would appear horribly on iOS devices, with colors messed up. It was a pretty surprising behaviour, which I initially attributed to bugs in Apple's PDF rendering code.
My first approach to getting around such problems was to use a server-side component powered by Ghostscript to convert each PDF page into an image, that would show up in the right colors, and display that. The advantage of this approach is that it can be fully automated. The disadvantage is that images may show up poorly (pixelated) when zoomed in, depending on their pixel density.
Upon investigating this problem further I discovered a way to fix this problem at its root, that is the source PDF document. Unfortunately it can't be automated easily because it involves running Adobe's Acrobat tool. In the absence of a public API to run that conversion programatically one has the option of using workflow-recording tools like Apple's Automator to drive the Acrobat automatically. While that approach may work, it would be hard to scale in a cost-effective manner.
Another option to handle this may be to use iText to automatically convert CYMK profiles to RGB, or if that's not possible, to at least detect the presence of CYMK profiles and alert the user about the need to convert the PDF manually using Acrobat.
Saturday, May 25, 2013
Tuesday, May 14, 2013
So we had the www for a while. What's next ?
The dawn of the web introduced us to URLs starting with "http://www". For those old enough to remember those days of Lynx, Mosaic, and Netscape, "www" was true to its meaning of "world-wide web". Suddenly anyone connected to the Internet was able to reach out to any remote place in the world, also connected to the net, and browse the information published there. Powerful.
Since then a lot has changed. The devices we use to access the net had moved from our desks to our palms, and soon to our glasses, and with the mobility they afford, the Internet access got a new dimension - that of context, or location. So in addition to what's out there (www) now what's over here becomes an interesting question. Perhaps some new URLs, like "http://here" and "http://now", start to make sense. Seems that I'm not the only one thinking so. Nokia appears to be doing something like that with http://here.com. If face recognition becomes a feature of Google Glass, then a "http://you" makes sense too. I'm probably stating the obvious here, but what I'd like to see is more of these 2 things:
1. The ability of users to freely add and annotate the content presented. Think of "Augmented Reality Wikipedia". Like "digital graffiti" curated through a vote-based meritocracy, or by applying a filtering based on what your contacts on social networks think of that content.
2. More mash-ups. With the growing number of web services (over 9000 APIs as of now), and the continued move toward a more semantic web, the ability to mash things up should continue to grow.
It'd be interesting to see how all this will unfold, and maybe even contribute to this vision. Such projects, once they pass a critical mass of adoption, they tend to snowball fast.
Since then a lot has changed. The devices we use to access the net had moved from our desks to our palms, and soon to our glasses, and with the mobility they afford, the Internet access got a new dimension - that of context, or location. So in addition to what's out there (www) now what's over here becomes an interesting question. Perhaps some new URLs, like "http://here" and "http://now", start to make sense. Seems that I'm not the only one thinking so. Nokia appears to be doing something like that with http://here.com. If face recognition becomes a feature of Google Glass, then a "http://you" makes sense too. I'm probably stating the obvious here, but what I'd like to see is more of these 2 things:
1. The ability of users to freely add and annotate the content presented. Think of "Augmented Reality Wikipedia". Like "digital graffiti" curated through a vote-based meritocracy, or by applying a filtering based on what your contacts on social networks think of that content.
2. More mash-ups. With the growing number of web services (over 9000 APIs as of now), and the continued move toward a more semantic web, the ability to mash things up should continue to grow.
It'd be interesting to see how all this will unfold, and maybe even contribute to this vision. Such projects, once they pass a critical mass of adoption, they tend to snowball fast.
Friday, May 10, 2013
Rendering office documents in HTML - an idea whose time has come
A while ago I was working on a project that needed rendering PDFs on iOS. There are several ways to do that. On iOS, that is. My client wanted a solution for Android too, and was complaining about the lack of similar capabilities there. Since Android wasn't one of my areas of competence at the time I wasn't involved with that effort directly. However I kept hearing about the need to use some 3rd party, rather expensive, libraries.
So naturally it dawned on me this idea: why don't the "owners" of the PDF format, Adobe, provide a cross-platform viewer. The natural solution would be an HTML based approach, since virtually all popular platforms provide a way to display HTML. At the time I was thinking that the capabilities provided by HTML 5 and CSS 3 should be enough to accomplish the "print-quality" level of rendering offered by Adobe's Acrobat Reader or Apple's Preview. Since Adobe wasn't doing much about it, it seemed a pretty good opportunity. Turns out that wasn't such a bad idea. In fact folks at Crocodoc were banging at it since 2006. Today was the payback. Good for them.
I only heard about them today. A brief glance at their API shows that right now it fetches the documents from some servers, where they're processed and converted from PDF to HTML. With this dependency on a server-side component it doesn't seem to have support for offline viewing. I guess one could hack around and save the generated HTML and all the referenced components (images, CSS, and what not), but that may be a brittle solution. With their new owner I'm sure they'll keep improving their technology and this may well become a compelling cross-platform document rendering solution.
So naturally it dawned on me this idea: why don't the "owners" of the PDF format, Adobe, provide a cross-platform viewer. The natural solution would be an HTML based approach, since virtually all popular platforms provide a way to display HTML. At the time I was thinking that the capabilities provided by HTML 5 and CSS 3 should be enough to accomplish the "print-quality" level of rendering offered by Adobe's Acrobat Reader or Apple's Preview. Since Adobe wasn't doing much about it, it seemed a pretty good opportunity. Turns out that wasn't such a bad idea. In fact folks at Crocodoc were banging at it since 2006. Today was the payback. Good for them.
I only heard about them today. A brief glance at their API shows that right now it fetches the documents from some servers, where they're processed and converted from PDF to HTML. With this dependency on a server-side component it doesn't seem to have support for offline viewing. I guess one could hack around and save the generated HTML and all the referenced components (images, CSS, and what not), but that may be a brittle solution. With their new owner I'm sure they'll keep improving their technology and this may well become a compelling cross-platform document rendering solution.
Saturday, May 04, 2013
Caveats of using MPMoviePlayerController
For a recent iOS project I had to play some videos, embedded in a subview of a larger view. One way to achieve that is to employ the MPMoviePlayerController. At first glance that component seems easy enough to use. Folks at Apple are trying to help by providing the following code snippet, right there in the official docs:
Right on. I'm adding this stuff to my app, and ... nothing happens. Nothing plays, just a black rectangle where the video player was supposed to show up. My first reaction was to look if they have a complete sample project that I could play with, and learn how to use the API. They do. I open it and it's using manual memory management (pre-ARC). Yuck ! Still I look there and see that they're using notifications to observe the state of the player. That sounds like a good way to debug what's going on. I do the same in my code. None of the notifications get called. What the ... ?
On a 2nd look at the code above, it becomes clear that in an ARC project, like mine was, the player local variable will be gone at the end of its enclosing scope. No wonder that nothing happens. The player must be kept alive through a strong ivar, or property. Doing that fixed my problem. So Apple's code snippet is pretty bad. It would have worked in the pre-ARC projects, albeit by leaking memory, but obviously this stuff was never updated for ARC.
Luckily I have plenty of experience doing manual memory management, and I can handle old code bases, like so many of Apple's code samples are. For now I'll inform their docs team of the problem.
MPMoviePlayerController *player =
[[MPMoviePlayerController alloc] initWithContentURL: myURL];
[player prepareToPlay];
[player.view setFrame: myView.bounds]; // player's frame must match parent's
[myView addSubview: player.view];
// ...
[player play];
Right on. I'm adding this stuff to my app, and ... nothing happens. Nothing plays, just a black rectangle where the video player was supposed to show up. My first reaction was to look if they have a complete sample project that I could play with, and learn how to use the API. They do. I open it and it's using manual memory management (pre-ARC). Yuck ! Still I look there and see that they're using notifications to observe the state of the player. That sounds like a good way to debug what's going on. I do the same in my code. None of the notifications get called. What the ... ?
On a 2nd look at the code above, it becomes clear that in an ARC project, like mine was, the player local variable will be gone at the end of its enclosing scope. No wonder that nothing happens. The player must be kept alive through a strong ivar, or property. Doing that fixed my problem. So Apple's code snippet is pretty bad. It would have worked in the pre-ARC projects, albeit by leaking memory, but obviously this stuff was never updated for ARC.
Luckily I have plenty of experience doing manual memory management, and I can handle old code bases, like so many of Apple's code samples are. For now I'll inform their docs team of the problem.
Saturday, April 27, 2013
Keeping the code clean of warnings helps your productivity
In some Fortune 500 companies I worked we had an official policy of 0 compilation warnings allowed in the code. That was a good practice that saved us of many troubles. Since then I try to follow that practice in my projects.
Let's see an example:
If I placed this code in a project with the default settings created by Xcode, the result wouldn't output the "OK" string, due to the "case1:" typo which transformed the "case 1:" case statement into an unused label. The compilation wouldn't produce any warning. Neither would the static analyzer.
You can however catch such problems at compile time, by changing the default settings like so:
This enables all warnings. The following setting treats all warning as errors:
If I now try to compile I get this error:
Catching this bug at compile time is much better than later when the app may have been released.
Another useful setting to have is this:
which runs the static analyzer on every compilation. I found this combination of settings very useful to smoke out bugs shortly after they have been introduced.
Let's see an example:
NSLog(@"Hello, World!");
int i = 1;
switch (i) {
case1:
NSLog(@"OK");
break;
}
If I placed this code in a project with the default settings created by Xcode, the result wouldn't output the "OK" string, due to the "case1:" typo which transformed the "case 1:" case statement into an unused label. The compilation wouldn't produce any warning. Neither would the static analyzer.
You can however catch such problems at compile time, by changing the default settings like so:
Another useful setting to have is this:
which runs the static analyzer on every compilation. I found this combination of settings very useful to smoke out bugs shortly after they have been introduced.
Saturday, April 20, 2013
Unexpected side effect of fast enumeration in Objective-C
Fast enumeration is a relatively new capability in Objective-C. For a thorough survey of all the ways one can enumerate through a collection, you can see this post.
In a recent project I had a code sequence like this (modified here for illustration):
My expectation was that at the end of the for loop the number variable would hold the last value of the array. Here's what I get instead:
Strangely the value comes back as nil. Which is what it was initialized to before entering the loop. Could that be the case ? Let's try this instead:
which produces:
Nope, I still get nil, not the @100 value at the entry of the loop. So it seems that fast enumeration blocks reset the value of the iteration pointer in the outer context back to nil. This is, at least to me, unexpected. Maybe it's a bug. I'll see what Apple has to say.
In a recent project I had a code sequence like this (modified here for illustration):
NSArray *array = @[@1, @2];
NSNumber *number = nil;
for (number in array) {
NSLog(@"number: %@", number);
}
NSLog(@"Last number: %@", number);
My expectation was that at the end of the for loop the number variable would hold the last value of the array. Here's what I get instead:
2013-04-19 17:13:06.223 FastEnumBug[49016:303] number: 1
2013-04-19 17:13:06.240 FastEnumBug[49016:303] number: 2
2013-04-19 17:13:06.241 FastEnumBug[49016:303] Last number: (null)
Strangely the value comes back as nil. Which is what it was initialized to before entering the loop. Could that be the case ? Let's try this instead:
NSArray *array = @[@1, @2];
NSNumber *number = @100;
for (number in array) {
NSLog(@"number: %@", number);
}
NSLog(@"Last number: %@", number);
which produces:
2013-04-20 18:47:34.209 FastEnumBug[62468:303] number: 1
2013-04-20 18:47:34.212 FastEnumBug[62468:303] number: 2
2013-04-20 18:47:34.212 FastEnumBug[62468:303] Last number: (null)
Nope, I still get nil, not the @100 value at the entry of the loop. So it seems that fast enumeration blocks reset the value of the iteration pointer in the outer context back to nil. This is, at least to me, unexpected. Maybe it's a bug. I'll see what Apple has to say.
Saturday, March 23, 2013
Charging for wireless services by auction
I've been thinking about this for a while now. All mobile network operators have a rigid price structure, in some markets, like Canada, with outrageous costs. Yet when certain areas of the network are idle, due to low traffic, the network equipment remains unused, and is producing no revenue. It strikes me as a very inefficient way to allocate and manage resources. I believe an auction-based model, where available capacity is up for bidding would be more efficient.
Once the costs of laying out a network have been sunk, the ongoing cost of operations should dwarf the initial investment, and should allow for a wide range of pricing to make economic sense. That is, the cost of allowing access to a spare channel of communication between a mobile device and the network shouldn't be much higher than the cost of keeping that channel unused, on stand-by. I'd venture to guess that most of the additional cost is for the energy used to power the transmission.
Hence the opportunity, if I'm a customer in the right place at the right time, and the cell covering that location has plenty of spare capacity at that particular time, I should be given the opportunity to get access to the network at a rate lower than the going rate. Whereas I would almost never pay the $10/GB going rate, I would be willing to pay a lower price. Because of the rigid pricing, however, this opportunity goes wasted. I don't get to use the service, the service provider doesn't get paid. Lose-lose scenario. Flexible pricing would turn it in a win-win.
Conversely, sometimes you might find yourself in an area that's highly congested, with no physical ability to accommodate everyone, even at the going rate. Some calls will need to be dropped. Flexible pricing would be a way to guarantee a connection for the higher bidders. So sometime I would be willing to pay above the $10/GB rate.
Such flexibility would benefit all parties. Sure there would be some complexity added to metering and billing, and some signalling mechanisms should be devised for dealing with the bidding process, but the gains may far outweigh the costs. Why nobody implemented something like this, especially for pre-paid or pay-per-use plans, remains a mystery.
Once the costs of laying out a network have been sunk, the ongoing cost of operations should dwarf the initial investment, and should allow for a wide range of pricing to make economic sense. That is, the cost of allowing access to a spare channel of communication between a mobile device and the network shouldn't be much higher than the cost of keeping that channel unused, on stand-by. I'd venture to guess that most of the additional cost is for the energy used to power the transmission.
Hence the opportunity, if I'm a customer in the right place at the right time, and the cell covering that location has plenty of spare capacity at that particular time, I should be given the opportunity to get access to the network at a rate lower than the going rate. Whereas I would almost never pay the $10/GB going rate, I would be willing to pay a lower price. Because of the rigid pricing, however, this opportunity goes wasted. I don't get to use the service, the service provider doesn't get paid. Lose-lose scenario. Flexible pricing would turn it in a win-win.
Conversely, sometimes you might find yourself in an area that's highly congested, with no physical ability to accommodate everyone, even at the going rate. Some calls will need to be dropped. Flexible pricing would be a way to guarantee a connection for the higher bidders. So sometime I would be willing to pay above the $10/GB rate.
Such flexibility would benefit all parties. Sure there would be some complexity added to metering and billing, and some signalling mechanisms should be devised for dealing with the bidding process, but the gains may far outweigh the costs. Why nobody implemented something like this, especially for pre-paid or pay-per-use plans, remains a mystery.
Saturday, February 16, 2013
UIView gotchas in iOS
In iOS the UI consists of a hierarchy of views and view controllers, linked through containment relationships. That's following the common composite design pattern, which should be familiar to most programmers. In addition there is a specific system-imposed lifecycle, which governs how all those pieces come into existence, and various callbacks that the system (Cocoa Touch) calls at specific points in this lifecycle. All this is fairly well documented, but with experience comes additional insights, and lessons learned the hard way. Here's what I discovered:
If your code relies on the coordinates of a view's frame, then your best bet is reading the frame in:
You may be tempted to use - (void)viewDidLoad for the same purpose, which gets called earlier in the lifecycle, however at that time the frame may not have its value set accurately. I find this happening often especially for iPad. Most times I use the storyboard (and IB in the past) to implement my UIs. I usually lay it out in portrait mode only, and rely on the autoresizing mask, or layout constraints, to position everything properly. Because on iPad you can launch an app in landscape mode directly, the frame in viewDidLoad will appear as defined in the storyboard, with the size for portrait. If those values are relied upon later, that would result in problems.
This problem may also occur in obscure contexts, where one is not directly using the frame. For example in a previous post I was mentioning some weird behaviour when setting the value of a progress bar, like so:
[self.progressBar setProgress:0.5 animated:YES];
Apple took a look at it and informed me that "The view animates from an undefined state. Doing such things in the viewDidLoad can only lead to problems.".
Update:
While watching the slides from Stanford's iOS Programming course I discovered that in case that autolayout is used, instead of using viewWillAppear to get the view's geometry, one must use
- (void)viewDidLayoutSubviews
Apparently the latter is guaranteed to be called each time the bounds of the view change, and by the time that happens all the autolayout constrains would have been processed and the geometry would have been established.
If your code relies on the coordinates of a view's frame, then your best bet is reading the frame in:
- (void)viewWillAppear:(BOOL)animated
You may be tempted to use - (void)viewDidLoad for the same purpose, which gets called earlier in the lifecycle, however at that time the frame may not have its value set accurately. I find this happening often especially for iPad. Most times I use the storyboard (and IB in the past) to implement my UIs. I usually lay it out in portrait mode only, and rely on the autoresizing mask, or layout constraints, to position everything properly. Because on iPad you can launch an app in landscape mode directly, the frame in viewDidLoad will appear as defined in the storyboard, with the size for portrait. If those values are relied upon later, that would result in problems.
This problem may also occur in obscure contexts, where one is not directly using the frame. For example in a previous post I was mentioning some weird behaviour when setting the value of a progress bar, like so:
[self.progressBar setProgress:0.5 animated:YES];
Apple took a look at it and informed me that "The view animates from an undefined state. Doing such things in the viewDidLoad can only lead to problems.".
Update:
While watching the slides from Stanford's iOS Programming course I discovered that in case that autolayout is used, instead of using viewWillAppear to get the view's geometry, one must use
- (void)viewDidLayoutSubviews
Apparently the latter is guaranteed to be called each time the bounds of the view change, and by the time that happens all the autolayout constrains would have been processed and the geometry would have been established.
Saturday, January 26, 2013
Unexpected behaviour when calling setProgress:animated: twice in the same run loop
I found some bugs in setProgress:animated: method of UIProgressView. It seems that calling it several times within the same run loop ignores all but the last call.
For instance, I have a bar set up in the storyboard with the progress all the way to the max (i.e. 1.0). If later in the code I do this:
- (IBAction)showBug {
[self.progressBar setProgress:0.0 animated:NO];
[self.progressBar setProgress:0.5 animated:YES];
}
I would expect the bar to jump to 0 and then animate to 0.5. What happens instead, the bar animates from 1.0 to 0.5.
One workaround, of course, is to place the 2nd call on a separate run loop, like so:
Ahh, the trusted 0 timer. How many times it saved my skin. For instance, when mutating collections while iterating. And not only in Objective-C, I remember using it in JavaScript to work around all kinds of browser bugs. That's the advantage of having exposure to multiple platforms and languages. You learn all kinds of tricks.
Incidentally, when I was preparing the bug report for Apple, I discovered yet another, unrelated bug. Placing this line:
[self.progressBar setProgress:0.5 animated:YES];
in the - (void)viewDidLoad method of the view controller makes another mess. Now the bar, which was placed statically in the storyboard, (in my example I had it centred) it animates when the app starts, through a "genie" animation from the top-left. Nice, but definitely not what I intended.
Update: Regarding the latter bug, Apple informed me that "The view animates from an undefined state. Doing such things in the viewDidLoad can only lead to problems.". See this post for a discussion of the perils of reading/writing the frame of a view in viewDidLoad.
For instance, I have a bar set up in the storyboard with the progress all the way to the max (i.e. 1.0). If later in the code I do this:
[self.progressBar setProgress:0.0 animated:NO];
[self.progressBar setProgress:0.5 animated:YES];
}
I would expect the bar to jump to 0 and then animate to 0.5. What happens instead, the bar animates from 1.0 to 0.5.
One workaround, of course, is to place the 2nd call on a separate run loop, like so:
- (void)hack
{
[self.progressBar setProgress:0.5 animated:YES];
}
- (IBAction)fixIt {
[self.progressBar setProgress:0.0 animated:NO];
[self performSelector:@selector(hack) withObject:nil afterDelay:0];
}
Ahh, the trusted 0 timer. How many times it saved my skin. For instance, when mutating collections while iterating. And not only in Objective-C, I remember using it in JavaScript to work around all kinds of browser bugs. That's the advantage of having exposure to multiple platforms and languages. You learn all kinds of tricks.
Incidentally, when I was preparing the bug report for Apple, I discovered yet another, unrelated bug. Placing this line:
[self.progressBar setProgress:0.5 animated:YES];
in the - (void)viewDidLoad method of the view controller makes another mess. Now the bar, which was placed statically in the storyboard, (in my example I had it centred) it animates when the app starts, through a "genie" animation from the top-left. Nice, but definitely not what I intended.
Update: Regarding the latter bug, Apple informed me that "The view animates from an undefined state. Doing such things in the viewDidLoad can only lead to problems.". See this post for a discussion of the perils of reading/writing the frame of a view in viewDidLoad.
Saturday, December 22, 2012
View gets inset by 20 points when instantiated through instantiateViewControllerWithIdentifier
In a recent project I had a custom container view controller in which I wanted to embed another custom content controller, as a child controller. Since the content controller already resided in the storyboard I thought I'd use a sequence of code like so:
Giving it a try produced this result:
You can see an unsightly white band at the top of the screen. By the looks of it it seems about the same size as the status bar. It's as if the view of the embedded view controller loaded by instantiateViewControllerWithIdentifier is inset to make room for the status bar. A little debugging confirmed that indeed that's the case:
My first thought was that it may be caused by the default "Status Bar" setting under the "Simulated Metrics" section in the utility pane. That "simulated" qualifier still raised some questions. AFAIK those simulated metrics are only used as an aid in designing the UI, without any effect at runtime. A more promising clue should be given by the size inspector of that view in IB:
Hmm, looks like that 20 points offset might be the culprit. Yet that's a grayed-out value. How to change that ? Let's fiddle with the simulated metrics, just for kicks:
and check out what we get as a result:
You'd think this would fix the problem. Alas it doesn't. I think this may be a bug in Xcode. I believe the change in the simulated metrics shouldn't affect the value of the view's frame. In any case, I find the interplay between the "Simulated Metrics" of the view controller and the "Size Inspector" of its view pretty confusing. For example, sometime you can't change the struts and springs in "Size Inspector" unless you set the "Size" in "Simulated Metrics" to freeform.
So it looks like we need to fix this in code, by pinning the frame of the embedded view in the top-left corner of its superview, like so:
This takes care of the problem. Hopefully Apple will keep improving their Interface Builder, and add more CASE capabilities to Xcode. I fondly remember using ObjecTime and enjoying the productivity gains that afforded. After all the best code is no code. Easiest to write, read and maintain :).
UPDATE:
I found a better fix for this issue. It started as a solution for another problem. It turns out that if you have a view controller defined in the storyboard, with a non-standard size, when loaded by default would show up at full screen size. It turns out all we need is to deselect the "Resize View From NIB" checkbox. This also eliminates that 20 points inset at the top.
PdfViewController *pvc = [self.storyboard instantiateViewControllerWithIdentifier:@"PdfViewController"];
[self addChildViewController:pvc];
[self.view addSubview:pvc.view];
[pvc didMoveToParentViewController:self];
Giving it a try produced this result:
You can see an unsightly white band at the top of the screen. By the looks of it it seems about the same size as the status bar. It's as if the view of the embedded view controller loaded by instantiateViewControllerWithIdentifier is inset to make room for the status bar. A little debugging confirmed that indeed that's the case:
(lldb) p (CGRect)[[pvc view] frame]
(CGRect) $1 = origin=(x=0, y=20) size=(width=320, height=460)
(lldb)
My first thought was that it may be caused by the default "Status Bar" setting under the "Simulated Metrics" section in the utility pane. That "simulated" qualifier still raised some questions. AFAIK those simulated metrics are only used as an aid in designing the UI, without any effect at runtime. A more promising clue should be given by the size inspector of that view in IB:
Hmm, looks like that 20 points offset might be the culprit. Yet that's a grayed-out value. How to change that ? Let's fiddle with the simulated metrics, just for kicks:
and check out what we get as a result:
You'd think this would fix the problem. Alas it doesn't. I think this may be a bug in Xcode. I believe the change in the simulated metrics shouldn't affect the value of the view's frame. In any case, I find the interplay between the "Simulated Metrics" of the view controller and the "Size Inspector" of its view pretty confusing. For example, sometime you can't change the struts and springs in "Size Inspector" unless you set the "Size" in "Simulated Metrics" to freeform.
So it looks like we need to fix this in code, by pinning the frame of the embedded view in the top-left corner of its superview, like so:
PdfViewController *pvc = [self.storyboard instantiateViewControllerWithIdentifier:@"PdfViewController"];
[self addChildViewController:pvc];
pvc.view.frame = CGRectMake(0, 0, pvc.view.frame.size.width, pvc.view.frame.size.height); // fix
[self.view addSubview:pvc.view];
[pvc didMoveToParentViewController:self];
UPDATE:
I found a better fix for this issue. It started as a solution for another problem. It turns out that if you have a view controller defined in the storyboard, with a non-standard size, when loaded by default would show up at full screen size. It turns out all we need is to deselect the "Resize View From NIB" checkbox. This also eliminates that 20 points inset at the top.
Saturday, November 17, 2012
Using the iOS simulator for debugging
If you spend some time with the iOS simulator you'll notice the "Debug" menu, which contains some capabilities that come handy when investigating problems with apps under development.
Such a potential problem could be flickery or jerky animations. Since the duration of the animation on normal use can be pretty short, it may be difficult to see what's going on. Using the "Toggle Slow Animations" option, the first under the "Debug" menu, slows it down enough to allow getting an insight on what may be the problem. For an example of such problem, and the difference that slower animations makes, see the video below:
Once we have visibility on the problem, we can try various fixes and use the slow-mo animations to see if the fix produces a better outcome:
Another group of debugging functionality has the "Color Blended Layers", "Color Copied Images", "Color Misaligned Images" and "Color Offscreen-Rendered" options. All these options can be used to expose inefficient composite drawing, and they're also available through Instruments.
Yet another group of functionality appears under the "Location", which allows simulating changing the location for apps that use location services.
I'm amazed by the quality of development tools that Apple provides, far exceeding other platforms.
If you have an app developed, perhaps by an outsourcing developer like me, and notice some problems with animation, you may ask your developer to toggle those options and produce a screen recording of the app running in the simulator, like I did above, to prove that any issues have been addressed.
Such a potential problem could be flickery or jerky animations. Since the duration of the animation on normal use can be pretty short, it may be difficult to see what's going on. Using the "Toggle Slow Animations" option, the first under the "Debug" menu, slows it down enough to allow getting an insight on what may be the problem. For an example of such problem, and the difference that slower animations makes, see the video below:
Another group of debugging functionality has the "Color Blended Layers", "Color Copied Images", "Color Misaligned Images" and "Color Offscreen-Rendered" options. All these options can be used to expose inefficient composite drawing, and they're also available through Instruments.
Yet another group of functionality appears under the "Location", which allows simulating changing the location for apps that use location services.
I'm amazed by the quality of development tools that Apple provides, far exceeding other platforms.
If you have an app developed, perhaps by an outsourcing developer like me, and notice some problems with animation, you may ask your developer to toggle those options and produce a screen recording of the app running in the simulator, like I did above, to prove that any issues have been addressed.
Saturday, November 03, 2012
How to access backing ivars for inherited properties in Objective-C
I had a superclass that declared and synthesized a read only property, meant to be set in its subclasses. The code was something like this:
The instance variable backing the property seemed to be invisible in the subclass. The error was pretty unclear when compiling an iOS app: "Use of undeclared identifier '_prop'". To get around the error I did the following:
That is, I explicitly declared the backing variable in the declaration of the superclass.
Interestingly, when I was preparing this blog post I compiled the code above as a "Command Line Tool" type of project, not an iOS app. The error was different, dare I say better, more telling of the root cause: "Instance variable '_prop' is private". So it's a matter of the visibility of the ivar, so perhaps a better fix would be:
Kind of interesting though that properties are synthesized by default as private ivars. Learned something new.
@interface A : NSObject
@property (nonatomic, readonly) NSUInteger prop;
@end
@implementation A
@synthesize prop = _prop;
@end
@interface B : A
@end
@implementation B
- (void)establishValueForProp
{
_prop = 1; // PROBLEM !!!
}
@end
The instance variable backing the property seemed to be invisible in the subclass. The error was pretty unclear when compiling an iOS app: "Use of undeclared identifier '_prop'". To get around the error I did the following:
@interface A : NSObject {
NSUInteger _prop;
}
@property (nonatomic, readonly) NSUInteger prop;
@end
That is, I explicitly declared the backing variable in the declaration of the superclass.
Interestingly, when I was preparing this blog post I compiled the code above as a "Command Line Tool" type of project, not an iOS app. The error was different, dare I say better, more telling of the root cause: "Instance variable '_prop' is private". So it's a matter of the visibility of the ivar, so perhaps a better fix would be:
@interface A : NSObject {
@protected
NSUInteger _prop;
}
@property (nonatomic, readonly) NSUInteger prop;
@end
Kind of interesting though that properties are synthesized by default as private ivars. Learned something new.
Friday, October 26, 2012
Dealing with exit code 5 when running Ghostscript under MAMP
I had been using Ghostscript for a project, calling it from a PHP script running in a MAMP environment on Snow Leopard. Several months later, on a different computer, running a newer version of MAMP (2.1.1), on Lion, I kept getting some weird errors.
When GS was called through passthru, like so:
passthru('gs -version', $retval);
I wouldn't get the expected output, indicating the version of GS, instead the command would be completely silent and $retval would be set to 5, indicating an error (on success it should have been 0).
Running the same command in a regular terminal window would produce the expected result:
$ gs -v
GPL Ghostscript 9.06 (2012-08-08)
Copyright (C) 2012 Artifex Software, Inc. All rights reserved.
My intuition told me that it may be a problem of the environment used by PHP interpreter running under MAMP. Taking a look it shows this:
Next step was to reproduce the same environment on the terminal, and surely the result confirmed my guess:
$ ./gs -version
dyld: Symbol not found: _iconv
Referenced from: /usr/lib/libcups.2.dylib
Expected in: /Applications/MAMP/Library/lib/libiconv.2.dylib
in /usr/lib/libcups.2.dylib
Trace/BPT trap: 5
Later I discovered that the same error also shows up in the Apache error log under MAMP, albeit without explicitly showing the error 5.
The problem seems to be caused by the dylibs under /Applications/MAMP/Library/lib/. MAMP seems to be prompted to look there by the DYLD_LIBRARY_PATH environment variable that comes with the default config (set by /Applications/MAMP/Library/bin/envvars).
The problem seems to be caused by the dylibs under /Applications/MAMP/Library/lib/. MAMP seems to be prompted to look there by the DYLD_LIBRARY_PATH environment variable that comes with the default config (set by /Applications/MAMP/Library/bin/envvars).
It seems that the dylibs used by MAMP are different than those used by a default terminal session. A regular shell session, ran from terminal, loads dylibs from /usr/lib. If I do this:
$ nm /usr/lib/libiconv.2.dylib | grep _iconv
00000000000f1af0 S ___iconv_2VersionNumber
00000000000f1b90 S ___iconv_2VersionString
000000000000a1e1 T _iconv
000000000000a5a0 T _iconv_canonicalize
0000000000013164 T _iconv_close
0000000000013171 T _iconv_open
000000000000a72c T _iconvctl
000000000000a20f T _iconvlist
it shows the _iconv symbol present in that version of the lib.
However, if I look in the version used by MAMP, it's missing:
$ nm /Applications/MAMP/Library/lib/libiconv.2.dylib | grep _iconv
0000000000029c50 T _iconv_canonicalize
Instead those symbols seem to have an extra "lib" prefix:
$ nm /Applications/MAMP/Library/lib/libiconv.2.dylib | grep iconv
000000000010d748 D __libiconv_version
0000000000029c50 T _iconv_canonicalize
0000000000028a90 T _libiconv
0000000000028b40 T _libiconv_close
00000000000280c0 T _libiconv_open
0000000000028b80 T _libiconv_open_into
000000000002a7c0 t _libiconv_relocate
000000000002a310 T _libiconv_set_relocation_prefix
0000000000029500 T _libiconvctl
0000000000029870 T _libiconvlist
It seems that MAMP loads libcups.2.dylib from /usr/lib/ and its libiconv.2.dylib dependency from /Applications/MAMP/Library/lib. That's, I believe, the root of the problem. Those 2 libraries have incompatible versions. That's confirmed by:
$ otool -L /usr/lib/libcups.2.dylib/usr/lib/libcups.2.dylib:
/usr/lib/libcups.2.dylib (compatibility version 2.0.0, current version 2.9.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 159.1.0)
/System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos (compatibility version 5.0.0, current version 6.0.0)
/System/Library/Frameworks/GSS.framework/Versions/A/GSS (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 635.21.0)
/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration (compatibility version 1.0.0, current version 395.11.0)
/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 46.1.0)
/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 55148.1.0)
/usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.5)
and:
$ otool -L /Applications/MAMP/Library/lib/libiconv.2.dylib
/Applications/MAMP/Library/lib/libiconv.2.dylib:
/Applications/MAMP/Library/lib/libiconv.2.dylib (compatibility version 8.0.0, current version 8.1.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 125.2.11)
A workaround may be as easy as easy as re-building libcups.2.dylib from sources, and configure it to pick the libs from /Applications/MAMP/Library/lib/.
$ cd cups-1.6.1
$ ./configure CC=clang CXX=clang++ LDFLAGS=-L/Applications/MAMP/Library/lib
$ make
$ cp ./cups/libcups.2.dylib /Applications/MAMP/Library/lib/
Seems to fix the problem with my script, so I can move on with my work. The question is how sane this rebuilt libcups.2.dylib is.
Doing
$ otool -L ./cups/libcups.2.dylib
./cups/libcups.2.dylib:
/usr/lib/libcups.2.dylib (compatibility version 2.0.0, current version 2.10.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 159.1.0)
/System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos (compatibility version 5.0.0, current version 6.0.0)
/System/Library/Frameworks/GSS.framework/Versions/A/GSS (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 635.21.0)
/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration (compatibility version 1.0.0, current version 395.11.0)
/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 46.1.0)
/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 55148.1.0)
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.5)
shows the dependency on /usr/lib/libiconv.2.dylib gone. A bit surprising.
A quick sanity check done like this:
$ make check
has most of the tests pass, except internationalization API tests, which is not that surprising, since I suppose libiconv.2.dylib would play an important role there.
All in all, that's how I was able to get around this problem with MAMP. I posted this issue on their forum, but almost 24 hr later it hasn't even cleared the moderator, let alone gotten a better solution.
Saturday, October 06, 2012
Caveats of calling subviews in a UIScrollView, and how to customize the scrollbars.
In a recent project I had a scroll view defined in a storyboard, together with 2 subviews. I was looking for a way to access them in the layoutSubviews method of the scroll view.
The first approach I took was to create 2 outlets in the scroll view, pointing to the to the said subviews. Even though I had it working fine, I had to twist Xcode's arm in doing that, since the normal drag-to-create-outlet maneouvre in the Assistant editor seems to work only when the target of the drag is the code pane of a view controller, not a view.
So, as an alternative, I figured, hey, we have this subviews property available, why not fast-iterate through it and call it a day. When testing that approach weird things started to happen. A little debugging showed this:
What ? I thought I had 2 subviews - this shows 4. Looking at the frames, autoresize mask and the fact that they ignore touches it's a strong indication that those 2 extra subviews may be the scrollbars - LOL.
Inspecting the UIScrollView confirms that. See the _horizontalScrollIndicator and _verticalScrollIndicator ivars:
This has one clear implication - if you're nuts enough you can customize the appearance of the scrollbars in a UIScrollView. They seem to be simple UIImageViews. However rummaging through UIKit internals like this is not condoned by Apple, since future updates may break your assumptions.
Incidentally, another approach I could have taken to access my own subviews would have been to tag them in the storyboard and use the viewWithTag method to get to them.
The first approach I took was to create 2 outlets in the scroll view, pointing to the to the said subviews. Even though I had it working fine, I had to twist Xcode's arm in doing that, since the normal drag-to-create-outlet maneouvre in the Assistant editor seems to work only when the target of the drag is the code pane of a view controller, not a view.
So, as an alternative, I figured, hey, we have this subviews property available, why not fast-iterate through it and call it a day. When testing that approach weird things started to happen. A little debugging showed this:
(lldb) po [self subviews]
(id) $1 = 0x06b805b0 <__NSArrayM 0x6b805b0>(
<SomePageView: 0x68968b0; frame = (0 0; 320 452.507); transform = [0.450704, 0, 0, 0.450704, 0, 0]; opaque = NO; autoresize = W+H; layer = <CALayer: 0x68965f0>>,
<AnotherPageView: 0x6896e40; frame = (0 0; 320 460); autoresize = W+H; layer = <CATiledLayer: 0x6896e90>>,
<UIImageView: 0x6897330; frame = (313 453; 7 7); alpha = 0; opaque = NO; autoresize = LM; userInteractionEnabled = NO; layer = <CALayer: 0x68973a0>>,
<UIImageView: 0x6897290; frame = (313 453; 7 7); alpha = 0; opaque = NO; autoresize = TM; userInteractionEnabled = NO; layer = <CALayer: 0x6897300>>
)
What ? I thought I had 2 subviews - this shows 4. Looking at the frames, autoresize mask and the fact that they ignore touches it's a strong indication that those 2 extra subviews may be the scrollbars - LOL.
Inspecting the UIScrollView confirms that. See the _horizontalScrollIndicator and _verticalScrollIndicator ivars:
This has one clear implication - if you're nuts enough you can customize the appearance of the scrollbars in a UIScrollView. They seem to be simple UIImageViews. However rummaging through UIKit internals like this is not condoned by Apple, since future updates may break your assumptions.
Incidentally, another approach I could have taken to access my own subviews would have been to tag them in the storyboard and use the viewWithTag method to get to them.
Sunday, September 09, 2012
How to read multiple lines from the standard input stream in Java
I was facing this problem, of reading lines of text from the console input, as I was under some intense time-pressure, while I was taking an online test. I was convinced that it must be something simple like:
Not such luck. This seemingly simple task in Java ain't that simple. Googling for this revealed (in multiple places) something along these lines:
which seems straightforward, until you try running this sequence repeatedly, perhaps in a loop.
If you feed this a multi-line input, pasted in the console, like so:
one
two
three
then you'd notice that the first gets read fine, then the subsequent reads block, without catching the lines:
two
Bad times. There's a fatal flaw in the code above, cited on so many sites, from one of which I copied it too. The readers hooked up to System.in don't get closed, and if that sequence of code is run in a loop we'd end up with multiple readers for the same single standard input stream. Now one could argue that an exception should be thrown in such case, where multiple readers are trying to slurp data from the same source. Alas it doesn't, and instead the behaviour is truly puzzling.
My faulty code had this function called repeatedly:
The fix consisted in doing this instead:
The static variable gets initialized only once, and now calling readLine() repeatedly works fine.
Alternatively in Java 1.6 one could also use:
with the caveat that this approach using console() doesn't work by default when running in Eclipse.
String line = System.in.readln();
Not such luck. This seemingly simple task in Java ain't that simple. Googling for this revealed (in multiple places) something along these lines:
import java.io;
try {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
return in.readLine();
} catch (IOException e) {
System.err.println(e);
}
which seems straightforward, until you try running this sequence repeatedly, perhaps in a loop.
If you feed this a multi-line input, pasted in the console, like so:
one
two
three
then you'd notice that the first gets read fine, then the subsequent reads block, without catching the lines:
two
three
My faulty code had this function called repeatedly:
public static String readline() {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
return in.readLine();
} catch (IOException e) {
return null;
}
}
The fix consisted in doing this instead:
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
public static String readline() {
try {
return in.readLine();
} catch (IOException e) {
return null;
}
}
The static variable gets initialized only once, and now calling readLine() repeatedly works fine.
Alternatively in Java 1.6 one could also use:
public static String readline() {
return System.console().readLine();
}
with the caveat that this approach using console() doesn't work by default when running in Eclipse.
Saturday, August 11, 2012
How to deal with inconsistent display of characters with diacritics on the web
A friend asked me to build a web site, targeted to Romanian visitors. Romanian language has some characters with diacritics, as documented in Wikipedia:
Here I have 5 paragraphs, showing the same text, each with a different font. First 2 are fonts downloaded on demand from Google ('Arvo' and 'Noticia Text'). The following 3 are stock fonts ('serif', 'sans-serif' and 'Verdana'). You can see that the stock fonts are failing badly, as marked with red. Those characters are either over or under-sized, relative to the others (they should have the same x-hight). The first 2 fonts aren't much better, since they fail in other contexts, depending on the size of the font chosen. So this yields the first insight - changing the font size may smooth out such issues. To check this I focus on the paragraph in the blue box and I tinker with the 'font-size' CSS selector. Seems to work:
The 4th paragraph looks better now. Applying the same trick in another context fails miserably:
That's using the 'serif' font, and on Mozilla it would fail regardless of the chosen size (at least on Windows XP). Incidentally, the same combination works perfectly on Safari on OS X, at any size:
Going back to Mozilla, if I change the 'font-family' to 'Arvo' it works fine (while that same combination would break in Safari):
To make matter worse, the initial set-up that would fail on Mozilla on Windows XP works fine on Mozilla on OS X:
Yet the same change in font-family that made it work on Mozilla on Windows XP brakes it on Mozilla on OS X (you can't make this up):
To get this right you need to test different combinations of 'font-family', 'font-size', browser and OS. I guess the biggest surprise for me was that for the 2nd paragraph, even if I used 'Noticia Text', included though @font-face from Google web fonts, which contains all glyphs with diacritics that I was looking for, it still failed to display properly on Windows. Doesn't that defeat the whole purpose of @font-face ?
So there you have it, another type of cross-browser inconsistencies that we must be aware of, together with the usual workarounds involving serving browser-specific CSS, consolidated in included files that can be applied to the whole site.
Romanian uses a breve on the letter a (ă) to indicate the sound schwa /ə/, as well as a circumflex over the letters a (â) and i (î) for the sound /ɨ/. Romanian also writes a comma below the letters s (ș) and t (ț) to represent the sounds /ʃ/ and /t͡s/, respectively.So I thought I'd use a UTF-8 encoding for the HTML, together with some fonts that support those characters and this should be easy enough. To my surprise the results were pretty bad:
Here I have 5 paragraphs, showing the same text, each with a different font. First 2 are fonts downloaded on demand from Google ('Arvo' and 'Noticia Text'). The following 3 are stock fonts ('serif', 'sans-serif' and 'Verdana'). You can see that the stock fonts are failing badly, as marked with red. Those characters are either over or under-sized, relative to the others (they should have the same x-hight). The first 2 fonts aren't much better, since they fail in other contexts, depending on the size of the font chosen. So this yields the first insight - changing the font size may smooth out such issues. To check this I focus on the paragraph in the blue box and I tinker with the 'font-size' CSS selector. Seems to work:
The 4th paragraph looks better now. Applying the same trick in another context fails miserably:
That's using the 'serif' font, and on Mozilla it would fail regardless of the chosen size (at least on Windows XP). Incidentally, the same combination works perfectly on Safari on OS X, at any size:
Going back to Mozilla, if I change the 'font-family' to 'Arvo' it works fine (while that same combination would break in Safari):
To make matter worse, the initial set-up that would fail on Mozilla on Windows XP works fine on Mozilla on OS X:
Yet the same change in font-family that made it work on Mozilla on Windows XP brakes it on Mozilla on OS X (you can't make this up):
To get this right you need to test different combinations of 'font-family', 'font-size', browser and OS. I guess the biggest surprise for me was that for the 2nd paragraph, even if I used 'Noticia Text', included though @font-face from Google web fonts, which contains all glyphs with diacritics that I was looking for, it still failed to display properly on Windows. Doesn't that defeat the whole purpose of @font-face ?
So there you have it, another type of cross-browser inconsistencies that we must be aware of, together with the usual workarounds involving serving browser-specific CSS, consolidated in included files that can be applied to the whole site.
Saturday, August 04, 2012
Spare your users the high cost of data roaming
You may have heard reports from the media, regarding the high cost of roaming charges that some people incurred. It's really no surprise, as some network carriers are practicing predatory pricing every time you leave the borders of their home country.
Although the end users are ultimately responsible for those charges, we, as developers, should strive to warn them when they're about to do some data transfers while using the apps we developed, if they're likely to be roaming at the time. That would help people that haven't turned off data roaming, through the settings provided by iOS.
The problem is that AFAIK there's no API exposed by iOS to determine if the user is roaming. So we'll need to get creative. First off, we have the SystemConfiguration.framework, which allows us to determine whether a Wi-Fi or cellular connection is in use. If it's the former, we have a non-issue. There won't be any roaming charges for Wi-Fi connections. If it's the latter we need to dig deeper. We could use the CoreLocation.framework to determine the current location of the device.
Then we could use NSLocale and its ability to determine the county code of the current locale (likely established when the device is first set up).
If the country of the locale differs from the country of the current location, there's a high probability that the user is roaming, so we could warn them about potential charges before any data transfer is initiated by the app.
This heuristic is not fool-proof, but it's better than nothing, and it can go a long way towards preventing some unwanted charges. An alternative is to track changes in the country reported by the core location framework and use that as a hint of possible roaming. A possible complication may be caused by the user not authorizing the app for location lookups. There's a way around that, which I'll explore in a future blog post.
The best approach would be to have an API to tell precisely when a user is roaming, and better yet, the data rates for that particular destination, but that will be the topic for another blog post.
Although the end users are ultimately responsible for those charges, we, as developers, should strive to warn them when they're about to do some data transfers while using the apps we developed, if they're likely to be roaming at the time. That would help people that haven't turned off data roaming, through the settings provided by iOS.
Then we could use NSLocale and its ability to determine the county code of the current locale (likely established when the device is first set up).
If the country of the locale differs from the country of the current location, there's a high probability that the user is roaming, so we could warn them about potential charges before any data transfer is initiated by the app.
This heuristic is not fool-proof, but it's better than nothing, and it can go a long way towards preventing some unwanted charges. An alternative is to track changes in the country reported by the core location framework and use that as a hint of possible roaming. A possible complication may be caused by the user not authorizing the app for location lookups. There's a way around that, which I'll explore in a future blog post.
The best approach would be to have an API to tell precisely when a user is roaming, and better yet, the data rates for that particular destination, but that will be the topic for another blog post.
Saturday, July 28, 2012
Notify your users about updates to your iOS apps
Most apps undergo updates, to fix problems, or to add new features. It's a fact of life. As is the fact that most updates go unnoticed by the end user. That's because by default Apple simply shows a badge on the icon of the "App Store" app, like so:
If you have a lot of apps installed, which are likely to produce lots of updates, I think most people would develop selective blindness towards those badge notifications, and would postpone the updates.
Another way to notify the users about an update available for a given app is to send a more vocal push notification (text alert, maybe with sound), assuming that the user agreed to receive such notifications. That may however be perceived as too pushy (or desperate), and that may be why people developed a more elegant way of notifying about updates, in a more appropriate context, which increases the chance of people acting on them. That's by simply showing the notifications when people are launching the old app, like so:
That makes sense, because now the end users are in a context where they demonstrate engagement with the app (they just started it) and the notifications have less chance to be perceived as off-putting. So this is a design idiom that makes sense to be followed.
In some instances the notifications must go further in providing more details about the upcoming update, and possibly assist the user with the upgrade. One such scenario is, for example, if the user moves an app that was initially a stand-alone app, with its own icon on the home screen, to the Newsstand. In that case the icon on the home screen would disappear and a cover corresponding to that app would appear in the Newsstand, like so:
That move has the potential to confuse people, and a notice shown by the old app before the upgrade could clarify the new location.
Another move scenario is moving an app to another developers' account. This tends to happen when a bespoke app is first developed as an experiment, with few expectations, and it's published under the developer's account. If the app is a success, the contracting clients often want to have the app moved to their own account. This is challenging because from Apple's perspective the app in the new account becomes a brand new app, completely independent from the old one. The upgrade prompt should set clear expectations about the old app being discontinued, with all future development and support slated to go in the new one.
If the app has in-app purchasing content, care must be taken to migrate purchases made in the old app to the new one. The usual purchase restoration mechanism provided by Apple is of no use here, because Apple has no notion about the 2 apps being related. So you'll need to roll out your own purchase migration mechanism. For example, when the old app is launched, you could make a snapshot of the past purchases, upload them to a server, and produce a redeemable code that could be used in the new app to restore the purchases. If you already have another way of identifying the users, perhaps through some user account/login scheme, you can use that instead to assist the migration. In iOS 5 and later this is facilitated by the newly introduced Account framework, that allows tying separate apps to the same users through their credentials on social networks like Twitter.
Another consideration would be the transfer of the name of the app. Since there can't be 2 apps with the same name, you'll need to allocate a temporary new name, then delete or rename the old app, to free up the old name, and later apply it to the new app.
If you have a lot of apps installed, which are likely to produce lots of updates, I think most people would develop selective blindness towards those badge notifications, and would postpone the updates.
Another way to notify the users about an update available for a given app is to send a more vocal push notification (text alert, maybe with sound), assuming that the user agreed to receive such notifications. That may however be perceived as too pushy (or desperate), and that may be why people developed a more elegant way of notifying about updates, in a more appropriate context, which increases the chance of people acting on them. That's by simply showing the notifications when people are launching the old app, like so:
That makes sense, because now the end users are in a context where they demonstrate engagement with the app (they just started it) and the notifications have less chance to be perceived as off-putting. So this is a design idiom that makes sense to be followed.
In some instances the notifications must go further in providing more details about the upcoming update, and possibly assist the user with the upgrade. One such scenario is, for example, if the user moves an app that was initially a stand-alone app, with its own icon on the home screen, to the Newsstand. In that case the icon on the home screen would disappear and a cover corresponding to that app would appear in the Newsstand, like so:
That move has the potential to confuse people, and a notice shown by the old app before the upgrade could clarify the new location.
Another move scenario is moving an app to another developers' account. This tends to happen when a bespoke app is first developed as an experiment, with few expectations, and it's published under the developer's account. If the app is a success, the contracting clients often want to have the app moved to their own account. This is challenging because from Apple's perspective the app in the new account becomes a brand new app, completely independent from the old one. The upgrade prompt should set clear expectations about the old app being discontinued, with all future development and support slated to go in the new one.
If the app has in-app purchasing content, care must be taken to migrate purchases made in the old app to the new one. The usual purchase restoration mechanism provided by Apple is of no use here, because Apple has no notion about the 2 apps being related. So you'll need to roll out your own purchase migration mechanism. For example, when the old app is launched, you could make a snapshot of the past purchases, upload them to a server, and produce a redeemable code that could be used in the new app to restore the purchases. If you already have another way of identifying the users, perhaps through some user account/login scheme, you can use that instead to assist the migration. In iOS 5 and later this is facilitated by the newly introduced Account framework, that allows tying separate apps to the same users through their credentials on social networks like Twitter.
Another consideration would be the transfer of the name of the app. Since there can't be 2 apps with the same name, you'll need to allocate a temporary new name, then delete or rename the old app, to free up the old name, and later apply it to the new app.
Subscribe to:
Posts (Atom)