Skip to content

Commit 3f67847

Browse files
committed
feat: add event handling to swift bindings
- Allow registering callbacks to be invoked when document changes - Add some documentation to the public APIs
1 parent 4cfda4c commit 3f67847

File tree

1 file changed

+100
-11
lines changed

1 file changed

+100
-11
lines changed

swift/LiveViewNative/Sources/LiveViewNative/LiveViewNative.swift

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import LiveViewNativeCore
22

3+
/// Raised when a `Document` fails to parse
34
public struct ParseError: Error {
45
let message: String
56

@@ -8,8 +9,26 @@ public struct ParseError: Error {
89
}
910
}
1011

12+
/// Represents the various types of events that a `Document` can produce
13+
public enum EventType {
14+
/// When a document is modified in some way, the `changed` event is raised
15+
case changed
16+
}
17+
18+
struct Handler {
19+
let context: AnyObject?
20+
let callback: (Document, AnyObject?) -> ()
21+
22+
func call(_ doc: Document) {
23+
callback(doc, context)
24+
}
25+
}
26+
27+
/// A `Document` corresponds to the tree of elements in a UI, and supports a variety
28+
/// of operations used to traverse, query, and mutate that tree.
1129
public class Document {
1230
var repr: __Document
31+
var handlers: [EventType: Handler] = [:]
1332

1433
init(_ doc: __Document) {
1534
self.repr = doc
@@ -18,9 +37,19 @@ public class Document {
1837
deinit {
1938
__liveview_native_core$Document$drop(self.repr)
2039
}
21-
}
2240

23-
extension Document {
41+
/// Parse a `Document` from the given `String` or `String`-like type
42+
///
43+
/// The given text should be a valid HTML-ish document, insofar that the structure should
44+
/// be that of an HTML document, but the tags, attributes, and their usages do not have to
45+
/// be valid according to the HTML spec.
46+
///
47+
/// - Parameters:
48+
/// - str: The string to parse
49+
///
50+
/// - Returns: A document representing the parsed content
51+
///
52+
/// - Throws: `ParseError` if the content cannot be parsed for some reason
2453
public static func parse<S: ToRustStr>(_ str: S) throws -> Document {
2554
try str.toRustStr({ rustStr -> Result<Document, ParseError> in
2655
let errorPtr = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout<_RustString>.stride, alignment: MemoryLayout<_RustString>.alignment).assumingMemoryBound(to: _RustString.self)
@@ -36,14 +65,44 @@ extension Document {
3665
}).get()
3766
}
3867

39-
public func merge(with doc: Document) -> Bool {
40-
return __liveview_native_core$Document$merge(self.repr, doc.repr)
68+
/// Register a callback to be fired when a matching event occurs on this document.
69+
///
70+
/// The given callback receives the document to which the event applies, as well as the
71+
/// (optional) context object provided.
72+
///
73+
/// Only one callback per event type is supported. Calling this function multiple times for the
74+
/// same event will only result in the last callback provided being invoked for that tevent
75+
///
76+
/// - Parameters:
77+
/// - event: The `EventType` for which the given callback should be invoked
78+
/// - context: A caller-provided value which should be passed to the callback when it is invoked
79+
/// - callback: The callback to invoke when an event of the given type occurs
80+
///
81+
public func on(_ event: EventType, with context: AnyObject?, _ callback: @escaping (Document, AnyObject?) -> ()) {
82+
self.handlers[event] = Handler(context: context, callback: callback)
83+
}
84+
85+
/// Updates this document by calculating the changes needed to make it equivalent to `doc`,
86+
/// and then applying those changes.
87+
///
88+
/// - Parameters:
89+
/// - doc: The document to compare against
90+
public func merge(with doc: Document) {
91+
if __liveview_native_core$Document$merge(self.repr, doc.repr) {
92+
if let handler = self.handlers[.changed] {
93+
handler.call(self)
94+
}
95+
}
4196
}
4297

98+
/// Returns a reference to the root node of the document
99+
///
100+
/// The root node is not part of the document itself, but can be used to traverse the document tree top-to-bottom.
43101
public func root() -> NodeRef {
44102
return __liveview_native_core$Document$root(self.repr)
45103
}
46104

105+
/// Enables indexing of the document by node reference, returning the reified `Node` to which it corresponds
47106
public subscript(ref: NodeRef) -> Node {
48107
let node = __liveview_native_core$Document$get(self.repr, ref)
49108
return Node(doc: self, ref: ref, data: node)
@@ -65,16 +124,33 @@ extension Document {
65124
}
66125
}
67126

127+
/// Represents a node in the document tree
128+
///
129+
/// A node can be one of three types:
130+
///
131+
/// - A root node, which is a special marker node for the root of the document
132+
/// - A leaf node, which is simply text content, cannot have children or attributes
133+
/// - An element node, which can have children and attributes
134+
///
135+
/// A node in a document is uniquely identified by a `NodeRef` for the lifetime of
136+
/// that node in the document. But a `NodeRef` is not a stable identifier when the
137+
/// tree is modified. In some cases the `NodeRef` remains the same while the content
138+
/// changes, and in others, a new node is allocated, so a new `NodeRef` is used.
68139
public class Node: Identifiable {
140+
/// The type and associated data of this node
69141
public enum Data {
70-
case Root
71-
case Element(HTMLElement)
72-
case Leaf(String)
142+
case root
143+
case element(ElementNode)
144+
case leaf(String)
73145
}
74146

75147
let doc: Document
148+
149+
/// The identifier for this node in its `Document`
76150
public let id: NodeRef
151+
/// The type and data associated with this node
77152
public let data: Data
153+
/// The attributes associated with this node
78154
public lazy var attributes: [Attribute] = {
79155
let refs = doc.getAttrs(id)
80156
var attributes: [Attribute] = []
@@ -89,14 +165,15 @@ public class Node: Identifiable {
89165
self.doc = doc
90166
switch data.ty {
91167
case .NodeTypeRoot:
92-
self.data = .Root
168+
self.data = .root
93169
case .NodeTypeElement:
94-
self.data = .Element(HTMLElement(doc: doc, ref: ref, data: data.data.element))
170+
self.data = .element(ElementNode(doc: doc, ref: ref, data: data.data.element))
95171
case .NodeTypeLeaf:
96-
self.data = .Leaf(RustStr(data.data.leaf).toString()!)
172+
self.data = .leaf(RustStr(data.data.leaf).toString()!)
97173
}
98174
}
99175

176+
/// Nodes are indexable by attribute name, returning the first attribute with that name
100177
public subscript(_ name: AttributeName) -> Attribute? {
101178
for attr in attributes {
102179
if attr.name == name {
@@ -113,6 +190,7 @@ extension Node: Sequence {
113190
}
114191
}
115192

193+
/// An iterator for the direct children of a `Node`
116194
public struct NodeChildrenIterator: IteratorProtocol {
117195
let doc: Document
118196
let children: RustSlice<NodeRef>
@@ -133,9 +211,13 @@ public struct NodeChildrenIterator: IteratorProtocol {
133211
}
134212
}
135213

136-
public struct HTMLElement {
214+
/// Represents a node in a `Document` which can have children and attributes
215+
public struct ElementNode {
216+
/// An (optional) namespace for the element tag name
137217
public let namespace: String?
218+
/// The name of the element tag in the document
138219
public let tag: String
220+
/// A dictionary of attributes associated with this element
139221
public let attributes: [AttributeName: String]
140222

141223
init(doc: Document, ref: NodeRef, data: __Element) {
@@ -151,8 +233,11 @@ public struct HTMLElement {
151233
}
152234
}
153235

236+
/// An attribute is a named string value associated with an element
154237
public struct Attribute {
238+
/// The fully-qualified name of the attribute
155239
public var name: AttributeName
240+
/// The value of this attribute, if there was one
156241
public var value: String?
157242

158243
init(name: AttributeName, value: String?) {
@@ -183,6 +268,10 @@ extension Attribute: Hashable {
183268
}
184269
}
185270

271+
/// Represents a fully-qualified attribute name
272+
///
273+
/// Attribute names can be namespaced, so rather than represent them as a plain `String`,
274+
/// we use this type to preserve the information for easy accessibility.
186275
public struct AttributeName {
187276
public var namespace: String?
188277
public var name: String

0 commit comments

Comments
 (0)