Categories
Foundation iOS Swift

Property wrapper for validating email using NSDataDetector

Property wrappers allow property declaration to state what kind of property wrapper is used for implementing the property. We can use it for implementing transformations on properties like validating if string is email or not. This is what we will do: creating a property wrapper for email properties and validating emails using NSDataDetector. If value being set is email, we store it and if it is not, we set the property to nil instead.

Creating property wrapper

Property wrappers are types annotated with @propertyWrapper. The type needs to implement one property: wrappedValue. Emails are represented with strings, therefore our wrappedValue property is optional string. Optional is required, because string can contain invalid email and in that case we set the property to nil. Whenever we would like to use this property wrapper, we just need to add @EmailValidated in front of the property definition.

@propertyWrapper
struct EmailValidated {
private var value: String?
var wrappedValue: String? {
get {
return value
}
set {
value = {
guard let trimmedString = newValue?.trimmingCharacters(in: .whitespacesAndNewlines) else { return nil }
return validate(trimmedString)
}()
}
}
}

Validating email using NSDataDetector

Validating emails using regular expressions is not easy. Fortunately Apple provides API exactly for this: NSDataDetector. We can create an instance of NSDataDetector with specifying link as detected types. When matching emails, we use anchored option as we expect the string to only include email, nothing else. Anchored will tell the data detector to match starting with the first character. As firstMatch(in:options:range:) uses NSRange, we need to convert Swift’s range to NSRange because those ranges do not have one-to-one match. For this, we can use special NSRange initialiser taking Swift string and its range.

NSDataDetector represents links with URL, therefore we will see if match contains an URL and if URL’s scheme is mailto. If it is, we can extract the matched email and return it.

private func validate(_ emailString: String) -> String? {
let dataDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
// Finding matches in string
let range = NSRange(emailString.startIndex..<emailString.endIndex, in: emailString)
guard let match = dataDetector.firstMatch(in: emailString, options: .anchored, range: range) else { return nil }
guard let url = match.url else { return nil }
// Extracting email from the matched url
let absoluteString = url.absoluteString
guard let index = absoluteString.range(of: "mailto:") else { return nil }
return String(url.absoluteString.suffix(from: index.upperBound))
}
view raw Validate.swift hosted with ❤ by GitHub

Using EmailValidated property wrapper

For using the created property wrapper, all we need to do is to annotate property with @EmailValidated.

struct Contact {
var fullName: String
@EmailValidated var email: String?
}
var contact = Contact(fullName: "Toomas")
contact.email = "invalidemail"
print(contact.email as Any) // nil
contact.email = " test toomas@email.zz"
print(contact.email as Any) // nil
contact.email = "toomas@email.zz"
print(contact.email as Any) // Optional("toomas@email.zz")

Summary

We created a simple property wrapper for validating emails. We saw that creating a property wrapper for validating email with NSDataDetector requires only a little bit of code.

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

EmailPropertyWrapper playground (Xcode 11b5)

Resources

Categories
Foundation iOS Swift

Key-value observing without NSObject and dynamic modifier in Swift

When writing code in Swift it is often needed to observe changes in other objects. We can use Apple’s key-value observation but it has some implications: requires to use NSObject and dynamic dispatch through Objective-C runtime. This time, let’s build a simple key-value observation in Swift what does not require to use NSObject at all. Although it is far from being as feature complete as Apple’s implementation, it delivers the basic use-case which is often all what we need.

Custom KeyValueObservable protocol

The approach we take here is defining a protocol, providing default implementations for all the functions. Then we can make any class to conform to this protocol, but as we need to store observation related information, then the class needs to define a property holding an instance of ObservationStore. Secondly, it is required to send key-value change notification manually using didChangeValue(for:).

Add observer function returns an instance of Observation what can be used for removing the added observation. If the observer does not need to be removed during the lifetime of the observer, it can be ignored. Observation is always cleaned up automatically next time any key value changes happen after observer is deallocated. This is due to the fact that observation handler captures observer weakly and during key-value changes, it is checked if the object is still alive or not.

