Skip to content

Commit a050038

Browse files
authored
Merge pull request #129 from liveviewnative/core
2 parents 9e4ca15 + e2183eb commit a050038

File tree

212 files changed

+2367
-1846
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

212 files changed

+2367
-1846
lines changed

Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,38 @@
44
import PackageDescription
55

66
let package = Package(
7-
name: "PhoenixLiveViewNative",
7+
name: "LiveViewNative",
88
platforms: [
9-
.iOS("15.0")
9+
.iOS("16.0")
1010
],
1111
products: [
1212
// Products define the executables and libraries a package produces, and make them visible to other packages.
1313
.library(
14-
name: "PhoenixLiveViewNative",
15-
targets: ["PhoenixLiveViewNative"]),
14+
name: "LiveViewNative",
15+
targets: ["LiveViewNative"]),
1616
],
1717
dependencies: [
1818
// Dependencies declare other packages that this package depends on.
1919
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.3.2"),
2020
.package(url: "https://github.com/davidstump/SwiftPhoenixClient.git", .upToNextMinor(from: "5.0.0")),
2121
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", .upToNextMinor(from: "0.1.4")),
22+
.package(url: "https://github.com/liveviewnative/liveview-native-core-swift.git", branch: "main"),
2223
],
2324
targets: [
2425
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2526
// Targets can depend on other targets in this package, and on products in packages this package depends on.
2627
.target(
27-
name: "PhoenixLiveViewNative",
28+
name: "LiveViewNative",
2829
dependencies: [
2930
"SwiftSoup",
3031
"SwiftPhoenixClient",
3132
.product(name: "Introspect", package: "SwiftUI-Introspect"),
32-
.target(name: "LVNObjC")
33+
.target(name: "LVNObjC"),
34+
.product(name: "LiveViewNativeCore", package: "liveview-native-core-swift"),
3335
]),
3436
.target(name: "LVNObjC"),
3537
.testTarget(
36-
name: "PhoenixLiveViewNativeTests",
37-
dependencies: ["PhoenixLiveViewNative"]),
38+
name: "LiveViewNativeTests",
39+
dependencies: ["LiveViewNative"]),
3840
]
3941
)

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# PhoenixLiveViewNative
1+
# LiveViewNative
22

3-
The PhoenixLiveViewNative Swift package lets you use Phoenix LiveView to build native iOS apps with SwiftUI.
3+
The LiveViewNative Swift package lets you use Phoenix LiveView to build native iOS apps with SwiftUI.
44

55
## Resources
66

