Categories
iOS Swift SwiftUI Xcode

Discovering #Preview macro in Swift

Previews are snippets of codes for creating a live preview of a view what we can see in Xcode. This enables to quickly iterate your views because we can see right away how the view looks after each of the code change. Previews are compiled alongside with the app, and therefore we can access all the other code and resources like images. If we want to use preview only resource, we can use Xcode’s development assets feature for making the resource only part of the app when rendering previews. Now when we know what previews are in general, let’s move on to creating a preview.

So far we have created a new struct which conforms to a PreviewProvider protocol if we wanted Xcode to render the view for us. Xcode 15 with Swift 5.9’s macro support introduces a #Preview macro, which replaces the old way of creating live previews. The benefit of the new macro is having to write less code to get going with live previews. Let’s compare the both approaches and have a look at an accessory view’s preview.

// Before
struct RatingsView_Previews: PreviewProvider {
static var previews: some View {
VStack {
ForEach(05, id: \.self) { value in
RatingsView(value: .constant(value))
}
}
.previewLayout(.sizeThatFits)
}
}
// After
#Preview(traits: .sizeThatFitsLayout) {
VStack {
ForEach(05, id: \.self) { value in
RatingsView(value: .constant(value))
}
}
}
view raw Preview.swift hosted with ❤ by GitHub

In the example above, we wanted to apply a trait since this view is a tiny accessory view, and therefore we would like to see it rendered as small as possible. A list of traits what we can use are listed here:

extension PreviewTrait where T == Preview.ViewTraits {
/// Preview with `.device` layout (the default).
public static var defaultLayout: PreviewTrait<Preview.ViewTraits> { get }
public static var sizeThatFitsLayout: PreviewTrait<Preview.ViewTraits> { get }
public static func fixedLayout(width: CGFloat, height: CGFloat) -> PreviewTrait<T>
public static var portrait: PreviewTrait<Preview.ViewTraits> { get }
public static var landscapeLeft: PreviewTrait<Preview.ViewTraits> { get }
public static var landscapeRight: PreviewTrait<Preview.ViewTraits> { get }
public static var portraitUpsideDown: PreviewTrait<Preview.ViewTraits> { get }
}

When working with full screen views, the preview macro can be as simple as this:

#Preview {
OnboardingView()
}

In addition to traits, we can also give a name to the preview which is displayed in the Xcode’s preview canvas. This can be useful if we create multiple previews for the same view.

A view with multiple previews with different names.

Another thing to note is that the preview canvas in Xcode also lists a pin button next to the name of the preview. Pinning previews is useful if we want to navigate to another file to make some changes and keeping the preview running. Maybe we want to change some constants which affects the layout of the view, but these constants are defined somewhere else. Then it is useful to keep the preview running and seeing how changing a constant in another view is reflected by the view using it.

There is another tip to keep in mind. We can run previews on physical devices as well. We just need to pick the physical device instead of a simulator from the device picker.

Preview canvas, which lists a physical device for preview.

Finally, let’s not forget about the great way to see all the colour scheme and dynamic type variants at the same time. There is a separate variants button for that next to the live preview and selectable preview buttons.

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 SwiftUI

Bizarre error in SwiftUI preview

The other day, I was playing around with matchedGeometryEffect view modifier in my sample app. I was just planning to show a list of items and then animate moving one item from one HStack to another. Suddenly, my SwiftUI preview stopped working. On the other hand, running exactly the same code on the simulator just worked fine. The code was very simple, consisting of view, view model and an Item model struct.

