diff --git a/Development/Demo/MyBook.swift b/Development/Demo/MyBook.swift index a461f14..b6be398 100644 --- a/Development/Demo/MyBook.swift +++ b/Development/Demo/MyBook.swift @@ -135,6 +135,114 @@ let myBook = Book.init( } ) +#Preview("Simple Sample") { + Text("Yo there!") + .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 { + return .init(.modifier(StorybookTrait())) + } +} + +struct Storybook: View { + + let content: AnyView + init( + @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) +} + +@available(iOS 17.0, *) +#Preview("States Sample") { + @Previewable @State var state: Int = 1 + + VStack { + Text("Aloha \(state)") + Button( + action: { state += 1 }, + label: { Text("+") } + ) + } +} + +@available(iOS 17.0, *) +#Preview("UIView Sample") { + { + var state: Int = 1 + + let label = UILabel() + label.text = "Test \(state)" + + let button = UIButton( + configuration: .bordered(), + primaryAction: .init { _ in + state += 1 + label.text = "Test \(state)" + } + ) + button.setTitle("+", for: .normal) + let stack = UIStackView(arrangedSubviews: [label, button]) + stack.spacing = 8 + return stack + }() +} + +@available(iOS 17.0, *) +#Preview("UIViewController Sample") { + { + let controller = UIViewController() + controller.view.backgroundColor = .systemMint + var state: Int = 1 + + let label = UILabel() + label.text = "Test \(state)" + + let button = UIButton( + configuration: .bordered(), + primaryAction: .init { _ in + state += 1 + label.text = "Test \(state)" + } + ) + button.setTitle("+", for: .normal) + let stack = UIStackView(arrangedSubviews: [label, button]) + stack.spacing = 8 + stack.frame = controller.view.bounds + controller.view.addSubview(stack) + return controller + }() +} + #StorybookPage(title: "UILabel updating text") { BookPreview { context in let label = UILabel() @@ -201,10 +309,10 @@ let myBook = Book.init( } } -#Preview("Some title 2") { +#Preview("Title") { #StorybookPreview { BookPreview { _ in - MyLabel(title: "MyLabel 2") + MyLabel(title: "Test") } } } @@ -213,9 +321,6 @@ let myBook = Book.init( BookPreview { _ in MyLabel(title: "Test") } - BookPreview { _ in - MyLabel(title: "Test") - } } #StorybookPage { diff --git a/Development/Demo/RootView.swift b/Development/Demo/RootView.swift index 7ef8f23..4e6b238 100644 --- a/Development/Demo/RootView.swift +++ b/Development/Demo/RootView.swift @@ -37,6 +37,14 @@ struct RootView: View { Book.allStorybookPages() .map({ $0.bookBody }) } + + if #available(iOS 17.0, *) { + if let nodes = Book.allBookPreviews() { + Book(title: "#Preview macro") { + nodes + } + } + } } ) ) diff --git a/Development/Storybook.xcodeproj/project.pbxproj b/Development/Storybook.xcodeproj/project.pbxproj index 1792ffe..b709384 100644 --- a/Development/Storybook.xcodeproj/project.pbxproj +++ b/Development/Storybook.xcodeproj/project.pbxproj @@ -324,7 +324,7 @@ CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Demo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -344,7 +344,7 @@ CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Demo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Development/Storybook.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Development/Storybook.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d0403d3..9514101 100644 --- a/Development/Storybook.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Development/Storybook.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "f5a894bbdc3287a91c8c33f864cfb447314305f7fc66cabf1c369e8c3a67521d", "pins" : [ { "identity" : "descriptors", @@ -32,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-macro-testing.git", "state" : { - "branch" : "main", - "revision" : "15916c0c328339f54c15d616465d79700e3f7de8" + "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4", + "version" : "0.5.2" } }, { @@ -41,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "e7b77228b34057041374ebef00c0fd7739d71a2b", - "version" : "1.15.3" + "revision" : "7b0bbbae90c41f848f90ac7b4df6c4f50068256d", + "version" : "1.17.5" } }, { @@ -50,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { @@ -86,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/FluidGroup/TextureBridging.git", "state" : { - "branch" : "main", - "revision" : "4383f8a9846a0507d2c22ee9fac22153f9b86fed" + "revision" : "4383f8a9846a0507d2c22ee9fac22153f9b86fed", + "version" : "3.2.1" } }, { @@ -95,10 +96,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/FluidGroup/TextureSwiftSupport.git", "state" : { - "branch" : "main", - "revision" : "5bae50cab3798dccb8b98c3ffbc70320ae66b45a" + "revision" : "fb748d6a9d0a2dca0635227e1db0360fd26e0e24", + "version" : "3.20.1" } } ], - "version" : 2 + "version" : 3 } diff --git a/Sources/StorybookKit/Internals/Preview/PreviewRegistryWrapper.swift b/Sources/StorybookKit/Internals/Preview/PreviewRegistryWrapper.swift new file mode 100644 index 0000000..c94af2a --- /dev/null +++ b/Sources/StorybookKit/Internals/Preview/PreviewRegistryWrapper.swift @@ -0,0 +1,223 @@ +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) { + guard let rawPreview = try? previewType.makePreview() else { + return { EmptyView() } + } + let preview: FieldReader = .init(rawPreview) + let title: String? = preview["displayName"] + let source: FieldReader = preview["source"] + switch source.typeName { + + case "SwiftUI.ViewPreviewSource": // iOS 17 + let makeView: MakeFunctionWrapper = .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("UIView Preview not supported on iOS 17") + .foregroundStyle(Color.red) + .font(.caption.monospacedDigit()) + Text("\(fileID):\(line)") + .font(.caption.monospacedDigit()) + BookSpacer(height: 16) + } + } + + case "UIKit.UIViewControllerPreviewSource": // 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("UIViewController Preview not supported on iOS 17") + .foregroundStyle(Color.red) + .font(.caption.monospacedDigit()) + Text("\(fileID):\(line)") + .font(.caption.monospacedDigit()) + BookSpacer(height: 16) + } + } + + case "DeveloperToolsSupport.DefaultPreviewSource": // iOS 18 + let makeBody: MakeFunctionWrapper = .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 = .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 = .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("Failed to load preview (\(sourceTypeName))") + .foregroundStyle(Color.red) + .font(.caption.monospacedDigit()) + Text("\(fileID):\(line)") + .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(_ 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>(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 { + + 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() + } + } +} diff --git a/Sources/StorybookKit/Internals/machOLoader.swift b/Sources/StorybookKit/Internals/machOLoader.swift index 2b5fdf6..0f9d200 100644 --- a/Sources/StorybookKit/Internals/machOLoader.swift +++ b/Sources/StorybookKit/Internals/machOLoader.swift @@ -47,6 +47,25 @@ extension Book { return results } + @available(iOS 17.0, *) + static func findAllPreviews( + excludeStorybookPageMacro: Bool = true + ) -> [PreviewRegistryWrapper]? { + let moduleName = Bundle.main.bundleURL.deletingPathExtension().lastPathComponent + guard !moduleName.isEmpty else { + return nil + } + var results: [PreviewRegistryWrapper] = [] + for imageIndex in 0 ..< _dyld_image_count() { + self.findAllPreviews( + inImageIndex: .init(imageIndex), + excludeStorybookPageMacro: excludeStorybookPageMacro, + results: &results + ) + } + return results + } + private static func findAllBookProviders( inImageIndex imageIndex: UInt32, filterByStorybookPageMacro: Bool, @@ -93,12 +112,7 @@ extension Book { } guard contextDescriptor.pointee.kind().canConformToProtocol, - !filterByStorybookPageMacro || self._magicSubstring.withCString( - { - let nameCString = contextDescriptor.resolvePointer(for: \.name) - return nil != strstr(nameCString, $0) - } - ) + !filterByStorybookPageMacro || contextDescriptor.nameContains(self._magicSubstring) else { continue } @@ -119,11 +133,83 @@ extension Book { results.append(bookProviderType) } } + + @available(iOS 17.0, *) + private static func findAllPreviews( + inImageIndex imageIndex: UInt32, + excludeStorybookPageMacro: Bool, + results: inout [PreviewRegistryWrapper] + ) { + // Follows same approach here: https://github.com/apple/swift-testing/blob/main/Sources/TestingInternals/Discovery.cpp#L318 + guard + let headerRawPtr: UnsafeRawPointer = _dyld_get_image_header(imageIndex) + .map(UnsafeRawPointer.init(_:)) + else { + return + } + let headerPtr = headerRawPtr.assumingMemoryBound( + to: mach_header_64.self + ) + // https://derekselander.github.io/dsdump/ + var size: UInt = 0 + guard + let sectionRawPtr = getsectiondata( + headerPtr, + SEG_TEXT, + "__swift5_types", + &size + ) + .map(UnsafeRawPointer.init(_:)) + else { + return + } + let capacity: Int = .init(size) / MemoryLayout.size + let sectionPtr = sectionRawPtr.assumingMemoryBound( + to: SwiftTypeMetadataRecord.self + ) + for index in 0 ..< capacity { + let record = sectionPtr.advanced(by: index) + guard + let contextDescriptor = record.pointee.contextDescriptor( + from: record + ) + else { + continue + } + guard !contextDescriptor.pointee.isGeneric() else { + continue + } + guard + case .structType = contextDescriptor.pointee.kind(), + contextDescriptor.nameContains("PreviewRegistry"), + !excludeStorybookPageMacro || !contextDescriptor.nameContains(self._magicSubstring) + else { + continue + } + let metadataClosure = contextDescriptor.resolveValue(for: \.metadataAccessFunction) + let metadata = metadataClosure(0xFF) + guard + let metadataAccessFunction = metadata.value + else { + continue + } + let anyType = unsafeBitCast( + metadataAccessFunction, + to: Any.Type.self + ) + guard + let previewType = anyType as? DeveloperToolsSupport.PreviewRegistry.Type + else { + continue + } + results.append(.init(previewType)) + } + } } extension UnsafePointer where Pointee: SwiftLayoutPointer { - func resolvePointer(for keyPath: KeyPath>) -> UnsafePointer { + fileprivate func resolvePointer(for keyPath: KeyPath>) -> UnsafePointer { let base: UnsafeRawPointer = .init(self) let fieldOffset = MemoryLayout.offset(of: keyPath)! let relativePointer = self.pointee[keyPath: keyPath] @@ -133,7 +219,7 @@ extension UnsafePointer where Pointee: SwiftLayoutPointer { .assumingMemoryBound(to: U.self) } - func resolveValue(for keyPath: KeyPath>) -> U { + fileprivate func resolveValue(for keyPath: KeyPath>) -> U { let base: UnsafeRawPointer = .init(self) let fieldOffset = MemoryLayout.offset(of: keyPath)! let relativePointer = self.pointee[keyPath: keyPath] @@ -144,17 +230,28 @@ extension UnsafePointer where Pointee: SwiftLayoutPointer { } } -protocol SwiftLayoutPointer { +extension UnsafePointer where Pointee == SwiftTypeContextDescriptor { + fileprivate func nameContains(_ string: String) -> Bool { + string.withCString( + { + let nameCString = self.resolvePointer(for: \.name) + return nil != strstr(nameCString, $0) + } + ) + } +} + +fileprivate protocol SwiftLayoutPointer { static var maskValue: Int32 { get } } extension SwiftLayoutPointer { - static var maskValue: Int32 { + fileprivate static var maskValue: Int32 { return 0 } } -struct SwiftRelativePointer { +fileprivate struct SwiftRelativePointer { var offset: Int32 = 0 @@ -192,7 +289,7 @@ extension SwiftRelativePointer where T: SwiftLayoutPointer { } } -struct SwiftTypeMetadataRecord: SwiftLayoutPointer { +fileprivate struct SwiftTypeMetadataRecord: SwiftLayoutPointer { var pointer: SwiftRelativePointer @@ -218,7 +315,7 @@ struct SwiftTypeMetadataRecord: SwiftLayoutPointer { } } -struct SwiftTypeContextDescriptor: SwiftLayoutPointer { +fileprivate struct SwiftTypeContextDescriptor: SwiftLayoutPointer { var flags: UInt32 = 0 var parent: SwiftRelativePointer = .init() var name: SwiftRelativePointer = .init() @@ -262,7 +359,7 @@ struct SwiftTypeContextDescriptor: SwiftLayoutPointer { } var canConformToProtocol: Bool { - return (Self.classType.rawValue ... Self.typesEnd.rawValue).contains(self.rawValue) + return (Self.typesStart.rawValue ... Self.typesEnd.rawValue).contains(self.rawValue) } } } diff --git a/Sources/StorybookKit/Primitives/Book.swift b/Sources/StorybookKit/Primitives/Book.swift index 74f81e9..642fc48 100644 --- a/Sources/StorybookKit/Primitives/Book.swift +++ b/Sources/StorybookKit/Primitives/Book.swift @@ -27,6 +27,49 @@ public struct Book: BookView, Identifiable { public let title: String public let contents: [Node] + /// All `#Preview`s as `BookPage`s + @available(iOS 17.0, *) + public static func allBookPreviews() -> [Node]? { + guard let sortedPreviewRegistries = self.findAllPreviews() else { + return nil + } + var fileIDsByModule: [String: Set] = [:] + var registriesByFileID: [String: [PreviewRegistryWrapper]] = [:] + for item in sortedPreviewRegistries { + fileIDsByModule[item.module, default: []].insert(item.fileID) + registriesByFileID[item.fileID, default: []].append(item) + } + return fileIDsByModule.keys.sorted().map { module in + return Node.folder( + .init( + title: module, + contents: { [fileIDs = fileIDsByModule[module]!.sorted()] in + fileIDs.map { fileID in + return Node.page( + .init( + fileID, + 0, + title: .init(fileID[fileID.index(after: module.endIndex)...]), + destination: { [registries = registriesByFileID[fileID]!] in + LazyVStack( + alignment: .center, + spacing: 16, + pinnedViews: .sectionHeaders + ) { + ForEach.inefficient(items: registries) { registry in + AnyView(registry.makeView()) + } + } + } + ) + ) + } + } + ) + ) + } + } + /// All conformers to `BookProvider`, including those declared from the `#StorybookPage` macro public static func allBookProviders() -> [any BookProvider.Type] { self.findAllBookProviders(filterByStorybookPageMacro: false) ?? [] @@ -85,6 +128,7 @@ public struct Book: BookView, Identifiable { folder } .navigationTitle(folder.title) + .navigationBarTitleDisplayMode(.inline) } label: { HStack { Image.init(systemName: "folder") diff --git a/Sources/StorybookKit/Primitives/BookPage.swift b/Sources/StorybookKit/Primitives/BookPage.swift index 2ed9210..956f702 100644 --- a/Sources/StorybookKit/Primitives/BookPage.swift +++ b/Sources/StorybookKit/Primitives/BookPage.swift @@ -59,16 +59,16 @@ public struct BookPage: BookView, Identifiable { public let title: String public let destination: AnyView public nonisolated let declarationIdentifier: DeclarationIdentifier - private let file: StaticString - private let line: UInt + private let fileID: any StringProtocol + private let line: any FixedWidthInteger public init( - _ file: StaticString = #fileID, - _ line: UInt = #line, + _ fileID: any StringProtocol = #fileID, + _ line: any FixedWidthInteger = #line, title: String, @ViewBuilder destination: @MainActor () -> Destination ) { - self.file = file + self.fileID = fileID self.line = line self.title = title self.destination = AnyView(destination()) @@ -82,15 +82,17 @@ public struct BookPage: BookView, Identifiable { destination } .listStyle(.plain) + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) .onAppear(perform: { - context?.onOpen(page: self) + context?.onOpen(pageID: id) }) } label: { HStack { Image.init(systemName: "doc") VStack(alignment: .leading) { Text(title) - Text("\(file.description):\(line.description)") + Text("\(fileID):\(line)") .font(.caption.monospacedDigit()) .opacity(0.8) } diff --git a/Sources/StorybookKit/Primitives/BookPreview.swift b/Sources/StorybookKit/Primitives/BookPreview.swift index 6289f68..820052a 100644 --- a/Sources/StorybookKit/Primitives/BookPreview.swift +++ b/Sources/StorybookKit/Primitives/BookPreview.swift @@ -46,20 +46,20 @@ public struct BookPreview: BookView { public let declarationIdentifier: DeclarationIdentifier - private let file: StaticString - private let line: UInt + private let fileID: any StringProtocol + private let line: any FixedWidthInteger private let title: String? private var frameConstraint: FrameConstraint = .init() public init( - _ file: StaticString = #fileID, - _ line: UInt = #line, + _ fileID: any StringProtocol = #fileID, + _ line: any FixedWidthInteger = #line, title: String? = nil, viewBlock: @escaping @MainActor (inout Context) -> UIView ) { self.title = title - self.file = file + self.fileID = fileID self.line = line self.viewBlock = viewBlock @@ -95,7 +95,7 @@ public struct BookPreview: BookView { controlView - Text("\(file.description):\(line.description)") + Text("\(fileID):\(line)") .font(.caption.monospacedDigit()) BookSpacer(height: 16) diff --git a/Sources/StorybookKit/Primitives/BookStore.swift b/Sources/StorybookKit/Primitives/BookStore.swift index 2d6c962..ffa02bf 100644 --- a/Sources/StorybookKit/Primitives/BookStore.swift +++ b/Sources/StorybookKit/Primitives/BookStore.swift @@ -44,13 +44,13 @@ public final class BookStore: ObservableObject { } - func onOpen(page: BookPage) { + func onOpen(pageID: DeclarationIdentifier) { - guard allPages.keys.contains(page.id) else { + guard allPages.keys.contains(pageID) else { return } - let index = page.declarationIdentifier.index + let index = pageID.index var current = userDefaults.array(forKey: "history") as? [Int] ?? [] if let index = current.firstIndex(of: index) { diff --git a/Sources/StorybookKitTextureSupport/BookNodePreview.swift b/Sources/StorybookKitTextureSupport/BookNodePreview.swift index ed4c2bd..ef3e294 100644 --- a/Sources/StorybookKitTextureSupport/BookNodePreview.swift +++ b/Sources/StorybookKitTextureSupport/BookNodePreview.swift @@ -30,13 +30,13 @@ public struct BookNodePreview: BookView { private var backing: BookPreview public init( - _ file: StaticString = #fileID, - _ line: UInt = #line, + _ fileID: any StringProtocol = #fileID, + _ line: any FixedWidthInteger = #line, title: String? = nil, nodeBlock: @escaping @MainActor (inout BookPreview.Context) -> ASDisplayNode ) { - self.backing = .init(file, line, title: title) { context in + self.backing = .init(fileID, line, title: title) { context in let body = nodeBlock(&context) let node = AnyDisplayNode { _, size in