💭

[iOS] [AWS] AppSync SDKからAmplify SDKへのマイグレーションガイド

2025/03/04に公開

初めに

自分が参加しているプロジェクトで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も使えるのでモダンになったのではないでしょうか。

参考

Amplify SDKドキュメント

脚注
  1. 正確にはAmplifyの初期化の際に渡すことはできますが、可読性的にJSONファイルに記載したほうが良いと思います。 ↩︎

  2. エラーは出ないがビルドが通らない状態になるので苦労しました。 ↩︎

  3. 多分それが正しい方法 ↩︎

  4. Amplify SDKにIssueを作成しました。自分で修正したい。 ↩︎

Discussion