Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e48448f
Start adding realm object to persist language download progress
levieggertcru Mar 20, 2025
79dab18
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Mar 25, 2025
989a398
Change NSNumber to Double value
levieggertcru Mar 25, 2025
cb4d62c
Add publisher to observe realm object changes
levieggertcru Mar 26, 2025
c560026
Add use case for observing tool language downloader download progress
levieggertcru Mar 26, 2025
acf2e6b
Update ToolLanguageDownloader to persist errors
levieggertcru Mar 26, 2025
bc90ffd
Remove downloadState and downloadError from recycle object
levieggertcru Mar 26, 2025
be7cec8
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Mar 27, 2025
8c55d9d
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Apr 14, 2025
41891f5
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Apr 21, 2025
db94c4c
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Apr 28, 2025
7558d45
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Apr 29, 2025
7fb73b1
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Aug 18, 2025
f4a65b0
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Aug 29, 2025
3c2b72d
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Sep 18, 2025
e4cba59
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Oct 28, 2025
fad2997
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Oct 28, 2025
7316331
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Oct 30, 2025
f0cc63f
Merge branch 'develop' into GT-2524-look-into-persisting-language-dow…
levieggertcru Nov 24, 2025
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
@@ -0,0 +1,38 @@
//
// GetDownloadToolLanguageProgress.swift
// godtools
//
// Created by Levi Eggert on 3/26/25.
// Copyright © 2025 Cru. All rights reserved.
//

import Foundation
import Combine

