Skip to content

(DOCSP-26313): Swift: Add a Handle Sync Errors page for SwiftUI #2524

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion examples/ios/RealmExamples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@
917E73E726B8A9F80068242A /* MongoDBRemoteAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 917E73E526B8A9F80068242A /* MongoDBRemoteAccess.swift */; };
917E73E826B8A9F80068242A /* MongoDBRemoteAccessAggregate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 917E73E626B8A9F80068242A /* MongoDBRemoteAccessAggregate.swift */; };
9185DB1925E82C0300AF1389 /* LocalOnlyCompleteQuickStart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9185DB1825E82C0300AF1389 /* LocalOnlyCompleteQuickStart.swift */; };
918F1FD62BF7C32E00F43489 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 918F1FD52BF7C32E00F43489 /* RealmSwift */; };
918F1FD82BF7C32E00F43489 /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 918F1FD52BF7C32E00F43489 /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
918F1FDA2BF7C3D100F43489 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 918F1FD92BF7C3D100F43489 /* RealmSwift */; };
918F1FDC2BF7C3D100F43489 /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 918F1FD92BF7C3D100F43489 /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
91AB031428981BF700A272E8 /* DeleteRealmFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AB031328981BF700A272E8 /* DeleteRealmFiles.swift */; };
91AB0316289965F800A272E8 /* CreateRealmObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AB0315289965F800A272E8 /* CreateRealmObjects.swift */; };
91AB03182899660A00A272E8 /* ReadRealmObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AB03172899660A00A272E8 /* ReadRealmObjects.swift */; };
Expand All @@ -110,6 +114,7 @@
91AB031E289B1E9700A272E8 /* AddSyncToApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AB031D289B1E9700A272E8 /* AddSyncToApp.swift */; };
91AE237728AFC7A40027C3AC /* SyncOrLocalRealm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AE237628AFC7A40027C3AC /* SyncOrLocalRealm.swift */; };
91AE237828AFC7A40027C3AC /* SyncOrLocalRealm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AE237628AFC7A40027C3AC /* SyncOrLocalRealm.swift */; };
91B4EBA32982CC4F0015FD7B /* HandleSyncErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B4EBA22982CC4F0015FD7B /* HandleSyncErrors.swift */; };
91B8A4D6278E24950018F72F /* TypeProjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B8A4D5278E24950018F72F /* TypeProjection.swift */; };
91CEF5FE260E308100A029E0 /* Compacting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91CEF5FD260E308100A029E0 /* Compacting.swift */; };
91CEF606260E325000A029E0 /* Compacting.m in Sources */ = {isa = PBXBuildFile; fileRef = 91CEF605260E325000A029E0 /* Compacting.m */; };
Expand Down Expand Up @@ -139,6 +144,28 @@
/* End PBXContainerItemProxy section */

