From f24ca1f6a4480c6844e8034f93b733730c877f58 Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Wed, 5 Mar 2025 03:53:08 -0500 Subject: [PATCH 1/5] Fix float parsing (#1550) --- CHANGELOG.md | 1 + .../swiftui/rules_parser/tokens.ex | 24 +++++++++---------- .../swiftui/rules_parser_test.exs | 4 ++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe49811b..42d593551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Views will now update properly when the server changes the value of a form field (#1483) +- Fixed float parsing for stylesheet rules ## [0.3.1] 2024-10-02 diff --git a/lib/live_view_native/swiftui/rules_parser/tokens.ex b/lib/live_view_native/swiftui/rules_parser/tokens.ex index 64ed52536..ae30f9212 100644 --- a/lib/live_view_native/swiftui/rules_parser/tokens.ex +++ b/lib/live_view_native/swiftui/rules_parser/tokens.ex @@ -21,32 +21,30 @@ defmodule LiveViewNative.SwiftUI.RulesParser.Tokens do def nil_(), do: replace(string("nil"), nil) - def minus(), do: string("-") - - def underscored_integer() do - integer(min: 1) + def digits() do + ascii_char([?0..?9]) |> repeat( choice([ ascii_char([?0..?9]), - ignore(string("_")) - |> ascii_char([?0..?9]) + ignore(string("_")) |> ascii_char([?0..?9]) ]) - |> reduce({List, :to_string, []}) ) - |> reduce({Enum, :join, [""]}) + |> reduce({List, :to_string, []}) + end + + def minus(), do: string("-") + + def frac() do + concat(string("."), digits()) end def integer() do optional(minus()) - |> concat(underscored_integer()) + |> concat(digits()) |> reduce({Enum, :join, [""]}) |> map({String, :to_integer, []}) end - def frac() do - concat(string("."), underscored_integer()) - end - def float() do integer() |> concat(frac()) diff --git a/test/live_view_native/swiftui/rules_parser_test.exs b/test/live_view_native/swiftui/rules_parser_test.exs index 6f8cdff6b..d285ff6dc 100644 --- a/test/live_view_native/swiftui/rules_parser_test.exs +++ b/test/live_view_native/swiftui/rules_parser_test.exs @@ -117,8 +117,8 @@ defmodule LiveViewNative.SwiftUI.RulesParserTest do assert parse(input) == output - input = "background(Color.blue.opacity(0.2).blendMode(.multiply).mix(with: .orange).opacity(0.7))" - output = {:background, [], [{:., [], [{:., [], [{:., [], [{:., [], [{:., [], [:Color, :blue]}, {:opacity, [], [0.2]}]}, {:blendMode, [], [{:., [], [nil, :multiply]}]}]}, {:mix, [], [{:with, {:., [], [nil, :orange]}}]}]}, {:opacity, [], [0.7]}]}]} + input = "background(Color.blue.opacity(0.02).blendMode(.multiply).mix(with: .orange).opacity(0.7))" + output = {:background, [], [{:., [], [{:., [], [{:., [], [{:., [], [{:., [], [:Color, :blue]}, {:opacity, [], [0.02]}]}, {:blendMode, [], [{:., [], [nil, :multiply]}]}]}, {:mix, [], [{:with, {:., [], [nil, :orange]}}]}]}, {:opacity, [], [0.7]}]}]} assert parse(input) == output end From e541b696d7b7791ff6d0fc52173b525d25e5760b Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Wed, 5 Mar 2025 03:53:47 -0500 Subject: [PATCH 2/5] Update Package.resolved --- Package.resolved | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index cd7f7fa36..27381643a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "4dd8c7cbc917ecfea85da314dc244366302002117a41d066ce90dade52deba33", + "originHash" : "a1b1e06fbdf5f90d2817b4e4ba491ff6fc177e87dad20844f45d67caa0b58193", "pins" : [ { "identity" : "liveview-native-core", "kind" : "remoteSourceControl", "location" : "https://github.com/liveview-native/liveview-native-core", "state" : { - "revision" : "7b490ee3ab76c7e83220e7255ad5b0d9d901a767", - "version" : "0.4.1-rc-1" + "revision" : "c067c8b458458c6eb01ef73f9e40e282aa79719a", + "version" : "0.4.1-rc-2" } }, { From 132c5c991527028f41ad654f94b1ea937e1c8807 Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Wed, 5 Mar 2025 03:54:38 -0500 Subject: [PATCH 3/5] Fix CI --- .github/workflows/elixir.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 2208abf6d..6c7c6ad99 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -14,7 +14,7 @@ env: jobs: build: name: Build and test - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Elixir From 16989625e3b9f62a8e5ab5b1b1cc3e4906f07421 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Thu, 6 Mar 2025 14:31:37 -0500 Subject: [PATCH 4/5] Fix live_patch handling --- .../Coordinators/LiveNavigationEntry.swift | 5 ++ .../Coordinators/LiveSessionCoordinator.swift | 82 +++++++++++++------ Sources/LiveViewNative/Live/LiveView.swift | 2 +- .../LiveViewNative/NavStackEntryView.swift | 5 ++ .../NavigationLink.swift | 1 + 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/Sources/LiveViewNative/Coordinators/LiveNavigationEntry.swift b/Sources/LiveViewNative/Coordinators/LiveNavigationEntry.swift index 446fcd1e3..0d3620de6 100644 --- a/Sources/LiveViewNative/Coordinators/LiveNavigationEntry.swift +++ b/Sources/LiveViewNative/Coordinators/LiveNavigationEntry.swift @@ -11,15 +11,20 @@ import SwiftUI public struct LiveNavigationEntry: Hashable { public let url: URL public let coordinator: LiveViewCoordinator + + let mode: LiveRedirect.Mode + let navigationTransition: Any? let pendingView: (any View)? public static func == (lhs: Self, rhs: Self) -> Bool { lhs.url == rhs.url && lhs.coordinator === rhs.coordinator + && lhs.mode == rhs.mode } public func hash(into hasher: inout Hasher) { hasher.combine(url) hasher.combine(ObjectIdentifier(coordinator)) + hasher.combine(mode) } } diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift index 2418bc2a4..6885a3058 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift @@ -100,7 +100,7 @@ public class LiveSessionCoordinator: ObservableObject { try? LiveViewNativeCore.storeSessionCookie("\(cookie.name)=\(cookie.value)", self.url.absoluteString) } - self.navigationPath = [.init(url: url, coordinator: .init(session: self, url: self.url), navigationTransition: nil, pendingView: nil)] + self.navigationPath = [.init(url: url, coordinator: .init(session: self, url: self.url), mode: .replaceTop, navigationTransition: nil, pendingView: nil)] self.mergedEventSubjects = self.navigationPath.first!.coordinator.eventSubject.compactMap({ [weak self] value in self.map({ ($0.navigationPath.first!.coordinator, value) }) @@ -112,28 +112,64 @@ public class LiveSessionCoordinator: ObservableObject { $navigationPath.scan(([LiveNavigationEntry](), [LiveNavigationEntry]()), { ($0.1, $1) }).sink { [weak self] prev, next in guard let self else { return } Task { - try await prev.last?.coordinator.disconnect() if prev.count > next.count { - let targetEntry = self.liveSocket!.getEntries()[next.count - 1] - next.last?.coordinator.join( - try await self.liveSocket!.traverseTo(targetEntry.id, - .some([ - "_format": .str(string: LiveSessionParameters.platform), - "_interface": .object(object: LiveSessionParameters.platformParams) - ]), - nil) - ) + // backward navigation + + // if the coordinator is connected, the mode was a `patch`, and the new entry has the same coordinator + // send a `live_patch` event and keep the same coordinator. + switch prev.last!.mode { + case .patch: + if case .connected = prev.last?.coordinator.state, + next.last?.coordinator === prev.last?.coordinator + { + _ = try await prev.last?.coordinator.doPushEvent( + "live_patch", + payload: .jsonPayload(json: .object(object: [ + "url": .str(string: next.last!.url.absoluteString) + ])) + ) + next.last!.coordinator.url = next.last!.url + next.last!.coordinator.objectWillChange.send() + if next.count <= 1 { // if we navigated back to the root page, trigger an update on the session too + self.objectWillChange.send() + } + return + } + case .replaceTop: + try await prev.last?.coordinator.disconnect() + let targetEntry = self.liveSocket!.getEntries()[next.count - 1] + next.last?.coordinator.join( + try await self.liveSocket!.traverseTo( + targetEntry.id, + .some([ + "_format": .str(string: LiveSessionParameters.platform), + "_interface": .object(object: LiveSessionParameters.platformParams) + ]), + nil + ) + ) + } } else if next.count > prev.count && prev.count > 0 { // forward navigation (from `redirect` or ``) - next.last?.coordinator.join( - try await self.liveSocket!.navigate(next.last!.url.absoluteString, - .some([ - "_format": .str(string: LiveSessionParameters.platform), - "_interface": .object(object: LiveSessionParameters.platformParams) - ]), - NavOptions(action: .push)) - ) + + // if the coordinator instance is the same and its connected, we don't need to handle a connection. + switch next.last!.mode { + case .patch: + next.last?.coordinator.url = next.last!.url + return + case .replaceTop: + try await prev.last?.coordinator.disconnect() + next.last?.coordinator.join( + try await self.liveSocket!.navigate(next.last!.url.absoluteString, + .some([ + "_format": .str(string: LiveSessionParameters.platform), + "_interface": .object(object: LiveSessionParameters.platformParams) + ]), + NavOptions(action: .push)) + ) + } } else if next.count == prev.count { + try await prev.last?.coordinator.disconnect() guard let liveChannel = try await self.liveSocket?.navigate(next.last!.url.absoluteString, .some([ @@ -318,7 +354,7 @@ public class LiveSessionCoordinator: ObservableObject { if case .user(user: "assets_change") = event.event { Task { @MainActor in await self.disconnect() - self.navigationPath = [.init(url: self.url, coordinator: .init(session: self, url: self.url), navigationTransition: nil, pendingView: nil)] + self.navigationPath = [.init(url: self.url, coordinator: .init(session: self, url: self.url), mode: .replaceTop, navigationTransition: nil, pendingView: nil)] await self.connect() self.lastReloadTime = Date() } @@ -374,7 +410,7 @@ public class LiveSessionCoordinator: ObservableObject { await self.disconnect() if let url { self.url = url - self.navigationPath = [.init(url: self.url, coordinator: self.navigationPath.first!.coordinator, navigationTransition: nil, pendingView: nil)] + self.navigationPath = [.init(url: self.url, coordinator: self.navigationPath.first!.coordinator, mode: .replaceTop, navigationTransition: nil, pendingView: nil)] } await self.connect(httpMethod: httpMethod, httpBody: httpBody, additionalHeaders: headers) // do { @@ -440,7 +476,7 @@ public class LiveSessionCoordinator: ObservableObject { switch redirect.mode { case .replaceTop: let coordinator = LiveViewCoordinator(session: self, url: redirect.to) - let entry = LiveNavigationEntry(url: redirect.to, coordinator: coordinator, navigationTransition: navigationTransition, pendingView: pendingView) + let entry = LiveNavigationEntry(url: redirect.to, coordinator: coordinator, mode: redirect.mode, navigationTransition: navigationTransition, pendingView: pendingView) switch redirect.kind { case .push: navigationPath.append(entry) @@ -458,7 +494,7 @@ public class LiveSessionCoordinator: ObservableObject { // patch is like `replaceTop`, but it does not disconnect. let coordinator = navigationPath.last!.coordinator coordinator.url = redirect.to - let entry = LiveNavigationEntry(url: redirect.to, coordinator: coordinator, navigationTransition: navigationTransition, pendingView: pendingView) + let entry = LiveNavigationEntry(url: redirect.to, coordinator: coordinator, mode: redirect.mode, navigationTransition: navigationTransition, pendingView: pendingView) switch redirect.kind { case .push: navigationPath.append(entry) diff --git a/Sources/LiveViewNative/Live/LiveView.swift b/Sources/LiveViewNative/Live/LiveView.swift index df9601eb9..077fd3186 100644 --- a/Sources/LiveViewNative/Live/LiveView.swift +++ b/Sources/LiveViewNative/Live/LiveView.swift @@ -288,7 +288,7 @@ struct PhxMain: View { @EnvironmentObject private var session: LiveSessionCoordinator var body: some View { - NavStackEntryView(.init(url: context.coordinator.url, coordinator: context.coordinator, navigationTransition: nil, pendingView: nil)) + NavStackEntryView(.init(url: context.coordinator.url, coordinator: context.coordinator, mode: .replaceTop, navigationTransition: nil, pendingView: nil)) } } diff --git a/Sources/LiveViewNative/NavStackEntryView.swift b/Sources/LiveViewNative/NavStackEntryView.swift index 53e3fc804..e6dc0c8e7 100644 --- a/Sources/LiveViewNative/NavStackEntryView.swift +++ b/Sources/LiveViewNative/NavStackEntryView.swift @@ -70,6 +70,11 @@ struct NavStackEntryView: View { .transition(coordinator.session.configuration.transition ?? .identity) } } + } else { + SwiftUI.ZStack { + SwiftUI.Rectangle().fill(.red) + SwiftUI.Text("\(coordinator.url) != \(entry.url)") + } } } .animation(coordinator.session.configuration.transition.map({ _ in .default }), value: coordinator.state.isConnected) diff --git a/Sources/LiveViewNative/Views/Layout Containers/Presentation Containers/NavigationLink.swift b/Sources/LiveViewNative/Views/Layout Containers/Presentation Containers/NavigationLink.swift index 905e9382d..27c23f234 100644 --- a/Sources/LiveViewNative/Views/Layout Containers/Presentation Containers/NavigationLink.swift +++ b/Sources/LiveViewNative/Views/Layout Containers/Presentation Containers/NavigationLink.swift @@ -123,6 +123,7 @@ struct NavigationLink: View { value: LiveNavigationEntry( url: url, coordinator: LiveViewCoordinator(session: $liveElement.context.coordinator.session, url: url), + mode: .replaceTop, navigationTransition: nil, // FIXME: navigationTransition pendingView: pendingView ) From b5b51214dabc3b236f9f077fc00c665a5cc5d6c0 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Thu, 6 Mar 2025 14:44:58 -0500 Subject: [PATCH 5/5] Fix live_patch navigation with `replace: true` and between regular routes --- .../Coordinators/LiveSessionCoordinator.swift | 63 ++++++++++--------- .../LiveViewNative/NavStackEntryView.swift | 5 -- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift index 6885a3058..6c879103b 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift @@ -117,25 +117,23 @@ public class LiveSessionCoordinator: ObservableObject { // if the coordinator is connected, the mode was a `patch`, and the new entry has the same coordinator // send a `live_patch` event and keep the same coordinator. - switch prev.last!.mode { - case .patch: - if case .connected = prev.last?.coordinator.state, - next.last?.coordinator === prev.last?.coordinator - { - _ = try await prev.last?.coordinator.doPushEvent( - "live_patch", - payload: .jsonPayload(json: .object(object: [ - "url": .str(string: next.last!.url.absoluteString) - ])) - ) - next.last!.coordinator.url = next.last!.url - next.last!.coordinator.objectWillChange.send() - if next.count <= 1 { // if we navigated back to the root page, trigger an update on the session too - self.objectWillChange.send() - } - return + if case .patch = prev.last!.mode, + case .connected = prev.last?.coordinator.state, + next.last?.coordinator === prev.last?.coordinator + { + _ = try await prev.last?.coordinator.doPushEvent( + "live_patch", + payload: .jsonPayload(json: .object(object: [ + "url": .str(string: next.last!.url.absoluteString) + ])) + ) + next.last!.coordinator.url = next.last!.url + next.last!.coordinator.objectWillChange.send() + if next.count <= 1 { // if we navigated back to the root page, trigger an update on the session too + self.objectWillChange.send() } - case .replaceTop: + return + } else { try await prev.last?.coordinator.disconnect() let targetEntry = self.liveSocket!.getEntries()[next.count - 1] next.last?.coordinator.join( @@ -151,8 +149,6 @@ public class LiveSessionCoordinator: ObservableObject { } } else if next.count > prev.count && prev.count > 0 { // forward navigation (from `redirect` or ``) - - // if the coordinator instance is the same and its connected, we don't need to handle a connection. switch next.last!.mode { case .patch: next.last?.coordinator.url = next.last!.url @@ -169,16 +165,23 @@ public class LiveSessionCoordinator: ObservableObject { ) } } else if next.count == prev.count { - try await prev.last?.coordinator.disconnect() - guard let liveChannel = - try await self.liveSocket?.navigate(next.last!.url.absoluteString, - .some([ - "_format": .str(string: LiveSessionParameters.platform), - "_interface": .object(object: LiveSessionParameters.platformParams) - ]), - NavOptions(action: .replace)) - else { return } - next.last?.coordinator.join(liveChannel) + switch next.last!.mode { + case .patch: + next.last?.coordinator.url = next.last!.url + return + case .replaceTop: + try await prev.last?.coordinator.disconnect() + guard let liveChannel = try await self.liveSocket?.navigate( + next.last!.url.absoluteString, + .some([ + "_format": .str(string: LiveSessionParameters.platform), + "_interface": .object(object: LiveSessionParameters.platformParams) + ]), + NavOptions(action: .replace) + ) + else { return } + next.last?.coordinator.join(liveChannel) + } } } }.store(in: &cancellables) diff --git a/Sources/LiveViewNative/NavStackEntryView.swift b/Sources/LiveViewNative/NavStackEntryView.swift index e6dc0c8e7..53e3fc804 100644 --- a/Sources/LiveViewNative/NavStackEntryView.swift +++ b/Sources/LiveViewNative/NavStackEntryView.swift @@ -70,11 +70,6 @@ struct NavStackEntryView: View { .transition(coordinator.session.configuration.transition ?? .identity) } } - } else { - SwiftUI.ZStack { - SwiftUI.Rectangle().fill(.red) - SwiftUI.Text("\(coordinator.url) != \(entry.url)") - } } } .animation(coordinator.session.configuration.transition.map({ _ in .default }), value: coordinator.state.isConnected)