import SwiftUI
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(viewModel.items) { item in
Text(verbatim: item.name)
}
}
.padding()
}
}
extension ContentView {
final class ViewModel: ObservableObject {
let items: [Item] = [
Item(name: "first"),
Item(name: "second")
]
func select(_ item: Item) {
// implement
}
}
struct Item: Identifiable {
let name: String
var id: String { name }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

If you try to render SwiftUI preview (I was using Xcode 14.3) then Xcode is giving up with “Failed to launch the app XXX in reasonable time”. But if I try to build and run it on simulator, it just works fine. After some trial and error, it turned out that SwiftUI previews broke as soon as I added the func select(_ item: Item) function. If you pay a close attention, then you can see that the longer type name for Item is ContentView.Item, but within the ContentView.ViewModel type I am using just Item. I do not know why, but SwiftUI previews seems to get confused by it. As soon as I change the function declaration to func select(_ item: ContentView.Item) the preview starts rendering again. Another way is declaring the Item struct outside the ContentView extension.

The learning point is that if SwiftUI previews stop working suddenly, then make sure to check how nested types are used in function declarations.

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

Using SwiftUI previews for UIKit views

SwiftUI provides wrappers for UIViewController and UIView on iOS. Same wrappers are also available for AppKit views on macOS. Let’s see how to use those wrappers for rendering UIKit views in SwiftUI previews and therefore benefiting from seeing changes immediately. Note that even when a project can’t support SwiftUI views because of the minimum deployment target, then this is still something what can be used when compiling the project with debug settings. Preview related code should only be compiled in debug builds and is never meant to be compiled in release builds. Before we jump in, there are two very useful shortcuts for keeping in mind: option+command+return for toggling previews and option+command+p for refreshing previews.

UIViewControllerRepresentable for wrapping UIViewControllers

UIViewControllerRepresentable is a protocol which can be used for wrapping UIViewController and representing it in SwiftUI. We can add a struct which conforms to that protocol and then creating an instance of the view controller in the makeUIViewController method. Second step is to add another struct which implements PreviewProvider protocol and which is used by Xcode for rendering previews. In simple cases we can get away only with such implementation but in more complex view controllers we would need to set up dependencies and generate example data for the preview. If need to do this, then all that code can be added to the makeUIViewController method.

import UIKit
import SwiftUI
final class ContentViewController: UIViewController {
override func loadView() {
self.view = UIView()
self.view.backgroundColor = .systemBackground
}
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView(frame: .zero)
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 16)
])
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(label)
label.textColor = .systemRed
label.text = "Red text"
}
}
// MARK: SwiftUI Preview
#if DEBUG
struct ContentViewControllerContainerView: UIViewControllerRepresentable {
typealias UIViewControllerType = ContentViewController
func makeUIViewController(context: Context) -> UIViewControllerType {
return ContentViewController()
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentViewController_Previews: PreviewProvider {
static var previews: some View {
ContentViewControllerContainerView().colorScheme(.light) // or .dark
}
}
#endif
Wrapping UIViewController with UIViewControllerRepresentable.
UIViewController shown using SwiftUI

UIViewRepresentable for wrapping UIViews

UIViewRepresentable follows the same flow. In the example below, we use Group for showing two views with fixed size and different appearances at the same time.

import SwiftUI
import UIKit
final class BackgroundView: UIView {
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .systemBackground
layer.cornerRadius = 32
layer.borderColor = UIColor.systemBlue.cgColor
layer.borderWidth = 14
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: SwiftUI Preview
#if DEBUG
struct BackgroundViewContainer: UIViewRepresentable {
typealias UIViewType = BackgroundView
func makeUIView(context: Context) -> UIViewType {
return BackgroundView(frame: .zero)
}
func updateUIView(_ uiView: BackgroundView, context: Context) {}
}
struct BackgroundViewContainer_Previews: PreviewProvider {
static var previews: some View {
Group {
BackgroundViewContainer().colorScheme(.light)
BackgroundViewContainer().colorScheme(.dark)
}.previewLayout(.fixed(width: 200, height: 200))
}
}
#endif
Wrapping UIView subclass with UIViewRepresentable.
Multiple UIViews shown in SwiftUI preview at the same time.

Summary

We looked into how to wrap view controllers and views for SwiftUI previews. Previews only required a little bit of code and therefore it is something what we can use for improving our workflows when working with UIKit 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.

Project

UIKitInSwiftUIPreview (Xcode 11.5)

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)