Skip to content

Commit

Permalink
Implement iCloud sync
Browse files Browse the repository at this point in the history
  • Loading branch information
prof18 committed Jul 29, 2024
1 parent 570a17e commit 3cfd3fd
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 6 deletions.
18 changes: 18 additions & 0 deletions iosApp/FeedFlow.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.prof18.feedflow</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudDocuments</string>
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>iCloud.com.prof18.feedflow</string>
</array>
</dict>
</plist>
4 changes: 4 additions & 0 deletions iosApp/FeedFlow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
CCA704D029D5A9B600560FA3 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
CCA704E429D625E200560FA3 /* HomeSheetToShow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSheetToShow.swift; sourceTree = "<group>"; };
CCA704E629D626D400560FA3 /* FilePickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePickerController.swift; sourceTree = "<group>"; };
CCD494BA2C56B38500C54C8D /* FeedFlow.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FeedFlow.entitlements; sourceTree = "<group>"; };
CCEA11BA2A6D2AC300C661AC /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
CCEB9C5729B33F5A0064D14A /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = "<group>"; };
CCF21A6D2BD32AB60017F663 /* CommonViewRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonViewRoute.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -231,6 +232,7 @@
7555FF72242A565900829871 = {
isa = PBXGroup;
children = (
CCD494BA2C56B38500C54C8D /* FeedFlow.entitlements */,
CC21E74C2BED7F980001F160 /* PrivacyInfo.xcprivacy */,
CCA704CD29D5A83D00560FA3 /* Assets */,
CC23292D29D62FE2005D0E06 /* Kotlin */,
Expand Down Expand Up @@ -953,6 +955,7 @@
baseConfigurationReference = CC0799452C29BC5F00067E06 /* Config.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDebug;
CODE_SIGN_ENTITLEMENTS = FeedFlow.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
Expand Down Expand Up @@ -989,6 +992,7 @@
baseConfigurationReference = CC0799452C29BC5F00067E06 /* Config.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = FeedFlow.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
Expand Down
12 changes: 12 additions & 0 deletions iosApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,17 @@
</array>
<key>DropboxApiKey</key>
<string>$(DROPBOX_API_KEY)</string>
<key>NSUbiquitousContainers</key>
<dict>
<key>iCloud.com.prof18.feedflow</key>
<dict>
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
<true/>
<key>NSUbiquitousContainerName</key>
<string>FeedFlow</string>
<key>NSUbiquitousContainerSupportedFolderLevels</key>
<string>Any</string>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ internal actual fun getPlatformModule(appEnvironment: AppEnvironment): Module =
dropboxSettings = get(),
settingsRepository = get(),
accountsRepository = get(),
iCloudSettings = get(),
)
}

Expand All @@ -124,6 +125,7 @@ internal actual fun getPlatformModule(appEnvironment: AppEnvironment): Module =
dateFormatter = get(),
accountsRepository = get(),
feedSyncMessageQueue = get(),
feedSyncRepository = get(),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.prof18.feedflow.feedsync.dropbox.DropboxDownloadParam
import com.prof18.feedflow.feedsync.dropbox.DropboxSettings
import com.prof18.feedflow.feedsync.dropbox.DropboxStringCredentials
import com.prof18.feedflow.feedsync.dropbox.DropboxUploadParam
import com.prof18.feedflow.feedsync.icloud.ICloudSettings
import com.prof18.feedflow.shared.domain.model.SyncResult
import com.prof18.feedflow.shared.domain.settings.SettingsRepository
import kotlinx.cinterop.BetaInteropApi
Expand All @@ -28,6 +29,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import platform.Foundation.NSApplicationSupportDirectory
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSError
import platform.Foundation.NSFileManager
import platform.Foundation.NSFileManagerItemReplacementUsingNewMetadataOnly
Expand All @@ -45,6 +47,7 @@ internal class FeedSyncIosWorker(
private val dropboxSettings: DropboxSettings,
private val settingsRepository: SettingsRepository,
private val accountsRepository: AccountsRepository,
private val iCloudSettings: ICloudSettings,
) : FeedSyncWorker {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

Expand Down Expand Up @@ -185,6 +188,7 @@ internal class FeedSyncIosWorker(
return false
}

@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
private suspend fun accountSpecificUpload(databasePath: NSURL) =
when (accountsRepository.getCurrentSyncAccount()) {
SyncAccounts.DROPBOX -> {
Expand All @@ -199,8 +203,31 @@ internal class FeedSyncIosWorker(
}

SyncAccounts.ICLOUD -> {
// TODO
logger.d { "TODO: Upload to iCloud" }
val iCloudUrl = getICloudFolderURL()
if (iCloudUrl != null) {
memScoped {
val errorPtr: ObjCObjectVar<NSError?> = alloc()

// Copy doesn't override the item, so we need to clear it before.
// An alternative would be checking the existence of the file before and copy or replace.
NSFileManager.defaultManager.removeItemAtURL(
iCloudUrl,
null,
)

NSFileManager.defaultManager.copyItemAtURL(
srcURL = databasePath,
toURL = iCloudUrl,
error = errorPtr.ptr,
)

if (errorPtr.value != null) {
logger.e { "Error uploading to iCloud: ${errorPtr.value}" }
}
}
}
iCloudSettings.setLastUploadTimestamp(Clock.System.now().toEpochMilliseconds())
logger.d { "Upload to iCloud successfully" }
}

SyncAccounts.LOCAL -> {
Expand Down Expand Up @@ -229,9 +256,7 @@ internal class FeedSyncIosWorker(
}

SyncAccounts.ICLOUD -> {
// TODO
logger.d { "TODO: Download from iCloud" }
SyncResult.Success
return iCloudDownload()
}

SyncAccounts.LOCAL -> {
Expand All @@ -240,4 +265,53 @@ internal class FeedSyncIosWorker(
}
}
}

@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
private fun iCloudDownload(): SyncResult {
val iCloudUrl = getICloudFolderURL()
val tempUrl = getTemporaryFileUrl()
if (iCloudUrl == null || tempUrl == null) {
logger.e { "Error downloading database" }
return SyncResult.Error
}
NSFileManager.defaultManager.removeItemAtURL(
tempUrl,
null,
)

memScoped {
val errorPtr: ObjCObjectVar<NSError?> = alloc()

NSFileManager.defaultManager.copyItemAtURL(
srcURL = iCloudUrl,
toURL = tempUrl,
error = errorPtr.ptr,
)

if (errorPtr.value != null) {
logger.e { "Error downloading from iCloud: ${errorPtr.value}" }
return SyncResult.Error
}

replaceDatabase(tempUrl)
iCloudSettings.setLastDownloadTimestamp(Clock.System.now().toEpochMilliseconds())
logger.d { "Download from iCloud successfully" }
return SyncResult.Success
}
}

private fun getICloudFolderURL(): NSURL? = NSFileManager.defaultManager
.URLForUbiquityContainerIdentifier("iCloud.com.prof18.feedflow")
?.URLByAppendingPathComponent("Documents")
?.URLByAppendingPathComponent(getDatabaseName())

private fun getTemporaryFileUrl(): NSURL? {
val documentsDirectory: NSURL? = NSFileManager.defaultManager.URLsForDirectory(
directory = NSDocumentDirectory,
inDomains = NSUserDomainMask,
).firstOrNull() as? NSURL?
val databaseUrl = documentsDirectory?.URLByAppendingPathComponent(getDatabaseName())

return databaseUrl
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import com.prof18.feedflow.feedsync.icloud.ICloudSettings
import com.prof18.feedflow.shared.domain.DateFormatter
import com.prof18.feedflow.shared.domain.feedsync.AccountsRepository
import com.prof18.feedflow.shared.domain.feedsync.FeedSyncMessageQueue
import com.prof18.feedflow.shared.domain.feedsync.FeedSyncRepository
import com.rickclephas.kmp.nativecoroutines.NativeCoroutines
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

class ICloudSyncViewModel internal constructor(
private val iCloudSettings: ICloudSettings,
private val dateFormatter: DateFormatter,
private val accountsRepository: AccountsRepository,
private val feedSyncRepository: FeedSyncRepository,
feedSyncMessageQueue: FeedSyncMessageQueue,
) : BaseViewModel() {

Expand Down Expand Up @@ -52,7 +55,11 @@ class ICloudSyncViewModel internal constructor(
}

fun triggerBackup() {
// TODO
scope.launch {
emitSyncLoading()
feedSyncRepository.performBackup(forceBackup = true)
emitLastSyncUpdate()
}
}

fun unlink() {
Expand Down Expand Up @@ -99,4 +106,28 @@ class ICloudSyncViewModel internal constructor(
iCloudSettings.getLastDownloadTimestamp()?.let { timestamp ->
dateFormatter.formatDateForLastRefresh(timestamp)
}

private fun emitSyncLoading() {
iCloudSyncUiMutableState.update { oldState ->
if (oldState is AccountConnectionUiState.Linked) {
AccountConnectionUiState.Linked(
syncState = AccountSyncUIState.Loading,
)
} else {
oldState
}
}
}

private fun emitLastSyncUpdate() {
iCloudSyncUiMutableState.update { oldState ->
if (oldState is AccountConnectionUiState.Linked) {
AccountConnectionUiState.Linked(
syncState = getSyncState(),
)
} else {
oldState
}
}
}
}

0 comments on commit 3cfd3fd

Please sign in to comment.