[SwiftUI] Created a simple Mac app using TCA

Yoki
3 min readMay 1, 2022

--

Photo by Pawel Czerwinski on Unsplash

What I created

https://github.com/yyokii/PomodoroApp

I created a simple Pomodoro Timer app using TCA, SwiftUI, and Firebase. Features include the following.

  • Pomodoro timer
  • Pomodoro setting change
  • Timer reset
  • Anonymous sign-in
  • Save data to server

I was going to add the following features, but currently do not have them.

  • Sign in/sign up with your email address
  • Viewing historical data

Application Structures

  • Client: SwiftUI and TCA
  • Backend: Firebase (Auth, Firestore)

Details

AppCore

In order to manage the state of the entire app, AppCore’s Reducer handles the actions of each feature. For example, when a user status changes or a change is made on one screen, it can be reflected on other screens by updating the state from AppCore. Currently, we are doing almost nothing for this simple application.

public struct AppState: Equatable {
var account: AccountState = AccountState()
var myData: MyDataState = MyDataState()
var pomodoroTimer: PomodoroTimerState = PomodoroTimerState()
public var settings: SettingsState = SettingsState()
public init() {}
}
public enum AppAction: Equatable {
case account(AccountAction)
case myData(MyDataAction)
case pomodoroTimer(PomodoroTimerAction)
case settings(SettingsAction)
...
}

Binding

When writing in SwiftUI, it is possible to bind views and data, and this is also possible in TCA. Binding is also available in TCA, but in a slightly special way.

In the State, add @BindableState to the View. By doing so, you declare that these values can be directly updated in, for example, a text field or a picker in a View.

public struct PomodoroTimerSettingsState: Equatable {
@BindableState var intervalTime: Int = 0
@BindableState var shortBreakTime: Int = 0
@BindableState var longBreakTime: Int = 0
@BindableState var intervalCountBeforeLongBreak: Int = 0
public init() {}
}

Add a binding case to Action so that processing can be performed when the value is updated.

public enum PomodoroTimerSettingsAction: Equatable, BindableAction {
case binding(BindingAction<PomodoroTimerSettingsState>)
case onAppear
case onDisappear
}

In Reducer, give .binding() to be able to handle Binding.

public let pomodoroTimerSettingsReducer: Reducer<PomodoroTimerSettingsState, PomodoroTimerSettingsAction, PomodoroTimerSettingsEnvironment> = .combine(
.init { state, action, environment in
switch action {
case .binding:
return environment.userDefaults.setPomodoroTimerSettings(.init(
intervalSeconds: state.intervalTime * 60,
shortBreakIntervalSeconds: state.shortBreakTime * 60,
longBreakIntervalSeconds: state.longBreakTime * 60,
intervalCountBeforeLongBreak: state.intervalCountBeforeLongBreak)
)
.fireAndForget()
...
}
}
).binding()

By doing the above, Binding can be achieved in View as follows.

Picker("", selection: viewStore.binding(\.$intervalTime)) {
...
}

For more information, please refer to the following articles.

Firebase Client

I created a simple Firebase Client to receive the Effect. Even if Firebase is offline, it will synchronize when it comes back online, so the save process returns Effect<None, Never> as a synchronization process.

public struct FirebaseAPIClient {
// Auth
public var checkUserStatus: () -> Effect<AppUser, Never>
public var signInAnonymously: () -> Effect<None, APIError>
public var signUp: (_ email: String, _ password: String) -> Effect<None, APIError>
// Pomodoro History
public var savePomodoroHistory: (_ history: PomodoroTimerHistory) -> Effect<None, Never>

...
}

Conclusion

Since this was a simple application, I feel that the amount of code increased with the use of TCA. However, it was good to learn how to manage states in TCA and how to split and integrate Reducer.

The code can be found here
https://github.com/yyokii/PomodoroApp

I would be happy to get a star if it is helpful.

--

--