Skip to content

Navigation patterns guide #1391

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

Draft
wants to merge 65 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
a772d33
Introduce v0.3.0 livebook guides
BrooklinJazz Apr 30, 2024
2c70332
add pretty: true config to live_view_native_stylesheet in livebook le…
BrooklinJazz Apr 30, 2024
a01e782
move livebooks
BrooklinJazz Apr 30, 2024
df47852
Draft forms and validations reading
BrooklinJazz May 3, 2024
667df60
grammar check create a swiftui app
BrooklinJazz May 3, 2024
d20310e
grammar check getting started
BrooklinJazz May 3, 2024
e037da6
grammar check swiftui views
BrooklinJazz May 3, 2024
9e85aed
Use flags to control mix docs tasks and generate markdown livebooks
BrooklinJazz May 7, 2024
aeca50f
Merge branch 'main' into v0.3.0-guides
BrooklinJazz May 7, 2024
74be999
Move form package setup to the create a swiftui application section
BrooklinJazz May 7, 2024
898b762
Fix examples in the interactive swiftui views reading to use LiveForm
BrooklinJazz May 8, 2024
4232b5f
remove unused check_errors value in the socket for forms and validati…
BrooklinJazz May 8, 2024
fbaf75f
update class -> style
BrooklinJazz May 15, 2024
9a80909
fix create a swiftui application bug
BrooklinJazz May 21, 2024
452a903
use single quotes for Color syntax
BrooklinJazz May 21, 2024
aa54597
add vscode to gitignore
BrooklinJazz May 21, 2024
8e8d082
Draft syntax conversion cheatsheet
BrooklinJazz May 21, 2024
164aa39
Replace ```html with ```heex
BrooklinJazz May 21, 2024
eb18a57
Merge branch 'main' into v0.3.0-guides
BrooklinJazz May 21, 2024
897daf5
Update livebooks/create-a-swiftui-application.livemd
BrooklinJazz May 22, 2024
dc28b2b
Update livebooks/create-a-swiftui-application.livemd
BrooklinJazz May 22, 2024
3d806a4
Update livebooks/create-a-swiftui-application.livemd
BrooklinJazz May 22, 2024
89b3487
resolve PR review comments
BrooklinJazz May 22, 2024
ec906ff
Update livebooks/stylesheets.livemd
BrooklinJazz May 22, 2024
e6e0c4b
Update livebooks/native-navigation.livemd
BrooklinJazz May 22, 2024
caf8c19
Update livebooks/forms-and-validation.livemd
BrooklinJazz May 22, 2024
29aa01e
Update livebooks/forms-and-validation.livemd
BrooklinJazz May 22, 2024
a5028d0
Update livebooks/forms-and-validation.livemd
BrooklinJazz May 22, 2024
07da913
PR review changes
BrooklinJazz May 22, 2024
75c520c
add config for physical devices
BrooklinJazz May 22, 2024
0103c5e
run gen doc commands
BrooklinJazz May 23, 2024
6cab4fe
change skip-gen-docs command name
BrooklinJazz May 27, 2024
89af0fa
add link component explanation to native navigation guide
BrooklinJazz May 30, 2024
fb80602
review getting started,forms and validation, and interactive swiftui …
BrooklinJazz Jun 27, 2024
d740367
quick test for brian
BrooklinJazz Jun 27, 2024
402409a
Update to 5.3.2 of SwiftPhoenixClient
carson-katri May 21, 2024
45a3c0d
Use `connecting` phase when channel is not connected, but socket is
carson-katri May 16, 2024
200d36c
Add `setup` state
carson-katri May 16, 2024
ce4b7bc
Use `style` attribute for generated docs
carson-katri May 22, 2024
bd40afd
Treat boolean attributes as true if present and != "false"
carson-katri May 23, 2024
4925931
Update core components to use `style` attribute
carson-katri May 23, 2024
9896dd3
Expose `node` and `data` on ElementNode
carson-katri May 23, 2024
30819fd
Expose URL and LiveViewCoordinator to content builders
carson-katri May 30, 2024
cc9a44b
Remove <%!-- template --%> specifier
carson-katri May 28, 2024
cf2220e
Add support for AttributeReference in individual Color properties
carson-katri May 29, 2024
f2b05e1
Fix ShapeStyle tests
carson-katri May 29, 2024
4720460
Expose the `document` and updated `NodeRef` for addon libraries (#1373)
carson-katri Jun 11, 2024
2f91282
Split complex Regex expression (#1376)
carson-katri Jun 12, 2024
d662cf0
Bundle xcodegen
bcardarella Jun 25, 2024
356a932
Skip stylesheet parsing if ContentBuilder doesn't provide any modifiers
carson-katri Jun 27, 2024
e71368e
Add nested keys for l10n and i18n
carson-katri Jun 25, 2024
e1657d6
Shorten keys
carson-katri Jun 25, 2024
afa0f63
rebase from main
BrooklinJazz Jul 3, 2024
761f8e3
Use flags to control mix docs tasks and generate markdown livebooks
BrooklinJazz May 7, 2024
e8682f0
fix issues from rebase
BrooklinJazz Jul 3, 2024
7286d73
Merge branch 'custom-color-test-branch' into v0.3.0-guides
BrooklinJazz Jul 9, 2024
969991a
remove example code for brian
BrooklinJazz Jul 9, 2024
849a7f8
add attribute parsers section
BrooklinJazz Jul 15, 2024
0460075
use kino_live_view_native github url instead of local path
BrooklinJazz Jul 15, 2024
e270a47
Introduce v0.3.0 livebook guides
BrooklinJazz Apr 30, 2024
668b112
Use flags to control mix docs tasks and generate markdown livebooks
BrooklinJazz May 7, 2024
f7a76b9
generate docs
BrooklinJazz Jul 16, 2024
4c28100
start navigation cookbook
BrooklinJazz Jul 16, 2024
01f3dba
draft navigation patterns drill-down pattern
BrooklinJazz Jul 18, 2024
038258e
use kino_live_view_native github url instead of local path
BrooklinJazz Jul 18, 2024
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
17 changes: 0 additions & 17 deletions .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,6 @@ permissions:
env:
MIX_ENV: test

steps:
- name: Update Homebrew
run: |
brew update --preinstall
cat "$(brew --repository)/Library/Taps/homebrew/homebrew-core/Formula/foo.rb" > .github/brew-formulae
- name: Configure Homebrew cache
uses: actions/cache@v2
with:
path: |
~/Library/Caches/Homebrew/foo--*
~/Library/Caches/Homebrew/downloads/*--foo-*
key: brew-${{ hashFiles('.github/brew-formulae') }}
restore-keys: brew-
- name: Install Homebrew dependencies
run: |
env HOMEBREW_NO_AUTO_UPDATE=1 brew install xcodegen

jobs:
build:
name: Build and test
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ live_view_native_swiftui-*.tar

# Temporary files, for example, from tests.
/tmp/

.vscode
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/davidstump/SwiftPhoenixClient.git",
"state" : {
"revision" : "613989bf2e562d8a851ea83741681c3439353b45",
"version" : "5.0.0"
"revision" : "588bf6baab5d049752748e19a4bff32421ea40ec",
"version" : "5.3.2"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.3.2"),
.package(url: "https://github.com/davidstump/SwiftPhoenixClient.git", .upToNextMinor(from: "5.0.0")),
.package(url: "https://github.com/davidstump/SwiftPhoenixClient.git", .upToNextMinor(from: "5.3.2")),
.package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"),
.package(url: "https://github.com/liveview-native/liveview-native-core-swift.git", exact: "0.2.1"),

Expand Down
27 changes: 15 additions & 12 deletions Sources/BuiltinRegistryGenerator/BuiltinRegistryGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,21 +132,24 @@ struct BuiltinRegistryGenerator: ParsableCommand {
// [platform] [version], ...
let platform = Reference(Substring.self)
let version = Reference(Double?.self)
let expression = Regex {
Capture(as: platform) {
OneOrMore(.word)
let platformExpr = Capture(as: platform) {
OneOrMore(.word)
}
let versionExpr = Capture(as: version) {
OneOrMore(.digit)
Optionally {
"."
OneOrMore(.digit)
}
} transform: {
Double($0)
}

let expression = Regex {
platformExpr
Optionally {
OneOrMore(.whitespace)
Capture(as: version) {
OneOrMore(.digit)
Optionally {
"."
OneOrMore(.digit)
}
} transform: {
Double($0)
}
versionExpr
}
}
let availability = String(match[availability])
Expand Down
60 changes: 43 additions & 17 deletions Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private let logger = Logger(subsystem: "LiveViewNative", category: "LiveSessionC
@MainActor
public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
/// The current state of the live view connection.
@Published public private(set) var state = LiveSessionState.notConnected
@Published public private(set) var state = LiveSessionState.setup

/// The current URL this live view is connected to.
public private(set) var url: URL
Expand Down Expand Up @@ -103,11 +103,11 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {

$navigationPath.scan(([LiveNavigationEntry<R>](), [LiveNavigationEntry<R>]()), { ($0.1, $1) }).sink { [weak self] prev, next in
guard let self else { return }
let isDisconnected: Bool
if case .notConnected = next.last!.coordinator.state {
isDisconnected = true
} else {
isDisconnected = false
let isDisconnected = switch next.last!.coordinator.state {
case .setup, .disconnected:
true
default:
false
}
if next.last!.coordinator.url != next.last!.url || isDisconnected {
Task {
Expand Down Expand Up @@ -151,15 +151,15 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
///
/// You generally do not call this function yourself. It is called automatically when the ``LiveView`` appears.
///
/// This function is a no-op unless ``state`` is ``LiveSessionState/notConnected``.
/// This function is a no-op unless ``state`` is ``LiveSessionState/setup`` or ``LiveSessionState/disconnected`` or ``LiveSessionState/connectionFailed(_:)``.
///
/// This is an async function which completes when the connection has been established or failed.
///
/// - Parameter httpMethod: The HTTP method to use for the dead render. Defaults to `GET`.
/// - Parameter httpBody: The HTTP body to send when requesting the dead render.
public func connect(httpMethod: String? = nil, httpBody: Data? = nil) async {
switch state {
case .notConnected, .connectionFailed:
case .setup, .disconnected, .connectionFailed:
break
default:
return
Expand Down Expand Up @@ -252,7 +252,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
}
self.socket?.disconnect()
self.socket = nil
self.state = .notConnected
self.state = .disconnected
}

/// Forces the session to disconnect then connect.
Expand Down Expand Up @@ -418,7 +418,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
socket.off(refs)
continuation.resume(returning: socket)
})
refs.append(socket.onError { [weak self, weak socket] (error) in
refs.append(socket.onError { [weak self, weak socket] (error, response) in
guard let socket else { return }
guard self != nil else {
socket.disconnect()
Expand Down Expand Up @@ -521,14 +521,16 @@ class LiveSessionURLSessionDelegate<R: RootRegistry>: NSObject, URLSessionTaskDe

extension LiveSessionCoordinator {
static var platform: String { "swiftui" }
static var platformParams: [String:String] {
static var platformParams: [String:Any] {
[
"app_version": getAppVersion(),
"app_build": getAppBuild(),
"bundle_id": getBundleID(),
"os": getOSName(),
"os_version": getOSVersion(),
"target": getTarget()
"target": getTarget(),
"l10n": getLocalization(),
"i18n": getInternationalization()
]
}

Expand Down Expand Up @@ -607,6 +609,18 @@ extension LiveSessionCoordinator {
}
#endif
}

private static func getLocalization() -> [String:Any] {
[
"locale": Locale.autoupdatingCurrent.identifier,
]
}

private static func getInternationalization() -> [String:Any] {
[
"time_zone": TimeZone.autoupdatingCurrent.identifier,
]
}
}

fileprivate extension URL {
Expand All @@ -619,12 +633,24 @@ fileprivate extension URL {
.init(name: "_format", value: LiveSessionCoordinator<R>.platform)
])
}
for (key, value) in LiveSessionCoordinator<R>.platformParams {
let name = "_interface[\(key)]"
/// Create a nested structure of query items.
///
/// `_root[key][nested_key]=value`
func queryParameters(for object: [String:Any]) -> [(name: String, value: String?)] {
object.reduce(into: [(name: String, value: String?)]()) { (result, pair) in
if let value = pair.value as? String {
result.append((name: "[\(pair.key)]", value: value))
} else if let nested = pair.value as? [String:Any] {
result.append(contentsOf: queryParameters(for: nested).map {
return (name: "[\(pair.key)]\($0.name)", value: $0.value)
})
}
}
}
for queryItem in queryParameters(for: LiveSessionCoordinator<R>.platformParams) {
let name = "_interface\(queryItem.name)"
if !(components?.queryItems?.contains(where: { $0.name == name }) ?? false) {
result.append(queryItems: [
.init(name: name, value: value)
])
result.append(queryItems: [.init(name: name, value: queryItem.value)])
}
}
return result
Expand Down
10 changes: 6 additions & 4 deletions Sources/LiveViewNative/Coordinators/LiveSessionState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@ import Foundation
/// The live view connection state.
public enum LiveSessionState {
/// The coordinator has not yet connected to the live view.
case notConnected
case setup
/// The coordinator is attempting to connect.
case connecting
/// The coordinator is attempting to reconnect.
case reconnecting
/// The coordinator has connected and the view tree can be rendered.
case connected
// todo: disconnected state?
/// The coordinator is disconnected.
case disconnected
/// The coordinator failed to connect and produced the given error.
case connectionFailed(Error)

/// Either `notConnected` or `connecting`
/// Either `setup` or `connecting`
var isPending: Bool {
switch self {
case .notConnected,
case .setup,
.disconnected,
.connecting,
.reconnecting:
return true
Expand Down
6 changes: 3 additions & 3 deletions Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private let logger = Logger(subsystem: "LiveViewNative", category: "LiveViewCoor
/// - ``handleEvent(_:handler:)``
@MainActor
public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
@Published internal private(set) var internalState: LiveSessionState = .notConnected
@Published internal private(set) var internalState: LiveSessionState = .setup

var state: LiveSessionState {
internalState
Expand Down Expand Up @@ -283,7 +283,7 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
channel.on("phx_close") { [weak self, weak channel] message in
Task { @MainActor in
guard channel === self?.channel else { return }
self?.internalState = .notConnected
self?.internalState = .disconnected
}
}

Expand Down Expand Up @@ -320,7 +320,7 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
}
await MainActor.run { [weak self] in
self?.channel = nil
self?.internalState = .notConnected
self?.internalState = .disconnected
}
}

Expand Down
4 changes: 3 additions & 1 deletion Sources/LiveViewNative/Live/LiveView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,9 @@ public struct LiveView<
return .connecting
case let .connectionFailed(error):
return .error(error)
case .notConnected:
case .setup:
return .connecting
case .disconnected:
return .disconnected
case .reconnecting:
return .reconnecting(_ConnectedContent<R>(session: session))
Expand Down
6 changes: 4 additions & 2 deletions Sources/LiveViewNative/NavStackEntryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ struct NavStackEntryView<R: RootRegistry>: View {

private var phase: LiveViewPhase<R> {
switch coordinator.state {
case .notConnected:
return .disconnected
case .setup:
return .connecting
case .connecting:
return .connecting
case .connectionFailed(let error):
return .error(error)
case .disconnected:
return .disconnected
case .reconnecting, .connected: // these phases should always be handled internally
fatalError()
}
Expand Down
19 changes: 12 additions & 7 deletions Sources/LiveViewNative/Property Wrappers/ObservedElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ public struct ObservedElement {
}

/// A publisher that publishes when the observed element changes.
public var projectedValue: some Publisher<Void, Never> {
observer.objectWillChange
public var projectedValue: some Publisher<NodeRef, Never> {
observer.elementChangedPublisher
}

var children: [Node] { overrideElement.flatMap({ Array($0.children()) }) ?? observer.resolvedChildren }
Expand Down Expand Up @@ -127,6 +127,8 @@ extension ObservedElement {

var objectWillChange = ObjectWillChangePublisher()

var elementChangedPublisher: AnyPublisher<NodeRef, Never>!

init(_ id: NodeRef) {
self.id = id
}
Expand All @@ -141,18 +143,21 @@ extension ObservedElement {
self.resolvedChildren = Array(self.resolvedElement.children())
self._resolvedChildIDs = nil

let publisher: AnyPublisher<(), Never>
let id = self.id

if observeChildren {
publisher = Publishers.MergeMany(
[context.elementChanged(id)] + self.resolvedChildIDs.map(context.elementChanged)
self.elementChangedPublisher = Publishers.MergeMany(
[context.elementChanged(id).map({ id })] + self.resolvedChildIDs.map({ id in
context.elementChanged(id).map({ id })
})
)
.eraseToAnyPublisher()
self.observedChildIDs = self.resolvedChildIDs
} else {
publisher = context.elementChanged(id).eraseToAnyPublisher()
self.elementChangedPublisher = context.elementChanged(id).map({ id }).eraseToAnyPublisher()
}

cancellable = publisher
cancellable = self.elementChangedPublisher
.sink { [weak self] _ in
guard let self else { return }
self.resolvedElement = context.document[id].asElement()
Expand Down
23 changes: 20 additions & 3 deletions Sources/LiveViewNative/Protocols/ContentBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,19 @@ public struct ContentBuilderContext<R: RootRegistry, Builder: ContentBuilder>: D

let resolvedStylesheet: [String:[BuilderModifierContainer<Builder>]]

public var coordinator: LiveViewCoordinator<R> {
context.coordinator
}

public var url: URL {
context.url
}

@MainActor
public var document: Document? {
context.coordinator.document
}

func value<OtherBuilder: ContentBuilder>(for _: OtherBuilder.Type = OtherBuilder.self) -> ContentBuilderContext<R, OtherBuilder>.Value {
return .init(
coordinatorEnvironment: coordinatorEnvironment,
Expand All @@ -363,9 +376,13 @@ public struct ContentBuilderContext<R: RootRegistry, Builder: ContentBuilder>: D
static func resolveStylesheet(
_ stylesheet: Stylesheet<R>
) throws -> [String:[BuilderModifierContainer<Builder>]] {
return try stylesheet.content.reduce(into: [:], {
$0.merge(try StylesheetParser<BuilderModifierContainer<Builder>>(context: .init()).parse($1.utf8), uniquingKeysWith: { $1 })
})
if Builder.ModifierType.self == EmptyContentModifier<Builder>.self {
return [:]
} else {
return try stylesheet.content.reduce(into: [:], {
$0.merge(try StylesheetParser<BuilderModifierContainer<Builder>>(context: .init()).parse($1.utf8), uniquingKeysWith: { $1 })
})
}
}
}

Expand Down
Loading
Loading