Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support @Previewable macro #108

Merged
merged 1 commit into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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