SmallTech's Blog http://www.smallte.ch iPhone and iPad app development en Wed, 8 Apr 2020 00:30:04 +0000 Releasing Smart List http://smallte.ch/blog-read_en_5693889524531200.html Wed, 8 Apr 2020 00:30:04 +0000 We are happy to announce the release of Smart List HD.: the fastest, simplest and yet most powerful shopping list.

Create a beautiful shopping list in a few taps !

Use prepared lists so you can shop again for the same items with minimal effort every week. Get access to lots of predefined items for food or beverages with beautiful icons for each category.

Plan your meals before you shop at your store so you get only exactly what you need for your delicious recipes to make your friends and family happy while keeping your budget.

Of course you can add or edit your own items and customize everything to fit your needs.

This app will help you organize and maintain your ideal diet, whether you are just lovers of good food who like to support local products or buy only organic whole foods.

It can be adapted to your needs and your culture, for example if you are vegan or vegetarian or if you have to be specially careful because of an allergy or lactose or gluten intolerance.

Get and eat exactly what you want in order to live a long and healthy life ! You will never need another shopping list.

]]>
Removing Best Restaurants from sale http://smallte.ch/blog-read_en_5685592721457152.html Tue, 1 Oct 2019 00:26:23 +0000 Unfortunately the huge price changes Google Places have applied this year to their API aren't sustainable. We tried a lot to reduce requests and so on but it seems Google Places API is now designed to kill all competing apps.

We will be removing Best Restaurants from both Google Play and iOS AppStore. We hope to be back when we have found a solution. Thanks for your understanding.

]]>
You should NOT require iOS 7 for an existing app yet ! http://smallte.ch/blog-read_en_5720827718795264.html Sat, 7 Dec 2013 20:24:47 +0000 As I write these lines, iOS7's share is above 70%. And I feel about 30% more stupid because I made a sizeable mistake.

Supporting both iOS 6 and iOS 7 is a pain in the ass. I won't go into detail here on why since if you care about it you already knew it.

With iOS 7 soon on most Apple devices, it sounds like the time has come to dump iOS 6 for any new project. But what about old projects ?

I recently read – ok, misread – in news outlet that you could now publish an iOS7 version of your app with no regret because iOS 5 & 6 devices would still get the old version of your apps. This sounded like great news because previously the inability to serve old users had forced me to keep a lot of legacy code.

So I published a last iOS 6 update for all my apps, updating third party libraries etc, to make sure they'd remain functional for at least a year. I considered that after this iOS 5 & 6 wouldn't matter that much. After all at worst iOS 5/6 users would get this rather recent version.

After this got approved, I happily I published and update to my famed Battery app requiring iOS7. I was hoping that iOS7 users would get the new update and that people on older devices would just get the old version.

I even tested this with my account on my old 3GS. I can go to the AppStore and it gives me the previous version. Yay? NOT.

That is sadly not the reality. Reading carefully Apple News from September 18, 2013 you can notice it clearly states that this is about re-download for existing users. Not new users.

So I reset that 3GS and tested from scratch. I searched for my app and I was relieved. It was there! But then I created a new account to reproduce the situation of a real new user. Then I faced an ugly alert box saying the app required iOS 7. Ouch.

The result of this mistake? Since iOS 7 is only on 70% of the devices my downloads for this app tanked approximately 30% immediately.

So summarizing again:

If you require iOS7 for your existing apps:

  • you will NOT GET ANY NEW CUSTOMER using iOS 5 / 6
  • you WILL LOSE 30% of your downloads
  • this WILL KILL YOUR RANKING
  • it may RUIN CHRISTMAS

Thanks go to @AlexandreTorres who helped confirm this.

As I write this, I am canceling all reviews that were waiting for my other apps and I will probably resubmit a version that runs on iOS 6 for this app.

Most of this post was about me being stupid. However I do I think Apple should allow users to get old apps even with new accounts.

