Categories
CoreData iOS Swift SwiftUI

NSFetchedResultsController wrapper for SwiftUI view models

Although SwiftUI framework contains a @FetchRequest property wrapper, it is not possible to use it outside of SwiftUI views because it relies on accessing the managed object context from the view environment. While working on an CoreData based app which uses view models a lot within SwiftUI views, I ended up creating a wrapper class for making it easier to use NSFetchedResultsController. The wrapper class is named FetchedResultList and what it does internally is creating a NSFetchedResultsController instance, handling its delegate methods, and notifying about data changes through closures. Here is an example of a view model using the wrapper.

@MainActor final class ViewModel: ObservableObject {
private let list: FetchedResultList<Fruit>
init(context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) {
list = FetchedResultList(context: context,
sortDescriptors: [
NSSortDescriptor(keyPath: \Fruit.name, ascending: true)
])
list.willChange = { [weak self] in self?.objectWillChange.send() }
}
var fruits: [Fruit] {
list.items
}
@Published var searchText: String = "" {
didSet {
if searchText.isEmpty {
list.predicate = nil
}
else {
list.predicate = NSPredicate(format: "name contains[cd] %@", searchText)
}
}
}
}
view raw Fruits.swift hosted with ❤ by GitHub

The view model conforms to ObservableObject protocol and FetchedResultList provides a willChange closure which gets called when NSFetchedResultsController’s will change delegate is called. Calling the objectWillChange publisher will signal the SwiftUI view about the data change and the view gets re-rendered. The wrapper also supports updating predicate or sort descriptors dynamically. Here, we can see how setting a searchableText property from the view will update the predicate of the wrapper class. The SwiftUI view which uses the view model looks like this:

struct FruitList: View {
@StateObject var viewModel = ViewModel()
var body: some View {
NavigationStack {
List(viewModel.fruits) { item in
NavigationLink(item.name) {
Text(verbatim: "Detail view for \(item.name)")
}
}
.navigationTitle("Fruits")
.searchable(text: $viewModel.searchText)
}
}
}
view raw Fruits.swift hosted with ❤ by GitHub

Let’s take a look at how the wrapper class is implemented. The core logic around the wrapper is creating an instance of NSFetchedResultsController, allowing to reconfigure it dynamically, handling its delegate, and notifying changes through closures. Using closures instead of conforming to ObservableObject is a conscious choice since the main use case is using the wrapper class in view models or in other controllers, and it means that propagating the data change to a SwiftUI view needs to be done manually. It is shorter to call view model’s objectWillChange in a closure than republishing wrapper’s objectWillChange to view model’s objectWillChange. Moreover, it would make more sense to use SwiftUI provided property wrapper instead of this wrapper if we would want to handle CoreData fetching in the SwiftUI view’s implementation.

@MainActor final class FetchedResultList<Result: NSManagedObject> {
private let fetchedResultsController: NSFetchedResultsController<Result>
private let observer: FetchedResultsObserver<Result>
init(context: NSManagedObjectContext, filter: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]) {
let request = NSFetchRequest<Result>(entityName: Result.entity().name ?? "<not set>")
request.predicate = filter
request.sortDescriptors = sortDescriptors.isEmpty ? nil : sortDescriptors
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
observer = FetchedResultsObserver(controller: fetchedResultsController)
observer.willChange = { [unowned self] in self.willChange?() }
observer.didChange = { [unowned self] in self.didChange?() }
refresh()
}
private func refresh() {
do {
try fetchedResultsController.performFetch()
}
catch {
Logger().error("Failed to load results")
}
}
var items: [Result] {
fetchedResultsController.fetchedObjects ?? []
}
var predicate: NSPredicate? {
get {
fetchedResultsController.fetchRequest.predicate
}
set {
fetchedResultsController.fetchRequest.predicate = newValue
refresh()
}
}
var sortDescriptors: [NSSortDescriptor] {
get {
fetchedResultsController.fetchRequest.sortDescriptors ?? []
}
set {
fetchedResultsController.fetchRequest.sortDescriptors = newValue.isEmpty ? nil : newValue
refresh()
}
}
var willChange: (() -> Void)?
var didChange: (() -> Void)?
}

The wrapper class uses private FetchedResultsObserver class which must derive from NSObject because it implements NSFetchedResultsControllerDelegate methods. This approach allows keeping the FetchedResultList class a pure Swift class and not a NSObject subclass which I like to avoid in SwiftUI apps (just a personal preference).

