Categories
Swift SwiftUI

NavigationLink and presentationMode environment value property for dismissing a view in SwiftUI

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

import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack(spacing: 8) {
Text("Hello World")
NavigationLink("Go to Detail View", destination: DetailView())
}.navigationBarTitle(Text("View 1"))
}
}
}
Content shown in navigation view
import SwiftUI
struct DetailView: View {
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack(spacing: 8) {
Text("Detail view")
Button("Go Back", action: {
self.presentationMode.wrappedValue.dismiss()
})
}
}
}
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.

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
Combine ImageIO iOS Swift SwiftUI

Animating GIFs and APNGs with CGAnimateImageAtURLWithBlock in SwiftUI

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.

#import <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ImageFrameScheduler: NSObject
– (instancetype)initWithURL:(NSURL *)imageURL;
@property (readonly) NSURL *imageURL;
– (BOOL)startWithFrameHandler:(void (^)(NSInteger, CGImageRef))handler;
– (void)stop;
@end
NS_ASSUME_NONNULL_END
#import "ImageFrameScheduler.h"
#import "ImageIO/CGImageAnimation.h" // Xcode 11 beta 7 – CGImageAnimation.h is not in umbrella header IOImage.h
@interface ImageFrameScheduler()
@property (readwrite) NSURL *imageURL;
@property (getter=isStopping) BOOL stopping;
@end
@implementation ImageFrameScheduler
– (instancetype)initWithURL:(NSURL *)imageURL {
if (self = [super init]) {
self.imageURL = imageURL;
}
return self;
}
– (BOOL)startWithFrameHandler:(void (^)(NSInteger, CGImageRef))handler {
__weak ImageFrameScheduler *weakSelf = self;
OSStatus status = CGAnimateImageAtURLWithBlock((CFURLRef)self.imageURL, nil, ^(size_t index, CGImageRef _Nonnull image, bool* _Nonnull stop) {
handler(index, image);
*stop = weakSelf.isStopping;
});
// See CGImageAnimationStatus for errors
return status == noErr;
}
– (void)stop {
self.stopping = YES;
}
@end

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.

import Combine
import UIKit
final class ImageAnimator: ObservableObject {
private let scheduler: ImageFrameScheduler
init(imageURL: URL) {
self.scheduler = ImageFrameScheduler(url: imageURL)
}
@Published var image: CGImage?
func startAnimating() {
let isRunning = scheduler.start { [weak self] (index, image) in
self?.image = image
}
if isRunning == false {
print("Failed animate image at url \(scheduler.imageURL)")
}
}
func stopAnimating() {
scheduler.stop()
}
}

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.

import SwiftUI
struct ContentView: View {
@ObservedObject var imageAnimator: ImageAnimator
var body: some View {
ZStack {
if imageAnimator.image != nil {
Image(imageAnimator.image!, scale: 1.0, label: Text("Gif"))
}
else {
Text("Paused")
}
}.onAppear {
self.imageAnimator.startAnimating()
}.onDisappear {
self.imageAnimator.stopAnimating()
}
}
}

Summary

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.

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.

Example Project

AnimateImageData (Xcode 11b7)

Categories
Swift SwiftUI UIKit Vision

Scanning text using SwiftUI and Vision on iOS

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.

struct ScannerView: UIViewControllerRepresentable {
    private let completionHandler: ([String]?) -> Void
    
    init(completion: @escaping ([String]?) -> Void) {
        self.completionHandler = completion
    }
    
    typealias UIViewControllerType = VNDocumentCameraViewController
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<ScannerView>) -> VNDocumentCameraViewController {
        let viewController = VNDocumentCameraViewController()
        viewController.delegate = context.coordinator
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: UIViewControllerRepresentableContext<ScannerView>) {}
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(completion: completionHandler)
    }
    
    final class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate {
        private let completionHandler: ([String]?) -> Void
        
        init(completion: @escaping ([String]?) -> Void) {
            self.completionHandler = completion
        }
        
        func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
            print("Document camera view controller did finish with ", scan)
            let recognizer = TextRecognizer(cameraScan: scan)
            recognizer.recognizeText(withCompletionHandler: completionHandler)
        }
        
        func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
            completionHandler(nil)
        }
        
        func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) {
            print("Document camera view controller did finish with error ", error)
            completionHandler(nil)
        }
    }
}

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.

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.

Example

VisionKitExample (Xcode 11)

Categories
Combine iOS Swift SwiftUI

Creating chat view with Combine and SwiftUI

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.

struct Message: Identifiable {
let id = UUID()
let sender: String
let text: String
}
view raw .swift hosted with ❤ by GitHub

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.

struct Session {
let messageFeed = PassthroughSubject<Message, Never>()
func send(_ message: Message) {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
self.messageFeed.send(message)
self.simulateReceivingMessages()
}
}
private func simulateReceivingMessages() {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
let receivedMessage = Message(sender: "Person B", text: UUID().uuidString)
self.messageFeed.send(receivedMessage)
}
}
}
view raw .swift hosted with ❤ by GitHub

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.

final class Conversation: ObservableObject {
private let session = Session()
private var messageSubscriber: AnyCancellable?
init() {
messageSubscriber = session.messageFeed.sink { [weak self] (receivedMessage) in
self?.messages.append(receivedMessage)
}
}
@Published private(set) var messages = [Message]()
func send(_ message: Message) {
session.send(message)
}
}
view raw .swift hosted with ❤ by GitHub

Creating simple list view

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.

struct ContentView: View {
@ObjectBinding var conversation: Conversation
var body: some View {
NavigationView {
VStack {
List(self.conversation.messages) { message in
Text(message.text)
}
InputView(conversation: self.conversation)
}.navigationBarTitle(Text("Conversation"))
}
}
}
view raw .swift hosted with ❤ by GitHub

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.

import Combine
import SwiftUI
struct InputView: View {
let conversation: Conversation
@State private var inputText = ""
var body: some View {
HStack {
TextField("", text: $inputText)
.padding(6)
.background(Color.white)
Button(action: sendMessage) {
Text("Send")
}
}.padding(12).background(Color.init(white: 0.75))
}
private func sendMessage() {
self.conversation.send(Message(sender: "PersonA", text: self.inputText))
self.inputText = ""
}
}
view raw .swift hosted with ❤ by GitHub

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.

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.

Example

ConversationInSwiftUI (Xcode 11, Swift 5.1)

Resources