Skip to content
Draft
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 @@ -334,7 +334,9 @@ class AppDataLayerDependencies {

func getTrackDownloadedTranslationsRepository() -> TrackDownloadedTranslationsRepository {
return TrackDownloadedTranslationsRepository(
cache: TrackDownloadedTranslationsCache(realmDatabase: sharedRealmDatabase)
cache: TrackDownloadedTranslationsCache(
realmDatabase: sharedRealmDatabase
)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class RealmDatabaseProductionConfiguration: RealmDatabaseConfiguration {

private static let diskFileName: String = "godtools_realm"

static let schemaVersion: UInt64 = 36
static let schemaVersion: UInt64 = 37

init() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,17 @@ extension RealmRepositorySyncPersistence {

let results = realm.objects(PersistObjectType.self)

if let filter = query?.filter {
if let filter = query?.filter, let sortByKeyPath = query?.sortByKeyPath {
return results
.filter(filter)
.sorted(byKeyPath: sortByKeyPath.keyPath, ascending: sortByKeyPath.ascending)
}
else if let filter = query?.filter, let sortByKeyPath = query?.sortByKeyPath {
else if let filter = query?.filter {
return results
.filter(filter)
}
else if let sortByKeyPath = query?.sortByKeyPath {
return results
.sorted(byKeyPath: sortByKeyPath.keyPath, ascending: sortByKeyPath.ascending)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ class RealmResourcesCacheSync {
return true
}

let latestTrackedDownloadedTranslation: DownloadedTranslationDataModel? = self.trackDownloadedTranslationsRepository.getLatestDownloadedTranslation(resourceId: resourceId, languageId: languageId)
let latestTrackedDownloadedTranslation: DownloadedTranslationDataModel? = self.trackDownloadedTranslationsRepository.cache.getLatestDownloadedTranslation(resourceId: resourceId, languageId: languageId)

let translationIsLatestDownloadedTranslation: Bool = latestTrackedDownloadedTranslation?.translationId == $0.id

Expand All @@ -206,7 +206,7 @@ class RealmResourcesCacheSync {
let resourcesRemoved: [ResourceDataModel] = existingResourcesMinusNewlyAddedResources.map({ResourceDataModel(interface: $0)})
let translationsRemoved: [TranslationDataModel] = existingTranslationsMinusNewlyAddedTranslations.map({TranslationDataModel(interface: $0)})
let attachmentsRemoved: [AttachmentDataModel] = existingAttachmentsMinusNewlyAddedAttachments.map({AttachmentDataModel(interface: $0, storedAttachment: nil)})
let downloadedTranslationsRemoved: [DownloadedTranslationDataModel] = downloadedTranslationsToRemove.map({DownloadedTranslationDataModel(model: $0)})
let downloadedTranslationsRemoved: [DownloadedTranslationDataModel] = downloadedTranslationsToRemove.map({DownloadedTranslationDataModel(interface: $0)})

// delete realm objects that no longer exist
var objectsToRemove: [Object] = Array()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ class SwiftResourcesCacheSync {
return true
}

let latestTrackedDownloadedTranslation: DownloadedTranslationDataModel? = self.trackDownloadedTranslationsRepository.getLatestDownloadedTranslation(resourceId: resourceId, languageId: languageId)
let latestTrackedDownloadedTranslation: DownloadedTranslationDataModel? = self.trackDownloadedTranslationsRepository.cache.getLatestDownloadedTranslation(resourceId: resourceId, languageId: languageId)

let translationIsLatestDownloadedTranslation: Bool = latestTrackedDownloadedTranslation?.translationId == $0.id

Expand All @@ -212,7 +212,7 @@ class SwiftResourcesCacheSync {
let resourcesRemoved: [ResourceDataModel] = existingResourcesMinusNewlyAddedResources.map({ResourceDataModel(interface: $0)})
let translationsRemoved: [TranslationDataModel] = existingTranslationsMinusNewlyAddedTranslations.map({TranslationDataModel(interface: $0)})
let attachmentsRemoved: [AttachmentDataModel] = existingAttachmentsMinusNewlyAddedAttachments.map({AttachmentDataModel(interface: $0, storedAttachment: nil)})
let downloadedTranslationsRemoved: [DownloadedTranslationDataModel] = downloadedTranslationsToRemove.map({DownloadedTranslationDataModel(model: $0)})
let downloadedTranslationsRemoved: [DownloadedTranslationDataModel] = downloadedTranslationsToRemove.map({DownloadedTranslationDataModel(interface: $0)})

// delete realm objects that no longer exist
var objectsToRemove: [any PersistentModel] = Array()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
import Foundation
import RealmSwift

class RealmDownloadedTranslation: Object, DownloadedTranslationDataModelInterface {
class RealmDownloadedTranslation: Object, IdentifiableRealmObject, DownloadedTranslationDataModelInterface {

@objc dynamic var id: String = ""
@objc dynamic var languageId: String = ""
@objc dynamic var manifestAndRelatedFilesPersistedToDevice: Bool = false
@objc dynamic var resourceId: String = ""
Expand All @@ -22,6 +23,7 @@ class RealmDownloadedTranslation: Object, DownloadedTranslationDataModelInterfac
}

func mapFrom(interface: DownloadedTranslationDataModelInterface) {
id = interface.id
languageId = interface.languageId
manifestAndRelatedFilesPersistedToDevice = interface.manifestAndRelatedFilesPersistedToDevice
resourceId = interface.resourceId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ enum SwiftDownloadedTranslationV1 {
}

func mapFrom(interface: DownloadedTranslationDataModelInterface) {
id = interface.id
languageId = interface.languageId
manifestAndRelatedFilesPersistedToDevice = interface.manifestAndRelatedFilesPersistedToDevice
resourceId = interface.resourceId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,145 @@
import Foundation
import RealmSwift
import Combine
import SwiftData

class TrackDownloadedTranslationsCache {
class TrackDownloadedTranslationsCache: SwiftElseRealmPersistence<DownloadedTranslationDataModel, DownloadedTranslationDataModel, RealmDownloadedTranslation> {

private let realmDatabase: RealmDatabase

init(realmDatabase: RealmDatabase) {
init(realmDatabase: RealmDatabase, swiftPersistenceIsEnabled: Bool? = nil) {

self.realmDatabase = realmDatabase

super.init(
realmDatabase: realmDatabase,
realmDataModelMapping: RealmDownloadedTranslationDataModelMapping(),
swiftPersistenceIsEnabled: swiftPersistenceIsEnabled
)
}

private func getDownloadedTranslationsSortedByLatestVersion(resourceId: String, languageId: String) -> [DownloadedTranslationDataModel] {

let realm: Realm = realmDatabase.openRealm()
@available(iOS 17.4, *)
override func getAnySwiftPersistence(swiftDatabase: SwiftDatabase) -> (any RepositorySyncPersistence<DownloadedTranslationDataModel, DownloadedTranslationDataModel>)? {
return getSwiftPersistence(swiftDatabase: swiftDatabase)
}

@available(iOS 17.4, *)
private func getSwiftPersistence() -> SwiftRepositorySyncPersistence<DownloadedTranslationDataModel, DownloadedTranslationDataModel, SwiftDownloadedTranslation>? {

let resourceIdPredicate = NSPredicate(format: "\(#keyPath(RealmDownloadedTranslation.resourceId)) == %@", resourceId)
let languageIdPredicate = NSPredicate(format: "\(#keyPath(RealmDownloadedTranslation.languageId)) == %@", languageId)
let filterPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [resourceIdPredicate, languageIdPredicate])

let latestTranslations: [DownloadedTranslationDataModel] = realm.objects(RealmDownloadedTranslation.self)
.filter(filterPredicate)
.sorted(byKeyPath: #keyPath(RealmDownloadedTranslation.version), ascending: false)
.map({ DownloadedTranslationDataModel(model: $0) })
guard let swiftDatabase = super.getSwiftDatabase() else {
return nil
}

return latestTranslations
return getSwiftPersistence(swiftDatabase: swiftDatabase)
}

func getLatestDownloadedTranslation(resourceId: String, languageId: String) -> DownloadedTranslationDataModel? {
@available(iOS 17.4, *)
private func getSwiftPersistence(swiftDatabase: SwiftDatabase) -> SwiftRepositorySyncPersistence<DownloadedTranslationDataModel, DownloadedTranslationDataModel, SwiftDownloadedTranslation>? {

guard let swiftDatabase = super.getSwiftDatabase() else {
return nil
}

return getDownloadedTranslationsSortedByLatestVersion(resourceId: resourceId, languageId: languageId).first
return SwiftRepositorySyncPersistence(
swiftDatabase: swiftDatabase,
dataModelMapping: SwiftDownloadedTranslationDataModelMapping()
)
}
}

// MARK: - Sort Descriptors

extension TrackDownloadedTranslationsCache {

@available(iOS 17.4, *)
private func getSortByLatestVersionDescriptor() -> [Foundation.SortDescriptor<SwiftDownloadedTranslation>] {
return [SortDescriptor(\SwiftDownloadedTranslation.version, order: .reverse)]
}

func trackTranslationDownloaded(translation: TranslationDataModel) -> AnyPublisher<[DownloadedTranslationDataModel], Error> {

return realmDatabase.writeObjectsPublisher { (realm: Realm) -> [RealmDownloadedTranslation] in
private func getSortByLatestVersionKeyPath() -> SortByKeyPath {
return SortByKeyPath(
keyPath: #keyPath(RealmDownloadedTranslation.version),
ascending: false
)
}
}

// MARK: - Query

extension TrackDownloadedTranslationsCache {

func getLatestDownloadedTranslations(resourceId: String, languageId: String) -> [DownloadedTranslationDataModel] {

let downloadedTranslation: RealmDownloadedTranslation = RealmDownloadedTranslation()
if #available(iOS 17.4, *), let swiftPersistence = getSwiftPersistence() {

guard let languageId = translation.languageDataModel?.id, !languageId.isEmpty, let resourceId = translation.resourceDataModel?.id, !resourceId.isEmpty else {

return []
let filter = #Predicate<SwiftDownloadedTranslation> { downloadedTranslation in
downloadedTranslation.resourceId == resourceId && downloadedTranslation.languageId == languageId
}

downloadedTranslation.languageId = languageId
downloadedTranslation.manifestAndRelatedFilesPersistedToDevice = true
downloadedTranslation.resourceId = resourceId
downloadedTranslation.translationId = translation.id
downloadedTranslation.version = translation.version
let query = SwiftDatabaseQuery(
filter: filter,
sortBy: getSortByLatestVersionDescriptor()
)

let objects: [RealmDownloadedTranslation] = [downloadedTranslation]
return swiftPersistence
.getObjects(query: query)
}
else {

let resourceIdPredicate = NSPredicate(format: "\(#keyPath(RealmDownloadedTranslation.resourceId)) == %@", resourceId)
let languageIdPredicate = NSPredicate(format: "\(#keyPath(RealmDownloadedTranslation.languageId)) == %@", languageId)
let filterPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [resourceIdPredicate, languageIdPredicate])

let query = RealmDatabaseQuery(
filter: filterPredicate,
sortByKeyPath: getSortByLatestVersionKeyPath()
)

return super.getRealmPersistence()
.getObjects(query: query)
}
}

func getLatestDownloadedTranslation(resourceId: String, languageId: String) -> DownloadedTranslationDataModel? {

return getLatestDownloadedTranslations(resourceId: resourceId, languageId: languageId).first
}
}

// MARK: - Track Downloads

extension TrackDownloadedTranslationsCache {

func trackTranslationDownloaded(translation: TranslationDataModel) -> AnyPublisher<[DownloadedTranslationDataModel], Error> {

guard let resourceId = translation.resourceDataModel?.id, let languageId = translation.languageDataModel?.id else {
let error: Error = NSError.errorWithDescription(description: "Failed to get resourceId and languageId for tracked downloaded translation.")
return Fail(error: error)
.eraseToAnyPublisher()
}

let downloadedTranslation = DownloadedTranslationDataModel(
id: translation.id,
languageId: languageId,
manifestAndRelatedFilesPersistedToDevice: true,
resourceId: resourceId,
translationId: translation.id,
version: translation.version
)

if #available(iOS 17.4, *), let swiftPersistence = getSwiftPersistence() {

return objects
_ = swiftPersistence
.writeObjects(externalObjects: [downloadedTranslation])
}
else {

} mapInBackgroundClosure: { (objects: [RealmDownloadedTranslation]) -> [DownloadedTranslationDataModel] in
return objects.map({DownloadedTranslationDataModel(model: $0)})
_ = super.getRealmPersistence()
.writeObjects(externalObjects: [downloadedTranslation], deleteObjectsNotFoundInExternalObjects: false)
}
.eraseToAnyPublisher()

return Just([downloadedTranslation])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,30 @@ import Foundation

struct DownloadedTranslationDataModel: DownloadedTranslationDataModelInterface {

let id: String
let languageId: String
let manifestAndRelatedFilesPersistedToDevice: Bool
let resourceId: String
let translationId: String
let version: Int

init(model: DownloadedTranslationDataModelInterface) {
init(id: String, languageId: String, manifestAndRelatedFilesPersistedToDevice: Bool, resourceId: String, translationId: String, version: Int) {

languageId = model.languageId
manifestAndRelatedFilesPersistedToDevice = model.manifestAndRelatedFilesPersistedToDevice
resourceId = model.resourceId
translationId = model.translationId
version = model.version
self.id = id
self.languageId = languageId
self.manifestAndRelatedFilesPersistedToDevice = manifestAndRelatedFilesPersistedToDevice
self.resourceId = resourceId
self.translationId = translationId
self.version = version
}

init(interface: DownloadedTranslationDataModelInterface) {

id = interface.id
languageId = interface.languageId
manifestAndRelatedFilesPersistedToDevice = interface.manifestAndRelatedFilesPersistedToDevice
resourceId = interface.resourceId
translationId = interface.translationId
version = interface.version
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation

protocol DownloadedTranslationDataModelInterface {

var id: String { get }
var languageId: String { get }
var manifestAndRelatedFilesPersistedToDevice: Bool { get }
var resourceId: String { get }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// RealmDownloadedTranslationDataModelMapping.swift
// godtools
//
// Created by Levi Eggert on 12/3/25.
// Copyright © 2025 Cru. All rights reserved.
//

import Foundation

class RealmDownloadedTranslationDataModelMapping: RepositorySyncMapping {

func toDataModel(externalObject: DownloadedTranslationDataModel) -> DownloadedTranslationDataModel? {
return DownloadedTranslationDataModel(interface: externalObject)
}

func toDataModel(persistObject: RealmDownloadedTranslation) -> DownloadedTranslationDataModel? {
return DownloadedTranslationDataModel(interface: persistObject)
}

func toPersistObject(externalObject: DownloadedTranslationDataModel) -> RealmDownloadedTranslation? {
return RealmDownloadedTranslation.createNewFrom(interface: externalObject)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// SwiftDownloadedTranslationDataModelMapping.swift
// godtools
//
// Created by Levi Eggert on 12/3/25.
// Copyright © 2025 Cru. All rights reserved.
//

import Foundation

@available(iOS 17.4, *)
class SwiftDownloadedTranslationDataModelMapping: RepositorySyncMapping {

func toDataModel(externalObject: DownloadedTranslationDataModel) -> DownloadedTranslationDataModel? {
return DownloadedTranslationDataModel(interface: externalObject)
}

func toDataModel(persistObject: SwiftDownloadedTranslation) -> DownloadedTranslationDataModel? {
return DownloadedTranslationDataModel(interface: persistObject)
}

func toPersistObject(externalObject: DownloadedTranslationDataModel) -> SwiftDownloadedTranslation? {
return SwiftDownloadedTranslation.createNewFrom(interface: externalObject)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,10 @@ import Combine

class TrackDownloadedTranslationsRepository {

private let cache: TrackDownloadedTranslationsCache
let cache: TrackDownloadedTranslationsCache

init(cache: TrackDownloadedTranslationsCache) {

self.cache = cache
}

func getLatestDownloadedTranslation(resourceId: String, languageId: String) -> DownloadedTranslationDataModel? {
return cache.getLatestDownloadedTranslation(resourceId: resourceId, languageId: languageId)
}

func trackTranslationDownloaded(translation: TranslationDataModel) -> AnyPublisher<[DownloadedTranslationDataModel], Error> {
return cache.trackTranslationDownloaded(translation: translation)
}
}
Loading
Loading