/* Begin PBXCopyFilesBuildPhase section */
918F1FD72BF7C32E00F43489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
918F1FD82BF7C32E00F43489 /* RealmSwift in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
918F1FDB2BF7C3D100F43489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
918F1FDC2BF7C3D100F43489 /* RealmSwift in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
91F04E9B2BE290A80082EB2B /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
Expand Down Expand Up @@ -248,6 +275,7 @@
91AB031B2899663600A272E8 /* DeleteRealmObjects.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteRealmObjects.swift; sourceTree = "<group>"; };
91AB031D289B1E9700A272E8 /* AddSyncToApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSyncToApp.swift; sourceTree = "<group>"; };
91AE237628AFC7A40027C3AC /* SyncOrLocalRealm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncOrLocalRealm.swift; sourceTree = "<group>"; };
91B4EBA22982CC4F0015FD7B /* HandleSyncErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleSyncErrors.swift; sourceTree = "<group>"; };
91B8A4D5278E24950018F72F /* TypeProjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeProjection.swift; sourceTree = "<group>"; };
91C68804274D8AFE001A5DBE /* SwiftUIExamples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUIExamples.app; sourceTree = BUILT_PRODUCTS_DIR; };
91CEF5FD260E308100A029E0 /* Compacting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compacting.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -291,13 +319,15 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
918F1FD62BF7C32E00F43489 /* RealmSwift in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
91713B0F28AC3D8300519F9D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
918F1FDA2BF7C3D100F43489 /* RealmSwift in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -501,6 +531,7 @@
91713B3028AC41B700519F9D /* Authenticate.swift */,
915B8EE429258B4300150F01 /* CreateObjects.swift */,
916E7D4528B57B6B00758A85 /* FilterData.swift */,
91B4EBA22982CC4F0015FD7B /* HandleSyncErrors.swift */,
91713B2428AC3DF300519F9D /* OpenRealm.swift */,
91713B3428AD2B0E00519F9D /* PassObjectsToView.swift */,
916E7D4228B5693100758A85 /* Previews.swift */,
Expand Down Expand Up @@ -594,13 +625,15 @@
91713AF528AC3D8200519F9D /* Sources */,
91713AF628AC3D8200519F9D /* Frameworks */,
91713AF728AC3D8200519F9D /* Resources */,
918F1FD72BF7C32E00F43489 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = SwiftUICatalog;
packageProductDependencies = (
918F1FD52BF7C32E00F43489 /* RealmSwift */,
);
productName = SwiftUICatalog;
productReference = 91713AF928AC3D8200519F9D /* SwiftUICatalog.app */;
Expand All @@ -613,6 +646,7 @@
91713B0E28AC3D8300519F9D /* Sources */,
91713B0F28AC3D8300519F9D /* Frameworks */,
91713B1028AC3D8300519F9D /* Resources */,
918F1FDB2BF7C3D100F43489 /* Embed Frameworks */,
);
buildRules = (
);
Expand All @@ -621,6 +655,7 @@
);
name = SwiftUICatalogUITests;
packageProductDependencies = (
918F1FD92BF7C3D100F43489 /* RealmSwift */,
);
productName = SwiftUICatalogUITests;
productReference = 91713B1228AC3D8300519F9D /* SwiftUICatalogUITests.xctest */;
Expand Down Expand Up @@ -888,6 +923,7 @@
91713AFC28AC3D8200519F9D /* SwiftUICatalogApp.swift in Sources */,
916E7D4928B5863A00758A85 /* QuickWrite.swift in Sources */,
91713B3528AD2B0E00519F9D /* PassObjectsToView.swift in Sources */,
91B4EBA32982CC4F0015FD7B /* HandleSyncErrors.swift in Sources */,
91713B2528AC3DF300519F9D /* OpenRealm.swift in Sources */,
916E7D4328B5693100758A85 /* Previews.swift in Sources */,
915B8EE529258B4300150F01 /* CreateObjects.swift in Sources */,
Expand Down Expand Up @@ -1464,7 +1500,7 @@
repositoryURL = "https://github.com/realm/realm-swift.git";
requirement = {
kind = exactVersion;
version = 10.50.0;
version = 10.49.3;
};
};
917CA79427ECADC200F9BDDC /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */ = {
Expand Down Expand Up @@ -1508,6 +1544,16 @@
package = 917CA79427ECADC200F9BDDC /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */;
productName = FacebookLogin;
};
918F1FD52BF7C32E00F43489 /* RealmSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 91508B752BE57EFA00817DBC /* XCRemoteSwiftPackageReference "realm-swift" */;
productName = RealmSwift;
};
918F1FD92BF7C3D100F43489 /* RealmSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 91508B752BE57EFA00817DBC /* XCRemoteSwiftPackageReference "realm-swift" */;
productName = RealmSwift;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 48DBFACE25101C3100391E2B /* Project object */;
Expand Down
56 changes: 56 additions & 0 deletions examples/ios/SwiftUICatalog/Views/Authenticate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,62 @@ struct FlexibleSyncContentView: View {
}
// :snippet-end:

