Building a SwiftUI Weather App with Lasso, Part 2: Adding the SwiftUI Views
In Part 1 of this tutorial, we set up the API and the Lasso building blocks for our SwiftUI app. If you didn’t read the first article in this series, be sure to check it out here.
Now comes the next step: implementing SwiftUI View
s with Lasso. In this part, we’ll implement the Home screen to illustrate key concepts when using Lasso and SwiftUI together. To see the implementation of the Detail and Map screens, be sure to review the source code.
If you’re unfamiliar with Lasso, see the documentation and WW Tech articles that introduce Lasso concepts.
- Lasso: Introducing a new architectural framework for iOS
- Lasso: An introduction to Flows
- Lasso documentation
This article assumes some knowledge of SwiftUI. As a result, this article excludes parts of the code and does not explain SwiftUI concepts; instead, this focuses on Lasso concepts and changes.
The project source code can be found here. As we go through this tutorial, be sure to follow along with the original project source code to add the necessary code blocks and files.
Defining a Screen
If you’re familiar with SwiftUI, you know that a View
can be as small as an icon and as large as an infinitely scrolling list. As a result, Lasso defines a Screen
as a View
that is not the child of a parent View
— for example, a home screen, a profile screen, a modal sheet, etc.
In this sample application, we have three Screen
s: Home, Detail, and Map. Each has child View
s, but each is not a child of any higher-level View
.
We’ll start by creating the Home screen. This will serve as the entry point of our app where users can search by zip code or select a recently searched city. To configure this screen, we’ll conform WeatherHomeView
to LassoView
.
Let’s take a look at the LassoView
protocol.
You’ll notice there is one requirement: A LassoView
must have a ViewStore
property called store
. The high-level views must conform to LassoView
to leverage static func createScreen(with store: ConcreteStore) -> Screen
when using a ScreenModule
. This function wraps a SwiftUI View
in a UIHostingController
under the hood.
The
store
property automatically conforms toObservableObject
. This conformance allows us to use the@ObservedObject
property wrapper, which automatically re-renders a SwiftUIView
when state changes occur.
Implementing the View
Next, we’ll begin implementing the components of our Home screen using SwiftUI. Before we do that, let’s take a look at the Home screen and remind ourselves of the major screen components.
The screenshot shows roughly three major components to our Home screen: a navigation bar title, a search bar, and a recent searches list. To further modularize this screen, we’ll extract the content of the list as a recent search row.
Recent search row
We’ll start by writing the list row component to display a recent search. To do this, we’ll create a View
that accepts a RecentSearch
data model instance. Using this data model, we’ll design a UI component with native SwiftUI components, such as VStack
and Text
, to display the city name and zip code from the search instance.
You can see
RecentSearchPreview
doesn’t conform toLassoView
because thisView
is not considered aScreen
and does not have anyObservedObject
s.
Later, we’ll wrap this UI component with a NavigationRow
, a custom UI component that accepts the view’s ViewStore
and an Action
value.
Leveraging Xcode’s Preview Canvas, we can preview our UI implementation in Xcode — without running the simulator — using mock data. Here is a screenshot of our current implementation.
Recent search list
Next we’ll build on our recent search row component by implementing the recent searches list. To accomplish this, we’ll use the following SwiftUI components:
List
: The foundational UI component that arranges our data and rows.Section
: This will group our recent search rows and add a header title.ForEach
: This iterates over anIdentifiable
collection and returns aView
.onDelete
: AViewModifier
that executes ourViewStore
’s logic when the user swipes to delete a row.listStyle
: A nativeViewModifier
that customizes ourList
’s appearance.
You can see how only a few lines of code are required to build a list in SwiftUI in comparison to UIKit; more importantly, note how easily Lasso fits into building these common components.
Using Xcode’s Preview Canvas, let’s take a look at the newly implemented List
. To do this, we can override the State
of our HomeScreenStore
with mock RecentSearch
values.
Creating the search bar with a SwiftUI Binding
When implementing our Home screen’s search bar, we’ll need to use a SwiftUI Binding
to observe changes to the search bar text. If you’ve used SwiftUI before, you’re probably used to the $
prefix syntax when creating a Binding
. This allows for easy read-and-write access on state values. For example, you might have a SwiftUI View
that looks like this:
At this point, you can’t leverage that syntax when using Lasso. Lasso’s read-only restriction on State
prevents this behavior. As a result, you’ll have to set up your Binding
with a bit more code when creating the search bar. For example, you might set up your search bar Binding
like this:
Let’s take a look at each of the steps involved.
- First, we’ll create a function that accepts a parameter matching the
Binding
’s wrapped type. This function will return anAction
fromstore
that will update the appropriateState
properties in theLassoStore
by callingupdate
. - Next, we’ll create a
Binding
using thebinding
method. This uses aWritableKeyPath
on theState
to determine which property to observe. Then we’ll pass in our function from step 1, which matches the input and output of theaction
parameter. - Last, we’ll continue implementing SwiftUI components as usual. The only difference is that we exclude the
$
prefix because we created aBinding
directly.
You must implement the
update
logic in yourLassoStore
with the correspondingAction
to update values appropriately. Otherwise, yourState
property will remain constant.
Putting it all together
Consistent with the screenshot of our app, we’ve implemented nearly every component of our Home screen. The last step that remains is to put them all together and add our navigation bar title to the Screen
. Realizing that our components align vertically, we’ll use a VStack
to build the Screen
.
Looking at this implementation, let’s highlight a few important facts.
- Navigation: You’ll see right away that we’re defining the navigation bar title without embedding the root view in a
NavigationView
. That’s because our LassoFlow
is handling the navigation stack for us. You must omitNavigationView
too. - Bindings: If you’re familiar with SwiftUI, you’ve probably used
Binding
s to handle changes to state properties. With Lasso,State
properties are read-only. This prevents the traditional$
syntax that is normally used with native SwiftUI components, such asAlert
andTextField
. As a work-around, Lasso provides abinding
method on thestore
property. - View methods: Many SwiftUI components have methods such as
onTapGesture
,onAppear
, etc. In our case, the list elements are using theonDelete
modifier. Many of these methods allow you to execute logic when appropriate. Looking at our project’s source code, you’ll see that Lasso extends many of these methods to accept the view’sViewStore
and anAction
value — keeping the business logic in theLassoStore
.
Using Xcode’s Preview Canvas one final time, we can see that our Home screen is complete.
Our navigation bar title doesn’t appear in the preview because our preview instance is not embedded in a
NavigationView
.
Checking in
At this point, we’ve implemented the Home screen of our app. We’ve built the user interface using small reusable components. Additionally, we’ve maintained separation of concerns by isolating the business logic in a LassoStore
. Most importantly, we’ve demonstrated the ability to build a scalable app with Lasso and SwiftUI.
When implementing the Detail screen, we can follow a process similar to the one we followed for the Home screen. When implementing the Map screen, the project follows a slightly different process by using UIViewRepresentable
as a wrapper around a UIKit MKMapView
component. To learn more about UIViewRepresentable
, check out Apple’s documentation here and see this project’s implementation in the file named MKMapView+SwiftUI.swift
. Regardless, be sure to check out the project’s source code to learn more about the remaining implementation details.
In the rest of this article, we’ll focus on some of the features that Lasso introduces to support SwiftUI, such as wrappers around native SwiftUI components and View
methods. Additionally, we’ll consider the need for a Lasso StoreModule
in a SwiftUI application and how we might implement one in an app.
Using Lasso’s extended SwiftUI Views
Lasso extends a lot of SwiftUI components, such as Button
, Alert
, TextField
, etc. Let’s look at Button
, for example. Traditionally, you might define one like this:
Lasso makes interfacing with SwiftUI components a bit easier by adding convenience initializers where possible. Looking at a similar example, here’s how you might now define a Button
with the same behavior.
The two examples look very similar. The latter intends to add some convenience, while also maintaining a slightly more Swifty approach.
Other examples
Lasso extends some other SwiftUI components for convenience purposes. Here are a couple of examples to illustrate possible use cases.
TextField
Alert.Button
NavigationLink
You’ll notice that we’re not using NavigationLink
in this sample app and the Lasso source code.
Currently, NavigationLink
doesn’t interface well with Lasso because NavigationLink
is tightly coupled with navigation logic and instantiates a View
. Lasso is opinionated in that this logic should live within a Flow
and LassoStore
. When using Lasso, it’s recommended that you use components like Button
instead of NavigationLink
to handle navigation behavior.
Check out
NavigationRow.swift
to see how the behavior ofNavigationLink
is maintained while abstracting the navigation and view instantiation logic.
Using Lasso’s View methods
Similar to the View
initializers, there are several convenience wrappers around native View
methods. For example, let’s look at onAppear(perform:)
. Traditionally, you might use this as follows:
Using the Lasso version, you can now use onAppear
like this:
While the two examples maintain the same behavior, you might find the second keeps the call site a bit more Swifty.
Other examples
Lasso further extends some SwiftUI View
methods for convenience purposes. Here are a couple of other examples to illustrate possible use cases.
onDisappear
onTapGesture
onLongPressGesture
Using a StoreModule
In this sample app, we created our Home screen using a ScreenModule
. However, we might encounter future scenarios where a StoreModule
is the more appropriate implementation option, such as when state and logic are shared across multiple Screen
s. Let’s take a closer look at how we might have implemented that functionality.
The example shows that this implementation remains nearly the same as in UIKit. There are only two differences.
- We instantiate the
View
with theViewStore
. - For UIKit interoperability, we wrap the
View
instance withUIHostingController
and return theUIHostingController
.
Wrapping up
This sample app is just the beginning of using Lasso with SwiftUI. Be on the lookout for more WW Tech articles and expect more updates to Lasso as SwiftUI matures and becomes increasingly common in production code.
If you’re interested in contributing to Lasso, we encourage you to fork the GitHub repository (found here) and open a pull request.
In the meantime, be sure to check out this tutorial’s source code here.
Metadata
Author: Charles Pisciotta, iOS Engineering Intern
Special thanks to Steven Grosmark, author of the Lasso framework, for his help on this tutorial project.
Notes
Are you interested in joining the WW team? Check out the careers page at WW.com to view technology job listings, as well as open positions on other teams.