肇鑫的技术博客

业精于勤,荒于嬉

Applying async/await in Xcode 13

Applying async/await was not hard. It just has some difficulties that I didn't expect.

Comparing Code Styles

I had already use AwaitKit to program async before Xcode 13. The async/await codes looked similar. Comparing:

    // AwaitKit version
    private func usersShow() throws {
        let weiboAPI = WeiboAPIType.users_show.weiboAPI
        let (data, reponse) = try `await`(URLSession.shared.dataTask(.promise, with: weiboAPI.urlRequest))
        let httpURLResponse = reponse as! HTTPURLResponse

        if (200..<300).contains(httpURLResponse.statusCode) {
            weiboAPI.saveStatus(from: data)
        } else {
            throw Result.weiboError(httpURLResponse.statusCode, data)
        }
    }
    
    // Xcode 13 version
    @available(iOS 15.0, *)
    private func usersShow() async throws {
        let weiboAPI = WeiboAPIType.users_show.weiboAPI
        let (data, reponse) = try await URLSession.shared.data(for: weiboAPI.urlRequest)
        let httpURLResponse = reponse as! HTTPURLResponse
        
        if (200..<300).contains(httpURLResponse.statusCode) {
            weiboAPI.saveStatus(from: data)
        } else {
            throw Result.weiboError(httpURLResponse.statusCode, data)
        }
    }

Only the definition line and `let (data, response) line are different. So refactoring old codes are easy.

Issues

Unlike AwaitKit, async/await in Xcode 13 sorts code to be async and sync. But what if you want to use an async function to be run in a closure that not allowed async to run?

The answer is to provide the async version functions. Like func data(from url: URL, delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse) for func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask. However, not all API currently are provided the counterpart. More will be come.

Apple provides sync and async versions of functions, but another problem shows. As async only works in IOS 15, so we may want the compiler to run the async version in iOS 15 and run the sync version in other prior iOS.

The compiler doesn't allow this feature. As somehow the compiler considers the async and sync versions are conflict to use together. So we should wait until the Concurrency is backwards to iOS 13.

References

Async Programming

There are many ways to program asynchronously. "GCD", "Operation queue" are commonly used. Also there is Combine and async/await that are newly introduced.

Combine

Combine is useful. You can think it as a notification, when a notice comes the receiver get notified and run some code.

However, combine has it limitation. It is a well-designed series operations followed by its rules. You have to carefully design the path. Also, it can't be finished with a throw. So you have to convert throw to Just.

async/await

async/await is more flexible. You can do it as what you want. Especially if you want to chain functions with throws.

Swift Packages Quick Learning

So far, I have learnt four ways of adding frameworks, manually, CocoaPods, Carthage and Swift Package Manager. This article is a summary of how to creating your own Swift Packages.

What is Swift Package?

A Swift Package is a bundle of source files with a meaningful name.

Platform

If unspecified, Swift Package works in all versions of Apple Operation Systems, this make a lot of version warnings. So a better idea is to provide the platform your package could work with.

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "MyAppStore",
    platforms: [
        .macOS(.v11)
    ],

Dependencies

Adding dependencies is easy. However, you must added the dependencies to both targets and test test targets.

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "MyAppStore",
    platforms: [
        .macOS(.v11)
    ],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "MyAppStore",
            targets: ["MyAppStore"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(url: "git@github.com:owenzhao/QRCodeKit.git", Package.Dependency.Requirement.branch("1.0.0")),
        .package(url: "git@github.com:weichsel/ZIPFoundation.git", "0.9.12"..<"1.0.0")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "MyAppStore",
            dependencies: ["QRCodeKit", "ZIPFoundation"]),
        .testTarget(
            name: "MyAppStoreTests",
            dependencies: ["MyAppStore", "QRCodeKit", "ZIPFoundation"]),
    ]
)

Versioning

For versioning, there is a trick. There is no where in the swift part to set the version of your package. The version is set by git tag. So you need to do the versioning in your git tool.

swift_package_versioning

Resources

Swift Packages can get some kinds of sources as resources automatically, but you may also need to add sources manually.

You can add them as files one by one. Or you can add the folder directly. If you use process, Xcode maybe take further optimization to the resources. If you want to keep them unchanged, you can use copy.

    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "MyAppStore",
            dependencies: ["QRCodeKit", "ZIPFoundation"],
            resources: [
                .process("icons"),
                .process("AllApps.zip")
            ]),

Localization

Add default localizedation in Package.swift

let package = Package(
    name: "MyAppStore",
    defaultLocalization: "en",
    platforms: [
        .macOS(.v11),
        .iOS(.v14)
    ],

You must use genstrings to get localized strings.

  1. Go to the directory of swift files.
  2. Create a directory named en.lpproj.
  3. Run code find ./ -name "*.m" -print0 | xargs -0 genstrings -o en.lproj.
  4. Copy en.lpproj as the name of you intent, say zh.lpproj.
  5. Translate the localized strings under zh.lpproj.
$ tree
.
├── AppInfoSwiftUIView.swift
├── MainSwiftUIView.swift
├── en.lproj
│   └── Localizable.strings
└── zh.lproj
      └── Localizable.strings

Using String resources in Swift Packages

Swift Package is built alone with the main bundle, it is called model Bundle, so you have to explicitly tell the bundle by name, or the system will try to find the string resources in the main bundle.

Button(action: download, label: {
    Text("Download", bundle: .module)
})

You should also notice that there are still some subtle issues. I found that the lang.lpproj directories must be under the target directory directly, or the preview won't take the environment effect.
However, running app goes fine whenever the directory goes.

References