Today, if you buy a used iPhone 3GS you will not be able to download most older apps. Unless you get some old account with it that is.

]]>
Android fragmentation - Five thousand variations http://smallte.ch/blog-read_en_81001.html Tue, 3 Jul 2012 19:17:50 +0000 Everybody talks about Android fragmentation. I have felt it. Inside me. It hurts.

For many apps, fragmentation isn't such a big issue. If you're building an app for a summer festival, all you need to handle is different screen sizes and it will work everywhere. Well, not really but you'll get there eventually.

There are however other types of apps:

  • Games
  • Apps that interact with hardware

The main issue with games is performance. Good games are fine-tuned for the hardware and it's hard to fine-tune for thousands of different devices. I haven't ported any games to Android so I won't comment. Enough others have done it.

I will also let aside the very upsetting fact that manufacturers like to remove languages or fonts. My small Sony Xperia Mini Pro has all languages and fonts, including Hindi, proving that the now old Android 2.1 provided everything for the whole world and that it ran on a cheap phone. Manufacturers and phone operators are the ones to screw everything. The result is I own a Xoom that is only in German for example, and if you're wondering, no I live in the french part of Switzerland.

Interacting with hardware

I have two very successful apps on Android. One is a Battery monitor which runs calibration tests, another is a "simple" Flashlight. Together they're reached over 10 million downloads so I think it makes my numbers reliable.

Both apps share in common that they have different behaviours depending on hardware.

Flashlight LED HD

It's a simple flashlight. SIMPLE. Really. That's what I thought when I asked my colleague to port it to Android.

In theory on Android 2.2+ you can turn on the LED with like three lines of code. The only issue here is that is basically never fucking works.

While some constructors like Sony and HTC usually cause very few issues, others like the dominant Samsung and the miserable Motorola have so many quirks and bugs that this app now has 15 different techniques and workarounds to turn on a freaking LED. When a manufacturer finally updates its device then we suddenly need to use a different technique depending on the system version. And this has to be fixed quick before users will get very angry when it stops working.

Most of the e-mail I get is from people saying their LED doesn't turn on on one model or another or that it does only using the test tool (meaning we've failed to detect which new variation it is), or that it blinks, or turns off after 5 seconds, or locks up the whole system... I keep telling every day to all ZTE device owners that the only way they can turn on the LED from a third party app is to root their phones, making it insecure. Unbelievable, right? But true.

Because of the milions of downloads involved, this mess is suprisingly worth maintaining. Thank you Admob. But when compared to my iOS port, which I only update every three months, this app really doesn't make Android development shine. There is not one single week where my colleague doesn't have to add another line to the code that says something like "if Model == X" and "API level = Y"...

A picture is worth a thousands words, so here's a picture of words. That's a screenshot of some of our ticket's titles.

Flashlight Tasks.

That's what we do on a nearly daily basis: handling inconsistencies, bugs and the like. All that hoping that a new users launching this app will just hear a 'click' and see light.

Battery HD

In order to provide estimates, this app runs calibration tests and saves the result on a server. New users can automatically get an average of tests run by people with the same device and the same system. This way it works fine for most users and running calibration tests (which may take several hours) is optional. As of today over 368,919 tests have been saved on our server, totalling over 212,312 hours of tests run by our users. Yes that's the equivalent of running them for about 24 years straight on one device. I am proud to say this works suprisingly well for the vast majority of users. The number of users makes up for the variety of devices.

The tests are rather simple so they rarely cause compatibilities issues. That is not counting, of course, the only hardware interaction it has: locking / unlocking the screen after tests. This is particularly inconsistent among manufacturers and system versions.

In order to provide the right crowd-sourced average estimate we create a unique key to identify the device's hardware+system combination. The values returned by Android devices when it comes to manufacturer, device name and motherboard name are just hilarious.

Can you imagine one iPhone's model being returned as "iPhone", another as "iphone" , another as "IPhone" or "iPhoneB" because it's sold in Brazil... only to switch to "i2923_39" after an official update ?

