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

Add @Environment support + documentation. #12

Merged
merged 1 commit into from
Aug 2, 2024
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
10 changes: 7 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ let package = Package(
platforms: [
.macOS("14"),
.iOS("17"),
.tvOS(.v16),
.watchOS("10")
],
products: [
// Executable can't share the same name as the library, or we get compiler errors due to conflicts of the two products.
Expand All @@ -27,9 +25,15 @@ let package = Package(

.target(name: "Slipstream", dependencies: [
"SwiftSoup",
"TypeIntrospection",
]),
.testTarget(name: "SlipstreamTests", dependencies: [
"Slipstream",
])
]),

.target(name: "TypeIntrospection"),
.testTarget(name: "TypeIntrospectionTests", dependencies: [
"TypeIntrospection",
]),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# How Slipstream works

A from the ground up explanation of Slipstream's architecture.

Slipstream is designed to offer a SwiftUI-like approach to building HTML documents
that are compatible with [Tailwind CSS](http://tailwindcss.com).

## Core Concepts

### Result Builders

Slipstream uses Swift [result builders](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0289-result-builders.md)
to enable the construction of HTML documents in a syntax similar to SwiftUI's.
Result builders encourage defining structural data in a hierarchical and declarative
manner, separating intent (what the site should look like) from implementation (how
it's turned into HTML). This separation of concerns allows you to focus on the
design and structure of your web pages without getting bogged down by the
intricacies of HTML generation.

### View Protocol

The primary type that Slipstream's result builders work with is the ``View`` protocol.
Like SwiftUI, a View represents a part of an HTML document that can be combined
with other View instances to create a website. The protocol defines a contract
that all views must adhere to, ensuring consistency in how views are constructed and
rendered as HTML.

```swift
public protocol View {
associatedtype Content: View
@ViewBuilder var body: Self.Content { get }

func render(_ container: Element) throws
}
```

The ``View/body`` property returns the content of the view, and in most cases, is
the only part of the View protocol that you need to implement. The ``ViewBuilder``
attribute is what enables our use of the SwiftUI-like syntax in the body implementation.

The ``View/render(_:environment:)`` method, on the other hand, is responsible for converting the
view’s content into HTML elements. You'll only need to implement this method if you
need to generate new types of HTML.

### W3C HTML Views

Slipstream provides a catalog of standard [W3C HTML](https://html.spec.whatwg.org/multipage/)
View implementations that can be used to build your website. Read <doc:Fundamentals> to
learn more about the different Views available in Slipstream.

### Rendering a View as HTML

The combination of result builders, the View protocol, and a ``Text`` view is all we
need to build a simple "Hello, world!" example:

```swift
struct HelloWorld: View {
var body: some View {
Text("Hello, world!")
}
}

print(try renderHTML(HelloWorld()))
```

In this example, the Text view is treated as a single "block" in HelloWorld's body.

Slipstream depends on [SwiftSoup](https://scinfu.github.io/SwiftSoup/) for rendering valid
HTML. Each call to ``renderHTML(_:)`` follows the same rough flow:

1. The ``renderHTML(_:)`` method creates a SwiftSoup `Document` object.
2. This object is then passed to HelloWorld's ``View/render(_:environment:)`` method,
which in turn calls Text's `render(_:environment:)` method, which appends the string
to the document. This step happens recursively until the entire view
hierarchy has had a chance to render its contents into the document.
3. The Document, at this point an in-memory Document Object Model (DOM) representation,
is then rendered as HTML using SwiftSoup and returned.

You can then save the resulting html string to the appropriate file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# ``Environment``

## Topics

### Getting the value

- ``Environment/wrappedValue``
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# ``EnvironmentValues``

## Topics

### Creating and accessing values

- ``EnvironmentValues/init()``
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Environment values

Share data throughout a view hierarchy using the environment.

Like SwiftUI, views in Slipstream can react to configuration information
that they read from the environment using an ``Environment`` property wrapper.

A view inherits its environment from its container view, subject to explicit
changes from an ``View/environment(_:_:)`` view modifier. As a result, you
can configure an entire hierarchy of views by modifying the environment of
the group’s container.

## Defining custom environment values

To create a custom environment value, you first define a type that conforms
to the ``EnvironmentKey`` protocol. This type will be used to uniquely
identify the value in the environment.

```swift
struct PathEnvironmentKey: EnvironmentKey {
static let defaultValue: String = "/"
}
```

You must then provide a way to read and write the environment value:

```swift
extension EnvironmentValues {
var path: String {
get { self[PathEnvironmentKey.self] }
set { self[PathEnvironmentKey.self] = newValue }
}
}
```

While not required, it's also a good practice to provide a ``View``
extension that modifies the environment value:

```swift
extension View {
func path(_ path: String) -> some View {
environment(\.path, path)
}
}
```

### How to read environment properties

In any ``View``, you can read an environment value using the ``Environment``
property wrapper:

```swift
struct MyView: View {
@Environment(\.path) var path

// ...
}
```

You can then read the environment property like any other property on
the view. When the view is rendered, the value of the property will
reflect the environment this view is being rendered within.

### How to change environment properties

Within the ``View/body`` of any view you can use the ``View/environment(_:_:)``
modifier to set the environment value for that view and its descendants.

```swift
MyView()
.environment(\.path, "/home")
```


## Topics

### Accessing environment values

- ``Environment``
- ``EnvironmentValues``

### Creating custom environment values

- ``EnvironmentKey``
15 changes: 9 additions & 6 deletions Sources/Slipstream/Documentation.docc/Slipstream.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,19 @@ print(try renderHTML(HelloWorld()))

## Topics

### Architecture

- <doc:HowSlipstreamWorks>

### Data and storage

- <doc:EnvironmentValuesSection>

### Views

- <doc:Catalog>
- <doc:Fundamentals>
- <doc:TextInputAndOutput>

### Rendering Views
### Rendering views

- ``renderHTML(_:)``

### Architecture

- <doc:HowSlipstreamWorks>
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The ``View/body`` property returns the content of the view, and in most cases, i
the only part of the View protocol that you need to implement. The ``ViewBuilder``
attribute is what enables our use of the SwiftUI-like syntax in the body implementation.

The ``View/render(_:)`` method, on the other hand, is responsible for converting the
The ``View/render(_:environment:)`` method, on the other hand, is responsible for converting the
view’s content into HTML elements. You'll only need to implement this method if you
need to generate new types of HTML.

Expand Down Expand Up @@ -66,6 +66,5 @@ print(try renderHTML(HelloWorld()))
In this example, the Text view is treated as a single "block" in HelloWorld's body.

The ``renderHTML(_:)`` method creates a SwiftSoup Document object and then passes this
document to the HelloWorld's ``View/render(_:)`` method, which in turn calls Text's
`render(_:)` method, which appends the string to the Document object.

document to the HelloWorld's ``View/render(_:environment:)`` method, which in turn calls Text's
`render(_:environment:)` method, which appends the string to the Document object.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ conforms to the ``View`` protocol can act as a view in your website.

## Topics

### Creating a View
### Creating a view

- ``View``
- ``ViewBuilder``

### Modifying a view

- ``ViewModifier``
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

- ``View/body``
- ``View/Content``
- ``View/modifier(_:)``

### Implementing HTML rendering

- ``View/render(_:)``
- ``View/render(_:environment:)``

### State modifiers

- ``View/environment(_:_:)``
2 changes: 1 addition & 1 deletion Sources/Slipstream/Rendering/Render.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ import SwiftSoup
/// - Returns: The generated and formatted HTML string.
public func renderHTML(_ view: any View) throws -> String {
let document = Document("/")
try view.render(document)
try view.render(document, environment: EnvironmentValues())
return try document.html()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/// A property wrapper that reads a value from a view's environment.
///
/// Use the `Environment` property wrapper to read a value stored in a view's
/// environment. Indicate the value to read using an ``EnvironmentValues``
/// key path in the property declaration.
///
/// You can condition a view's content on the associated value, which
/// you read from the declared property's ``wrappedValue``. As with any property
/// wrapper, you access the wrapped value by directly referring to the property:
///
/// You can use this property wrapper to read --- but not set --- an environment
/// value. You can override existing environment values, as well as set custom
/// environment values that you define, using the ``View/environment(_:_:)``
/// view modifier.
@available(iOS 17.0, macOS 14.0, *)
@propertyWrapper
public struct Environment<Value> {
/// Creates an environment property to read the specified key path.
///
/// Don’t call this initializer directly. Instead, declare a property
/// with the ``Environment`` property wrapper, and provide the key path of
/// the environment value that the property should reflect:
///
/// ```swift
/// struct MyView: View {
/// @Environment(\.path) var path
///
/// // ...
/// }
/// ```
///
/// You can't modify the environment value using a property like this. Instead,
/// use the ``View/environment(_:_:)`` view modifier on a view to set
/// a value for a view hierarchy.
///
/// - Parameter keyPath: A key path to a specific resulting value.
public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
self.keyPath = keyPath
}

/// The current value of the environment property.
///
/// The wrapped value property provides primary access to the value's data.
/// However, you don't access `wrappedValue` directly. Instead, you read the
/// property variable created with the ``Environment`` property wrapper.
public var wrappedValue: Value {
get { environmentValues[keyPath: keyPath] }
set { fatalError("Wrapped value should not be used.") }
}

/// The environment storage, from which the value of the property will be retrieved.
///
/// When a view is rendered, this property will be modified with the contextual environment values.
private var environmentValues: EnvironmentValues = EnvironmentValues()

/// The key path to the property represented by this Environment wrapper.
private let keyPath: KeyPath<EnvironmentValues, Value>
}
Loading