7-
- Browse the [documentation](https://liveviewnative.github.io/liveview-client-swiftui/documentation/phoenixliveviewnative/) to read about the API.
7+
- Browse the [documentation](https://liveviewnative.github.io/liveview-client-swiftui/documentation/liveviewnative/) to read about the API.
88
- Follow the [tutorial](https://liveviewnative.github.io/liveview-client-swiftui/tutorials/yourfirstapp) to get a step-by-step walkthrough of building an app with LiveView Native.
99
- Check out the [ElixirConf '22 chat app](https://github.com/liveviewnative/elixirconf_chat) for an example of a complete app.

Sources/LVNObjC/ProxyingNavigationControllerDelegate.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// ProxyingNavigationControllerDelegate.m
3-
// PhoenixLiveViewNative
3+
// LiveViewNative
44
//
55
// Created by Shadowfacts on 6/13/22.
66
//

Sources/LVNObjC/include/LVNObjC.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// LVNObjC.h
3-
// PhoenixLiveViewNative
3+
// LiveViewNative
44
//
55
// Created by Shadowfacts on 6/13/22.
66
//
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// ViewRegistry.swift
3+
// LiveViewNative
4+
//
5+
// Created by Shadowfacts on 2/11/22.
6+
//
7+
8+
import SwiftUI
9+
10+
struct BuiltinRegistry {
11+
12+
static let attributeDecoder = JSONDecoder()
13+
14+
@ViewBuilder
15+
static func lookup<R: CustomRegistry>(_ name: String, _ element: ElementNode, context: LiveContext<R>) -> some View {
16+
switch name {
17+
case "textfield":
18+
TextField<R>(element: element, context: context)
19+
case "text":
20+
Text(element: element, context: context)
21+
case "hstack":
22+
HStack<R>(element: element, context: context)
23+
case "vstack":
24+
VStack<R>(element: element, context: context)
25+
case "zstack":
26+
ZStack<R>(element: element, context: context)
27+
case "button":
28+
Button<R>(element: element, context: context, action: nil)
29+
case "image":
30+
Image(element: element, context: context)
31+
case "asyncimage":
32+
AsyncImage(element: element, context: context)
33+
case "scrollview":
34+
ScrollView<R>(element: element, context: context)
35+
case "spacer":
36+
Spacer(element: element, context: context)
37+
case "navigationlink":
38+
NavigationLink(element: element, context: context)
39+
case "list":
40+
List<R>(element: element, context: context)
41+
case "rectangle":
42+
Shape(element: element, context: context, shape: Rectangle())
43+
case "roundedrectangle":
44+
Shape(element: element, context: context, shape: RoundedRectangle(from: element))
45+
46+
case "phx-form":
47+
PhxForm<R>(element: element, context: context)
48+
case "phx-hero":
49+
PhxHeroView(element: element, context: context)
50+
case "phx-submit-button":
51+
PhxSubmitButton(element: element, context: context)
52+
default:
53+
// log here that view type cannot be found
54+
EmptyView()
55+
}
56+
}
57+
58+
enum ModifierType: String {
59+
case frame
60+
case listRowInsets = "list_row_insets"
61+
case listRowSeparator = "list_row_separator"
62+
case navigationTitle = "navigation_title"
63+
case padding
64+
case tint
65+
}
66+
67+
static func decodeModifier(_ type: ModifierType, from decoder: Decoder) throws -> any ViewModifier {
68+
switch type {
69+
case .frame:
70+
return try FrameModifier(from: decoder)
71+
case .listRowInsets:
72+
return try ListRowInsetsModifier(from: decoder)
73+
case .listRowSeparator:
74+
return try ListRowSeparatorModifier(from: decoder)
75+
case .navigationTitle:
76+
return try NavigationTitleModifier(from: decoder)
77+
case .padding:
78+
return try PaddingModifier(from: decoder)
79+
case .tint:
80+
return try TintModifier(from: decoder)
81+
}
82+
}
83+
}

Sources/PhoenixLiveViewNative/CustomRegistry.swift renamed to Sources/LiveViewNative/CustomRegistry.swift

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
//
22
// CustomRegistry.swift
3-
// PhoenixLiveViewNative
3+
// LiveViewNative
44
//
55
// Created by Shadowfacts on 2/16/22.
66
//
77

88
import SwiftUI
9-
import SwiftSoup
10-
11-
// typealiases so people implementing CustomRegistry don't have to also import SwiftSoup
12-
public typealias Element = SwiftSoup.Element
13-
public typealias Attribute = SwiftSoup.Attribute
9+
import LiveViewNativeCore
1410

1511
/// A custom registry allows clients to include custom view types in the LiveView DOM.
1612
///
17-
/// To add a custom element or attribute, define an enum for the type alias for the tag/attribute name and implement the appropriate method. To customize the loading view, implement the ``loadingView(for:state:)-vg2v`` method.
13+
/// To add a custom element or attribute, define an enum for the type alias for the tag/attribute name and implement the appropriate method. To customize the loading view, implement the ``loadingView(for:state:)-2uoy9`` method.
1814
///
1915
/// To use your custom registry implementation, provide it as the generic parameter for the ``LiveViewCoordinator`` you construct:
2016
///
@@ -27,13 +23,15 @@ public typealias Attribute = SwiftSoup.Attribute
2723
/// ## Topics
2824
/// ### Custom Tags
2925
/// - ``TagName``
30-
/// - ``lookup(_:element:context:)-2rzrl``
31-
/// ### Custom Attributes
32-
/// - ``AttributeName``
33-
/// - ``lookupModifier(_:value:element:context:)-9qdm4``
26+
/// - ``lookup(_:element:context:)-895au``
27+
/// - ``CustomView``
28+
/// ### Custom View Modifiers
29+
/// - ``ModifierType``
30+
/// - ``decodeModifier(_:from:context:)-4j076``
3431
/// ### Customizing the Loading View
35-
/// - ``loadingView(for:state:)-vg2v``
36-
/// ### See Also
32+
/// - ``loadingView(for:state:)-2uoy9``
33+
/// - ``LoadingView``
34+
/// ### Supporting Types
3735
/// - ``EmptyRegistry``
3836
public protocol CustomRegistry {
3937
/// A type representing the tag names that this registry type can provide views for.
@@ -57,34 +55,34 @@ public protocol CustomRegistry {
5755
/// }
5856
/// ```
5957
associatedtype TagName: RawRepresentable where TagName.RawValue == String
60-
/// A type represnting the attribute names that this registry can handle.
58+
/// A type represnting the custom modifier types that this registry can handle.
6159
///
62-
/// The attribute name type must be `RawRepresentable` and its raw values must be strings. All raw value strings must be lowercased, otherwise the framework will not be able to construct your attribute types from strings in the DOM.
60+
/// This type must be `RawRepresentable` and its raw values must be strings.
6361
///
6462
/// Generally, this is an enum which declares variants for the support attributes:
6563
/// ```swift
6664
/// struct MyRegistry: CustomRegistry {
6765
/// enum AttributeName: String {
6866
/// case foo
69-
/// case barBaz = "bar-baz"
67+
/// case barBaz
7068
/// }
7169
/// }
7270
/// ```
7371
///
74-
/// If your registry does not support any custom attributes, you can set this type alias to the ``EmptyRegistry/None`` type:
72+
/// If your registry does not support any custom modifiers, you can set this type alias to the ``EmptyRegistry/None`` type:
7573
/// ```swift
7674
/// struct MyRegistry: CustomRegistry {
77-
/// typealias AttributeName = EmptyRegistry.None
75+
/// typealias ModifierType = EmptyRegistry.None
7876
/// }
7977
/// ```
80-
associatedtype AttributeName: RawRepresentable where AttributeName.RawValue == String
78+
associatedtype ModifierType: RawRepresentable where ModifierType.RawValue == String
8179
/// The type of view this registry returns from the `lookup` method.
8280
///
83-
/// Generally, implementors will use an opaque return type on their ``lookup(_:element:context:)-2rzrl`` implementations and this will be inferred automatically.
81+
/// Generally, implementors will use an opaque return type on their ``lookup(_:element:context:)-895au`` implementations and this will be inferred automatically.
8482
associatedtype CustomView: View
8583
/// The type of view this registry produces for loading views.
8684
///
87-
/// Generally, implementors will use an opaque return type on their ``loadingView(for:state:)-vg2v`` implementations and this will be inferred automatically.
85+
/// Generally, implementors will use an opaque return type on their ``loadingView(for:state:)-2uoy9`` implementations and this will be inferred automatically.
8886
associatedtype LoadingView: View
8987

9088
/// This method is called by LiveView Native when it needs to construct a custom view.
@@ -95,18 +93,18 @@ public protocol CustomRegistry {
9593
/// - Parameter element: The element that a view should be created for.
9694
/// - Parameter context: The live context in which the view is being created.
9795
@ViewBuilder
98-
static func lookup(_ name: TagName, element: Element, context: LiveContext<Self>) -> CustomView
96+
static func lookup(_ name: TagName, element: ElementNode, context: LiveContext<Self>) -> CustomView
9997

100-
/// This method is called by LiveView Native when it encounters a custom attribute your registry has declared support for.
98+
/// This method is called by LiveView Native when it encounters a view modifier your registry has declared support for.
10199
///
102-
/// If your custom registry does not support any attributes, you can set the `AttributeName` type alias to ``EmptyRegistry/None`` and omit this method.
100+
/// If your custom registry does not support any custom modifiers, you can set the `ModifierType` type alias to ``EmptyRegistry/None`` and omit this method.
103101
///
104-
/// - Parameter name: The name of the attribute.
105-
/// - Parameter value: The value of the attribute. If no value is provided in the DOM, an empty string will be used as the value.
106-
/// - Parameter element: The element on which the attribute is present.
102+
/// - Parameter type: The type of the modifier.
103+
/// - Parameter decoder: The decoder representing the JSON object of the modifier. Implement `Decodable` on your modifier type and call `MyModifier(from: decoder)` to use it.
107104
/// - Parameter context: The live context in which the view is being built.
108105
/// - Returns: A struct that implements the `SwiftUI.ViewModifier` protocol.
109-
static func lookupModifier(_ name: AttributeName, value: String, element: Element, context: LiveContext<Self>) -> any ViewModifier
106+
/// - Throws: If decoding the modifier fails.
107+
static func decodeModifier(_ type: ModifierType, from decoder: Decoder, context: LiveContext<Self>) throws -> any ViewModifier
110108

111109
/// This method is called when it needs a view to display while connecting to the live view.
112110
///
@@ -129,7 +127,7 @@ extension CustomRegistry where LoadingView == Never {
129127
public struct EmptyRegistry {
130128
}
131129
extension EmptyRegistry: CustomRegistry {
132-
/// A type that can be used as ``CustomRegistry/TagName`` or ``CustomRegistry/AttributeName`` for registries which don't support any custom tags or attributes.
130+
/// A type that can be used as ``CustomRegistry/TagName`` or ``CustomRegistry/ModifierType`` for registries which don't support any custom tags or attributes.
133131
public struct None: RawRepresentable {
134132
public typealias RawValue = String
135133
public var rawValue: String
@@ -140,17 +138,17 @@ extension EmptyRegistry: CustomRegistry {
140138
}
141139

142140
public typealias TagName = None
143-
public typealias AttributeName = None
141+
public typealias ModifierType = None
144142
}
145143
extension CustomRegistry where TagName == EmptyRegistry.None, CustomView == Never {
146144
/// A default implementation that does not provide any custom elements. If you omit the ``CustomRegistry/TagName`` type alias, this implementation will be used.
147-
public static func lookup(_ name: TagName, element: Element, context: LiveContext<Self>) -> Never {
145+
public static func lookup(_ name: TagName, element: ElementNode, context: LiveContext<Self>) -> Never {
148146
fatalError()
149147
}
150148
}
151-
extension CustomRegistry where AttributeName == EmptyRegistry.None {
152-
/// A default implementation that does not provide any custom attributes. If you omit the ``CustomRegistry/AttributeName`` type alias, this implementation will be used.
153-
public static func lookupModifier(_ name: AttributeName, value: String, element: Element, context: LiveContext<Self>) -> any ViewModifier {
149+
extension CustomRegistry where ModifierType == EmptyRegistry.None {
150+
/// A default implementation that does not provide any custom modifiers. If you omit the ``CustomRegistry/ModifierType`` type alias, this implementation will be used.
151+
public static func decodeModifier(_ type: ModifierType, from decoder: Decoder, context: LiveContext<Self>) -> any ViewModifier {
154152
fatalError()
155153
}
156154
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// Environment.swift
3+
// LiveViewNative
4+
//
5+
// Created by Shadowfacts on 7/21/22.
6+
//
7+
8+
import SwiftUI
9+
import LiveViewNativeCore
10+
import Combine
11+
12+
private struct FormModelKey: EnvironmentKey {
13+
static let defaultValue: FormModel? = nil
14+
}
15+
16+
private struct ElementKey: EnvironmentKey {
17+
static let defaultValue: ElementNode? = nil
18+
}
19+
20+
private struct TextFieldPrimaryActionKey: EnvironmentKey {
21+
public static var defaultValue: (() -> Void)? = nil
22+
}
23+
24+
/// Provides access to ``LiveViewCoordinator`` properties via the environment.
25+
/// This exists to type-erase the coordinator, since environment properties can't be generic.
26+
struct CoordinatorEnvironment {
27+
let pushEvent: (String, String, Any) async throws -> Void
28+
let elementChanged: AnyPublisher<NodeRef, Never>
29+
let document: Document
30+
31+
@MainActor
32+
init<R: CustomRegistry>(_ coordinator: LiveViewCoordinator<R>, document: Document) {
33+
self.pushEvent = coordinator.pushEvent
34+
self.document = document
35+
self.elementChanged = coordinator.elementChanged.filter({ _ in
36+
// only pass through changes that happen for our document
37+
coordinator.document === document
38+
}).eraseToAnyPublisher()
39+
}
40+
41+
fileprivate struct Key: EnvironmentKey {
42+
static var defaultValue: CoordinatorEnvironment? = nil
43+
}
44+
}
45+
46+
extension EnvironmentValues {
47+
/// The model for the nearest ancestor `<form>` element (or `nil`, if there is no such element).
48+
public var formModel: FormModel? {
49+
get { self[FormModelKey.self] }
50+
set { self[FormModelKey.self] = newValue }
51+
}
52+
53+
/// The DOM element that the view was constructed from.
54+
public var element: ElementNode? {
55+
get { self[ElementKey.self] }
56+
set { self[ElementKey.self] = newValue }
57+
}
58+
59+
@_spi(NarwinChat) public var textFieldPrimaryAction: (() -> Void)? {
60+
get { self[TextFieldPrimaryActionKey.self] }
61+
set { self[TextFieldPrimaryActionKey.self] = newValue }
62+
}
63+
64+
var coordinatorEnvironment: CoordinatorEnvironment? {
65+
get { self[CoordinatorEnvironment.Key.self] }
66+
set { self[CoordinatorEnvironment.Key.self] = newValue }
67+
}
68+
}

0 commit comments

Comments
 (0)