Skip to content

Instantly share code, notes, and snippets.

@buh
Last active December 14, 2023 09:42
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save buh/63e1dd41b267bb65baacf03b8557786a to your computer and use it in GitHub Desktop.
Save buh/63e1dd41b267bb65baacf03b8557786a to your computer and use it in GitHub Desktop.
Example for a Finite-State Machine
/// The authentication state.
enum AuthenticationState: StateType {
// A list of events.
enum Event {
case userSignIn(email: String, password: String)
case accessTokenReceived(AccessToken)
case userReceived(User)
case userSignedOut
}
// A list of states.
case userSignedOut
case userSigningIn(email: String, password: String)
case authenticated(AccessToken)
case signedIn(AccessToken, User)
// The initial value.
static var initial: Self = .userSignedOut
// The reducer function.
mutating func reduce(with event: Event) {
switch (event, self) {
// User is signing in with credentials.
case (.userSignIn(let email, let password), .userSignedOut):
self = .userSigningIn(email: email, password: password)
return
// Access token received when user was signed out.
case (.accessTokenReceived(let accessToken), .userSignedOut):
self = .authenticated(accessToken)
return
// Access token received when the user was already signed in.
// Just update the access token with a new one.
case (.accessTokenReceived(let accessToken), .signedIn(_, let user)):
self = .signedIn(accessToken, user)
return
}
// The user is received after authentication.
case (.userReceived(let user), .authenticated(let accessToken)):
self = .signedIn(accessToken, user)
return
}
// The user is received when the user was already signed in.
// Just update the user with a new one.
case (.userReceived(let user), .signedIn(let accessToken, _)):
self = .signedIn(accessToken, user)
return
}
// Simply sign out after the user's action.
case (.userSignedOut, _):
self = .userSignedOut
return
}
// It is important to stop the app in debug mode to deal with unhandled events.
assertionFailure("Unexpected behaviour for state: \(self). The event wasn't handled: \(event)")
}
}
// MARK: - State Extension
extention AuthenticationState {
/// Checks if the user is authenticated.
var isAuthenticated: Bool {
if case .authenticated = self {
return true
}
return isSignedIn
}
/// Checks if the user is signed in.
var isSignedIn: Bool {
if case .signedIn = self {
return true
}
return false
}
/// Checks if the user is signed out.
var isSignedOut: Bool {
if case .userSignedOut = self {
return true
}
return false
}
/// Checks if the authentication is in progress.
var isSigningIn: Bool {
if case .userSigningIn = self {
return true
}
if case .authenticated = self {
return true
}
return false
}
/// Returns the authenticated user.
var user: User? {
if case .signedIn(_, let user) = self {
return user
}
return nil
}
}
// MARK: - Authentication Manager
final class AuthenticationManager {
// The state already has an initial value,
// so there is no need to initialise it when registering.
@Registered var state: AuthenticationState
// Inject a client dependency to make requests to the server.
@Injected var client: Client
/// Define the state reducer in the manager,
/// because only the manager has access to change it.
func reduceState(event: AuthenticationState.Event) {
_state.reduceState(event: event)
// We can also carry out additional activities
// as part of the manager's responsibility.
if case .authenticated = state {
fetchUser { error in /* ... */ }
}
}
/// Sign-in with credentionals.
/// In the completion block, we don't need to return a new state.
/// It needs to be changed via the reducer function and thus
/// its subscribers will be able to receive the new value.
func signIn(email: String, password: String, _ completion: @escaping (ClientError?) -> Void) {
reduceState(event: .userSignIn(email: email, password: password))
client.post(.signIn(email: email, password: password)) { [unowned self] (result: Result<AccessToken, ClientError>) in
do {
let accessToken = try result.get()
reduceState(event: .accessTokenReceived(accessToken))
completion(nil)
} catch {
completion(error)
}
}
}
/// Fetch a user object.
func fetchUser(_ completion: @escaping (ClientError?) -> Void) {
client.get(.user) { [unowned self] (result: Result<User, ClientError>) in
do {
let user = try result.get()
reduceState(event: .userReceived(user))
completion(nil)
} catch {
completion(error)
}
}
}
}
// MARK: - Root View Controller
final class RootViewController: UIViewController {
@Registered var authenticationManager: AuthenticationManager
override func viewDidLoad() {
super.viewDidLoad()
// Subscribe to changes in authentication status.
onChange(authenticationManager.$state) { [weak self] in
self?.handleAuthenticationState($0)
}
}
// Handles the authentication state to show the corresponding view controller.
private func handleAuthenticationState(_ state: AuthenticationState) {
if state.isSignedOut {
showLoginViewController()
} else if state.isSignedIn {
showContentViewController()
}
}
// ... some implementation for showLoginViewController() and showContentViewController()
}
// MARK: - Login View Controller
final class LoginViewController: UIViewController {
// As the AuthenticationManager has already been registered
// in the parent RootViewController it can be injected here.
@Injected var authenticationManger: AuthenticationManager
/* UI elements */
private func loginButtonDidTap() {
guard let email = emailTextField.text,
!email.isEmpty
let password = passwordTextField.text,
!password.isEmpty else {
// Show the message with incorrect fields.
return
}
// Sends a sign-in request and if success the RootViewController will
// dismiss this view controller and show the ContentViewController.
authenticationManger.signIn(email: email, password: password) { [weak self] error in
if let error = error {
self?.showErrorAlert(error)
}
}
}
}
// MARK: - Content View Controller
final class ContentViewController: UIViewController {
// Inject the user from the AuthenticatedState and every time
// the user data is updated we get an updated object.
@Injected(\AuthenticationState.user) var user: User?
private let usernameLabel = UILabel(frame: .zero)
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
onChange($user) { [usernameLabel] user in
usernameLabel.text = "Welcome, \(user?.name ?? "user")!"
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment