In this tutorial, we’ll explore how to create a simple “Tap Tap” app using RxSwift, a powerful reactive programming framework for iOS.
By harnessing the power of RxSwift, we can manage asynchronous events in a clean and manageable way, making it perfect for building interactive apps.
We’ll walk through setting up a tap counter that responds in real-time, integrating RxSwift to handle taps, and creating a dynamic user experience.
Let’s dive in and see how RxSwift can simplify reactive event handling in iOS development!
Most important RXSwift commands used
- PublishSubject
- BehaviorRelay
- DisposeBag
- RxAlamofire.requestData
- TextField.rx.text.orEmpty.bind
- Switch.rx.isOn
- Button.rx.tap.flatMapLatest
- .subscribe
- .disposed(by: disposeBag)
Creating the Project
We chose to use UIKit for this project because it integrates more seamlessly with RxSwift, which is specifically designed to enhance reactive programming in UIKit-based applications. While SwiftUI has its own reactive framework (Combine), UIKit allows us to leverage RxSwift’s extensive operators and tools, making it ideal for managing asynchronous events and complex UI interactions in this project. This approach ensures we can build a responsive and interactive app efficiently, using RxSwift’s capabilities to their fullest within the UIKit environment.
To get started, it’s essential to import RxSwift and RxAlamofire, as these libraries provide the reactive tools needed for handling asynchronous tasks and network requests in a reactive style. You can add these libraries to your project using either Swift Package Manager (SPM) or CocoaPods. In this project, we chose to use Swift Package Manager for its simplicity and native integration with Xcode. With SPM, we can easily manage dependencies directly in Xcode, ensuring updates and compatibility are handled efficiently. This setup allows us to leverage RxSwift’s powerful operators and RxAlamofire’s reactive network handling seamlessly.
While using RxAlamofire isn’t strictly necessary, we included it in this project to demonstrate how reactive programming can simplify network requests. RxAlamofire builds on top of Alamofire, wrapping network calls in RxSwift observables, which allows us to handle responses in a reactive, chainable way. This integration makes it easy to manage asynchronous network tasks alongside other reactive elements in the app, providing a clear, streamlined approach for handling requests and responses as part of the reactive workflow.
Using Swift Package Manger import:
https://github.com/ReactiveX/RxSwift.git (5.2.0)
https://github.com/RxSwiftCommunity/RxAlamofire.git (5.1.0)
Then add it to your target:
Login Screen
The login screen was created specifically to demonstrate how RxAlamofire can be used for network requests in a reactive setup. By simulating a login process, we can show how RxAlamofire wraps network calls, allowing us to handle authentication requests reactively. This example highlights how RxAlamofire integrates seamlessly with RxSwift, making it easy to initiate, observe, and manage asynchronous login requests while keeping the code clean and responsive. This approach provides a practical example of handling real-time network communication in a reactive architecture.
First of all create the LoginViewController:
The key difference in this login screen is how RxSwift performs binding reactively, streamlining the flow of data between UI components and the ViewModel. In setupBindings()
, RxSwift binds each UI element—such as loginTextField
, passwordTextField
, and rememberSwitch
—directly to properties in the ViewModel
, ensuring that changes are automatically synchronized. For example:
- Text Fields: The
username
andpassword
fields use reactive bindings that immediately update the ViewModel with any user input. - Switch and Button States: The state of
rememberSwitch
is bound to a ViewModel property, while theisLoginButtonEnabled
property in the ViewModel is reactively bound to theloginButton
‘sisEnabled
state. - Login Button Action: When the
loginButton
is tapped, RxSwift initiates a login request using RxAlamofire, managing loading states and error handling reactively. Any errors automatically trigger a dialog, while successful login responses navigate to the home screen. - Guest Login: The guest login button reacts similarly, executing logic in the ViewModel and navigating when tapped.
This reactive binding with RxSwift not only simplifies the code by removing manual updating and monitoring but also makes it more responsive, as the UI dynamically reacts to changes in real time.
private func setupBindings() {
loginTextField.rx.text.orEmpty
.bind(to: viewModel.username)
.disposed(by: disposeBag)
passwordTextField.rx.text.orEmpty
.bind(to: viewModel.password)
.disposed(by: disposeBag)
rememberSwitch.rx.isOn
.bind(to: viewModel.rememberMe)
.disposed(by: disposeBag)
viewModel.isLoginButtonEnabled
.bind(to: loginButton.rx.isEnabled)
.disposed(by: disposeBag)
loginButton.rx.tap
.flatMapLatest { [unowned self] in
showLoadingScreen()
return self.viewModel.login()
.catchError { error in
self.presentErrorDialog()
print("Login error: \(error.localizedDescription)")
return Observable.empty()
}
}
.subscribe (onNext: { response in
print("Response: \(response.token)")
self.navigateToHome()
}, onError: { error in
self.presentErrorDialog()
print("Error: \(error.localizedDescription)")
})
.disposed(by: disposeBag)
guestButton.rx.tap
.do { _ in
print("taping guest")
self.viewModel.loginAsGuest()
}
.subscribe { _ in
self.navigateToHome()
}
.disposed(by: disposeBag)
}
NetworkService
In this project, the core logic for handling RxAlamofire requests is encapsulated in NetworkService.swift
. This class manages network requests in a reactive, streamlined way, using RxAlamofire to simplify the flow of HTTP requests and responses.
The NetworkService
class includes a private request
method that handles requests of various HTTP methods, such as GET, POST, PUT, and DELETE, by defining each one as a public method (e.g., get
, post
). Here’s how it works:
- Reactive HTTP Requests: Using
RxAlamofire.requestData
, each request is wrapped in an observable, which allows it to emit the response data or an error. - Response Handling: The
request
method checks the HTTP status code. If successful, the data is passed along the observable chain; otherwise, it emits an error with a message. - Decoding Data: The response data is decoded using
JSONDecoder
, mapping it to the expected data type (T: Decodable
). - Error Handling with Default Values: If decoding fails,
catchError
provides default values for common types, ensuring the app remains responsive even if the request encounters an error.
This setup enables us to perform network requests in a clean, type-safe way while allowing other parts of the app to handle results reactively. With NetworkService
, we can integrate RxAlamofire into the app’s reactive architecture, making asynchronous networking straightforward and consistent.
LoginViewModel
The LoginViewModel
class handles the logic for managing the login process in a reactive way using RxSwift. This ViewModel encapsulates both the data-binding of user input fields and the logic for making network requests, allowing the LoginViewController
to remain focused on UI tasks.
Here’s a breakdown of the main components:
- Reactive Properties:
username
,password
, andrememberMe
areBehaviorSubject
s that hold the current values entered by the user and are used to track updates in real time.isLoginButtonEnabled
is an observable derived fromusername
andpassword
. It usescombineLatest
to monitor changes in both fields and enables the login button only when both fields are non-empty.
- Dependencies:
networkService
is an instance ofNetworkService
, used here to handle network requests for login. It’s injected as a dependency, making it easy to mock for testing.userDefaultData
stores user data and the “Remember Me” preference inUserDefaults
. It’s also injected, providing flexibility in data storage management.
login()
Method:- This function performs the actual login request by calling
networkService.post
with the user’s email and password as parameters. - The method returns an
Observable
ofLoginResponseDTO
, which allows theViewController
to subscribe to the result and react accordingly. - Using
.do(onNext:)
, the login response is saved touserDefaultData
, along with the current value ofrememberMe
.
- This function performs the actual login request by calling
- Other Functions:
hasRememberMeSave()
: Checks if the “Remember Me” option is enabled and if there’s stored user data.loginAsGuest()
: Sets the “Remember Me” option for guest users.
This reactive approach keeps the login logic encapsulated in the LoginViewModel
, making it easy to manage user input, perform network requests, and handle login states efficiently. The ViewModel ensures a clear separation of concerns, with each element of the login flow being reactive, clean, and testable.
HomeViewController
The HomeViewController
class is a UITabBarController
that serves as the main navigation hub of the app, featuring two tabs: Store and Lobby. It sets up and configures these tabs and interacts with the HomeViewModel
to start certain reactive processes.
Key Components:
- ViewModel:
HomeViewController
initializes aHomeViewModel
instance to handle data and business logic related to the home screen.- The call to
viewModel.startTapFactory()
inviewDidLoad()
activates any reactive processes, such as incrementing taps, which may be managed by theViewModel
.
- UI Setup (
setupUI()
):- Background: Sets a white background color for a clean appearance.
- Tabs:
- First Tab (Store): Configures a
StoreViewController
instance and assigns it aUITabBarItem
with the title “Store” and an icon of a storefront. This tab serves as the main entry point for the store. - Second Tab (Lobby): Configures a
LobbyViewController
instance with aUITabBarItem
that displays “Lobby” with a house icon. - Selected Tab: Sets the initial tab to be the “Lobby” (index
1
), so it’s shown when the user first enters theHomeViewController
.
- First Tab (Store): Configures a
- Navigation Bar Visibility:
- The
viewWillAppear(_:)
method hides the navigation bar, creating a full-screen experience and keeping the focus on the tab bar and the content of each tab.
- The
Purpose:
This HomeViewController
serves as a centralized UI controller, simplifying navigation by providing quick access to the Store
and Lobby
views. The integration with HomeViewModel
allows it to manage reactive processes, keeping logic separate and ensuring that the UI remains responsive and easy to navigate.
LobbyViewController
The LobbyViewController
is a simple, interactive screen that encourages user engagement by prompting the user to tap a specific area. This screen is built using UIKit and RxSwift to manage user interactions reactively.
Key Components:
- UI Elements:
statusBarView
: A custom view that likely displays status-related information and sits at the top of the screen.tapHereLabel
: A label prompting the user to “Tap me!” in bold, large text, making it clear that the view is interactive.tapView
: A borderedUIView
that serves as the tappable area, visually separating it as an area to interact with.
- UI Layout (
setupUI()
):- The screen has a white background, with
statusBarView
at the top. tapView
fills most of the screen below thestatusBarView
, withtapHereLabel
centered within it.- The layout is set up with constraints to ensure responsive positioning, adapting to different screen sizes.
- The screen has a white background, with
- Reactive Binding with RxSwift (
setupBindings()
):- A
UITapGestureRecognizer
is added totapView
, allowing taps to trigger reactive events. - Using
tapGesture.rx.event
, the gesture is bound to a reactive chain that, on each tap, callsTapCountManager.shared.incrementTapCount()
to increase the tap count. - The RxSwift
disposeBag
manages the subscription, ensuring memory is handled correctly.
- A
Purpose:
The LobbyViewController
provides an interactive experience for users, encouraging taps and increasing a global tap count managed by TapCountManager
. By utilizing RxSwift, the view controller remains clean and reactive, responding instantly to user interaction and updating the tap count efficiently. This setup exemplifies how RxSwift can be used to handle UI gestures in a reactive, declarative manner, keeping the UI responsive and concise.
StatusBarView
The StatusBarView
is a custom UIView
component that displays the current tap count in real-time. It’s designed to provide feedback on the user’s progress by showing the number of taps dynamically, using RxSwift for reactive updates.
Key Components:
tapCountManager
Dependency:tapCountManager
is an instance ofTapCountManagerProtocol
(usingTapCountManager.shared
by default) and provides access to the current tap count. This is where the view retrieves the latest tap count, making theStatusBarView
responsive to changes in the global tap count.
- UI Element (
tapLabelScore
):tapLabelScore
is aUILabel
centered withinStatusBarView
, styled with medium font and centered alignment to clearly display the current number of taps.
- Initialization:
- The
StatusBarView
has two initializers:init(frame:)
andinit(coder:)
, both callingsetupView()
to configure the view’s appearance and layout.
- The
- UI Setup (
setupView()
):- The background color is set to a light gray (
systemGray5
) to make the status bar visually distinct. tapLabelScore
is added to the center of the view using Auto Layout constraints, ensuring it’s centrally aligned.
- The background color is set to a light gray (
- Reactive Binding (
setupBindings()
):- The
setupBindings()
method bindstapCountManager.tapCount
totapLabelScore.rx.text
:tapCountManager.tapCount
emits the latest tap count as an observable.- The
.map { "Taps: \($0)" }
operator formats the tap count to display as"Taps: [count]"
. .bind(to: tapLabelScore.rx.text)
updates the label’s text reactively.- The subscription is disposed of in
disposeBag
to manage memory automatically.
- The
Purpose:
The StatusBarView
provides a clear and responsive display of the user’s tap count. By using RxSwift for reactive data binding, it automatically updates the tap count without requiring additional code in view controllers. This setup illustrates how custom views can integrate RxSwift for real-time updates, keeping the UI responsive and cleanly separated from business logic.
StoreViewController
The StoreViewController
is a UIViewController
that displays the in-game store where players can purchase items using taps, with each item displayed in a table view. This controller is built with RxSwift, allowing it to react to real-time changes in the tap count and trigger actions based on certain conditions, such as when the player wins the game.
Key Components:
- ViewModel and RxSwift Integration:
StoreViewController
relies onStoreViewModel
for managing data and business logic, making the controller primarily focused on UI tasks.disposeBag
handles the disposal of subscriptions to RxSwift observables, preventing memory leaks.
- UI Elements (
setupUI
):statusBarView
displays a summary of the player’s progress or status and is anchored to the top of the view.tableView
is used to list store items, each represented by a row, and fills the rest of the screen belowstatusBarView
.
- Bindings (
setupBindings
):- Tap Count Binding: Observes the
tapCount
fromviewModel
, reloading the table whenever the count changes, so the UI remains updated based on the player’s available taps. - Win Dialog: Observes
viewModel.showWinDialog
, triggeringpresentWinDialog()
when the player reaches the winning conditions.
- Tap Count Binding: Observes the
- Present Win Dialog (
presentWinDialog
):- Displays an alert congratulating the player when they win the game. This function is triggered by the
showWinDialog
observable from theViewModel
.
- Displays an alert congratulating the player when they win the game. This function is triggered by the
- TableView DataSource:
numberOfRowsInSection
: Returns the number of items in theviewModel.items
array.willSelectRowAt
: Enables row selection only if the player has enough taps to buy the item.cellForRowAt
: Configures each cell to display item details (icon, name, and price) and adjusts the cell’s interactivity and opacity based on the player’s ability to buy it.
- TableView Delegate (
didSelectRowAt
):- Handles the purchase action when an item is selected by calling
viewModel.purchase(item:)
. - After a purchase, the table view is reloaded to update item states and ensure accurate UI representation.
- Handles the purchase action when an item is selected by calling
Purpose:
StoreViewController
provides a responsive in-game store experience where players can purchase upgrades using taps. Through RxSwift bindings, it stays updated with the latest tap count and dynamically adjusts item accessibility and interactivity. The presentWinDialog
offers feedback when the player achieves the game’s win conditions, creating a seamless, reactive user experience.
Leave a Reply