Swift Combine: Tap Tap Sample App

|

Swift Combine: Tap Tap Sample App is a sample application that demonstrates the use of SwiftUI and Combine framework for reactive programming in iOS. The app provides an interactive login flow with data binding, state management, and API requests using Combine, creating a smooth and responsive user experience. Perfect for developers looking to learn the basics of Combine and SwiftUI in a practical, hands-on way.

Alamofire Dependency

For this example was used Alamofire, so add this SPM dependency in your project:

https://github.com/Alamofire/Alamofire

Then add to your target:

The Content View

The ContentView in this code serves as the main entry point to the app’s interface, organizing the view structure based on whether the user is logged in. Two key objects, loginViewModel and appManager, are created using @StateObject. This property wrapper ensures that these objects are instantiated once and persist throughout the lifecycle of the view. As @StateObject, they become the single sources of truth for the login state and app-wide settings or shared data.

Within the view’s body, a NavigationView wraps the content, allowing for navigable, stack-based transitions within the app. Inside this NavigationView, the view dynamically switches between HomeView and LoginView depending on the isLoggedIn property of loginViewModel. When isLoggedIn is true, HomeView is displayed, and appManager is passed to it as an environment object, which means that appManager becomes available not just to HomeView but to any view within its hierarchy. This approach enables the sharing of app-wide data, such as user preferences or shared resources, across multiple views without having to pass appManager explicitly to each one.

If isLoggedIn is false, LoginView is shown instead, with loginViewModel passed as an argument. This allows LoginView to handle login-related actions and state updates. For instance, if the user successfully logs in, loginViewModel updates isLoggedIn to true, which immediately triggers SwiftUI to re-render ContentView, swapping LoginView out for HomeView. This setup creates a responsive and intuitive flow where the app automatically transitions between login and home screens based on the user’s authentication status.

In essence, ContentView uses a combination of @StateObject for state persistence, conditional rendering for seamless view transitions, and @EnvironmentObject for efficient data sharing, creating a streamlined and reactive user experience in SwiftUI.

The Login

The LoginView in this code is a SwiftUI view responsible for rendering the login screen interface and linking it to the LoginViewModel. It makes use of the MVVM (Model-View-ViewModel) pattern by observing the viewModel through @ObservedObject, which allows the UI to reactively update whenever the view model’s properties change.

The body of LoginView is composed of a ZStack that layers a login form over an optional loading overlay. Within the main VStack, the user is greeted with a title, followed by fields for email and password input. These fields are bound to the email and password properties in the viewModel using $viewModel.email and $viewModel.password, so any changes to the fields are instantly reflected in the view model’s state. This makes it easy to validate user input and adjust the interface accordingly. For example, the button enabling login is only active when the input meets certain criteria, as dictated by viewModel.isLoginButtonEnabled.

The view also includes an optional error message, which displays in red text when viewModel.loginErrorMessage is populated. This error message provides feedback to the user in case of login failure, such as invalid credentials.

Additionally, there’s a “Remember me” toggle, allowing users to save their login status, which is bound to viewModel.rememberMe. Below the form fields, two buttons handle login actions: a primary “Login” button, which triggers the viewModel.login() method, and a secondary “Enter as guest” button, which calls viewModel.loginAsGuest(). Each button dynamically updates based on viewModel properties; for instance, the “Login” button changes color and becomes disabled if isLoginButtonEnabled is false, providing visual feedback on when the form is ready to be submitted.

Finally, the ZStack allows for a loading overlay, LoadingView, which appears above the form when viewModel.isLoading is true. This visual indicator informs the user that their login request is being processed, creating a more polished and responsive experience.

Overall, LoginView uses SwiftUI’s reactive approach to build a responsive, user-friendly login interface. It tightly integrates with LoginViewModel, enabling real-time updates based on user actions and the app’s authentication state. This setup provides a cohesive and intuitive experience, where each UI element is dynamically controlled by the view model’s state.

Swift Combine in LoginViewModel

In this code, Publishers.CombineLatest is used to monitor changes to both the email and password properties and to determine when the login button should be enabled.

Here’s how it works in detail:

Publishers.CombineLatest($email, $password)
    .map { email, password in
        return !email.isEmpty && !password.isEmpty && email.contains("@")
    }
    .assign(to: &$isLoginButtonEnabled)

