Skip to content
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

refactor: improve DittoService state management #171

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,48 @@
// Copyright © 2025 DittoLive Incorporated. All rights reserved.
//

import Combine
import DittoSwift

/// Represents the different states of the Ditto sync engine
public enum DittoSyncState: String {
case noLicense = "No license found."
case inactive = "Ditto is not running."
case active = "Ditto is active."
}

/// A service that manages the lifecycle of a Ditto instance, including initialization, and synchronization.
///
/// `DittoService` is designed as a singleton to provide a centralized interface for working with a Ditto instance
/// within an app. It allows for initializing Ditto with specific credentials, and managing its synchronization engine.
///
/// ## Features
/// - **Singleton Access**: Use `DittoService.shared` to access the single instance.
/// - **Sync Engine Management**: Start, stop, or restart the Ditto synchronization engine.
/// - **Identity Management**: Initialize Ditto with secure Credentials to manage offline license tokens.
/// - **Singleton Access**: Use `DittoService.shared` to access the single instance
/// - **State Management**: Track the Ditto sync engine state through the `syncState` property
/// - **Sync Engine Control**: Start, stop, or restart the Ditto synchronization engine
/// - **Identity Management**: Initialize Ditto with secure Credentials to manage offline license tokens
///
/// ## Usage:
/// ```swift
/// let dittoService = DittoService.shared
/// try? dittoService.initializeDitto(with: credentials)
/// dittoService.startSyncEngine()
/// try? dittoService.startSyncEngine()
///
/// // Observe sync state changes
/// if dittoService.syncState == .active {
/// print("Sync engine is running")
/// }
/// ```
///
/// - Note: This service is tightly coupled with the Ditto SDK and requires identity and license configuration.
///
/// ## Topics
/// ### Initialization
/// - `initializeDitto(with:useIsolatedDirectories:)`
/// - `destroyDittoInstance(clearConfig:)`
/// - `destroyDittoInstance(clearingCredentials:)`
///
/// ### State Management
/// - `syncState`: Current state of the Ditto sync engine
/// - `DittoSyncState`: Enum representing possible sync states
///
/// ### Synchronization
/// - `startSyncEngine()`
Expand All @@ -45,6 +61,9 @@ public class DittoService: ObservableObject {
/// Optional Ditto instance that can be initialized later
@Published public private(set) var ditto: Ditto?

/// Tracks the current state of the sync engine
@Published public private(set) var syncState: DittoSyncState = .noLicense

// MARK: - Singleton

/// Shared instance of the `DittoService`.
Expand Down Expand Up @@ -131,7 +150,6 @@ public class DittoService: ObservableObject {
/// - Parameter clearingCredentials: A Boolean value indicating whether the active credentials
/// should also be cleared. If `true`, the active credentials will be removed. Defaults to `false`.
func destroyDittoInstance(clearingCredentials: Bool = false) {

// Stop the sync engine if it is active
stopSyncEngine()

Expand All @@ -144,10 +162,25 @@ public class DittoService: ObservableObject {
CredentialsService.shared.activeCredentials = nil
}

updateSyncState()
print("Ditto instance destroyed successfully. Ditto = \(String(describing: ditto))")
}

// MARK: - Private Helper Methods
// MARK: - Private Methods

/// Updates the sync state by checking both activation and sync status of the Ditto instance
private func updateSyncState() {
guard let ditto else {
syncState = .noLicense
return
}

if ditto.activated {
syncState = ditto.isSyncActive ? .active : .inactive
} else {
syncState = .noLicense
}
}

/// Sets the offline license token on the Ditto instance if required by the identity type.
private func setOfflineLicenseTokenIfNeeded(for credentials: Credentials, on ditto: Ditto) throws {
Expand All @@ -171,27 +204,31 @@ public class DittoService: ObservableObject {
///
/// - Throws: `DittoServiceError` if the sync engine fails to start.
func startSyncEngine() throws {
guard let ditto = ditto else { throw DittoServiceError.noInstance }

guard let ditto else { throw DittoServiceError.noInstance }
ditto.delegate = self

do {
try ditto.startSync()
updateSyncState()
print("Ditto sync engine started successfully.")
} catch {
updateSyncState()
throw DittoServiceError.syncFailed(error.localizedDescription)
}
}

/// Stops the sync engine on the Ditto instance.
func stopSyncEngine() {
guard let ditto = ditto else { return }

if !ditto.isSyncActive {
guard let ditto else {
updateSyncState()
print("Ditto is not running.")
return
}

ditto.stopSync()
if ditto.isSyncActive {
ditto.stopSync()
}

updateSyncState()
print("Ditto sync engine stopped successfully.")
}

Expand Down
81 changes: 46 additions & 35 deletions DittoToolsApp/DittoToolsApp/Views/Menu View/SyncButton.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//
//
// SyncButton.swift
//
// Copyright © 2024 DittoLive Incorporated. All rights reserved.
Expand All @@ -7,60 +7,71 @@
import DittoSwift
import SwiftUI


struct SyncButton: View {
var dittoService: DittoService?
@ObservedObject var dittoService: DittoService

@State private var isAnimating = false
@State private var rotationAngle: Double = 0

var body: some View {
Button(action: {
if let dittoService, let ditto = dittoService.ditto {
if ditto.isSyncActive {
dittoService.stopSyncEngine()
isAnimating = false
rotationAngle = 0
} else {
try? ditto.startSync()
isAnimating = true
}
}
}) {
if let ditto = dittoService?.ditto, ditto.activated {
HStack(spacing: 12) {
Text(ditto.isSyncActive ? "Ditto is active." : "Ditto is not running.")
.font(.subheadline)

#if !os(tvOS)
Button(action: handleSyncButtonTapped) {
HStack(spacing: 12) {
Text(dittoService.syncState.rawValue)
.font(.subheadline)

#if !os(tvOS)
// The way focus is handled on tvOS can interfere with animation updates, so omit on tvOS.
Image(systemName: "arrow.triangle.2.circlepath")
.font(.caption)
.rotationEffect(.degrees(rotationAngle))
#endif
}
} else {
Text("No license found.")

// Only show the image if there is a licence (ie. the engine is active or paused)
if dittoService.syncState != .noLicense {
Image(systemName: "arrow.triangle.2.circlepath")
.font(.caption)
.rotationEffect(.degrees(rotationAngle))
}
#endif
}
}
.onAppear {
if let ditto = dittoService?.ditto {
isAnimating = ditto.isSyncActive
}
updateAnimationState()
}
.onChange(of: dittoService.syncState) { _ in
updateAnimationState()
}
.onChange(of: isAnimating) { rotating in
if rotating {
startRotation()
}
}
.disabled(dittoService?.ditto == nil)
.disabled(dittoService.syncState == .noLicense)
}


private func updateAnimationState() {
isAnimating = dittoService.syncState == .active
if !isAnimating {
rotationAngle = 0
}
}

private func handleSyncButtonTapped() {
switch dittoService.syncState {
case .active:
dittoService.stopSyncEngine()
isAnimating = false
rotationAngle = 0
case .inactive:
try? dittoService.startSyncEngine()
isAnimating = true
case .noLicense:
break // Can't do anything without a license
Copy link
Member

Choose a reason for hiding this comment

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

Can we show something in the UI or at least print a log indicating why we're not doing anything to help guide users towards activating properly?

}
}

private func startRotation() {
if isAnimating {
withAnimation(.linear(duration: 3.4).repeatForever(autoreverses: false)) {
rotationAngle = 360
}
}
}

}