[SWIFT] The story of AppClip support in Anyca

AppClip has received a lot of attention as a new feature of iOS14, but I think that there are few chances to see it actually used compared to the widget announced at the same time.

Anyca released AppClip in October, which provides a function to browse publicly available car information. In this article, I would like to introduce some of the stumbling blocks in the release of AppClip on Anyca and how to avoid them.

This article is the 16th day article of DeNA Advent Calendar 2020.

What is AppClip?

AppClip is a new feature that can be used from iOS 14 that can provide some functions of the app without installing the app from the App Store. You can display and start the UI called AppClip card starting from a specific URL, NFC tag, QR code, etc.

If you have an iOS device with iOS 14 installed, please scan the QR code below. Anyca's AppClip card will be displayed, and if you press the display button, Anyca's AppClip will start even if the app is not installed, and the detailed information screen of the car will be displayed.

In this way, it is possible to smoothly provide the functions of the application to the user, so the threshold for getting the user to use the application can be significantly lowered.

AppClip 10MB limit

Although it is a convenient AppClip like this, there are some restrictions such as not being able to use some frameworks and restricting the acquisition of privacy information compared to ordinary apps. The biggest limitation of this is the binary size.

The binary size of AppClip is limited to 10MB in order to launch AppClip quickly. This 10MB constraint is very strict, as it is imposed on the uncompressed size, not on the compressed app size as in the ipa format.

For example, the Anyca app is about 65MB in ipa format, but it is about 140MB in uncompressed state. In order to provide some functions of the app as AppClip, it is necessary to cut out only what is necessary to provide the functions and reduce the size to about 1/10.

How to check the size of the AppClip binary

Even if the created AppClip exceeds 10MB, it can be used normally during development. However, when I try to apply for the app including AppClip, the following error occurs.

ITMS-90865: Thinned app clip size is too large - The universal variant app clip /Payload/xxx.app exceeds the maximum allowable size of 10MB. For details about app thinning, see: https://help.apple.com/xcode/mac/current/#/devbbdc5ce4f.

To check the size of the AppClip before applying, archive the app containing the AppClip and export the AppClip with Bitcode and App Thinning enabled in AdHoc. When the export is complete, a file called App Thinning Size Report.txt will be generated with ipa. You can check the size of AppClip by checking the contents of this file.


App Thinning Size Report for All Variants of anyca-Release

:

Variant: Clip.ipa
Supported variant descriptors: Universal
App + On Demand Resources size: 4 MB compressed, 10.3 MB uncompressed
App size: 4 MB compressed, 10.3 MB uncompressed
On Demand Resources size: Zero KB compressed, Zero KB uncompressed

If xx.x MB uncompressed in the part of App size: xx.x MB compressed, xx.x MB uncompressed is less than 10MB, it means that the size limit of AppClip can be passed.

Narrow down the required file resource libraries

You don't have to use all the code, libraries, and resources that your current app uses to provide the functionality you use with AppClip. When creating a new target for AppClip, remove all unused views, features, code, resources, and libraries from the AppClip target.

Since it is not used in AppClip in the code, there may be some parts that you want to exclude from the build target. In such a case, the AppClip target defines APP_CLIP as a compiler flag, and uses the preprocessor to exclude some code from the build when building for AppClip.

#if !APP_CLIP
  //Unnecessary processing in AppClip
#endif

Basically, by narrowing down the target files, I think that you can avoid the size limit in most cases. However, in the case of Anyca, this alone could not be less than 10MB, and some other measures had to be taken.

Compress resources

In Anyca, there is a part where the UI is defined by its own layout file, and this layout file is included in the resource in JSON format. Also, some designs used custom fonts, which also had to be included in the resource.

Each of these files is not that big, and if you use ipa, compression will work, so you do not have to worry about it in normal application development, but in AppClip, the file size in the uncompressed state Affects the size limit, so the more files you have, the easier it is to get caught up in the limit.

Anyca decided to reduce the size of the AppClip by zipping these files at build time.

First, I added a script to the build phase of AppClip, and when the resource copy was completed, I zipped the target file and deleted the original data.

cd "${TARGET_BUILD_DIR}/${EXECUTABLE_FOLDER_PATH}"
zip fonts.zip *.ttf
zip layouts.zip layout_*.json
rm -f *.ttf
rm -f layout_*.json

Then, when you start AppClip, unzip the zip file and use the unzipped resources.

func decompressLayoutFiles(destinationPath: String) {
    let bundlePath = Bundle(for: type(of: self)).bundlePath
    guard let contents = try? FileManager.default.contentsOfDirectory(atPath: bundlePath) else { return }
    
    //If there is a layout compressed file, expand it
    if let compressedFile = contents.first(where: { $0 == "layouts.zip" }) {
        SSZipArchive.unzipFile(atPath: "\(bundlePath)/\(compressedFile)", toDestination: destinationPath)
    }
}
func registerCompressed(bundle: Bundle? = .main) {
    guard let compressedFile = bundle?.path(forResource: "fonts", ofType: "zip"),
          let cacheDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first else { return }

    do {
        let fontDirectory = "\(cacheDirectory)/Fonts"
        if !FileManager.default.fileExists(atPath: fontDirectory) {
            try FileManager.default.createDirectory(atPath: fontDirectory, withIntermediateDirectories: false)
        }
        
        SSZipArchive.unzipFile(atPath: compressedFile, toDestination: fontDirectory)
        
        let contents = try FileManager.default.contentsOfDirectory(atPath: fontDirectory)
        for content in contents where content.hasSuffix("ttf") {
            guard let fontData = NSData(contentsOfFile: "\(fontDirectory)/\(content)"),
                  let dataProvider = CGDataProvider(data: fontData),
                  let cgFont = CGFont(dataProvider) else { continue }

            var errorRef: Unmanaged<CFError>? = nil
            CTFontManagerRegisterGraphicsFont(cgFont, &errorRef)
        }
    } catch {
        // do nothing
    }
}

