Skip to content
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
37 changes: 8 additions & 29 deletions ONMIR.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
"Support/Firebase/Production/GoogleService-release-Info.plist",
"Support/Firebase/Staging/GoogleService-staging-Info.plist",
);
target = D011D3022DB4DAAB00412C5C /* ONMIR */;
};
Expand Down Expand Up @@ -136,7 +134,6 @@
D011D3002DB4DAAB00412C5C /* Frameworks */,
460F8D5B2DC77850003F019A /* Firebase Info configure */,
D011D3012DB4DAAB00412C5C /* Resources */,
462108A92DCF8B4200E9A1F2 /* Firebase Crashlytics */,
);
buildRules = (
);
Expand Down Expand Up @@ -298,30 +295,6 @@
shellPath = /bin/sh;
shellScript = "\nGOOGLESERVICE_INFO_STAGING=\"${PROJECT_DIR}/${TARGET_NAME}/Support/Firebase/Staging/GoogleService-staging-Info.plist\"\nGOOGLESERVICE_INFO_PROD=\"${PROJECT_DIR}/${TARGET_NAME}/Support/Firebase/Production/GoogleService-release-Info.plist\"\n\necho \"${PROJECT_DIR}\"\n\necho \"${CONFIGURATION}\"\nif [ \"${CONFIGURATION}\" == \"Debug\" ]; then\n cp \"${GOOGLESERVICE_INFO_STAGING}\" \"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist\"\nelif [ \"${CONFIGURATION}\" == \"Release\" ]; then\n cp \"${GOOGLESERVICE_INFO_PROD}\" \"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist\"\nelse\n echo \"Error: Invalid Build Configuration. Expected 'Debug' or 'Release'.\"\n exit 1\nfi\n\n";
};
462108A92DCF8B4200E9A1F2 /* Firebase Crashlytics */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}",
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}",
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist",
"$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",
"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)",
);
name = "Firebase Crashlytics";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${BUILD_DIR%Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\n";
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
Expand Down Expand Up @@ -367,8 +340,11 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = ONMIR/ONMIR.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 235C2RVZ7L;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ONMIR/Info.plist;
Expand All @@ -382,8 +358,9 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.msg.onmir;
PRODUCT_BUNDLE_IDENTIFIER = com.msg.booktracker;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand All @@ -395,8 +372,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = ONMIR/ONMIR.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 235C2RVZ7L;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ONMIR/Info.plist;
Expand All @@ -410,7 +389,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.msg.onmir;
PRODUCT_BUNDLE_IDENTIFIER = com.msg.booktracker;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions ONMIR/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
didFinishLaunchingWithOptions launchOptions: [UIApplication
.LaunchOptionsKey: Any]?
) -> Bool {
initializeCoreDataStack()

FirebaseApp.configure()

return true
}

Expand Down Expand Up @@ -43,4 +46,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}

private func initializeCoreDataStack() {
let bookSourceValueTransformer = BookSourceTypeValueTransformer()
ValueTransformer.setValueTransformer(bookSourceValueTransformer, forName: BookSourceTypeValueTransformer.name)

let bookStatusValueTransformer = BookStatusValueTransformer()
ValueTransformer.setValueTransformer(bookStatusValueTransformer, forName: BookStatusValueTransformer.name)

_ = ContextManager.shared
}
}
157 changes: 157 additions & 0 deletions ONMIR/Core/CoreData/ContextManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
@preconcurrency import CoreData
import os

public final class ContextManager: CoreDataStack, Sendable {
private enum Constants: Sendable {
static let appGroup = "group.msg.booktracker"
static let cloudContainerIdentifier: String = "iCloud.msg.onmir"
static let inMemoryStoreURL: URL = URL(fileURLWithPath: "/dev/null")
static let databaseName: String = "OnmirModel"
}

private let modelName: String
private let storeURL: URL
private let persistentContainer: NSPersistentCloudKitContainer

public var mainContext: NSManagedObjectContext {
persistentContainer.viewContext
}

public static let shared: ContextManager = {
ContextManager(
modelName: Constants.databaseName,
store: storeURL()
)
}()

init(modelName: String, store storeURL: URL) {
self.modelName = modelName
self.storeURL = storeURL
self.persistentContainer = Self.createPersistentContainer(
storeURL: storeURL,
modelName: modelName
)

mainContext.automaticallyMergesChangesFromParent = true
mainContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}

public func newDerivedContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return context
}
}