protocol KeyValueObservable where Self: AnyObject {
/// Stores all the added observations.
var observationStore: ObservationStore<Self> { get }
/// Sends key-value change notification to all the observers for this key path.
func didChangeValue(for keyPath: PartialKeyPath<Self>)
/// Adds observer for key path and returns observation token.
/// – Note: Observation token is only useful if it is needed to remove observation before observer is deallocated. When observer is deallocated, then observation is removed when next key value change is handled.
@discardableResult func addObserver<Observer: AnyObject, Value>(_ observer: Observer,
keyPath: KeyPath<Self, Value>,
options: Observation.Options,
handler: @escaping (Observer, Value) -> Void) -> Observation
/// Removes added observation.
func removeObservation(_ observation: Observation)
}

Adding default implementations

When adding observer, we create an observation handler what captures self and observer weakly. Handler returns boolean, what tells if the handler is still valid or not. Handler is not valid when observer has been deallocated since the last change. Otherwise handler is valid and should not be removed automatically.

extension KeyValueObservable {
@discardableResult func addObserver<Observer: AnyObject, Value>(_ observer: Observer, keyPath: KeyPath<Self, Value>, options: Observation.Options, handler: @escaping (Observer, Value) -> Void) -> Observation {
let observation = Observation()
let observationHandler: (PartialKeyPath<Self>) -> Bool = { [weak self, weak observer] changedKeyPath in
guard let self = self else { return false }
guard let observer = observer else { return false }
guard changedKeyPath == keyPath else { return true }
handler(observer, self[keyPath: keyPath])
return true
}
observationStore.observationInfos[observation] = observationHandler
if options.contains(.initial) {
handler(observer, self[keyPath: keyPath])
}
return observation
}
func removeObservation(_ observation: Observation) {
observationStore.observationInfos.removeValue(forKey: observation)
}
func didChangeValue(for keyPath: PartialKeyPath<Self>) {
observationStore.observationInfos = observationStore.observationInfos.filter({ (_, handler) -> Bool in
return handler(keyPath)
})
}
}

Supporting objects to key-value observing

As mentioned before, ObservationStore is needed to added to every class conforming to KeyValueObservable protocol. It stores all the observations and restricts the access to modifying the observations directly from the observable class.

Observation is a simple struct containing an identifier and subtype defining the observation options. In this basic case, it just has initial option what assures handler is called immediately when adding an observer.

final class ObservationStore<T> {
fileprivate var observationInfos = [Observation: (PartialKeyPath<T>) -> Bool]()
var observations: [Observation] {
return observationInfos.map({ $0.key })
}
func removeAll() {
observationInfos.removeAll()
}
}
struct Observation: Hashable {
fileprivate let identifier = UUID()
struct Options: OptionSet {
let rawValue: Int
static let initial = Options(rawValue: 1 << 0)
}
}

Conforming to KeyValueObservable

In this small example a class Event conforms to KeyValueObservable and ViewController observers the title change and updates a label.

final class Event: KeyValueObservable {
let observationStore = ObservationStore<Event>()
var title: String = "Initial Title" {
didSet {
didChangeValue(for: \Event.title)
}
}
}
final class ViewController: UIViewController {
let event = Event()
@IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
event.addObserver(self, keyPath: \.title, options: .initial) { (observer, title) in
observer.label.text = title
}
}
@IBAction func changeTitle(_ sender: Any) {
event.title = "New Title"
}
}
view raw Observing.swift hosted with ❤ by GitHub

Summary

This time we added basic support for observing key paths without using key-value observing APIs known already from Objective-C times. The added KeyValueObservable protocol is easy to add to existing classes but requires manually calling didChangeValue(for:) for every property change.

Inspiration came from Observers in Swift part 2 (Swift by Sundell).

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.

Resources

Categories
Foundation iOS Swift UIKit

