Skip to content

Commit

Permalink
Merge pull request #108 from BarredEwe/supportPreviewableMacro
Browse files Browse the repository at this point in the history
Support `@Previewable` macro
  • Loading branch information
BarredEwe authored Feb 24, 2025
2 parents ee3b27f + a2bbfe7 commit 100ed9a
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 44 deletions.
Binary file not shown.
4 changes: 2 additions & 2 deletions Example/PreFireExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
};
};
};
buildConfigurationList = 9735EE26289C5CF200309267 /* Build configuration list for PBXProject "PreFireExample" */;
buildConfigurationList = 9735EE26289C5CF200309267 /* Build configuration list for PBXProject "PrefireExample" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
Expand Down Expand Up @@ -534,7 +534,7 @@
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
9735EE26289C5CF200309267 /* Build configuration list for PBXProject "PreFireExample" */ = {
9735EE26289C5CF200309267 /* Build configuration list for PBXProject "PrefireExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9735EE3E289C5CF300309267 /* Debug */,
Expand Down
3 changes: 3 additions & 0 deletions Example/Shared/Examples/PrefireView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ struct PrefireView_Preview: PreviewProvider, PrefireProvider {
}

#Preview("PrefireViewMacroAnother") {
@Previewable @State var title: String = "Prefire"

PrefireView()
.navigationTitle(title)
.previewUserStory(.auth)
.snapshot(perceptualPrecision: 0.98)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,12 @@ extension String {
fatalError(error.localizedDescription)
}
}