extension ContextManager {
private static func createPersistentContainer(
storeURL: URL,
modelName: String
) -> NSPersistentCloudKitContainer {
guard
let modelFileURL = Bundle.main.url(
forResource: modelName,
withExtension: "momd"
)
else {
fatalError("Can't find \(Constants.databaseName).momd")
}

guard
let objectModel = NSManagedObjectModel(contentsOf: modelFileURL)
else {
fatalError(
"Can't create object model named \(modelName) at \(modelFileURL)"
)
}

guard
let stagedMigrationFactory = StagedMigrationFactory(
bundle: .main,
momdURL: modelFileURL,
logger: os.Logger.contextManager
)
else {
fatalError("Can't create StagedMigrationFactory")
}

let baseURL =
storeURL
.appendingPathComponent("Onmir", isDirectory: true)
.appendingPathComponent("CoreData", isDirectory: true)

if !FileManager.default.fileExists(
atPath: baseURL.path(percentEncoded: false)
) {
do {
try FileManager.default.createDirectory(
at: baseURL,
withIntermediateDirectories: true
)
} catch {
os.Logger.contextManager.error("Can't create directory: \(error)")
}
Comment on lines +91 to +93
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The error handling for directory creation only logs the error. If creating the directory fails, loadPersistentStores will subsequently fail, but the cause might not be immediately obvious. Consider making this a fatal error to fail fast and make the root cause clearer.

      } catch {
        fatalError("Can't create directory for Core Data store: \(error)")
      }

}

let sqliteURL =
baseURL
.appending(component: "ONMIR", directoryHint: .notDirectory)
.appendingPathExtension("sqlite")

os.Logger.contextManager.debug("\(sqliteURL)")
let storeDescription = NSPersistentStoreDescription(url: sqliteURL)
// storeDescription.url = Constants.inMemoryStoreURL
storeDescription.type = NSSQLiteStoreType
storeDescription.setOption(
stagedMigrationFactory.create(),
forKey: NSPersistentStoreStagedMigrationManagerOptionKey
)
storeDescription.shouldAddStoreAsynchronously = false

storeDescription.cloudKitContainerOptions =
NSPersistentCloudKitContainerOptions(
containerIdentifier: Constants.cloudContainerIdentifier
)
storeDescription.setOption(
true as NSNumber,
forKey: NSPersistentHistoryTrackingKey
)
storeDescription.setOption(
true as NSNumber,
forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey
)

let persistentContainer = NSPersistentCloudKitContainer(
name: modelName,
managedObjectModel: objectModel
)
persistentContainer.persistentStoreDescriptions = [storeDescription]

persistentContainer.viewContext.automaticallyMergesChangesFromParent = true

persistentContainer.loadPersistentStores { _, error in
if let error {
os.Logger.contextManager.error("\(error)")

assertionFailure("Can't initialize Core Data stack")
}
}

return persistentContainer
}

private static func storeURL() -> URL {
guard
let fileContainer = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: Constants.appGroup
)
else {
fatalError("Unable to find container for app group: \(Constants.appGroup)")
}
return fileContainer
}
}

private extension os.Logger {
static let contextManager = os.Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "ContextManager")
}
70 changes: 70 additions & 0 deletions ONMIR/Core/CoreData/CoreDataStack.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@_exported import CoreData

public protocol CoreDataStack: Sendable {
var mainContext: NSManagedObjectContext { get }

func newDerivedContext() -> NSManagedObjectContext

func performAndSaveLock<T>(_ block: sending @escaping (NSManagedObjectContext) throws -> T) throws -> T
func performAndSave<T>(_ block: sending @escaping (NSManagedObjectContext) throws -> T) async throws -> T

func performAndSaveLock(_ block: sending @escaping (NSManagedObjectContext) throws -> Void) throws
func performAndSave(_ block: sending @escaping (NSManagedObjectContext) throws -> Void) async throws

func performQueryLock<T>(_ block: sending @escaping (NSManagedObjectContext) throws -> T) throws -> T
func performQuery<T>(_ block: sending @escaping (NSManagedObjectContext) throws -> T) async throws -> T
}

extension CoreDataStack {
public func performAndSaveLock<T>(_ block: sending @escaping (NSManagedObjectContext) throws -> T) throws -> T {
let context = newDerivedContext()
return try context.performAndWait {
let result = try block(context)

try context.save()
return result
}
}

public func performAndSave<T>(_ block: sending @escaping (NSManagedObjectContext) throws -> T) async throws -> T {
let context = newDerivedContext()
return try await context.perform {
let result = try block(context)

try context.save()
return result
}
}

public func performAndSaveLock(_ block: sending @escaping (NSManagedObjectContext) throws -> Void) throws {
let context = newDerivedContext()
try context.performAndWait {
try block(context)

try context.save()
}
}

public func performAndSave(_ block: sending @escaping (NSManagedObjectContext) throws -> Void) async throws {
let context = newDerivedContext()
try await context.perform {
try block(context)

try context.save()
}
}

public func performQueryLock<T>(_ block: sending @escaping (NSManagedObjectContext) throws -> T) throws -> T {
let context = newDerivedContext()
return try context.performAndWait {
try block(context)
}
}

public func performQuery<T>(_ block: sending @escaping (NSManagedObjectContext) throws -> T) async throws -> T {
let context = newDerivedContext()
return try await context.perform {
try block(context)
}
}
}
32 changes: 32 additions & 0 deletions ONMIR/Core/CoreData/StagedMigrationFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import CoreData
import Foundation
import OSLog

extension NSManagedObjectModelReference {
fileprivate convenience init(in database: URL, modelName: String) {
let modelURL = database.appending(component: "\(modelName).mom")
guard let model = NSManagedObjectModel(contentsOf: modelURL) else { fatalError() }

self.init(model: model, versionChecksum: model.versionChecksum)
}
}

struct StagedMigrationFactory: Sendable {
private let momdURL: URL
private let logger: os.Logger

init?(
bundle: Bundle = .main,
momdURL: URL,
logger: os.Logger
) {
self.momdURL = momdURL
self.logger = logger
}

func create() -> NSStagedMigrationManager {
let allStages: [NSCustomMigrationStage] = []

return NSStagedMigrationManager(allStages)
}
}
5 changes: 5 additions & 0 deletions ONMIR/Core/Enum/BookSource/BookSourceType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

public enum BookSourceType: String, Sendable {
case googleBooks = "GOOGLE_BOOKS"
}
Loading