Displaying menus with UIContextMenuInteraction on iOS

iOS 13 added a new class named UIContextMenuInteraction what is used for attaching menus to views. When adding menu interaction to a view and user uses 3D Touch or long press gesture on devices not supporting it, a menu is presented alongside with the highlighted content view. Therefore depending on the available space, not all the menu items can fit into the menu.

Setting up UIContextMenuInteraction

UIContextMenuInteraction is initialised with a delegate. Delegate’s job is to create an instance of UIContextMenuConfiguration with provider block for creating a menu when needed. It also should be noted that delegate can return nil in what case no menu is shown.

let interaction = UIContextMenuInteraction(delegate: self)
imageView.addInteraction(interaction)
imageView.isUserInteractionEnabled = true

Creating UIMenus and UIActions

Action provider of UIContextMenuConfiguration is a function taking in suggested menu items and returning an instance of UIMenu. Suggested menu items are the ones provided by responders from responder chain. For example we could use it for sharing actions among multiple responders.

Every menu item is represented by UIAction or another UIMenu allowing to have nested menus. UIActions have title and optionally image and state icon if the state is on. In addition we can explicitly disable actions and set a destructive appearance.

extension ViewController: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
let actionProvider: ([UIMenuElement]) -> UIMenu? = { _ in // menu elements from responder chain if any
// Creating actions…
let imageAction = UIAction(title: "Title (image)",
image: UIImage(systemName: "arkit"),
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .off,
handler: actionHandler)
let destructiveAction = UIAction(title: "Title (destructive)",
image: nil,
identifier: nil,
discoverabilityTitle: nil,
attributes: .destructive,
state: .off,
handler: actionHandler)
let submenu = UIMenu(title: "Submenu",
image: nil,
identifier: nil,
options: [],
children: [destructiveAction, disabledAction])
return UIMenu(title: "Optional Menu Title",
image: nil,
identifier: nil,
options: [],
children: [titleAction, imageAction, onDiscoverabilityTitleAction, submenu])
}
return UIContextMenuConfiguration(identifier: "my identifier" as NSCopying,
previewProvider: nil,
actionProvider: actionProvider)
}

Summary

Creating menus using UIContextMenuInteraction are easy to setup and they look like familiar NSMenu when building iOS app for macOS.

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

UIContextMenu (Xcode 11 beta 4)

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

Categories
iOS Swift

Hashing data using CryptoKit

So far we have been using CommonCrypto when it has come to creating hashes of data. I even wrote about it some time ago and presented a thin layer on top of it making it more convenient to use. In WWDC’19 Apple presented a new framework called CryptoKit. And of course, it contains functions for hashing data.

SHA512, SHA384, SHA256, SHA1 and MD5

CryptoKit contains separate types for SHA512, SHA384 and SHA256. In addition, there are MD5 and SHA1 but those are considered to be insecure and available only because of backwards compatibility reasons. With CryptoKit, hashing data becomes one line of code.

import CryptoKit
let sourceData = "The quick brown fox jumps over the lazy dog".data(using: .utf8)!
let sha512Digest = SHA512.hash(data: sourceData)
print(sha512Digest) // 07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6
let sha384Digest = SHA384.hash(data: sourceData)
print(sha384Digest) // ca737f1014a48f4c0b6dd43cb177b0afd9e5169367544c494011e3317dbf9a509cb1e5dc1e85a941bbee3d7f2afbc9b1
let sha256Digest = SHA256.hash(data: sourceData)
print(sha256Digest) // d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592
view raw .swift hosted with ❤ by GitHub

In case we do not have the whole data available in memory (e.g. really huge file), new types support creating hash by feeding data in piece by piece (just highlighting here how to use the hasher with incremental data).

let dataPieces = ["The ", "quick ", "brown ", "fox ", "jumps ", "over ", "the ", "lazy ", "dog"].map({ $0.data(using: .utf8)! })
var hasher = SHA512()
dataPieces.forEach { (data) in
hasher.update(data: data)
}
print(hasher.finalize()) // 07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6
view raw .swift hosted with ❤ by GitHub

