Categories
Swift SwiftUI

Opening hyperlinks in SwiftUI

Opening hyperlinks in UIKit with UILabel is unexpectedly complex, what about SwiftUI? In this post, we’ll dive into opening hyperlinks in SwiftUI.

If we just would like to show a hyperlink, then the best way is to the Link view. We can just feed it with a title and the destination URL. In addition, we can even apply a button style to it.

Link("Silky Brew", destination: AppConstants.URLs.silkyBrew)
  .buttonStyle(.borderedProminent)

By default, URLs are opened in the default web browser or if we are dealing with universal links, then in the appropriate app. If we have a desire to change how links are opened, we can apply a custom OpenURLAction. Here is an example how to open a URL in SFSafariViewController (SafariURL is just an Identifiable supported URL wrapper used for sheet’s binding and SafariView is SFSafariViewController wrapper with UIViewControllerRepresentable).

Link("Signal Path", destination: AppConstants.URLs.signalPath)
  .environment(\.openURL, OpenURLAction(handler: { url in
    safariURL = SafariURL(url: url)
    return .handled
}))
  .sheet(item: $safariURL, content: { safariURL in
    SafariView(url: safariURL.url) 
  })

Often we are dealing with a case where we have text which contains some links as well. In comparison to UIKit, it is way more simple. We can just use the Markdown syntax to define the link and that is all to it.

Text("Hello, world! Here is my [blog](https://augmentedcode.io/blog)")

If we would like to use a custom URL handler, then we can override the default handler through the openURL environment value. Can be handy to just have keys for URL in text and substituting these with actual URLs when handling the tap.

Text("Here are some apps: [Silky Brew](silky), [Signal Path](signal), and [Drifty Asteroid](drifty)")
                .environment(\.openURL, OpenURLAction(handler: { url in
                    switch url.absoluteString {
                    case "drifty": .systemAction(AppConstants.URLs.driftyAsteroid)
                    case "signal": .systemAction(AppConstants.URLs.signalPath)
                    case "silky": .systemAction(AppConstants.URLs.silkyBrew)
                    default: .systemAction
                    }
                }))

When talking about the OpenURLAction in greater detail, then the different return values are:

  • handledĀ – handler took care of opening the URL (e.g. opening the URL in SFSafariViewController)
  • discardedĀ – handler ignored the handling
  • systemActionĀ – system handler opens the URL
  • systemAction(_:)Ā – use a different URL (e.g. adding query parameters)

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

Changes to URL string parsing in iOS 17

If we go to Apple’s documentation for URL’s init(string:) method, then this comes with a huge yellow banner describing new behaviour in iOS 17.

For apps linked on or after iOS 17 and aligned OS versions,Ā URLĀ parsing has updated from the obsolete RFC 1738/1808 parsing to the sameĀ RFC 3986Ā parsing asĀ URLComponents. This unifies the parsing behaviors of theĀ URLĀ andĀ URLComponentsĀ APIs. Now,Ā URLĀ automatically percent- and IDNA-encodes invalid characters to help create a valid URL.

https://developer.apple.com/documentation/foundation/url/3126806-init

Switching to the newer URL specification is a big deal, and in addition, the last sentence says that the new default is that URL(string:) tries to encode invalid characters. This is a big deal. This was not a case before. If we want to keep using the pre-iOS 17 behaviour, then we would need to replace URL(string:) with the new URL(string:encodingInvalidCharacters:) and passing false to the second argument. Apps which deals with URL strings definitely need to be tested thoroughly on iOS 17.

// iOS 16
URL(string: "my string") -> nil
// iOS 17
URL(string: "my string") -> my%20string
but
URL(string: "my string", encodingInvalidCharacters: false) -> nil 

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

URL type properties for folders in iOS

I have a tiny Swift package, what I have been using for reading and writing data on disk. Data is written to a subfolder in the documents folder. Beginning with iOS 16 there is a new way how to create that URL. It is such a tiny addition to what should have been there a long time ago.

let url = try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
// vs
let url2 = URL.documentsDirectory
view raw URL.swift hosted with ❤ by GitHub

Here we can see that instead of a throwing function which has 4 parameters, we can replace it with a non-throwing type property. Finally! Not sure why it gives me so much happiness, maybe because I always forget the URL API whenever I need it.

static var applicationDirectory: URL
static var applicationSupportDirectory: URL
static var cachesDirectory: URL
static var desktoDirectory: URL
static var documentsDirectory: URL
static var downloadsDirectory: URL
static var homeDirectory: URL
static var libraryDirectory: URL
static var moviesDirectory: URL
static var musicDirectory: URL
static var picturesDirectory: URL
static var sharedPublicDirectory: URL
static var temporaryDirectory: URL
static var trashDirectory: URL
static var userDirectory: URL
view raw URL.swift hosted with ❤ by GitHub

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)