Skip to content

Commit 8f6092b

Browse files
authored
Add preview gallery (#19)
1 parent c28fb24 commit 8f6092b

14 files changed

Lines changed: 407 additions & 0 deletions

DemoApp/DemoApp.xcodeproj/project.pbxproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
FA1671FB2A53E03800A42DB0 /* MessageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1671FA2A53E03800A42DB0 /* MessageRow.swift */; };
2929
FA1671FD2A53E11E00A42DB0 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1671FC2A53E11E00A42DB0 /* MessageView.swift */; };
3030
FA1671FF2A53E1AF00A42DB0 /* ConversationMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA1671FE2A53E1AF00A42DB0 /* ConversationMessageView.swift */; };
31+
FA170AD32A8B3BA400D1D53D /* PreviewGallery in Frameworks */ = {isa = PBXBuildFile; productRef = FA170AD22A8B3BA400D1D53D /* PreviewGallery */; };
3132
FA3C0DE72A620E9A00278952 /* MyPreviewTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3C0DE62A620E9A00278952 /* MyPreviewTest.swift */; };
3233
FA40515D2A95B587007A66D4 /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA40515C2A95B587007A66D4 /* EmptyView.swift */; };
3334
FA4F5AD92A7A3E7700B268FF /* Snapshotting in Frameworks */ = {isa = PBXBuildFile; productRef = FA4F5AD82A7A3E7700B268FF /* Snapshotting */; };
@@ -127,6 +128,7 @@
127128
isa = PBXFrameworksBuildPhase;
128129
buildActionMask = 2147483647;
129130
files = (
131+
FA170AD32A8B3BA400D1D53D /* PreviewGallery in Frameworks */,
130132
FACA23792A55FBEE0080545A /* DemoModule.framework in Frameworks */,
131133
);
132134
runOnlyForDeploymentPostprocessing = 0;
@@ -276,6 +278,7 @@
276278
);
277279
name = DemoApp;
278280
packageProductDependencies = (
281+
FA170AD22A8B3BA400D1D53D /* PreviewGallery */,
279282
);
280283
productName = DemoApp;
281284
productReference = FA1671BF2A5367A800A42DB0 /* DemoApp.app */;
@@ -827,6 +830,10 @@
827830
isa = XCSwiftPackageProductDependency;
828831
productName = SnapshottingTests;
829832
};
833+
FA170AD22A8B3BA400D1D53D /* PreviewGallery */ = {
834+
isa = XCSwiftPackageProductDependency;
835+
productName = PreviewGallery;
836+
};
830837
FA4F5AD82A7A3E7700B268FF /* Snapshotting */ = {
831838
isa = XCSwiftPackageProductDependency;
832839
productName = Snapshotting;

DemoApp/DemoApp/ContentView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77

88
import SwiftUI
99
import DemoModule
10+
import PreviewGallery
1011

1112
struct ContentView: View {
1213
var body: some View {
1314
NavigationView {
1415
VStack {
16+
NavigationLink("Open Gallery") {
17+
LazyView(PreviewGallery())
18+
}
1519
Image(systemName: "globe")
1620
.imageScale(.large)
1721
.foregroundStyle(.tint)

Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ let package = Package(
88
platforms: [.iOS(.v16)],
99
products: [
1010
// Products define the executables and libraries a package produces, making them visible to other packages.
11+
.library(
12+
name: "PreviewGallery",
13+
targets: ["PreviewGallery"]),
1114
// Test library to import in your XCTest target.
1215
// This is the only library that depends on XCTest.framework
1316
.library(
@@ -34,6 +37,7 @@ let package = Package(
3437
.target(name: "Snapshotting", dependencies: ["SnapshottingSwift"]),
3538
// Swift code in the inserted dylib
3639
.target(name: "SnapshottingSwift", dependencies: ["SnapshotPreviewsCore"]),
40+
.target(name: "PreviewGallery", dependencies: ["SnapshotPreviewsCore"]),
3741
.testTarget(
3842
name: "SnapshotPreviewsTests",
3943
dependencies: ["SnapshotPreviewsCore"]),

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ Emerge handles the heavy lifting of generating, diffing and hosting the snapshot
1313

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

16+
## Preview Gallery
17+
18+
`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)
19+
or even entire interactive features.
20+
21+
<p align="center">
22+
<img src="./images/image1.png" />
23+
</p>
24+
25+
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:
26+
27+
```swift
28+
import SwiftUI
29+
import PreviewGallery
30+
31+
NavigationLink("Open Gallery") { PreviewGallery() }
32+
```
33+
1634
## Local Debugging
1735

1836
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:
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// Checkerboard.swift
3+
//
4+
//
5+
// Created by Noah Martin on 7/4/23.
6+
//
7+
8+
import SwiftUI
9+
10+
struct Checkerboard: Shape {
11+
func path(in rect: CGRect) -> Path {
12+
var path = Path()
13+
14+
let rowSize: Double = 10
15+
let columnSize: Double = 10
16+
17+
let rows = Int(rect.height / CGFloat(rowSize))
18+
let columns = Int(rect.width / CGFloat(columnSize))
19+
let columnRemainder = rect.width - Double(columns) * columnSize
20+
let rowRemainder = rect.height - Double(rows) * rowSize
21+
22+
for row in 0 ..< rows {
23+
for column in 0 ..< columns {
24+
if (row + column).isMultiple(of: 2) {
25+
let startX = Double(columnSize) * Double(column)
26+
let startY = Double(rowSize) * Double(row)
27+
28+
let rect = CGRect(x: startX, y: startY, width: columnSize, height: rowSize)
29+
path.addRect(rect)
30+
}
31+
}
32+
if (row + columns).isMultiple(of: 2) {
33+
if columnRemainder > 0 {
34+
let startX = Double(columnSize) * Double(columns)
35+
let startY = Double(rowSize) * Double(row)
36+
37+
let rect = CGRect(x: startX, y: startY, width: columnRemainder, height: rowSize)
38+
path.addRect(rect)
39+
}
40+
}
41+
}
42+
if rowRemainder > 0 {
43+
for column in 0..<columns {
44+
if (rows + column).isMultiple(of: 2) {
45+
let startX = Double(columnSize) * Double(column)
46+
let startY = Double(rowSize) * Double(rows)
47+
48+
let rect = CGRect(x: startX, y: startY, width: columnSize, height: rowRemainder)
49+
path.addRect(rect)
50+
}
51+
}
52+
if (rows + columns).isMultiple(of: 2) {
53+
let startX = Double(columnSize) * Double(columns)
54+
let startY = Double(rowSize) * Double(rows)
55+
56+
let rect = CGRect(x: startX, y: startY, width: columnRemainder, height: rowRemainder)
57+
path.addRect(rect)
58+
}
59+
}
60+
61+
return path
62+
}
63+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// ModuleFeatures.swift
3+
//
4+
//
5+
// Created by Noah Martin on 8/31/23.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
11+
struct ModuleFeatures: View {
12+
13+
let module: String
14+
let data: PreviewData
15+
16+
var body: some View {
17+
let featureProviders = data.previews(in: module).filter { provider in
18+
return provider.previews.contains { preview in
19+
return preview.requiresFullScreen
20+
}
21+
}
22+
return List {
23+
ForEach(featureProviders) { provider in
24+
let featurePreviews = provider.previews.filter { $0.requiresFullScreen }
25+
NavigationLink {
26+
if featurePreviews.count == 1 {
27+
try! featurePreviews[0].view()
28+
} else {
29+
List {
30+
ForEach(featurePreviews) { preview in
31+
NavigationLink(preview.displayName ?? provider.displayName) {
32+
try! preview.view()
33+
}
34+
}
35+
}.navigationTitle(provider.displayName)
36+
}
37+
} label: {
38+
VStack(alignment: .leading) {
39+
Text(provider.displayName)
40+
.font(.headline)
41+
.foregroundStyle(Color(UIColor.label))
42+
.padding(.leading, 8)
43+
44+
Text("\(featurePreviews.count) Preview\(featurePreviews.count != 1 ? "s" : "")")
45+
.font(.subheadline)
46+
.foregroundStyle(Color(UIColor.secondaryLabel))
47+
.padding(.leading, 8)
48+
}
49+
}
50+
}
51+
}.navigationTitle("Features")
52+
}
53+
54+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// ModulePreviews.swift
3+
//
4+
//
5+
// Created by Noah Martin on 7/3/23.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
11+
struct ModulePreviews: View {
12+
let module: String
13+
let data: PreviewData
14+
15+
var body: some View {
16+
let componentProviders = data.previews(in: module).filter { provider in
17+
return provider.previews.contains { preview in
18+
return !preview.requiresFullScreen
19+
}
20+
}
21+
let featureProviders = data.previews(in: module).filter { provider in
22+
return provider.previews.contains { preview in
23+
return preview.requiresFullScreen
24+
}
25+
}
26+
return NavigationLink(module) {
27+
ScrollView {
28+
LazyVStack(alignment: .leading) {
29+
if !featureProviders.isEmpty {
30+
NavigationLink {
31+
ModuleFeatures(module: module, data: data)
32+
} label: {
33+
VStack {
34+
TitleSubtitleRow(
35+
title: "Features",
36+
subtitle: "\(featureProviders.count) Preview\(featureProviders.count != 1 ? "s" : "")")
37+
Divider()
38+
}
39+
}
40+
}
41+
ForEach(componentProviders) { preview in
42+
PreviewCellView(preview: preview)
43+
}
44+
}
45+
}
46+
.navigationTitle(module)
47+
}
48+
}
49+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// Preview+FullScreen.swift
3+
//
4+
//
5+
// Created by Noah Martin on 8/31/23.
6+
//
7+
8+
import Foundation
9+
import SnapshotPreviewsCore
10+
11+
extension Preview {
12+
13+
var requiresFullScreen: Bool {
14+
switch layout {
15+
case .device:
16+
return true
17+
default:
18+
return false
19+
}
20+
}
21+
22+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// PreviewCell.swift
3+
//
4+
//
5+
// Created by Noah Martin on 8/18/23.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
import SnapshotPreviewsCore
11+
12+
struct PreviewCell: View {
13+
14+
let preview: SnapshotPreviewsCore.Preview
15+
16+
@Environment(\.colorScheme) var colorScheme
17+
18+
var body: some View {
19+
VStack {
20+
try! preview.view()
21+
.border(Color(UIColor.separator))
22+
.background {
23+
Checkerboard()
24+
.foregroundStyle(Color(UIColor.label))
25+
.opacity(0.1)
26+
.background(Color(UIColor.systemBackground))
27+
}
28+
.preferredColorScheme(nil)
29+
.colorScheme(try! preview.colorScheme() ?? colorScheme)
30+
}
31+
.background(Color(UIColor.systemBackground))
32+
}
33+
34+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// PreviewData.swift
3+
//
4+
//
5+
// Created by Noah Martin on 7/3/23.
6+
//
7+
8+
import Foundation
9+
import SnapshotPreviewsCore
10+
11+
public struct PreviewData {
12+
let previews: [PreviewType]
13+
14+
func previews(in module: String) -> [PreviewType] {
15+
previews.filter { $0.module == module }.sorted { $0.typeName < $1.typeName }
16+
}
17+
18+
var modules: Set<String> {
19+
Set(previews.map { $0.module })
20+
}
21+
22+
public static var `default`: PreviewData {
23+
self.init(previews: findPreviews())
24+
}
25+
}

0 commit comments

Comments
 (0)