I am learning to program vis-a-vis Swift (the language itself) and SwiftUI (framework).
I have completed most of the offical tutorials offered by Apple BUT I struggle to know when code I got to work by deviating from their pattern is actually good or not(please don't judge, I am still learning)
For example: In a bid to incoporate encapsulation, I moved implementation details for each modelcontainer into their respective files.
Example of a UserModel:
public final class UserDBImplementation {
private let container: ModelContainer
public init() throws {
self.container = try UserDBImplementation.buildContainer()
}
static private var currentAppEnv: String? {
return Bundle.main.infoDictionary?["APP_ENV"] as? String
}
static private func buildContainer() throws -> ModelContainer {
guard let appENV = currentAppEnv else {
let res = try createInMemoryContainer()
return res
}
return try createContainer(for: appENV)
}
static private func createInMemoryContainer() throws -> ModelContainer {
let configuration = ModelConfiguration(for: UserModel.self, isStoredInMemoryOnly: true)
return try ModelContainer(for: UserModel.self, configurations: configuration)
}
static private func createContainer(for appEnv: String) throws -> ModelContainer {
if appEnv == "Staging" {// Release && TestFlight
let configuration = buildConfiguration(for: appEnv)
let container = try ModelContainer(for: UserModel.self, configurations: configuration)
return container
} else if appEnv == "Production" { // Release && AppStore
let configuration = buildConfiguration(for: appEnv)
let container = try ModelContainer(for: UserModel.self, configurations: configuration)
return container
} else {
let container = try createInMemoryContainer()
return container
}
}
static private func buildConfiguration(for appEnv: String) -> ModelConfiguration {
let fileName = appEnv.appending(".sqlite")
let fileURL = FileManager.default.urls(
for: .documentDirectory, in: .userDomainMask
).first!.appendingPathComponent(fileName)
return ModelConfiguration(url: fileURL)
}
}
I am concerned about the use of Static methods to do the work required to initialize an instance. Searching online doesn't yield a lot of information BUT this Stackover which advocates for it under certain conditions; conditions which I think UserDBImplementation meets.
Any feedback would be greatly apprecaited. Thank you!
Well, in general there is nothing wrong with using static methods during init. If initialization of an object is complex, that's better than having large init. But there might be room for improvement, for example, if you want to reuse this initialization, or this initialization is not intended to be part of the object (e.g. object has to do a lot of other things), you can leverage factory patten and extract this into separate type:
public struct UserModelContainerFactory {
public init() {
}
func make() -> ModelContainer {
let currentAppEnv = Bundle.main.infoDictionary?["APP_ENV"] as? String
guard let appENV = currentAppEnv else {
let res = try createInMemoryContainer()
return res
}
return try createContainer(for: appENV)
}
private func createInMemoryContainer() throws -> ModelContainer {
let configuration = ModelConfiguration(for: UserModel.self, isStoredInMemoryOnly: true)
return try ModelContainer(for: UserModel.self, configurations: configuration)
}
private func createContainer(for appEnv: String) throws -> ModelContainer {
if appEnv == "Staging" { // Release && TestFlight
let configuration = buildConfiguration(for: appEnv)
let container = try ModelContainer(for: UserModel.self, configurations: configuration)
return container
} else if appEnv == "Production" { // Release && AppStore
let configuration = buildConfiguration(for: appEnv)
let container = try ModelContainer(for: UserModel.self, configurations: configuration)
return container
} else {
let container = try createInMemoryContainer()
return container
}
}
private func buildConfiguration(for appEnv: String) -> ModelConfiguration {
let fileName = appEnv.appending(".sqlite")
let fileURL = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first!
.appendingPathComponent(fileName)
return ModelConfiguration(url: fileURL)
}
}
public final class UserDBImplementation {
private let container: ModelContainer
public init(container: ModelContainer) throws {
self.container = container
}
}
let factory = UserModelContainerFactory()
let db = UserDBImplementation(container: factory.make())
So now you have separated construction of container from usage. If other type (say, PostDBImplementation) would need same container construction, you can make this a generic and reuse:
public struct ModelContainerFactory<ModelType> where ModelType: PersistentModel {
// ...
}
If you're going to reuse initialization logic, then static methods are an option. But the simplest code and tightest encapsulation for this kind of thing comes from an inline closure. Any closure which is executed this way could be named makeX, substituting the name of the thing on the left of the =, for X.
A make can be thought of a semantic specialization of of an init. So when needing to move one to an outer scope, I prefer a private extension initializer to a static method, when possible.
Thank you so much for your feedback - the point about refactoring for reusability is particularly useful as it allows for use of generics. Appreciate your time