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

Creating persistent data store on iOS

Storing data persistently on iOS is something what is needed quite often. In this post, we are going to look into how to build a persistent data store and how to store image data.

Initialising the persistent data store

Persistent data store is an object managing a folder on disk. It allows writing and reading data asynchronously.
Firstly, we need to create a folder where to store all the files. As every instance of the data store should manage its own folder, we will add an argument name to the initialiser. Then we can create a folder in user’s documents folder with that name. As writing and reading data is an expensive operation, we are going to offload the work to a concurrent DispatchQueue. Concurrent dispatch queue allows us to read multiple files at the same time (more about it a bit later).

final class PersistentDataStore {
let name: String
private let dataStoreURL: URL
private let queue: DispatchQueue
init(name: String) throws {
self.name = name
queue = DispatchQueue(label: "com.augmentedcode.persistentdatastore", qos: .userInitiated, attributes: .concurrent, autoreleaseFrequency: .workItem)
let documentsURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
dataStoreURL = documentsURL.appendingPathComponent(name, isDirectory: true)
try FileManager.default.createDirectory(at: dataStoreURL, withIntermediateDirectories: true, attributes: nil)
}
}

Storing data asynchronously

Method for storing data on disk consists of closure, identifier and completion handler. This allows us to create a closure what transforms object to data. For example, it could transform UIImage to Data. Secondly, this transformation, possibly slow operation, can be offloaded to the same thread writing the data into a file. Using closure gives us a flexible API what we can extend with convenience methods.

typealias Identifier = String
enum Result {
case failed(Error)
case noData
case success(Identifier)
}
func storeData(_ dataProvider: @escaping () -> (Data?), identifier: Identifier = UUID().uuidString, completionHandler block: @escaping (Result) -> ()) {
queue.async(flags: .barrier) {
let url = self.url(forIdentifier: identifier)
guard let data = dataProvider(), !data.isEmpty else {
DispatchQueue.main.async {
block(.noData)
}
return
}
do {
try data.write(to: url, options: .atomic)
DispatchQueue.main.async {
block(.success(identifier))
}
}
catch {
DispatchQueue.main.async {
block(.failed(error))
}
}
}
}
// Example (adding data to data store with unique identifier):
persistentStore.storeData({ () -> (Data?) in
return image.jpegData(compressionQuality: 1.0)
}) { (result) in
switch result {
case .success(let identifier):
print("Stored data successfully with identifier \(identifier).")
case .noData:
print("No data to store.")
case .failed(let error):
print("Failed storing data with error \(error)")
}
}

Identifier is internally used as a filename and default implementation creates unique identifier. Therefore, when data store consumer would like to replace the current file, it can supply an identifier, otherwise new file is created.
Completion handler contains a Result enum type. Result enum consists of three cases: success, transformation failure and data writing failure. Success’ associated value is identifier, failure contains error object and transformation failure is equal to noData.
Important to note here is that the work item has barrier specified. Barrier means that when DispatchQueue starts to handle the work item, it will wait until all the previous work items have finished running. Meaning, we will never try to update a file on disk when some other request is busy reading it.

Loading data asynchronously

Load data is generic method allowing the data transformation closure to return a specific type (e.g. transforming Data to UIImage). Shortly, load data reads file from disk and transforms it into a different type. As transformation can be a lengthy task, it is yet again running on the background thread and will not cause any hiccups in the UI.

func loadData<T>(forIdentifier identifier: Identifier, dataTransformer: @escaping (Data) -> (T?), completionHandler block: @escaping (T?) -> ()) {
queue.async {
let url = self.url(forIdentifier: identifier)
guard FileManager.default.fileExists(atPath: url.path) else {
DispatchQueue.main.async {
block(nil)
}
return
}
do {
let data = try Data(contentsOf: url, options: .mappedIfSafe)
let object = dataTransformer(data)
DispatchQueue.main.async {
block(object)
}
}
catch {
print("Failed reading data at URL \(url).")
DispatchQueue.main.async {
block(nil)
}
}
}
}
// Example
persistentStore.loadData(forIdentifier: "my_identifier", dataTransformer: { UIImage(data: $0) }) { (image) in
guard let image = image else {
print("Failed loading image.")
return
}
print(image)
}

Removing data asynchronously

Removing a single file or all of the files is pretty straight-forward. As we are modifying files on disk, we will use barrier again and then FileManager’s removeItem(at:) together with contentsOfDirectory(at:includingPropertiesForKeys:options:).

func removeData(forIdentifier identifier: Identifier) {
queue.async(flags: .barrier) {
let url = self.url(forIdentifier: identifier)
guard FileManager.default.fileExists(atPath: url.path) else { return }
do {
try FileManager.default.removeItem(at: url)
}
catch {
print("Failed removing file at URL \(url) with error \(error).")
}
}
}
func removeAll() {
queue.async(flags: .barrier) {
do {
let urls = try FileManager.default.contentsOfDirectory(at: self.dataStoreURL, includingPropertiesForKeys: nil, options: [])
try urls.forEach({ try FileManager.default.removeItem(at: $0) })
}
catch {
print("Failed removing all files with error \(error).")
}
}
}

Extension for storing images

It is easy to extend the PersistentDataStore with convenience methods for storing a specific type of data. This allows us to hide the technical details of transforming image to data and vice-versa. Moreover, calling the method gets easier to read as data transformation closure is not visible anymore.

extension PersistentDataStore {
func loadImage(forIdentifier identifier: Identifier, completionHandler block: @escaping (UIImage?) -> (Void)) {
loadData(forIdentifier: identifier, dataTransformer: { UIImage(data: $0) }, completionHandler: block)
}
func storeImage(_ image: UIImage, identifier: String = UUID().uuidString, completionHandler handler: @escaping (Result) -> ()) {
storeData({ image.jpegData(compressionQuality: 1.0) }, identifier: identifier, completionHandler: handler)
}
}
// Examples:
persistentStore.storeImage(image) { (result) in
print(result)
}
persistentStore.loadImage(forIdentifier: "my_identifier") { (image) -> (Void) in
guard let image = image else {
print("Failed loading image.")
return
}
print(image)
}

Summary

We created a persistent data store what is performant and has a flexible API. API can be extended easily to support any other data transformation. In addition, it uses thread-safe techniques for making sure data never gets corrupted.

Playground

PersistentDataStore (GitHub) Xcode 10, Swift 4.2

References

DispatchQueues (Apple)
dispatch_barrier_async (Apple)