Explanation of Publishers.CombineLatest

  • $email and $password: These are the @Published properties of email and password, automatically turned into publishers by Swift’s property wrappers. When either email or password changes, they emit a new value.
  • Publishers.CombineLatest: Publishers.CombineLatest is a Combine operator that takes two publishers ($email and $password in this case) and emits a new combined value whenever either of the publishers produces a new value. It doesn’t emit a new value until both publishers have emitted at least one value.
  • Purpose in this Code: Here, CombineLatest allows LoginViewModel to reactively check if both email and password meet specific conditions every time either property is updated. This ensures that the login button will only be enabled when:
    • email and password are not empty.
    • email contains the @ symbol, which is a basic validation for email format.
  • map Operator: The map operator takes the latest email and password values and applies a transformation to check if they meet the criteria for enabling the login button. The result of this transformation is a Bool that is true if both fields are valid and false otherwise.
  • assign(to: &$isLoginButtonEnabled): The assign(to:) operator assigns the result of the map operation to the isLoginButtonEnabled property. With assign(to: &$isLoginButtonEnabled), any changes to email or password automatically update isLoginButtonEnabled, which controls whether the login button is enabled in the UI.

Summary of Purpose

By using Publishers.CombineLatest, this code enables reactive validation for the login form. The login button will only be enabled when both email and password fields are filled in correctly. This setup avoids the need to write complex logic for button enabling elsewhere, as it’s automatically handled by the reactive chain created with Combine.

The NetworkManager using Alamofire

The NetworkManager class is designed to centralize and simplify network requests in the app, using the Alamofire library to handle HTTP communication. It’s implemented as a singleton, meaning only one instance of NetworkManager will ever be created. This is achieved by making the initializer private and providing access to the single instance through NetworkManager.shared. The singleton approach is useful in this case because it allows all network requests to be managed through one consistent interface across the app.

At the core of this class is a private request method, which is generic and can handle any type conforming to the Decodable protocol. This generic approach allows the method to automatically decode JSON responses into specific Swift model types, reducing the need for manual JSON parsing. The request method takes a URL, HTTP method, optional parameters, and optional headers. It also decides on the appropriate parameter encoding based on the HTTP method, using URL encoding for GET and DELETE requests and JSON encoding for POST and PUT requests. By using AF.request, the method initiates an Alamofire request, specifying the necessary parameters, encoding, and headers. It then uses .validate() to ensure the response status code is within an acceptable range and .responseDecodable(of: T.self) to decode the JSON response directly into the specified model type. The decoded result, whether successful or as an error, is then passed back through a completion handler, making it easy for the caller to handle success or failure.

To make network requests even simpler, NetworkManager includes convenience methods like getRequest, postRequest, putRequest, and deleteRequest. These methods wrap around the request method, each specifying a different HTTP method. This design allows for clean, readable code by letting the caller make requests with specific methods without repeatedly specifying HTTP method types.

In summary, NetworkManager is built to make API requests straightforward and consistent. By centralizing network logic, it leverages Alamofire’s capabilities for making requests, validating responses, and decoding JSON into Swift models. The use of a generic request method paired with convenience wrappers allows for flexible and maintainable code, ensuring that API calls across the app remain consistent and easy to manage.

The HomeView

The HomeView in this code is a SwiftUI view that serves as the main interface for navigating between different sections of the app, using a tabbed layout. It’s structured with two main views, StoreView and LobbyView, that the user can switch between using a TabView. This view also interacts with AppManager, which likely manages shared data or actions across different parts of the app.

The property @State private var selectedTab = 1 keeps track of the currently selected tab. It’s set to 1 initially, which means the LobbyView tab will be selected when the view first appears. The @EnvironmentObject var appManager: AppManager property allows HomeView to access the AppManager instance injected into the SwiftUI environment. This enables HomeView to use shared functionality from AppManager without needing to pass it explicitly as a parameter to each child view.

In the body, a TabView is created to manage the different tabs. The TabView uses $selectedTab as a binding, allowing HomeView to control which tab is currently active. The first tab is StoreView, which is created with appManager as a parameter. This tab displays a Label with a storefront icon and the label “Store”. It’s assigned a .tag(0) to uniquely identify it within the TabView. The second tab is LobbyView, displayed with a house icon and the label “Lobby”. This tab is assigned .tag(1), making it the default selected tab due to the initial value of selectedTab.

In the .onAppear modifier, selectedTab is set to 1 whenever HomeView appears, ensuring that LobbyView is selected by default each time this view comes into focus. Additionally, appManager.startTapIncrementLoop() is called. This likely starts a background process or timer managed by AppManager to increment a tap count periodically, a feature relevant to the app’s functionality. This ensures that the tap incrementing begins when the user reaches the home screen, setting up any background activity needed for the rest of the app.

Overall, HomeView acts as the central hub for navigation within the app, allowing the user to switch between different sections through a tabbed interface. It also initializes essential background tasks via AppManager, making it a key part of the app’s user experience and functionality. The use of @State and @EnvironmentObject helps manage the app’s state and shared data effectively, making HomeView both interactive and responsive to the app’s needs.

The StoreView

    @ObservedObject var viewModel: StoreViewModel
    
    init(appManager: AppManager) {
        _viewModel = ObservedObject(wrappedValue: StoreViewModel(appManager: appManager))
    }