private final class FetchedResultsObserver<Result: NSManagedObject>: NSObject, NSFetchedResultsControllerDelegate {
var willChange: () -> Void = {}
var didChange: () -> Void = {}
init(controller: NSFetchedResultsController<Result>) {
super.init()
controller.delegate = self
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
willChange()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
didChange()
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Grid view in SwiftUI

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.

struct ContentView: View {
var body: some View {
Grid(alignment: .center,
horizontalSpacing: 16,
verticalSpacing: 8) {
Separator(title: "Today")
.gridCellUnsizedAxes(.horizontal)
GridRow {
Text("Finals")
Cell(title: "Archery", imageName: "figure.archery")
Cell(title: "Badminton", imageName: "figure.badminton")
}
Separator(title: "Tomorrow")
.gridCellUnsizedAxes(.horizontal)
GridRow(alignment: .bottom) {
Text("Qualifications")
Cell(title: "Bowling", imageName: "figure.archery")
Cell(title: "Golf", imageName: "figure.golf")
Cell(title: "Handball", imageName: "figure.handball")
}
}
}
}
view raw Grid.swift hosted with ❤ by GitHub

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.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Presenting multiple sheets in SwiftUI

SwiftUI has multiple view modifiers for presenting sheets. If we just want to present a modal view, then we can use one of these:

func sheet<Content>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
content: @escaping () -> Content
) -> some View where Content : View
func sheet<Item, Content>(
item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
content: @escaping (Item) -> Content
) -> some View where Item : Identifiable, Content : View
view raw Sheets.swift hosted with ❤ by GitHub

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:

struct ContentView: View {
enum Sheet: String, Identifiable {
case addItem, settings
var id: String { rawValue }
}
@State private var sheet: Sheet?
var body: some View {
VStack {
Button("Add Item", action: { sheet = .addItem })
Button("Settings", action: { sheet = .settings })
}
.sheet(item: $sheet, content: makeSheet)
}
@ViewBuilder
func makeSheet(_ sheet: Sheet) -> some View {
switch sheet {
case .addItem:
AddItemView()
case .settings:
SettingsView()
}
}
}

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.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift Swift Package

Most visited blog posts in 2022

Time to take a look at most visited blog posts in 2022 in two categories: most visited posts written in 2022 and most visited posts in overall.

Top 10 written in 2022

Top 10 overall

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift Swift Package

Basic unit-tests for SwiftUI view with ViewInspector

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.

struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text(viewModel.title)
.font(.title)
}
.padding()
}
}
extension ContentView {
final class ViewModel: ObservableObject {
@Published var style: Style = .hello
var title: String {
switch style {
case .hello: return "Hello World!"
case .welcome: return "Welcome World!"
}
}
}
enum Style {
case hello, welcome
}
}

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.

@testable import SwiftUIExampleViewInspector
import SwiftUI
import ViewInspector
import XCTest
// Do not forget this!
extension ContentView: Inspectable {}
final class ContentViewTests: XCTestCase {
func testInitialTitle() throws {
let contentView = ContentView()
let text = try contentView.inspect()
.find(text: "Hello World!")
let font = try text.attributes().font()
XCTAssertEqual(font, Font.title)
}
func testWelcomeTitle() throws {
let viewModel = ContentView.ViewModel()
viewModel.style = .welcome
let contentView = ContentView(viewModel: viewModel)
_ = try contentView.inspect().find(text: "Welcome World!")
}
}

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.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Wrapping delegates for @MainActor consumers in Swift

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.

final class ImageBatchLoader {
weak var delegate: ImageBatchLoaderDelegate?
init(delegate: ImageBatchLoaderDelegate) {
self.delegate = delegate
}
func start() {
DispatchQueue.global().async {
self.delegate?.imageLoader(self, didLoadBatch: [UIImage()])
}
}
}
protocol ImageBatchLoaderDelegate: AnyObject {
func imageLoader(_ imageLoader: ImageBatchLoader, didLoadBatch batch: [UIImage])
}
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.

