How to navigate to a new view in SwiftUI and then dismissing it? Let’s set up a main view in NavigationView and NavigationLink for opening detail view. NavigationLink is a button triggering a navigation to a specified view. Detail view will contain a button for navigating back to the first view. But how do we dismiss presented detail view?
Reading environment and dismissing the detail view
SwiftUI provides a dynamic view property named Environment. It can be used for reading values from the view’s environment. Presentation mode is one of those values we can read. This is how we get access to PresentationMode structure what has isPresented property and function for dismissing the view. Therefore, we need to add dynamic view property to our detail view and then calling dismiss() when the button is tapped
This file contains hidden or 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 hidden or 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
Detail view with button for navigation back to the content view
Summary
SwiftUI has a dynamic view property Environment what we can use for getting more knowledge about the environment view is presented (locale, displayScale etc). One of the environment values is PresentationMode what we can use for dismissing the detail view.
This year Apple added CGAnimateImageAtURLWithBlock and CGAnimateImageDataWithBlock for animating GIFs and APNGs on all the platforms to the ImageIO framework. We can pass in URL or data and get callbacks when animation changes the current frame. In Xcode 11 beta 7 implicit bridging to Swift is disabled for those APIs and therefore we need to create a small wrapper around it in Objective-C.
Creating ImageFrameScheduler for managing CGAnimateImageAtURLWithBlock in Objective-C
Calling CGAnimateImageAtURLWithBlock starts the animation immediately. When animation frame changes, the handler block is called with frame index, current animation frame image and stop argument. When setting stop to YES, we can stop the animation. With this in mind we can create ImageFrameScheduler what takes in URL and has methods for starting and stopping the animation. Then we can expose this class to Swift and use it for managing the animation.
This file contains hidden or 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 hidden or 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
ImageAnimator conforming to ObservableObject in Swift
When updating views in SwiftUI, we can use ObservableObject protocol and @Published property wrapper what enables SwiftUI to get notified when the ObservableObject changes. This means that we need a model object written in Swift what stores our Objective-C class ImageFrameScheduler and exposes the current animation frame when animation is running. Whenever we update the property internally, property wrapper will take care of notifying SwiftUI to update the view.
This file contains hidden or 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
ContentView displaying animation frames in SwiftUI
Integrating ImageAnimator with ContentView is now pretty straight-forward, we check if animation frame image is available and display it. Animation is started when SwiftUI appears and stopped when it disappears.
This file contains hidden or 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
Although CGAnimateImageAtURLWithBlock and CGAnimateImageDataWithBlock are not directly usable in Swift, we can get away from it by adding a simple wrapper class in Objective-C. ImageFrameScheduler could be used in non-SwiftUI views by updating UIImageView when frame changes. In SwiftUI, views can use ImageAnimator for storing the current animation frame and using @Published property wrapper for letting SwiftUI view to know when to refresh.
Apple’s Vision framework contains computer vision related functionality and with iOS 13 it can detect text on images as well. Moreover, Apple added a new framework VisionKit what makes it easy to integrate document scanning functionality. For demonstrating the usage of it, let’s build a simple UI what can present the scanner and display scanned text.
Cropping text area when scanning documents
Scanning text with Vision
VisionKit has VNDocumentCameraViewController and when presented, it allows scanning documents and cropping scanned documents. It uses delegate for publishing scanned documents via an instance of VNDocumentCameraScan. This object contains all the taken images (documents). Next, we can use VNImageRequestHandler in Vision for detecting text on those images.
final class TextRecognizer {
let cameraScan: VNDocumentCameraScan
init(cameraScan: VNDocumentCameraScan) {
self.cameraScan = cameraScan
}
private let queue = DispatchQueue(label: "com.augmentedcode.scan", qos: .default, attributes: [], autoreleaseFrequency: .workItem)
func recognizeText(withCompletionHandler completionHandler: @escaping ([String]) -> Void) {
queue.async {
let images = (0..<self.cameraScan.pageCount).compactMap({ self.cameraScan.imageOfPage(at: $0).cgImage })
let imagesAndRequests = images.map({ (image: $0, request: VNRecognizeTextRequest()) })
let textPerPage = imagesAndRequests.map { image, request -> String in
let handler = VNImageRequestHandler(cgImage: image, options: [:])
do {
try handler.perform([request])
guard let observations = request.results as? [VNRecognizedTextObservation] else { return "" }
return observations.compactMap({ $0.topCandidates(1).first?.string }).joined(separator: "\n")
}
catch {
print(error)
return ""
}
}
DispatchQueue.main.async {
completionHandler(textPerPage)
}
}
}
}
Presenting document scanner with SwiftUI
As VNDocumentCameraViewController is UIKit view controller we can’t directly present it in SwiftUI. For making this work, we’ll need to use a separate value type conforming to UIViewControllerRepresentable protocol. UIViewControllerRepresentable is the glue between SwiftUI and UIKit and enables us to present UIKit views. This protocol requires us to define the class of the view controller and then implementing makeUIViewController(context:) and updateUIViewController(_:context:). In addition, we’ll also create coordinator what is going to be VNDocumentCameraViewController’s delegate. SwiftUI uses UIViewRepresentableContext for holding onto the coordinator and managing the view controller updates behind the scenes. Our case is pretty simple, we just use completion handler for passing back scanned text or nil when it was closed or error occurred. No need to update the view controller itself, only to pass data from it back to SwiftUI.
ContentView is the main SwiftUI view presenting the content for our very simple UI with static text, button and scanned text. When pressing on the button, we’ll set isShowingScannerSheet property to true. As it is @State property, then this change triggers SwiftUI update and sheet modifier will take care of presenting ScannerView with VNDocumentCameraViewController. When view controller finishes, completion handler is called and we will update the text property and set isShowingScannerSheet to false which triggers tearing down the modal during the next update.
struct ContentView: View {
private let buttonInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
var body: some View {
VStack(spacing: 32) {
Text("Vision Kit Example")
Button(action: openCamera) {
Text("Scan").foregroundColor(.white)
}.padding(buttonInsets)
.background(Color.blue)
.cornerRadius(3.0)
Text(text).lineLimit(nil)
}.sheet(isPresented: self.$isShowingScannerSheet) { self.makeScannerView() }
}
@State private var isShowingScannerSheet = false
@State private var text: String = ""
private func openCamera() {
isShowingScannerSheet = true
}
private func makeScannerView() -> ScannerView {
ScannerView(completion: { textPerPage in
if let text = textPerPage?.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines) {
self.text = text
}
self.isShowingScannerSheet = false
})
}
}
Summary
With the new addition of VisionKit and text recognition APIs, it is extremely easy to add support of scanning text using camera.
Let’s build a conversation view which shows a list of messages and has input text field with send button. Sent and received messages are managed by Conversation object. Conversation object manages a Session object which is simulating networking stack. This kind of setup allows us to look into how to propagate received messages from Session object to Conversation and then to the list view. We’ll jump into using types Combine and SwiftUI provide therefore if you need more information, definitely watch WWDC videos about Combine and SwiftUI.
Data layer
In the UI we are going to show a list of messages, therefore let’s define a struct for a Message. We’ll make the Message to conform to protocol defined in SwiftUI – Identifiable. We can add conformance by adding id property with type UUID what provides us unique identifier whenever we create a message. Identification is used by SwiftUI to identify messages and finding changes in the messages list.
This file contains hidden or 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
Session is owned by Conversation and simulates a networking stack dealing with sending and receiving messages. This like a place were we could use delegate pattern for forwarding received messages back to the Conversation. Instead of delegation pattern, we can use Combine’s PassthroughSubject. It enables us to publish new messages which we can then collect on the Conversation side. Great, but let’s see how to receive messages which are published by PassthroughSubject.
This file contains hidden or 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
Conversation is responsible of receiving messages from the Session and keeping the current history: list of messages. For receiving messages published by Session, we can use a subscriber called sink, which just gives access to values flowing through the channel. Subscribers are added directly to publishers, then publisher sends a subscription object back to the subscriber what subscriber can use for communicating with publisher. Here, communicating means requesting values from publisher. To recap: Session owns PassthroughSubject what Conversation starts to listen by attaching subscriber to it.
Conversation conforms to SwiftUI’s ObservableObject. When marking properties with @Published property wrapper, changes in those properties trigger updates in SwiftUI.
This file contains hidden or 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 SwiftUI, views are described by value types conforming to View protocol. Every view return their content in the body property. Our UI is simple enough and requires to add navigation view, list and then input view. List is the table view construct which creates new rows whenever it needs to. As we made Message to conform to Identifiable, then we can pass the messages directly to the List.
This file contains hidden or 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
Input view contains text field and button for sending the entered message. Input text is local state owned by the view itself. @State is a property wrapper and internally it creates a separate storage where the input text is stored and read during view updates.
This file contains hidden or 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
Now we have a the whole picture put together. Conversation object manages messages and lets SwiftUI know when it changes by using @Published property wrapper. When property wrapper dispatches change to SwiftUI, it compares the changes in the view hierarchy and updates only what is needed.
Summary
We created a basic list view what displays messages in the conversation object. We used simple constructs for passing on the data down from the Session to the SwiftUI layer. The aim of the sample project was to try out some of the ways Combine and SwiftUI allow us to build views.