Well, that's approximately what our friends at Samsungs and their siblings do. Nearly every time they bring a major update to the system they change something. Sometimes they just change capitalization, other times they just completely change the motherboard name (because it previously contained the phone's model... oops).

Then you need to factor in the wide amount of custom ROMs available and installed everywhere by many users trying to compensate for the lack of official updates.

ROMs with wrong hardware identifiers are particularly harmful since they make all detection fail, which in turns makes us use the wrong workaround for their particular system bug. What do you do with a device supposedly manufactured by "Samsung" but named "HTC Desire HD" ?

Yes, I know. You trash it.

So how many combinations do we have now for

Manufacturer + Model + API Level ?

According to the server stats from Battery HD, the total number of variations since the last app update is now 5208. I didn't believe it either. Check for yourself in this terrifying CSV.

To be fair if we convert all to lowercase we only get 5008 CSV.

Yes, that is five fucking thousand variations.

One could argue that even if there are so many variations, it's very often the same hardware behind. We could easily account for this if manufacturers used the os.android.Build.BOARD field correctly. By correctly I mean that the name of the hardware wouldn't change with every major update or just not be plain empty or contain the same value as another field, such as the device name.

And this mess comes from the same people who reject an app because the BSD license in the about box isn't translated in Serbian. Yes, I am looking at you Samsung Apps.

]]>
Android permissions or the Mobclix Catastrophy http://smallte.ch/blog-read_en_51001.html Mon, 27 Feb 2012 22:25:46 +0000 An innocent fallback network

As regular readers may know my LED Flashlight HD app has had quite a tremendous success on the Android market. On this day it passed the 5 million download mark. Champagne. I am a proud leader in the glorious flashlight market.

Because Admob could only fill 98% of my ad requests, I decided to add mobclix as a fallback network using Adwhirl (via a custom command). My colleague Vadim spent some time integrating the Mobclix SDK, following their instructions, all that to fill the missing 2% requests.

I peacefully released an app update which included amongst other compatibility changes the addition of this new network.

So, as one does in this type of situations I went to the plunge pool with my girlfriend – yes, I am on 'holidays' in Portugal as I write this. When I came back to my machine... this Katastrophuck had happened:

I had about three pages of this. Despite not being very attentive to the German lessons in school, I quickly understood this was a serious problem.

Mobclix's SDK requires adding a permission whose name looks rather innocent: READ_PHONE_STATE. As innocent as it may seem to a programmer blindly following a tutorial, the description shown to the user upgrading the app is this:

READ PHONE STATE AND IDENTITY Allows the application to access the phone features of the device. An application with this permission can determine the phone number and serial number of this phone, whether a call is active, the number that call is connected to and the like.

WTF !

There is a disproportionate amount of german users reacting to this permission change. I have German users yet they are not the biggest market for this app. I assume that the number of scams related to SMS+ in Germany is just so huge that they are all terrified of being scammed. I must have some hidden german ancestry myself because when I read this description I thought I would never download an upgrade with such a warning.

So I quickly released an other update, disabling Mobclix calls and permissions and apologized to everyone who had e-mailed me in anger.

