How I solved reloading codeless (w/o ABC) SWF files on iOS with Adobe AIR 4.0

By | February 26, 2014

I have been working on an aviation training app with a great team at Pegasus Interactive. The app was originally done in Flash AS 2.0 using my state engine, then converted to work in AS 3.0 on iOS and AIR. One thing it does a lot of is load SWF files dynamically. We managed to get the app into the App Store in the middle of last year, while using AIR 3.5. We stayed with AIR 3.5 because AIR 3.6 introduced a restriction, namely that SWF files, even codeless ones (without ABC, or “ActionScript Byte Code”), no longer could be reloaded. Seems like this issue has been floating around since the latter part of 2013. For future updates, I was nervous, because caching SWF files would consume memory fast, and otherwise we would have to do a significant re-architecting of the app, such as allowing external loading SWF files attached to classes. So I went the easy route, waiting for 3.7 in hopes Adobe would solve the problem.

I saw a glimmer of hope when someone said it was fixed in AIR 3.7+, but that hope was shortlived. I had to face a more severe re-architecting future. The issue finally came to a head earlier this month, when Apple stopped accepting apps and updates that used the older SDK’s. Whoops, we were going to have to get through this, now.

This post describes how we have successfully gotten to reload SWF’s (without code) on AIR 4.0, SWF’s we were downloading and SWF’s we had included in our initial distribution. In a nutshell, we needed two methods. The first method was for SWF’s we downloaded into the cache — essentially we got a file reference to it, and the reloading worked. Getting the file reference did not work when we tried to reload a SWF that we had packaged with the application–the second method added a URL variable to the loading, which then worked. I worked with Brandon Krakowsky on this, who helped a lot. I hope it can help someone struggling with this problem, perhaps seeing all the changes for 4.0 we needed to do!

Important: I have not submitted the app to Apple yet, so there is a chance they will reject it for some reason. If they do, I will update this post with that information. [Update: the app was accepted without changes based on what I describe here. Yay!]

Here are the issues we addressed:

  1. Getting a SWF to Load the First Time: Getting SWF’s to load at all, i.e., addressing changes that were made somewhere between AIR 3.5 to 4.0;
  2. Reloading SWF’s: Loading and reloading SWF files that were downloaded from the Internet; and,
  3. Status Bar Issue: Adjusting the status bar (top 20 pixels) text color on the screen — it was showing black, but our background was also black.

Getting a SWF to Load the First Time

The first time Brandon and I moved to AIR 4.0 and tried to load the first SWF (from the app:/ area, since it was packaged with the app), the SWF file did not load at all — it got an error about multiple application domains. We looked online to find this article about it, and solved the problem by adding the LoaderContext:

request = new URLRequest(path);
request.cacheResponse = false;
request.useCache = false;
_lc = new LoaderContext(false, ApplicationDomain.currentDomain, null);
moduleLoader.load(request, _lc);

You’ll also see here that we added cacheResponse and useCache options to URLRequest — I don’t know if this had an effect, but it seemed safest to use it.

After we had this, then at least we could load a SWF file (without ActionScript Byte Code, I remind you) — once.

Reloading SWF’s

We were first trying to load the SWF file that happened to be packaged with the app, in a folder called “modules”. Using the typical trick for loading web pages without caching, we appended a variable to the URL, which worked:

path = moduleDirectoryPath + module_folder + "/" + module + "?nf="+getTimer();

Then using the moduleLoader chunk of code above, it worked. If we did not have the variable at the end, we could load the SWF once, but the second time it gave us a “cannot reload SWF” error. Oddly enough, this happened with one of two of our SWF files — with one SWF file, it allowed us to reload it even without the appended variable. We never found out why.

After our initial success, we tried this with our externally loaded content, which we had of two types: (a) content downloaded into the cacheDirectory (hence a file pointer); and (b) content streamed from the Internet (http). We got an error trying the variable trick with our file pointer, because it could not find the file when it had the appended variable, like “file://filename.swf?nf=3322”. We were a bit stuck.

