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 ofemail
andpassword
, automatically turned into publishers by Swift’s property wrappers. When eitheremail
orpassword
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
allowsLoginViewModel
to reactively check if bothemail
andpassword
meet specific conditions every time either property is updated. This ensures that the login button will only be enabled when:email
andpassword
are not empty.email
contains the@
symbol, which is a basic validation for email format.
map
Operator: Themap
operator takes the latestemail
andpassword
values and applies a transformation to check if they meet the criteria for enabling the login button. The result of this transformation is aBool
that istrue
if both fields are valid andfalse
otherwise.assign(to: &$isLoginButtonEnabled)
: Theassign(to:)
operator assigns the result of themap
operation to theisLoginButtonEnabled
property. Withassign(to: &$isLoginButtonEnabled)
, any changes toemail
orpassword
automatically updateisLoginButtonEnabled
, 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 muchtapCount
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.
Leave a Reply