Categories
Foundation iOS Swift UIKit

Embedding a view in UIScrollView on iOS

This time we are going to look into how to embed custom view in UIScrollView and setting up autolayout constraints for different scenarios.

Setting up view controller with scroll view

We’ll gonna create a new view controller what contains UIScrollView as it’s subview. UIScrollView will cover the whole view area. Everything is set up in code as it is easier to represent the steps needed. It is easy to use those steps and setting up constraints in interface builder.

override func loadView() {
self.view = UIView(frame: .zero)
let scrollView: UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.backgroundColor = .white
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
view.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
Adding scroll view to parent view

Case 1: Vertically scrollable view

Most common case is to have scroll view with vertically scrollable content. Embedded view’s leading, trailing, top and bottom layout anchors should be equal to scroll view’s content layout margin’s corresponding anchors. For making the view only scrollable in vertical axis, embedded view’s width should match with scroll view’s width. This is achieved by adding an extra width constraint to the embedded view. Width constraint should make the view width equal to scroll view’s width. When setting view’s up in interface builder, make sure to add at least one subview to the embedded view what has intrinsic content size (label, button etc) (also add constraints). Otherwise interface builder does not know what size the embedded view has.

let customView = CustomView(frame: .zero) // view with intrinsicContentSize
customView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(customView)
NSLayoutConstraint.activate([
customView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
customView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
customView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
customView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
customView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
})
Vertically scrollable embedded view

Case 2: Horizontally scrollable view

Horizontally scrollable view follows the same setup except instead of width constraint, we’ll add height constraint instead. When setting up the height constraint, scroll view’s safeAreaLayoutGuide’s heightAnchor should be used. This layout guide ignores the portion of the view what is covered by navigation bars, tab bars, toolbars, and other views.

let customView = CustomView(frame: .zero) // view with intrinsicContentSize
customView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(customView)
NSLayoutConstraint.activate([
customView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
customView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
customView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
customView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
customView.heightAnchor.constraint(equalTo: scrollView.safeAreaLayoutGuide.heightAnchor)
])
Horizontally scrollable embedded view

Case 3: Vertically and horizontally scrollable view

For making the embedded view to be scrollable in horizontal and vertical axis, we’ll only add leading, trailing, top and bottom constraints to scroll view’s contentLayoutGuide’s corresponding anchors. Embedded view should define intrinsicContentSize as this size is used by scroll view for knowing what size the view wants to be. This kind of layout is probably usable only for fixed size view’s. For example, building a custom grid view where the row and column count define the size of the view.

let customView = CustomView(frame: .zero) // view with intrinsicContentSize
customView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(customView)
NSLayoutConstraint.activate([
customView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
customView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
customView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
customView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor)
])
Vertically and horizontally scrollable embedded view

Summary

We took a look on how to add constraints to embedded view in scroll view for getting a specific scrolling behaviour. In one sentence: leading, trailing, top, and bottom constraints to contentLayoutGuide and adding width or height constraint when necessary.

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

UIScrollViewExample (Xcode 11.0)

Categories
Foundation iOS Swift

Replacing multiple text tokens in Swift

Let’s see how to replace multiple tokens in string. As an example problem to solve we will use this string:
The quick <color_1> <animal_1> jumps over the lazy <animal_2>

String extension for replacing tokens

Token’s format is < text _ numbers > what can be turned into regular expression: <[:alpha:]+_{1}[:digit:]+>.

We’ll extend string and add a function what takes in regular expression and closure responsible of providing replacement strings. For finding tokens, we’ll use NSRegularExpression and get all the matches in the string. Next step is to reverse enumerate matches and replace tokens. Reverse enumerating is required it ensures that token ranges are constant. If we would start replacing from the first match, then all the succeeding ranges should be shifted based on the length difference of all the preceding tokens and replacements. In this case reduce is convenient because we can enumerate all the matches and then mutating the copy of the initial string with very few lines. Another aspect to note is that NSRegularExpression uses NSRange instead of <a rel="noreferrer noopener" aria-label="RangeRange<String.Index>. Therefore we need to convert ranges from one type to another making sure character indexes match.
This function can now be used with custom logic when providing replacements. For example: we can have a simple mapping or even returning the same replacement string.