By compressing and decompressing the files on our own in this way, we were able to reduce the size by several MB.

Make Swift optimization size-first

I think the above content will make it a lot slimmer, but if you want to reduce the size one more time, set the optimization level of the Swift compiler to size priority.

SWIFT_OPTIMIZATION_LEVEL = -Osize

This reduced the size by about 20% compared to the normal optimization level -O.

AppClip operation check

Just like a regular app, AppClip under development can be debugged from Xcode. At this time, by specifying the startup URL in the runtime environment variable _XCAppClipURL, it is possible to test the state in which AppClip is started based on this URL.

To actually read the URL from the QR code or NFC tag and start AppClip By adding the Local Experience of AppClip from the developer settings of the development terminal, when reading the QR code or NFC tag including the set URL You can launch AppClip on.

It is possible to provide AppClip tests to testers via TestFlight, but it can only be launched from the TestFlight app and you can only specify up to 3 launch URLs.

AppClip Experience Registration

There are two types of AppClip, Default AppClip Experience and Advanced AppClip Experience, and you can set the contents of the AppClip card displayed when AppClip is started from App Store Connect.

AppClip launch using QR code or NFC tag can only be used in Advanced AppClip Experience, so if you want to launch with these as triggers, you need to set both.

Summary

There are still few apps provided by AppClip, and how to use them is still being groped. Although the size limit is strict, the user can touch the app much more smoothly than installing the app via the App Store, so I think it will be a very fun feature if it can be incorporated well.

Currently, Anyca is on display at b8ta in Yurakucho, and you can also try AppClip from the QR code installed here. Please try it when you come near us.

In addition, DeNA's official Twitter account @DeNAxTech publishes not only blog articles but also presentation materials at various study sessions. If you are interested, please follow us.

Recommended Posts

The story of AppClip support in Anyca
The story of writing Java in Emacs
The story of low-level string comparison in Java
The story of making ordinary Othello in Java
The story of learning Java in the first programming
Review the multilingual support (i18n) of the ios app in 3 minutes
The story of an Illegal State Exception in Jetty.
The story of throwing BLOB data from EXCEL in DBUnit
[Java version] The story of serialization
The story of @ViewScoped consuming memory
Order of processing in the program
The story of encountering Spring custom annotation
The identity of params [: id] in rails
The story of updating SonarQube's Docker Container
Rails refactoring story learned in the field
The story of RxJava suffering from NoSuchElementException
Write the movement of Rakefile in the runbook
The story of acquiring Java Silver in two months from completely inexperienced.
[Git] The horrifying story of deleting the master branch ~ The answer is in English ~
[Order method] Set the order of data in Rails
The story of intentionally using try catch for the first time in my life
About the idea of anonymous classes in Java
A story about the JDK in the Java 11 era
The story of making the existing Dockerfile GPU compatible
Measure the size of a folder in Java
The story of introducing Ajax communication to ruby
Story of implementing update function in form object
The story of raising Spring Boot 1.5 series to 2.1 series
Feel the passage of time even in Java
The story of tuning android apps with libGDX
The story of adding the latest Node.js to DockerFile
Support out of support in docker environment using centos6
The story of initializing Money :: Currency when testing
Import files of the same hierarchy in Java
Write a test by implementing the story of Mr. Nabeats in the world with ruby
Get the URL of the HTTP redirect destination in Java
Specify the encoding of static resources in Spring Boot
Count the number of occurrences of a string in Ruby
Format of the log output by Tomcat itself in Tomcat 8
In Time.strptime,% j (total date of the year) is
Access the war file in the root directory of Tomcat
Return the execution result of Service class in ServiceResponse class
[For beginners] DI ~ The basics of DI and DI in Spring ~
Decimal numbers are dangerous in programming, right? The story.
Get the name of the test case in the JUnit test class
The story of migrating from Paperclip to Active Storage
About the problem of deadlock in parallel processing in gem'sprockets' 4.0
The story of making a reverse proxy with ProxyServlet
Understand the characteristics of Scala in 5 minutes (Introduction to Scala)
Examine the boundaries of the "32GB memory wall" in Elasticsearch
[Java] Get the file in the jar regardless of the environment
The objects in the List were references, right? Confirmation of
Personal summary of the guys often used in JUnit 4
Set the maximum number of characters in UITextField in RxSwift
Change the storage quality of JPEG images in Java
Find the approximate value of log (1 + x) in Swift
SSL in the local environment of Docker / Rails / puma
Get the URL of the HTTP redirect destination in Ruby
The story of making Dr. Oakid using LINE BOT
The story of making dto, dao-like with java, sqlite
Summarize the additional elements of the Optional class in Java 9