So who is to blame?

  • First of all, of course my own sorry ass. I should have read what this permission allowed. My only excuse is that I don't do my Android dev myself because... how do I put it... I don't really admire Android. I still should have reviewed this more carefully. On the other hand, there was a plunge pool nearby.

  • Mobclix: has a blog post explaining the need for a unique ID and the permissions.

    I understand that Mobclix or its partner networks may not use this permission to its full extent and not be as nasty as they seem but whatever the reasons are developer's don't want to explain this shit over and over again in their app descriptions and to every user who e-mails angrily. If this is an issue with users using an OS older than 2.3 then don't serve ads to them if this permission isn't set.

    The obvious solution is: do NOT use mobclix if you respect your user's privacy. If the people at Mobclix aim to chip some market share away from Admob they should definitely offer an option to use their SDK without these permissions, even if this means limiting the amount of partner networks. At this point in time they don't. Without this permission calls to the mobclix SDK fail and crash. I for one, chose my user's privacy and removed them. In the long run I prefer happy users, even if that means losing some revenue.

    Short said: if Google (Admob) can be the leader without requiring this permission, others networks could survive without it too.

    One should note that I still happily use Mobclix as my fallback network on iOS since this permission problem doesn't exist. Or at least it isn't shown in the face of the users big enough to deter them from downloading the app.

  • Google: There should definitely more granularity in permissions. If all you want is have access to some network information you shouldn't have to require a permission that allows accessing the user's phone number. When Google decided not to have a review process they put the users in a state of fragility. Reducing the span of permissions groups would help reassure them.

]]>
Optimizing the world. Or why I believe Steve Jobs was Batman. http://smallte.ch/blog-read_en_44001.html Wed, 12 Oct 2011 01:50:28 +0000 As a programmer I learned the basics of optimization: if you're going to need the same data a million times, prepare it once then reuse it. Or in other words if you do things once instead of a million times, surprisingly it will happen faster overall.

Why is it that geeks who seem to understand this concept perfectly when it comes to machine code, just do not get it when it comes to interaction with other human beings ?

I have always been a ruthless geek. Mind you, for a full year I ran a Linux distribution as my main desktop system (Mandrake, then Debian). That was around 2002 and I probably admired Linus more than Steve Jobs back then. It seemed natural to me to know which type of RAM each PC used and I could never have imagined that ten years later I just wouldn't remember nor care much what GPU is in my laptop.

Then after struggling for a year editing text files here and there to get back my X display and after having succesfully compiled my 802.11b modules as explained in one of the 236 contradicting tutorials around, I suddenly had my share of the constant management that was required by a Linux desktop. I realized I was getting a bit old to waste my only life fiddling with my system. Maybe this was preventing me from learning stuff that is more important.

When my Laptop died I just didn't have the motivation anymore to go through the endless search for the perfect system and my next PC naturally came with Windows® installed. Once I changed the desktop background it was still ugly but useable. I could write code. I didn't like Windows, but it worked fine. I kept a Debian/Ubuntu partition around for a while hoping stuff would get better. Part of it did.

So about ten years have passed. However if you do a google search today there are still zillions of recent tutorials that explain how to do stuff on some Linux distribution or another. Most of it is about getting hardware or software to work. The result of these procedures is not some amazing achievement. You're not installing Linux on your alien wristwatch, you just get to use software or hardware that you couldn't otherwise on this particular computer or distribution.

One thing strikes me: out there thousands of people are following tutorials to solve the exact same problems. Just like I did years ago. Instead of having someone responsible of fixing the root of the issue (also known as rewrite), a seemingly infinite number of people keep on patching stuff hoping that one day everything would be fine. But it just never is. If it was fixed, there just wouldn't be so many tutorials. It may have the appearance of knowledge, but if it is outdated a month later, then it isn't worth much more than your groceries shopping-list.

Linux users are stuck in a for-loop duplicating all the same processing over and over again because there was nobody to simplify and optimize the process in the first place. This has to be the worst optimization of ressources in recent history.

The Linux world is an extreme case. But this syndrome of useless repetition of tasks affects us in daily life even in the most basic iPhone apps.

Let's say we want to buid a weather app: a French developer would set Celsius as the default and an American would set Fahrenheit. Both would be wrong. To save himself two lines of code for appropriate defaults based on the locale the developer here creates an unnecessary effort for potentially millions of users.

Who cares? It's one checkbox to click right? It may be three taps in your app but multiply that by each interaction a user has every day and you might be one of the drops in the ocean of confusion that prevents people from learning a whole new skill. All that because they wasted their precious time doing something that they might not have needed to do at all.