class GetDownloadToolLanguageProgress: GetDownloadToolLanguageProgressInterface {

private let toolLanguageDownloader: ToolLanguageDownloader
private let downloadedLanguagesRepository: DownloadedLanguagesRepository

init(toolLanguageDownloader: ToolLanguageDownloader, downloadedLanguagesRepository: DownloadedLanguagesRepository) {

self.toolLanguageDownloader = toolLanguageDownloader
self.downloadedLanguagesRepository = downloadedLanguagesRepository
}

func getProgressPublisher(languageId: String) -> AnyPublisher<DownloadToolLanguageProgressDomainModel, Never> {

return toolLanguageDownloader
.observeToolLanguageDownloadPublisher(languageId: languageId)
.map { (download: ToolLanguageDownload) in

let domainModel = DownloadToolLanguageProgressDomainModel(
dataModelId: languageId,
progress: download.downloadProgress
)

return domainModel
}
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// RealmToolLanguageDownload.swift
// godtools
//
// Created by Levi Eggert on 3/20/25.
// Copyright © 2025 Cru. All rights reserved.
//

import Foundation
import RealmSwift

class RealmToolLanguageDownload: Object {

@objc dynamic var languageId: String = ""
@objc dynamic var downloadErrorDescription: String?
@objc dynamic var downloadErrorHttpStatusCode: Int = -1
@objc dynamic var downloadProgress: Double = 0
@objc dynamic var downloadStartedAt: Date = Date()

override static func primaryKey() -> String? {
return "languageId"
}

func mapFrom(dataModel: ToolLanguageDownload) {

languageId = dataModel.languageId
downloadErrorDescription = dataModel.downloadErrorDescription
downloadErrorHttpStatusCode = dataModel.downloadErrorHttpStatusCode
downloadProgress = dataModel.downloadProgress
downloadStartedAt = dataModel.downloadStartedAt
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// RealmToolLanguageDownloaderCache.swift
// godtools
//
// Created by Levi Eggert on 3/20/25.
// Copyright © 2025 Cru. All rights reserved.
//

import Foundation
import RealmSwift
import Combine

class RealmToolLanguageDownloaderCache {

private let realmDatabase: RealmDatabase

init(realmDatabase: RealmDatabase) {

self.realmDatabase = realmDatabase
}

func getToolLanguageDownloadElseNew(languageId: String) -> RealmToolLanguageDownload {

let existingObject: RealmToolLanguageDownload? = realmDatabase.readObject(primaryKey: languageId)

if let existingObject = existingObject {

return existingObject
}
else {

let dataModel = ToolLanguageDownload(
languageId: languageId,
downloadErrorDescription: nil,
downloadErrorHttpStatusCode: nil,
downloadProgress: 0,
downloadStartedAt: Date()
)

let newObject: RealmToolLanguageDownload = RealmToolLanguageDownload()
newObject.mapFrom(dataModel: dataModel)

storeToolLanguageDownload(realmObject: newObject)

return newObject
}
}

func updateDownloadProgressPublisher(languageId: String, downloadProgress: Double) -> AnyPublisher<[ToolLanguageDownload], Error> {

return realmDatabase.writeObjectsPublisher(updatePolicy: .modified, writeClosure: { (realm: Realm) in

var realmObjects: [RealmToolLanguageDownload] = Array()

if let object = realm.object(ofType: RealmToolLanguageDownload.self, forPrimaryKey: languageId) {
object.downloadProgress = downloadProgress
realmObjects = [object]
}

return realmObjects
},
mapInBackgroundClosure: { (realmObjects: [RealmToolLanguageDownload]) in

let downloads: [ToolLanguageDownload] = realmObjects.map {
ToolLanguageDownload(realmToolLanguageDownload: $0)
}

return downloads
})
.eraseToAnyPublisher()
}

func storeToolLanguageDownload(dataModel: ToolLanguageDownload) {

let realmObject = RealmToolLanguageDownload()

realmObject.mapFrom(dataModel: dataModel)

storeToolLanguageDownload(realmObject: realmObject)
}

private func storeToolLanguageDownload(realmObject: RealmToolLanguageDownload) {

_ = realmDatabase.writeObjects(realm: realmDatabase.openRealm(), updatePolicy: .modified) { (realm: Realm) in
let objects: [RealmToolLanguageDownload] = [realmObject]
return objects
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// ToolLanguageDownload.swift
// godtools
//
// Created by Levi Eggert on 3/20/25.
// Copyright © 2025 Cru. All rights reserved.
//

import Foundation

struct ToolLanguageDownload {

let languageId: String
let downloadErrorDescription: String?
let downloadErrorHttpStatusCode: Int
let downloadProgress: Double
let downloadStartedAt: Date

init(languageId: String, downloadErrorDescription: String?, downloadErrorHttpStatusCode: Int?, downloadProgress: Double, downloadStartedAt: Date) {

self.languageId = languageId
self.downloadErrorDescription = downloadErrorDescription
self.downloadErrorHttpStatusCode = downloadErrorHttpStatusCode ?? -1
self.downloadProgress = downloadProgress
self.downloadStartedAt = downloadStartedAt
}

init(realmToolLanguageDownload: RealmToolLanguageDownload) {

languageId = realmToolLanguageDownload.languageId
downloadErrorDescription = realmToolLanguageDownload.downloadErrorDescription
downloadErrorHttpStatusCode = realmToolLanguageDownload.downloadErrorHttpStatusCode
downloadProgress = realmToolLanguageDownload.downloadProgress
downloadStartedAt = realmToolLanguageDownload.downloadStartedAt
}

var secondsSinceDownloadStartedAt: TimeInterval? {

return Date().timeIntervalSince(downloadStartedAt)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,59 @@ class ToolLanguageDownloader {
private let languagesRepository: LanguagesRepository
private let toolDownloader: ToolDownloader
private let downloadedLanguagesRepository: DownloadedLanguagesRepository
private let cache: RealmToolLanguageDownloaderCache

init(resourcesRepository: ResourcesRepository, languagesRepository: LanguagesRepository, toolDownloader: ToolDownloader, downloadedLanguagesRepository: DownloadedLanguagesRepository) {
init(resourcesRepository: ResourcesRepository, languagesRepository: LanguagesRepository, toolDownloader: ToolDownloader, downloadedLanguagesRepository: DownloadedLanguagesRepository, cache: RealmToolLanguageDownloaderCache) {

self.resourcesRepository = resourcesRepository
self.languagesRepository = languagesRepository
self.toolDownloader = toolDownloader
self.downloadedLanguagesRepository = downloadedLanguagesRepository
self.cache = cache
}

func observeToolLanguageDownloadPublisher(languageId: String) -> AnyPublisher<ToolLanguageDownload, Never> {

let realmObjectToObserve: RealmToolLanguageDownload = cache.getToolLanguageDownloadElseNew(
languageId: languageId
)

return Publishers.RealmObjectPublisher(realmObject: realmObjectToObserve)
.map { change in

return ToolLanguageDownload(
realmToolLanguageDownload: realmObjectToObserve
)
}
.eraseToAnyPublisher()
}

func downloadToolLanguagePublisher(languageId: String) -> AnyPublisher<ToolDownloaderDataModel, Error> {

guard let languageModel = languagesRepository.persistence.getObject(id: languageId) else {

let error: Error = NSError.errorWithDomain(domain: "ToolLanguageDownloader", code: -1, description: "Internal Error in ToolLanguageDownloader. Failed to fetch language with language id: \(languageId)")
let error: Error = NSError.errorWithDomain(
domain: "ToolLanguageDownloader",
code: -1,
description: "Internal Error in ToolLanguageDownloader. Failed to fetch language with language id: \(languageId)"
)

return Fail(error: error)
.eraseToAnyPublisher()
}

let cache: RealmToolLanguageDownloaderCache = self.cache

let dataModel = ToolLanguageDownload(
languageId: languageId,
downloadErrorDescription: nil,
downloadErrorHttpStatusCode: nil,
downloadProgress: 0,
downloadStartedAt: Date()
)

cache.storeToolLanguageDownload(dataModel: dataModel)

let includeToolTypes: [ResourceType] = ResourceType.toolTypes + [.lesson]

let tools: [ResourceDataModel] = resourcesRepository.getCachedResourcesByFilter(
Expand All @@ -45,6 +79,26 @@ class ToolLanguageDownloader {
})

return toolDownloader.downloadToolsPublisher(tools: downloadTools, requestPriority: .low)
.flatMap({ (dataModel: ToolDownloaderDataModel) -> AnyPublisher<ToolLanguageDownload?, Error> in

return cache.updateDownloadProgressPublisher(
languageId: languageId,
downloadProgress: dataModel.progress
)
.map {
$0.first
}
.eraseToAnyPublisher()
})
.map { (download: ToolLanguageDownload?) in

if let download = download {
return ToolDownloaderDataModel(attachments: [], progress: download.downloadProgress, translations: [])
}
else {
return ToolDownloaderDataModel(attachments: [], progress: 0, translations: [])
}
}
.eraseToAnyPublisher()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class AppLanguageFeatureDataLayerDependencies {
resourcesRepository: coreDataLayer.getResourcesRepository(),
languagesRepository: coreDataLayer.getLanguagesRepository(),
toolDownloader: coreDataLayer.getToolDownloader(),
downloadedLanguagesRepository: getDownloadedLanguagesRepository()
downloadedLanguagesRepository: getDownloadedLanguagesRepository(),
cache: RealmToolLanguageDownloaderCache(realmDatabase: coreDataLayer.getSharedRealmDatabase())
)
}

Expand Down Expand Up @@ -115,6 +116,13 @@ class AppLanguageFeatureDataLayerDependencies {
)
}

func getDownloadToolLanguageProgress() -> GetDownloadToolLanguageProgressInterface {
return GetDownloadToolLanguageProgress(
toolLanguageDownloader: getToolLanguageDownloader(),
downloadedLanguagesRepository: getDownloadedLanguagesRepository()
)
}

func getDownloadToolLanguageRepositoryInterface() -> DownloadToolLanguageRepositoryInterface {
return DownloadToolLanguageRepository(
downloadedLanguagesRepository: getDownloadedLanguagesRepository(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ class AppLanguageFeatureDomainLayerDependencies {
)
}

func getDownloadToolLanguageProgressUseCase() -> GetDownloadToolLanguageProgressUseCase {
return GetDownloadToolLanguageProgressUseCase(
getProgress: dataLayer.getDownloadToolLanguageProgress()
)
}

func getInterfaceLayoutDirectionUseCase() -> GetInterfaceLayoutDirectionUseCase {
return GetInterfaceLayoutDirectionUseCase(
getLayoutDirectionInterface: dataLayer.getAppInterfaceLayoutDirectionInterface()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// DownloadToolLanguageProgressDomainModel.swift
// godtools
//
// Created by Levi Eggert on 3/26/25.
// Copyright © 2025 Cru. All rights reserved.
//

import Foundation

struct DownloadToolLanguageProgressDomainModel {

let dataModelId: String
let progress: Double
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// GetDownloadToolLanguageProgressInterface.swift
// godtools
//
// Created by Levi Eggert on 3/26/25.
// Copyright © 2025 Cru. All rights reserved.
//

import Foundation
import Combine

protocol GetDownloadToolLanguageProgressInterface {

func getProgressPublisher(languageId: String) -> AnyPublisher<DownloadToolLanguageProgressDomainModel, Never>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// GetDownloadToolLanguageProgressUseCase.swift
// godtools
//
// Created by Levi Eggert on 3/26/25.
// Copyright © 2025 Cru. All rights reserved.
//

import Foundation
import Combine

class GetDownloadToolLanguageProgressUseCase {

private let getProgress: GetDownloadToolLanguageProgressInterface

init(getProgress: GetDownloadToolLanguageProgressInterface) {

self.getProgress = getProgress
}

func getProgressPublisher(languageId: String) -> AnyPublisher<DownloadToolLanguageProgressDomainModel, Never> {

return getProgress
.getProgressPublisher(languageId: languageId)
}
}
Loading
Loading