Skip to content

Commit

Permalink
Support modifying parameters in multiple previews (#40)
Browse files Browse the repository at this point in the history
Resolves #14

This allows for consumers to override parameters in previews by using `Exhibit.preview(parameters: [String: Any]) -> some View`

This allows for multiple previews to be had for easing development.

In order to support the passing of parameters in this way, some refactoring was required:

`Exhibit` is now a strict model, not a view itself, allowing it to be generic and more easily understood.

`AnyExhibit` is added which is a type-erased version for use in arrays, and then `AnyExhibitView` displays the actual exhibit with support for context observing.

For some reason, a compiler error unfortunately occurs in the preview definition if `layout` is left as a closure on `Exhibit`. The only solution I could find which left types intact is to move the layout override to a separate function in the `Exhibit` protocol.
  • Loading branch information
Malcolm Jarvis authored Mar 4, 2022
1 parent 128890e commit e193122
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 83 deletions.
8 changes: 8 additions & 0 deletions Example/CustomButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,13 @@ struct CustomButton_Previews: ExhibitProvider, PreviewProvider {
action: context.parameter(name: "action")
)
}

static var previews: some View {
exhibit.preview()
.previewLayout(.sizeThatFits)

exhibit.preview(parameters: ["title": "Other"])
.previewLayout(.sizeThatFits)
}
}

7 changes: 4 additions & 3 deletions Example/CustomToggle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ struct CustomToggle_Previews: ExhibitProvider, PreviewProvider {
title: context.parameter(name: "title", defaultValue: "Title"),
isOn: context.parameter(name: "isOn")
)
} layout: { exhibit in
exhibit
.padding()
}