If there is something good a software designer can do it is to think ahead and solve people's problems without them ever knowing. Think of yourself as of Batman, quietly saving people in the night. They will never know but the world will be better off. And no, you don't need the costume. You hopeless nerd.

Note that this is not necessarily opposed to choice. If you want to put options in your apps do it, some people will like them. Just make sure the majority will never need to use them.

I was recently reading slashdot user comments on Steve Jobs' death. It was painful and yet amusing. Most dismissed him as a "fashion designer" or just some "marketer". Blasphemy I tell you, blasphemy !

Imagine spending 20 years of your life studying Mandarin, and then comes a guy with a 99 $ device which translates your thoughts perfectly in all languages. Wouldn't you somehow hate the guy who created it AFTER your learned each one of those very complicated symbols ? And wouldn't you denigrate all the "idiots" who just use the bloody device and are suddenly your equal ?

Computer geeks usually despise Steve Jobs, not as they like to tell themselves because he supposedly took liberties away from them – by offering a set of products they don't buy – but because his life's work was to destroy the complexity they held as their pride. Jobs was one of those who wanted to allow everyone to use computers. He did so because he was more interested in listening to the music he loved than in the AAC compression algorithm, which is more or less the definition of a normal human-being. He didn't make computers easy with a snap of his fingers. A lot of people worked to make computing simpler, including Bill Gates. Jobs got more credit because he was simply better at it.

Programmers will look at iOS and be bewildered at the dissappearance (user-wise) of the general purpose file-system. The others will be thankful that this thing they never really understood went away and that they now have a thing called "photos" and another called "music". For those born afterwards, managing your files manually will be akin to using a Terminal today: a programmer's thing.

Some talk with pride of their shiny Android devices, boasting about their numerical specs, and could spend a night discussing which is the best task-killer available or the best backup solution or how well they have configured them. The others have either never heard about task-killers or just wish they didn't exist. They wish that backup didn't need to be setup: that it would be there already, appearing only when you thought you had lost your data. Batman-style.

This notable participation in the common search for simplicity – not dumbing-down – is why a lot of people feel sad that Jobs is gone. Simplicity in computing is the modern times' printing press. It increases the spread of knowledge to the masses and optimizes the life of all humanity, so great people can dedicate to great discoveries, not having to worry about their tools.

Speaking of tools, what is Richard Stallman up to these days? Still browsing the web via e-mail in a text console I guess.

]]>
Using instance cache in AppEngine instead of 'free' memcache really reduces costs http://smallte.ch/blog-read_en_34003.html Fri, 9 Sep 2011 18:48:24 +0000 In my previous post about Google's AppEngine horrible price hike I jokingly concluded that to optimize my apps in order to make appengine costs reasonable again the only solution would be to remove any useful code.

The higher the latency for your page, the more instances get spawned by AppEngine. Given the new prohibitive prices this can get very expensive even if your pages use little RAM and CPU. One of the reasons a very simple page can take 50ms instead of 6ms to return is simply the use of memcache.

AppEngine's memcache has a confusing name. It's not in your instances memory memory otherwise it couldn't be shared and consistent across all instances (which it is).

For many apps though we don't really care about consistency between instances. For example my Horoscope HD app just gets a simple cached page that only changes once a day. The same happens on the associated website. My previous implementation used AppEngine's memcache for every request. This used very little CPU and was therefore pretty cost-efficient with the previous prices.

It turns out that to greatly reduce latency, all I had to do was declare a dictionary before my class:

_instanceCache = dict()

class MainHandler(webapp.RequestHandler):

Then before trying to get the same key from memcache I'd try to get it from my own instance. It is vastly faster because this is real memcache. The app now has three levels of cache:

  • Instance dictionnary: very fast (just like real memcache)
  • AppEngine memcache: fast enough
  • AppEngine DataStore: rather slow and costly

After this change the daily cost for this particular app went back down to 0.14$ /day. Note that the price was also largely affected by the fact that I removed the Always On instances which cost me (future) money doing nothing. With Always on instances and no "instance cache" the site was bound to cost me nearly 6.– $ per day....