Apple has an excellent playground describing the common operations developers need when using CryptoKit. Highly recommend to check it out if you need something more than just creating hashes.

Summary

CryptoKit is long waited framework what is easy to use and does not require managing raw pointers what was needed to when using CommonCrypto. It now just takes some time when we can bump deployment targets and forget CommonCrypto.

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
Generics iOS Swift UIKit

Testing networking code with custom URLProtocol on iOS

Testing networking code might sound tricky at first but in reality, it just means using custom URLProtocol what returns data we would like to. This allows testing the networking module without mocking URLSession. Using this approach we could do so much more, even integrating a third party networking library.

Networking class wrapping URLSession

Firstly, let’s set up a simple WebClient class what uses URLSession for initiating networking requests. It has a fetch method for loading URLRequest and transforming the response to expected payload type using Codable. As payload can be any type, we use generics here. Note that we need to pass in the payload type as a variable because we need the exact type when decoding the JSON data. How can we test this as URLSession would try to send an actual request to designated URL? As unit tests should behave exactly the same all the time and should not depend on external factors, then using a separate test server is not preferred. Instead, we can intercept the request and provide the response with custom URLProtocol.

final class WebClient {
private let urlSession: URLSession
init(urlSession: URLSession) {
self.urlSession = urlSession
}
func fetch<T: Decodable>(_ request: URLRequest, requestDataType: T.Type, completionHandler: @escaping (Result<T, FetchError>) -> Void) {
let dataTask = urlSession.dataTask(with: request) { (data, urlResponse, error) in
if let error = error {
DispatchQueue.main.async {
completionHandler(.failure(.connection(error)))
}
return
}
guard let urlResponse = urlResponse as? HTTPURLResponse else {
DispatchQueue.main.async {
completionHandler(.failure(.unknown))
}
return
}
switch urlResponse.statusCode {
case 200..<300:
do {
let payload = try JSONDecoder().decode(requestDataType, from: data ?? Data())
DispatchQueue.main.async {
completionHandler(.success(payload))
}
}
catch let jsonError {
DispatchQueue.main.async {
completionHandler(.failure(.invalidData(jsonError)))
}
}
default:
DispatchQueue.main.async {
completionHandler(.failure(.response(urlResponse.statusCode)))
}
}
}
dataTask.resume()
}
}
extension WebClient {
enum FetchError: Error {
case response(Int)
case invalidData(Error)
case connection(Error)
case unknown
}
}
view raw WebClient.swift hosted with ❤ by GitHub

Creating custom URLProtocol for unit tests

URLProtocol is meant to be overridden. Firstly, we’ll need to override canInit(with:) and return true here allowing URLSession to use this protocol for any URL request. Secondly, it is required to override canonicalRequest(for:) where we can just return the same request. Thirdly, startLoading, where we have the loading logic which uses class property for returning appropriate response. This allows us to set this property in unit tests and then returning the result when URLSession handles the fetch request. Finally, URLProtocol also needs to define stopLoading method what we can just leave empty as this protocol is not asynchronous.

final class TestURLProtocol: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
static var loadingHandler: ((URLRequest) -> (HTTPURLResponse, Data?, Error?))?
override func startLoading() {
guard let handler = TestURLProtocol.loadingHandler else {
XCTFail("Loading handler is not set.")
return
}
let (response, data, error) = handler(request)
if let data = data {
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
}
else {
client?.urlProtocol(self, didFailWithError: error!)
}
}
override func stopLoading() {}
}

Using TestURLProtocol for mocking network requests in unit tests

Setting up a unit test requires to set the TestURLProtocol’s loadingHandler and returning the data we would like to. Then we create URLSessionConfiguration and set our TestURLProtocol to protocolClasses. After that we can use this configuration for initialising URLSession and using this session in our WebClient which handles fetch requests. That is pretty much all we need to do for testing networking requests.

