SwiftUI framework got a new addition in iOS 16 named Grid which is enables creating grid layouts in a quick way. The main difference between using Grid instead of combining HStack and VStack is that all the cells will get the same size. SwiftUI will create all the cells, measure the size, and apply the largest size to all the cells. Here is an example of a grid which in addition to cells have additional accessory views.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
In the example above we can see how Grid is created: first we have the Grid container view with one or multiple GridRow views which represents a single row of cells. If we want to decorate the grid with accessory views, we can just add more views which are not wrapped into GridRow. Separator view is just a view which displays text and a divider. The gridCellUnsizedAxes() allows controlling how these accessory views are laid out. Separator contains a Divider, which is a flexible view and wants to take as much space it could. By applying the view modifier we can disable this behaviour and the width of the accessory view is not limited by the number of columns in the grid.
SwiftUI has multiple view modifiers for presenting sheets. If we just want to present a modal view, then we can use one of these:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The first requires a boolean binding, whereas the second an identifiable item. When dealing with views which need to present different views in a sheet, then the latter can be easily expanded to support that. We can create an enum, conform it to Identifiable and then add an optional @State property which selects the view we should be presenting. The Identifiable protocol requires implementing an id property, which we can easily do by reusing rawValue property of an enum with raw types. If we put all of this together, then we can write something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
In the example above, I also separated the sheet view creation by having a separate function with an argument of type ContentView.Sheet. Since the function returns views with different types, then it needs to be annotated with @ViewBuilder. All in all it is a pretty concise and gives a nice call site where we just assign a sheet identifier to the sheet property.
While using SwiftUI for building UIs, sooner or later we would like to write some unit-tests as well. Of course, we could always go for UI-tests, but these are much slower and therefore not so scalable if we would just want to have a basic verification for our view. It is easy to get going with writing unit-tests for UIKit code, but it is much more difficult for SwiftUI views. Currently, there seems to be two main ways: snapshot testing using the pointfreeco’s library or inspecting views with ViewInspector. Today, we are not going to compare these libraries and instead just take a look at how to get going with ViewInspector.
Although ViewInspector supports using bindings etc for updating the view while running the unit-test, I personally feel like these kinds of tests where we interact with the view is probably better for UI-tests. Therefore, in this blog post, we just take a look at a basic SwiftUI view and how to inspect it in a unit-tests.
Here we have a basic SwiftUI view which uses a view model to provide data for the view. The style property is driving the title of the view. A pretty simple example to demonstrate the usage of the ViewInspector library.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Time to add two unit-tests where we first configure the view model and then verify. It is also possible to write a test where we dynamically change the style property, but that requires some extra code for supporting it. The first step after adding the library and only adding to the unit-testing target is to opt-in the custom view for inspection. Without the ContentView extension the library is not able to inspect any views. In the example below, we are just using the find method for looking for a Text with specific string, which depends on the style property. I feel like this library is really nice for these kinds of unit-tests where we create a view and just verify what it is displaying.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
In summary, I think that ViewInspector is a really nice library for unit-testing SwiftUI views. Since it requires more work to support reacting to view updates dynamically I feel like, at least for now, I am going to use it for static views and use UI-tests instead for tests simulating user interaction.
Sometimes we need to handle delegates in a class which has the @MainActor annotation. Often it can be a view model where we expect that code runs on the main thread. Therefore, view models have the @MainActor annotation, since we want that their methods run on the main thread when interacting with other async code. In an example below, we’ll be looking into integrating a delegate based ImageBatchLoader class which calls delegate methods on a background thread. The end goal is to handle the delegate in a view model and making sure it runs on the main thread.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
An example ImageBatchLoader with stubbed out start method.
This is an example of a class which uses delegates and calls delegate methods from background threads. If we have a view model with @MainActor annotation, then we just can’t conform to that delegate since the delegate does not use any async-await support. Xcode would show a warning saying that the protocol is non-isolated. A protocol would be isolated if it would have, for example, @MainActor annotation as well for that protocol. Let’s say this is not possible and it is a third party code instead.
The solution I have personally settled with is creating a wrapper class which conforms to that delegate and then uses main thread bound closures to notify when any of the delegate callbacks happen.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Here we can see a class which conforms to the ImageBatchLoaderDelegate and provides a didLoadBatch closure which has an @MainActor annotation. Since we use @MainActor and tap into the async-await concurrency, then we need an async context as well, which the Task provides.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Finally we have hooked up the image loader, its handler and also forwarding the didLoadBatch to a separate function which is part of the view model. With a little bit of code, we achieved what we wanted: listening to delegate callbacks and forwarding them to the view model on the main thread. If we ran the code we would see that the delegate callback runs on a background thread but the view model method runs on the main thread.
While I was working on a mixed UIKit and SwiftUI project, I needed a way to access the UIHostingController within the SwiftUI view so that I could use it for interacting with other UIKit methods. This blog post tackles the problem and provides a simple solution how to implement it.
The approach we are taking is using the SwiftUI environment and inserting an object into the environment, which then keeps a weak reference to the view controller hosting the SwiftUI view. Using the SwiftUI view environment has a benefit of allowing multiple other SwiftUI views within the hierarchy to use it as well. In the end, we would like to write something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
In the snippet above, we use a custom embeddedInHostingController() function which inserts a new ViewControllerProvider type the to the environment. Let’s take a closer look how this function and type are implemented.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The ViewControllerProvider class keeps a weak reference to the view controller. Since UIHostingController is a subclass of UIViewController we can just use UIViewController as a type. The embedded function creates an instance of the provider and a hosting controller, inserts the provider into the SwiftUI view environment and then sets the weak property which we can access later.
SwiftUI has view modifiers like onAppear() and onDisappear() for letting the view know when it is going to be displayed and when it is removed from the screen. In addition, there is a task() view modifier for running async functions. Something to keep in mind with onAppear() and task() is that the closure passed into the view modifier can be called multiple times when the view hierarchy changes. For example, when we have a TabView then the view receives onAppear() callback and also the task part of the task() is triggered each time when the tab presenting it is activated. In this blog post, we are looking into a case where we have some code which we only want to run once during the view’s lifetime. One of the use-cases is preparing content in view models. Let’s take a look at these cases where one view and its view model has synchronous prepare function and the other one has async prepare function (e.g. starting a network requests in the prepare function).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
SwiftUI uses view modifiers for configuring views. This is what we want to do here as well. We can create a new view modifier by conforming to the ViewModifier protocol and implementing the body function, where we add additional functionality to the existing view. The view modifier uses internal state for tracking if the closure was called already or not in the onAppear(). SwiftUI ensures that onAppear is called before the view is rendered. Below is the view modifier’s implementation with a view extension which creates it and finally an example view and its view model using it.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
If we have a view model which needs to do async calls in the prepare() function then we need a slightly different view modifier. Since async functions can run a long time, then we should also handle cancellation. If the view disappears, we should cancel the task if it is still running and restart it next time when the view is shown. Cancellation is implemented by keeping a reference to the task and calling cancel() on the task in the onDisappear(). For making the cancellation working properly, we need to make sure the async function actually implements cancellation by using, for example, Task.checkCancellation() within its implementation. Other than that, the view modifier implementation looks quite similar to the one above.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
LibraryContentProvider protocol in Xcode is a way for adding custom views and view modifiers to the Xcode library, which enables drag and dropping them to the SwiftUI preview. Xcode automatically looks for types implementing the protocol and then adds items to the library. It takes so little code that it makes sense to do it if you are using the library for building views.
As an example, we’ll add a custom SwiftUI view SubtitledButton to the library.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The only thing we need to do is creating a new type which conforms to LibraryContentProvider and then returning the button as a LibraryItem.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Recently I was asked a question about creating an app which has SwiftUI views but no navigation logic in it. Instead, UIKit controls how views are presented. It is a fair question since SwiftUI views have navigation support, but not everything is there if we need to support previous iOS versions as well, or we have a case of an app which have both UIKit and SwiftUI views. Therefore, let’s take a look at on one approach, how to handle navigation on the UIKit side but still use SwiftUI views.
UIHostingController presenting SwiftUI view
SwiftUI views are presented in UIKit views with UIHostingController which just takes in the SwiftUI view. UIHostingController is a UIViewController subclass, therefore it can be used like any other view controller in the view hierarchy. For getting things started, let’s configure SceneDelegate to use an object named FlowCoordinator which will handle navigation logic and then ask it to present a simple SwiftUI view.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
A simple app showing a green view which is rendered with SwiftUI but presented with UIKit.
Inserting the FlowCoordinator into SwiftUI view
The next step is that we want to allow SwiftUI view to control what is presented on the screen. For example, let’s add a button to the SwiftUI view which should present a sheet with another SwiftUI view. The button action needs to be able to talk to the flow coordinator, which controls what is presented on the screen. One way to insert the FlowCoordinator into SwiftUI environment is by conforming to ObservableObject and using the environmentObject() view modifier. Alternative is using EnvironmentValues and defining a key. For more information please check Injecting dependencies using environment values and keys in SwiftUI.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The sheet presentation code goes into the FlowCoordinator and as an example we show a DetailView which has a button for dismissing itself. Yet again, SwiftUI view just passes the handling to the FlowCoordinator.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
A simple app showing an orange sheet which is rendered with SwiftUI but presented with UIKit.
Summary
We created a simple sample app which uses UIKit navigation logic but renders views with SwiftUI views. This kind of setup could be useful for apps which mix UIKit and SwiftUI. But I believe that even in case of that we could still use SwiftUI navigation in sub-flows but could keep using this approach for handling root view navigation.
SwiftUI has a Picker view available with multiple different styles. One example of when it falls short is when we want to use a multi component picker with wheel style. One way how to try to achieve this is using a HStack with two Picker views, but it does not work very well, especially when trying to show it inside a Form view. So what else we can do? If something can’t be done in SwiftUI then we can use UIKit instead.
In my case, I wanted to create a picker which allows picking a date duration. It would have one wheel for selecting a number and the other wheel for selecting either days, weeks or months.
Firstly, let’s create a tiny struct which is going to hold the state of this picker. It needs to store a numeric value and the unit: days, weeks, months. Let’s name it as DateDuration. Since we want to iterate over the DateDuration.Unit, we’ll conform it to CaseIterable protocol.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
UIPickerView in UIKit can do everything we want, therefore we’ll need to wrap it into a SwiftUI view. This can be done by creating a new type which conforms to UIViewRepresentable protocol. Also, we need a binding which holds the value of the current selection: when the user changes it, the binding communicates the changes back and vice-versa. Additionally, we’ll add properties for configuring values and units. UIPickerView us created and configured in the makeUIView(context:) function. UIPickerView is driven by a data source and a delegate, which means we require a coordinator object as well. Coordinator is part of the UIViewRepresentable protocol.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Coordinator is created in the makeCoordinator() function. It is going to do most of the work by providing data to the UIPickerView and handling the current selection. Therefore, we’ll store the selection binding, values, and units in the Coordinator class as well.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The last missing piece is implementing UIPickerViewDataSource and UIPickerViewDelegate methods in the Coordinator class. This is pretty straight-forward to do. We’ll need to display two components where the first component is the list of values and the second component is the unit: days, weeks, months. When the user selects a new value, we’ll change the DateDuration value of the binding.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters