[iOS] [AWS] AppSync SDKからAmplify SDKへのマイグレーションガイド
初めに
自分が参加しているプロジェクトでAppSync SDKが使われていましたが、Maintenance Modeになりアップデートが停止したため、Amplify SDKに切り替える作業を行なったため、その際の手順とポイントを書きたいと思います。
AmplifyのドキュメントにはUpgrade from AppSync SDKがあるのですが、それだけでは足りないところがありました。
対象読者
AppSync SDKを使われている方
Amplify SDKへの移行を考えている方
実施環境
Xcode 16.2
Amplify SDK 2.45.4
プロジェクトの構成
今回のプロジェクトではAmplify CLIで生成されたSwiftファイルの型定義をもとにAPIを実行しています。
また合わせて以下のライブラリも使っていました。
- AppSync SDK
- AWSMobileClient
- AWSPinpoint
- AWSUserPoolsSignIn
- AWSS3
マイグレーションのあれこれ
前提
元々はAppSync SDK, S3, Cognito用それぞれ別々のライブラリを入れる必要がありましたが、
Amplify SDKはそれらを全て統合したライブラリになっているので、Amplify SDK一つで完結します(というかそうしないと無理でした)
ライブラリは本体のAmplify、使いたいサービスごとに
- AWSCognitoAuthPlugin
- AWSAPIPlugin
- AWSS3StoragePlugin
- AWSPinpointPushNotificationsPlugin
などのライブラリが用意されているので、適宜入れていきます。
また後述するJSONファイルにもそれぞれを記載する必要があります。
認証
AppSync SDKを使う際の認証は、コード上でCognitoのPoolKeyやIDなどを記載して、Configurationを作成していたと思います。
let pool = AWSCognitoIdentityUserPool(forKey: userPoolKey)
let credentialsProvider = AWSCognitoCredentialsProvider(
regionType: RegionType,
identityPoolId: CredentialsProvider.identityPoolId,
identityProviderManager: pool
)
let config = AWSServiceConfiguration(region: RegionType, credentialsProvider: credentialsProvider)
AWSServiceManager.default()?.defaultServiceConfiguration = config
Amplify SDKの場合はJSONファイルに記載することになります。[1]
json
{
"auth": {
"plugins": {
"awsCognitoAuthPlugin": {
"IdentityManager": {
"Default": {}
},
"CredentialsProvider": {
"CognitoIdentity": {
"Default": {
"PoolId": "[COGNITO IDENTITY POOL ID]",
"Region": "[REGION]"
}
}
},
"CognitoUserPool": {
"Default": {
"PoolId": "[COGNITO USER POOL ID]",
"AppClientId": "[COGNITO USER POOL APP CLIENT ID]",
"Region": "[REGION]"
}
},
"Auth": {
"Default": {
"authenticationFlowType": "USER_SRP_AUTH",
"OAuth": {
"WebDomain": "[YOUR COGNITO DOMAIN ]",
"AppClientId": "[COGNITO USER POOL APP CLIENT ID]",
"SignInRedirectURI": "[CUSTOM REDIRECT SCHEME AFTER SIGN IN, e.g. myapp://]",
"SignOutRedirectURI": "[CUSTOM REDIRECT SCHEME AFTER SIGN OUT, e.g. myapp://]",
"Scopes": [
"phone",
"email",
"openid",
"profile",
"aws.cognito.signin.user.admin"
]
}
}
}
}
}
}
}
その後、Amplifyの初期化を行う際にJSONを渡します。
let path = Bundle.main.path(
forResource: "amplifyconfiguration",
ofType: "json"
)!
let configurationPath = URL(fileURLWithPath: path)
// Cognito用のプラグインを追加
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.configure(.init(configurationFile: configurationPath))
API(GraphQL)
Closureベースだったものが、ConcurrencyかCombineに対応したものに変更になりました。
ここら辺は機械的に置き換えていけるかと思います。
初期化の際にプラグインを追加しますが、AWSCognitoAuthPluginがない場合、初期化に失敗するので注意です。
// AppSync SDKの場合
let mutationInput = CreateTodoInput(name: "Use AppSync", description:"Realtime and Offline")
appSyncClient?.perform(mutation: CreateTodoMutation(input: mutationInput)) { (result, error) in
if let error = error as? AWSAppSyncClientError {
print("Error occurred: \(error.localizedDescription )")
}
if let resultError = result?.errors {
print("Error saving the item on server: \(resultError)")
return
}
}
// Amplify SDKの場合
// 初期化処理でプラグインを追加する
try Amplify.add(plugin: AWSAPIPlugin())
// Concurrencyを使った場合
let mutationInput = CreateTodoInput(name: "Use AWSAPIPlugin",
description: "Realtime and Offline")
let request = GraphQLRequest(document: CreateTodoMutation.operationString,
variables: CreateTodoMutation(input: mutationInput).variables?.jsonObject,
responseType: CreateTodoMutation.Data.self)
do {
let result = try await Amplify.API.mutate(request: request)
switch result {
case .success(let todo):
print("Successfully created todo: \(todo)")
case .failure(let error):
print("Got failed result with \(error.errorDescription)")
}
} catch let error as APIError {
print("Failed to update todo: ", error)
} catch {
print("Unexpected error: \(error)")
}
// Combineを使った場合
let mutationInput = CreateTodoInput(name: "Use AWSAPIPlugin",
description: "Realtime and Offline")
let request = GraphQLRequest(document: CreateTodoMutation.operationString,
variables: CreateTodoMutation(input: mutationInput).variables?.jsonObject,
responseType: CreateTodoMutation.Data.self)
let sink = Amplify.Publisher.create {
Amplify.API.mutate(request: request)
}
.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print("Unexpected error: \(error)")
}
}
receiveValue: { result in
switch result {
case .success(let todo):
print("Successfully created todo: \(todo)")
case .failure(let error):
print("Got failed result with \(error.errorDescription)")
}
}
.store(in: &cancellables)
S3
ここもConcurrency, Combineに対応したものに変更になりました。
// Amplify SDK
try Amplify.add(plugin: AWSS3StoragePlugin())
// Concurrencyを使った場合
let dataString = "My Data"
let data = Data(dataString.utf8)
let uploadTask = Amplify.Storage.uploadData(
path: .fromString("public/example/path"),
data: data
)
Task {
for await progress in await uploadTask.progress {
print("Progress: \(progress)")
}
}
let value = try await uploadTask.value
print("Completed: \(value)")
// Combineを使った場合
let dataString = "My Data"
let data = Data(dataString.utf8)
let uploadTask = Amplify.Storage.uploadData(
path: .fromString("public/example/path"),
data: data
)
let progressSink = uploadTask
.inProcessPublisher
.sink { progress in
print("Progress: \(progress)")
}
let resultSink = uploadTask
.resultPublisher
.sink {
if case let .failure(storageError) = $0 {
print("Failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
}
}
receiveValue: { data in
print("Completed: \(data)")
}
主な変更として、
旧S3ライブラリの場合はURLはオブジェクトURLが返却されていましたが、
Amplify SDKの場合はURLはStoragePathが返却されます。
Pinpoint
ドキュメントになかったのですが、Pinpointを使う場合は以下の項目をJSONに追加してください。
(ない場合プラグインを追加した際に初期化が止まります)
"pinpoint": {
"plugins": {
"awsPinpointPushNotificationsPlugin": {
"appId": "[PINPOINT APP ID]",
"region": "[REGION]"
}
}
}
移行に伴って発生した問題
1. Cognito周りのライブラリの競合
既存のコードではクライアントの認証にはCognitoを使っていため、AWSCognitoIdentityProviderを使っていました。
が、AWSCognitoAuthPluginの内部でもAWSCognitoIdentityProviderという名前の別ライブラリが使われているため、これらの競合が発生しました。[2]
結局以下のライブラリも全てAmplify SDKベースに置き換えることにしました😇[3]
- AWSPinpoint
- AWSUserPoolsSignIn
- AWSS3
2. Enumがキャストできない
クラインアントはAmplify CLIが生成したSwiftファイルの型定義を使っていますが、
その中にEnumがあった場合、キャストができずにクラッシュする問題が発生しました。
public var status: HogeStatus {
get {
return snapshot["status"]! as! HogeStatus // <- Could not cast value of type ‘__NSCFString’ to ‘HogeStatus’.になる
}
set {
snapshot.updateValue(newValue, forKey: "status")
}
}
吐き出されたSwiftファイル内でデコード処理が行われていますが、そこの修正が必要そうです。[4]
あんまり良くないですが、以下のようにして回避するようにしました。
extension GraphQLSelectionSet {
func getEnum<T: RawRepresentable>(key: String, as type: T.Type) -> T? where T.RawValue == String {
guard let value = snapshot[key] as? String else {
return nil
}
return T(rawValue: value)
}
}
終わり
Amplify SDKを使った場合、認証周りがうまく隠蔽されているのでAppSyncだけなくS3などを使う時のコードもだいぶスッキリしました。
さらにConcurrencyも使えるのでモダンになったのではないでしょうか。
Discussion