/// Indents the lines in the receiver
///
/// - Parameters:
/// - amount: A int specifies the amount of indentString for one level idnentation
func ident(_ count: Int) -> String {
components(separatedBy: "\n").map({ String(repeating: " ", count: count) + $0 }).joined(separator: "\n")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation

extension PreviewLoader {
private static let yamlSettings = "|-4\n\n"
private static let previewSpaces = " "

/// Loading and creating lines of code with the `PreviewModel` array
/// - Parameters:
Expand All @@ -13,15 +12,17 @@ extension PreviewLoader {
guard let findedBodies = await loadRawPreviewBodies(for: sources, defaultEnabled: defaultEnabled) else { return nil }

let previewModels = findedBodies
.compactMap { RawPreviewModel(from: $0.value, filename: $0.key, lineSymbol: previewSpaces)?.previewModel }
.joined(separator: "\n")
.sorted { $0.key > $1.key }
.compactMap { RawPreviewModel(from: $0.value, filename: $0.key) }

return yamlSettings +
"""
@MainActor
private struct MacroPreviews {
\(previewModels.filter({ $0.properties != nil }).map({ $0.previewWrapper }).joined(separator: "\r\n"))
static var previews: [PreviewModel] = [
\(previewModels)
\(previewModels.map(\.previewModel).joined(separator: "\r\n"))
]
}
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,40 @@ import Foundation
extension PreviewLoader {
private static let funcCharacterSet = CharacterSet(arrayLiteral: "_").inverted.intersection(.alphanumerics.inverted)
private static let yamlSettings = "|-4\n\n"
private static let previewSpaces = " "

static func loadPreviewBodies(for sources: [String], defaultEnabled: Bool) async -> String? {
guard let findedBodies = await loadRawPreviewBodies(for: sources, defaultEnabled: defaultEnabled) else { return nil }

let result = findedBodies
.sorted(by: { $0.key > $1.key })
.compactMap { makeFunc(fileName: $0.key, body: $0.value)?.appending("\r\n") }
.joined()

return yamlSettings + result
guard let rawBodies = await loadRawPreviewBodies(for: sources, defaultEnabled: defaultEnabled) else { return nil }
let functions = rawBodies
.sorted { $0.key > $1.key }
.compactMap { makeTestFunc(fileName: $0.key, body: $0.value) }
.joined(separator: "\r\n")
return yamlSettings + functions
}

private static func makeFunc(fileName: String, body: String) -> String? {
guard let rawPreviewModel = RawPreviewModel(from: body, filename: fileName, lineSymbol: previewSpaces) else { return nil }
let isScreen = rawPreviewModel.traits == ".device"
let componentTestName = rawPreviewModel.displayName.components(separatedBy: funcCharacterSet).joined()
let snapshotSettings = rawPreviewModel.snapshotSettings?.replacingOccurrences(of: "snapshot", with: "init")

return
"""
private static func makeTestFunc(fileName: String, body: String) -> String? {
guard let model = RawPreviewModel(from: body, filename: fileName) else { return nil }

let componentTestName = model.displayName.components(separatedBy: funcCharacterSet).joined()
let settingsSuffix = (model.snapshotSettings?.replacingOccurrences(of: "snapshot", with: "init")).flatMap { ", settings: \($0)" } ?? ""

let previewCode: String
let content: String
if model.properties == nil {
previewCode = " let preview = {\n\(model.body.ident(12))\n }"
content = "preview()"
} else {
previewCode = model.previewWrapper.ident(4)
content = "PreviewWrapper\(model.displayName)()"
}

let prefireSnapshot = "PrefireSnapshot(\(content), name: \"\(model.displayName)\", isScreen: \(model.isScreen), device: deviceConfig\(settingsSuffix))"

return """
func test_\(componentTestName)_Preview() {
let preview = {
\(rawPreviewModel.body)
}
if let failure = assertSnapshots(for: PrefireSnapshot(preview(), name: "\(rawPreviewModel.displayName)", isScreen: \(isScreen), device: deviceConfig\(snapshotSettings.flatMap({ ", settings: " + $0 }) ?? ""))) {
\(previewCode)
if let failure = assertSnapshots(for: \(prefireSnapshot)) {
XCTFail(failure)
}
}
Expand Down
48 changes: 35 additions & 13 deletions PrefireExecutable/Sources/prefire/Previews/RawPreviewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ struct RawPreviewModel {
var displayName: String
var traits: String
var body: String
var properties: String?
var snapshotSettings: String?

var isScreen: Bool {
traits == Constants.defaultTrait
}
}

extension RawPreviewModel {
private enum Markers {
static let previewMacro = "#Preview"
static let traits = "traits: "
static let snasphotSettings = ".snapshot("
static let previewable = "@Previewable"
}

private enum Constants {
Expand All @@ -22,10 +28,9 @@ extension RawPreviewModel {
/// - Parameters:
/// - macroBody: Preview View body
/// - filename: File name in which the macro was found
/// - lineSymbol: The line separator symbol
init?(from macroBody: String, filename: String, lineSymbol: String = "") {
let lines = macroBody.split(separator: "\n", omittingEmptySubsequences: false)
guard let firstLine = lines.first else { return nil }
init?(from macroBody: String, filename: String) {
var lines = macroBody.split(separator: "\n", omittingEmptySubsequences: false).dropLast(2)
let firstLine = lines.removeFirst()

// Define displayName by splitting the first line by "
let parts = firstLine.split(separator: "\"")
Expand All @@ -45,25 +50,42 @@ extension RawPreviewModel {
}
self.traits = previewTrait ?? Constants.defaultTrait

// Search for the line with snapshot settings
if let configLine = lines.first(where: { $0.contains(Markers.snasphotSettings) }) {
self.snapshotSettings = configLine.trimmingCharacters(in: .whitespaces)
} else {
self.snapshotSettings = nil

for (index, line) in lines.enumerated() {
// Search for the line with snapshot settings
if snapshotSettings == nil, line.contains(Markers.snasphotSettings) {
self.snapshotSettings = line.trimmingCharacters(in: .whitespaces)
} else if line.contains(Markers.previewable) {
lines.remove(at: index + 1)
if self.properties == nil {
self.properties = String(line.replacing("\(Markers.previewable) ", with: ""))
} else {
self.properties! += "\n" + String(line.replacing(Markers.previewable, with: ""))
}
}
}

// Forming the Preview body: remove the first and last two lines
let bodyLines = lines.dropFirst().dropLast(2)
self.body = bodyLines.map { lineSymbol + $0 }.joined(separator: "\n")
self.body = lines.joined(separator: "\n")
}
}

extension RawPreviewModel {
var previewWrapper: String {
"""
struct PreviewWrapper\(displayName): SwiftUI.View {
\(properties ?? "")
var body: some SwiftUI.View {
\(body.ident(12))
}
}
"""
}

var previewModel: String {
"""
PreviewModel(
content: {
\(body)
\(properties == nil ? body.ident(16) : " PreviewWrapper\(displayName)()")
},
name: \"\(displayName)\",
type: \(traits == ".device" ? ".screen" : ".component")
Expand Down
13 changes: 10 additions & 3 deletions PrefireExecutable/Tests/PrefireTests/RawPreviewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,30 @@ class RawPreviewModelTests: XCTestCase {
let rawPreviewModel = RawPreviewModel(from: previewBodyWithName, filename: "Test")

XCTAssertEqual(rawPreviewModel?.body, " Text(\"TestView\")")
XCTAssertEqual(rawPreviewModel?.properties, nil)
XCTAssertEqual(rawPreviewModel?.displayName, "TestViewName")
XCTAssertEqual(rawPreviewModel?.traits, ".sizeThatFitsLayout")
XCTAssertEqual(rawPreviewModel?.snapshotSettings, nil)
}

func test_initWithoutName() {
let previewBodyWithoutName = """
#Preview(traits: .sizeThatFitsLayout) {
@Previewable @State var name: String = "TestView"
VStack {
Text("TestView")
Text(name)
}
.snapshot(delay: 8)
}
"""
let rawPreviewModel = RawPreviewModel(from: previewBodyWithoutName, filename: "TestView")

XCTAssertEqual(rawPreviewModel?.body, " VStack {\n Text(\"TestView\")\n }")

XCTAssertEqual(rawPreviewModel?.body, "\n VStack {\n Text(name)\n }\n .snapshot(delay: 8)")
XCTAssertEqual(rawPreviewModel?.properties, " @State var name: String = \"TestView\"")
XCTAssertEqual(rawPreviewModel?.displayName, "TestView")
XCTAssertEqual(rawPreviewModel?.traits, ".sizeThatFitsLayout")
XCTAssertEqual(rawPreviewModel?.snapshotSettings, ".snapshot(delay: 8)")
}
}
2 changes: 1 addition & 1 deletion Sources/Prefire/Preview/PreviewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public struct PreviewModel: Identifiable {
@MainActor
public init(
id: String? = nil,
content: @escaping () -> any View,
@ViewBuilder content: @escaping @MainActor () -> any View,
name: String,
type: LayoutType = .component,
device: PreviewDevice? = nil,
Expand Down

0 comments on commit 100ed9a

Please sign in to comment.