final class WebClientTests: XCTestCase {
override func tearDown() {
TestURLProtocol.loadingHandler = nil
}
struct TestPayload: Codable, Equatable {
let country: String
}
func testFetchingDataSuccessfully() {
let expected = TestPayload(country: "Estonia")
let request = URLRequest(url: URL(string: "https://www.example.com&quot;)!)
let responseJSONData = try! JSONEncoder().encode(expected)
TestURLProtocol.loadingHandler = { request in
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
return (response, responseJSONData, nil)
}
let expectation = XCTestExpectation(description: "Loading")
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [TestURLProtocol.self]
let client = WebClient(urlSession: URLSession(configuration: configuration))
client.fetch(request, requestDataType: TestPayload.self) { (result) in
switch result {
case .failure(let error):
XCTFail("Request was not successful: \(error.localizedDescription)")
case .success(let payload):
XCTAssertEqual(payload, expected)
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
}

Summary

Testing networking code at first might sound daunting. But actually it just boils down to using custom URLProtocol and providing response we need to in our test.

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

TestingNetworkRequests (Xcode 5.0, Xcode 10.2.1)

Resources

Categories
iOS Swift UIKit

Storing struct in UserDefaults

Structs can’t be directly stored in UserDefaults because UserDefaults does not know how to serialize it. As UserDefaults is backed by plist files, struct needs to be converted representation supported by it. The core idea boils down to the question, how to convert struct into Dictionary.

Converting struct to Dictionary

The most straight-forward way would be to manually create dictionary by adding all the properties one by one. Depending on the struct, it might get pretty long and also requires maintenance when changing the struct. Therefore, let’s take a look on a way of converting struct into JSON representation instead. This can be achieved by conforming to Encodable and using JSONEncoder and JSONSerialization. In the same way, we can convert Dictionary back to struct with JSONSerialization, JSONDecoder and conforming to Decodable. When conforming struct to Encodable and Decodable, Swift compiler will take care of generating default implementations for methods in those protocols. JSONEncoder and JSONDecoder use those methods for converting struct to Data and back. It should be noted that we could just store data in user defaults. But as data is not human-readable, let’s convert it to Dictionary instead.

struct Context: Codable {
let duration: TimeInterval
let view: String
}
struct SearchInfo: Codable {
let query: String
let numberOfMatches: Int
let context: Context
}
let searchInfos = [SearchInfo(query: "query1", numberOfMatches: 1, context: Context(duration: 1.0, view: "view1")),
SearchInfo(query: "query2", numberOfMatches: 2, context: Context(duration: 2.0, view: "view2"))]
// Converting to dictionary
extension SearchInfo {
var dictionaryRepresentation: [String: Any] {
let data = try! JSONEncoder().encode(self)
return try! JSONSerialization.jsonObject(with: data, options: []) as! [String : Any]
}
}
// Converting back to struct
extension SearchInfo {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { return nil }
guard let info = try? JSONDecoder().decode(SearchInfo.self, from: data) else { return nil }
self = info
}
}
let defaults = UserDefaults()
// [Struct] -> [Dictionary]
let searchInfoDictionaries = searchInfos.map({ $0.dictionaryRepresentation })
// [Dictionary] to UserDefaults
defaults.set(searchInfoDictionaries, forKey: "Searches")
// [Dictionary] from UserDefaults
let dictionariesFromUserDefaults = defaults.array(forKey: "Searches")! as! [[String: Any]]
// [Dictionary] -> [Struct]
let convertedSearchInfos = dictionariesFromUserDefaults.compactMap({ SearchInfo(dictionary: $0) })

Adding DictionaryConvertible and DictionaryDecodable

This implementation can be made a bit more usable by using protocols and protocol extensions for providing default implementations. This makes it extremely easy to adopt this to any objects.

protocol DictionaryConvertible {
var dictionaryRepresentation: [String: Any] { get }
}
protocol DictionaryDecodable {
init?(dictionary: [String: Any])
}
typealias DictionaryRepresentable = DictionaryConvertible & DictionaryDecodable
extension DictionaryConvertible where Self: Encodable {
var dictionaryRepresentation: [String: Any] {
let data = try! JSONEncoder().encode(self)
return try! JSONSerialization.jsonObject(with: data, options: []) as! [String : Any]
}
}
extension DictionaryDecodable where Self: Decodable {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { return nil }
guard let info = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
self = info
}
}
struct AutocompleteResult: Codable, DictionaryRepresentable {
let text: String
let suggestions: [String]
}

Summary

Using Swift’s Codable together with JSONEncoder, JSONDecoder and JSONSerialization we can skip writing code for converting data into different types and instead, providing a concise implementation for turning structs into dictionaries. We only talked about structs but this approach can be applied to classes as well.

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.

Playground

StoringStructInUserDefaults (Xcode 10.2.1, Swift 5.0)

Categories
iOS Swift UIKit

Interactive animation with UIViewPropertyAnimator on iOS

UIViewPropertyAnimator enables configuring animations which can be modified when running. Animations can be paused and progress can be changed allowing to build interactive animations. UIViewPropertyAnimations are in stopped state by default. If we want to run the animation immediately, we can use class method runningPropertyAnimator(withDuration:delay:options:animations:completion:). UIViewPropertyAnimator gives us a lot of flexibility when it comes to composing animations and controlling them. Therefore let’s build an animation consisting of rotating and moving a view out of the visible rect.

Adding a view to animate

Firstly, we need to add a view which we are going to animate using UIViewPropertyAnimator. View is a UIView subclass which just overrides layerClass and returns CAGradientLayer instead. View is positioned into initial place using auto layout.

private lazy var animatingView: UIView = {
let view = GradientView(frame: .zero)
view.gradientLayer.colors = (1…3).map({ "Gradient\($0)" }).map({ UIColor(named: $0)!.cgColor })
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
view.addGestureRecognizer(gestureRecognizer)
view.addSubview(animatingView)
NSLayoutConstraint.activate([
animatingView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 72),
animatingView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -72),
animatingView.topAnchor.constraint(equalTo: view.topAnchor, constant: 72),
animatingView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -72)
])
}