Sadly you can't do this type of optimization for all types of apps but for ones that merely serve cached content this can be a real life saver. Depending on the variety of content it might be wise to put a timestamp in your cache entries but in this case I can live with the idea that the instance will be killed if it ever uses too much memory (a rare occurence given the size of the data) and that the data for a given key never changes.

That doesn't change the fact that the price increase is outrageous. If I needed a lot of processing and data consistency I'd be pretty much stuck. I guess my private statistics site (which was promising to cost up to 400$ monthly) could be optimized a lot using instance variables but I am not taking the risk to do all this work again to see Google change the rules at their will. I did trash the site and all the hardwork that went into it was rendered moot by this insane new pricing.

Update: Using Google's front-end cache

Tammo Freese recently replied to my GAE Groups post suggesting that for an even more efficient cache one could set the Cache-Control header that should enable caching by Google's front end cache, generating zero actual requests (nor costs).

The following code would do the trick:

seconds_valid = 15*60 # 15 minutes

self.response.headers['Cache-Control'] = "public, max-age=%d" % seconds_valid

He added: You see the front-end cache working if your log shows "204 No Content" 
log entries with 0 CPU seconds in your log.

Update

I have just tested the above code and it works fine. A lot of content now gets cached properly and returns code 204.

]]>
Optimizing your AppEngine website for the new pricing: How to get from 422 $ per month back to free in a few lines. http://smallte.ch/blog-read_en_35001.html Thu, 1 Sep 2011 15:57:15 +0000 There has a been a lot of badmouthing about Google's AppEngine new pricing. Basically it went from being rather cheap to just incredibly expensive compared to normal hosting. Each of my instances (using ~30 mb of memory) will now cost (0.08 x 24 hours x 30 days) ~= 57.6 $ per month. They generously offer you half price until november on Python instances... so it'll be only 28 $ per month per instance for a pair of months.

If your site has a reasonable amount of traffic you'll have several instances. In two weeks the cost of my private iPhone stats website will be going from 12$ a month to about 211$. Yes, Google is generous, they're leaving me a whole two weeks to adapt. I am even allowed to work on the week-end or at night if I want. If feel humbled by such generosity and to be honest it helps me fill my agenda that was otherwise empty.

But the cheap 211 $ price is only because Python instances are half-priced until november (to compensate they say for the lack of support for concurrency in Python until 2.7 is released.) After that, unless I spend an undetermined amount of hours on my stats website to implement concurrency possibly reducing the cost, the monthly cost will double. That is $ 422.– per month instead of $ 12.–. All that to save my iPhone apps stats using a total RAM for all instances that is below 256mb.

A bargain compared to say an OVH server that would only get me 16 GB of ram for 69 € / month . There's no way such a miserable machine could handle 700k requests writing to memcache per day... Oh wait it would. I wouldn't even even need memcache. I could just fucking write to the mysql database for every request and it'd work. Then I could use the rest of the machine's power to crack the human genome, again.

Of course my first reaction was anger but after calming down I found a simple trick :-)

A simple solution through simplificative optimization

Thankfully not all is lost. Using to some good old optimization and by trashing parts that weren't really necessary, I have been able to greatly reduce the cost of my stats websiste. Previously my /log page saved data in memcache and used an insane amount of memory of up to 30 Mb per instance. That was due to the titanesque processing required to store values in memcache and saving them to disk once every 20 minutes.

My previous code was basically an adaptation of this code.

I made some adaptations for the new pricing model and now the code is much simpler than before and reaches a very low latency so it can all run on one free instance instead of 7 :

AppEngine

Oh, and fuck you Google, seriously.

PS: While the code above is amusing it seems you can reduce further the load by just serving a static file instead, which I did.

]]>
Average rating of 4.5 stars for a flashlight ? Checked. http://smallte.ch/blog-read_en_33001.html Sun, 21 Aug 2011 20:31:48 +0000 Here's a small follow-up on my post on increasing the number of reviews. As I stated before this has been a huge success, particulary for my Battery HD app. Here I'll share the results I got from my LED Flashlight app.

