SwiftData Modularization with TCA in Tuist
I had never separated a project into sub modules. Only once I created a player module as Private CocoaPods. You know nowadays many Pods are being deprecated and we should use SPM.
In this time, I challenged separating the database module to sub project in Tuist.
Let’s see current directory
Sources
directory has Providers, Containers, Dependencies …
Core - Model
TCA calls modules feature
. Each feature will use models shared between them. So I will modularize shared model first.
I would move main shared codes into Core
directory
I created new Tuist project, but I saw two Project.swift. One is for Core project, another is for the root project.
And I inserted suffix Core
to all project name and bundle id.
Then I generated Core
Project.
To see Core Project in App Project, I modified Project.swift of app.
let project = Project(
packages: [
...,
.package(path: .relativeToCurrentFile("Modules/Core"))
],
targets: [
.target(
...,
dependencies: [
...,
.package(product: "...Core", type: .runtime)
],
And I can saw the package after regenerating Tuist.
However I couldn’t import it.
Workspace
Because we cannot import projects between them without workspace. So I had to configure the workspace. We can see example for workspace in the repository.
First, create workspace in root path of project
and move all project files excepting Workspace into Projects/App
directory.
Also rename the project to simplify, removing the project name.
Run generate, so we can now App and Core projects under Projects
.
Move sources to contain into Core
module.
Now we can import Core framework. But we cannot use types within it.
All types are typically not opened to another projects, so we should publish them.
@Model
public final class QBCharacter {
Of course nested types also should be published.
public var elementalType: ElementalType
Even though I published all types. My app still couldn’t use them. Why?
The reason is the app didn’t embed the framework.
So we should alter the Workspace manifest again.
Add Core
project dependency into App Project’s manifest.
dependencies: [
...,
.project(target: "....Core",
path: .relativeToManifest("../Core"))
],
Now we can see the Core framework in my App project!
Building has been also succeeded.
tuist build
tuist graph
Repository - Database
My app utilized local database using DatabaseProvider
. So this time I will move it to Repository
project.
First I createdLocal
Project under Repositories.
tuist init — platform ios -n Local
And I moved database sources into the project.
Lastly App
embeded the repository project.
dependencies: [
...,
.project(target: "....LocalRepository",
path: .relativeToManifest("../Repositories/Local"))
],
Now the name is Provider
, so I renamed to DatabaseRepositoryProvider
and created protocol
.
public protocol DatabaseRepository{
func fetchCharacter(where keyword: String) throws -> [QBCharacter]?
func insert<Model>(_ model: Model) where Model : PersistentModel
}
extension DatabaseRepositoryProvider : DatabaseRepository
And I replaced the database dependency type with the protocol.
private enum DatabaseDependencyKey: DependencyKey {
@MainActor
static var liveValue: DatabaseRepository
= DatabaseRepositoryProvider.init(...)
Store - search
TCA introduced Feature
as each module name. However I named the store because the type name StoreOf
.
@Bindable var store: StoreOf<CharacterSearchStore>
If the feature should have view and store together, the name will have suffix feature
.
Modularizing the store is more complex than above modules. It will use DependencyKey
and other stores.
ModelContainer
The DependencyKey
uses ModelContainer
, so we have to make an interface for it.
private enum DatabaseDependencyKey: DependencyKey {
@MainActor
static var liveValue: DatabaseRepository
= DatabaseRepositoryProvider.init(container: ModelContainer.generate())
So We will first make Shared
project and move ModelContainer.generate into the Sources
.
DependencyKey
To use database from Store
, the dependency key also should be in Shared
project. But Xcode cannot compile Shared
project without importing TCA.
let project = Project(
name: "Shared",
packages: [.remote(
url: "https://github.com/pointfreeco/swift-composable-architecture",
requirement: .upToNextMinor(from: "1.9.2"))],
targets: [
.target(
...,
dependencies: [.project(target: "....Core",
path: .relativeToManifest("../Core")),
.package(product: "ComposableArchitecture", type: .runtime)]
Now we can build Shared
project. However the compiler couldn’t find DependencyKey
during the App
project compile.
I googled and checked others repositories and found a way. It is to contain only the DependencyValue
and TestDependencyKey in sub project like this.
extension DatabaseDependencyKey: TestDependencyKey {
@MainActor
public static var testValue: DatabaseRepository = ...
}
public extension DependencyValues {
var database: DatabaseRepository {
The Xcode also complained to me with same errors related to TestDependencyKey
.
I don’t know why, but I could solve the problem by switching the framework type to static 😀. (Please tell me better solution)
targets: [
.target(
name: "....Shared",
destinations: .iOS,
product: .staticFramework,
I examined an assumption if ThirdParty
project contains TCA and it is staticFramework, Shared project can use TCA by embedding ThirdParty.
However my examination has been failed. Shared
project have to be static.
AddStore
Now we can create SearchStore
project, but it has AddStore
as sub store.
@Reducer
struct CharacterSearchStore {
@ObservableState
struct State: Equatable {
...
@Presents var addCharacterState: AddCharacterStore.State?
}
So I created AddStore
as static framework and made AddCharacterStore and it’s nested State and Action also. Additionally I created initializer to publish it. Maybe you can use third party macro like @MemberwiseInit(.public) 😎
@Reducer
public struct AddCharacterStore {
@ObservableState
public struct State: Equatable {
public var ...
public init(...) {
...
}
}
public enum Action {
case ...
}
public init(){}
SearchStore
Finally we can create our destination project of this article.
import ComposableArchitecture
import ...HelperCore
import ...HelperLocalRepository
import ...HelperShared
import AddCharacterStore
@Reducer
public struct CharacterSearchStore {
@ObservableState
public struct State: Equatable {
...
@Presents
public var addDateState: AddDateStore.State?
public init(...) {
...
}
}
public enum Action {
...
}
@Dependency(\.database) var database
public init() {
}
Simplify
Dependencies
As you can see, there are duplications of dependency. If the module imports Shared
project, it doesn’t have to import Core
. Store
projects import Shared
project, so we don’t need even it.
Now the App
project depends on only Stores.
dependencies: [
.package(product: "LSExtensions", type: .runtime),
.package(product: "RoundedBorder", type: .runtime),
.project(target: "AddCharacterStore",
path: .relativeToManifest("../Stores/AddCharacter")),
.project(target: "SearchCharacterStore",
path: .relativeToManifest("../Stores/SearchCharacters"))
],
Project Description Helper
Now the Tuist manifests have many projects and it make them more complex. However as you can see, manifests are created by Swift, therefore we can make our own shortcuts for the project like this.
extension TargetDependency {
class Stores {
static let addCharacter : TargetDependency
= .project(target: "AddCharacterStore",
path: .relativeToRoot("Projects/Stores/AddCharacter"))
}
}
How can we share this extension across the projects? Tuist offers Project description helper. To share our extension, we should just move into Tuist/ProjectDescriptionHelpers
directory and import ProjectDescriptionHelpers
There is not Tuist
directory. Don’t worry. Just creat it.
Even though we created the extension file, we cannot import ProjectDesriptionHelpers
. Rerun tuist edit, so you can see the framework in your Xcode.
After importing the framework, your Xcode will emit error. Because our extension is now embed in the framework, so we should open the classes to use in our project manifest.
public extension TargetDependency {
class Stores {
public static let addCharacter : TargetDependency
= .project(target: "AddCharacterStore",
path: .relativeToRoot("Projects/Stores/AddCharacter"))
}
}
Without rerunning tuist edit, we cannot solve the error. Now my code looks better.
Tuist provides also Template, I won’t talk about it here. There are already many articles to make good project structures. I focused only how to separate Store
to sub project in Tuist.
If you found any help in this post, please give it a round of applause 👏. Explore more iOS-related content in my other posts.
You can give me claps up to 50 if you keep pressing the button :)
For additional insights and updates, check out my LinkedIn profile. Thank you for your support!
Troubleshootings
Cannot find type … in scope
attach public to type definition.
Property ‘id’ must be declared public because it matches a requirement in public protocol ‘Identifiable’
public var id
Property cannot be declared public because its type uses an internal type
Make nested type public.
Couldn’t find target ‘
Fit target of project dependency to project’s target name.
Property ‘…’ must be declared public because it matches a requirement in public protocol ‘Reducer’
Make State, Action, Body
initializer is inaccessible due to ‘internal’ protection level
public init(){ }