Creating animations with UIViewPropertyAnimator

UIViewPropertyAnimator has several initialisers allowing to control the used timing function. In this example we’ll just use built-in ease in and ease out timing. This just means animation pace increases in the beginning and slows down at the end of the animation. In addition to mentioned UICubicTimingParameters (ease in and ease out), there is support for UISpringTimingParameters as well. Both timing parameters can be passed in using the convenience initialiser init(duration:timingParameters:). The animation is configured to rotate the view by 90 degrees and move the view following a spline created by current point of the view and two other points. When animation ends, we reset the transform and tell auto layout to update the layout which will just move the view back to the initial position.

private func makeAnimator() -> UIViewPropertyAnimator {
let animator = UIViewPropertyAnimator(duration: 2.0, curve: .easeInOut)
let bounds = view.bounds
animator.addAnimations { [weak animatingView] in
guard let animatingView = animatingView else { return }
animatingView.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0)
UIView.animateKeyframes(withDuration: 2.0, delay: 0.0, options: .calculationModeCubic, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.4, animations: {
animatingView.center = CGPoint(x: bounds.width * 0.8, y: bounds.height * 0.85)
})
UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.6, animations: {
animatingView.center = CGPoint(x: bounds.width + animatingView.bounds.height, y: bounds.height * 0.6)
})
})
}
animator.addCompletion({ [weak self] (_) in
guard let self = self else { return }
self.animatingView.transform = CGAffineTransform(rotationAngle: 0.0)
self.animator = nil
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
})
return animator
}
view raw Animator.swift hosted with ❤ by GitHub

Interrupting animation with UIPanGestureRecognizer