// :snippet-start: flexible-sync-view-with-client-reset-handling
struct FlexSyncContentView: View {
// Observe the Realm App object in order to react to login state changes.
@ObservedObject var flexibleSyncApp: RealmSwift.App
// Use the error handler that you've injected into the environment
// to react to Device Sync errors.
// :uncomment-start:
//@EnvironmentObject var errorHandler: ErrorHandler
// :uncomment-end:

var body: some View {
if let user = flexibleSyncApp.currentUser {
let config = user.flexibleSyncConfiguration(
// :emphasize-start:
clientResetMode: .recoverUnsyncedChanges(
beforeReset: { realm in
// A block called after a client reset error is detected, but before the
// client recovery process is executed.
// This block could be used for any custom logic, reporting, debugging etc.
// For more information, refer to: https://www.mongodb.com/docs/realm/sdk/swift/sync/handle-sync-errors/
print("Before client reset block")
}, afterReset: { before,after in
// A block called after the client recovery process has executed.
// This block could be used for custom recovery, reporting, debugging etc.
// For SwiftUI, you might modify a @State variable to trigger views to reload
// or advise the user to restart the app.
// For more information, refer to: https://www.mongodb.com/docs/realm/sdk/swift/sync/handle-sync-errors/
print("After client reset block")
}),
// :emphasize-end:
initialSubscriptions: { subs in
let peopleSubscriptionExists = subs.first(named: "people")
let dogSubscriptionExists = subs.first(named: "dogs")
// Check whether the subscription already exists. Adding it more
// than once causes an error.
if (peopleSubscriptionExists != nil) && (dogSubscriptionExists != nil) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not needed to specified != than nil, you can

if peopleSubsriptionExist && dogSubscriptionExist {

or

if let peopleSubsriptionExist = subs.first(named: "people"), let dogSubscriptionExist = subs.first(named: "dogs") {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tried updating this syntax, but I get an error message:

Optional type 'SyncSubscription?' cannot be used as a boolean; test for '!= nil' instead

That's why I originally had the != nil in the example. This seemed like the easiest way to bypass the optional unwrapping.

// Existing subscriptions found - do nothing
return
} else {
// Add queries for any objects you want to use in the app.
// Linked objects do not automatically get queried, so you
// must explicitly query for all linked objects you want to include.
subs.append(QuerySubscription<SwiftUI_Person>(name: "people"))
subs.append(QuerySubscription<SwiftUI_Dog>(name: "dogs"))
}
}
)
OpenFlexibleSyncRealmView()
.environment(\.realmConfiguration, config)
} else {
FlexibleSyncLoginView()
}
}
}
// :snippet-end:

struct PBSContentView: View {
// Observe the Realm app object in order to react to login state changes.
@ObservedObject var partitionBasedSyncApp: RealmSwift.App
Expand Down
72 changes: 72 additions & 0 deletions examples/ios/SwiftUICatalog/Views/HandleSyncErrors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import SwiftUI
import RealmSwift
import Foundation

// :snippet-start: swiftui-app-with-error-handler
let app = App(id: flexibleSyncAppId)

// :uncomment-start:
// @main
// :uncomment-end:
struct realmSwiftUIApp: SwiftUI.App {
// Initialize the error handler
@StateObject var errorHandler = ErrorHandler(app: app)

var body: some Scene {
WindowGroup {
NextView(app: app)
// Inject the error handler as an environment object
.environmentObject(errorHandler)
// Display an alert to the user containing the error when a Sync error occurs
.alert(Text("Error"), isPresented: .constant(errorHandler.error != nil)) {
Button("OK", role: .cancel) { errorHandler.error = nil }
} message: {
Text(errorHandler.error?.localizedDescription ?? "")
}
}
}
}
// :snippet-end:

// :snippet-start: swiftui-error-handler
final class ErrorHandler: ObservableObject {
@Published var error: Swift.Error?

init(app: RealmSwift.App) {
// Sync Manager listens for sync errors.
app.syncManager.errorHandler = { error, syncSession in
if let error = error as? SyncError {
/* Handle specific SyncError cases, or use a switch
* statement to handle all Sync error codes.
* In this case, ignore a .connectionFailed error and
* continue executing the app code. */
if error.code == .connectionFailed {
return
}
self.error = error
} else if let error = error as? POSIXError {
/* The error handler may also report NSError types to
* allow for error handling in a platform-idiomatic way.
* In this case, handle a connection timeout error as
* an .ETIMEDOUT error in the POSIXError domain. */
if error.code == .ETIMEDOUT {
return
}
self.error = error
}
}
}
}
// :snippet-end:

// :snippet-start: use-app-and-error-handler-in-next-view
struct NextView: View {
@ObservedObject var app: RealmSwift.App
// Use the error handler that you injected into the environment
@EnvironmentObject var errorHandler: ErrorHandler

var body: some View {
Text("You might log users in or handle errors in this view")
}
}
// :snippet-end:
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.. code-block:: swift
:emphasize-lines: 11-25

struct FlexSyncContentView: View {
// Observe the Realm App object in order to react to login state changes.
@ObservedObject var app: RealmSwift.App
// Use the error handler that you've injected into the environment
// to react to Device Sync errors.
@EnvironmentObject var errorHandler: ErrorHandler

var body: some View {
if let user = app.currentUser {
let config = user.flexibleSyncConfiguration(
clientResetMode: .recoverUnsyncedChanges(
beforeReset: { realm in
// A block called after a client reset error is detected, but before the
// client recovery process is executed.
// This block could be used for any custom logic, reporting, debugging etc.
// For more information, refer to: https://www.mongodb.com/docs/realm/sdk/swift/sync/handle-sync-errors/
print("Before client reset block")
}, afterReset: { before,after in
// A block called after the client recovery process has executed.
// This block could be used for custom recovery, reporting, debugging etc.
// For SwiftUI, you might modify a @State variable to trigger views to reload
// or advise the user to restart the app.
// For more information, refer to: https://www.mongodb.com/docs/realm/sdk/swift/sync/handle-sync-errors/
print("After client reset block")
}),
initialSubscriptions: { subs in
let peopleSubscriptionExists = subs.first(named: "people")
let dogSubscriptionExists = subs.first(named: "dogs")
// Check whether the subscription already exists. Adding it more
// than once causes an error.
if (peopleSubscriptionExists != nil) && (dogSubscriptionExists != nil) {
// Existing subscriptions found - do nothing
return
} else {
// Add queries for any objects you want to use in the app.
// Linked objects do not automatically get queried, so you
// must explicitly query for all linked objects you want to include.
subs.append(QuerySubscription<Person>(name: "people"))
subs.append(QuerySubscription<Dog>(name: "dogs"))
}
}
)
OpenFlexibleSyncRealmView()
.environment(\.realmConfiguration, config)
} else {
LoginView()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
let app = App(id: flexibleSyncAppId)

@main
struct realmSwiftUIApp: SwiftUI.App {
// Initialize the error handler
@StateObject var errorHandler = ErrorHandler(app: app)

var body: some Scene {
WindowGroup {
NextView(app: app)
// Inject the error handler as an environment object
.environmentObject(errorHandler)
// Display an alert to the user containing the error when a Sync error occurs
.alert(Text("Error"), isPresented: .constant(errorHandler.error != nil)) {
Button("OK", role: .cancel) { errorHandler.error = nil }
} message: {
Text(errorHandler.error?.localizedDescription ?? "")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
final class ErrorHandler: ObservableObject {
@Published var error: Swift.Error?

init(app: RealmSwift.App) {
// Sync Manager listens for sync errors.
app.syncManager.errorHandler = { error, syncSession in
if let error = error as? SyncError {
/* Handle specific SyncError cases, or use a switch
* statement to handle all Sync error codes.
* In this case, ignore a .connectionFailed error and
* continue executing the app code. */
if error.code == .connectionFailed {
return
}
self.error = error
} else if let error = error as? POSIXError {
/* The error handler may also report NSError types to
* allow for error handling in a platform-idiomatic way.
* In this case, handle a connection timeout error as
* an .ETIMEDOUT error in the POSIXError domain. */
if error.code == .ETIMEDOUT {
return
}
self.error = error
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
struct NextView: View {
@ObservedObject var app: RealmSwift.App
// Use the error handler that you injected into the environment
@EnvironmentObject var errorHandler: ErrorHandler

var body: some View {
Text("You might log users in or handle errors in this view")
}
}
Loading
Loading