Skip to content

Commit

Permalink
Add preview gallery (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
noahsmartin authored Aug 31, 2023
1 parent c28fb24 commit 8f6092b
Show file tree
Hide file tree
Showing 14 changed files with 407 additions and 0 deletions.
7 changes: 7 additions & 0 deletions DemoApp/DemoApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
FA1671FB2A53E03800A42DB0 /* MessageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1671FA2A53E03800A42DB0 /* MessageRow.swift */; };
FA1671FD2A53E11E00A42DB0 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1671FC2A53E11E00A42DB0 /* MessageView.swift */; };
FA1671FF2A53E1AF00A42DB0 /* ConversationMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1671FE2A53E1AF00A42DB0 /* ConversationMessageView.swift */; };
FA170AD32A8B3BA400D1D53D /* PreviewGallery in Frameworks */ = {isa = PBXBuildFile; productRef = FA170AD22A8B3BA400D1D53D /* PreviewGallery */; };
FA3C0DE72A620E9A00278952 /* MyPreviewTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3C0DE62A620E9A00278952 /* MyPreviewTest.swift */; };
FA40515D2A95B587007A66D4 /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA40515C2A95B587007A66D4 /* EmptyView.swift */; };
FA4F5AD92A7A3E7700B268FF /* Snapshotting in Frameworks */ = {isa = PBXBuildFile; productRef = FA4F5AD82A7A3E7700B268FF /* Snapshotting */; };
Expand Down Expand Up @@ -127,6 +128,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FA170AD32A8B3BA400D1D53D /* PreviewGallery in Frameworks */,
FACA23792A55FBEE0080545A /* DemoModule.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -276,6 +278,7 @@
);
name = DemoApp;
packageProductDependencies = (
FA170AD22A8B3BA400D1D53D /* PreviewGallery */,
);
productName = DemoApp;
productReference = FA1671BF2A5367A800A42DB0 /* DemoApp.app */;
Expand Down Expand Up @@ -827,6 +830,10 @@
isa = XCSwiftPackageProductDependency;
productName = SnapshottingTests;
};
FA170AD22A8B3BA400D1D53D /* PreviewGallery */ = {
isa = XCSwiftPackageProductDependency;
productName = PreviewGallery;
};
FA4F5AD82A7A3E7700B268FF /* Snapshotting */ = {
isa = XCSwiftPackageProductDependency;
productName = Snapshotting;
Expand Down
4 changes: 4 additions & 0 deletions DemoApp/DemoApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@

import SwiftUI
import DemoModule
import PreviewGallery

struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink("Open Gallery") {
LazyView(PreviewGallery())
}
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Expand Down
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ let package = Package(
platforms: [.iOS(.v16)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "PreviewGallery",
targets: ["PreviewGallery"]),
// Test library to import in your XCTest target.
// This is the only library that depends on XCTest.framework
.library(
Expand All @@ -34,6 +37,7 @@ let package = Package(
.target(name: "Snapshotting", dependencies: ["SnapshottingSwift"]),
// Swift code in the inserted dylib
.target(name: "SnapshottingSwift", dependencies: ["SnapshotPreviewsCore"]),
.target(name: "PreviewGallery", dependencies: ["SnapshotPreviewsCore"]),
.testTarget(
name: "SnapshotPreviewsTests",
dependencies: ["SnapshotPreviewsCore"]),
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ Emerge handles the heavy lifting of generating, diffing and hosting the snapshot

See [the documentation](https://docs.emergetools.com/docs/swiftui-previews) for how to set up snapshot testing for your app.

## Preview Gallery

`PreviewGallery` is an interactive UI built on top of snapshot extraction. It turns your SwiftUI previews into a gallery of compoents and features you can access from your application. Xcode is not required to view the previews. You can use it to preview individual components (buttons/rows/icons/etc)
or even entire interactive features.

<p align="center">
<img src="./images/image1.png" />
</p>

The public API of PreviewGallery is just one SwiftUI `View` named `PreviewGallery`. Just display this view to get the full gallery. For example, you could add a button like this:

```swift
import SwiftUI
import PreviewGallery

NavigationLink("Open Gallery") { PreviewGallery() }
```

## Local Debugging

Use this Swift Package for locally debugging your views snapshots. You’ll need a UI test target that imports the `SnapshottingTests` and `Snapshotting` products from this package. Create a test that inherits from `PreviewTest` like this:
Expand Down
63 changes: 63 additions & 0 deletions Sources/PreviewGallery/Checkerboard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// Checkerboard.swift
//
//
// Created by Noah Martin on 7/4/23.
//

import SwiftUI

struct Checkerboard: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()

let rowSize: Double = 10
let columnSize: Double = 10

let rows = Int(rect.height / CGFloat(rowSize))
let columns = Int(rect.width / CGFloat(columnSize))
let columnRemainder = rect.width - Double(columns) * columnSize
let rowRemainder = rect.height - Double(rows) * rowSize

for row in 0 ..< rows {
for column in 0 ..< columns {
if (row + column).isMultiple(of: 2) {
let startX = Double(columnSize) * Double(column)
let startY = Double(rowSize) * Double(row)

let rect = CGRect(x: startX, y: startY, width: columnSize, height: rowSize)
path.addRect(rect)
}
}
if (row + columns).isMultiple(of: 2) {
if columnRemainder > 0 {
let startX = Double(columnSize) * Double(columns)
let startY = Double(rowSize) * Double(row)

let rect = CGRect(x: startX, y: startY, width: columnRemainder, height: rowSize)
path.addRect(rect)
}
}
}
if rowRemainder > 0 {
for column in 0..<columns {
if (rows + column).isMultiple(of: 2) {
let startX = Double(columnSize) * Double(column)
let startY = Double(rowSize) * Double(rows)

let rect = CGRect(x: startX, y: startY, width: columnSize, height: rowRemainder)
path.addRect(rect)
}
}
if (rows + columns).isMultiple(of: 2) {
let startX = Double(columnSize) * Double(columns)
let startY = Double(rowSize) * Double(rows)

let rect = CGRect(x: startX, y: startY, width: columnRemainder, height: rowRemainder)
path.addRect(rect)
}
}

return path
}
}
54 changes: 54 additions & 0 deletions Sources/PreviewGallery/ModuleFeatures.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// ModuleFeatures.swift
//
//
// Created by Noah Martin on 8/31/23.
//

import Foundation
import SwiftUI

struct ModuleFeatures: View {

let module: String
let data: PreviewData

var body: some View {
let featureProviders = data.previews(in: module).filter { provider in
return provider.previews.contains { preview in
return preview.requiresFullScreen
}
}
return List {
ForEach(featureProviders) { provider in
let featurePreviews = provider.previews.filter { $0.requiresFullScreen }
NavigationLink {
if featurePreviews.count == 1 {
try! featurePreviews[0].view()
} else {
List {
ForEach(featurePreviews) { preview in
NavigationLink(preview.displayName ?? provider.displayName) {
try! preview.view()
}
}
}.navigationTitle(provider.displayName)
}
} label: {
VStack(alignment: .leading) {
Text(provider.displayName)
.font(.headline)
.foregroundStyle(Color(UIColor.label))
.padding(.leading, 8)

Text("\(featurePreviews.count) Preview\(featurePreviews.count != 1 ? "s" : "")")
.font(.subheadline)
.foregroundStyle(Color(UIColor.secondaryLabel))
.padding(.leading, 8)
}
}
}
}.navigationTitle("Features")
}

}
49 changes: 49 additions & 0 deletions Sources/PreviewGallery/ModulePreviews.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// ModulePreviews.swift
//
//
// Created by Noah Martin on 7/3/23.
//

import Foundation
import SwiftUI

struct ModulePreviews: View {
let module: String
let data: PreviewData

var body: some View {
let componentProviders = data.previews(in: module).filter { provider in
return provider.previews.contains { preview in
return !preview.requiresFullScreen
}
}
let featureProviders = data.previews(in: module).filter { provider in
return provider.previews.contains { preview in
return preview.requiresFullScreen
}
}
return NavigationLink(module) {
ScrollView {
LazyVStack(alignment: .leading) {
if !featureProviders.isEmpty {
NavigationLink {
ModuleFeatures(module: module, data: data)
} label: {
VStack {
TitleSubtitleRow(
title: "Features",
subtitle: "\(featureProviders.count) Preview\(featureProviders.count != 1 ? "s" : "")")
Divider()
}
}
}
ForEach(componentProviders) { preview in
PreviewCellView(preview: preview)
}
}
}
.navigationTitle(module)
}
}
}
22 changes: 22 additions & 0 deletions Sources/PreviewGallery/Preview+FullScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Preview+FullScreen.swift
//
//
// Created by Noah Martin on 8/31/23.
//

import Foundation
import SnapshotPreviewsCore

extension Preview {

var requiresFullScreen: Bool {
switch layout {
case .device:
return true
default:
return false
}
}

}
34 changes: 34 additions & 0 deletions Sources/PreviewGallery/PreviewCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// PreviewCell.swift
//
//
// Created by Noah Martin on 8/18/23.
//

import Foundation
import SwiftUI
import SnapshotPreviewsCore

struct PreviewCell: View {

let preview: SnapshotPreviewsCore.Preview

@Environment(\.colorScheme) var colorScheme

var body: some View {
VStack {
try! preview.view()
.border(Color(UIColor.separator))
.background {
Checkerboard()
.foregroundStyle(Color(UIColor.label))
.opacity(0.1)
.background(Color(UIColor.systemBackground))
}
.preferredColorScheme(nil)
.colorScheme(try! preview.colorScheme() ?? colorScheme)
}
.background(Color(UIColor.systemBackground))
}

}
25 changes: 25 additions & 0 deletions Sources/PreviewGallery/PreviewData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// PreviewData.swift
//
//
// Created by Noah Martin on 7/3/23.
//

import Foundation
import SnapshotPreviewsCore

public struct PreviewData {
let previews: [PreviewType]

func previews(in module: String) -> [PreviewType] {
previews.filter { $0.module == module }.sorted { $0.typeName < $1.typeName }
}

var modules: Set<String> {
Set(previews.map { $0.module })
}

public static var `default`: PreviewData {
self.init(previews: findPreviews())
}
}
Loading

0 comments on commit 8f6092b

Please sign in to comment.