So on a lark, I removed the appended URL variable, and voila, the SWF loaded (and more importantly, reloaded). It also reloaded for our streamed content (item b, described in the last paragraph). I would prefer not to append the variable on the URL, to have a single approach for loading files, so I tried to get the content from the application folder into a “file” type of path to load, trying something like new File(File.applicationDirectory …) and the like. I got the correct file finally, but in the end, it gave me the reloading error. Very odd, so loading from applicationDirectory was different from loading from cacheDirectory, as far as I could tell.

So I took a step back and applied the dual approach:

if (loadFromApp) {
   path = moduleDirectoryPath + module_folder + "/" + module + "?nf="+getTimer();
} else {
   path = moduleDirectoryPath + module_folder + "/" + module;
}

In other words, if I was loading the SWF from the applications/modules folder, I added the URL variable. If I was loading the SWF from the cache, I did not use it. So the path request to the loader was something like “/modules/…” if it was from the application folder (app:/), but it was “file://…” if it was from the cache). Here is the code to initially set the path:

var moduleDirectoryPath = "/modules/";

if (externalContent == FROM_CACHE) {
   moduleDirectoryPath = File.cacheDirectory.url + moduleDirectoryPath;
} else if (externalContent == FROM_WEB) {
   moduleDirectoryPath = "http://our-online-content.com" + moduleDirectoryPath;
}

and then it all worked fine!

Status Bar Issue

The last piece of the puzzle was handling the problem of the status bar text color — by default, it was set to black, but the background was black as well. Our app is not full-screen, so the status bar is showing. I needed to make the text color white (in addition to moving my graphics down about 20 pixels).

This change I made in the manifest file. However, we are using Flash CS6 to produce the ipa, and whenever I saved the iOS Settings, it would overwrite the manifest file! So my manual changes to the manifest file, to add the appropriate lines, kept getting overwritten. The final result (we’ll skip the pain), was to have my iPhone section as follows (note that our app is just for the iPad, hence the UIDeviceFamily setting):

<iPhone>
  <requestedDisplayResolution>standard</requestedDisplayResolution>
  <InfoAdditions>
    <![CDATA[<key>UIViewControllerBasedStatusBarAppearance</key><false/><key>UIStatusBarStyle</key><string>UIStatusBarStyleLightContent</string><key>UIDeviceFamily</key><array><string>2</string></array>]]>
  </InfoAdditions>
</iPhone>

The key was adding values for UIViewControllerBasedStatusBarAppearance and UIStatusBarStyle. However, when I tried to add them manually to the manifest, and then updating the iOS Settings in Flash CS6, the results came out wrong–Flash would absorb some of the keys and values, but not all. My solution from trial and error was to enter the following manually into the xml file:

<iPhone>
  <requestedDisplayResolution>standard</requestedDisplayResolution>
  <InfoAdditions>
    <![CDATA[<key>UIViewControllerBasedStatusBarAppearance</key><false/>]]>
    <![CDATA[<key>UIStatusBarStyle</key><string>UIStatusBarStyleLightContent</string>]]>
    <![CDATA[<key>UIDeviceFamily</key><array><string>2</string></array>]]>
  </InfoAdditions>
</iPhone>

Then Flash collapsed the CDATA’s when it wrote the file (after I saved the fla’s iOS Settings), into the final result (copied here):

<iPhone>
  <requestedDisplayResolution>standard</requestedDisplayResolution>
  <InfoAdditions>
    <![CDATA[<key>UIViewControllerBasedStatusBarAppearance</key><false/><key>UIStatusBarStyle</key><string>UIStatusBarStyleLightContent</string><key>UIDeviceFamily</key><array><string>2</string></array>]]>
  </InfoAdditions>
</iPhone>

Quite an adventure, to say the least!