About the number of downloads

Note that the following numbers are a bit skewed because this app was updated at the same time as my Battery app which happened to reach the top rankings in Korea, Japan, UK and Germany at that moment. The cross-promotion links increased the number of downloads for the Flashlight app from about 2000 per day to between 3500 and 4000 per day. As a result the number of reviews below were effectively doubled by the doubling of daily downloads. Of course that alone cannot explain the quadrupling of daily reviews, nor the improved rating.

The results

I think they can speak for themselves. Figuratively that is. Technically it's just a table and if it starts speaking to you I would suggest lowering the daily intake of crack.

VersionRelease dateDays on AppStoreReviewsper dayAverage Stars
1.12August 20less than 2 days21 ~ 4.57
1.11August 419 days905.6 / day4.34
1.10June 379 days280.35 / day3.89
1.04April 1351 days761.35 / day3.52

I'd like to point out a few points about the evolution we see here:

  • Version 1.04 had some nasty bugs that caused equally nasty reviews. Release 1.10 fixed the bugs and enhanced the application speed. The results is that I got less reviews per day (since it didn't crash so much) and the overall rating improved.

  • Version 1.11 was the one in which I included my now famous (to me) about box. The number of reviews per day was multiplied by a factor of 4. While this factor was even bigger for my Battery app, I think these are pretty good results given the app we're talking about. I think the difference can easily be explained by the question Who the hell expresses enthusiasm for a Flashlight?

  • Version 1.12 fixed small issues in the color selector. Overall the results are similar to 1.11 but it's a bit too early to comment since it was approved only yesterday (which explains the review spike due to the update day).

As always the most notable change here is that the average rating is now between 4.3 and 4.5. That's something I couldn't have dreamt about before, and not only because it'd be pathetic to dream about app ratings.

]]>
Automate taking screenshots thanks to UIAutomation http://smallte.ch/blog-read_en_29001.html Wed, 10 Aug 2011 21:21:48 +0000 Taking screenshots is painful

Either by choice or simply because I failed to succeed there I decided to focus on countries other than the biggest market: the U.S.A. As a result, while never reaching the top #50 in U.S.A. some of my apps have ranked #1 in other countries including amongst others the relativiely big markets of France, UK, Germany, Spain, Japan and China. This has been a lucky move considering some ad networks (notably Admob and MobFox) tend to have higher eCPM for my apps in Europe.

To achieve this localization was an absolute requirement.

Once you use a streamlined system (such as iCanLocalize ) managing various languages isn't too hard, as long as your apps – like mine – do not hold too much text. If I need a new string, I'll just add it to my en.lproj strings file, upload it to iCanLocalize and pay only for the missing words, then in a few hours (or days) I'll download the resulting strings files for all languages.

Now the thing I hate the most with an app that has 10 languages is that if you change something visible you need to update all the screenshots. Shudder.

This includes two very painful steps:

  • A: change the language in your phone 10 times and pick every screenshot again with the Organizer and then give it a proper name by hand. You could also change the language 10 times in your simulator (slightly faster) but then you'd have to crop your shots and have to worry about gamma issues or any possible difference between simulator and device.

  • B: Upload each screenshot manually while insulting loudly the guys who designed iTunesConnect. A zip file with standardized names would be SO much faster... one can dream.

I am afraid I can't do much about B, so let's keep cursing about this. However, there's a solution to improve the speed for taking screenshots on device, making more cursing optional but still enjoyable.

Enter UIAutomation. Writing a simple script

As the name implies, UIAutomation let's you automate your apps. This is primarily aimed a running Unit Tests but who needs them, we have user reviews for that, right?

To use UIAutomation all you need to do is write a javascript file and run it together with your app in the Profiler.