final class ImageBatchLoaderHandler: ImageBatchLoaderDelegate {
var didLoadBatch: @MainActor ([UIImage]) -> Void = { _ in }
func imageLoader(_ imageLoader: ImageBatchLoader, didLoadBatch batch: [UIImage]) {
print("isMainThread", Thread.isMainThread, #function)
Task {
await didLoadBatch(batch)
}
}
}

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.

@MainActor final class ViewModel: ObservableObject {
private let imageLoader: ImageBatchLoader
private let imageLoaderHandler: ImageBatchLoaderHandler
init() {
imageLoaderHandler = ImageBatchLoaderHandler()
imageLoader = ImageBatchLoader(delegate: imageLoaderHandler)
imageLoaderHandler.didLoadBatch = handleBatch
imageLoader.start()
}
func handleBatch(_ batch: [UIImage]) {
print("isMainThread", Thread.isMainThread, #function)
// redacted
}
}
view raw ViewModel.swift hosted with ❤ by GitHub

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.

isMainThread false imageLoader(_:didLoadBatch:)
isMainThread true handleBatch(_:)

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Accessing UIHostingController from a SwiftUI view

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:

// Presenting the detail view using UIKit presentation methods
let hostingController = DetailView().embeddedInHostingController()
presentingViewController.present(hostingController, animated: true)
// The view which needs access to the view controller hosting it
struct DetailView: View {
@EnvironmentObject var hostingProvider: ViewControllerProvider
var body: some View {
VStack {
Text("Detail")
Button("Access View Controller") {
let viewController = hostingProvider.viewController
// … do something with the view controller
}
}
}
}

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.

extension View {
func embeddedInHostingController() -> UIHostingController<some View> {
let provider = ViewControllerProvider()
let hostingAccessingView = environmentObject(provider)
let hostingController = UIHostingController(rootView: hostingAccessingView)
provider.viewController = hostingController
return hostingController
}
}
final class ViewControllerProvider: ObservableObject {
fileprivate(set) weak var viewController: UIViewController?
}

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.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

View modifier for preparing view data in SwiftUI

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).

extension ContentView {
@MainActor final class ViewModel: ObservableObject {
func prepare() {
//
}
}
}
extension OtherView {
@MainActor final class ViewModel: ObservableObject {
func prepare() async {
//
}
}
}
view raw ViewModel.swift hosted with ❤ by GitHub

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.

struct PrepareViewData: ViewModifier {
@State var hasPrepared = false
let action: (() -> Void)
func body(content: Content) -> some View {
content
.onAppear {
if !hasPrepared {
action()
hasPrepared = true
}
}
}
}
extension View {
func prepare(perform action: @escaping () -> Void) -> some View {
modifier(PrepareViewData(action: action))
}
}
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
// redacted
}
.prepare {
viewModel.prepare()
}
}
}

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.

struct PrepareAsyncViewData: ViewModifier {
@State var hasPrepared = false
@State var task: Task<Void, Never>?
let action: (() async -> Void)
func body(content: Content) -> some View {
content
.onAppear {
guard !hasPrepared else { return }
guard task == nil else { return }
task = Task {
await action()
hasPrepared = true
}
}
.onDisappear {
task?.cancel()
task = nil
}
}
}
extension View {
func prepare(perform action: @escaping () async -> Void) -> some View {
modifier(PrepareAsyncViewData(action: action))
}
}
struct OtherView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
// redacted
}
.prepare {
await viewModel.prepare()
}
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
Swift SwiftUI Xcode

LibraryContentProvider in Xcode

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.

struct SubtitledButton: View {
let title: LocalizedStringKey
let subtitle: LocalizedStringKey
let action: () -> Void
var body: some View {
Button(action: action, label: {
VStack(spacing: 4) {
Text(title)
Text(subtitle)
.font(.footnote)
}
})
}
}

The only thing we need to do is creating a new type which conforms to LibraryContentProvider and then returning the button as a LibraryItem.

struct LibraryContent: LibraryContentProvider {
var views: [LibraryItem] {
return [
LibraryItem(SubtitledButton(title: "Title",
subtitle: "Subtitle",
action: {}),
title: "SubtitledButton",
category: .control)
]
}
}

After adding the new type, Xcode updates the library automatically.

Xcode library view with SubtitledButton showing up.
Xcode library with SubtitledButton.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI UIKit

UIKit navigation with SwiftUI views

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.

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private lazy var flowController = FlowCoordinator(window: window!)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
flowController.showRootView()
window?.makeKeyAndVisible()
}
}
final class FlowCoordinator {
private let window: UIWindow
init(window: UIWindow) {
self.window = window
}
func showRootView() {
let swiftUIView = ContentView()
let hostingView = UIHostingController(rootView: swiftUIView)
window.rootViewController = UINavigationController(rootViewController: hostingView)
}
}
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.green)
}
}
A simple iOS app showing a green view which is rendered with SwiftUI but presented with UIKit
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.

final class FlowCoordinator: ObservableObject {}
let swiftUIView = ContentView()
.environmentObject(self) // self is FlowCoordinator
struct ContentView: View {
@EnvironmentObject var flowController: FlowCoordinator
var body: some View {
VStack(spacing: 8) {
Text("Root view")
Button("Present Sheet", action: flowController.showDetailView)

Presenting a sheet

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.

final class FlowCoordinator: ObservableObject {
// …
func showDetailView() {
let detailView = DetailView()
.environmentObject(self)
let viewController = UIHostingController(rootView: detailView)
window.rootViewController?.present(viewController, animated: true, completion: nil)
}
func closeDetailView() {
// Needs to be more sophisticated later when there are more views
window.rootViewController?.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
struct DetailView: View {
@EnvironmentObject var flowController: FlowCoordinator
var body: some View {
VStack(spacing: 8) {
Text("Detail view content")
Button("Dismiss", action: flowController.closeDetailView)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.orange)
}
}
A simple iOS app showing an orange sheet which is rendered with SwiftUI but presented with UIKit
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.

Example Project

UIKitNavigationWithSwiftUIViews (Xcode 13.2.1)

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.