Skip to content

Commit

Permalink
cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnEstropia committed Dec 5, 2024
1 parent ddd6f85 commit 235503b
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 252 deletions.
44 changes: 39 additions & 5 deletions Development/Demo/MyBook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,43 @@ let myBook = Book.init(
.foregroundStyle(Color.green)
}

struct StorybookTrait: PreviewModifier {
func body(content: Content, context: Void) -> some View {
content
}
}

@available(iOS 18.0, *)
extension PreviewTrait where T == Preview.ViewTraits {

@MainActor public static var storybook: PreviewTrait<Preview.ViewTraits> {
return .init(.modifier(StorybookTrait()))
}
}

struct Storybook: View {

let content: AnyView
init<Content: View>(
@ViewBuilder content: () -> Content
) {
self.content = .init(content())
}

var body: some View {
content
}
}

@available(iOS 18.0, *)
#Preview(traits: .storybook) {
Text("My Component")
}

#Preview {
Text("My Component")
}

#Preview {
Text("NO TITLE!")
.foregroundStyle(Color.purple)
Expand Down Expand Up @@ -272,10 +309,10 @@ let myBook = Book.init(
}
}

#Preview("Some title 2") {
#Preview("Title") {
#StorybookPreview<MyLabel> {
BookPreview { _ in
MyLabel(title: "MyLabel 2")
MyLabel(title: "Test")
}
}
}
Expand All @@ -284,9 +321,6 @@ let myBook = Book.init(
BookPreview { _ in
MyLabel(title: "Test")
}
BookPreview { _ in
MyLabel(title: "Test")
}
}

#StorybookPage<MyLabel> {
Expand Down
6 changes: 4 additions & 2 deletions Development/Demo/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ struct RootView: View {
}

if #available(iOS 17.0, *) {
Book(title: "#Preview macro") {
Book.allBookPreviews()
if let nodes = Book.allBookPreviews() {
Book(title: "#Preview macro") {
nodes
}
}
}
}
Expand Down
203 changes: 203 additions & 0 deletions Sources/StorybookKit/Internals/Preview/PreviewRegistryWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import DeveloperToolsSupport
import Foundation
import SwiftUI
import UIKit

@available(iOS 17.0, *)
struct PreviewRegistryWrapper: Comparable {

let previewType: any DeveloperToolsSupport.PreviewRegistry.Type
let module: String

init(_ previewType: any DeveloperToolsSupport.PreviewRegistry.Type) {
self.previewType = previewType
self.module = previewType.fileID.components(separatedBy: "/").first!
}

var fileID: String { previewType.fileID }
var line: Int { previewType.line }
var column: Int { previewType.column }

@MainActor
var makeView: (@MainActor () -> any View) {
let preview: FieldReader = .init(try! previewType.makePreview())
let title: String? = preview["displayName"]
let source: FieldReader = preview["source"]
switch source.typeName {

case "SwiftUI.ViewPreviewSource": // iOS 17
let makeView: MakeFunctionWrapper<any SwiftUI.View> = .init(source["makeView"])
return {
VStack {
if let title, !title.isEmpty {
Text(title)
.font(.system(size: 17, weight: .semibold))
}
AnyView(makeView())
Text("\(fileID):\(line)")
.font(.caption.monospacedDigit())
BookSpacer(height: 16)
}
}

case "UIKit.UIViewPreviewSource": // iOS 17
// Unsupported due to iOS 17 not supporting casting between non-sendable closure types
return {
VStack {
if let title, !title.isEmpty {
Text(title)
.font(.system(size: 17, weight: .semibold))
}
Text("\(fileID):\(line)")
.font(.caption.monospacedDigit())
Text("UIView Preview not supported (UIKit.UIViewPreviewSource)")
.foregroundStyle(Color.red)
.font(.caption.monospacedDigit())
BookSpacer(height: 16)
}
}

case "DeveloperToolsSupport.DefaultPreviewSource<SwiftUI.ViewPreviewBody>": // iOS 18
let makeBody: MakeFunctionWrapper<any SwiftUI.View> = .init(source["structure", "singlePreview", "makeBody"])
return {
VStack {
if let title, !title.isEmpty {
Text(title)
.font(.system(size: 17, weight: .semibold))
}
AnyView(makeBody())
Text("\(fileID):\(line)")
.font(.caption.monospacedDigit())
BookSpacer(height: 16)
}
}

case "DeveloperToolsSupport.DefaultPreviewSource<__C.UIView>": // iOS 18
let makeBody: MakeFunctionWrapper<UIView> = .init(source["structure", "singlePreview", "makeBody"])
return {
BookPreview(
fileID,
line,
title: title ?? source.typeName,
viewBlock: { _ in
makeBody()
}
)
}

case "DeveloperToolsSupport.DefaultPreviewSource<__C.UIViewController>": // iOS 18
let makeBody: MakeFunctionWrapper<UIViewController> = .init(source["structure", "singlePreview", "makeBody"])
return {
BookPresent(
title: title ?? source.typeName,
presentingViewControllerBlock: {
makeBody()
}
)
}

case let sourceTypeName:
return {
VStack {
if let title, !title.isEmpty {
Text(title)
.font(.system(size: 17, weight: .semibold))
}
Text("\(fileID):\(line)")
.font(.caption.monospacedDigit())
Text("Failed to load preview (\(sourceTypeName))")
.foregroundStyle(Color.red)
.font(.caption.monospacedDigit())
BookSpacer(height: 16)
}
}
}
}


// MARK: Comparable

static func < (lhs: PreviewRegistryWrapper, rhs: PreviewRegistryWrapper) -> Bool {
if lhs.module == rhs.module {
return lhs.line < rhs.line
}
return lhs.module < rhs.module
}


// MARK: Equatable

static func == (lhs: PreviewRegistryWrapper, rhs: PreviewRegistryWrapper) -> Bool {
lhs.line == rhs.line && lhs.module == rhs.module
}


// MARK: - FieldReader

private struct FieldReader {

let instance: Any
let typeName: String

init(_ instance: Any) {
self.instance = instance
self.typeName = String(reflecting: type(of: instance))
let mirror: Mirror = .init(reflecting: instance)
self.fields = .init(
uniqueKeysWithValues: mirror.children.compactMap { (label, value) in
label.map({ ($0, value) })
}
)
}

subscript<T>(_ key: String, _ nextKeys: String...) -> T {
if nextKeys.isEmpty {
return fields[key] as! T
}
else {
return Self.traverse(from: fields[key]!, nextKeys: nextKeys) as! T
}
}

subscript(_ key: String, _ nextKeys: String...) -> FieldReader {
.init(Self.traverse(from: fields[key]!, nextKeys: nextKeys))
}

private let fields: [String: Any]

private static func traverse<C: Collection<String>>(from first: Any, nextKeys: C) -> Any {
if let key = nextKeys.first {
let mirror: Mirror = .init(reflecting: first)
return self.traverse(
from: mirror.children.first(where: { $0.label == key })!.value,
nextKeys: nextKeys.dropFirst()
)
}
else {
return first
}
}
}


// MARK: - MakeFunctionWrapper

@MainActor
private struct MakeFunctionWrapper<T> {

typealias Closure = @MainActor () -> T
private let closure: Closure

init(_ closure: Any) {
// TODO: We need a workaround to avoid implicit @Sendable from @MainActor closures
self.closure = unsafeBitCast(
closure,
to: Closure.self
)
}

func callAsFunction() -> T {
closure()
}
}
}
Loading

0 comments on commit 235503b

Please sign in to comment.