Here's a simple example script, showcasing some of the stuff you can do. Here we'll take two screenshots on two different screens.



// Define the language for the current run (here, Japanese)
var forcelanguage  ='ja';

// Setup some variables for readability
var target  = UIATarget.localTarget();
var app     = target.frontMostApp();
var win      = app.mainWindow();

// Overwrite the language in NSUserDefaults
// Note that this will only take effect on next run.
// So you have to run this twice.
// Once to set the language, another to actually pick the screenshot.
app.setPreferencesValueForKey([forcelanguage], 'AppleLanguages');

// You can define your own settings of course, for example:
app.setPreferencesValueForKey("0.85", 'fakePercentageForShots');

// Store current language name for use in filenames
lang = target.frontMostApp().preferencesValueForKey("AppleLanguages")[0];

// Save screenshot for home screen
target.captureScreenWithName( lang+"_home");

// Pause for two seconds (screen capture takes a second !)
target.delay(2);

// Open settings screen by pressing a button
// (note that the button must have the xcode label 'settings' for this to work)
// you could also reference the button by its number (here: 0)
 win.buttons()['settings'].tap();

// Pause for two seconds (new screen needs some time to load and appear)
target.delay(2);

// Save screenshot for settings screen
target.captureScreenWithName( lang+"_settings");

// Pause for two seconds (screen capture takes a second !)
target.delay(2);

// Hit the back button to go back to home screen
// otherwise upon next run the app will still be on settings screen
win.toolbar().buttons()[0].tap();

Here's the downloadable autoshot.js with the code above. It has a bit more logging than the code above too.

Launching the script

Open your project In XCode, use the menu : Products -> Profile. The following screen will show up and you'll very wisely pick Automation, unless you have been speed-reading all along and you think this is about zombies.

Automation in Profiler

You can abort the first run immediately since you have to change the settings:

  • Select your .js script file and enable Run on Record.
  • Enable Continous logging at the bottom and point it to an empty folder. I named mine 'shots' and put it on my desktop.

Automation settings

Now press the Run button again and watch as your screenshots are saved:

execution log

Stuff to Remember:

  • taking screenshots just doesn't work from the simulator, so you have to run this on the real device.

  • the screenshots are saved in the format that you have defined as the default for XCode (I usually pick PNG)

  • this runs in a full blown javascript interpreter. You can create functions or whatever. I made this example all in one linear script so it could be easy to read and understand.

  • Overwriting the language in NSUserDefaults only affects the next run so for each session you have to define the language variable, run the script once and abort it (so it memorizes the language), then run it again to get the actual screenshots.

  • Don't forget that after doing this your app will NOT use the device's language anymore. I'd recommend deleting the app afterwards otherwise you might be puzzled next time you run it a week later and you totally forgot about this :-)

  • You can find how to reach each element in your app by calling this method which will dump the entire UI structure (at this particular moment)


UIATarget.localTarget().frontMostApp().logElementTree()

Looping over all languages ? Not yet.

As you might have noticed, the sad part here is you still have to run the script once in every language. I am still looking for a solution to update the language without having stop and restart the application but so far I haven't found a way. This would maket the full process 100% automated. Any suggestions ?

Update: Taking screenshots with the simulator

XCode 4.5+ fixed the issue where scripts wouldn't be able to take shots on the simulator. However, an issue remains: when taking shots from the simulator the language forced from the script isn't taken into account.

If you need to take shots in the simulator, as happened to me with the iPhone 5 because I could own one, you can just add this in your main.m:


  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];


#ifdef DEBUG
    NSString* forceLanguage = @"fr";
    [[NSUserDefaults standardUserDefaults] setObject: [NSArray arrayWithObjects:forceLanguage, nil] forKey:@"AppleLanguages"];
#endif


int retVal = UIApplicationMain(argc, argv, nil, nil);
    [pool release];
    return retVal;


Obviously the #ifdef DEBUG is a good security measure to make sure this never ends up in the final release if you forget to comment it.

]]>