This code defines viewModel as an @ObservedObject of type StoreViewModel, allowing StoreView to observe changes in viewModel and automatically update the UI whenever properties within viewModel change. @ObservedObject makes viewModel reactive to any published data in StoreViewModel, which is essential for keeping the view in sync with dynamic data.

The initializer takes an AppManager instance as a parameter and uses it to set up viewModel. In this context, appManager likely provides shared app-wide functionality or data needed by StoreViewModel. Within the initializer, _viewModel (using the underscore notation to access the ObservedObject property wrapper directly) is assigned a new instance of StoreViewModel, initialized with appManager as an argument. This technique ensures that StoreView is created with a properly configured StoreViewModel, which has access to appManager from the beginning.

This approach allows for dependency injection, making StoreView more modular and testable, as different instances of AppManager could be passed in if needed, such as during unit tests. By setting up viewModel this way, StoreView remains decoupled from the creation details of StoreViewModel and any global instances of AppManager. This setup encapsulates the dependencies of StoreView within the initializer, providing a clean and flexible way to inject dependencies and reactively manage state updates in the view.


The LobbyView

The LobbyView is a straightforward view that primarily serves as an interface for monitoring and adding taps within the app. It uses @EnvironmentObject to access appManager, which is responsible for managing the app’s shared state, including the tap count.

In the view’s layout, LobbyView consists of a VStack containing a StatusBarView at the top and a tappable area below. The tappable area is created using a ZStack, which layers a white rectangle with a black border and a “Tap me!” label centered on top. The rectangle uses the .onTapGesture modifier to listen for taps, and each tap triggers appManager.incrementTap(), which likely increases the app’s tap count.

The purpose of LobbyView is simple: it provides a user interface where taps can be registered and counted, allowing the app to track user interaction through the appManager. The view’s design is minimal, focusing on this single function without additional complexity.

The StatusBarView

The StatusBarView is a simple SwiftUI view that displays the current number of taps, as managed by the appManager. This view uses the @EnvironmentObject property wrapper to access an instance of AppManager, a shared object in the app’s environment, which holds and manages the tapCount.

In the body of StatusBarView, a Text view displays the tap count by interpolating appManager.tapCount within the string “Taps: (appManager.tapCount)”. This dynamically updates whenever tapCount changes, as SwiftUI will re-render the view to reflect the updated value.

The Text view is styled with a medium-weight, 16-point font and is configured to expand horizontally to fill the available width using frame(maxWidth: .infinity). It also includes padding around the text for better spacing, a light gray background color (Color(.systemGray5)) for contrast, and a rounded corner radius of 8 to give it a subtle, modern look.

Overall, StatusBarView provides a clean and reactive way to display the tap count, updating automatically as the value changes in AppManager. This makes it a straightforward, reusable component for showing the status of taps across the app.

The AppManager

The AppManager class is an ObservableObject that manages the state and behavior related to taps within the app. It centralizes the tap counting logic, provides functionality for incrementing and decrementing taps, and saves relevant data to UserDefaults to persist it between app launches. This design allows AppManager to be shared across multiple views in SwiftUI, where views can observe it and update automatically when tap-related properties change.

At the top, AppManager defines three keys (tapCountKey, increasePerTapKey, factoryIncreaseTapKey) that it uses with UserDefaults for persisting data. The class has three primary @Published properties that trigger UI updates when they change:

  • tapCount, which tracks the current number of taps,
  • increasePerTap, which defines how much tapCount should increase per user tap,
  • factoryIncreaseTap, which determines the increase applied by a factory mechanism over time.

The initializer fetches saved values from UserDefaults, initializing tapCount, increasePerTap, and factoryIncreaseTap with stored values or default values if no saved data exists. If increasePerTap is zero (which might happen if it hasn’t been set before), it defaults to 1.

incrementTap adds increasePerTap to tapCount and then saves the updated tapCount to UserDefaults. The factoryTapIncrease function, marked with @objc for compatibility with Timer, adds factoryIncreaseTap to tapCount and saves it as well. This allows AppManager to periodically increase tapCount automatically, mimicking a “tap factory.”

The startTapIncrementLoop and stopTapIncrementLoop methods manage the creation and cancellation of a Timer that calls factoryTapIncrease every second. When started, the timer continuously increases tapCount by factoryIncreaseTap each second until stopped, enabling an automated tap increment.

Other utility functions include decreaseTap, which subtracts a given count from tapCount; increasePerTap and increaseFactoryTap, which add values to increasePerTap and factoryIncreaseTap, respectively, and save the updated values to UserDefaults. Finally, saveTapCount is a helper function that saves tapCount to UserDefaults for persistence.

In summary, AppManager is a shared data manager and state controller for the app’s tap mechanics. It handles user taps, automated factory increments, and persistent data storage, allowing any observing SwiftUI view to reactively update when the tap-related properties change.

Visit the repository to get the source code

https://github.com/san0suke/swiftui-combine-sample

Leave a Reply

Your email address will not be published. Required fields are marked *