static func exhibitLayout(_ content: CustomToggle) -> some View {
content.padding()
}
}
6 changes: 3 additions & 3 deletions Example/Exhibition.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ public struct Exhibition: View {
NavigationView {
ExhibitListView(
exhibits: [
CustomButton_Previews.exhibit,
CustomDatePicker_Previews.exhibit,
CustomToggle_Previews.exhibit,
CustomButton_Previews.anyExhibit,
CustomDatePicker_Previews.anyExhibit,
CustomToggle_Previews.anyExhibit,
]
)
}
Expand Down
2 changes: 1 addition & 1 deletion Exhibition.swifttemplate
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public struct Exhibition: View {
NavigationView {
ExhibitListView(
exhibits: [<% for type in types.types where type.inheritedTypes.contains("ExhibitProvider") { %>
<%= type.name %>.exhibit,<% } %>
<%= type.name %>.anyExhibit,<% } %>
]
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
import SwiftUI

extension Exhibit {
public class Context: ObservableObject {
@Published var parameters: [String: Any] = [:]
@Published var log: [String] = []

public func parameter<T>(name: String, defaultValue: T) -> T {
guard let binding = parameters[name] else {
parameters[name] = defaultValue
return defaultValue
}

return binding as! T
}

public func parameter<T>(name: String) -> T where T: Defaultable {
parameter(name: name, defaultValue: T.defaultValue)
}

public func parameter<T>(name: String, defaultValue: T) -> Binding<T> {
return Binding(
get: { [unowned self] in
self.parameter(name: name, defaultValue: defaultValue)
},
set: { [unowned self] newValue in
parameters[name] = newValue
}
)
}

public func parameter<T>(name: String) -> Binding<T> where T: Defaultable {
parameter(name: name, defaultValue: T.defaultValue)
public class Context: ObservableObject {
@Published var parameters: [String: Any]
@Published var log: [String] = []

init(parameters: [String: Any] = [:]) {
self.parameters = parameters
}

public func parameter<T>(name: String, defaultValue: T) -> T {
guard let binding = parameters[name] else {
parameters[name] = defaultValue
return defaultValue
}

public func log(_ text: String) {
log.append(text)
}
return binding as! T
}

public func parameter<T>(name: String) -> T where T: Defaultable {
parameter(name: name, defaultValue: T.defaultValue)
}

public func parameter<T>(name: String, defaultValue: T) -> Binding<T> {
return Binding(
get: { [unowned self] in
self.parameter(name: name, defaultValue: defaultValue)
},
set: { [unowned self] newValue in
parameters[name] = newValue
}
)
}

public func parameter<T>(name: String) -> Binding<T> where T: Defaultable {
parameter(name: name, defaultValue: T.defaultValue)
}

public func log(_ text: String) {
log.append(text)
}
}

// MARK: - Closure Parameters

extension Exhibit.Context {
extension Context {
/// A closure parameter with no arguments
///
/// EG: `action: () -> Void`
Expand Down
2 changes: 1 addition & 1 deletion Sources/Exhibition/DebugView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI

/// A presented view used to modify accessibility and exhibit parameters.
struct DebugView: View {
@ObservedObject var context: Exhibit.Context
@ObservedObject var context: Context

@Binding var preferredColorScheme: ColorScheme
@Binding var layoutDirection: LayoutDirection
Expand Down
67 changes: 42 additions & 25 deletions Sources/Exhibition/Exhibit.swift
Original file line number Diff line number Diff line change
@@ -1,48 +1,65 @@
import SwiftUI

public struct Exhibit: View {
public struct Exhibit<Content: View> {

let name: String
let section: String
let view: (Context) -> AnyView
let layout: (Self) -> AnyView

@ObservedObject var context = Context()
let content: (Context) -> Content

public init<T: View, S: View>(
public init(
name: String,
section: String = "",
@ViewBuilder builder: @escaping (Context) -> T,
@ViewBuilder layout: @escaping (Self) -> S
@ViewBuilder builder: @escaping (Context) -> Content
) {
self.name = name
self.section = section
view = { context in AnyView(builder(context)) }
self.layout = { exhibit in AnyView(layout(exhibit)) }
self.content = builder
}
}

extension Exhibit {
@ViewBuilder public func preview(parameters: [String: Any] = [:]) -> some View {
ExhibitView(
exhibit: self,
context: Context(parameters: parameters)
)
}
}

struct ExhibitView<Content: View>: View {
let exhibit: Exhibit<Content>
@ObservedObject var context: Context

var body: some View {
exhibit.content(context)
}
}

public struct AnyExhibit {
let name: String
let section: String
let content: (Context) -> AnyView

public var body: some View {
view(context)
.navigationTitle(name)
init<Content: View, Layout: View>(_ exhibit: Exhibit<Content>, layout: @escaping (Content) -> Layout) {
self.name = exhibit.name
self.section = exhibit.section
self.content = { context in
AnyView(layout(exhibit.content(context)))
}
}
}

extension Exhibit: Identifiable {
extension AnyExhibit: Identifiable {
public var id: String {
name
}
}

extension Exhibit {
public init<T: View>(
name: String,
section: String = "",
@ViewBuilder builder: @escaping (Context) -> T
) {
self.init(
name: name,
section: section,
builder: builder
) { AnyView($0) }
struct AnyExhibitView: View {
let exhibit: AnyExhibit
@ObservedObject var context: Context

var body: some View {
exhibit.content(context)
}
}
35 changes: 22 additions & 13 deletions Sources/Exhibition/ExhibitListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI

public struct ExhibitListView: View {

let exhibits: [Exhibit]
let exhibits: [AnyExhibit]

@State var displayed: AnyHashable?
@State var rootDebugViewPresented: Bool = false
Expand All @@ -13,7 +13,7 @@ public struct ExhibitListView: View {
@State var preferredColorScheme: ColorScheme = .light
@State var layoutDirection: LayoutDirection = .leftToRight

private var sections: [String: [Exhibit]] {
private var sections: [String: [AnyExhibit]] {
Dictionary(grouping: searchResults, by: \.section)
}

Expand All @@ -30,7 +30,7 @@ public struct ExhibitListView: View {
)
}

public init(exhibits: [Exhibit]) {
public init(exhibits: [AnyExhibit]) {
self.exhibits = exhibits
}

Expand Down Expand Up @@ -78,8 +78,9 @@ public struct ExhibitListView: View {
.environment(\.layoutDirection, layoutDirection)
}

private func debuggable(_ exhibit: Exhibit) -> some View {
exhibit.layout(exhibit)
private func debuggable(_ exhibit: AnyExhibit) -> some View {
let context = Context()
return AnyExhibitView(exhibit: exhibit, context: context)
.toolbar {
ToolbarItem {
Button {
Expand All @@ -91,7 +92,7 @@ public struct ExhibitListView: View {
}
.sheet(isPresented: $exhibitDebugViewPresented) {
DebugView(
context: exhibit.context,
context: context,
preferredColorScheme: $preferredColorScheme,
layoutDirection: $layoutDirection
)
Expand All @@ -103,7 +104,7 @@ public struct ExhibitListView: View {
.parameterView(ClosureParameterView.self)
}

private var searchResults: [Exhibit] {
private var searchResults: [AnyExhibit] {
if searchText.isEmpty {
return exhibits
} else {
Expand All @@ -116,15 +117,23 @@ public struct ExhibitListView: View {
}

struct ExhibitListView_Previews: PreviewProvider {
struct First: ExhibitProvider {
static let exhibit = Exhibit(name: "Text", section: "Section 1") { context in
Text(context.parameter(name: "Content", defaultValue: "Text"))
}
}

struct Second: ExhibitProvider {
static let exhibit = Exhibit(name: "Text", section: "Section 1") { context in
Text(context.parameter(name: "Content", defaultValue: "Text"))
}
}

static var previews: some View {
ExhibitListView(
exhibits: [
.init(name: "Text", section: "Section 1") { context in
Text(context.parameter(name: "Content", defaultValue: "Text"))
},
.init(name: "Text2", section: "Section 2") { context in
Text(context.parameter(name: "Content", defaultValue: "Text"))
}
First.anyExhibit,
Second.anyExhibit,
]
)
}
Expand Down
18 changes: 17 additions & 1 deletion Sources/Exhibition/ExhibitProvider.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import SwiftUI

public protocol ExhibitProvider {
static var exhibit: Exhibit { get }
associatedtype Content: View
associatedtype Layout: View

static var exhibit: Exhibit<Content> { get }

static func exhibitLayout(_ content: Content) -> Layout
}

public extension ExhibitProvider {
static var previews: some View {
exhibit
.preview()
.previewLayout(.sizeThatFits)
}

static var anyExhibit: AnyExhibit {
AnyExhibit(exhibit, layout: exhibitLayout)
}
}

public extension ExhibitProvider where Content == Layout {
static func exhibitLayout(_ content: Content) -> Layout {
content
}
}
2 changes: 1 addition & 1 deletion Sources/Exhibition/ParameterView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension View {
// MARK: - Internal

/// Type erased parameter view for storage in an array.
typealias AnyParameterView = (String, Any, Exhibit.Context) -> AnyView?
typealias AnyParameterView = (String, Any, Context) -> AnyView?
func erase<P: ParameterView>(_ parameterView: P.Type) -> AnyParameterView {
return { name, value, parameters in
guard let value = value as? P.Value else {
Expand Down

0 comments on commit e193122

Please sign in to comment.