SwiftUI UIKit – part2: embed SwiftUI in UIViewController
Get link
Facebook
X
Pinterest
Email
Other Apps
In a previous post, I was showing you how to embed a UIViewController into a SwiftUI view. For this example, I used a map view, touching some of the basic functionality of MapKit as well. In this article, I will try the opposite exercise: embed SwiftUI into a ViewController and call it inside other View Controller.
Apple made it very simple to combine SwiftUI and UiKit. In this way, a developer can use existing UiKit functionality, controls and view controllers in SwiftUI views. And vice versa, your shiny brand new SwiftUI view can be called from UIView controller. If you remember the previous post, I was showing you an example of a search bar and a map view. Whenever a search was performed, if at least one relevant item was found, a pin was added to the list. But what if there are multiple results? Well, in this case, I want to show a view displaying all those results, to let my user select which one he or she actually intended to search for. And the one that is selected will be added on the map as a pin. Lets start with this exercise!
1. AlertAction
Let’s represent the information that will be shown in the selection view in a simple AlertAction struct; it will have a title, a sub-title, and an action, executed if tapped. I am making this struct Identifiable, so that it can be used by ForEach, later on.
struct AlertAction: Identifiable{
var id: Int
var title: String
var subTitle: String
var action: ()->Void
}
2. Show a list of AlertActions
I create a simple view, showing a list of alerts that are passed in as parameter. I am using the GeometryReader to get the height of the view and distribute it equally to the items in the alert list. After the user selects an item and action is executed, I am calling a dismiss callback, that is used to dismiss the alert.
struct ResultsAlert: View {
var actions: [AlertAction]
var dismiss: ()->Void
var body: some View {
GeometryReader{ geometry in
VStack(spacing: 0){
ForEach( self.actions ){ action in
ZStack{
Rectangle()
.fill( Color.clear)
.frame( height: geometry.size.height / CGFloat(self.actions.count))
VStack{
Text(action.title).font(.body)
Text(action.subTitle).font(.caption)
}
}.onTapGesture{
action.action()
self.dismiss()
}
}
}.background(RoundedRectangle( cornerRadius: 10)
.fill(LinearGradient(gradient: Gradient(colors: [Color.gray, Color.white]), startPoint: .topLeading, endPoint: .bottomTrailing)))
}
}
}
3. Wrap the View in an UIViewController
Now, here is how to wrap the View in an UIViewController. I am not using a storyboard, constraints or anything in that area; I am just simply creating a view, assigning it a frame having enough size to accommodate the whole content of ResultsAlert. After that, I use an UIHostingController to embed the SwiftUI view and I am adding the underlying view of this controller as sub-view for the above-created view. Also the dismiss method is just dismissing the controller. And that’s it, now you have an UIViewController encapsulating the SwiftUI view
Lets make some small changes to the MapViewController. Instead of just adding the pin for the first results in the list of search results, I am calling a resultsAlert method:
Therefore, here is the implementation of resultsAlert. Sometimes, the same item appears twice in the list of results, and this may be be confusing for the user. Therefore, I am presenting a unique list of items.
func resultsAlert(mapItems: [MKMapItem]){
var items: Set<String> = []
var actions: [AlertAction] = []
var idx: Int = 0 for mapItem in mapItems{
if items.contains(mapItem.name!){
continue
}
items.insert(mapItem.name!)
idx = idx + 1
let placemark = mapItem.placemark
actions.append(AlertAction(id: idx, title: mapItem.name!, subTitle: placemark.title!, action: {
self.addPin( placemark: placemark)
}))
}
let customAlert = ResultsAlertController( actions:actions)
customAlert.providesPresentationContextTransitionStyle = true
customAlert.definesPresentationContext = true
customAlert.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
customAlert.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(customAlert, animated: true, completion: nil)
}
6. Entire MapViewController
In the end, the entire MapViewController, with all the features developed in part 1 of this post included:
import UIKit
import MapKit
protocol MapViewDelegate{
func saveLocation(placemark: MKPlacemark)
}
class MapViewController: UIViewController {
var delegate:MapViewDelegate!
var mapView: MKMapView!
var searchBar: UISearchBar!
fileprivate var localSearchRequest: MKLocalSearch.Request!
fileprivate var localSearch: MKLocalSearch!
fileprivate var localSearchResponse: MKLocalSearch.Response!
fileprivate var annotation: MKAnnotation!
fileprivate var isCurrentLocation: Bool = false
var selectedPin: MKPlacemark?
override func viewDidLoad() {
super.viewDidLoad()
mapView = MKMapView()
let leftMargin:CGFloat = 10
let topMargin:CGFloat = 60
let mapWidth:CGFloat = view.frame.size.width-20
let mapHeight:CGFloat = view.frame.size.height - 100
mapView.frame = CGRect(x: leftMargin, y: topMargin, width: mapWidth, height: mapHeight)
mapView.isZoomEnabled = true
mapView.isScrollEnabled = true
self.view.addSubview(mapView)
searchBar = UISearchBar(frame: CGRect(x: 10, y: 0, width: mapWidth, height: 60))
searchBar.delegate = self
self.view.addSubview(searchBar)
mapView.delegate = self
mapView.mapType = .hybrid
}
}
extension MapViewController:MKMapViewDelegate{
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
guard !(annotation is MKUserLocation) else { return nil }
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
}
pinView?.pinTintColor = UIColor.orange
pinView?.canShowCallout = true
let smallSquare = CGSize(width: 30, height: 30)
let button = UIButton(frame: CGRect(origin: CGPoint.zero, size: smallSquare))
button.setBackgroundImage(UIImage(systemName: "plus.square"), for: .normal)
button.addTarget(self, action: #selector(savedPin), for: .touchUpInside)
pinView?.leftCalloutAccessoryView = button
return pinView
}
@objc func savedPin(){
guard let delegate = delegate, let placemark = selectedPin else { return}
delegate.saveLocation(placemark: placemark)
}
}
extension MapViewController:UISearchBarDelegate{
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
if self.mapView.annotations.count != 0 {
annotation = self.mapView.annotations[0]
self.mapView.removeAnnotation(annotation)
}
localSearchRequest = MKLocalSearch.Request()
localSearchRequest.naturalLanguageQuery = searchBar.text
localSearch = MKLocalSearch(request: localSearchRequest)
localSearch.start { (response, error) in
if let response = response {
self.resultsAlert( mapItems: response.mapItems )
}
}
}
func resultsAlert(mapItems: [MKMapItem]){
var items: Set<String> = []
var actions: [AlertAction] = []
var idx: Int = 0
for mapItem in mapItems{
if items.contains(mapItem.name!){
continue
}
items.insert(mapItem.name!)
idx = idx + 1
let placemark = mapItem.placemark
actions.append(AlertAction(id: idx, title: mapItem.name!, subTitle: placemark.title!, action: {
self.addPin( placemark: placemark)
}))
}
let customAlert = ResultsAlertController( actions:actions)
customAlert.providesPresentationContextTransitionStyle = true
customAlert.definesPresentationContext = true
customAlert.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
customAlert.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(customAlert, animated: true, completion: nil)
}
func showAlert(){
let alert = UIAlertController(title: nil, message:"Place not found", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Try again", style: .default) { _ in })
self.present(alert, animated: true){}
}
func addPin(placemark: MKPlacemark){
let annotation = MKPointAnnotation()
annotation.coordinate = placemark.coordinate
annotation.title = placemark.name
if let city = placemark.locality, let state = placemark.administrativeArea {
annotation.subtitle = "\(city) \(state)"
}
addAnnotation(annotation: annotation)
}
func addAnnotation( annotation:MKPointAnnotation ){
mapView.addAnnotation(annotation)
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let region = MKCoordinateRegion(center: annotation.coordinate, span: span)
mapView.setRegion(region, animated: true)
}
}
Also a screenshot, showing how this alert view is looking like:
This is already part 3 of a small series of articles about the SwiftUI and UIKit interaction. You can read the part1 and part 2 on my blog. This article is about pushing data and triggering actions from the SwiftUI to the UIViewController. The examples presented by Apple about interfacing with UIKit , shows a coordinator conforming to a protocol and having it as a delegate for a view or a view controller. However, I was not able to find any information on how to push back data into the View Controller, as result of actions performed by the user in the SwiftUI view. Therefore, in this article, I will try to show exactly this; I will be covering also the very cool feature of adding a custom overlay to a map view. So let’s dive into the sample. 1. Custom overlay This article will not really get into the details about creating the overlay. I am pretty sure there are more interesting examples on the internet, however for illustration purposes, I will create a circle, with the color ...
One of the latest exciting addition announced by Apple at WWDC 2019 is dark mode for IOS . Basically, the app can display different colors if ran on dark comparing to light color scheme. The colors of your app can be defined in your assets, specifying a different color for dark and for light scheme, under the same color name. It is not as easy with images. Therefore handling images in Dark theme can be a really useful feature for your bag of tricks. I recently ran into the very comprehensive collection of icon images from Icons8 . They offer thousands of drawings that can be easily used in your app, providing a rich visual experience for the users. However, there is s slight issue regarding this: the default colouring of the images is black on a white background. Of course I could invert them in Photoshop and provide 2 versions of the same image; however, this would make my image maintenance more difficult. Therefore I decided to implement a simple class, a replacement for Image,...
SwiftUI comes with good integration with existing UIKit framework. It is possible to achieve SwiftUI interaction with UIKit by wrapping UIViewControllers into SwiftUI views and the other way around, embed SwiftUI views into view controllers. In this post, I will show you an example of how to include the MapKit functionality in your SwiftUI app, wrapping a view controller. The purpose of this demo is to create a view, containing a search bar and a map view. The user types in a place in the search bar; the first result in the list of results retrieved by the map search is added to the map as a pin. Finnaly , I will show how to interact with the outside view by creating a delegate for the controller. 1. Start with a view controller I create a view controller and I programatically include all the elements that I want to show in my view: a search bar and a map view: class MapViewController: UIViewController { var mapView: MKMapView! fileprivate var searchBar: UI...
Comments
Post a Comment