iTranslated by AI
Calling App Store Connect API from Swift
The App Store Connect API is detailed in the documentation, so please refer to it for the basics.
In this article, I will introduce some points I found tricky: how to create tokens and how to read the contents of gzip data.
How to Create a Token
The documentation for tokens can be found here.
App Store Connect API uses JWT.
First, create an API key in App Store Connect.
Download the p8 file and note the Issuer ID and Key ID.
let header: [String: String] = [
"alg": "ES256",
"kid": keyID,
"typ": "jwt",
]
let payload: [String: Any] = [
"iss": issuerID,
"iat": Int(Date().timeIntervalSince1970),
"exp": Int(Date().timeIntervalSince1970 + (20 * 60)),
"aud": "appstoreconnect-v1",
]
Create the header and payload like this. (The scope is optional, so it is ignored here.)
After converting these into JSON strings, convert them into Base64 (URL encoded) format and join them with ..
let headerJson = try! JSONSerialization.data(withJSONObject: header)
let payloadJson = try! JSONSerialization.data(withJSONObject: payload)
let input = "\(headerJson.base64urlEncodedString()).\(payloadJson.base64urlEncodedString())"
How to convert to URL-encoded Base64
import Foundation
extension Data {
func base64urlEncodedString() -> String {
return base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
Next, use the downloaded private key to encrypt it in ES256 format and create a signature.
We will use CryptoKit for this.
let privateKey = try P256.Signing.PrivateKey(pemRepresentation: privateKey)
let data = input.data(using: .utf8)!
let signature = try privateKey.signature(for: data)
It is convenient that you can pass the PEM format string (the format enclosed by -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----) directly to the initializer of P256.Signing.PrivateKey. (As this is @available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *), you might need to be careful about the version support.)
Finally, the API token is completed by joining the Base64-converted header and payload with the Base64-converted signature using ..
let raw = signature.rawRepresentation
let apiToken = "\(input).\(raw.base64urlEncodedString())"
After that, you can make requests by adding it to the API request header in the form of Authorization: Bearer \(apiToken).
Column
By the way, when I tried to use the private key by saving it in GitHub Actions Secrets, it didn't work for some reason, and I was stuck for about an hour.
The cause was that the newline codes were converted when saved in the GitHub Actions settings.
I wrote an article about this yesterday, so please refer to it.
How to View the Contents of gzip-Compressed Data
Most responses from the App Store Connect API are returned in JSON format, but some data is returned compressed in gzip format.
One way to decompress this data is by using a library.
This time, I decided not to use this library. Instead, while researching, I found that it could be decompressed using zlib, so I asked ChatGPT to write the code for me.
Code to decompress gzip-formatted data
import Foundation
import zlib
func gunzip(data: Data) -> Data? {
guard data.count > 0 else {
return nil
}
var stream = z_stream()
var status: Int32
status = inflateInit2_(&stream, 16 + MAX_WBITS, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size))
guard status == Z_OK else {
return nil
}
var decompressedData = Data(capacity: data.count * 2)
data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
stream.next_in = UnsafeMutablePointer<Bytef>(
mutating: bytes.bindMemory(to: Bytef.self).baseAddress!)
stream.avail_in = uint(data.count)
while stream.avail_in > 0 {
if Int(stream.total_out) >= decompressedData.count {
decompressedData.count += data.count / 2
}
stream.next_out = decompressedData.withUnsafeMutableBytes {
$0.bindMemory(to: Bytef.self).baseAddress! + Int(stream.total_out)
}
stream.avail_out = uint(decompressedData.count) - uint(stream.total_out)
status = inflate(&stream, Z_SYNC_FLUSH)
guard status == Z_OK || status == Z_STREAM_END else {
inflateEnd(&stream)
return
}
}
}
inflateEnd(&stream)
decompressedData.count = Int(stream.total_out)
return decompressedData
}
I am using the code generated by ChatGPT as is, so please let me know if there are any errors.
(It worked fine for me, so I'm using it as it is.)
Summary
In this article, I introduced how to create tokens for use with the App Store Connect API in Swift and how to decompress gzip-formatted data.
I am building a CLI tool that uses these methods to summarize and post daily app installation counts and user reviews to Slack.
Feel free to take a look if you're interested.
Discussion