UIPanGestureRecognizer is used for interrupting the animation. When user starts dragging a finger on the screen, we capture the current animation progress and the initial point of the touch. Then, we can update the animation progress when dragging the finger to the left or right. When moving the finger back and forth, we can move the animation forward or backwards. As soon as letting the finger go, we start the animation which continues the animation from the update fractionComplete. The constant 300 is just a value defining the amount user needs to move the finger to be able to change the fractionComplete from 0.0 to 1.0.

private lazy var gestureRecognizer: UIPanGestureRecognizer = {
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(updateProgress(_:)))
recognizer.maximumNumberOfTouches = 1
return recognizer
}()
@objc private func updateProgress(_ recognizer: UIPanGestureRecognizer) {
if animator == nil {
animator = makeAnimator()
}
guard let animator = animator else { return }
switch recognizer.state {
case .began:
animator.pauseAnimation()
fractionCompletedStart = animator.fractionComplete
dragStartPosition = recognizer.location(in: view)
case .changed:
animator.pauseAnimation()
let delta = recognizer.location(in: view).x – dragStartPosition.x
animator.fractionComplete = max(0.0, min(1.0, fractionCompletedStart + delta / 300.0))
case .ended:
animator.startAnimation()
default:
break
}
}

Summary

With UIViewPropertyAnimator we can build interactive animations with a very little code. Its API allows controlling the flow of the animations by pausing the animation and controlling the progress of the animation dynamically.

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

InteractiveAnimation (Xcode 10.2, Swift 5)

Resources

Categories
iOS Swift UIKit

UICollectionViewFlowLayout and auto layout on iOS

UICollectionViewFlowLayout is layout object supplied by UIKit and enables showing items in grid. It allows customising spacings and supports fixed size cells and cells with different sizes. This time I am going to show how to build a collection view containing items with different sizes where sizes are defined by auto layout constraints.

Cell sizes in UICollectionViewFlowLayout

By default UICollectionViewFlowLayout uses itemSize property for defining the sizes of the cells. Another way is to use UICollectionViewDelegateFlowLayout and supplying item sizes by implementing collectionView(_:layout:sizeForItemAt:). Third way is to let auto layout for defining the size of the cell. This is what I am going to build this time.

Enabling auto layout based cell sizes

When setting UICollectionViewFlowLayout’s estimatedItemSize to UICollectionViewFlowLayout.automaticSize enables layout object to use auto layout.

final class CollectionViewController: UICollectionViewController {
let content: [String]
init(content: [String]) {
self.content = content
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
super.init(collectionViewLayout: layout)
}
}

Setting up collection view cell with auto-layout constraints

When layout object is configured, second step is to create a cell. In the current prototype it is going to be a simple cell with a label and border around the label. Constraints are set up to have a 8 points space around the label. Constraints together with label’s intrinsicContentSize define the minimum size for the cell. If text is longer, intrinsicContentSize is wider.

final class TextCollectionViewCell: UICollectionViewCell {
let textLabel: UILabel
override init(frame: CGRect) {
textLabel = {
let label = UILabel(frame: .zero)
label.adjustsFontForContentSizeCategory = true
label.font = UIFont.preferredFont(forTextStyle: .body)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
super.init(frame: frame)
contentView.addSubview(textLabel)
NSLayoutConstraint.activate([
textLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
textLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
textLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8),
textLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8)
])
contentView.layer.borderColor = UIColor.darkGray.cgColor
contentView.layer.borderWidth = 1
contentView.layer.cornerRadius = 4
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

When putting all the things together, the end result is a collection view where every cell has its own size. Moreover, it supports dynamic type and cells will grow if user changes default text sizes.

Summary

UICollectionViewFlowLayout’s estimatedItemSize enables using auto-layout for defining cell sizes. Therefore creating cells where text defines the size of the cell is simple to do on iOS.

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

FittingCellsInCollectionViewFlowLayout Xcode 10.2, Swift 5.0

Resources