3 thoughts on “How I solved reloading codeless (w/o ABC) SWF files on iOS with Adobe AIR 4.0

  1. Stoyan Yankov

    Hello Jonathan,

    Great blog!. It’s a very informative post.

    I am working on iOS application where I have stripped swf files (without ABC) on my server. What I want to do is to download a file from the server, save it locally and use it multiple times.
    However I faced the problem when trying to load the file several times.

    I have this code in my application:

    1. Downloading the file from server:
    var path:String = “http://mysite.com/skin1.swf”;
    var urlRequest:URLRequest = new URLRequest(path);
    urlRequest.cacheResponse = false;
    urlRequest.useCache = false;
    var skinUrlLoader = new URLLoader();
    skinUrlLoader.addEventListener(Event.COMPLETE, completeHandler);
    skinUrlLoader.load(urlRequest);

    2. Saving the file locally (in completeHandler):
    var data:ByteArray = event.target.data;
    var fileStream:FileStream = new FileStream();
    fileStream.addEventListener(Event.COMPLETE, fileStreamCompleteHandler);
    //fileStream.openAsync(File.applicationStorageDirectory.resolvePath(“skin1.swf”), FileMode.UPDATE);
    fileStream.openAsync(File.cacheDirectory.resolvePath(“skin.swf”), FileMode.UPDATE);
    fileStream.writeBytes(data);

    3. Loading the file from the local file system (in fileStreamCompleteHandler):
    //__url = File.applicationStorageDirectory.resolvePath(“skin1.swf”).url;
    __url = File.cacheDirectory.resolvePath(“skin1.swf”).url;
    var urlRequest:URLRequest = new URLRequest(__url);
    urlRequest.cacheResponse = false;
    urlRequest.useCache = false;
    var ldrContext:LoaderContext = new LoaderContext(false, ApplicationDomain.currentDomain, null);
    var loader:Loader = new Loader();
    loader.load(__urlRequest, __ldrContext);

    So the code above works fine the first time but if I try to reload the file from the saved location I have Error 3764.
    As you can see I tried both applicationStorageDirectory and cacheDirectory but I have exactly the same problem in both cases.

    I tried with AIR SDK 4.0 and 14.0 – no difference.

    Do you have any ideas why it doesn’t work in my case which is very similar to what you describe in your post?

    Thank you in advance,
    Stoyan

  2. Jonathan Kaye Post author

    Hi Stoyan, I just saw your note (sorry, I get so much spam that it’s hard to see real comments through all of it) — I will take a look at it in the next few days and let you know where I think the differences may be.
    -jonathan

  3. Jonathan Kaye Post author

    Hi Stoyan, I would try adding the “getTimer” URL variable trick to load from the applicationStorage area, just to see if any reloading works. Of course applicationStorage may not be the right place to load your stuff, according to iOS policies, but at least it would give you some confidence you’re on the right path.
    In my situation, I am downloading a zip file then unpacking it into the cacheDirectory, rather than directly in there. I don’t think there’s anything special about that, but maybe me mentioning that would trigger something in your mind. When I am downloading the zip file (into the tmp directory), I’m using URLStream to download the file, then I use FileStream to write the zip file. I don’t remember why I have that 2 step process.
    I noticed in my code (not shown in post) I am doing the download synchronously, not async. Maybe you want to try that (fileData is my ByteArray):

    filestream.open(fileptr, FileMode.WRITE);
    filestream.writeBytes(fileData, 0, fileData.length);

    I would also ensure the program works in the iOS simulator on Windows/Mac, just in case there is something else going on (though your’s is working the first time on iOS, so I doubt the Win/Mac will fail). You shouldn’t get the reload error on Win/Mac.
    I see you are using resolvePath() off File.cacheDirectory, I was constructing the path manually, like File.cacheDirectory.url + “/skin.swf”, but your’s is loading the first time fine so I highly doubt there’s any issue there.

Leave a Reply

Your email address will not be published. Required fields are marked *