UIKit Swift UIRecipes
UIKit Swift UIRecipes
GitHub: https://github.com/azamsharp/UIKitUsingSwiftUIRecipes
One of the best things about SwiftUI is that it is interoperable with UIKit
framework. This means, views created in SwiftUI can easily be loaded into
existing UIKit applications. In Listing 1 we have created a StocksScreen in
SwiftUI, which displays information about few stocks.
navigateToStocksButton.addAction(UIAction { _ in
self.navigationController?.pushViewController(UIHostingController(rootView:
StockListScreen()), animated: true)
}, for: .touchUpInside)
self.view.addSubview(navigateToStocksButton)
// setup constraints ….
}
There are situations where you want to insert a SwiftUI view into an existing
UIKit view instead of displaying a brand new view implemented in SwiftUI.
Listing 3 shows implementation of a RatingView in SwiftUI.
import SwiftUI
self.view.addSubview(ratingsView)
Loading a SwiftUI view and embedding it into a UIKit view is great, but
sometimes we need to access a value from a SwiftUI view into our UIKit view. For
this scenario, we will be working on the same RatingView with one limitation, we
are not allowed to change the implementation of the RatingView.
self.view.addSubview(ratingsView)
We are creating a rating variable inside the ViewController and decorating it with
@State property wrapper. Later we passed rating as a binding to the RatingView.
All looks nice but when you run the app, you will get the following warning.
Accessing State's value outside of being installed on a View. This will result
in a constant Binding of the initial value and will not update.
The warning is due to the fact that you are trying to access @State property
wrapper outside the SwiftUI view and that is not allowed.
Another method that was suggested by Asperi and Andrew here involves
wrapping the RatingView with a container/parent view. This is implemented in
Listing 6.
fi
class ViewController: UIViewController, ObservableObject {
}()
self.view.addSubview(ratingsView)
if let rating {
// Now you have access to the rating
self?.ratingLabel.text = "\(rating)"
}
}
// stack view
let stackView = UIStackView(arrangedSubviews: [ratingLabel,
ratingsView])
stackView.axis = .vertical
self.view.addSubview(stackView)
In Listing 6, we loaded a SwiftUI view inside a UIKit view through the use of a
container view (HolderView). Another way to communicate between a UIKit and
SwiftUI view is by using a View Model. In this example, we will create a SwiftUI
counter view. When the counter is pressed the updated value is passed to the
UIKit and displayed on the label.
class CounterViewModel {
@Published var value: Int = 0
}
let vm = CounterViewModel()
var cancellable: AnyCancellable?
}()
self.view.backgroundColor = .white
let hostingController = UIHostingController(rootView: CounterView(vm:
vm))
self.view.addSubview(counterView)
// add subscriptions
self.cancellable = vm.$value.sink { [weak self] value in
self?.counterLabel.text = "\(value)"
}
// stack view
let stackView = UIStackView(arrangedSubviews: [counterLabel,
counterView])
stackView.axis = .vertical
self.view.addSubview(stackView)
// adding constraints
}
}
In the last recipe, if you try to display the updated counter value in a SwiftUI
view, it will not work. This is shown in Listing 9.
Listing 10: Displaying updated counter value using @StateObject and ObservableObject
Sometimes, you have a scenario where you want to share state with multiple
SwiftUI views. This can be achieved by using the @EnvironmentObject
property wrapper, which represents the global state of the application. In this
recipe, we are going to add another SwiftUI called “FancyCounterView”.
fi
ff
FancyCounterView will also display the updated counter value, along with
CounterView and the CounterViewController.
We will begin by creating a global state object. This is shown in Listing 11.
We will update our implementation for CounterView and also create a brand
new view called FancyCounterView. Both views make use of the global state.
This is shown in Listing 12.
FancyCounterView()
.padding()
.foregroundColor(.white)
.background {
Color.green
}
}
}
}
Listing 12: CounterView and FancyCounterView using global state
If you run the app and click on the increment button, you will notice that the
counter value is updating in CounterView, FancyCounterView and the
CounterViewController. EnvironmentObject is a great option to share state with
multiple SwiftUI views.
Sometimes you have a scenario, where you have to load a UIKit view into a
SwiftUI application. In order to load a UIKit view into a SwiftUI view, you will
have to implement UIViewRepresentable protocol. UIViewRepresentable
protocol allows you to wrap your UIKit view to integrate into a SwiftUI
application.
import SwiftUI
Now, when you tap the “Toggle” button you can view and hide the activity
indicator view.
Sometimes when you are loading a UIKit view in a SwiftUI application, you also
need to implement the delegate methods exposed by UIKit view. This can be for
variety of reasons, maybe you need to handle a speci c UI event, which is
exposed through those delegate methods.
import Foundation
import MapKit
import SwiftUI
// add annotation
let pointAnnotation = MKPointAnnotation()
pointAnnotation.title = "Apple Campus"
pointAnnotation.coordinate = CLLocationCoordinate2D(latitude:
37.331821, longitude: -122.031181)
map.addAnnotation(pointAnnotation)
return map
}
Great!
But what if you want to change the annotation view. Maybe you are interested in
displaying an Apple logo instead of the default annotation marker. Luckily,
MKMapView provides delegate methods that allows you to change the
// add annotation
let pointAnnotation = MKPointAnnotation()
pointAnnotation.title = "Apple Campus"
pointAnnotation.coordinate = CLLocationCoordinate2D(latitude:
37.331821, longitude: -122.031181)
map.addAnnotation(pointAnnotation)
return map
}
}
func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator()
}
If you run the app, nothing will change it will still shows you the default marker
as an annotation. But now you have an option to return a di erent view for your
annotation through the use of MKMapViewDelegate protocol. Update your
MapViewCoordinator and implement the viewForAnnotation delegate function
as shown in Listing 20.
override init() {
super.init()
registerMapAnnotationViews()
}
mapView.register(AppleMarkerAnnotationView.self,
forAnnotationViewWithReuseIdentifier:
NSStringFromClass(AppleMarkerAnnotationView.self))
}
switch annotation {
case is MKPointAnnotation:
return AppleMarkerAnnotationView(annotation: annotation,
reuseIdentifier: NSStringFromClass(AppleMarkerAnnotationView.self))
default:
print("NIL")
return nil
}
}
}
In iOS 16 and Xcode 14, Apple introduced new hosting con gurations, which
allows you to load a SwiftUI view as a cell for your UITableView or
UICollectionView. This is great news because now we can use the declarative API
of SwiftUI to construct our UITableView layout.
The rst step is to implement your cell using SwiftUI. This is implemented in
Listing 21.
Text(donut.name)
Spacer()
Image(systemName: "chevron.right")
}.padding()
}
}
Our cell is called DonutCellView and it displays the name and image associated
with the donut. Figure 6 shows the rendering of the DonutCellView.
fi
fi
Figure 6: DonutCellView implemented in SwiftUI
Next, we will use the new contentCon guration available on the UITableViewCell
to load the DonutCellView in our UIKit app. The implementation is shown in
Listing 22.
let donuts = [Donut(name: "Donut 1", picture: "1"), Donut(name: "Donut 2",
picture: "2"),Donut(name: "Donut 3", picture: "3"),Donut(name: "Donut 4",
picture: "4")]
navigationController?.navigationBar.prefersLargeTitles = true
self.title = "Donuts"
cell.contentConfiguration = UIHostingConfiguration(content: {
DonutCellView(donut: donut) // SwiftUI View
})
return cell
}
One of the best things about working in SwiftUI is the ability to visually see your
user interface through the use of Xcode previews. I know Xcode previews does
have some quirks but at least for me it works majority of the time. Wouldn’t it be
really cool, if we can also visualize our View Controller in UIKit using Xcode
previews? Let’s see how we can achieve it.
}
}
Conclusion
I hope you have enjoyed this small recipe book on UIKit with SwiftUI. If you are
interested in my other books then check out the section below.
Books: