-
Notifications
You must be signed in to change notification settings - Fork 418
Working With iOS
iOS apps seem to start almost instantly in comparison to Android apps.+ There is a trick to that, iOS applications have a file traditionally called Default.png that includes a 320x480 pixel image of the first screen of the application. This is an "illusion" of the application instantly coming to life and filling up with data, this rather clever but is a source trouble as the platform grows [1].
The screenshot process was a pretty clever workaround but as Apple introduced the retina display 640x960 it required
a higher resolution [email protected]
file, then it added the iPad, iPad Retina and iPhone 5 [2]
iPhone 6 & 6+ all of which required images of their own.
To make matters worse iPad apps (and iPhone 6+ apps) can be launched in landscape mode so that’s two more resolutions for the horizontal orientation iPad. Overall as of this writing (or until Apple adds more resolutions) we need 10 screenshots for a typical iOS app:
Resolution | File Name | Devices |
---|---|---|
320x480 |
|
iPhone 3gs |
640x960 |
iPhone 4x |
|
640x1136 |
iPhone 5x |
|
1024x768 |
|
Non-retina ipads in portrait mode |
768x1024 |
|
Non-retina ipads in landscape mode |
2048x1536 |
Retina ipads in portrait mode |
|
1536x2048 |
Retina ipads in landscape mode |
|
750x1334 |
iPhone 6 |
|
1242x2208 |
iPhone 6 Plus Portrait |
|
2208x1242 |
iPhone 6 Plus Landscape |
iOS developers who use this approach literally run their applications 10 times with blank data to grab these screenshots every time they change something in the first form of their application!
With Codename One this will not be feasible since the fonts and behavior might not match the device. Thus Codename One runs the application 10 times in the build server, grabs the right sized screenshots in the simulator and then builds the app!
This means the process of the iPhone splash screen is almost seamless to the developer, however like every abstraction this too leaks.
One of the first things we ran into when building one of our demos was a case where an app that wasn’t very big in terms of functionality took up 30mb!
After inspecting the app we discovered that the iPad retina PNG files were close to 5mb in size… Since we had 2 of them (landscape and portrait) this was the main problem. The iPad retina is a 2048x1536 device and with the leather theme the PNG images are almost impossible to compress because of the richness of details within that theme. This produced the huge screenshots that ballooned the application.
A very common use case is to have an application that pops up a login dialog on first run. This doesn’t work well since the server takes a picture of the login screen and the login screen will appear briefly for future loads and will never appear again.
One of the biggest obstacles is with heavyweight components, e.g. if you use a browser or maps on the first screen of the app you will see a partially loaded/distorted MapComponent and the native webkit browser obviously can’t be rendered properly by our servers.
The workaround for such issues is to have a splash screen that doesn’t include any of the above. Its OK to show it for a very brief amount of time since the screenshot process is pretty fast.
Important
|
The certificate wizard that ships as part fo the Codename One plugin automatically handles provisioning and certificates. This section is only necessary for special cases when the certificate wizard isn’t enough! For more about the certificate wizard check out the signing section of this guide. |
One of the hardest parts in developing for iOS is the total mess they made with their overly complex certificate/provisioning process. Relatively for the complexity the guys at Apple did a great job of hiding allot of the crude details but its still difficult to figure out where to start.
Start by logging in to the iOS-provisioning portal
![Log into IOS provisioning portal](img/developer-guide/login-ios-provisioning.png)
In the certificates section you can download your development and distribution certificates.
![Download development provisioning profile](img/developer-guide/download-dev-provisioning-profile.png)
![Download distribution provisioning profile](img/developer-guide/download-dist-provisioning-profile.png)
In the devices section add device ids for the development devices you want to support. Notice no more than 100 devices are supported!
![Add devices](img/developer-guide/ios-add-devices.png)
Create an application id; it should match the package identifier of your application perfectly!
![Create application id](img/developer-guide/ios-create-app-id.png)
Create a provisioning profile for development, make sure to select the right app and make sure to add the devices you want to use during debug.
![Create provisioning profile step 1](img/developer-guide/ios-create-prov-profile-1.png)
![Create provisioning profile step 2](img/developer-guide/ios-create-prov-profile-2.png)
Refresh the screen to see the profile you just created and press the download button to download your development provisioning profile.
![Create provisioning profile step 3](img/developer-guide/ios-create-prov-profile-3.png)
Create a distribution provisioning profile; it will be used when uploading to the app store. There is no need to specify devices here.
![Create distribution provisioning profile](img/developer-guide/ios-create-dist-prov-profile.png)
Download the distribution provisioning profile.
![Download distribution provisioning profile](img/developer-guide/ios-download-dist-prov-profile.png)
We can now import the cer files into the key chain tool on a Mac by double clicking the file, on Windows the process is slightly more elaborate
![Import cer files](img/developer-guide/ios-import-cer-files.png)
We can export the p12 files for the distribution and development profiles through the keychain tool
![Export p12 files](img/developer-guide/ios-export-p12.png)
In the IDE we enter the project settings, configure our provisioning profile, the password we typed when exporting and the p12 certificates. It is now possible to send the build to the server.
![IOS Project Settings](img/developer-guide/ios-project-settings.png)
Local notifications are similar to push notifications, except that they are initiated locally by the app, rather than remotely. They are useful for communicating information to the user while the app is running in the background, since they manifest themselves as pop-up notifications on supported devices.
The process for sending a notification is:
-
Create a LocalNotification object with the information you want to send in the notification.
-
Pass the object to
Display.scheduleLocalNotification()
.
Notifications can either be set up as one-time only or as repeating.
LocalNotification n = new LocalNotification();
n.setId("demo-notification");
n.setAlertBody("It's time to take a break and look at me");
n.setAlertTitle("Break Time!");
n.setAlertSound("beep-01a.mp3");
Display.getInstance().scheduleLocalNotification(
n,
System.currentTimeMillis() + 10 * 1000, // fire date/time
LocalNotification.REPEAT_MINUTE // Whether to repeat and what frequency
);
The resulting notification will look like
![Resulting notification in iOS](img/developer-guide/f7200840-677e-11e5-8fd7-41eb027f8a6c.png)
The above screenshot was taken on the iOS simulator.
The API for receiving/handling local notifications is also similar to push. Your application’s main lifecycle class needs to implement the com.codename1.notifications.LocalNotificationCallback
interface which includes a single method:
public void localNotificationReceived(String notificationId)
The notificationId
parameter will match the id
value of the notification as set using LocalNotification.setId()
.
public class BackgroundLocationDemo implements LocalNotificationCallback {
//...
public void init(Object context) {
//...
}
public void start() {
//...
}
public void stop() {
//...
}
public void destroy() {
//...
}
public void localNotificationReceived(String notificationId) {
System.out.println("Received local notification "+notificationId);
}
}
Note
|
localNotificationReceived() is only called when the user responds to the notification by tapping on the alert. If the user doesn’t opt to click on the notification, then this event handler will never be fired.
|
Repeating notifications will continue until they are canceled by the app. You can cancel a single notification by calling:
Display.getInstance().cancelLocalNotification(notificationId);
Where notificationId
is the string id that was set for the notification using LocalNotification.setId()
.
We are starting the complete overhaul of our push implementation that will allow us to deliver improved push related fixes/features and provide more reliability to the push service. When we designed our push offering initially it was focused around the limitations of Google App Engine which we are finally phasing out. The new servers are no longer constrained by this can scale far more easily and efficiently for all requirements.
However, as part of this we decided to separate the push functionality into two very different capabilities: push & device registration.+ Currently only push is supported and so the feature of pushing to all devices is effectively unsupported at the moment. However, once the device registration API is exposed it will allow you to perform many tasks that were often requested such as the ability to push to a cross section of devices (e.g. users in a given city).
The original push API mixed device tracking and the core push functionality in a single API which meant we
had scaling issues when dealing with large volumes of push due to database issues. So the new API discards
the old device id structure which is just a numeric key into our database. With the new API we have a new
device key which includes the native OS push key as part of its structure e.g. cn1-ios-nativedevicekey
instead of 999999
.
Assuming you store device ids in your server code you can easily convert them to the new device ids, your server
code can check if a device id starts with cn1-
and if not convert the old numeric id to the new ID
using this request (assuming 999999 is the device id):
https://codename-one.appspot.com/token?m=id&i=999999
The response to that will be something like this:
{"key":"cn1-ios-nativedevicecode"}
The response to that will be something or this:
{"error":"Unsupported device type"}
To verify that a push is being sent by your account and associate the push quotas correctly the new API requires a push token. You can see your push token in the developer console at the bottom of the account settings tab. If it doesn’t appear logout and login again.
The new API is roughly identical to the old API with two major exceptions:
Push.getDeviceKey()
with Push.getPushKey()
.We thought about keeping the exact same API but eventually decided that creating a separate API will simplify migration and allow you to conduct it at your own pace. .All push methods now require the push token as their first argument. The old methods will push to the old push servers and the new identical methods that accept a token go to the new servers.
To send a push directly to the servers you can use very similar code to the old Java SE code we provided just
changing the URL, adding the token and removing some of the unnecessary arguments. Send the push to the
URL https://push.codenameone.com/push/push
which accepts the following arguments:
cert
- http or https URL containing the push certificate for an iOS pushE.g. we can send push to the new servers using something like this from Java SE/EE:
URLConnection connection = new URL("https://push.codenameone.com/push/push").openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
String query = "token=TOKEN&device=" + deviceId1 +
"&device=" + deviceId2 + "&device=" + deviceId3 +
"&type=1&auth=GOOGLE_AUTHKEY&certPassword=CERTIFICATE_PASSWORD&" +
"cert=LINK_TO_YOUR_P12_FILE&body=" + URLEncoder.encode(MESSAGE_BODY, "UTF-8");
try (OutputStream output = connection.getOutputStream()) {
output.write(query.getBytes("UTF-8"));
}
int c = connection.getResponseCode();
... read response and parse JSON
Unlike the previous API which was completely asynchronous and decoupled the new API is mostly synchronous so we return JSON that you should inspect for results and to maintain your device list. E.g. if there is an error that isn’t fatal such as quota exceeded etc. you will get an error message like this:
{"error":"Error message"}
A normal response though, will be an array with results:
[
{"id"="deviceId","status"="error","message"="Invalid Device ID"},
{"id"="cn1-gcm-nativegcmkey","status"="updateId" newId="cn1-gcm-newgcmkey"},
{"id"="cn1-gcm-okgcmkey","status"="OK"},
{"id"="cn1-gcm-errorkey","status"="error" message="Server error message"},
{"id"="cn1-ios-iphonekey","status"="inactive" },
]
There are several things to notice in the responses above:
status=updateId
it means that the GCM server wants you to update the device id toa new device id. You should do that in the database and avoid sending pushes to the old key.
.iOS doesn’t acknowledge device receipt but it does send a status=inactive
result which
you should use to remove the device from the list of devices.
NOTICE: It seems that APNS (Apple’s push service) returns uppercase key results. So you need to query the database in a case insensitive way.
Warning
|
We are currently in the process of migrating to a new push notification architecture, see the section above for further details. A lot of the topics mentioned below are still relevant for the new push architecture which is why we are currently leaving this section intact. |
Push notification is only enabled for pro accounts in Codename One due to some of the server side infrastructure we need to maintain to support this feature.
Push notification allows you to send a message to a device, usually a simple text message which is sent to the application or prompted to the user appropriately. When supported by the device it can be received even when the application isn’t running and doesn’t require polling which can drain the devices battery. The keyword here is "when supported" unfortunately not all devices support push notification e.g. Android device that don’t have the Google Play application (formerly Android Market) don’t support push and must fall back to polling the server which isn’t ideal.
Currently Codename One supports pushing to Google authorized Android devices: GCM (Google Cloud Messaging), to iOS devices: Push Notification & to blackberry devices.
For other devices we will fallback to polling the server in a given frequency, not an ideal solution by any means so Codename One provides the option to not fallback.
This is how Apple describes push notification (image source Apple):
![Apple push notification workflow](img/developer-guide/push-notifications-graphic.png)
The "provider" is the server code that wishes to notify the device. It needs to ask Apple to push to a specific device and a specific client application. There are many complexities not mentioned here such as the need to have a push certificate or how the notification to APNS actually happens but the basic idea is identical in iOS and Android’s GCM.
Codename One hides some but not all of the complexities involved in the push notification process. Instead of working between the differences of APNS/GCM & falling back to polling, we effectively do everything that’s involved.
Push consists of the following stages on the client:
-
Local Registration - an application needs to register to ask for push. This is done by invoking:
-
Display.getInstance().registerPush(metaData, fallback);
-
The fallback flag indicates whether the system should fallback to polling if push isn’t supported.
-
On iOS this stage prompts the user indicating that the application is interested in receiving push notification messages.
-
Remote registration - once registration in the client works, the device needs to register to the cloud. This is an important step since push requires a specific device registration key (think of it as a "phone number" for the device). Normally Codename One registers all devices that reach this stage so you can push a notification for everyone, however if you wish to push to a specific device you will need to catch this information! To get push events your main class (important, this must be your main class!) should implement the PushCallback interface. The registeredForPush(String) callback is invoked with the device native push ID (not the id you should use. Once this method is invoked the device is ready to receive push messages.
-
In case of an error during push registration you will receive the dreaded: pushRegistrationError.
Figure 16. Gmails launch on iOS was famously tarnished by the dreaded aps-environment issue! -
This is a very problematic area on iOS, where you must have a package name that matches EXACTLY the options in your provisioning profile, which is setup to support push. It is also critical that you do not use a provisioning profile containing a
*
character in it. -
You will receive a push callback if all goes well.
First there are several prerequisites you will need in order to get started with push:
-
Android - you can find the full instructions from Google at http://developer.android.com/google/gcm/gs.html. You will need a project id that looks something like this: 4815162342. You will also need the server key, which looks something like this: AIzaSyATSw_rGeKnzKWULMGEk7MDfEjRxJ1ybqo.
-
iOS - You will need to create a provisioning profile that doesn’t have the
*
element within it.For that provisioning profile you will need to enable push and download a push certificate. Notice that this push certificate should be converted to a P12 file in the same manner we used in the signing tutorials.
You will need the password for that P12 file as well.
You will need a distribution P12 and a testing P12.
Warning! The P12 for push is completely different from the one used to build your application, don’t confuse them! You will need to place the certificate on the web so our push server can access them, we often use dropbox to store our certificates for push. * Blackberry - you need to register with Blackberry for credentials to use their push servers at https://developer.blackberry.com/devzone/develop/platform_services/push_overview.html.
+ Notice that initially you need to register for evaluation and later on move your app to production. This registration will trigger an email which you will receive that will contain all the information you will need later on. Such as your app ID, push URL (which during development is composed from your app ID), special password and client port number.
To start using push (on any platform) you will need to implement the PushCallback interface within your main class. The methods in that interface will be invoked when a push message arrives:
public class PushDemo implements PushCallback {
private Form current;
public void init(Object context) {
}
public void start() {
if(current != null){
current.show();
return;
}
new StateMachine("/theme");
}
public void stop() {
current = Display.getInstance().getCurrent();
}
public void destroy() {
}
public void push(String value) {
Dialog.show("Push Received", value, "OK", null);
}
public void registeredForPush(String deviceId) {
Dialog.show("Push Registered", "Device ID: " + deviceId + "\nDevice Key: " + Push.getDeviceKey() , "OK", null);
}
public void pushRegistrationError(String error, int errorCode) {
Dialog.show("Registration Error", "Error " + errorCode + "\n" + error, "OK", null);
}
}
You will then need to register to receive push notifications (its OK to call register every time the app loads) by invoking this code below (notice that the google project id needs to be passed to registration):
@Override
protected void onMain_RegisterForPushAction(Component c, ActionEvent event) {
Hashtable meta = new Hashtable();
meta.put(com.codename1.push.Push.GOOGLE_PUSH_KEY, findGoogleProjectId(c));
Display.getInstance().registerPush(meta, true);
}
Sending the push is a more elaborate affair; we need to pass the elements to the push that is necessary for the various device types depending on the target device. If we send null as the destination device our message will be sent to all devices running our app. However, if we use the device key which you can get via Push.getDeviceKey() you can target the device directly. Notice that the device key is not the argument passed to the registration confirmation callback!
Other than that we need to send various arguments whether this is a production push (valid for iOS where there is a strict separation between the debug and the production push builds) as well as the variables discussed above:
@Override
protected void onMain_SendPushAction(Component c, ActionEvent event) {
String dest = findDestinationDevice(c).getText();
if(dest.equals("")) {
dest = null;
}
boolean prod = findProductionEnvironement(c).isSelected();
String googleServerKey = findGoogleServerKey(c).getText();
String iOSCertURL = findIosCert(c).getText();
String iOSCertPassword = findIosPassword(c).getText();
String bbPushURL = findBbPushURL(c).getText();
String bbAppId = findBbAppId(c).getText();
String bbPassword = findBbPassword(c).getText();
String bbPort = findBbPort(c).getText();
Push.sendPushMessage(findPushMessage(c).getText(), dest, prod, googleServerKey, iOSCertURL, iOSCertPassword, bbPushURL, bbAppId, bbPassword, bbPort);
}
Unfortunately we aren’t done yet!
We must define the following build arguments in the project properties:
ios.includePush=true rim.includePush=true rim.ignor_legacy=true rim.pushPort=... rim.pushAppId=... rim.pushBpsURL=...
Once you define all of these push should work for all platforms.
You can perform the same task using our server API to send a push message directly from your server using the following web API. Notice that all arguments must be submitted as post!
Argument | Values | Description |
---|---|---|
|
numeric id or none |
Optional, if omitted the message is sent to all devices |
|
com.myapp… |
The package name of your main class uniquely identifies your app. This is required even when submitting a device ID |
|
The email address of the developer who built the app. This provides validation regarding the target of the push |
|
|
numeric defaults to |
The type of push, standard is 1 but there are additional types like 2 which is a silent push (nothing will be shown to the user) or 3 which combines a hidden payload with text visible to the user |
|
Google authorization key |
Needed to perform the GCM push |
|
password |
The password for the P12 push certificate for an iOS push |
|
URL |
A url containing a downloadable P12 push certificate for iOS |
|
Arbitrary message |
The payload message sent to the device |
|
|
Whether the push is sent to the iOS production or debug environment |
|
URL |
The blackberry push URL |
|
App ID sent by RIM |
|
|
Push password from RIM |
|
|
Push port from RIM |
This can be accomplished on a Java server or client application using code such as this:
URLConnection connection = new URL("https://codename-one.appspot.com/sendPushMessage").openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
String query = "packageName=PACKAGE_NAME&[email protected]&device=" + deviceId +
"&type=1&auth=GOOGLE_AUTHKEY&certPassword=CERTIFICATE_PASSWORD&" +
"cert=LINK_TO_YOUR_P12_FILE&body=" + URLEncoder.encode(MESSAGE_BODY, "UTF-8");
try (OutputStream output = connection.getOutputStream()) {
output.write(query.getBytes("UTF-8"));
}
int c = connection.getResponseCode();
This can be accomplished in PHP using code like this:
$args = http_build_query(array(
'certPassword' => 'CERTIFICATE_PASSWORD',
'cert' => 'LINK_TO_YOUR_P12_FILE',
'production' => false,
'device' => $device['deviceId'],
'packageName' => 'YOUR_APP_PACKAGE_HERE',
'email' => 'YOUR_EMAIL_ADDRESS_HERE',
'type' => 1,
'auth' => 'YOUR_GOOGLE_AUTH_KEY',
'body' => $wstext));
$opts = array('http' =>
array(
'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded',
'content' => $args
)
);
$context = stream_context_create($opts);
$response = file_get_contents("https://codename-one.appspot.com/sendPushMessage", false, $context);
![iOS App Badges](img/developer-guide/badges-1.png)
iOS provides the ability to send a push notification that triggers a numeric badge on the application icon. This can be achieved by sending a push notification with the type 100 and the number for the badge. That number would appear on the icon, when you launch the app the next time the default behavior is to clear the badge value.
There is also an option to send push type 101 and provide a badge number semi-colon and a message e.g. use a message body such as this: "3;This message is shown to the user with number 3 badge". Obviously, this feature will only work for iOS so don’t send these push types to other platforms…
The badge number can be set thru code as well, this is useful if you want the badge to represent the unread count
within your application. To do this we have two methods in display: isBadgingSupported()
& setBadgeNumber(int)
.
Notice that even if isBadgingSupported
will return true, it will not work unless you activate push support!
To truly utilize this you might need to disable the clearing of the badges on startup which you can do with the
build argument ios.enableBadgeClear=false
.
In recent versions of iOS Apple added the ability to distribute beta versions of your application to beta testers using tools they got from the testflight acquisition. This is supported for pro users as part of the crash protection feature.
To take advantage of that use the build argument ios.testFlight=true and then submit the app to the store for beta testing. Make sure to use a release build target.
About This Guide
Introduction
Basics: Themes, Styles, Components & Layouts
Theme Basics
Advanced Theming
Working With The GUI Builder
The Components Of Codename One
Using ComponentSelector
Animations & Transitions
The EDT - Event Dispatch Thread
Monetization
Graphics, Drawing, Images & Fonts
Events
File-System,-Storage,-Network-&-Parsing
Miscellaneous Features
Performance, Size & Debugging
Advanced Topics/Under The Hood
Signing, Certificates & Provisioning
Appendix: Working With iOS
Appendix: Working with Mac OS X
Appendix: Working With Javascript
Appendix: Working With UWP
Security
cn1libs
Appendix: Casual Game Programming