let text = "The quick <color_1> <animal_1> jumps over the lazy <animal_2>"
let replacementMap = ["<animal_1>": "fox", "<animal_2>": "dog", "<color_1>": "brown"]
extension String {
func replacingOccurrences(matchingPattern pattern: String, replacementProvider: (String) -> String?) -> String {
let expression = try! NSRegularExpression(pattern: pattern, options: [])
let matches = expression.matches(in: self, options: [], range: NSRange(startIndex..<endIndex, in: self))
return matches.reversed().reduce(into: self) { (current, result) in
let range = Range(result.range, in: current)!
let token = String(current[range])
guard let replacement = replacementProvider(token) else { return }
current.replaceSubrange(range, with: replacement)
}
}
}
let finalString1 = text.replacingOccurrences(matchingPattern: "<[:alpha:]+_{1}[:digit:]+>", replacementProvider: { replacementMap[$0] })
let finalString2 = text.replacingOccurrences(matchingPattern: "<[:alpha:]+_{1}[:digit:]+>", replacementProvider: { _ in "REPLACEMENT" })
print(text)
print(finalString1) // The quick brown fox jumps over the lazy dog
print(finalString2) // The quick REPLACEMENT REPLACEMENT jumps over the lazy REPLACEMENT
String extension replacing tokens matching a pattern.

Summary

When we would like to do multiple replacements in a string, then one of the approaches is to get all the replacement ranges and then reverse enumerating the ranges and making replacements. In this way we can avoid having complex code trying to adjust based on the length difference of the source and replacement string.

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
iOS LinkPresentation macOS Swift UIKit

Loading URL previews using LinkPresentation framework in Swift

iOS and macOS got a new framework in WWDC’19 named LinkPresentation. LinkPresentation enables fetching URL previews.

Adding LPLinkView for presenting preview

LPLinkView is a view subclass meant for rendering LPLinkMetadata. LPLinkMetadata contains information about the link: title, icon, image, video.

import LinkPresentation
let linkView = LPLinkView(metadata: LPLinkMetadata())
linkView.translatesAutoresizingMaskIntoConstraints = false
stackView.insertArrangedSubview(linkView, at: 0)
Adding LPLinkView to stack view

Fetching previews with LPMetadataProvider

Instances of LPLinkMetadata are fetched using LPLinkMetadataProvider for a given url. LPLinkMetadata is conforming to NSSecureCoding what enables a way of converting it to Data and storing the metadata on disk. Next time we need metadata for this url, we can use a locally cached data instead. Archiving and unarchiving is done with help of NSKeyedArchiver and NSKeyedUnarchiver and in the example archived data is stored in UserDefaults. In real apps it makes sense to store the data in separate files instead and not polluting UserDefaults with preview data.

private let metadataStorage = MetadataStorage()
private lazy var metadataProvider = LPMetadataProvider()
private weak var linkView: LPLinkView?
@IBAction func loadPreview(_ sender: UIButton) {
if let text = textField.text, let url = URL(string: text) {
// Avoid fetching LPLinkMetadata every time and archieve it disk
if let metadata = metadataStorage.metadata(for: url) {
linkView?.metadata = metadata
return
}
metadataProvider.startFetchingMetadata(for: url) { [weak self] (metadata, error) in
if let error = error {
print(error)
}
else if let metadata = metadata {
DispatchQueue.main.async { [weak self] in
self?.metadataStorage.store(metadata)
self?.linkView?.metadata = metadata
}
}
}
}
}
Fetching and caching LPMetadata
struct MetadataStorage {
private let storage = UserDefaults.standard
func store(_ metadata: LPLinkMetadata) {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: metadata, requiringSecureCoding: true)
var metadatas = storage.dictionary(forKey: "Metadata") as? [String: Data] ?? [String: Data]()
while metadatas.count > 10 {
metadatas.removeValue(forKey: metadatas.randomElement()!.key)
}
metadatas[metadata.originalURL!.absoluteString] = data
storage.set(metadatas, forKey: "Metadata")
}
catch {
print("Failed storing metadata with error \(error as NSError)")
}
}
func metadata(for url: URL) -> LPLinkMetadata? {
guard let metadatas = storage.dictionary(forKey: "Metadata") as? [String: Data] else { return nil }
guard let data = metadatas[url.absoluteString] else { return nil }
do {
return try NSKeyedUnarchiver.unarchivedObject(ofClass: LPLinkMetadata.self, from: data)
}
catch {
print("Failed to unarchive metadata with error \(error as NSError)")
return nil
}
}
}
Local LPMetadata storage using UserDefaults as storage

Summary

LinkPresentation framework adds a easy way of fetching previews for web pages. It provides a LPLinkView class making it extremely easy to render the preview.

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

LinkPresentationView (Xcode 11 GM)

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
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
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