Name:
interface
Value:
Amplify has re-imagined the way frontend developers build fullstack applications. Develop and deploy without the hassle.
Gen1 DocsLegacy

Page updated Mar 26, 2026

オプティミスティックUI

Amplify DataでオプティミスティックUIを実装することで、CRUD操作がリクエストのラウンドトリップが完了する前にUIに即座にレンダリングされ、APIコールが失敗した場合はUIの変更をロールバックできます。

以下の例では、新しく作成されたアイテム、更新、削除をオプティミスティックにレンダリングするリストビューを作成します。Dataスキーマを修正して、この「Real Estate Property」の例を使用します:

amplify/data/resource.ts
const schema = a.schema({
RealEstateProperty: a.model({
name: a.string().required(),
address: a.string(),
}).authorization(allow => [allow.guest()])
})
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'iam',
},
});

ファイルを保存し、npx ampx sandboxを実行してバックエンドのクラウドサンドボックスに変更をデプロイします。このガイドでは、Real Estate Propertyリスティングアプリケーションを構築します。

バックエンドがプロビジョニングされたら、npx ampx generate graphql-client-code --format modelgen --model-target swift --out <path_to_swift_project>/AmplifyModelsを実行してアプリ用のSwiftモデル型を生成します。

次に、Amplifyパッケージ(https://github.com/aws-amplify/amplify-swift.git)をXcodeプロジェクトに追加し、プロンプトが表示されたときに次のモジュールを選択してインポートします:

  • AWSAPIPlugin
  • AWSCognitoAuthPlugin
  • AWSS3StoragePlugin
  • Amplify

完全な動作例については、下の完全な例を参照してください。

Swiftアクターを使用してオプティミスティックUI更新を実行する方法

Swiftアクターは、基になるプロパティへのアクセスをシリアライズします。この例では、アクターがアイテムのリストを保持し、リストにアクセスされるたびにCombineパブリッシャーを通じてUIに公開されます。高レベルでは、アクターのメソッドは以下を実行します:

  • 新しいモデルを作成し、リストに追加し、APIリクエストが失敗した場合は新しく追加されたアイテムをリストから削除
  • 既存のモデルをリストで更新し、APIリクエストが失敗した場合は更新をリストでリバート
  • リストから既存のモデルを削除し、APIリクエストが失敗した場合はアイテムをリストに戻す

アクターオブジェクトを通じてこれらのメソッドを提供することで、基になるリストはシリアルでアクセスされるため、必要に応じて操作全体をロールバックできます。

オプティミスティックUI更新を可能にするアクターオブジェクトを作成するには、新しいファイルを作成して次のコードを追加します。

import Amplify
import SwiftUI
import Combine
actor RealEstatePropertyList {
private var properties: [RealEstateProperty?] = [] {
didSet {
subject.send(properties.compactMap { $0 })
}
}
private let subject = PassthroughSubject<[RealEstateProperty], Never>()
var publisher: AnyPublisher<[RealEstateProperty], Never> {
subject.eraseToAnyPublisher()
}
func listProperties() async throws {
let result = try await Amplify.API.query(request: .list(RealEstateProperty.self))
guard case .success(let propertyList) = result else {
print("Failed with error: ", result)
return
}
properties = propertyList.elements
}
}

listProperties()メソッドを呼び出すと、Amplify Data APIでクエリが実行され、結果がpropertiesプロパティに保存されます。このプロパティが設定されると、リストがサブスクライバーに送信されます。UIで、ビューモデルを作成して更新をサブスクライブします:

class RealEstatePropertyContainerViewModel: ObservableObject {
@Published var properties: [RealEstateProperty] = []
var sink: AnyCancellable?
var propertyList = RealEstatePropertyList()
init() {
Task {
sink = await propertyList.publisher
.receive(on: DispatchQueue.main)
.sink { properties in
print("Updating property list")
self.properties = properties
}
}
}
func loadList() {
Task {
try? await propertyList.listProperties()
}
}
}
struct RealEstatePropertyContainerView: View {
@StateObject var vm = RealEstatePropertyContainerViewModel()
@State private var propertyName: String = ""
var body: some View {
Text("Hello")
}
}

新しく作成されたレコードをオプティミスティックにレンダリングする

Amplify Data APIから返された新しく作成されたレコードをオプティミスティックにレンダリングするには、actor RealEstatePropertyListにメソッドを追加します:

func createProperty(name: String, address: String? = nil) {
let property = RealEstateProperty(name: name, address: address)
// オプティミスティックに新しく作成されたプロパティを送信し、UIでレンダリング
properties.append(property)
Task {
do {
// プロパティレコードを作成
let result = try await Amplify.API.mutate(request: .create(property))
guard case .failure(let graphQLResponse) = result else {
return
}
print("Failed with error: ", graphQLResponse)
// 新しく作成されたプロパティを削除
if let index = properties.firstIndex(where: { $0?.id == property.id }) {
properties.remove(at: index)
}
} catch {
print("Failed with error: ", error)
// 新しく作成されたプロパティを削除
if let index = properties.firstIndex(where: { $0?.id == property.id }) {
properties.remove(at: index)
}
}
}
}

レコード更新をオプティミスティックにレンダリングする

単一のアイテムの更新をオプティミスティックにレンダリングするには、以下のようなコードスニペットを使用します:

func updateProperty(_ property: RealEstateProperty) async {
guard let index = properties.firstIndex(where: { $0?.id == property.id }) else {
print("No property to update")
return
}
// オプティミスティックにプロパティを更新し、UIでレンダリング
let rollbackProperty = properties[index]
properties[index] = property
do {
// プロパティレコードを更新
let result = try await Amplify.API.mutate(request: .update(property))
guard case .failure(let graphQLResponse) = result else {
return
}
print("Failed with error: ", graphQLResponse)
properties[index] = rollbackProperty
} catch {
print("Failed with error: ", error)
properties[index] = rollbackProperty
}
}

レコード削除をオプティミスティックにレンダリングする

Amplify Data API削除をオプティミスティックにレンダリングするには、以下のようなコードスニペットを使用します:

func deleteProperty(_ property: RealEstateProperty) async {
guard let index = properties.firstIndex(where: { $0?.id == property.id }) else {
print("No property to remove")
return
}
// オプティミスティックにプロパティを削除し、UIでレンダリング
let rollbackProperty = properties[index]
properties[index] = nil
do {
// プロパティレコードを削除
let result = try await Amplify.API.mutate(request: .delete(property))
switch result {
case .success:
// 削除を確定
properties.remove(at: index)
case .failure(let graphQLResponse):
print("Failed with error: ", graphQLResponse)
// 削除を取り消し
properties[index] = rollbackProperty
}
} catch {
print("Failed with error: ", error)
// 削除を取り消し
properties[index] = rollbackProperty
}
}

完全な例

import SwiftUI
import Amplify
import AWSAPIPlugin
@main
struct OptimisticUIApp: App {
init() {
do {
Amplify.Logging.logLevel = .verbose
try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))
try Amplify.configure(with: .amplifyOutputs)
print("Amplify configured with API, Storage, and Auth plugins!")
} catch {
print("Failed to initialize Amplify with \(error)")
}
}
var body: some Scene {
WindowGroup {
RealEstatePropertyContainerView()
}
}
}
// モデルを拡張してSwiftUIの`ForEach`と互換性を持たせるためにIdentifiableに対応させる。
extension RealEstateProperty: Identifiable { }
struct TappedButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(10)
.background(configuration.isPressed ? Color.teal.opacity(0.8) : Color.teal)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
actor RealEstatePropertyList {
private var properties: [RealEstateProperty?] = [] {
didSet {
subject.send(properties.compactMap { $0 })
}
}
private let subject = PassthroughSubject<[RealEstateProperty], Never>()
var publisher: AnyPublisher<[RealEstateProperty], Never> {
subject.eraseToAnyPublisher()
}
func listProperties() async throws {
let result = try await Amplify.API.query(request: .list(RealEstateProperty.self))
guard case .success(let propertyList) = result else {
print("Failed with error: ", result)
return
}
properties = propertyList.elements
}
func createProperty(name: String, address: String? = nil) {
let property = RealEstateProperty(name: name, address: address)
// オプティミスティックに新しく作成されたプロパティを送信し、UIでレンダリング
properties.append(property)
Task {
do {
// プロパティレコードを作成
let result = try await Amplify.API.mutate(request: .create(property))
guard case .failure(let graphQLResponse) = result else {
return
}
print("Failed with error: ", graphQLResponse)
// 新しく作成されたプロパティを削除
if let index = properties.firstIndex(where: { $0?.id == property.id }) {
properties.remove(at: index)
}
} catch {
print("Failed with error: ", error)
// 新しく作成されたプロパティを削除
if let index = properties.firstIndex(where: { $0?.id == property.id }) {
properties.remove(at: index)
}
}
}
}
func updateProperty(_ property: RealEstateProperty) async {
guard let index = properties.firstIndex(where: { $0?.id == property.id }) else {
print("No property to update")
return
}
// オプティミスティックにプロパティを更新し、UIでレンダリング
let rollbackProperty = properties[index]
properties[index] = property
do {
// プロパティレコードを更新
let result = try await Amplify.API.mutate(request: .update(property))
guard case .failure(let graphQLResponse) = result else {
return
}
print("Failed with error: ", graphQLResponse)
properties[index] = rollbackProperty
} catch {
print("Failed with error: ", error)
properties[index] = rollbackProperty
}
}
func deleteProperty(_ property: RealEstateProperty) async {
guard let index = properties.firstIndex(where: { $0?.id == property.id }) else {
print("No property to remove")
return
}
// オプティミスティックにプロパティを削除し、UIでレンダリング
let rollbackProperty = properties[index]
properties[index] = nil
do {
// プロパティレコードを削除
let result = try await Amplify.API.mutate(request: .delete(property))
switch result {
case .success:
// 削除を確定
properties.remove(at: index)
case .failure(let graphQLResponse):
print("Failed with error: ", graphQLResponse)
// 削除を取り消し
properties[index] = rollbackProperty
}
} catch {
print("Failed with error: ", error)
// 削除を取り消し
properties[index] = rollbackProperty
}
}
}
class RealEstatePropertyContainerViewModel: ObservableObject {
@Published var properties: [RealEstateProperty] = []
var sink: AnyCancellable?
var propertyList = RealEstatePropertyList()
init() {
Task {
sink = await propertyList.publisher
.receive(on: DispatchQueue.main)
.sink { properties in
print("Updating property list")
self.properties = properties
}
}
}
func loadList() {
Task {
try? await propertyList.listProperties()
}
}
func createPropertyButtonTapped(name: String) {
Task {
await propertyList.createProperty(name: name)
}
}
func updatePropertyButtonTapped(_ property: RealEstateProperty) {
Task {
await propertyList.updateProperty(property)
}
}
func deletePropertyButtonTapped(_ property: RealEstateProperty) {
Task {
await propertyList.deleteProperty(property)
}
}
}
struct RealEstatePropertyContainerView: View {
@StateObject var viewModel = RealEstatePropertyContainerViewModel()
@State private var propertyName: String = ""
var body: some View {
VStack {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach($viewModel.properties) { $property in
HStack {
TextField("Update property name", text: $property.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.multilineTextAlignment(.center)
Button("Update") {
viewModel.updatePropertyButtonTapped(property)
}
Button {
viewModel.deletePropertyButtonTapped(property)
} label: {
Image(systemName: "xmark")
.foregroundColor(.red)
}
}.padding(.horizontal)
}
}
}.refreshable {
viewModel.loadList()
}
TextField("New property name", text: $propertyName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.multilineTextAlignment(.center)
Button("Save") {
viewModel.createPropertyButtonTapped(name: propertyName)
self.propertyName = ""
}
.buttonStyle(TappedButtonStyle())
}.task {
viewModel.loadList()
}
}
}
struct RealEstatePropertyContainerView_Previews: PreviewProvider {
static var previews: some View {
RealEstatePropertyContainerView()
}
}