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 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/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" } }, { 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..6c879103b 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,37 +112,76 @@ 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. + 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() + } + return + } else { + 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)) - ) - } else if next.count == prev.count { - guard let liveChannel = - try await self.liveSocket?.navigate(next.last!.url.absoluteString, + 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: .replace)) - else { return } - next.last?.coordinator.join(liveChannel) + ]), + NavOptions(action: .push)) + ) + } + } else if next.count == prev.count { + 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) @@ -318,7 +357,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 +413,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 +479,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 +497,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/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 ) 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