From a772d33e96094aeca733f0956c2610bce5c10a8f Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Tue, 30 Apr 2024 13:20:35 -0400 Subject: [PATCH 01/62] Introduce v0.3.0 livebook guides --- .../create-a-swiftui-application.md | 213 ++++ .../ex_doc_notebooks/forms-and-validation.md | 640 ++++++++++++ guides/ex_doc_notebooks/getting-started.md | 87 ++ .../interactive-swiftui-views.md | 756 ++++++++++++++ guides/ex_doc_notebooks/native-navigation.md | 303 ++++++ guides/ex_doc_notebooks/stylesheets.md | 538 ++++++++++ guides/ex_doc_notebooks/swiftui-views.md | 693 +++++++++++++ .../create-a-swiftui-application.livemd | 276 +++++ guides/livebooks/forms-and-validation.livemd | 661 ++++++++++++ guides/livebooks/getting-started.livemd | 149 +++ .../interactive-swiftui-views.livemd | 947 ++++++++++++++++++ guides/livebooks/native-navigation.livemd | 410 ++++++++ guides/livebooks/stylesheets.livemd | 659 ++++++++++++ guides/livebooks/swiftui-views.livemd | 937 +++++++++++++++++ .../create-a-swiftui-application.md | 213 ++++ .../forms-and-validation.md | 640 ++++++++++++ guides/markdown_livebooks/getting-started.md | 87 ++ .../interactive-swiftui-views.md | 756 ++++++++++++++ .../markdown_livebooks/native-navigation.md | 303 ++++++ guides/markdown_livebooks/stylesheets.md | 538 ++++++++++ guides/markdown_livebooks/swiftui-views.md | 693 +++++++++++++ lib/mix/tasks/livebooks_to_markdown.ex | 48 + mix.exs | 57 +- test/mix/tasks/livebooks_to_markdown_test.exs | 129 +++ 24 files changed, 10717 insertions(+), 16 deletions(-) create mode 100644 guides/ex_doc_notebooks/create-a-swiftui-application.md create mode 100644 guides/ex_doc_notebooks/forms-and-validation.md create mode 100644 guides/ex_doc_notebooks/getting-started.md create mode 100644 guides/ex_doc_notebooks/interactive-swiftui-views.md create mode 100644 guides/ex_doc_notebooks/native-navigation.md create mode 100644 guides/ex_doc_notebooks/stylesheets.md create mode 100644 guides/ex_doc_notebooks/swiftui-views.md create mode 100644 guides/livebooks/create-a-swiftui-application.livemd create mode 100644 guides/livebooks/forms-and-validation.livemd create mode 100644 guides/livebooks/getting-started.livemd create mode 100644 guides/livebooks/interactive-swiftui-views.livemd create mode 100644 guides/livebooks/native-navigation.livemd create mode 100644 guides/livebooks/stylesheets.livemd create mode 100644 guides/livebooks/swiftui-views.livemd create mode 100644 guides/markdown_livebooks/create-a-swiftui-application.md create mode 100644 guides/markdown_livebooks/forms-and-validation.md create mode 100644 guides/markdown_livebooks/getting-started.md create mode 100644 guides/markdown_livebooks/interactive-swiftui-views.md create mode 100644 guides/markdown_livebooks/native-navigation.md create mode 100644 guides/markdown_livebooks/stylesheets.md create mode 100644 guides/markdown_livebooks/swiftui-views.md create mode 100644 lib/mix/tasks/livebooks_to_markdown.ex create mode 100644 test/mix/tasks/livebooks_to_markdown_test.exs diff --git a/guides/ex_doc_notebooks/create-a-swiftui-application.md b/guides/ex_doc_notebooks/create-a-swiftui-application.md new file mode 100644 index 000000000..134e8af89 --- /dev/null +++ b/guides/ex_doc_notebooks/create-a-swiftui-application.md @@ -0,0 +1,213 @@ + + +# Create a SwiftUI Application + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%create-a-swiftui-application.livemd) + +## Overview + +This guide will teach you how to set up a SwiftUI Application for LiveView Native. + +Typically, we recommend using the `mix lvn.install` task as described in the [Installation Guide](https://hexdocs.pm/live_view_native/installation.html#5-enable-liveview-native) to add LiveView Native to a Phoenix project. However, we will walk through the steps of manually setting up an Xcode iOS project to learn how the iOS side of a LiveView Native application works. + +In future lessons, you'll use this iOS application to view iOS examples in the Xcode simulator (or a physical device if you prefer.) + +## Prerequisites + +First, make sure you have followed the [Getting Started](https://hexdocs.pm/live_view_native/getting_started.md) guide. Then evaluate the smart cell below and visit http://localhost:4000 to ensure the Phoenix server runs properly. You should see the text `Hello from LiveView!` + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +## Create the iOS Application + +Open Xcode and select Create New Project. + + + +![Xcode Create New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-create-new-project.png?raw=true) + + + +Select the `iOS` and `App` options to create an iOS application. Then click `Next`. + + + +![Xcode Create Template For New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-create-template-for-new-project.png?raw=true) + + + +Choose options for your new project that match the following image, then click `Next`. + +### What do these options mean? + +* **Product Name:** The name of the application. This can be any valid name. We've chosen `Guides`. +* **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recomends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using `com.example.your_name` where `your_name` is your organization or personal name. +* **Interface:**: The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. +* **Language:** Determines which language Xcode should use for the project. Select `Swift`. + + + + +![Xcode Choose Options For Your New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-choose-options-for-your-new-project.png?raw=true) + + + +Select an appropriate folder location where you would like to store the iOS project, then click `Create`. + + + +![Xcode select folder location](https://github.com/liveview-native/documentation_assets/blob/main/xcode-select-folder-location.png?raw=true) + + + +You should see the default iOS application generated by Xcode. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/default-xcode-app.png?raw=true) + +## Add the LiveView Client SwiftUI Package + +In Xcode from the project you just created, select `File -> Add Package Dependencies`. Then, search for `liveview-client-swiftui`. Once you have selected the package, click `Add Package`. + +The image below was created using version `0.2.0`. You should select whichever is the latest version of LiveView Native. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/add-liveview-swiftui-client-package-0.2.0.png?raw=true) + + + +Choose the Package Products for `liveview-client-swiftui`. Select `Guides` as the target for `LiveViewNative` and `LiveViewNativeStylesheet`. This adds both of these dependencies to your iOS project. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/select-package-products.png?raw=true) + + + +At this point, you'll need to enable permissions for plugins used by LiveView Native. +You should see the following prompt. Click `Trust & Enable All`. + + + +![Xcode some build plugins are disabled](https://github.com/liveview-native/documentation_assets/blob/main/xcode-some-build-plugins-are-disabled.png?raw=true) + + + +You'll also need to manually navigate to the error tab (shown below) and manually trust and enable packages. Click on each error to trigger a prompt. Select `Trust & Enable All` to enable the plugin. + +The specific plugins are subject to change. At the time of writing you need to enable `LiveViewNativeStylesheetMacros`, `LiveViewNativeMacros`, and `CasePathMacros` as shown in the images below. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-liveview-native-stylesheet.png?raw=true) + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-liveview-native-macros.png?raw=true) + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-case-path-macros.png?raw=true) + +## Setup the SwiftUI LiveView + +The [ContentView](https://developer.apple.com/tutorials/swiftui-concepts/exploring-the-structure-of-a-swiftui-app#Content-view) contains the main view of our iOS application. + +Replace the code in the `ContentView` file with the following to connect the SwiftUI application and the Phoenix application. + + + +```swift +import SwiftUI +import LiveViewNative + +struct ContentView: View { + + var body: some View { + LiveView(.automatic( + development: .localhost(path: "/"), + production: .custom(URL(string: "https://example.com/")!) + )) + } +} + + +// Optionally preview the native UI in Xcode +#Preview { + ContentView() +} +``` + + + +The code above sets up the SwiftUI LiveView. By default, the SwiftUI LiveView connects to any Phoenix app running on http://localhost:4000. + + + + + +```mermaid +graph LR; + subgraph I[iOS App] + direction TB + ContentView + SL[SwiftUI LiveView] + end + subgraph P[Phoenix App] + LiveView + end + SL --> P + ContentView --> SL + + +``` + +## Start the Active Scheme + +Click the `start active scheme` button to build the project and run it on the iOS simulator. + +> A [build scheme](https://developer.apple.com/documentation/xcode/build-system) contains a list of targets to build, and any configuration and environment details that affect the selected action. For example, when you build and run an app, the scheme tells Xcode what launch arguments to pass to the app. +> +> * https://developer.apple.com/documentation/xcode/build-system + +After you start the active scheme, the simulator should open the iOS application and display `Hello from LiveView Native!`. If you encounter any issues see the **Troubleshooting** section below. + + + +
+ +
+ +## Troubleshooting + +If you encountered any issues with the native application, here are some troubleshooting steps you can use: + +* **Reset Package Caches:** In the Xcode application go to `File -> Packages -> Reset Package Caches`. +* **Update Packages:** In the Xcode application go to `File -> Packages -> Update to Latest Package Versions`. +* **Rebuild the Active Scheme**: In the Xcode application, press the `start active scheme` button to rebuild the active scheme and run it on the Xcode simulator. +* Update your [Xcode](https://developer.apple.com/xcode/) version if it is not already the latest version +* Check for error messages in the Livebook smart cells. + +You can also [raise an issue](https://github.com/liveview-native/live_view_native/issues/new) if you would like support from the LiveView Native team. diff --git a/guides/ex_doc_notebooks/forms-and-validation.md b/guides/ex_doc_notebooks/forms-and-validation.md new file mode 100644 index 000000000..c6db3323f --- /dev/null +++ b/guides/ex_doc_notebooks/forms-and-validation.md @@ -0,0 +1,640 @@ +# Forms and Validation + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%forms-and-validation.livemd) + +## Overview + +The [LiveView Native Live Form](https://github.com/liveview-native/liveview-native-live-form) project makes it easier to build forms in LiveView Native. This project enables you to group different [Control Views](https://developer.apple.com/documentation/swiftui/controls-and-indicators) inside of a `LiveForm` and control them collectively under a single `phx-change` or `phx-submit` event handler, rather than with multiple different `phx-change` event handlers. + +Getting the most out of this material requires some understanding of the [Ecto](https://hexdocs.pm/ecto/Ecto.html) project and in particular a reasonably deep understanding of [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html). Review the linked Ecto documentation if you find any of the examples difficult to follow. + +## Installing LiveView Native Live Form + +To install LiveView Native Form, we need to add the `liveview-native-live-form` SwiftUI package to our iOS application. + +Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README and come back to this guide after you have finished the installation process. + +## Creating a Basic Form + +Once you have the LiveView Native Form package installed, you can use the `LiveForm` and `LiveSubmitButton` views to build forms more conveniently. + +Here's a basic example of a `LiveForm`. Keep in mind that `LiveForm` requires an `id` attribute. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.ExampleLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Placeholder + Submit + + """ + end + + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +When a form is submitted, its data is sent as a map where each key is the 'name' attribute of the form's control views. Evaluate the example above in your simulator and you will see a map similar to the following: + + + +```elixir +%{"my-text" => "some value"} +``` + +In a real-world application you could use these params to trigger some application logic, such as inserting a record into the database. + +## Controls and Indicators + +We've already covered many individual controls and indicator views that you can use inside of forms. For more information on those, go to the [Interactive SwiftUI Views](https://hexdocs.pm/live_view_native/interactive-swiftui-views.html) guide. + + + +### Your Turn + +Create a form that has `TextField`, `Slider`, `Toggle`, and `DatePicker` fields. + +### Example Solution + +```elixir +defmodule Server.MultiInputFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Placeholder + + + + Submit + + """ + end + + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +``` + + + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.MultiInputFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + """ + end + + # You may use this handler to test your solution. + # You should not need to modify this handler. + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +### Controlled Values + +Some control views such as the `Stepper` require manually displaying their value. In this case, we can store the form params in the socket and update them everytime the `phx-change` form binding submits an event. You can also use this pattern to provide default values. + +Evaluate the example below to see this in action. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.StepperLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, params: %{"my-stepper" => 1})} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + <%= @params["my-stepper"] %> + + """ + end + + @impl true + def handle_event("change", params, socket) do + IO.inspect(params) + {:noreply, assign(socket, params: params)} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +### Secure Field + +For password entry, or anytime you want to hide a given value, you can use the [SecureField](https://developer.apple.com/documentation/swiftui/securefield) view. This field works mostly the same as a `TextField` but hides the visual text. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.SecureLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + Enter a Password + """ + end + + @impl true + def handle_event("change", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +## Keyboard Types + +To format a `TextField` for specific input types we can use the [keyboardType](https://developer.apple.com/documentation/swiftui/view/keyboardtype(_:)) modifier. + +For a complete list of accepted keyboard types, see the [UIKeyboardType](https://developer.apple.com/documentation/uikit/uikeyboardtype) documentation. + +Below we've created several different common keyboard types. We've also included a generic `keyboard-*` to demonstrate how you can make a reusable class. + +```elixir +defmodule KeyboardStylesheet do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "number-pad" do + keyboardType(.numberPad) + end + + "email-address" do + keyboardType(.emailAddress) + end + + "phone-pad" do + keyboardType(.phonePad) + end + + "keyboard-" <> type do + keyboardType(to_ime(type)) + end + """ +end +``` + +Evaluate the example below to see the different keyboards as you focus on each input. If you don't see the keyboard, go to `I/O` -> `Keyboard` -> `Toggle Software Keyboard` to enable the software keyboard in your simulator. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.KeyboardLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use KeyboardStylesheet + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + Enter Email + Enter Phone + Enter Number + Enter Number + """ + end + + def render(assigns) do + ~H""" +

Hello from LiveView!

+ """ + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +## Validation + +In this section, we'll focus mainly on using [Ecto Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) to validate data, but know that this is not the only way to validate data if you would like to write your own custom logic in the form event handlers, you absolutely can. + + + +### LiveView Native Changesets Coming Soon! + +LiveView Native Form doesn't currently natively support [Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) and [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) structs the way a traditional [Phoenix.Component.form](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1) does. However there is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/5) to add this behavior so this may change in the near future. As a result, this section is somewhat more verbose than will be necessary in the future, as we have to manually define much of the error handling logic that we expect will no longer be necessary in version `0.3` of LiveView Native. + +To make error handling easier, we've defined an `ErrorUtils` module below that will handle extracting the error message out of a Changeset. This will not be necessary in future versions of LiveView Native, but is a convenient helper for now. + +```elixir +defmodule ErrorUtils do + def error_message(errors, field) do + with {msg, opts} <- errors[field] do + Server.CoreComponents.translate_error({msg, opts}) + else + _ -> "" + end + end +end +``` + +For the sake of context, the `translate_message/2` function handles formatting Ecto Changeset errors. For example, it will inject values such as `count` into the string. + +```elixir +Server.CoreComponents.translate_error( + {"name must be longer than %{count} characters", [count: 10]} +) +``` + +### Changesets + +Here's a `User` changeset we're going to use to validate a `User` struct's `email` field. + +```elixir +defmodule User do + import Ecto.Changeset + defstruct [:email] + @types %{email: :string} + + def changeset(user, params) do + {user, @types} + |> cast(params, [:email]) + |> validate_required([:email]) + |> validate_format(:email, ~r/@/) + end +end +``` + +We're going to define an `error` class so errors will appear red and be left-aligned. + +```elixir +defmodule ErrorStylesheet do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "error" do + foregroundStyle(.red) + frame(maxWidth: .infinity, alignment: .leading) + end + """ +end +``` + +Then, we're going to create a LiveView that uses the `User` changeset to validate data. + +Evaluate the example below and view it in your simulator. We've included and `IO.inspect/2` call to view the changeset after submitting the form. Try submitting the form with different values to understand how those values affect the changeset. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.FormValidationLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use ErrorStylesheet + + @impl true + def mount(_params, _session, socket) do + user_changeset = User.changeset(%User{}, %{}) + {:ok, assign(socket, :user_changeset, user_changeset)} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Enter your email + + <%= ErrorUtils.error_message(@user_changeset.errors, :email) %> + + Submit + + """ + end + + @impl true + def handle_event("validate", params, socket) do + user_changeset = + User.changeset(%User{}, params) + # Preserve the `:action` field so errors do not vanish. + |> Map.put(:action, socket.assigns.user_changeset.action) + + {:noreply, assign(socket, :user_changeset, user_changeset)} + end + + def handle_event("submit", params, socket) do + user_changeset = + User.changeset(%User{}, params) + # faking a Database insert action + |> Map.put(:action, :insert) + # Submit the form and inspect the logs below to view the changeset. + |> IO.inspect(label: "Form Field Values") + + {:noreply, assign(socket, :user_changeset, user_changeset)} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +In the code above, the `"sumbit"` and `"validate"` events update the changeset based on the current form params. This fills the `errors` field used by the `ErrorUtils` module to format the error message. + +After submitting the form, the `:action` field of the changeset has a value of `:insert`, so the red Text appears using the `:if` conditional display logic. + +In the future, this complexity will likely be handled by the `live_view_native_form` library, but for now this example exists to show you how to write your own error handling based on changesets if needed. + + + +### Empty Fields Send `"null"`. + +If you submit a form with empty fields, those fields may currently send `"null"`. There is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/6) to fix this bug, but it may affect your form behavior for now and require a temporary workaround until the issue is fixed. + +## Mini Project: User Form + +Taking everything you've learned, you're going to create a more complex user form with data validation and error displaying. We've defined a `FormStylesheet` you can use (and modify) if you would like to style your form. + +```elixir +defmodule FormStylesheet do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "error" do + foregroundStyle(.red) + frame(maxWidth: .infinity, alignment: .leading) + end + + "keyboard-" <> type do + keyboardType(to_ime(type)) + end + """ +end +``` + +### User Changeset + +First, create a `CustomUser` changeset below that handles data validation. + +**Requirements** + +* A user should have a `name` field +* A user should have a `password` string field of 10 or more characters. Note that for simplicity we are not hashing the password or following real security practices since our pretend application doesn't have a database. In real-world apps passwords should **never** be stored as a simple string, they should be encrypted. +* A user should have an `age` number field greater than `0` and less than `200`. +* A user should have an `email` field which matches an email format (including `@` is sufficient). +* A user should have a `accepted_terms` field which must be true. +* A user should have a `birthdate` field which is a date. +* All fields should be required + +### Example Solution + +```elixir +defmodule CustomUser do + import Ecto.Changeset + defstruct [:name, :password, :age, :email, :accepted_terms, :birthdate] + + @types %{ + name: :string, + password: :string, + age: :integer, + email: :string, + accepted_terms: :boolean, + birthdate: :date + } + + def changeset(user, params) do + {user, @types} + |> cast(params, Map.keys(@types)) + |> validate_required(Map.keys(@types)) + |> validate_length(:password, min: 10) + |> validate_number(:age, greater_than: 0, less_than: 200) + |> validate_acceptance(:accepted_terms) + end + + def error_message(changeset, field) do + with {msg, _reason} <- changeset.errors[field] do + msg + else + _ -> "" + end + end +end +``` + + + +```elixir +defmodule CustomUser do + # define the struct keys + defstruct [] + + # define the types + @types %{} + + def changeset(user, params) do + # Enter your solution + end +end +``` + +### LiveView + +Next, create the `CustomUserFormLive` Live View that lets the user enter their information and displays errors for invalid information upon form submission. + +**Requirements** + +* The `name` field should be a `TextField`. +* The `email` field should be a `TextField`. +* The `password` field should be a `SecureField`. +* The `age` field should be a `TextField` with a `.numberPad` keyboard or a `Slider`. +* The `accepted_terms` field should be a `Toggle`. +* The `birthdate` field should be a `DatePicker`. + +### Example Solution + +```elixir +defmodule Server.CustomUserFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use FormStylesheet + + @impl true + def mount(_params, _session, socket) do + changeset = CustomUser.changeset(%CustomUser{}, %{}) + + {:ok, assign(socket, :changeset, changeset)} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + name... + <.form_error changeset={@changeset} field={:name}/> + + email... + <.form_error changeset={@changeset} field={:email}/> + + age... + <.form_error changeset={@changeset} field={:age}/> + + password... + <.form_error changeset={@changeset} field={:password}/> + + Accept the Terms and Conditions: + <.form_error changeset={@changeset} field={:accepted_terms}/> + + Birthday: + <.form_error changeset={@changeset} field={:birthdate}/> + Submit + + """ + end + + @impl true + def handle_event("validate", params, socket) do + user_changeset = + CustomUser.changeset(%CustomUser{}, params) + |> Map.put(:action, socket.assigns.changeset.action) + + {:noreply, assign(socket, :changeset, user_changeset)} + end + + def handle_event("submit", params, socket) do + user_changeset = + CustomUser.changeset(%CustomUser{}, params) + |> Map.put(:action, :insert) + + {:noreply, assign(socket, :changeset, user_changeset)} + end + + # While not strictly required, the form_error component reduces code bloat. + def form_error(assigns) do + ~SWIFTUI""" + + <%= CustomUser.error_message(@changeset, @field) %> + + """ + end +end +``` + + + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.CustomUserFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use FormStylesheet + + @impl true + def mount(_params, _session, socket) do + # Remember to provide the initial changeset + {:ok, socket} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + """ + end + + @impl true + # Write your `"validate"` event handler + def handle_event("validate", params, socket) do + {:noreply, socket} + end + + # Write your `"submit"` event handler + def handle_event("submit", params, socket) do + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` diff --git a/guides/ex_doc_notebooks/getting-started.md b/guides/ex_doc_notebooks/getting-started.md new file mode 100644 index 000000000..63e640e5e --- /dev/null +++ b/guides/ex_doc_notebooks/getting-started.md @@ -0,0 +1,87 @@ +# Getting Started + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%getting-started.livemd) + +## Overview + +Our livebook guides provide step-by-step lessons to help you learn LiveView Native using Livebook. These guides assume that you already have some familiarity with Phoenix LiveView applications. + +You can read these guides online, or for the best experience we recommend you click on the "Run in Livebook" badge to import and run these guides locally with Livebook. + +Each guide can be completed independently, but we suggest following them chronologically for the most comprehensive learning experience. + +## Prerequisites + +To use these guides, you'll need to install the following prerequisites: + +* [Elixir/Erlang](https://elixir-lang.org/install.html) +* [Livebook](https://livebook.dev/) +* [Xcode](https://developer.apple.com/xcode/) + +While not necessary for our guides, we also recommend you install the following for general LiveView Native development: + +* [Phoenix](https://hexdocs.pm/phoenix/installation.html) +* [PostgreSQL](https://www.postgresql.org/download/) +* [LiveView Native VS Code Extension](https://github.com/liveview-native/liveview-native-vscode) + +## Hello World + +If you are not already running this guide in Livebook, click on the "Run in Livebook" badge at the top of this page to import this guide into Livebook. + +Then, you can evaluate the following smart cell and visit http://localhost:4000 to ensure this Livebook works correctly. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns) do + ~H""" +

Hello from LiveView!

+ """ + end +end +``` + +In an upcoming lesson, you'll set up an iOS application with Xcode so you can run code native examples. + +## Your Turn: Live Reloading + +Change `Hello from LiveView!` to `Hello again from LiveView!` in the above LiveView. Re-evaluate the cell and notice the application live reloads and automatically updates in the browser. + +## Kino LiveView Native + +To run a Phoenix Server setup with LiveView Native from within Livebook we built the [Kino LiveView Native](https://github.com/liveview-native/kino_live_view_native) library. + +Whenever you run one of our Livebooks, a server starts on localhost:4000. Ensure you have no other servers running on port 4000 + +Kino LiveView Native defines the **LiveView Native: LiveView** and **LiveViewNative: Render Component** smart cells within these guides. + +## Troubleshooting + +Some common issues you may encounter are: + +* Another server is already running on port 4000. +* Your version of Livebook needs to be updated. +* Your version of Elixir/Erlang needs to be updated. +* Your version of Xcode needs to be updated. +* This Livebook has cached outdated versions of dependencies + +Ensure you have the latest versions of all necessary software installed, and ensure no other servers are running on port 4000. + +To clear the cache, you can click the `Setup without cache` button revealed by clicking the dropdown next to the `setup` button at the top of the Livebook. + +If that does not resolve the issue, you can [raise an issue](https://github.com/liveview-native/live_view_native/issues/new) to receive support from the LiveView Native team. diff --git a/guides/ex_doc_notebooks/interactive-swiftui-views.md b/guides/ex_doc_notebooks/interactive-swiftui-views.md new file mode 100644 index 000000000..4f5ddc2bb --- /dev/null +++ b/guides/ex_doc_notebooks/interactive-swiftui-views.md @@ -0,0 +1,756 @@ +# Interactive SwiftUI Views + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%interactive-swiftui-views.livemd) + +## Overview + +In this guide, you'll learn how to build interactive LiveView Native applications using event bindings. + +This guide assumes some existing familiarity with [Phoenix Bindings](https://hexdocs.pm/phoenix_live_view/bindings.html) and how to set/access state stored in the LiveView's socket assigns. To get the most out of this material, you should already understand the `assign/3`/`assign/2` function, and how event bindings such as `phx-click` interact with the `handle_event/3` callback function. + +We'll use the following LiveView and define new render component examples throughout the guide. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +## Event Bindings + +We can bind any available `phx-*` [Phoenix Binding](https://hexdocs.pm/phoenix_live_view/bindings.html) to a SwiftUI Element. However certain events are not available on native. + +LiveView Native currently supports the following events on all SwiftUI views: + +* `phx-window-focus`: Fired when the application window gains focus, indicating user interaction with the Native app. +* `phx-window-blur`: Fired when the application window loses focus, indicating the user's switch to other apps or screens. +* `phx-focus`: Fired when a specific native UI element gains focus, often used for input fields. +* `phx-blur`: Fired when a specific native UI element loses focus, commonly used with input fields. +* `phx-click`: Fired when a user taps on a native UI element, enabling a response to tap events. + +> The above events work on all SwiftUI views. Some events are only available on specific views. For example, `phx-change` is available on controls and `phx-throttle/phx-debounce` is available on views with events. + +There is also a [Pull Request](https://github.com/liveview-native/liveview-client-swiftui/issues/1095) to add Key Events which may have been merged since this guide was published. + +## Basic Click Example + +The `phx-click` event triggers a corresponding [handle_event/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_event/3) callback function whenever a SwiftUI view is pressed. + +In the example below, the client sends a `"ping"` event to the server, and trigger's the LiveView's `"ping"` event handler. + +Evaluate the example below, then click the `"Click me!"` button. Notice `"Pong"` printed in the server logs below. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("ping", _params, socket) do + IO.puts("Pong") + {:noreply, socket} + end +end +``` + +### Click Events Updating State + +Event handlers in LiveView can update the LiveView's state in the socket. + +Evaluate the cell below to see an example of incrementing a count. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :count, 0)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("increment", _params, socket) do + {:noreply, assign(socket, :count, socket.assigns.count + 1)} + end +end +``` + +### Your Turn: Decrement Counter + +You're going to take the example above, and create a counter that can **both increment and decrement**. + +There should be two buttons, each with a `phx-click` binding. One button should bind the `"decrement"` event, and the other button should bind the `"increment"` event. Each event should have a corresponding handler defined using the `handle_event/3` callback function. + +### Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + <%= @count %> + + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :count, 0)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("increment", _params, socket) do + {:noreply, assign(socket, :count, socket.assigns.count + 1)} + end + + def handle_event("decrement", _params, socket) do + {:noreply, assign(socket, :count, socket.assigns.count - 1)} + end +end +``` + + + + + +### Enter Your Solution Below + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + <%= @count %> + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :count, 0)} + end + + @impl true + def render(assigns), do: ~H"" +end +``` + +## Selectable Lists + +`List` views support selecting items within the list based on their id. To select an item, provide the `selection` attribute with the item's id. + +Pressing a child item in the `List` on a native device triggers the `phx-change` event. In the example below we've bound the `phx-change` event to send the `"selection-changed"` event. This event is then handled by the `handle_event/3` callback function and used to change the selected item. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + Item <%= i %> + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, selection: "None")} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("selection-changed", %{"selection" => selection}, socket) do + {:noreply, assign(socket, selection: selection)} + end +end +``` + +## Expandable Lists + +`List` views support hierarchical content using the [DisclosureGroup](https://developer.apple.com/documentation/swiftui/disclosuregroup) view. Nest `DisclosureGroup` views within a list to create multiple levels of content as seen in the example below. + +To control a `DisclosureGroup` view, use the `is-expanded` boolean attribute as seen in the example below. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + Level 1 + Item 1 + Item 2 + Item 3 + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :is_expanded, false)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("toggle", %{"is-expanded" => is_expanded}, socket) do + {:noreply, assign(socket, is_expanded: !is_expanded)} + end +end +``` + +### Multiple Expandable Lists + +The next example shows one pattern for displaying multiple expandable lists without needing to write multiple event handlers. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + Level 1 + Item 1 + + Level 2 + Item 2 + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :expanded_groups, %{1 => false, 2 => false})} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("toggle-" <> level, %{"is-expanded" => is_expanded}, socket) do + level = String.to_integer(level) + + {:noreply, + assign( + socket, + :expanded_groups, + Map.replace!(socket.assigns.expanded_groups, level, !is_expanded) + )} + end +end +``` + +## Controls and Indicators + +In Phoenix, the `phx-change` event must be applied to a parent form. However in SwiftUI there is no similar concept of forms. Instead, SwiftUI provides [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) views. We can apply the `phx-change` binding to any of these views. + +Once bound, the SwiftUI view will send a message to the LiveView anytime the control or indicator changes its value. + +The params of the message are based on the name of the [Binding](https://developer.apple.com/documentation/swiftui/binding) argument of the view's initializer in SwiftUI. + + + +### Event Value Bindings + +Many views use the `value` binding argument, so event params are generally sent as `%{"value" => value}`. However, certain views such as `TextField` and `Toggle` deviate from this pattern because SwiftUI uses a different `value` binding argument. For example, the `TextField` view uses `text` to bind its value, so it sends the event params as `%{"text" => value}`. + +When in doubt, you can connect the event handler and inspect the params to confirm the shape of map. + +## Text Field + +The following example shows you how to connect a SwiftUI [TextField](https://developer.apple.com/documentation/swiftui/textfield) with a `phx-change` event binding to a corresponding event handler. + +Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the server logs in the console below as a map of `%{"text" => value}`. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Enter text here + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("type", params, socket) do + IO.inspect(params, label: "params") + {:noreply, socket} + end +end +``` + +### Storing TextField Values in the Socket + +The following example demonstrates how to set/access a TextField's value by controlling it using the socket assigns. + +This pattern is useful when rendering the TextField's value elsewhere on the page, using the `TextField` view's value in other event handler logic, or to set an initial value. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Enter text here + + The current value: <%= @text %> + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :text, "initial value")} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("type", %{"text" => text}, socket) do + {:noreply, assign(socket, :text, text)} + end + + @impl true + def handle_event("pretty-print", _params, socket) do + IO.puts(""" + ================== + #{socket.assigns.text} + ================== + """) + + {:noreply, socket} + end +end +``` + +## Slider + +This code example renders a SwiftUI [Slider](https://developer.apple.com/documentation/swiftui/slider). It triggers the change event when the slider is moved and sends a `"slide"` message. The `"slide"` event handler then logs the value to the console. + +Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the console below as a map of `%{"value" => value}`. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + Percent Completed + 0% + 100% + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("slide", params, socket) do + IO.inspect(params, label: "Slide Params") + {:noreply, socket} + end +end +``` + +## Stepper + +This code example renders a SwiftUI [Stepper](https://developer.apple.com/documentation/swiftui/stepper). It triggers the change event and sends a `"change-tickets"` message when the stepper increments or decrements. The `"change-tickets"` event handler then updates the number of tickets stored in state, which appears in the UI. + +Evaluate the example and increment/decrement the step. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + Tickets <%= @tickets %> + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :tickets, 0)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("change-tickets", %{"value" => tickets}, socket) do + {:noreply, assign(socket, :tickets, tickets)} + end +end +``` + +## Toggle + +This code example renders a SwiftUI [Toggle](https://developer.apple.com/documentation/swiftui/toggle). It triggers the change event and sends a `"toggle"` message when toggled. The `"toggle"` event handler then updates the `:on` field in state, which allows the `Toggle` view to be toggled on. Without providing the `is-on` attribute, the `Toggle` view could not be flipped on and off. + +Evaluate the example below and click on the toggle. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + On/Off + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :on, false)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("toggle", %{"is-on" => on}, socket) do + {:noreply, assign(socket, :on, on)} + end +end +``` + +## DatePicker + +The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :date, nil)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("pick-date", params, socket) do + IO.inspect(params, label: "Date Params") + {:noreply, socket} + end +end +``` + +### Parsing Dates + +The date from the `DatePicker` is in iso8601 format. You can use the `from_iso8601` function to parse this string into a `DateTime` struct. + +```elixir +iso8601 = "2024-01-17T20:51:00.000Z" + +DateTime.from_iso8601(iso8601) +``` + +### Your Turn: Displayed Components + +The `DatePicker` view accepts a `displayed-components` attribute with the value of `"hour-and-minute"` or `"date"` to only display one of the two components. By default, the value is `"all"`. + +You're going to change the `displayed-components` attribute in the example below to see both of these options. Change `"all"` to `"date"`, then to `"hour-and-minute"`. Re-evaluate the cell between changes and see the updated UI. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + def handle_event("pick-date", params, socket) do + {:noreply, socket} + end +end +``` + +## Small Project: Todo List + +Using the previous examples as inspiration, you're going to create a todo list. + +**Requirements** + +* Items should be `Text` views rendered within a `List` view. +* Item ids should be stored in state as a list of integers i.e. `[1, 2, 3, 4]` +* Use a `TextField` to provide the name of the next added todo item. +* An add item `Button` should add items to the list of integers in state when pressed. +* A delete item `Button` should remove the currently selected item from the list of integers in state when pressed. + +### Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Todo... + + + + <%= content %> + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, items: [], selection: "None", item_name: "", next_item_id: 1)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("type-name", %{"text" => name}, socket) do + {:noreply, assign(socket, :item_name, name)} + end + + def handle_event("add-item", _params, socket) do + updated_items = [ + {"item-#{socket.assigns.next_item_id}", socket.assigns.item_name} + | socket.assigns.items + ] + + {:noreply, + assign(socket, + item_name: "", + items: updated_items, + next_item_id: socket.assigns.next_item_id + 1 + )} + end + + def handle_event("delete-item", _params, socket) do + updated_items = + Enum.reject(socket.assigns.items, fn {id, _name} -> id == socket.assigns.selection end) + {:noreply, assign(socket, :items, updated_items)} + end + + def handle_event("selection-changed", %{"selection" => selection}, socket) do + {:noreply, assign(socket, selection: selection)} + end +end +``` + + + + + +### Enter Your Solution Below + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + # Define your mount/3 callback + + @impl true + def render(assigns), do: ~H"" + + # Define your render/3 callback + + # Define any handle_event/3 callbacks +end +``` diff --git a/guides/ex_doc_notebooks/native-navigation.md b/guides/ex_doc_notebooks/native-navigation.md new file mode 100644 index 000000000..e29631de7 --- /dev/null +++ b/guides/ex_doc_notebooks/native-navigation.md @@ -0,0 +1,303 @@ +# Native Navigation + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%native-navigation.livemd) + +## Overview + +This guide will teach you how to create multi-page applications using LiveView Native. We will cover navigation patterns specific to native applications and how to reuse the existing navigation patterns available in LiveView. + +Before diving in, you should have a basic understanding of navigation in LiveView. You should be familiar with the [redirect/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#redirect/2), [push_patch/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_patch/2) and [push_navigate/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2) functions, which are used to trigger navigation from within a LiveView. Additionally, you should know how to define routes in the router using the [live/4](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Router.html#live/4) macro. + +## NavigationStack + +LiveView Native applications are generally wrapped in a [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack) view. This view usually exists in the `root.swiftui.heex` file, which looks something like the following: + + + +```elixir +<.csrf_token /> + + +
+ Hello, from LiveView Native! +
+
+``` + +Notice the [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack) view wraps the template. This view manages the state of navigation history and allows for navigating back to previous pages. + +## Navigation Links + +We can use the [NavigationLink](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/navigationlink) view for native navigation, similar to how we can use the [.link](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#link/1) component with the `navigate` attribute for web navigation. + +We've created the same example of navigating between the `Main` and `About` pages. Each page using a `NavigationLink` to navigate to the other page. + +Evaluate **both** of the code cells below and click on the `NavigationLink` in your simulator to navigate between the two views. + + + +```elixir +defmodule ServerWeb.AboutLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the about page + + To Home + + """ + end +end + +defmodule ServerWeb.AboutLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + + + +```elixir +defmodule ServerWeb.HomeLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the main page + + To About + + """ + end +end + +defmodule ServerWeb.HomeLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +The `destination` attribute works the same as the `navigate` attribute on the web. The current LiveView will shut down, and a new one will mount without re-establishing a new socket connection. + +## Push Navigation + +For LiveView Native views, we can still use the same [redirect/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#redirect/2), [push_patch/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_patch/2), and [push_navigate/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2) functions used in typical LiveViews. + +These functions are preferable over `NavigationLink` views when you want to share navigation handlers between web and native, and/or when you want to have more customized navigation handling. + +Evaluate **both** of the code cells below and click on the `Button` view in your simulator that triggers the `handle_event/3` navigation handler to navigate between the two views. + + + +```elixir +defmodule ServerWeb.MainLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the Main Page + + """ + end +end + +defmodule ServerWeb.MainLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("to-about", _params, socket) do + {:noreply, push_navigate(socket, to: "/about")} + end +end +``` + + + +```elixir +defmodule ServerWeb.AboutLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the About Page + + """ + end +end + +defmodule ServerWeb.AboutLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("to-main", _params, socket) do + {:noreply, push_navigate(socket, to: "/")} + end +end +``` + +## Routing + +The `KinoLiveViewNative` smart cells used in this guide automatically define routes for us. Be aware there is no difference between how we define routes for LiveView or LiveView Native. + +The routes for the main and about pages might look like the following in the router: + + + +```elixir +live "/", Server.MainLive +live "/about", Server.AboutLive +``` + +## Native Navigation Events + +LiveView Native navigation mirrors the same navigation behavior you'll find on the web. + +Evaluate the example below and press each button. Notice that: + +1. `redirect/2` triggers the `mount/3` callback re-establishes a socket connection. +2. `push_navigate/2` triggers the `mount/3` callbcak and re-uses the existing socket connection. +3. `push_patch/2` does not trigger the `mount/3` callback, but does trigger the `handle_params/3` callback. This is often useful when using navigation to trigger page changes such as displaying a modal or overlay. + +You can see this for yourself using the following example. Click each of the buttons for redirect, navigate, and patch behavior. + + + +```elixir +# This module built for example purposes to persist logs between mounting LiveViews. +defmodule PersistantLogs do + def get do + :persistent_term.get(:logs) + end + + def put(log) when is_binary(log) do + :persistent_term.put(:logs, [{log, Time.utc_now()} | get()]) + end + + def reset do + :persistent_term.put(:logs, []) + end +end + +PersistantLogs.reset() + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + + + + + Socket ID<%= @socket_id %> + LiveView PID:<%= @live_view_pid %> + <%= for {log, time} <- Enum.reverse(@logs) do %> + + <%= Calendar.strftime(time, "%H:%M:%S") %>: + <%= log %> + + <% end %> + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + PersistantLogs.put("MOUNT") + + {:ok, + assign(socket, + socket_id: socket.id, + connected: connected?(socket), + logs: PersistantLogs.get(), + live_view_pid: inspect(self()) + )} + end + + @impl true + def handle_params(_params, _url, socket) do + PersistantLogs.put("HANDLE PARAMS") + + {:noreply, assign(socket, :logs, PersistantLogs.get())} + end + + @impl true + def render(assigns), + do: ~H"" + + @impl true + def handle_event("redirect", _params, socket) do + PersistantLogs.reset() + PersistantLogs.put("--REDIRECTING--") + {:noreply, redirect(socket, to: "/")} + end + + def handle_event("navigate", _params, socket) do + PersistantLogs.put("---NAVIGATING---") + {:noreply, push_navigate(socket, to: "/")} + end + + def handle_event("patch", _params, socket) do + PersistantLogs.put("----PATCHING----") + {:noreply, push_patch(socket, to: "/")} + end +end +``` diff --git a/guides/ex_doc_notebooks/stylesheets.md b/guides/ex_doc_notebooks/stylesheets.md new file mode 100644 index 000000000..979af24f3 --- /dev/null +++ b/guides/ex_doc_notebooks/stylesheets.md @@ -0,0 +1,538 @@ +# Stylesheets + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%stylesheets.livemd) + +## Overview + +In this guide, you'll learn how to use stylesheets to customize the appearance of your LiveView Native Views. You'll also learn about the inner workings of how LiveView Native uses stylesheets to implement modifiers, and how those modifiers style and customize SwiftUI Views. By the end of this lesson, you'll have the fundamentals you need to create beautiful native UIs. + +## The Stylesheet AST + +LiveView Native parses through your application at compile time to create a stylesheet AST representation of all the styles in your application. This stylesheet AST is used by the LiveView Native Client application when rendering the view hierarchy to apply modifiers to a given view. + +```mermaid +sequenceDiagram + LiveView->>LiveView: Create stylesheet + Client->>LiveView: Send request to "http://localhost:4000/?_format=swiftui" + LiveView->>Client: Send LiveView Native template in response + Client->>LiveView: Send request to "http://localhost:4000/assets/app.swiftui.styles" + LiveView->>Client: Send stylesheet in response + Client->>Client: Parses stylesheet into SwiftUI modifiers + Client->>Client: Apply modifiers to the view hierarchy +``` + +We've setup this Livebook to be included when parsing the application for modifiers. You can visit http://localhost:4000/assets/app.swiftui.styles to see the Stylesheet AST created by all of the styles in this Livebook and any other styles used in the `kino_live_view_native` project. + +LiveView Native watches for changes and updates the stylesheet, so those will be dynamically picked up and applied, You may notice a slight delay as the Livebook takes **5 seconds** to write it's contents to a file. + +## Modifiers + +SwiftUI employs **modifiers** to style and customize views. In SwiftUI syntax, each modifier is a function that can be chained onto the view they modify. LiveView Native has a minimal DSL (Domain Specific Language) for writing SwiftUI modifiers. + +Modifers can be applied through a LiveView Native Stylesheet and applying them through classes as described in the [LiveView Native Stylesheets](#liveview-native-stylesheets) section, or can be applied directly through the `class` attribute as described in the [Utility Styles](#utility-styles) section. + + + +### SwiftUI Modifiers + +Here's a basic example of making text red using the [foregroundStyle](https://developer.apple.com/documentation/swiftui/text/foregroundstyle(_:)) modifier: + +```swift +Text("Some Red Text") + .foregroundStyle(.red) +``` + +Many modifiers can be applied to a view. Here's an example using [foregroundStyle](https://developer.apple.com/documentation/swiftui/text/foregroundstyle(_:)) and [frame](https://developer.apple.com/documentation/swiftui/view/frame(width:height:alignment:)). + +```swift +Text("Some Red Text") + .foregroundStyle(.red) + .font(.title) +``` + + + +### Implicit Member Expression + +Implicit Member Expression in SwiftUI means that we can implicityly access a member of a given type without explicitly specifying the type itself. For example, the `.red` value above is from the [Color](https://developer.apple.com/documentation/swiftui/color) structure. + +```swift +Text("Some Red Text") + .foregroundStyle(Color.red) +``` + + + +### LiveView Native Modifiers + +The DSL (Domain Specific Language) used in LiveView Native drops the `.` dot before each modifier, but otherwise remains largely the same. We do not document every modifier separately, since you can translate SwiftUI examples into the DSL syntax. + +For example, Here's the same `foregroundStyle` modifier as it would be written in a LiveView Native stylesheet or class attribute, which we'll cover in a moment. + +```swift +foregroundStyle(.red) +``` + +There are some exceptions where the DSL differs from SwiftUI syntax, which we'll cover in the sections below. + +## Utility Styles + +In addition to introducing stylesheets, LiveView Native `0.3.0` also introduced Utility classes, which will be our prefered method for writing styles in these Livebook guides. + +The same SwiftUI syntax used inside of a stylesheet can be used directly inside of a `class` attribute. The example below defines the `foregroundStyle(.red)` modifier. Evaluate the example and view it in your simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +### Multiple Modifiers + +You can write multiple modifiers, separate each by a space or newline character. + +```html +Hello, from LiveView Native! +``` + +For newline characters, you'll need to wrap the string in curly brackets `{}`. Using multiple lines can better organize larger amounts of modifiers. + +```html + +Hello, from LiveView Native! + +``` + +## Dynamic Class Names + +LiveView Native parses styles in your project to define a single stylesheet. You can find the AST representation of this stylesheet at http://localhost:4000/assets/app.swiftui.styles. This stylesheet is compiled on the server and then sent to the client. For this reason, class names must be fully-formed. For example, the following class using string interpolation is **invalid**. + +```html + +Invalid Example + +``` + +However, we can still use dynamic styles so long as the class names are fully formed. + +```html + +Red or Blue Text + +``` + +Evaluate the example below multiple times while watching your simulator. Notice that the text is dynamically red or blue. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + Hello, from LiveView Native! + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +## Modifier Order + +Modifier order matters. Changing the order that modifers are applied can have a significant impact on their behavior. + +To demonstrate this concept, we're going to take a simple example of applying padding and background color. + +If we apply the background color first, then the padding, The background is applied to original view, leaving the padding filled with whitespace. + + + +```elixir +background(.orange) +padding(20) +``` + +```mermaid +flowchart + +subgraph Padding + View +end + +style View fill:orange +``` + +If we apply the padding first, then the background, the background is applied to the view with the padding, thus filling the entire area with background color. + + + +```elixir +padding(20) +background(.orange) +``` + +```mermaid +flowchart + +subgraph Padding + View +end + +style Padding fill:orange +style View fill:orange +``` + +Evaluate the example below to see this in action. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +## Injecting Views in Stylesheets + +SwiftUI modifiers sometimes accept SwiftUI views as arguments. Here's an example using the `clipShape` modifier with a `Circle` view. + +```swift +Image("logo") + .clipShape(Circle()) +``` + +However, LiveView Native does not support using SwiftUI views directly within a stylesheet. Instead, we have a few alternative options in cases like this where we want to use a view within a modifier. + + + +### Using Members on a Given Type + +We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, if you look at the [clipShape](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) documentation you'll notice it accepts the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. This type defines the [circle](https://developer.apple.com/documentation/swiftui/shape/circle) property which we can use since it's equivalent to the [Circle](https://developer.apple.com/documentation/swiftui/circle) view for our purposes. + +We can use `Shape.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. + +```swift +Image("logo") + .clipShape(Shape.circle) +``` + +Using implicit member expression, we can simplify this code to the following: + +```swift +Image("logo") + .clipShape(.circle) +``` + +Which is simple to convert to the LiveView Native DSL using the rules we've already learned. + + + +```elixir +"example-class" do + clipShape(.circle) +end +``` + + + +### Injecting a View + +For more complex cases, we can inject a view directly into a stylesheet. + +Here's an example where this might be useful. SwiftUI has modifers that represent a named content area for views to be placed within. These views can even have their own modifiers, so it's not enough to use a simple static property on the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. + +```swift +Image("logo") + .overlay(content: { + Circle().stroke(.red, lineWidth: 4) + }) +``` + +To get around this issue, we instead inject a view into the stylesheet. First, define the modifier and use an atom to represent the view that's going to be injected. + + + +```elixir +"overlay-circle" do + overlay(content: :circle) +end +``` + +Then use the `template` attribute on the view to be injected into the stylesheet. This view should be a child of the view with the given class. + +```html + + + +``` + +We can then apply modifiers to the child view through a class as we've already seen. + +## Custom Colors + +### SwiftUI Color Struct + +The SwiftUI [Color](https://developer.apple.com/documentation/swiftui/color) structure accepts either the name of a color in the asset catalog or the RGB values of the color. + +Therefore we can define custom RBG styles like so: + +```swift +foregroundStyle(Color(.sRGB, red: 0.4627, green: 0.8392, blue: 1.0)) +``` + +Evaluate the example below to see the custom color in your simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + Hello, from LiveView Native! + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +### Custom Colors in the Asset Catalogue + +Custom colors can be defined in the [Asset Catalogue](https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). Once defined in the asset catalogue of the Xcode application, the color can be referenced by name like so: + +```swift +foregroundStyle(Color("MyColor")) +``` + +Generally using the asset catalog is more performant and customizable than using custom RGB colors with the [Color](https://developer.apple.com/documentation/swiftui/color) struct. + + + +### Your Turn: Custom Colors in the Asset Catalog + +Custom colors can be defined in the asset catalog (https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). Generat + +To create a new color go to the `Assets` folder in your iOS app and create a new color set. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/asset-catalogue-create-new-color-set.png?raw=true) + + + +To create a color set, enter the RGB values or a hexcode as shown in the image below. If you don't see the sidebar with color options, click the icon in the top-right of your Xcode app and click the **Show attributes inspector** icon shown highlighted in blue. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/asset-catalogue-modify-my-color.png?raw=true) + + + +The defined color is now available for use within LiveView Native styles. However, the app needs to be re-compiled to pick up a new color set. + +Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +## LiveView Native Stylesheets + +In LiveView Native, we use `~SHEET` sigil stylesheets to organize modifers by classes using an Elixir-oriented DSL similar to CSS for styling web elements. + +We group modifiers together within a class that can be applied to an element. Here's an example of how modifiers can be grouped into a "red-title" class in a stylesheet: + + + +```elixir +~SHEET""" + "red-title" do + foregroundColor(.red) + font(.title) + end +""" +``` + +We're mostly using Utility styles for these guides, but the stylesheet module does contain some important configuration to `@import` the utility styles module. It can also be used to group styles within a class if you have a set of modifiers you're repeatedly using and want to group together. + + + +```elixir +defmodule ServerWeb.Styles.App.SwiftUI do + use LiveViewNative.Stylesheet, :swiftui + @import LiveViewNative.SwiftUI.UtilityStyles + + ~SHEET""" + "red-title" do + foregroundColor(.red) + font(.title) + end + """ +end +``` + +Since the Phoenix server runs in a dependency for these guides, you don't have direct access to the stylesheet module. + +## Apple Documentation + +You can find documentation and examples of modifiers on [Apple's SwiftUI documentation](https://developer.apple.com/documentation/swiftui) which is comprehensive and thorough, though it may feel unfamiliar at first for Elixir Developers when compared to HexDocs. + + + +### Finding Modifiers + +The [Configuring View Elements](https://developer.apple.com/documentation/swiftui/view#configuring-view-elements) section of apple documentation contains links to modifiers organized by category. In that documentation you'll find useful references such as [Style Modifiers](https://developer.apple.com/documentation/swiftui/view-style-modifiers), [Layout Modifiers](https://developer.apple.com/documentation/swiftui/view-layout), and [Input and Event Modifiers](https://developer.apple.com/documentation/swiftui/view-input-and-events). + +You can also find the same modifiers with LiveView Native examples on the [LiveView Client SwiftUI Docs](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/paddingmodifier). + +## Visual Studio Code Extension + +If you use Visual Studio Code, we strongly recommend you install the [LiveView Native Visual Studio Code Extension](https://github.com/liveview-native/liveview-native-vscode) which provides autocompletion and type information thus making modifiers significantly easier to write and lookup. + +## Your Turn: Syntax Conversion + +Part of learning LiveView Native is learning SwiftUI. Fortunately we can leverage the existing SwiftUI ecosystem and convert examples into LiveView Native syntax. + +You're going to convert the following SwiftUI code into a LiveView Native template. This example is inspired by the official [SwiftUI Tutorials](https://developer.apple.com/tutorials/swiftui/creating-and-combining-views). + + + +```elixir + VStack { + VStack(alignment: .leading) { + Text("Turtle Rock") + .font(.title) + HStack { + Text("Joshua Tree National Park") + Spacer() + Text("California") + } + .font(.subheadline) + + Divider() + + Text("About Turtle Rock") + .font(.title2) + Text("Descriptive text goes here") + } + .padding() + + Spacer() +} +``` + +### Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + Turtle Rock + + Joshua Tree National Park + + California + + + About Turtle Rock + Descriptive text goes here + + """ + end +end +``` + + + +Enter your solution below. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + """ + end +end +``` diff --git a/guides/ex_doc_notebooks/swiftui-views.md b/guides/ex_doc_notebooks/swiftui-views.md new file mode 100644 index 000000000..110ceb07b --- /dev/null +++ b/guides/ex_doc_notebooks/swiftui-views.md @@ -0,0 +1,693 @@ +# SwiftUI Views + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%swiftui-views.livemd) + +## Overview + +LiveView Native aims to use minimal SwiftUI code. All patterns for building interactive UIs are the same as LiveView. However, unlike LiveView for the web, LiveView Native uses SwiftUI templates to build the native UI. + +This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common uses of each view and give you practical examples you can use to build your own native UIs. This lesson is like a recipe book you can refer back to whenever you need an example of how to use a particular SwiftUI view. In addition, once you understand how to convert these views into the LiveView Native DSL, you should have the tools to convert essentially any SwiftUI View into the LiveView Native DSL. + +## Render Components + +LiveView Native `0.3.0` introduced render components to better encourage isolation of native and web templates and move away from co-location templates within the same LiveView module. + +Render components are namespaced under the main LiveView, and are responsible for defining the `render/1` callback function that returns the native template. + +For example, and `ExampleLive` LiveView module would have an `ExampleLive.SwiftUI` render component module for the native Template. + +This `ExampleLive.SwiftUI` render component may define a `render/1` callback function as seen below. + + + +```elixir +# Render Component +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +# LiveView +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns) do + ~H""" +

Hello from LiveView!

+ """ + end +end +``` + +Throughout this and further material we'll re-define render components you can evaluate and see reflected in your Xcode iOS simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Hello, from a LiveView Native Render Component! + """ + end +end +``` + +### Embedding Templates + +Alternatively, you may omit the render callback and instead define a `.neex` (Native + Embedded Elixir) template. + +By default, the module above would look for a template in the `swiftui/example_live*` path relative to the module's location. You can see the `LiveViewNative.Component` documentation for further explanation. + +For the sake of ease when working in Livebook, we'll prefer defining the `render/1` callback. However, we recommend you generally prefer template files when working locally in Phoenix LiveView Native projects. + +## SwiftUI Views + +In SwiftUI, a "View" is like a building block for what you see on your app's screen. It can be something simple like text or an image, or something more complex like a layout with multiple elements. Views are the pieces that make up your app's user interface. + +Here's an example `Text` view that represents a text element. + +```swift +Text("Hamlet") +``` + +LiveView Native uses the following syntax to represent the view above. + + + +```elixir +Hamlet +``` + +SwiftUI provides a wide range of Views that can be used in native templates. You can find a full reference of these views in the SwiftUI Documentation at https://developer.apple.com/documentation/swiftui/. You can also find a shorthand on how to convert SwiftUI syntax into the LiveView Native DLS in the [LiveView Native Syntax Conversion Cheatsheet](https://hexdocs.pm/live_view_native/cheatsheet.cheatmd). + +## Text + +We've already seen the [Text](https://developer.apple.com/documentation/swiftui/text) view, but we'll start simple to get the interactive tutorial running. + +Evaluate the cell below, then in Xcode, Start the iOS application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native/create-a-swiftui-application.html) lesson and ensure you see the `"Hello, from LiveView Native!"` text. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end +``` + +## HStack and VStack + +SwiftUI includes many [Layout](https://developer.apple.com/documentation/swiftui/layout-fundamentals) container views you can use to arrange your user Interface. Here are a few of the most commonly used: + +* [VStack](https://developer.apple.com/documentation/swiftui/vstack): Vertically arranges nested views. +* [HStack](https://developer.apple.com/documentation/swiftui/hstack): Horizontally arranges nested views. + +Below, we've created a simple 3X3 game board to demonstrate how to use `VStack` and `HStack` to build a layout of horizontal rows in a single vertical column.o + +Here's a diagram to demonstrate how these rows and columns create our desired layout. + +```mermaid +flowchart +subgraph VStack + direction TB + subgraph H1[HStack] + direction LR + 1[O] --> 2[X] --> 3[X] + end + subgraph H2[HStack] + direction LR + 4[X] --> 5[O] --> 6[O] + end + subgraph H3[HStack] + direction LR + 7[X] --> 8[X] --> 9[O] + end + H1 --> H2 --> H3 +end +``` + +Evaluate the example below and view the working 3X3 layout in your Xcode simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + O + X + X + + + X + O + O + + + X + X + O + + + """ + end +end +``` + +### Your Turn: 3x3 board using columns + +In the cell below, use `VStack` and `HStack` to create a 3X3 board using 3 columns instead of 3 rows as demonstrated above. The arrangement of `X` and `O` does not matter, however the content will not be properly aligned if you do not have exactly one character in each `Text` element. + +```mermaid +flowchart +subgraph HStack + direction LR + subgraph V1[VStack] + direction TB + 1[O] --> 2[X] --> 3[X] + end + subgraph V2[VStack] + direction TB + 4[X] --> 5[O] --> 6[O] + end + subgraph V3[VStack] + direction TB + 7[X] --> 8[X] --> 9[O] + end + V1 --> V2 --> V3 +end +``` + +### Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + O + X + X + + + X + O + O + + + X + X + O + + + """ + end +end +``` + + + + + +### Enter Your Solution Below + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +## Grid + +`VStack` and `HStack` do not provide vertical-alignment between horizontal rows. Notice in the following example that the rows/columns of the 3X3 board are not aligned, just centered. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + X + X + + + X + O + O + + + X + O + + + """ + end +end +``` + +Fortunately, we have a few common elements for creating a grid-based layout. + +* [Grid](https://developer.apple.com/documentation/swiftui/grid): A grid that arranges its child views in rows and columns that you specify. +* [GridRow](https://developer.apple.com/documentation/swiftui/gridrow): A view that arranges its children in a horizontal line. + +A grid layout vertically and horizontally aligns elements in the grid based on the number of elements in each row. + +Evaluate the example below and notice that rows and columns are aligned. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + XX + X + X + + + X + X + + + X + X + X + + + """ + end +end +``` + +## List + +The SwiftUI [List](https://developer.apple.com/documentation/swiftui/list) view provides a system-specific interface, and has better performance for large amounts of scrolling elements. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + Item 1 + Item 2 + Item 3 + + """ + end +end +``` + +### Multi-dimensional lists + +Alternatively we can separate children within a `List` view in a `Section` view as seen in the example below. Views in the `Section` can have the `template` attribute with a `"header"` or `"footer"` value which controls how the content is displayed above or below the section. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + +
+ Header + Content + Footer +
+
+ """ + end +end +``` + +## ScrollView + +The SwiftUI [ScrollView](https://developer.apple.com/documentation/swiftui/scrollview) displays content within a scrollable region. ScrollView is often used in combination with [LazyHStack](https://developer.apple.com/documentation/swiftui/lazyvstack), [LazyVStack](https://developer.apple.com/documentation/swiftui/lazyhstack), [LazyHGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid), and [LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid) to create scrollable layouts optimized for displaying large amounts of data. + +While `ScrollView` also works with typical `VStack` and `HStack` views, they are not optimal choices for large amounts of data. + + + +### ScrollView with VStack + +Here's an example using a `ScrollView` and a `HStack` to create scrollable text arranged horizontally. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +``` + +### ScrollView with HStack + +By default, the [axes](https://developer.apple.com/documentation/swiftui/scrollview/axes) of a `ScrollView` is vertical. To make a horizontal `ScrollView`, set the `axes` attribute to `"horizontal"` as seen in the example below. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +``` + +### Optimized ScrollView with LazyHStack and LazyVStack + +`VStack` and `HStack` are inefficient for large amounts of data because they render every child view. To demonstrate this, evaluate the example below. You should experience lag when you attempt to scroll. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +``` + +To resolve the performance problem for large amounts of data, you can use the Lazy views. Lazy views only create items as needed. Items won't be rendered until they are present on the screen. + +The next example demonstrates how using `LazyVStack` instead of `VStack` resolves the performance issue. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +``` + +## Spacers + +[Spacers](https://developer.apple.com/documentation/swiftui/spacer) take up all remaining space in a container. + +![Apple Documentation](https://docs-assets.developer.apple.com/published/189fa436f07ed0011bd0c1abeb167723/Building-Layouts-with-Stack-Views-4@2x.png) + +> Image originally from https://developer.apple.com/documentation/swiftui/spacer + +Evaluate the following example and notice the `Text` element is pushed to the right by the `Spacer`. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + This text is pushed to the right + + """ + end +end +``` + +### Your Turn: Bottom Text Spacer + +In the cell below, use `VStack` and `Spacer` to place text in the bottom of the native view. + +### Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + Hello + + """ + end +end +``` + + + + + +### Enter Your Solution Below + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +## AsyncImage + +`AsyncImage` is best for network images, or images served by the Phoenix server. + +Here's an example of `AsyncImage` with a lorem picsum image from https://picsum.photos/400/600. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +### Loading Spinner + +`AsyncImage` displays a loading spinner while loading the image. Here's an example of using `AsyncImage` without a URL so that it loads forever. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +### Relative Path + +For images served by the Phoenix server, LiveView Native evaluates URLs relative to the LiveView's host URL. This way you can use the path to static resources as you normally would in a Phoenix application. + +For example, the path `/images/logo.png` evaluates as http://localhost:4000/images/logo.png below. This serves the LiveView Native logo. + +Evaluate the example below to see the LiveView Native logo in the iOS simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +## Image + +The `Image` element is best for system images such as the built in [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) or images placed into the SwiftUI [asset catalogue](https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). + + + +### System Images + +You can use the `system-image` attribute to provide the name of system images to the `Image` element. + +For the full list of SF Symbols you can download Apple's [Symbols 5](https://developer.apple.com/sf-symbols/) application. + +Evaluate the cell below to see an example using the `square.and.arrow.up` symbol. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +### Your Turn: Asset Catalogue + +You can place assets in your SwiftUI application's asset catalogue. Using the asset catalogue for SwiftUI assets provide many benefits such as device-specific image variants, dark mode images, high contrast image mode, and improved performance. + +Follow this guide: https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs#Add-a-new-asset to create a new asset called Image. + +Then evaluate the following example and you should see this image in your simulator. For a convenient image, you can right-click and save the following LiveView Native logo. + +![LiveView Native Logo](https://github.com/liveview-native/documentation_assets/blob/main/logo.png?raw=true) + +You will need to **rebuild the native application** to pick up the changes to the assets catalogue. + + + +### Enter Your Solution Below + +You should not need to make changes to this cell. Set up an image in your asset catalogue named "Image", rebuild your native application, then evaluate this cell. You should see the image in your iOS simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +## Button + +A Button is a clickable SwiftUI View. + +The label of a button can be any view, such as a [Text](https://developer.apple.com/documentation/swiftui/text) view for text-only buttons or a [Label](https://developer.apple.com/documentation/swiftui/label) view for buttons with icons. + +Evaluate the example below to see the SwiftUI [Button](https://developer.apple.com/documentation/swiftui/button) element. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + """ + end +end +``` + +## Further Resources + +See the [SwiftUI Documentation](https://developer.apple.com/documentation/swiftui) for a complete list of SwiftUI elements and the [LiveView Native SwiftUI Documentation](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/) for LiveView Native examples of the SwiftUI elements. diff --git a/guides/livebooks/create-a-swiftui-application.livemd b/guides/livebooks/create-a-swiftui-application.livemd new file mode 100644 index 000000000..06a322508 --- /dev/null +++ b/guides/livebooks/create-a-swiftui-application.livemd @@ -0,0 +1,276 @@ + + +# Create a SwiftUI Application + +```elixir +notebook_path = __ENV__.file |> String.split("#") |> hd() + +Mix.install( + [ + {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} + ], + config: [ + server: [ + {ServerWeb.Endpoint, + [ + server: true, + url: [host: "localhost"], + adapter: Phoenix.Endpoint.Cowboy2Adapter, + render_errors: [ + formats: [html: ServerWeb.ErrorHTML, json: ServerWeb.ErrorJSON], + layout: false + ], + pubsub_server: Server.PubSub, + live_view: [signing_salt: "JSgdVVL6"], + http: [ip: {127, 0, 0, 1}, port: 4000], + secret_key_base: String.duplicate("a", 64), + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg|styles)$", + ~r/#{notebook_path}$/ + ] + ] + ]} + ], + kino: [ + group_leader: Process.group_leader() + ], + phoenix: [ + template_engines: [neex: LiveViewNative.Engine] + ], + phoenix_template: [format_encoders: [swiftui: Phoenix.HTML.Engine]], + mime: [ + types: %{"text/swiftui" => ["swiftui"], "text/styles" => ["styles"]} + ], + live_view_native: [plugins: [LiveViewNative.SwiftUI]], + live_view_native_stylesheet: [ + content: [ + swiftui: [ + "lib/**/*swiftui*", + notebook_path + ] + ], + output: "priv/static/assets" + ] + ], + force: true +) +``` + +## Overview + +This guide will teach you how to set up a SwiftUI Application for LiveView Native. + +Typically, we recommend using the `mix lvn.install` task as described in the [Installation Guide](https://hexdocs.pm/live_view_native/installation.html#5-enable-liveview-native) to add LiveView Native to a Phoenix project. However, we will walk through the steps of manually setting up an Xcode iOS project to learn how the iOS side of a LiveView Native application works. + +In future lessons, you'll use this iOS application to view iOS examples in the Xcode simulator (or a physical device if you prefer.) + +## Prerequisites + +First, make sure you have followed the [Getting Started](https://hexdocs.pm/live_view_native/getting_started.md) guide. Then evaluate the smart cell below and visit http://localhost:4000 to ensure the Phoenix server runs properly. You should see the text `Hello from LiveView!` + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Create the iOS Application + +Open Xcode and select Create New Project. + + + +![Xcode Create New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-create-new-project.png?raw=true) + + + +Select the `iOS` and `App` options to create an iOS application. Then click `Next`. + + + +![Xcode Create Template For New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-create-template-for-new-project.png?raw=true) + + + +Choose options for your new project that match the following image, then click `Next`. + +
+What do these options mean? + +* **Product Name:** The name of the application. This can be any valid name. We've chosen `Guides`. +* **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recomends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using `com.example.your_name` where `your_name` is your organization or personal name. +* **Interface:**: The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. +* **Language:** Determines which language Xcode should use for the project. Select `Swift`. +
+ + + +![Xcode Choose Options For Your New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-choose-options-for-your-new-project.png?raw=true) + + + +Select an appropriate folder location where you would like to store the iOS project, then click `Create`. + + + +![Xcode select folder location](https://github.com/liveview-native/documentation_assets/blob/main/xcode-select-folder-location.png?raw=true) + + + +You should see the default iOS application generated by Xcode. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/default-xcode-app.png?raw=true) + +## Add the LiveView Client SwiftUI Package + +In Xcode from the project you just created, select `File -> Add Package Dependencies`. Then, search for `liveview-client-swiftui`. Once you have selected the package, click `Add Package`. + +The image below was created using version `0.2.0`. You should select whichever is the latest version of LiveView Native. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/add-liveview-swiftui-client-package-0.2.0.png?raw=true) + + + +Choose the Package Products for `liveview-client-swiftui`. Select `Guides` as the target for `LiveViewNative` and `LiveViewNativeStylesheet`. This adds both of these dependencies to your iOS project. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/select-package-products.png?raw=true) + + + +At this point, you'll need to enable permissions for plugins used by LiveView Native. +You should see the following prompt. Click `Trust & Enable All`. + + + +![Xcode some build plugins are disabled](https://github.com/liveview-native/documentation_assets/blob/main/xcode-some-build-plugins-are-disabled.png?raw=true) + + + +You'll also need to manually navigate to the error tab (shown below) and manually trust and enable packages. Click on each error to trigger a prompt. Select `Trust & Enable All` to enable the plugin. + +The specific plugins are subject to change. At the time of writing you need to enable `LiveViewNativeStylesheetMacros`, `LiveViewNativeMacros`, and `CasePathMacros` as shown in the images below. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-liveview-native-stylesheet.png?raw=true) + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-liveview-native-macros.png?raw=true) + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-case-path-macros.png?raw=true) + +## Setup the SwiftUI LiveView + +The [ContentView](https://developer.apple.com/tutorials/swiftui-concepts/exploring-the-structure-of-a-swiftui-app#Content-view) contains the main view of our iOS application. + +Replace the code in the `ContentView` file with the following to connect the SwiftUI application and the Phoenix application. + + + +```swift +import SwiftUI +import LiveViewNative + +struct ContentView: View { + + var body: some View { + LiveView(.automatic( + development: .localhost(path: "/"), + production: .custom(URL(string: "https://example.com/")!) + )) + } +} + + +// Optionally preview the native UI in Xcode +#Preview { + ContentView() +} +``` + + + +The code above sets up the SwiftUI LiveView. By default, the SwiftUI LiveView connects to any Phoenix app running on http://localhost:4000. + + + + + +```mermaid +graph LR; + subgraph I[iOS App] + direction TB + ContentView + SL[SwiftUI LiveView] + end + subgraph P[Phoenix App] + LiveView + end + SL --> P + ContentView --> SL + + +``` + +## Start the Active Scheme + +Click the `start active scheme` button to build the project and run it on the iOS simulator. + +> A [build scheme](https://developer.apple.com/documentation/xcode/build-system) contains a list of targets to build, and any configuration and environment details that affect the selected action. For example, when you build and run an app, the scheme tells Xcode what launch arguments to pass to the app. +> +> * https://developer.apple.com/documentation/xcode/build-system + +After you start the active scheme, the simulator should open the iOS application and display `Hello from LiveView Native!`. If you encounter any issues see the **Troubleshooting** section below. + + + +
+ +
+ +## Troubleshooting + +If you encountered any issues with the native application, here are some troubleshooting steps you can use: + +* **Reset Package Caches:** In the Xcode application go to `File -> Packages -> Reset Package Caches`. +* **Update Packages:** In the Xcode application go to `File -> Packages -> Update to Latest Package Versions`. +* **Rebuild the Active Scheme**: In the Xcode application, press the `start active scheme` button to rebuild the active scheme and run it on the Xcode simulator. +* Update your [Xcode](https://developer.apple.com/xcode/) version if it is not already the latest version +* Check for error messages in the Livebook smart cells. + +You can also [raise an issue](https://github.com/liveview-native/liveview-client-swiftui/issues/new) if you would like support from the LiveView Native team. diff --git a/guides/livebooks/forms-and-validation.livemd b/guides/livebooks/forms-and-validation.livemd new file mode 100644 index 000000000..415ed98dc --- /dev/null +++ b/guides/livebooks/forms-and-validation.livemd @@ -0,0 +1,661 @@ +# Forms and Validation + +```elixir +Mix.install( + [ + {:kino_live_view_native, "0.2.1"}, + {:ecto, "~> 3.11"} + ], + config: [ + live_view_native: [plugins: [LiveViewNative.SwiftUI]], + live_view_native_stylesheet: [parsers: [swiftui: LiveViewNative.SwiftUI.RulesParser]], + phoenix_template: [ + format_encoders: [ + swiftui: Phoenix.HTML.Engine + ] + ] + ] +) + +KinoLiveViewNative.start([]) +``` + +## Overview + +The [LiveView Native Live Form](https://github.com/liveview-native/liveview-native-live-form) project makes it easier to build forms in LiveView Native. This project enables you to group different [Control Views](https://developer.apple.com/documentation/swiftui/controls-and-indicators) inside of a `LiveForm` and control them collectively under a single `phx-change` or `phx-submit` event handler, rather than with multiple different `phx-change` event handlers. + +Getting the most out of this material requires some understanding of the [Ecto](https://hexdocs.pm/ecto/Ecto.html) project and in particular a reasonably deep understanding of [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html). Review the linked Ecto documentation if you find any of the examples difficult to follow. + +## Installing LiveView Native Live Form + +To install LiveView Native Form, we need to add the `liveview-native-live-form` SwiftUI package to our iOS application. + +Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README and come back to this guide after you have finished the installation process. + +## Creating a Basic Form + +Once you have the LiveView Native Form package installed, you can use the `LiveForm` and `LiveSubmitButton` views to build forms more conveniently. + +Here's a basic example of a `LiveForm`. Keep in mind that `LiveForm` requires an `id` attribute. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.ExampleLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Placeholder + Submit + + """ + end + + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +When a form is submitted, its data is sent as a map where each key is the 'name' attribute of the form's control views. Evaluate the example above in your simulator and you will see a map similar to the following: + + + +```elixir +%{"my-text" => "some value"} +``` + +In a real-world application you could use these params to trigger some application logic, such as inserting a record into the database. + +## Controls and Indicators + +We've already covered many individual controls and indicator views that you can use inside of forms. For more information on those, go to the [Interactive SwiftUI Views](https://hexdocs.pm/live_view_native/interactive-swiftui-views.html) guide. + + + +### Your Turn + +Create a form that has `TextField`, `Slider`, `Toggle`, and `DatePicker` fields. + +
+Example Solution + +```elixir +defmodule Server.MultiInputFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Placeholder + + + + Submit + + """ + end + + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +``` + +
+ + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.MultiInputFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + """ + end + + # You may use this handler to test your solution. + # You should not need to modify this handler. + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +### Controlled Values + +Some control views such as the `Stepper` require manually displaying their value. In this case, we can store the form params in the socket and update them everytime the `phx-change` form binding submits an event. You can also use this pattern to provide default values. + +Evaluate the example below to see this in action. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.StepperLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, params: %{"my-stepper" => 1})} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + <%= @params["my-stepper"] %> + + """ + end + + @impl true + def handle_event("change", params, socket) do + IO.inspect(params) + {:noreply, assign(socket, params: params)} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +### Secure Field + +For password entry, or anytime you want to hide a given value, you can use the [SecureField](https://developer.apple.com/documentation/swiftui/securefield) view. This field works mostly the same as a `TextField` but hides the visual text. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.SecureLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + Enter a Password + """ + end + + @impl true + def handle_event("change", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +## Keyboard Types + +To format a `TextField` for specific input types we can use the [keyboardType](https://developer.apple.com/documentation/swiftui/view/keyboardtype(_:)) modifier. + +For a complete list of accepted keyboard types, see the [UIKeyboardType](https://developer.apple.com/documentation/uikit/uikeyboardtype) documentation. + +Below we've created several different common keyboard types. We've also included a generic `keyboard-*` to demonstrate how you can make a reusable class. + +```elixir +defmodule KeyboardStylesheet do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "number-pad" do + keyboardType(.numberPad) + end + + "email-address" do + keyboardType(.emailAddress) + end + + "phone-pad" do + keyboardType(.phonePad) + end + + "keyboard-" <> type do + keyboardType(to_ime(type)) + end + """ +end +``` + +Evaluate the example below to see the different keyboards as you focus on each input. If you don't see the keyboard, go to `I/O` -> `Keyboard` -> `Toggle Software Keyboard` to enable the software keyboard in your simulator. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.KeyboardLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use KeyboardStylesheet + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Enter Phone + Enter Number + Enter Number + """ + end + + def render(assigns) do + ~H""" +

Hello from LiveView!

+ """ + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +## Validation + +In this section, we'll focus mainly on using [Ecto Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) to validate data, but know that this is not the only way to validate data if you would like to write your own custom logic in the form event handlers, you absolutely can. + + + +### LiveView Native Changesets Coming Soon! + +LiveView Native Form doesn't currently natively support [Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) and [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) structs the way a traditional [Phoenix.Component.form](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1) does. However there is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/5) to add this behavior so this may change in the near future. As a result, this section is somewhat more verbose than will be necessary in the future, as we have to manually define much of the error handling logic that we expect will no longer be necessary in version `0.3` of LiveView Native. + +To make error handling easier, we've defined an `ErrorUtils` module below that will handle extracting the error message out of a Changeset. This will not be necessary in future versions of LiveView Native, but is a convenient helper for now. + +```elixir +defmodule ErrorUtils do + def error_message(errors, field) do + with {msg, opts} <- errors[field] do + Server.CoreComponents.translate_error({msg, opts}) + else + _ -> "" + end + end +end +``` + +For the sake of context, the `translate_message/2` function handles formatting Ecto Changeset errors. For example, it will inject values such as `count` into the string. + +```elixir +Server.CoreComponents.translate_error( + {"name must be longer than %{count} characters", [count: 10]} +) +``` + +### Changesets + +Here's a `User` changeset we're going to use to validate a `User` struct's `email` field. + +```elixir +defmodule User do + import Ecto.Changeset + defstruct [:email] + @types %{email: :string} + + def changeset(user, params) do + {user, @types} + |> cast(params, [:email]) + |> validate_required([:email]) + |> validate_format(:email, ~r/@/) + end +end +``` + +We're going to define an `error` class so errors will appear red and be left-aligned. + +```elixir +defmodule ErrorStylesheet do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "error" do + foregroundStyle(.red) + frame(maxWidth: .infinity, alignment: .leading) + end + """ +end +``` + +Then, we're going to create a LiveView that uses the `User` changeset to validate data. + +Evaluate the example below and view it in your simulator. We've included and `IO.inspect/2` call to view the changeset after submitting the form. Try submitting the form with different values to understand how those values affect the changeset. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.FormValidationLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use ErrorStylesheet + + @impl true + def mount(_params, _session, socket) do + user_changeset = User.changeset(%User{}, %{}) + {:ok, assign(socket, :user_changeset, user_changeset)} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Enter your email + + <%= ErrorUtils.error_message(@user_changeset.errors, :email) %> + + Submit + + """ + end + + @impl true + def handle_event("validate", params, socket) do + user_changeset = + User.changeset(%User{}, params) + # Preserve the `:action` field so errors do not vanish. + |> Map.put(:action, socket.assigns.user_changeset.action) + + {:noreply, assign(socket, :user_changeset, user_changeset)} + end + + def handle_event("submit", params, socket) do + user_changeset = + User.changeset(%User{}, params) + # faking a Database insert action + |> Map.put(:action, :insert) + # Submit the form and inspect the logs below to view the changeset. + |> IO.inspect(label: "Form Field Values") + + {:noreply, assign(socket, :user_changeset, user_changeset)} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +In the code above, the `"sumbit"` and `"validate"` events update the changeset based on the current form params. This fills the `errors` field used by the `ErrorUtils` module to format the error message. + +After submitting the form, the `:action` field of the changeset has a value of `:insert`, so the red Text appears using the `:if` conditional display logic. + +In the future, this complexity will likely be handled by the `live_view_native_form` library, but for now this example exists to show you how to write your own error handling based on changesets if needed. + + + +### Empty Fields Send `"null"`. + +If you submit a form with empty fields, those fields may currently send `"null"`. There is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/6) to fix this bug, but it may affect your form behavior for now and require a temporary workaround until the issue is fixed. + +## Mini Project: User Form + +Taking everything you've learned, you're going to create a more complex user form with data validation and error displaying. We've defined a `FormStylesheet` you can use (and modify) if you would like to style your form. + +```elixir +defmodule FormStylesheet do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "error" do + foregroundStyle(.red) + frame(maxWidth: .infinity, alignment: .leading) + end + + "keyboard-" <> type do + keyboardType(to_ime(type)) + end + """ +end +``` + +### User Changeset + +First, create a `CustomUser` changeset below that handles data validation. + +**Requirements** + +* A user should have a `name` field +* A user should have a `password` string field of 10 or more characters. Note that for simplicity we are not hashing the password or following real security practices since our pretend application doesn't have a database. In real-world apps passwords should **never** be stored as a simple string, they should be encrypted. +* A user should have an `age` number field greater than `0` and less than `200`. +* A user should have an `email` field which matches an email format (including `@` is sufficient). +* A user should have a `accepted_terms` field which must be true. +* A user should have a `birthdate` field which is a date. +* All fields should be required + +
+Example Solution + +```elixir +defmodule CustomUser do + import Ecto.Changeset + defstruct [:name, :password, :age, :email, :accepted_terms, :birthdate] + + @types %{ + name: :string, + password: :string, + age: :integer, + email: :string, + accepted_terms: :boolean, + birthdate: :date + } + + def changeset(user, params) do + {user, @types} + |> cast(params, Map.keys(@types)) + |> validate_required(Map.keys(@types)) + |> validate_length(:password, min: 10) + |> validate_number(:age, greater_than: 0, less_than: 200) + |> validate_acceptance(:accepted_terms) + end + + def error_message(changeset, field) do + with {msg, _reason} <- changeset.errors[field] do + msg + else + _ -> "" + end + end +end +``` + +
+ +```elixir +defmodule CustomUser do + # define the struct keys + defstruct [] + + # define the types + @types %{} + + def changeset(user, params) do + # Enter your solution + end +end +``` + +### LiveView + +Next, create the `CustomUserFormLive` Live View that lets the user enter their information and displays errors for invalid information upon form submission. + +**Requirements** + +* The `name` field should be a `TextField`. +* The `email` field should be a `TextField`. +* The `password` field should be a `SecureField`. +* The `age` field should be a `TextField` with a `.numberPad` keyboard or a `Slider`. +* The `accepted_terms` field should be a `Toggle`. +* The `birthdate` field should be a `DatePicker`. + +
+Example Solution + +```elixir +defmodule Server.CustomUserFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use FormStylesheet + + @impl true + def mount(_params, _session, socket) do + changeset = CustomUser.changeset(%CustomUser{}, %{}) + + {:ok, assign(socket, :changeset, changeset)} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + name... + <.form_error changeset={@changeset} field={:name}/> + + email... + <.form_error changeset={@changeset} field={:email}/> + + age... + <.form_error changeset={@changeset} field={:age}/> + + password... + <.form_error changeset={@changeset} field={:password}/> + + Accept the Terms and Conditions: + <.form_error changeset={@changeset} field={:accepted_terms}/> + + Birthday: + <.form_error changeset={@changeset} field={:birthdate}/> + Submit + + """ + end + + @impl true + def handle_event("validate", params, socket) do + user_changeset = + CustomUser.changeset(%CustomUser{}, params) + |> Map.put(:action, socket.assigns.changeset.action) + + {:noreply, assign(socket, :changeset, user_changeset)} + end + + def handle_event("submit", params, socket) do + user_changeset = + CustomUser.changeset(%CustomUser{}, params) + |> Map.put(:action, :insert) + + {:noreply, assign(socket, :changeset, user_changeset)} + end + + # While not strictly required, the form_error component reduces code bloat. + def form_error(assigns) do + ~SWIFTUI""" + + <%= CustomUser.error_message(@changeset, @field) %> + + """ + end +end +``` + +
+ + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.CustomUserFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use FormStylesheet + + @impl true + def mount(_params, _session, socket) do + # Remember to provide the initial changeset + {:ok, socket} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + """ + end + + @impl true + # Write your `"validate"` event handler + def handle_event("validate", params, socket) do + {:noreply, socket} + end + + # Write your `"submit"` event handler + def handle_event("submit", params, socket) do + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` diff --git a/guides/livebooks/getting-started.livemd b/guides/livebooks/getting-started.livemd new file mode 100644 index 000000000..bc2c2dadc --- /dev/null +++ b/guides/livebooks/getting-started.livemd @@ -0,0 +1,149 @@ +# Getting Started + +```elixir +notebook_path = __ENV__.file |> String.split("#") |> hd() + +Mix.install( + [ + {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} + ], + config: [ + server: [ + {ServerWeb.Endpoint, + [ + server: true, + url: [host: "localhost"], + adapter: Phoenix.Endpoint.Cowboy2Adapter, + render_errors: [ + formats: [html: ServerWeb.ErrorHTML, json: ServerWeb.ErrorJSON], + layout: false + ], + pubsub_server: Server.PubSub, + live_view: [signing_salt: "JSgdVVL6"], + http: [ip: {127, 0, 0, 1}, port: 4000], + secret_key_base: String.duplicate("a", 64), + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg|styles)$", + ~r/#{notebook_path}$/ + ] + ] + ]} + ], + kino: [ + group_leader: Process.group_leader() + ], + phoenix: [ + template_engines: [neex: LiveViewNative.Engine] + ], + phoenix_template: [format_encoders: [swiftui: Phoenix.HTML.Engine]], + mime: [ + types: %{"text/swiftui" => ["swiftui"], "text/styles" => ["styles"]} + ], + live_view_native: [plugins: [LiveViewNative.SwiftUI]], + live_view_native_stylesheet: [ + content: [ + swiftui: [ + "lib/**/*swiftui*", + notebook_path + ] + ], + output: "priv/static/assets" + ] + ], + force: true +) +``` + +## Overview + +Our livebook guides provide step-by-step lessons to help you learn LiveView Native using Livebook. These guides assume that you already have some familiarity with Phoenix LiveView applications. + +You can read these guides online, or for the best experience we recommend you click on the "Run in Livebook" badge to import and run these guides locally with Livebook. + +Each guide can be completed independently, but we suggest following them chronologically for the most comprehensive learning experience. + +## Prerequisites + +To use these guides, you'll need to install the following prerequisites: + +* [Elixir/Erlang](https://elixir-lang.org/install.html) +* [Livebook](https://livebook.dev/) +* [Xcode](https://developer.apple.com/xcode/) + +While not necessary for our guides, we also recommend you install the following for general LiveView Native development: + +* [Phoenix](https://hexdocs.pm/phoenix/installation.html) +* [PostgreSQL](https://www.postgresql.org/download/) +* [LiveView Native VS Code Extension](https://github.com/liveview-native/liveview-native-vscode) + +## Hello World + +If you are not already running this guide in Livebook, click on the "Run in Livebook" badge at the top of this page to import this guide into Livebook. + +Then, you can evaluate the following smart cell and visit http://localhost:4000 to ensure this Livebook works correctly. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns) do + ~H""" +

Hello from LiveView!

+ """ + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +In an upcoming lesson, you'll set up an iOS application with Xcode so you can run code native examples. + +## Your Turn: Live Reloading + +Change `Hello from LiveView!` to `Hello again from LiveView!` in the above LiveView. Re-evaluate the cell and notice the application live reloads and automatically updates in the browser. + +## Kino LiveView Native + +To run a Phoenix Server setup with LiveView Native from within Livebook we built the [Kino LiveView Native](https://github.com/liveview-native/kino_live_view_native) library. + +Whenever you run one of our Livebooks, a server starts on localhost:4000. Ensure you have no other servers running on port 4000 + +Kino LiveView Native defines the **LiveView Native: LiveView** and **LiveViewNative: Render Component** smart cells within these guides. + +## Troubleshooting + +Some common issues you may encounter are: + +* Another server is already running on port 4000. +* Your version of Livebook needs to be updated. +* Your version of Elixir/Erlang needs to be updated. +* Your version of Xcode needs to be updated. +* This Livebook has cached outdated versions of dependencies + +Ensure you have the latest versions of all necessary software installed, and ensure no other servers are running on port 4000. + +To clear the cache, you can click the `Setup without cache` button revealed by clicking the dropdown next to the `setup` button at the top of the Livebook. + +If that does not resolve the issue, you can [raise an issue](https://github.com/liveview-native/liveview-client-swiftui/issues/new) to receive support from the LiveView Native team. diff --git a/guides/livebooks/interactive-swiftui-views.livemd b/guides/livebooks/interactive-swiftui-views.livemd new file mode 100644 index 000000000..7f25b39fd --- /dev/null +++ b/guides/livebooks/interactive-swiftui-views.livemd @@ -0,0 +1,947 @@ +# Interactive SwiftUI Views + +```elixir +notebook_path = __ENV__.file |> String.split("#") |> hd() + +Mix.install( + [ + # {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} + {:kino_live_view_native, path: "../kino_live_view_native"} + ], + config: [ + server: [ + {ServerWeb.Endpoint, + [ + server: true, + url: [host: "localhost"], + adapter: Phoenix.Endpoint.Cowboy2Adapter, + render_errors: [ + formats: [html: ServerWeb.ErrorHTML, json: ServerWeb.ErrorJSON], + layout: false + ], + pubsub_server: Server.PubSub, + live_view: [signing_salt: "JSgdVVL6"], + http: [ip: {127, 0, 0, 1}, port: 4000], + secret_key_base: String.duplicate("a", 64), + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg|styles)$", + ~r/#{notebook_path}$/ + ] + ] + ]} + ], + kino: [ + group_leader: Process.group_leader() + ], + phoenix: [ + template_engines: [neex: LiveViewNative.Engine] + ], + phoenix_template: [format_encoders: [swiftui: Phoenix.HTML.Engine]], + mime: [ + types: %{"text/swiftui" => ["swiftui"], "text/styles" => ["styles"]} + ], + live_view_native: [plugins: [LiveViewNative.SwiftUI]], + live_view_native_stylesheet: [ + content: [ + swiftui: [ + "lib/**/*swiftui*", + notebook_path + ] + ], + output: "priv/static/assets" + ] + ], + force: true +) +``` + +## Overview + +In this guide, you'll learn how to build interactive LiveView Native applications using event bindings. + +This guide assumes some existing familiarity with [Phoenix Bindings](https://hexdocs.pm/phoenix_live_view/bindings.html) and how to set/access state stored in the LiveView's socket assigns. To get the most out of this material, you should already understand the `assign/3`/`assign/2` function, and how event bindings such as `phx-click` interact with the `handle_event/3` callback function. + +We'll use the following LiveView and define new render component examples throughout the guide. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Event Bindings + +We can bind any available `phx-*` [Phoenix Binding](https://hexdocs.pm/phoenix_live_view/bindings.html) to a SwiftUI Element. However certain events are not available on native. + +LiveView Native currently supports the following events on all SwiftUI views: + +* `phx-window-focus`: Fired when the application window gains focus, indicating user interaction with the Native app. +* `phx-window-blur`: Fired when the application window loses focus, indicating the user's switch to other apps or screens. +* `phx-focus`: Fired when a specific native UI element gains focus, often used for input fields. +* `phx-blur`: Fired when a specific native UI element loses focus, commonly used with input fields. +* `phx-click`: Fired when a user taps on a native UI element, enabling a response to tap events. + +> The above events work on all SwiftUI views. Some events are only available on specific views. For example, `phx-change` is available on controls and `phx-throttle/phx-debounce` is available on views with events. + +There is also a [Pull Request](https://github.com/liveview-native/liveview-client-swiftui/issues/1095) to add Key Events which may have been merged since this guide was published. + +## Basic Click Example + +The `phx-click` event triggers a corresponding [handle_event/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_event/3) callback function whenever a SwiftUI view is pressed. + +In the example below, the client sends a `"ping"` event to the server, and trigger's the LiveView's `"ping"` event handler. + +Evaluate the example below, then click the `"Click me!"` button. Notice `"Pong"` printed in the server logs below. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("ping", _params, socket) do + IO.puts("Pong") + {:noreply, socket} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Click Events Updating State + +Event handlers in LiveView can update the LiveView's state in the socket. + +Evaluate the cell below to see an example of incrementing a count. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :count, 0)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("increment", _params, socket) do + {:noreply, assign(socket, :count, socket.assigns.count + 1)} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Your Turn: Decrement Counter + +You're going to take the example above, and create a counter that can **both increment and decrement**. + +There should be two buttons, each with a `phx-click` binding. One button should bind the `"decrement"` event, and the other button should bind the `"increment"` event. Each event should have a corresponding handler defined using the `handle_event/3` callback function. + +
+Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + <%= @count %> + + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :count, 0)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("increment", _params, socket) do + {:noreply, assign(socket, :count, socket.assigns.count + 1)} + end + + def handle_event("decrement", _params, socket) do + {:noreply, assign(socket, :count, socket.assigns.count - 1)} + end +end +``` + +
+ + + +### Enter Your Solution Below + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + <%= @count %> + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :count, 0)} + end + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Selectable Lists + +`List` views support selecting items within the list based on their id. To select an item, provide the `selection` attribute with the item's id. + +Pressing a child item in the `List` on a native device triggers the `phx-change` event. In the example below we've bound the `phx-change` event to send the `"selection-changed"` event. This event is then handled by the `handle_event/3` callback function and used to change the selected item. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + Item <%= i %> + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, selection: "None")} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("selection-changed", %{"selection" => selection}, socket) do + {:noreply, assign(socket, selection: selection)} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Expandable Lists + +`List` views support hierarchical content using the [DisclosureGroup](https://developer.apple.com/documentation/swiftui/disclosuregroup) view. Nest `DisclosureGroup` views within a list to create multiple levels of content as seen in the example below. + +To control a `DisclosureGroup` view, use the `is-expanded` boolean attribute as seen in the example below. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + Level 1 + Item 1 + Item 2 + Item 3 + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :is_expanded, false)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("toggle", %{"is-expanded" => is_expanded}, socket) do + {:noreply, assign(socket, is_expanded: !is_expanded)} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Multiple Expandable Lists + +The next example shows one pattern for displaying multiple expandable lists without needing to write multiple event handlers. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + Level 1 + Item 1 + + Level 2 + Item 2 + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :expanded_groups, %{1 => false, 2 => false})} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("toggle-" <> level, %{"is-expanded" => is_expanded}, socket) do + level = String.to_integer(level) + + {:noreply, + assign( + socket, + :expanded_groups, + Map.replace!(socket.assigns.expanded_groups, level, !is_expanded) + )} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Controls and Indicators + +In Phoenix, the `phx-change` event must be applied to a parent form. However in SwiftUI there is no similar concept of forms. Instead, SwiftUI provides [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) views. We can apply the `phx-change` binding to any of these views. + +Once bound, the SwiftUI view will send a message to the LiveView anytime the control or indicator changes its value. + +The params of the message are based on the name of the [Binding](https://developer.apple.com/documentation/swiftui/binding) argument of the view's initializer in SwiftUI. + + + +### Event Value Bindings + +Many views use the `value` binding argument, so event params are generally sent as `%{"value" => value}`. However, certain views such as `TextField` and `Toggle` deviate from this pattern because SwiftUI uses a different `value` binding argument. For example, the `TextField` view uses `text` to bind its value, so it sends the event params as `%{"text" => value}`. + +When in doubt, you can connect the event handler and inspect the params to confirm the shape of map. + +## Text Field + +The following example shows you how to connect a SwiftUI [TextField](https://developer.apple.com/documentation/swiftui/textfield) with a `phx-change` event binding to a corresponding event handler. + +Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the server logs in the console below as a map of `%{"text" => value}`. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Enter text here + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("type", params, socket) do + IO.inspect(params, label: "params") + {:noreply, socket} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Storing TextField Values in the Socket + +The following example demonstrates how to set/access a TextField's value by controlling it using the socket assigns. + +This pattern is useful when rendering the TextField's value elsewhere on the page, using the `TextField` view's value in other event handler logic, or to set an initial value. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Enter text here + + The current value: <%= @text %> + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :text, "initial value")} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("type", %{"text" => text}, socket) do + {:noreply, assign(socket, :text, text)} + end + + @impl true + def handle_event("pretty-print", _params, socket) do + IO.puts(""" + ================== + #{socket.assigns.text} + ================== + """) + + {:noreply, socket} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Slider + +This code example renders a SwiftUI [Slider](https://developer.apple.com/documentation/swiftui/slider). It triggers the change event when the slider is moved and sends a `"slide"` message. The `"slide"` event handler then logs the value to the console. + +Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the console below as a map of `%{"value" => value}`. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + Percent Completed + 0% + 100% + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("slide", params, socket) do + IO.inspect(params, label: "Slide Params") + {:noreply, socket} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Stepper + +This code example renders a SwiftUI [Stepper](https://developer.apple.com/documentation/swiftui/stepper). It triggers the change event and sends a `"change-tickets"` message when the stepper increments or decrements. The `"change-tickets"` event handler then updates the number of tickets stored in state, which appears in the UI. + +Evaluate the example and increment/decrement the step. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + Tickets <%= @tickets %> + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :tickets, 0)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("change-tickets", %{"value" => tickets}, socket) do + {:noreply, assign(socket, :tickets, tickets)} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Toggle + +This code example renders a SwiftUI [Toggle](https://developer.apple.com/documentation/swiftui/toggle). It triggers the change event and sends a `"toggle"` message when toggled. The `"toggle"` event handler then updates the `:on` field in state, which allows the `Toggle` view to be toggled on. Without providing the `is-on` attribute, the `Toggle` view could not be flipped on and off. + +Evaluate the example below and click on the toggle. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + On/Off + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :on, false)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("toggle", %{"is-on" => on}, socket) do + {:noreply, assign(socket, :on, on)} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## DatePicker + +The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :date, nil)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("pick-date", params, socket) do + IO.inspect(params, label: "Date Params") + {:noreply, socket} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Parsing Dates + +The date from the `DatePicker` is in iso8601 format. You can use the `from_iso8601` function to parse this string into a `DateTime` struct. + +```elixir +iso8601 = "2024-01-17T20:51:00.000Z" + +DateTime.from_iso8601(iso8601) +``` + +### Your Turn: Displayed Components + +The `DatePicker` view accepts a `displayed-components` attribute with the value of `"hour-and-minute"` or `"date"` to only display one of the two components. By default, the value is `"all"`. + +You're going to change the `displayed-components` attribute in the example below to see both of these options. Change `"all"` to `"date"`, then to `"hour-and-minute"`. Re-evaluate the cell between changes and see the updated UI. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + def handle_event("pick-date", params, socket) do + {:noreply, socket} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Small Project: Todo List + +Using the previous examples as inspiration, you're going to create a todo list. + +**Requirements** + +* Items should be `Text` views rendered within a `List` view. +* Item ids should be stored in state as a list of integers i.e. `[1, 2, 3, 4]` +* Use a `TextField` to provide the name of the next added todo item. +* An add item `Button` should add items to the list of integers in state when pressed. +* A delete item `Button` should remove the currently selected item from the list of integers in state when pressed. + +
+Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Todo... + + + + <%= content %> + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, items: [], selection: "None", item_name: "", next_item_id: 1)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("type-name", %{"text" => name}, socket) do + {:noreply, assign(socket, :item_name, name)} + end + + def handle_event("add-item", _params, socket) do + updated_items = [ + {"item-#{socket.assigns.next_item_id}", socket.assigns.item_name} + | socket.assigns.items + ] + + {:noreply, + assign(socket, + item_name: "", + items: updated_items, + next_item_id: socket.assigns.next_item_id + 1 + )} + end + + def handle_event("delete-item", _params, socket) do + updated_items = + Enum.reject(socket.assigns.items, fn {id, _name} -> id == socket.assigns.selection end) + {:noreply, assign(socket, :items, updated_items)} + end + + def handle_event("selection-changed", %{"selection" => selection}, socket) do + {:noreply, assign(socket, selection: selection)} + end +end +``` + +
+ + + +### Enter Your Solution Below + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + # Define your mount/3 callback + + @impl true + def render(assigns), do: ~H"" + + # Define your render/3 callback + + # Define any handle_event/3 callbacks +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` diff --git a/guides/livebooks/native-navigation.livemd b/guides/livebooks/native-navigation.livemd new file mode 100644 index 000000000..0188e0d1d --- /dev/null +++ b/guides/livebooks/native-navigation.livemd @@ -0,0 +1,410 @@ +# Native Navigation + +```elixir +notebook_path = __ENV__.file |> String.split("#") |> hd() + +Mix.install( + [ + {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} + ], + config: [ + server: [ + {ServerWeb.Endpoint, + [ + server: true, + url: [host: "localhost"], + adapter: Phoenix.Endpoint.Cowboy2Adapter, + render_errors: [ + formats: [html: ServerWeb.ErrorHTML, json: ServerWeb.ErrorJSON], + layout: false + ], + pubsub_server: Server.PubSub, + live_view: [signing_salt: "JSgdVVL6"], + http: [ip: {127, 0, 0, 1}, port: 4000], + secret_key_base: String.duplicate("a", 64), + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg|styles)$", + ~r/#{notebook_path}$/ + ] + ] + ]} + ], + kino: [ + group_leader: Process.group_leader() + ], + phoenix: [ + template_engines: [neex: LiveViewNative.Engine] + ], + phoenix_template: [format_encoders: [swiftui: Phoenix.HTML.Engine]], + mime: [ + types: %{"text/swiftui" => ["swiftui"], "text/styles" => ["styles"]} + ], + live_view_native: [plugins: [LiveViewNative.SwiftUI]], + live_view_native_stylesheet: [ + content: [ + swiftui: [ + "lib/**/*swiftui*", + notebook_path + ] + ], + output: "priv/static/assets" + ] + ], + force: true +) +``` + +## Overview + +This guide will teach you how to create multi-page applications using LiveView Native. We will cover navigation patterns specific to native applications and how to reuse the existing navigation patterns available in LiveView. + +Before diving in, you should have a basic understanding of navigation in LiveView. You should be familiar with the [redirect/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#redirect/2), [push_patch/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_patch/2) and [push_navigate/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2) functions, which are used to trigger navigation from within a LiveView. Additionally, you should know how to define routes in the router using the [live/4](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Router.html#live/4) macro. + +## NavigationStack + +LiveView Native applications are generally wrapped in a [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack) view. This view usually exists in the `root.swiftui.heex` file, which looks something like the following: + + + +```elixir +<.csrf_token /> + + +
+ Hello, from LiveView Native! +
+
+``` + +Notice the [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack) view wraps the template. This view manages the state of navigation history and allows for navigating back to previous pages. + +## Navigation Links + +We can use the [NavigationLink](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/navigationlink) view for native navigation, similar to how we can use the [.link](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#link/1) component with the `navigate` attribute for web navigation. + +We've created the same example of navigating between the `Main` and `About` pages. Each page using a `NavigationLink` to navigate to the other page. + +Evaluate **both** of the code cells below and click on the `NavigationLink` in your simulator to navigate between the two views. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.AboutLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the about page + + To Home + + """ + end +end + +defmodule ServerWeb.AboutLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/about") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.HomeLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the main page + + To About + + """ + end +end + +defmodule ServerWeb.HomeLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +The `destination` attribute works the same as the `navigate` attribute on the web. The current LiveView will shut down, and a new one will mount without re-establishing a new socket connection. + +## Push Navigation + +For LiveView Native views, we can still use the same [redirect/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#redirect/2), [push_patch/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_patch/2), and [push_navigate/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2) functions used in typical LiveViews. + +These functions are preferable over `NavigationLink` views when you want to share navigation handlers between web and native, and/or when you want to have more customized navigation handling. + +Evaluate **both** of the code cells below and click on the `Button` view in your simulator that triggers the `handle_event/3` navigation handler to navigate between the two views. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.MainLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the Main Page + + """ + end +end + +defmodule ServerWeb.MainLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("to-about", _params, socket) do + {:noreply, push_navigate(socket, to: "/about")} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.AboutLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the About Page + + """ + end +end + +defmodule ServerWeb.AboutLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("to-main", _params, socket) do + {:noreply, push_navigate(socket, to: "/")} + end +end +|> Server.SmartCells.LiveViewNative.register("/about") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Routing + +The `KinoLiveViewNative` smart cells used in this guide automatically define routes for us. Be aware there is no difference between how we define routes for LiveView or LiveView Native. + +The routes for the main and about pages might look like the following in the router: + + + +```elixir +live "/", Server.MainLive +live "/about", Server.AboutLive +``` + +## Native Navigation Events + +LiveView Native navigation mirrors the same navigation behavior you'll find on the web. + +Evaluate the example below and press each button. Notice that: + +1. `redirect/2` triggers the `mount/3` callback re-establishes a socket connection. +2. `push_navigate/2` triggers the `mount/3` callbcak and re-uses the existing socket connection. +3. `push_patch/2` does not trigger the `mount/3` callback, but does trigger the `handle_params/3` callback. This is often useful when using navigation to trigger page changes such as displaying a modal or overlay. + +You can see this for yourself using the following example. Click each of the buttons for redirect, navigate, and patch behavior. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +# This module built for example purposes to persist logs between mounting LiveViews. +defmodule PersistantLogs do + def get do + :persistent_term.get(:logs) + end + + def put(log) when is_binary(log) do + :persistent_term.put(:logs, [{log, Time.utc_now()} | get()]) + end + + def reset do + :persistent_term.put(:logs, []) + end +end + +PersistantLogs.reset() + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + + + + + Socket ID<%= @socket_id %> + LiveView PID:<%= @live_view_pid %> + <%= for {log, time} <- Enum.reverse(@logs) do %> + + <%= Calendar.strftime(time, "%H:%M:%S") %>: + <%= log %> + + <% end %> + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + PersistantLogs.put("MOUNT") + + {:ok, + assign(socket, + socket_id: socket.id, + connected: connected?(socket), + logs: PersistantLogs.get(), + live_view_pid: inspect(self()) + )} + end + + @impl true + def handle_params(_params, _url, socket) do + PersistantLogs.put("HANDLE PARAMS") + + {:noreply, assign(socket, :logs, PersistantLogs.get())} + end + + @impl true + def render(assigns), + do: ~H"" + + @impl true + def handle_event("redirect", _params, socket) do + PersistantLogs.reset() + PersistantLogs.put("--REDIRECTING--") + {:noreply, redirect(socket, to: "/")} + end + + def handle_event("navigate", _params, socket) do + PersistantLogs.put("---NAVIGATING---") + {:noreply, push_navigate(socket, to: "/")} + end + + def handle_event("patch", _params, socket) do + PersistantLogs.put("----PATCHING----") + {:noreply, push_patch(socket, to: "/")} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` diff --git a/guides/livebooks/stylesheets.livemd b/guides/livebooks/stylesheets.livemd new file mode 100644 index 000000000..0b93678ef --- /dev/null +++ b/guides/livebooks/stylesheets.livemd @@ -0,0 +1,659 @@ +# Stylesheets + +```elixir +notebook_path = __ENV__.file |> String.split("#") |> hd() + +Mix.install( + [ + {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} + ], + config: [ + server: [ + {ServerWeb.Endpoint, + [ + server: true, + url: [host: "localhost"], + adapter: Phoenix.Endpoint.Cowboy2Adapter, + render_errors: [ + formats: [html: ServerWeb.ErrorHTML, json: ServerWeb.ErrorJSON], + layout: false + ], + pubsub_server: Server.PubSub, + live_view: [signing_salt: "JSgdVVL6"], + http: [ip: {127, 0, 0, 1}, port: 4000], + secret_key_base: String.duplicate("a", 64), + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg|styles)$", + ~r/#{notebook_path}$/ + ] + ] + ]} + ], + kino: [ + group_leader: Process.group_leader() + ], + phoenix: [ + template_engines: [neex: LiveViewNative.Engine] + ], + phoenix_template: [format_encoders: [swiftui: Phoenix.HTML.Engine]], + mime: [ + types: %{"text/swiftui" => ["swiftui"], "text/styles" => ["styles"]} + ], + live_view_native: [plugins: [LiveViewNative.SwiftUI]], + live_view_native_stylesheet: [ + content: [ + swiftui: [ + "lib/**/*swiftui*", + notebook_path + ] + ], + pretty: true, + output: "priv/static/assets" + ] + ], + force: true +) +``` + +## Overview + +In this guide, you'll learn how to use stylesheets to customize the appearance of your LiveView Native Views. You'll also learn about the inner workings of how LiveView Native uses stylesheets to implement modifiers, and how those modifiers style and customize SwiftUI Views. By the end of this lesson, you'll have the fundamentals you need to create beautiful native UIs. + +## The Stylesheet AST + +LiveView Native parses through your application at compile time to create a stylesheet AST representation of all the styles in your application. This stylesheet AST is used by the LiveView Native Client application when rendering the view hierarchy to apply modifiers to a given view. + +```mermaid +sequenceDiagram + LiveView->>LiveView: Create stylesheet + Client->>LiveView: Send request to "http://localhost:4000/?_format=swiftui" + LiveView->>Client: Send LiveView Native template in response + Client->>LiveView: Send request to "http://localhost:4000/assets/app.swiftui.styles" + LiveView->>Client: Send stylesheet in response + Client->>Client: Parses stylesheet into SwiftUI modifiers + Client->>Client: Apply modifiers to the view hierarchy +``` + +We've setup this Livebook to be included when parsing the application for modifiers. You can visit http://localhost:4000/assets/app.swiftui.styles to see the Stylesheet AST created by all of the styles in this Livebook and any other styles used in the `kino_live_view_native` project. + +LiveView Native watches for changes and updates the stylesheet, so those will be dynamically picked up and applied, You may notice a slight delay as the Livebook takes **5 seconds** to write its contents to a file. + +## Modifiers + +SwiftUI employs **modifiers** to style and customize views. In SwiftUI syntax, each modifier is a function that can be chained onto the view they modify. LiveView Native has a minimal DSL (Domain Specific Language) for writing SwiftUI modifiers. + +Modifers can be applied through a LiveView Native Stylesheet and applying them through classes as described in the [LiveView Native Stylesheets](#liveview-native-stylesheets) section, or can be applied directly through the `class` attribute as described in the [Utility Styles](#utility-styles) section. + + + +### SwiftUI Modifiers + +Here's a basic example of making text red using the [foregroundStyle](https://developer.apple.com/documentation/swiftui/text/foregroundstyle(_:)) modifier: + +```swift +Text("Some Red Text") + .foregroundStyle(.red) +``` + +Many modifiers can be applied to a view. Here's an example using [foregroundStyle](https://developer.apple.com/documentation/swiftui/text/foregroundstyle(_:)) and [frame](https://developer.apple.com/documentation/swiftui/view/frame(width:height:alignment:)). + +```swift +Text("Some Red Text") + .foregroundStyle(.red) + .font(.title) +``` + + + +### Implicit Member Expression + +Implicit Member Expression in SwiftUI means that we can implicityly access a member of a given type without explicitly specifying the type itself. For example, the `.red` value above is from the [Color](https://developer.apple.com/documentation/swiftui/color) structure. + +```swift +Text("Some Red Text") + .foregroundStyle(Color.red) +``` + + + +### LiveView Native Modifiers + +The DSL (Domain Specific Language) used in LiveView Native drops the `.` dot before each modifier, but otherwise remains largely the same. We do not document every modifier separately, since you can translate SwiftUI examples into the DSL syntax. + +For example, Here's the same `foregroundStyle` modifier as it would be written in a LiveView Native stylesheet or class attribute, which we'll cover in a moment. + +```swift +foregroundStyle(.red) +``` + +There are some exceptions where the DSL differs from SwiftUI syntax, which we'll cover in the sections below. + +## Utility Styles + +In addition to introducing stylesheets, LiveView Native `0.3.0` also introduced Utility classes, which will be our prefered method for writing styles in these Livebook guides. + +Utility styles are comperable to inline styles in HTML, which have been largely discouraged in the CSS community. We recommend Utility styles for now as the easiest way to prototype applications. But, we hope to replace Utility styles with a more mature styling framework in the future. + +The same SwiftUI syntax used inside of a stylesheet can be used directly inside of a `class` attribute. The example below defines the `foregroundStyle(.red)` modifier. Evaluate the example and view it in your simulator. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Multiple Modifiers + +You can write multiple modifiers, separate each by a space or newline character. + +```html +Hello, from LiveView Native! +``` + +For newline characters, you'll need to wrap the string in curly brackets `{}`. Using multiple lines can better organize larger amounts of modifiers. + +```html + +Hello, from LiveView Native! + +``` + + + +### Spaces + +At the time of writing, the parser for utility styles interprets space characters as a separator for each rule, thus you should not includes spaces in modifiers that might traditionally have a space. + +```html +Hello, from LiveView Native! +``` + +## Dynamic Class Names + +LiveView Native parses styles in your project to define a single stylesheet. You can find the AST representation of this stylesheet at http://localhost:4000/assets/app.swiftui.styles. This stylesheet is compiled on the server and then sent to the client. For this reason, class names must be fully-formed. For example, the following class using string interpolation is **invalid**. + +```html + +Invalid Example + +``` + +However, we can still use dynamic styles so long as the class names are fully formed. + +```html + +Red or Blue Text + +``` + +Evaluate the example below multiple times while watching your simulator. Notice that the text is dynamically red or blue. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + Hello, from LiveView Native! + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Modifier Order + +Modifier order matters. Changing the order that modifers are applied can have a significant impact on their behavior. + +To demonstrate this concept, we're going to take a simple example of applying padding and background color. + +If we apply the background color first, then the padding, The background is applied to original view, leaving the padding filled with whitespace. + + + +```elixir +background(.orange) +padding(20) +``` + +```mermaid +flowchart + +subgraph Padding + View +end + +style View fill:orange +``` + +If we apply the padding first, then the background, the background is applied to the view with the padding, thus filling the entire area with background color. + + + +```elixir +padding(20) +background(.orange) +``` + +```mermaid +flowchart + +subgraph Padding + View +end + +style Padding fill:orange +style View fill:orange +``` + +Evaluate the example below to see this in action. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Injecting Views in Stylesheets + +SwiftUI modifiers sometimes accept SwiftUI views as arguments. Here's an example using the `clipShape` modifier with a `Circle` view. + +```swift +Image("logo") + .clipShape(Circle()) +``` + +However, LiveView Native does not support using SwiftUI views directly within a stylesheet. Instead, we have a few alternative options in cases like this where we want to use a view within a modifier. + + + +### Using Members on a Given Type + +We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, if you look at the [clipShape](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) documentation you'll notice it accepts the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. This type defines the [circle](https://developer.apple.com/documentation/swiftui/shape/circle) property which we can use since it's equivalent to the [Circle](https://developer.apple.com/documentation/swiftui/circle) view for our purposes. + +We can use `Shape.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. + +```swift +Image("logo") + .clipShape(Shape.circle) +``` + +Using implicit member expression, we can simplify this code to the following: + +```swift +Image("logo") + .clipShape(.circle) +``` + +Which is simple to convert to the LiveView Native DSL using the rules we've already learned. + + + +```elixir +"example-class" do + clipShape(.circle) +end +``` + + + +### Injecting a View + +For more complex cases, we can inject a view directly into a stylesheet. + +Here's an example where this might be useful. SwiftUI has modifers that represent a named content area for views to be placed within. These views can even have their own modifiers, so it's not enough to use a simple static property on the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. + +```swift +Image("logo") + .overlay(content: { + Circle().stroke(.red, lineWidth: 4) + }) +``` + +To get around this issue, we instead inject a view into the stylesheet. First, define the modifier and use an atom to represent the view that's going to be injected. + + + +```elixir +"overlay-circle" do + overlay(content: :circle) +end +``` + +Then use the `template` attribute on the view to be injected into the stylesheet. This view should be a child of the view with the given class. + +```html + + + +``` + +We can then apply modifiers to the child view through a class as we've already seen. + +## Custom Colors + +### SwiftUI Color Struct + +The SwiftUI [Color](https://developer.apple.com/documentation/swiftui/color) structure accepts either the name of a color in the asset catalog or the RGB values of the color. + +Therefore we can define custom RBG styles like so: + +```swift +foregroundStyle(Color(.sRGB, red: 0.4627, green: 0.8392, blue: 1.0)) +``` + +Evaluate the example below to see the custom color in your simulator. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + Hello, from LiveView Native! + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Custom Colors in the Asset Catalogue + +Custom colors can be defined in the [Asset Catalogue](https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). Once defined in the asset catalogue of the Xcode application, the color can be referenced by name like so: + +```swift +foregroundStyle(Color("MyColor")) +``` + +Generally using the asset catalog is more performant and customizable than using custom RGB colors with the [Color](https://developer.apple.com/documentation/swiftui/color) struct. + + + +### Your Turn: Custom Colors in the Asset Catalog + +Custom colors can be defined in the asset catalog (https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). Generat + +To create a new color go to the `Assets` folder in your iOS app and create a new color set. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/asset-catalogue-create-new-color-set.png?raw=true) + + + +To create a color set, enter the RGB values or a hexcode as shown in the image below. If you don't see the sidebar with color options, click the icon in the top-right of your Xcode app and click the **Show attributes inspector** icon shown highlighted in blue. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/asset-catalogue-modify-my-color.png?raw=true) + + + +The defined color is now available for use within LiveView Native styles. However, the app needs to be re-compiled to pick up a new color set. + +Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## LiveView Native Stylesheets + +In LiveView Native, we use `~SHEET` sigil stylesheets to organize modifers by classes using an Elixir-oriented DSL similar to CSS for styling web elements. + +We group modifiers together within a class that can be applied to an element. Here's an example of how modifiers can be grouped into a "red-title" class in a stylesheet: + + + +```elixir +~SHEET""" + "red-title" do + foregroundColor(.red) + font(.title) + end +""" +``` + +We're mostly using Utility styles for these guides, but the stylesheet module does contain some important configuration to `@import` the utility styles module. It can also be used to group styles within a class if you have a set of modifiers you're repeatedly using and want to group together. + + + +```elixir +defmodule ServerWeb.Styles.App.SwiftUI do + use LiveViewNative.Stylesheet, :swiftui + @import LiveViewNative.SwiftUI.UtilityStyles + + ~SHEET""" + "red-title" do + foregroundColor(.red) + font(.title) + end + """ +end +``` + +Since the Phoenix server runs in a dependency for these guides, you don't have direct access to the stylesheet module. + +## Apple Documentation + +You can find documentation and examples of modifiers on [Apple's SwiftUI documentation](https://developer.apple.com/documentation/swiftui) which is comprehensive and thorough, though it may feel unfamiliar at first for Elixir Developers when compared to HexDocs. + + + +### Finding Modifiers + +The [Configuring View Elements](https://developer.apple.com/documentation/swiftui/view#configuring-view-elements) section of apple documentation contains links to modifiers organized by category. In that documentation you'll find useful references such as [Style Modifiers](https://developer.apple.com/documentation/swiftui/view-style-modifiers), [Layout Modifiers](https://developer.apple.com/documentation/swiftui/view-layout), and [Input and Event Modifiers](https://developer.apple.com/documentation/swiftui/view-input-and-events). + +You can also find more on modifiers with LiveView Native examples on the [liveview-client-swiftui](https://hexdocs.pm/live_view_native_swiftui) HexDocs. + +## Visual Studio Code Extension + +If you use Visual Studio Code, we strongly recommend you install the [LiveView Native Visual Studio Code Extension](https://github.com/liveview-native/liveview-native-vscode) which provides autocompletion and type information thus making modifiers significantly easier to write and lookup. + +## Your Turn: Syntax Conversion + +Part of learning LiveView Native is learning SwiftUI. Fortunately we can leverage the existing SwiftUI ecosystem and convert examples into LiveView Native syntax. + +You're going to convert the following SwiftUI code into a LiveView Native template. This example is inspired by the official [SwiftUI Tutorials](https://developer.apple.com/tutorials/swiftui/creating-and-combining-views). + + + +```elixir + VStack { + VStack(alignment: .leading) { + Text("Turtle Rock") + .font(.title) + HStack { + Text("Joshua Tree National Park") + Spacer() + Text("California") + } + .font(.subheadline) + + Divider() + + Text("About Turtle Rock") + .font(.title2) + Text("Descriptive text goes here") + } + .padding() + + Spacer() +} +``` + +
+Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + Turtle Rock + + Joshua Tree National Park + + California + + + About Turtle Rock + Descriptive text goes here + + """ + end +end +``` + +
+ +Enter your solution below. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` diff --git a/guides/livebooks/swiftui-views.livemd b/guides/livebooks/swiftui-views.livemd new file mode 100644 index 000000000..326f6198a --- /dev/null +++ b/guides/livebooks/swiftui-views.livemd @@ -0,0 +1,937 @@ +# SwiftUI Views + +```elixir +notebook_path = __ENV__.file |> String.split("#") |> hd() + +Mix.install( + [ + {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} + ], + config: [ + server: [ + {ServerWeb.Endpoint, + [ + server: true, + url: [host: "localhost"], + adapter: Phoenix.Endpoint.Cowboy2Adapter, + render_errors: [ + formats: [html: ServerWeb.ErrorHTML, json: ServerWeb.ErrorJSON], + layout: false + ], + pubsub_server: Server.PubSub, + live_view: [signing_salt: "JSgdVVL6"], + http: [ip: {127, 0, 0, 1}, port: 4000], + secret_key_base: String.duplicate("a", 64), + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg|styles)$", + ~r/#{notebook_path}$/ + ] + ] + ]} + ], + kino: [ + group_leader: Process.group_leader() + ], + phoenix: [ + template_engines: [neex: LiveViewNative.Engine] + ], + phoenix_template: [format_encoders: [swiftui: Phoenix.HTML.Engine]], + mime: [ + types: %{"text/swiftui" => ["swiftui"], "text/styles" => ["styles"]} + ], + live_view_native: [plugins: [LiveViewNative.SwiftUI]], + live_view_native_stylesheet: [ + content: [ + swiftui: [ + "lib/**/*swiftui*", + notebook_path + ] + ], + output: "priv/static/assets" + ] + ], + force: true +) +``` + +## Overview + +LiveView Native aims to use minimal SwiftUI code. All patterns for building interactive UIs are the same as LiveView. However, unlike LiveView for the web, LiveView Native uses SwiftUI templates to build the native UI. + +This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common uses of each view and give you practical examples you can use to build your own native UIs. This lesson is like a recipe book you can refer back to whenever you need an example of how to use a particular SwiftUI view. In addition, once you understand how to convert these views into the LiveView Native DSL, you should have the tools to convert essentially any SwiftUI View into the LiveView Native DSL. + +## Render Components + +LiveView Native `0.3.0` introduced render components to better encourage isolation of native and web templates and move away from co-location templates within the same LiveView module. + +Render components are namespaced under the main LiveView, and are responsible for defining the `render/1` callback function that returns the native template. + +For example, and `ExampleLive` LiveView module would have an `ExampleLive.SwiftUI` render component module for the native Template. + +This `ExampleLive.SwiftUI` render component may define a `render/1` callback function as seen below. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +# Render Component +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +# LiveView +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns) do + ~H""" +

Hello from LiveView!

+ """ + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +Throughout this and further material we'll re-define render components you can evaluate and see reflected in your Xcode iOS simulator. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Hello, from a LiveView Native Render Component! + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Embedding Templates + +Alternatively, you may omit the render callback and instead define a `.neex` (Native + Embedded Elixir) template. + +By default, the module above would look for a template in the `swiftui/example_live*` path relative to the module's location. You can see the `LiveViewNative.Component` documentation for further explanation. + +For the sake of ease when working in Livebook, we'll prefer defining the `render/1` callback. However, we recommend you generally prefer template files when working locally in Phoenix LiveView Native projects. + +## SwiftUI Views + +In SwiftUI, a "View" is like a building block for what you see on your app's screen. It can be something simple like text or an image, or something more complex like a layout with multiple elements. Views are the pieces that make up your app's user interface. + +Here's an example `Text` view that represents a text element. + +```swift +Text("Hamlet") +``` + +LiveView Native uses the following syntax to represent the view above. + + + +```elixir +Hamlet +``` + +SwiftUI provides a wide range of Views that can be used in native templates. You can find a full reference of these views in the SwiftUI Documentation at https://developer.apple.com/documentation/swiftui/. You can also find a shorthand on how to convert SwiftUI syntax into the LiveView Native DLS in the [LiveView Native Syntax Conversion Cheatsheet](https://hexdocs.pm/live_view_native/cheatsheet.cheatmd). + +## Text + +We've already seen the [Text](https://developer.apple.com/documentation/swiftui/text) view, but we'll start simple to get the interactive tutorial running. + +Evaluate the cell below, then in Xcode, Start the iOS application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native/create-a-swiftui-application.html) lesson and ensure you see the `"Hello, from LiveView Native!"` text. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## HStack and VStack + +SwiftUI includes many [Layout](https://developer.apple.com/documentation/swiftui/layout-fundamentals) container views you can use to arrange your user Interface. Here are a few of the most commonly used: + +* [VStack](https://developer.apple.com/documentation/swiftui/vstack): Vertically arranges nested views. +* [HStack](https://developer.apple.com/documentation/swiftui/hstack): Horizontally arranges nested views. + +Below, we've created a simple 3X3 game board to demonstrate how to use `VStack` and `HStack` to build a layout of horizontal rows in a single vertical column.o + +Here's a diagram to demonstrate how these rows and columns create our desired layout. + +```mermaid +flowchart +subgraph VStack + direction TB + subgraph H1[HStack] + direction LR + 1[O] --> 2[X] --> 3[X] + end + subgraph H2[HStack] + direction LR + 4[X] --> 5[O] --> 6[O] + end + subgraph H3[HStack] + direction LR + 7[X] --> 8[X] --> 9[O] + end + H1 --> H2 --> H3 +end +``` + +Evaluate the example below and view the working 3X3 layout in your Xcode simulator. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + O + X + X + + + X + O + O + + + X + X + O + + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Your Turn: 3x3 board using columns + +In the cell below, use `VStack` and `HStack` to create a 3X3 board using 3 columns instead of 3 rows as demonstrated above. The arrangement of `X` and `O` does not matter, however the content will not be properly aligned if you do not have exactly one character in each `Text` element. + +```mermaid +flowchart +subgraph HStack + direction LR + subgraph V1[VStack] + direction TB + 1[O] --> 2[X] --> 3[X] + end + subgraph V2[VStack] + direction TB + 4[X] --> 5[O] --> 6[O] + end + subgraph V3[VStack] + direction TB + 7[X] --> 8[X] --> 9[O] + end + V1 --> V2 --> V3 +end +``` + +
+Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + O + X + X + + + X + O + O + + + X + X + O + + + """ + end +end +``` + +
+ + + +### Enter Your Solution Below + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Grid + +`VStack` and `HStack` do not provide vertical-alignment between horizontal rows. Notice in the following example that the rows/columns of the 3X3 board are not aligned, just centered. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + X + X + + + X + O + O + + + X + O + + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +Fortunately, we have a few common elements for creating a grid-based layout. + +* [Grid](https://developer.apple.com/documentation/swiftui/grid): A grid that arranges its child views in rows and columns that you specify. +* [GridRow](https://developer.apple.com/documentation/swiftui/gridrow): A view that arranges its children in a horizontal line. + +A grid layout vertically and horizontally aligns elements in the grid based on the number of elements in each row. + +Evaluate the example below and notice that rows and columns are aligned. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + XX + X + X + + + X + X + + + X + X + X + + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## List + +The SwiftUI [List](https://developer.apple.com/documentation/swiftui/list) view provides a system-specific interface, and has better performance for large amounts of scrolling elements. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + Item 1 + Item 2 + Item 3 + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Multi-dimensional lists + +Alternatively we can separate children within a `List` view in a `Section` view as seen in the example below. Views in the `Section` can have the `template` attribute with a `"header"` or `"footer"` value which controls how the content is displayed above or below the section. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + +
+ Header + Content + Footer +
+
+ """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## ScrollView + +The SwiftUI [ScrollView](https://developer.apple.com/documentation/swiftui/scrollview) displays content within a scrollable region. ScrollView is often used in combination with [LazyHStack](https://developer.apple.com/documentation/swiftui/lazyvstack), [LazyVStack](https://developer.apple.com/documentation/swiftui/lazyhstack), [LazyHGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid), and [LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid) to create scrollable layouts optimized for displaying large amounts of data. + +While `ScrollView` also works with typical `VStack` and `HStack` views, they are not optimal choices for large amounts of data. + + + +### ScrollView with VStack + +Here's an example using a `ScrollView` and a `HStack` to create scrollable text arranged horizontally. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### ScrollView with HStack + +By default, the [axes](https://developer.apple.com/documentation/swiftui/scrollview/axes) of a `ScrollView` is vertical. To make a horizontal `ScrollView`, set the `axes` attribute to `"horizontal"` as seen in the example below. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Optimized ScrollView with LazyHStack and LazyVStack + +`VStack` and `HStack` are inefficient for large amounts of data because they render every child view. To demonstrate this, evaluate the example below. You should experience lag when you attempt to scroll. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +To resolve the performance problem for large amounts of data, you can use the Lazy views. Lazy views only create items as needed. Items won't be rendered until they are present on the screen. + +The next example demonstrates how using `LazyVStack` instead of `VStack` resolves the performance issue. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Spacers + +[Spacers](https://developer.apple.com/documentation/swiftui/spacer) take up all remaining space in a container. + +![Apple Documentation](https://docs-assets.developer.apple.com/published/189fa436f07ed0011bd0c1abeb167723/Building-Layouts-with-Stack-Views-4@2x.png) + +> Image originally from https://developer.apple.com/documentation/swiftui/spacer + +Evaluate the following example and notice the `Text` element is pushed to the right by the `Spacer`. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + This text is pushed to the right + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Your Turn: Bottom Text Spacer + +In the cell below, use `VStack` and `Spacer` to place text in the bottom of the native view. + +
+Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + Hello + + """ + end +end +``` + +
+ + + +### Enter Your Solution Below + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## AsyncImage + +`AsyncImage` is best for network images, or images served by the Phoenix server. + +Here's an example of `AsyncImage` with a lorem picsum image from https://picsum.photos/400/600. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Loading Spinner + +`AsyncImage` displays a loading spinner while loading the image. Here's an example of using `AsyncImage` without a URL so that it loads forever. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Relative Path + +For images served by the Phoenix server, LiveView Native evaluates URLs relative to the LiveView's host URL. This way you can use the path to static resources as you normally would in a Phoenix application. + +For example, the path `/images/logo.png` evaluates as http://localhost:4000/images/logo.png below. This serves the LiveView Native logo. + +Evaluate the example below to see the LiveView Native logo in the iOS simulator. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Image + +The `Image` element is best for system images such as the built in [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) or images placed into the SwiftUI [asset catalogue](https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). + + + +### System Images + +You can use the `system-image` attribute to provide the name of system images to the `Image` element. + +For the full list of SF Symbols you can download Apple's [Symbols 5](https://developer.apple.com/sf-symbols/) application. + +Evaluate the cell below to see an example using the `square.and.arrow.up` symbol. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +### Your Turn: Asset Catalogue + +You can place assets in your SwiftUI application's asset catalogue. Using the asset catalogue for SwiftUI assets provide many benefits such as device-specific image variants, dark mode images, high contrast image mode, and improved performance. + +Follow this guide: https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs#Add-a-new-asset to create a new asset called Image. + +Then evaluate the following example and you should see this image in your simulator. For a convenient image, you can right-click and save the following LiveView Native logo. + +![LiveView Native Logo](https://github.com/liveview-native/documentation_assets/blob/main/logo.png?raw=true) + +You will need to **rebuild the native application** to pick up the changes to the assets catalogue. + + + +### Enter Your Solution Below + +You should not need to make changes to this cell. Set up an image in your asset catalogue named "Image", rebuild your native application, then evaluate this cell. You should see the image in your iOS simulator. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Button + +A Button is a clickable SwiftUI View. + +The label of a button can be any view, such as a [Text](https://developer.apple.com/documentation/swiftui/text) view for text-only buttons or a [Label](https://developer.apple.com/documentation/swiftui/label) view for buttons with icons. + +Evaluate the example below to see the SwiftUI [Button](https://developer.apple.com/documentation/swiftui/button) element. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + """ + end +end +|> Server.SmartCells.RenderComponent.register() + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +## Further Resources + +See the [SwiftUI Documentation](https://developer.apple.com/documentation/swiftui) for a complete list of SwiftUI elements and the [LiveView Native SwiftUI Documentation](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/) for LiveView Native examples of the SwiftUI elements. diff --git a/guides/markdown_livebooks/create-a-swiftui-application.md b/guides/markdown_livebooks/create-a-swiftui-application.md new file mode 100644 index 000000000..e337fa751 --- /dev/null +++ b/guides/markdown_livebooks/create-a-swiftui-application.md @@ -0,0 +1,213 @@ + + +# Create a SwiftUI Application + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%create-a-swiftui-application.livemd) + +## Overview + +This guide will teach you how to set up a SwiftUI Application for LiveView Native. + +Typically, we recommend using the `mix lvn.install` task as described in the [Installation Guide](https://hexdocs.pm/live_view_native/installation.html#5-enable-liveview-native) to add LiveView Native to a Phoenix project. However, we will walk through the steps of manually setting up an Xcode iOS project to learn how the iOS side of a LiveView Native application works. + +In future lessons, you'll use this iOS application to view iOS examples in the Xcode simulator (or a physical device if you prefer.) + +## Prerequisites + +First, make sure you have followed the [Getting Started](https://hexdocs.pm/live_view_native/getting_started.md) guide. Then evaluate the smart cell below and visit http://localhost:4000 to ensure the Phoenix server runs properly. You should see the text `Hello from LiveView!` + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +## Create the iOS Application + +Open Xcode and select Create New Project. + + + +![Xcode Create New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-create-new-project.png?raw=true) + + + +Select the `iOS` and `App` options to create an iOS application. Then click `Next`. + + + +![Xcode Create Template For New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-create-template-for-new-project.png?raw=true) + + + +Choose options for your new project that match the following image, then click `Next`. + +### What do these options mean? + +* **Product Name:** The name of the application. This can be any valid name. We've chosen `Guides`. +* **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recomends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using `com.example.your_name` where `your_name` is your organization or personal name. +* **Interface:**: The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. +* **Language:** Determines which language Xcode should use for the project. Select `Swift`. + + + + +![Xcode Choose Options For Your New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-choose-options-for-your-new-project.png?raw=true) + + + +Select an appropriate folder location where you would like to store the iOS project, then click `Create`. + + + +![Xcode select folder location](https://github.com/liveview-native/documentation_assets/blob/main/xcode-select-folder-location.png?raw=true) + + + +You should see the default iOS application generated by Xcode. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/default-xcode-app.png?raw=true) + +## Add the LiveView Client SwiftUI Package + +In Xcode from the project you just created, select `File -> Add Package Dependencies`. Then, search for `liveview-client-swiftui`. Once you have selected the package, click `Add Package`. + +The image below was created using version `0.2.0`. You should select whichever is the latest version of LiveView Native. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/add-liveview-swiftui-client-package-0.2.0.png?raw=true) + + + +Choose the Package Products for `liveview-client-swiftui`. Select `Guides` as the target for `LiveViewNative` and `LiveViewNativeStylesheet`. This adds both of these dependencies to your iOS project. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/select-package-products.png?raw=true) + + + +At this point, you'll need to enable permissions for plugins used by LiveView Native. +You should see the following prompt. Click `Trust & Enable All`. + + + +![Xcode some build plugins are disabled](https://github.com/liveview-native/documentation_assets/blob/main/xcode-some-build-plugins-are-disabled.png?raw=true) + + + +You'll also need to manually navigate to the error tab (shown below) and manually trust and enable packages. Click on each error to trigger a prompt. Select `Trust & Enable All` to enable the plugin. + +The specific plugins are subject to change. At the time of writing you need to enable `LiveViewNativeStylesheetMacros`, `LiveViewNativeMacros`, and `CasePathMacros` as shown in the images below. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-liveview-native-stylesheet.png?raw=true) + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-liveview-native-macros.png?raw=true) + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-case-path-macros.png?raw=true) + +## Setup the SwiftUI LiveView + +The [ContentView](https://developer.apple.com/tutorials/swiftui-concepts/exploring-the-structure-of-a-swiftui-app#Content-view) contains the main view of our iOS application. + +Replace the code in the `ContentView` file with the following to connect the SwiftUI application and the Phoenix application. + + + +```swift +import SwiftUI +import LiveViewNative + +struct ContentView: View { + + var body: some View { + LiveView(.automatic( + development: .localhost(path: "/"), + production: .custom(URL(string: "https://example.com/")!) + )) + } +} + + +// Optionally preview the native UI in Xcode +#Preview { + ContentView() +} +``` + + + +The code above sets up the SwiftUI LiveView. By default, the SwiftUI LiveView connects to any Phoenix app running on http://localhost:4000. + + + + + +```mermaid +graph LR; + subgraph I[iOS App] + direction TB + ContentView + SL[SwiftUI LiveView] + end + subgraph P[Phoenix App] + LiveView + end + SL --> P + ContentView --> SL + + +``` + +## Start the Active Scheme + +Click the `start active scheme` button to build the project and run it on the iOS simulator. + +> A [build scheme](https://developer.apple.com/documentation/xcode/build-system) contains a list of targets to build, and any configuration and environment details that affect the selected action. For example, when you build and run an app, the scheme tells Xcode what launch arguments to pass to the app. +> +> * https://developer.apple.com/documentation/xcode/build-system + +After you start the active scheme, the simulator should open the iOS application and display `Hello from LiveView Native!`. If you encounter any issues see the **Troubleshooting** section below. + + + +
+ +
+ +## Troubleshooting + +If you encountered any issues with the native application, here are some troubleshooting steps you can use: + +* **Reset Package Caches:** In the Xcode application go to `File -> Packages -> Reset Package Caches`. +* **Update Packages:** In the Xcode application go to `File -> Packages -> Update to Latest Package Versions`. +* **Rebuild the Active Scheme**: In the Xcode application, press the `start active scheme` button to rebuild the active scheme and run it on the Xcode simulator. +* Update your [Xcode](https://developer.apple.com/xcode/) version if it is not already the latest version +* Check for error messages in the Livebook smart cells. + +You can also [raise an issue](https://github.com/liveview-native/liveview-client-swiftui/issues/new) if you would like support from the LiveView Native team. diff --git a/guides/markdown_livebooks/forms-and-validation.md b/guides/markdown_livebooks/forms-and-validation.md new file mode 100644 index 000000000..828e14a9c --- /dev/null +++ b/guides/markdown_livebooks/forms-and-validation.md @@ -0,0 +1,640 @@ +# Forms and Validation + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%forms-and-validation.livemd) + +## Overview + +The [LiveView Native Live Form](https://github.com/liveview-native/liveview-native-live-form) project makes it easier to build forms in LiveView Native. This project enables you to group different [Control Views](https://developer.apple.com/documentation/swiftui/controls-and-indicators) inside of a `LiveForm` and control them collectively under a single `phx-change` or `phx-submit` event handler, rather than with multiple different `phx-change` event handlers. + +Getting the most out of this material requires some understanding of the [Ecto](https://hexdocs.pm/ecto/Ecto.html) project and in particular a reasonably deep understanding of [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html). Review the linked Ecto documentation if you find any of the examples difficult to follow. + +## Installing LiveView Native Live Form + +To install LiveView Native Form, we need to add the `liveview-native-live-form` SwiftUI package to our iOS application. + +Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README and come back to this guide after you have finished the installation process. + +## Creating a Basic Form + +Once you have the LiveView Native Form package installed, you can use the `LiveForm` and `LiveSubmitButton` views to build forms more conveniently. + +Here's a basic example of a `LiveForm`. Keep in mind that `LiveForm` requires an `id` attribute. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.ExampleLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Placeholder + Submit + + """ + end + + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +When a form is submitted, its data is sent as a map where each key is the 'name' attribute of the form's control views. Evaluate the example above in your simulator and you will see a map similar to the following: + + + +```elixir +%{"my-text" => "some value"} +``` + +In a real-world application you could use these params to trigger some application logic, such as inserting a record into the database. + +## Controls and Indicators + +We've already covered many individual controls and indicator views that you can use inside of forms. For more information on those, go to the [Interactive SwiftUI Views](https://hexdocs.pm/live_view_native/interactive-swiftui-views.html) guide. + + + +### Your Turn + +Create a form that has `TextField`, `Slider`, `Toggle`, and `DatePicker` fields. + +### Example Solution + +```elixir +defmodule Server.MultiInputFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Placeholder + + + + Submit + + """ + end + + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +``` + + + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.MultiInputFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + """ + end + + # You may use this handler to test your solution. + # You should not need to modify this handler. + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +### Controlled Values + +Some control views such as the `Stepper` require manually displaying their value. In this case, we can store the form params in the socket and update them everytime the `phx-change` form binding submits an event. You can also use this pattern to provide default values. + +Evaluate the example below to see this in action. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.StepperLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, params: %{"my-stepper" => 1})} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + <%= @params["my-stepper"] %> + + """ + end + + @impl true + def handle_event("change", params, socket) do + IO.inspect(params) + {:noreply, assign(socket, params: params)} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +### Secure Field + +For password entry, or anytime you want to hide a given value, you can use the [SecureField](https://developer.apple.com/documentation/swiftui/securefield) view. This field works mostly the same as a `TextField` but hides the visual text. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.SecureLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + Enter a Password + """ + end + + @impl true + def handle_event("change", params, socket) do + IO.inspect(params) + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +## Keyboard Types + +To format a `TextField` for specific input types we can use the [keyboardType](https://developer.apple.com/documentation/swiftui/view/keyboardtype(_:)) modifier. + +For a complete list of accepted keyboard types, see the [UIKeyboardType](https://developer.apple.com/documentation/uikit/uikeyboardtype) documentation. + +Below we've created several different common keyboard types. We've also included a generic `keyboard-*` to demonstrate how you can make a reusable class. + +```elixir +defmodule KeyboardStylesheet do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "number-pad" do + keyboardType(.numberPad) + end + + "email-address" do + keyboardType(.emailAddress) + end + + "phone-pad" do + keyboardType(.phonePad) + end + + "keyboard-" <> type do + keyboardType(to_ime(type)) + end + """ +end +``` + +Evaluate the example below to see the different keyboards as you focus on each input. If you don't see the keyboard, go to `I/O` -> `Keyboard` -> `Toggle Software Keyboard` to enable the software keyboard in your simulator. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.KeyboardLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use KeyboardStylesheet + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Enter Phone + Enter Number + Enter Number + """ + end + + def render(assigns) do + ~H""" +

Hello from LiveView!

+ """ + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +## Validation + +In this section, we'll focus mainly on using [Ecto Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) to validate data, but know that this is not the only way to validate data if you would like to write your own custom logic in the form event handlers, you absolutely can. + + + +### LiveView Native Changesets Coming Soon! + +LiveView Native Form doesn't currently natively support [Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) and [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) structs the way a traditional [Phoenix.Component.form](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1) does. However there is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/5) to add this behavior so this may change in the near future. As a result, this section is somewhat more verbose than will be necessary in the future, as we have to manually define much of the error handling logic that we expect will no longer be necessary in version `0.3` of LiveView Native. + +To make error handling easier, we've defined an `ErrorUtils` module below that will handle extracting the error message out of a Changeset. This will not be necessary in future versions of LiveView Native, but is a convenient helper for now. + +```elixir +defmodule ErrorUtils do + def error_message(errors, field) do + with {msg, opts} <- errors[field] do + Server.CoreComponents.translate_error({msg, opts}) + else + _ -> "" + end + end +end +``` + +For the sake of context, the `translate_message/2` function handles formatting Ecto Changeset errors. For example, it will inject values such as `count` into the string. + +```elixir +Server.CoreComponents.translate_error( + {"name must be longer than %{count} characters", [count: 10]} +) +``` + +### Changesets + +Here's a `User` changeset we're going to use to validate a `User` struct's `email` field. + +```elixir +defmodule User do + import Ecto.Changeset + defstruct [:email] + @types %{email: :string} + + def changeset(user, params) do + {user, @types} + |> cast(params, [:email]) + |> validate_required([:email]) + |> validate_format(:email, ~r/@/) + end +end +``` + +We're going to define an `error` class so errors will appear red and be left-aligned. + +```elixir +defmodule ErrorStylesheet do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "error" do + foregroundStyle(.red) + frame(maxWidth: .infinity, alignment: .leading) + end + """ +end +``` + +Then, we're going to create a LiveView that uses the `User` changeset to validate data. + +Evaluate the example below and view it in your simulator. We've included and `IO.inspect/2` call to view the changeset after submitting the form. Try submitting the form with different values to understand how those values affect the changeset. + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.FormValidationLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use ErrorStylesheet + + @impl true + def mount(_params, _session, socket) do + user_changeset = User.changeset(%User{}, %{}) + {:ok, assign(socket, :user_changeset, user_changeset)} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + Enter your email + + <%= ErrorUtils.error_message(@user_changeset.errors, :email) %> + + Submit + + """ + end + + @impl true + def handle_event("validate", params, socket) do + user_changeset = + User.changeset(%User{}, params) + # Preserve the `:action` field so errors do not vanish. + |> Map.put(:action, socket.assigns.user_changeset.action) + + {:noreply, assign(socket, :user_changeset, user_changeset)} + end + + def handle_event("submit", params, socket) do + user_changeset = + User.changeset(%User{}, params) + # faking a Database insert action + |> Map.put(:action, :insert) + # Submit the form and inspect the logs below to view the changeset. + |> IO.inspect(label: "Form Field Values") + + {:noreply, assign(socket, :user_changeset, user_changeset)} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` + +In the code above, the `"sumbit"` and `"validate"` events update the changeset based on the current form params. This fills the `errors` field used by the `ErrorUtils` module to format the error message. + +After submitting the form, the `:action` field of the changeset has a value of `:insert`, so the red Text appears using the `:if` conditional display logic. + +In the future, this complexity will likely be handled by the `live_view_native_form` library, but for now this example exists to show you how to write your own error handling based on changesets if needed. + + + +### Empty Fields Send `"null"`. + +If you submit a form with empty fields, those fields may currently send `"null"`. There is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/6) to fix this bug, but it may affect your form behavior for now and require a temporary workaround until the issue is fixed. + +## Mini Project: User Form + +Taking everything you've learned, you're going to create a more complex user form with data validation and error displaying. We've defined a `FormStylesheet` you can use (and modify) if you would like to style your form. + +```elixir +defmodule FormStylesheet do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "error" do + foregroundStyle(.red) + frame(maxWidth: .infinity, alignment: .leading) + end + + "keyboard-" <> type do + keyboardType(to_ime(type)) + end + """ +end +``` + +### User Changeset + +First, create a `CustomUser` changeset below that handles data validation. + +**Requirements** + +* A user should have a `name` field +* A user should have a `password` string field of 10 or more characters. Note that for simplicity we are not hashing the password or following real security practices since our pretend application doesn't have a database. In real-world apps passwords should **never** be stored as a simple string, they should be encrypted. +* A user should have an `age` number field greater than `0` and less than `200`. +* A user should have an `email` field which matches an email format (including `@` is sufficient). +* A user should have a `accepted_terms` field which must be true. +* A user should have a `birthdate` field which is a date. +* All fields should be required + +### Example Solution + +```elixir +defmodule CustomUser do + import Ecto.Changeset + defstruct [:name, :password, :age, :email, :accepted_terms, :birthdate] + + @types %{ + name: :string, + password: :string, + age: :integer, + email: :string, + accepted_terms: :boolean, + birthdate: :date + } + + def changeset(user, params) do + {user, @types} + |> cast(params, Map.keys(@types)) + |> validate_required(Map.keys(@types)) + |> validate_length(:password, min: 10) + |> validate_number(:age, greater_than: 0, less_than: 200) + |> validate_acceptance(:accepted_terms) + end + + def error_message(changeset, field) do + with {msg, _reason} <- changeset.errors[field] do + msg + else + _ -> "" + end + end +end +``` + + + +```elixir +defmodule CustomUser do + # define the struct keys + defstruct [] + + # define the types + @types %{} + + def changeset(user, params) do + # Enter your solution + end +end +``` + +### LiveView + +Next, create the `CustomUserFormLive` Live View that lets the user enter their information and displays errors for invalid information upon form submission. + +**Requirements** + +* The `name` field should be a `TextField`. +* The `email` field should be a `TextField`. +* The `password` field should be a `SecureField`. +* The `age` field should be a `TextField` with a `.numberPad` keyboard or a `Slider`. +* The `accepted_terms` field should be a `Toggle`. +* The `birthdate` field should be a `DatePicker`. + +### Example Solution + +```elixir +defmodule Server.CustomUserFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use FormStylesheet + + @impl true + def mount(_params, _session, socket) do + changeset = CustomUser.changeset(%CustomUser{}, %{}) + + {:ok, assign(socket, :changeset, changeset)} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + name... + <.form_error changeset={@changeset} field={:name}/> + + email... + <.form_error changeset={@changeset} field={:email}/> + + age... + <.form_error changeset={@changeset} field={:age}/> + + password... + <.form_error changeset={@changeset} field={:password}/> + + Accept the Terms and Conditions: + <.form_error changeset={@changeset} field={:accepted_terms}/> + + Birthday: + <.form_error changeset={@changeset} field={:birthdate}/> + Submit + + """ + end + + @impl true + def handle_event("validate", params, socket) do + user_changeset = + CustomUser.changeset(%CustomUser{}, params) + |> Map.put(:action, socket.assigns.changeset.action) + + {:noreply, assign(socket, :changeset, user_changeset)} + end + + def handle_event("submit", params, socket) do + user_changeset = + CustomUser.changeset(%CustomUser{}, params) + |> Map.put(:action, :insert) + + {:noreply, assign(socket, :changeset, user_changeset)} + end + + # While not strictly required, the form_error component reduces code bloat. + def form_error(assigns) do + ~SWIFTUI""" + + <%= CustomUser.error_message(@changeset, @field) %> + + """ + end +end +``` + + + + + +```elixir +require KinoLiveViewNative.Livebook +import KinoLiveViewNative.Livebook +import Kernel, except: [defmodule: 2] + +defmodule Server.CustomUserFormLive do + use Phoenix.LiveView + use LiveViewNative.LiveView + use FormStylesheet + + @impl true + def mount(_params, _session, socket) do + # Remember to provide the initial changeset + {:ok, socket} + end + + @impl true + def render(%{format: :swiftui} = assigns) do + ~SWIFTUI""" + + """ + end + + @impl true + # Write your `"validate"` event handler + def handle_event("validate", params, socket) do + {:noreply, socket} + end + + # Write your `"submit"` event handler + def handle_event("submit", params, socket) do + {:noreply, socket} + end +end +|> KinoLiveViewNative.register("/", ":index") + +import KinoLiveViewNative.Livebook, only: [] +import Kernel +:ok +``` diff --git a/guides/markdown_livebooks/getting-started.md b/guides/markdown_livebooks/getting-started.md new file mode 100644 index 000000000..ecd39aa8b --- /dev/null +++ b/guides/markdown_livebooks/getting-started.md @@ -0,0 +1,87 @@ +# Getting Started + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%getting-started.livemd) + +## Overview + +Our livebook guides provide step-by-step lessons to help you learn LiveView Native using Livebook. These guides assume that you already have some familiarity with Phoenix LiveView applications. + +You can read these guides online, or for the best experience we recommend you click on the "Run in Livebook" badge to import and run these guides locally with Livebook. + +Each guide can be completed independently, but we suggest following them chronologically for the most comprehensive learning experience. + +## Prerequisites + +To use these guides, you'll need to install the following prerequisites: + +* [Elixir/Erlang](https://elixir-lang.org/install.html) +* [Livebook](https://livebook.dev/) +* [Xcode](https://developer.apple.com/xcode/) + +While not necessary for our guides, we also recommend you install the following for general LiveView Native development: + +* [Phoenix](https://hexdocs.pm/phoenix/installation.html) +* [PostgreSQL](https://www.postgresql.org/download/) +* [LiveView Native VS Code Extension](https://github.com/liveview-native/liveview-native-vscode) + +## Hello World + +If you are not already running this guide in Livebook, click on the "Run in Livebook" badge at the top of this page to import this guide into Livebook. + +Then, you can evaluate the following smart cell and visit http://localhost:4000 to ensure this Livebook works correctly. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns) do + ~H""" +

Hello from LiveView!

+ """ + end +end +``` + +In an upcoming lesson, you'll set up an iOS application with Xcode so you can run code native examples. + +## Your Turn: Live Reloading + +Change `Hello from LiveView!` to `Hello again from LiveView!` in the above LiveView. Re-evaluate the cell and notice the application live reloads and automatically updates in the browser. + +## Kino LiveView Native + +To run a Phoenix Server setup with LiveView Native from within Livebook we built the [Kino LiveView Native](https://github.com/liveview-native/kino_live_view_native) library. + +Whenever you run one of our Livebooks, a server starts on localhost:4000. Ensure you have no other servers running on port 4000 + +Kino LiveView Native defines the **LiveView Native: LiveView** and **LiveViewNative: Render Component** smart cells within these guides. + +## Troubleshooting + +Some common issues you may encounter are: + +* Another server is already running on port 4000. +* Your version of Livebook needs to be updated. +* Your version of Elixir/Erlang needs to be updated. +* Your version of Xcode needs to be updated. +* This Livebook has cached outdated versions of dependencies + +Ensure you have the latest versions of all necessary software installed, and ensure no other servers are running on port 4000. + +To clear the cache, you can click the `Setup without cache` button revealed by clicking the dropdown next to the `setup` button at the top of the Livebook. + +If that does not resolve the issue, you can [raise an issue](https://github.com/liveview-native/liveview-client-swiftui/issues/new) to receive support from the LiveView Native team. diff --git a/guides/markdown_livebooks/interactive-swiftui-views.md b/guides/markdown_livebooks/interactive-swiftui-views.md new file mode 100644 index 000000000..95e34addb --- /dev/null +++ b/guides/markdown_livebooks/interactive-swiftui-views.md @@ -0,0 +1,756 @@ +# Interactive SwiftUI Views + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%interactive-swiftui-views.livemd) + +## Overview + +In this guide, you'll learn how to build interactive LiveView Native applications using event bindings. + +This guide assumes some existing familiarity with [Phoenix Bindings](https://hexdocs.pm/phoenix_live_view/bindings.html) and how to set/access state stored in the LiveView's socket assigns. To get the most out of this material, you should already understand the `assign/3`/`assign/2` function, and how event bindings such as `phx-click` interact with the `handle_event/3` callback function. + +We'll use the following LiveView and define new render component examples throughout the guide. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +## Event Bindings + +We can bind any available `phx-*` [Phoenix Binding](https://hexdocs.pm/phoenix_live_view/bindings.html) to a SwiftUI Element. However certain events are not available on native. + +LiveView Native currently supports the following events on all SwiftUI views: + +* `phx-window-focus`: Fired when the application window gains focus, indicating user interaction with the Native app. +* `phx-window-blur`: Fired when the application window loses focus, indicating the user's switch to other apps or screens. +* `phx-focus`: Fired when a specific native UI element gains focus, often used for input fields. +* `phx-blur`: Fired when a specific native UI element loses focus, commonly used with input fields. +* `phx-click`: Fired when a user taps on a native UI element, enabling a response to tap events. + +> The above events work on all SwiftUI views. Some events are only available on specific views. For example, `phx-change` is available on controls and `phx-throttle/phx-debounce` is available on views with events. + +There is also a [Pull Request](https://github.com/liveview-native/liveview-client-swiftui/issues/1095) to add Key Events which may have been merged since this guide was published. + +## Basic Click Example + +The `phx-click` event triggers a corresponding [handle_event/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_event/3) callback function whenever a SwiftUI view is pressed. + +In the example below, the client sends a `"ping"` event to the server, and trigger's the LiveView's `"ping"` event handler. + +Evaluate the example below, then click the `"Click me!"` button. Notice `"Pong"` printed in the server logs below. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("ping", _params, socket) do + IO.puts("Pong") + {:noreply, socket} + end +end +``` + +### Click Events Updating State + +Event handlers in LiveView can update the LiveView's state in the socket. + +Evaluate the cell below to see an example of incrementing a count. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :count, 0)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("increment", _params, socket) do + {:noreply, assign(socket, :count, socket.assigns.count + 1)} + end +end +``` + +### Your Turn: Decrement Counter + +You're going to take the example above, and create a counter that can **both increment and decrement**. + +There should be two buttons, each with a `phx-click` binding. One button should bind the `"decrement"` event, and the other button should bind the `"increment"` event. Each event should have a corresponding handler defined using the `handle_event/3` callback function. + +### Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + <%= @count %> + + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :count, 0)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("increment", _params, socket) do + {:noreply, assign(socket, :count, socket.assigns.count + 1)} + end + + def handle_event("decrement", _params, socket) do + {:noreply, assign(socket, :count, socket.assigns.count - 1)} + end +end +``` + + + + + +### Enter Your Solution Below + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + <%= @count %> + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :count, 0)} + end + + @impl true + def render(assigns), do: ~H"" +end +``` + +## Selectable Lists + +`List` views support selecting items within the list based on their id. To select an item, provide the `selection` attribute with the item's id. + +Pressing a child item in the `List` on a native device triggers the `phx-change` event. In the example below we've bound the `phx-change` event to send the `"selection-changed"` event. This event is then handled by the `handle_event/3` callback function and used to change the selected item. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + Item <%= i %> + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, selection: "None")} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("selection-changed", %{"selection" => selection}, socket) do + {:noreply, assign(socket, selection: selection)} + end +end +``` + +## Expandable Lists + +`List` views support hierarchical content using the [DisclosureGroup](https://developer.apple.com/documentation/swiftui/disclosuregroup) view. Nest `DisclosureGroup` views within a list to create multiple levels of content as seen in the example below. + +To control a `DisclosureGroup` view, use the `is-expanded` boolean attribute as seen in the example below. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + Level 1 + Item 1 + Item 2 + Item 3 + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :is_expanded, false)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("toggle", %{"is-expanded" => is_expanded}, socket) do + {:noreply, assign(socket, is_expanded: !is_expanded)} + end +end +``` + +### Multiple Expandable Lists + +The next example shows one pattern for displaying multiple expandable lists without needing to write multiple event handlers. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + Level 1 + Item 1 + + Level 2 + Item 2 + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :expanded_groups, %{1 => false, 2 => false})} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("toggle-" <> level, %{"is-expanded" => is_expanded}, socket) do + level = String.to_integer(level) + + {:noreply, + assign( + socket, + :expanded_groups, + Map.replace!(socket.assigns.expanded_groups, level, !is_expanded) + )} + end +end +``` + +## Controls and Indicators + +In Phoenix, the `phx-change` event must be applied to a parent form. However in SwiftUI there is no similar concept of forms. Instead, SwiftUI provides [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) views. We can apply the `phx-change` binding to any of these views. + +Once bound, the SwiftUI view will send a message to the LiveView anytime the control or indicator changes its value. + +The params of the message are based on the name of the [Binding](https://developer.apple.com/documentation/swiftui/binding) argument of the view's initializer in SwiftUI. + + + +### Event Value Bindings + +Many views use the `value` binding argument, so event params are generally sent as `%{"value" => value}`. However, certain views such as `TextField` and `Toggle` deviate from this pattern because SwiftUI uses a different `value` binding argument. For example, the `TextField` view uses `text` to bind its value, so it sends the event params as `%{"text" => value}`. + +When in doubt, you can connect the event handler and inspect the params to confirm the shape of map. + +## Text Field + +The following example shows you how to connect a SwiftUI [TextField](https://developer.apple.com/documentation/swiftui/textfield) with a `phx-change` event binding to a corresponding event handler. + +Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the server logs in the console below as a map of `%{"text" => value}`. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Enter text here + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("type", params, socket) do + IO.inspect(params, label: "params") + {:noreply, socket} + end +end +``` + +### Storing TextField Values in the Socket + +The following example demonstrates how to set/access a TextField's value by controlling it using the socket assigns. + +This pattern is useful when rendering the TextField's value elsewhere on the page, using the `TextField` view's value in other event handler logic, or to set an initial value. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Enter text here + + The current value: <%= @text %> + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :text, "initial value")} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("type", %{"text" => text}, socket) do + {:noreply, assign(socket, :text, text)} + end + + @impl true + def handle_event("pretty-print", _params, socket) do + IO.puts(""" + ================== + #{socket.assigns.text} + ================== + """) + + {:noreply, socket} + end +end +``` + +## Slider + +This code example renders a SwiftUI [Slider](https://developer.apple.com/documentation/swiftui/slider). It triggers the change event when the slider is moved and sends a `"slide"` message. The `"slide"` event handler then logs the value to the console. + +Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the console below as a map of `%{"value" => value}`. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + Percent Completed + 0% + 100% + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("slide", params, socket) do + IO.inspect(params, label: "Slide Params") + {:noreply, socket} + end +end +``` + +## Stepper + +This code example renders a SwiftUI [Stepper](https://developer.apple.com/documentation/swiftui/stepper). It triggers the change event and sends a `"change-tickets"` message when the stepper increments or decrements. The `"change-tickets"` event handler then updates the number of tickets stored in state, which appears in the UI. + +Evaluate the example and increment/decrement the step. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + Tickets <%= @tickets %> + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :tickets, 0)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("change-tickets", %{"value" => tickets}, socket) do + {:noreply, assign(socket, :tickets, tickets)} + end +end +``` + +## Toggle + +This code example renders a SwiftUI [Toggle](https://developer.apple.com/documentation/swiftui/toggle). It triggers the change event and sends a `"toggle"` message when toggled. The `"toggle"` event handler then updates the `:on` field in state, which allows the `Toggle` view to be toggled on. Without providing the `is-on` attribute, the `Toggle` view could not be flipped on and off. + +Evaluate the example below and click on the toggle. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + On/Off + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :on, false)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("toggle", %{"is-on" => on}, socket) do + {:noreply, assign(socket, :on, on)} + end +end +``` + +## DatePicker + +The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :date, nil)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("pick-date", params, socket) do + IO.inspect(params, label: "Date Params") + {:noreply, socket} + end +end +``` + +### Parsing Dates + +The date from the `DatePicker` is in iso8601 format. You can use the `from_iso8601` function to parse this string into a `DateTime` struct. + +```elixir +iso8601 = "2024-01-17T20:51:00.000Z" + +DateTime.from_iso8601(iso8601) +``` + +### Your Turn: Displayed Components + +The `DatePicker` view accepts a `displayed-components` attribute with the value of `"hour-and-minute"` or `"date"` to only display one of the two components. By default, the value is `"all"`. + +You're going to change the `displayed-components` attribute in the example below to see both of these options. Change `"all"` to `"date"`, then to `"hour-and-minute"`. Re-evaluate the cell between changes and see the updated UI. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + def handle_event("pick-date", params, socket) do + {:noreply, socket} + end +end +``` + +## Small Project: Todo List + +Using the previous examples as inspiration, you're going to create a todo list. + +**Requirements** + +* Items should be `Text` views rendered within a `List` view. +* Item ids should be stored in state as a list of integers i.e. `[1, 2, 3, 4]` +* Use a `TextField` to provide the name of the next added todo item. +* An add item `Button` should add items to the list of integers in state when pressed. +* A delete item `Button` should remove the currently selected item from the list of integers in state when pressed. + +### Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Todo... + + + + <%= content %> + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, items: [], selection: "None", item_name: "", next_item_id: 1)} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("type-name", %{"text" => name}, socket) do + {:noreply, assign(socket, :item_name, name)} + end + + def handle_event("add-item", _params, socket) do + updated_items = [ + {"item-#{socket.assigns.next_item_id}", socket.assigns.item_name} + | socket.assigns.items + ] + + {:noreply, + assign(socket, + item_name: "", + items: updated_items, + next_item_id: socket.assigns.next_item_id + 1 + )} + end + + def handle_event("delete-item", _params, socket) do + updated_items = + Enum.reject(socket.assigns.items, fn {id, _name} -> id == socket.assigns.selection end) + {:noreply, assign(socket, :items, updated_items)} + end + + def handle_event("selection-changed", %{"selection" => selection}, socket) do + {:noreply, assign(socket, selection: selection)} + end +end +``` + + + + + +### Enter Your Solution Below + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + # Define your mount/3 callback + + @impl true + def render(assigns), do: ~H"" + + # Define your render/3 callback + + # Define any handle_event/3 callbacks +end +``` diff --git a/guides/markdown_livebooks/native-navigation.md b/guides/markdown_livebooks/native-navigation.md new file mode 100644 index 000000000..52746e1df --- /dev/null +++ b/guides/markdown_livebooks/native-navigation.md @@ -0,0 +1,303 @@ +# Native Navigation + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%native-navigation.livemd) + +## Overview + +This guide will teach you how to create multi-page applications using LiveView Native. We will cover navigation patterns specific to native applications and how to reuse the existing navigation patterns available in LiveView. + +Before diving in, you should have a basic understanding of navigation in LiveView. You should be familiar with the [redirect/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#redirect/2), [push_patch/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_patch/2) and [push_navigate/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2) functions, which are used to trigger navigation from within a LiveView. Additionally, you should know how to define routes in the router using the [live/4](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Router.html#live/4) macro. + +## NavigationStack + +LiveView Native applications are generally wrapped in a [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack) view. This view usually exists in the `root.swiftui.heex` file, which looks something like the following: + + + +```elixir +<.csrf_token /> + + +
+ Hello, from LiveView Native! +
+
+``` + +Notice the [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack) view wraps the template. This view manages the state of navigation history and allows for navigating back to previous pages. + +## Navigation Links + +We can use the [NavigationLink](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/navigationlink) view for native navigation, similar to how we can use the [.link](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#link/1) component with the `navigate` attribute for web navigation. + +We've created the same example of navigating between the `Main` and `About` pages. Each page using a `NavigationLink` to navigate to the other page. + +Evaluate **both** of the code cells below and click on the `NavigationLink` in your simulator to navigate between the two views. + + + +```elixir +defmodule ServerWeb.AboutLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the about page + + To Home + + """ + end +end + +defmodule ServerWeb.AboutLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + + + +```elixir +defmodule ServerWeb.HomeLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the main page + + To About + + """ + end +end + +defmodule ServerWeb.HomeLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +The `destination` attribute works the same as the `navigate` attribute on the web. The current LiveView will shut down, and a new one will mount without re-establishing a new socket connection. + +## Push Navigation + +For LiveView Native views, we can still use the same [redirect/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#redirect/2), [push_patch/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_patch/2), and [push_navigate/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2) functions used in typical LiveViews. + +These functions are preferable over `NavigationLink` views when you want to share navigation handlers between web and native, and/or when you want to have more customized navigation handling. + +Evaluate **both** of the code cells below and click on the `Button` view in your simulator that triggers the `handle_event/3` navigation handler to navigate between the two views. + + + +```elixir +defmodule ServerWeb.MainLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the Main Page + + """ + end +end + +defmodule ServerWeb.MainLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("to-about", _params, socket) do + {:noreply, push_navigate(socket, to: "/about")} + end +end +``` + + + +```elixir +defmodule ServerWeb.AboutLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the About Page + + """ + end +end + +defmodule ServerWeb.AboutLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("to-main", _params, socket) do + {:noreply, push_navigate(socket, to: "/")} + end +end +``` + +## Routing + +The `KinoLiveViewNative` smart cells used in this guide automatically define routes for us. Be aware there is no difference between how we define routes for LiveView or LiveView Native. + +The routes for the main and about pages might look like the following in the router: + + + +```elixir +live "/", Server.MainLive +live "/about", Server.AboutLive +``` + +## Native Navigation Events + +LiveView Native navigation mirrors the same navigation behavior you'll find on the web. + +Evaluate the example below and press each button. Notice that: + +1. `redirect/2` triggers the `mount/3` callback re-establishes a socket connection. +2. `push_navigate/2` triggers the `mount/3` callbcak and re-uses the existing socket connection. +3. `push_patch/2` does not trigger the `mount/3` callback, but does trigger the `handle_params/3` callback. This is often useful when using navigation to trigger page changes such as displaying a modal or overlay. + +You can see this for yourself using the following example. Click each of the buttons for redirect, navigate, and patch behavior. + + + +```elixir +# This module built for example purposes to persist logs between mounting LiveViews. +defmodule PersistantLogs do + def get do + :persistent_term.get(:logs) + end + + def put(log) when is_binary(log) do + :persistent_term.put(:logs, [{log, Time.utc_now()} | get()]) + end + + def reset do + :persistent_term.put(:logs, []) + end +end + +PersistantLogs.reset() + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + + + + + Socket ID<%= @socket_id %> + LiveView PID:<%= @live_view_pid %> + <%= for {log, time} <- Enum.reverse(@logs) do %> + + <%= Calendar.strftime(time, "%H:%M:%S") %>: + <%= log %> + + <% end %> + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + PersistantLogs.put("MOUNT") + + {:ok, + assign(socket, + socket_id: socket.id, + connected: connected?(socket), + logs: PersistantLogs.get(), + live_view_pid: inspect(self()) + )} + end + + @impl true + def handle_params(_params, _url, socket) do + PersistantLogs.put("HANDLE PARAMS") + + {:noreply, assign(socket, :logs, PersistantLogs.get())} + end + + @impl true + def render(assigns), + do: ~H"" + + @impl true + def handle_event("redirect", _params, socket) do + PersistantLogs.reset() + PersistantLogs.put("--REDIRECTING--") + {:noreply, redirect(socket, to: "/")} + end + + def handle_event("navigate", _params, socket) do + PersistantLogs.put("---NAVIGATING---") + {:noreply, push_navigate(socket, to: "/")} + end + + def handle_event("patch", _params, socket) do + PersistantLogs.put("----PATCHING----") + {:noreply, push_patch(socket, to: "/")} + end +end +``` diff --git a/guides/markdown_livebooks/stylesheets.md b/guides/markdown_livebooks/stylesheets.md new file mode 100644 index 000000000..fa99bdf4a --- /dev/null +++ b/guides/markdown_livebooks/stylesheets.md @@ -0,0 +1,538 @@ +# Stylesheets + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%stylesheets.livemd) + +## Overview + +In this guide, you'll learn how to use stylesheets to customize the appearance of your LiveView Native Views. You'll also learn about the inner workings of how LiveView Native uses stylesheets to implement modifiers, and how those modifiers style and customize SwiftUI Views. By the end of this lesson, you'll have the fundamentals you need to create beautiful native UIs. + +## The Stylesheet AST + +LiveView Native parses through your application at compile time to create a stylesheet AST representation of all the styles in your application. This stylesheet AST is used by the LiveView Native Client application when rendering the view hierarchy to apply modifiers to a given view. + +```mermaid +sequenceDiagram + LiveView->>LiveView: Create stylesheet + Client->>LiveView: Send request to "http://localhost:4000/?_format=swiftui" + LiveView->>Client: Send LiveView Native template in response + Client->>LiveView: Send request to "http://localhost:4000/assets/app.swiftui.styles" + LiveView->>Client: Send stylesheet in response + Client->>Client: Parses stylesheet into SwiftUI modifiers + Client->>Client: Apply modifiers to the view hierarchy +``` + +We've setup this Livebook to be included when parsing the application for modifiers. You can visit http://localhost:4000/assets/app.swiftui.styles to see the Stylesheet AST created by all of the styles in this Livebook and any other styles used in the `kino_live_view_native` project. + +LiveView Native watches for changes and updates the stylesheet, so those will be dynamically picked up and applied, You may notice a slight delay as the Livebook takes **5 seconds** to write it's contents to a file. + +## Modifiers + +SwiftUI employs **modifiers** to style and customize views. In SwiftUI syntax, each modifier is a function that can be chained onto the view they modify. LiveView Native has a minimal DSL (Domain Specific Language) for writing SwiftUI modifiers. + +Modifers can be applied through a LiveView Native Stylesheet and applying them through classes as described in the [LiveView Native Stylesheets](#liveview-native-stylesheets) section, or can be applied directly through the `class` attribute as described in the [Utility Styles](#utility-styles) section. + + + +### SwiftUI Modifiers + +Here's a basic example of making text red using the [foregroundStyle](https://developer.apple.com/documentation/swiftui/text/foregroundstyle(_:)) modifier: + +```swift +Text("Some Red Text") + .foregroundStyle(.red) +``` + +Many modifiers can be applied to a view. Here's an example using [foregroundStyle](https://developer.apple.com/documentation/swiftui/text/foregroundstyle(_:)) and [frame](https://developer.apple.com/documentation/swiftui/view/frame(width:height:alignment:)). + +```swift +Text("Some Red Text") + .foregroundStyle(.red) + .font(.title) +``` + + + +### Implicit Member Expression + +Implicit Member Expression in SwiftUI means that we can implicityly access a member of a given type without explicitly specifying the type itself. For example, the `.red` value above is from the [Color](https://developer.apple.com/documentation/swiftui/color) structure. + +```swift +Text("Some Red Text") + .foregroundStyle(Color.red) +``` + + + +### LiveView Native Modifiers + +The DSL (Domain Specific Language) used in LiveView Native drops the `.` dot before each modifier, but otherwise remains largely the same. We do not document every modifier separately, since you can translate SwiftUI examples into the DSL syntax. + +For example, Here's the same `foregroundStyle` modifier as it would be written in a LiveView Native stylesheet or class attribute, which we'll cover in a moment. + +```swift +foregroundStyle(.red) +``` + +There are some exceptions where the DSL differs from SwiftUI syntax, which we'll cover in the sections below. + +## Utility Styles + +In addition to introducing stylesheets, LiveView Native `0.3.0` also introduced Utility classes, which will be our prefered method for writing styles in these Livebook guides. + +The same SwiftUI syntax used inside of a stylesheet can be used directly inside of a `class` attribute. The example below defines the `foregroundStyle(.red)` modifier. Evaluate the example and view it in your simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +### Multiple Modifiers + +You can write multiple modifiers, separate each by a space or newline character. + +```html +Hello, from LiveView Native! +``` + +For newline characters, you'll need to wrap the string in curly brackets `{}`. Using multiple lines can better organize larger amounts of modifiers. + +```html + +Hello, from LiveView Native! + +``` + +## Dynamic Class Names + +LiveView Native parses styles in your project to define a single stylesheet. You can find the AST representation of this stylesheet at http://localhost:4000/assets/app.swiftui.styles. This stylesheet is compiled on the server and then sent to the client. For this reason, class names must be fully-formed. For example, the following class using string interpolation is **invalid**. + +```html + +Invalid Example + +``` + +However, we can still use dynamic styles so long as the class names are fully formed. + +```html + +Red or Blue Text + +``` + +Evaluate the example below multiple times while watching your simulator. Notice that the text is dynamically red or blue. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + Hello, from LiveView Native! + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +## Modifier Order + +Modifier order matters. Changing the order that modifers are applied can have a significant impact on their behavior. + +To demonstrate this concept, we're going to take a simple example of applying padding and background color. + +If we apply the background color first, then the padding, The background is applied to original view, leaving the padding filled with whitespace. + + + +```elixir +background(.orange) +padding(20) +``` + +```mermaid +flowchart + +subgraph Padding + View +end + +style View fill:orange +``` + +If we apply the padding first, then the background, the background is applied to the view with the padding, thus filling the entire area with background color. + + + +```elixir +padding(20) +background(.orange) +``` + +```mermaid +flowchart + +subgraph Padding + View +end + +style Padding fill:orange +style View fill:orange +``` + +Evaluate the example below to see this in action. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +## Injecting Views in Stylesheets + +SwiftUI modifiers sometimes accept SwiftUI views as arguments. Here's an example using the `clipShape` modifier with a `Circle` view. + +```swift +Image("logo") + .clipShape(Circle()) +``` + +However, LiveView Native does not support using SwiftUI views directly within a stylesheet. Instead, we have a few alternative options in cases like this where we want to use a view within a modifier. + + + +### Using Members on a Given Type + +We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, if you look at the [clipShape](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) documentation you'll notice it accepts the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. This type defines the [circle](https://developer.apple.com/documentation/swiftui/shape/circle) property which we can use since it's equivalent to the [Circle](https://developer.apple.com/documentation/swiftui/circle) view for our purposes. + +We can use `Shape.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. + +```swift +Image("logo") + .clipShape(Shape.circle) +``` + +Using implicit member expression, we can simplify this code to the following: + +```swift +Image("logo") + .clipShape(.circle) +``` + +Which is simple to convert to the LiveView Native DSL using the rules we've already learned. + + + +```elixir +"example-class" do + clipShape(.circle) +end +``` + + + +### Injecting a View + +For more complex cases, we can inject a view directly into a stylesheet. + +Here's an example where this might be useful. SwiftUI has modifers that represent a named content area for views to be placed within. These views can even have their own modifiers, so it's not enough to use a simple static property on the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. + +```swift +Image("logo") + .overlay(content: { + Circle().stroke(.red, lineWidth: 4) + }) +``` + +To get around this issue, we instead inject a view into the stylesheet. First, define the modifier and use an atom to represent the view that's going to be injected. + + + +```elixir +"overlay-circle" do + overlay(content: :circle) +end +``` + +Then use the `template` attribute on the view to be injected into the stylesheet. This view should be a child of the view with the given class. + +```html + + + +``` + +We can then apply modifiers to the child view through a class as we've already seen. + +## Custom Colors + +### SwiftUI Color Struct + +The SwiftUI [Color](https://developer.apple.com/documentation/swiftui/color) structure accepts either the name of a color in the asset catalog or the RGB values of the color. + +Therefore we can define custom RBG styles like so: + +```swift +foregroundStyle(Color(.sRGB, red: 0.4627, green: 0.8392, blue: 1.0)) +``` + +Evaluate the example below to see the custom color in your simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + Hello, from LiveView Native! + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +### Custom Colors in the Asset Catalogue + +Custom colors can be defined in the [Asset Catalogue](https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). Once defined in the asset catalogue of the Xcode application, the color can be referenced by name like so: + +```swift +foregroundStyle(Color("MyColor")) +``` + +Generally using the asset catalog is more performant and customizable than using custom RGB colors with the [Color](https://developer.apple.com/documentation/swiftui/color) struct. + + + +### Your Turn: Custom Colors in the Asset Catalog + +Custom colors can be defined in the asset catalog (https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). Generat + +To create a new color go to the `Assets` folder in your iOS app and create a new color set. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/asset-catalogue-create-new-color-set.png?raw=true) + + + +To create a color set, enter the RGB values or a hexcode as shown in the image below. If you don't see the sidebar with color options, click the icon in the top-right of your Xcode app and click the **Show attributes inspector** icon shown highlighted in blue. + + + +![](https://github.com/liveview-native/documentation_assets/blob/main/asset-catalogue-modify-my-color.png?raw=true) + + + +The defined color is now available for use within LiveView Native styles. However, the app needs to be re-compiled to pick up a new color set. + +Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +``` + +## LiveView Native Stylesheets + +In LiveView Native, we use `~SHEET` sigil stylesheets to organize modifers by classes using an Elixir-oriented DSL similar to CSS for styling web elements. + +We group modifiers together within a class that can be applied to an element. Here's an example of how modifiers can be grouped into a "red-title" class in a stylesheet: + + + +```elixir +~SHEET""" + "red-title" do + foregroundColor(.red) + font(.title) + end +""" +``` + +We're mostly using Utility styles for these guides, but the stylesheet module does contain some important configuration to `@import` the utility styles module. It can also be used to group styles within a class if you have a set of modifiers you're repeatedly using and want to group together. + + + +```elixir +defmodule ServerWeb.Styles.App.SwiftUI do + use LiveViewNative.Stylesheet, :swiftui + @import LiveViewNative.SwiftUI.UtilityStyles + + ~SHEET""" + "red-title" do + foregroundColor(.red) + font(.title) + end + """ +end +``` + +Since the Phoenix server runs in a dependency for these guides, you don't have direct access to the stylesheet module. + +## Apple Documentation + +You can find documentation and examples of modifiers on [Apple's SwiftUI documentation](https://developer.apple.com/documentation/swiftui) which is comprehensive and thorough, though it may feel unfamiliar at first for Elixir Developers when compared to HexDocs. + + + +### Finding Modifiers + +The [Configuring View Elements](https://developer.apple.com/documentation/swiftui/view#configuring-view-elements) section of apple documentation contains links to modifiers organized by category. In that documentation you'll find useful references such as [Style Modifiers](https://developer.apple.com/documentation/swiftui/view-style-modifiers), [Layout Modifiers](https://developer.apple.com/documentation/swiftui/view-layout), and [Input and Event Modifiers](https://developer.apple.com/documentation/swiftui/view-input-and-events). + +You can also find the same modifiers with LiveView Native examples on the [LiveView Client SwiftUI Docs](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/paddingmodifier). + +## Visual Studio Code Extension + +If you use Visual Studio Code, we strongly recommend you install the [LiveView Native Visual Studio Code Extension](https://github.com/liveview-native/liveview-native-vscode) which provides autocompletion and type information thus making modifiers significantly easier to write and lookup. + +## Your Turn: Syntax Conversion + +Part of learning LiveView Native is learning SwiftUI. Fortunately we can leverage the existing SwiftUI ecosystem and convert examples into LiveView Native syntax. + +You're going to convert the following SwiftUI code into a LiveView Native template. This example is inspired by the official [SwiftUI Tutorials](https://developer.apple.com/tutorials/swiftui/creating-and-combining-views). + + + +```elixir + VStack { + VStack(alignment: .leading) { + Text("Turtle Rock") + .font(.title) + HStack { + Text("Joshua Tree National Park") + Spacer() + Text("California") + } + .font(.subheadline) + + Divider() + + Text("About Turtle Rock") + .font(.title2) + Text("Descriptive text goes here") + } + .padding() + + Spacer() +} +``` + +### Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + Turtle Rock + + Joshua Tree National Park + + California + + + About Turtle Rock + Descriptive text goes here + + """ + end +end +``` + + + +Enter your solution below. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + """ + end +end +``` diff --git a/guides/markdown_livebooks/swiftui-views.md b/guides/markdown_livebooks/swiftui-views.md new file mode 100644 index 000000000..6803a0906 --- /dev/null +++ b/guides/markdown_livebooks/swiftui-views.md @@ -0,0 +1,693 @@ +# SwiftUI Views + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%swiftui-views.livemd) + +## Overview + +LiveView Native aims to use minimal SwiftUI code. All patterns for building interactive UIs are the same as LiveView. However, unlike LiveView for the web, LiveView Native uses SwiftUI templates to build the native UI. + +This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common uses of each view and give you practical examples you can use to build your own native UIs. This lesson is like a recipe book you can refer back to whenever you need an example of how to use a particular SwiftUI view. In addition, once you understand how to convert these views into the LiveView Native DSL, you should have the tools to convert essentially any SwiftUI View into the LiveView Native DSL. + +## Render Components + +LiveView Native `0.3.0` introduced render components to better encourage isolation of native and web templates and move away from co-location templates within the same LiveView module. + +Render components are namespaced under the main LiveView, and are responsible for defining the `render/1` callback function that returns the native template. + +For example, and `ExampleLive` LiveView module would have an `ExampleLive.SwiftUI` render component module for the native Template. + +This `ExampleLive.SwiftUI` render component may define a `render/1` callback function as seen below. + + + +```elixir +# Render Component +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +# LiveView +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns) do + ~H""" +

Hello from LiveView!

+ """ + end +end +``` + +Throughout this and further material we'll re-define render components you can evaluate and see reflected in your Xcode iOS simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + Hello, from a LiveView Native Render Component! + """ + end +end +``` + +### Embedding Templates + +Alternatively, you may omit the render callback and instead define a `.neex` (Native + Embedded Elixir) template. + +By default, the module above would look for a template in the `swiftui/example_live*` path relative to the module's location. You can see the `LiveViewNative.Component` documentation for further explanation. + +For the sake of ease when working in Livebook, we'll prefer defining the `render/1` callback. However, we recommend you generally prefer template files when working locally in Phoenix LiveView Native projects. + +## SwiftUI Views + +In SwiftUI, a "View" is like a building block for what you see on your app's screen. It can be something simple like text or an image, or something more complex like a layout with multiple elements. Views are the pieces that make up your app's user interface. + +Here's an example `Text` view that represents a text element. + +```swift +Text("Hamlet") +``` + +LiveView Native uses the following syntax to represent the view above. + + + +```elixir +Hamlet +``` + +SwiftUI provides a wide range of Views that can be used in native templates. You can find a full reference of these views in the SwiftUI Documentation at https://developer.apple.com/documentation/swiftui/. You can also find a shorthand on how to convert SwiftUI syntax into the LiveView Native DLS in the [LiveView Native Syntax Conversion Cheatsheet](https://hexdocs.pm/live_view_native/cheatsheet.cheatmd). + +## Text + +We've already seen the [Text](https://developer.apple.com/documentation/swiftui/text) view, but we'll start simple to get the interactive tutorial running. + +Evaluate the cell below, then in Xcode, Start the iOS application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native/create-a-swiftui-application.html) lesson and ensure you see the `"Hello, from LiveView Native!"` text. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end +``` + +## HStack and VStack + +SwiftUI includes many [Layout](https://developer.apple.com/documentation/swiftui/layout-fundamentals) container views you can use to arrange your user Interface. Here are a few of the most commonly used: + +* [VStack](https://developer.apple.com/documentation/swiftui/vstack): Vertically arranges nested views. +* [HStack](https://developer.apple.com/documentation/swiftui/hstack): Horizontally arranges nested views. + +Below, we've created a simple 3X3 game board to demonstrate how to use `VStack` and `HStack` to build a layout of horizontal rows in a single vertical column.o + +Here's a diagram to demonstrate how these rows and columns create our desired layout. + +```mermaid +flowchart +subgraph VStack + direction TB + subgraph H1[HStack] + direction LR + 1[O] --> 2[X] --> 3[X] + end + subgraph H2[HStack] + direction LR + 4[X] --> 5[O] --> 6[O] + end + subgraph H3[HStack] + direction LR + 7[X] --> 8[X] --> 9[O] + end + H1 --> H2 --> H3 +end +``` + +Evaluate the example below and view the working 3X3 layout in your Xcode simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + O + X + X + + + X + O + O + + + X + X + O + + + """ + end +end +``` + +### Your Turn: 3x3 board using columns + +In the cell below, use `VStack` and `HStack` to create a 3X3 board using 3 columns instead of 3 rows as demonstrated above. The arrangement of `X` and `O` does not matter, however the content will not be properly aligned if you do not have exactly one character in each `Text` element. + +```mermaid +flowchart +subgraph HStack + direction LR + subgraph V1[VStack] + direction TB + 1[O] --> 2[X] --> 3[X] + end + subgraph V2[VStack] + direction TB + 4[X] --> 5[O] --> 6[O] + end + subgraph V3[VStack] + direction TB + 7[X] --> 8[X] --> 9[O] + end + V1 --> V2 --> V3 +end +``` + +### Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + O + X + X + + + X + O + O + + + X + X + O + + + """ + end +end +``` + + + + + +### Enter Your Solution Below + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +## Grid + +`VStack` and `HStack` do not provide vertical-alignment between horizontal rows. Notice in the following example that the rows/columns of the 3X3 board are not aligned, just centered. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + X + X + + + X + O + O + + + X + O + + + """ + end +end +``` + +Fortunately, we have a few common elements for creating a grid-based layout. + +* [Grid](https://developer.apple.com/documentation/swiftui/grid): A grid that arranges its child views in rows and columns that you specify. +* [GridRow](https://developer.apple.com/documentation/swiftui/gridrow): A view that arranges its children in a horizontal line. + +A grid layout vertically and horizontally aligns elements in the grid based on the number of elements in each row. + +Evaluate the example below and notice that rows and columns are aligned. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + XX + X + X + + + X + X + + + X + X + X + + + """ + end +end +``` + +## List + +The SwiftUI [List](https://developer.apple.com/documentation/swiftui/list) view provides a system-specific interface, and has better performance for large amounts of scrolling elements. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + Item 1 + Item 2 + Item 3 + + """ + end +end +``` + +### Multi-dimensional lists + +Alternatively we can separate children within a `List` view in a `Section` view as seen in the example below. Views in the `Section` can have the `template` attribute with a `"header"` or `"footer"` value which controls how the content is displayed above or below the section. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + +
+ Header + Content + Footer +
+
+ """ + end +end +``` + +## ScrollView + +The SwiftUI [ScrollView](https://developer.apple.com/documentation/swiftui/scrollview) displays content within a scrollable region. ScrollView is often used in combination with [LazyHStack](https://developer.apple.com/documentation/swiftui/lazyvstack), [LazyVStack](https://developer.apple.com/documentation/swiftui/lazyhstack), [LazyHGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid), and [LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid) to create scrollable layouts optimized for displaying large amounts of data. + +While `ScrollView` also works with typical `VStack` and `HStack` views, they are not optimal choices for large amounts of data. + + + +### ScrollView with VStack + +Here's an example using a `ScrollView` and a `HStack` to create scrollable text arranged horizontally. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +``` + +### ScrollView with HStack + +By default, the [axes](https://developer.apple.com/documentation/swiftui/scrollview/axes) of a `ScrollView` is vertical. To make a horizontal `ScrollView`, set the `axes` attribute to `"horizontal"` as seen in the example below. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +``` + +### Optimized ScrollView with LazyHStack and LazyVStack + +`VStack` and `HStack` are inefficient for large amounts of data because they render every child view. To demonstrate this, evaluate the example below. You should experience lag when you attempt to scroll. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +``` + +To resolve the performance problem for large amounts of data, you can use the Lazy views. Lazy views only create items as needed. Items won't be rendered until they are present on the screen. + +The next example demonstrates how using `LazyVStack` instead of `VStack` resolves the performance issue. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + Item <%= n %> + + + """ + end +end +``` + +## Spacers + +[Spacers](https://developer.apple.com/documentation/swiftui/spacer) take up all remaining space in a container. + +![Apple Documentation](https://docs-assets.developer.apple.com/published/189fa436f07ed0011bd0c1abeb167723/Building-Layouts-with-Stack-Views-4@2x.png) + +> Image originally from https://developer.apple.com/documentation/swiftui/spacer + +Evaluate the following example and notice the `Text` element is pushed to the right by the `Spacer`. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + This text is pushed to the right + + """ + end +end +``` + +### Your Turn: Bottom Text Spacer + +In the cell below, use `VStack` and `Spacer` to place text in the bottom of the native view. + +### Example Solution + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns, _interface) do + ~LVN""" + + + Hello + + """ + end +end +``` + + + + + +### Enter Your Solution Below + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +## AsyncImage + +`AsyncImage` is best for network images, or images served by the Phoenix server. + +Here's an example of `AsyncImage` with a lorem picsum image from https://picsum.photos/400/600. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +### Loading Spinner + +`AsyncImage` displays a loading spinner while loading the image. Here's an example of using `AsyncImage` without a URL so that it loads forever. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +### Relative Path + +For images served by the Phoenix server, LiveView Native evaluates URLs relative to the LiveView's host URL. This way you can use the path to static resources as you normally would in a Phoenix application. + +For example, the path `/images/logo.png` evaluates as http://localhost:4000/images/logo.png below. This serves the LiveView Native logo. + +Evaluate the example below to see the LiveView Native logo in the iOS simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +## Image + +The `Image` element is best for system images such as the built in [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) or images placed into the SwiftUI [asset catalogue](https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). + + + +### System Images + +You can use the `system-image` attribute to provide the name of system images to the `Image` element. + +For the full list of SF Symbols you can download Apple's [Symbols 5](https://developer.apple.com/sf-symbols/) application. + +Evaluate the cell below to see an example using the `square.and.arrow.up` symbol. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +### Your Turn: Asset Catalogue + +You can place assets in your SwiftUI application's asset catalogue. Using the asset catalogue for SwiftUI assets provide many benefits such as device-specific image variants, dark mode images, high contrast image mode, and improved performance. + +Follow this guide: https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs#Add-a-new-asset to create a new asset called Image. + +Then evaluate the following example and you should see this image in your simulator. For a convenient image, you can right-click and save the following LiveView Native logo. + +![LiveView Native Logo](https://github.com/liveview-native/documentation_assets/blob/main/logo.png?raw=true) + +You will need to **rebuild the native application** to pick up the changes to the assets catalogue. + + + +### Enter Your Solution Below + +You should not need to make changes to this cell. Set up an image in your asset catalogue named "Image", rebuild your native application, then evaluate this cell. You should see the image in your iOS simulator. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + """ + end +end +``` + +## Button + +A Button is a clickable SwiftUI View. + +The label of a button can be any view, such as a [Text](https://developer.apple.com/documentation/swiftui/text) view for text-only buttons or a [Label](https://developer.apple.com/documentation/swiftui/label) view for buttons with icons. + +Evaluate the example below to see the SwiftUI [Button](https://developer.apple.com/documentation/swiftui/button) element. + + + +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use LiveViewNative.Component, + format: :swiftui + + def render(assigns, _interface) do + ~LVN""" + + + """ + end +end +``` + +## Further Resources + +See the [SwiftUI Documentation](https://developer.apple.com/documentation/swiftui) for a complete list of SwiftUI elements and the [LiveView Native SwiftUI Documentation](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/) for LiveView Native examples of the SwiftUI elements. diff --git a/lib/mix/tasks/livebooks_to_markdown.ex b/lib/mix/tasks/livebooks_to_markdown.ex new file mode 100644 index 000000000..a00373a4f --- /dev/null +++ b/lib/mix/tasks/livebooks_to_markdown.ex @@ -0,0 +1,48 @@ + +defmodule Mix.Tasks.LivebooksToMarkdown do + @moduledoc "Generates ex_doc friendly markdown guides from Livebook notebooks" + @destination "guides/markdown_livebooks" + use Mix.Task + def run(_args) do + # clean up old notebooks + File.rm_rf(@destination) + File.mkdir(@destination) + + File.ls!("guides/livebooks") |> Enum.filter(fn file_name -> file_name =~ ".livemd" end) + |> Enum.each(fn file_name -> + ex_doc_friendly_content = make_ex_doc_friendly(File.read!("guides/livebooks/#{file_name}"), file_name) + File.write!("#{@destination}/#{Path.basename(file_name, ".livemd")}.md", ex_doc_friendly_content) + end) + end + + def make_ex_doc_friendly(content, file_name) do + content + |> replace_setup_section_with_badge(file_name) + |> remove_kino_boilerplate() + |> convert_details_sections() + end + + defp replace_setup_section_with_badge(content, file_name) do + badge = "[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%#{file_name})" + String.replace(content, ~r/```elixir(.|\n)+?```/, badge, global: false) + end + + defp remove_kino_boilerplate(content) do + content + |> String.replace(""" + require Server.Livebook + import Server.Livebook + import Kernel, except: [defmodule: 2] + + """, "") + |> String.replace(~r/\|\> Server\.SmartCells\.LiveViewNative\.register\(\".+\"\)\n\nimport Server\.Livebook, only: \[\]\nimport Kernel\n:ok\n/, "") + |> String.replace(~r/\|\> Server\.SmartCells\.RenderComponent\.register\(\)\n\nimport Server\.Livebook, only: \[\]\nimport Kernel\n:ok\n/, "") + end + + defp convert_details_sections(content) do + # Details sections do not properly render on ex_doc, so we convert them to headers + Regex.replace(~r/([^<]+)<\/summary>((.|\n)+?)(?=<\/details>)<\/details>/, content, fn _full, title, content -> + "### #{title}#{content}" + end) + end +end diff --git a/mix.exs b/mix.exs index 92513bc8e..a51fa6734 100644 --- a/mix.exs +++ b/mix.exs @@ -33,7 +33,7 @@ defmodule LiveViewNative.SwiftUI.MixProject do defp aliases do [ - docs: ["lvn.swiftui.gen.docs", "docs"] + docs: ["lvn.swiftui.gen.docs", "livebooks_to_markdown", "docs"] ] end @@ -63,22 +63,9 @@ defmodule LiveViewNative.SwiftUI.MixProject do end defp docs do - guides = Path.wildcard("guides/**/*.md") - generated_docs = Path.wildcard("generated_docs/**/*.{md,cheatmd}") - - extras = ["README.md"] ++ guides ++ generated_docs - - guide_groups = [ - "Architecture": Path.wildcard("guides/architecture/*.md") - ] - - generated_groups = - Path.wildcard("generated_docs/*") - |> Enum.map(&({Path.basename(&1) |> String.to_atom(), Path.wildcard("#{&1}/*.md")})) - [ - extras: extras, - groups_for_extras: guide_groups ++ generated_groups, + extras: extras(), + groups_for_extras: groups_for_extras(), main: "readme", source_url: @source_url, source_ref: "v#{@version}", @@ -106,6 +93,10 @@ defmodule LiveViewNative.SwiftUI.MixProject do } }); + """ } ] @@ -113,6 +104,40 @@ defmodule LiveViewNative.SwiftUI.MixProject do defp description, do: "LiveView Native SwiftUI Client" + defp extras do + guides = Path.wildcard("guides/**/*.md") + generated_docs = Path.wildcard("generated_docs/**/*.{md,cheatmd}") + + livebooks = if System.get_env("LIVEBOOKS_ENABLED") do + [ + "guides/markdown_livebooks/getting-started.md", + "guides/markdown_livebooks/create-a-swiftui-application.md", + "guides/markdown_livebooks/swiftui-views.md", + "guides/markdown_livebooks/interactive-swiftui-views.md", + "guides/markdown_livebooks/stylesheets.md", + "guides/markdown_livebooks/native-navigation.md", + "guides/markdown_livebooks/forms-and-validation.md" + ] + else + [] + end + + ["README.md"] ++ guides ++ generated_docs ++ livebooks + end + + defp groups_for_extras do + guide_groups = [ + "Architecture": Path.wildcard("guides/architecture/*.md"), + "Livebooks": ~r/markdown_livebooks/ + ] + + generated_groups = + Path.wildcard("generated_docs/*") + |> Enum.map(&({Path.basename(&1) |> String.to_atom(), Path.wildcard("#{&1}/*.md")})) + + guide_groups ++ generated_groups + end + defp package do %{ maintainers: ["Brian Cardarella"], diff --git a/test/mix/tasks/livebooks_to_markdown_test.exs b/test/mix/tasks/livebooks_to_markdown_test.exs new file mode 100644 index 000000000..fe61cda57 --- /dev/null +++ b/test/mix/tasks/livebooks_to_markdown_test.exs @@ -0,0 +1,129 @@ +defmodule Mix.Tasks.LivebooksToMarkdownTest do + use ExUnit.Case + alias Mix.Tasks.LivebooksToMarkdown + + test "make_ex_doc_friendly/1 removes Mix.install/2 section and adds Run in Livebook badge" do + content = """ + ```elixir + notebook_path = __ENV__.file |> String.split("#") |> hd() + + Mix.install( + [ + {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} + ], + config: [ + server: [ + {ServerWeb.Endpoint, + [ + server: true, + url: [host: "localhost"], + adapter: Phoenix.Endpoint.Cowboy2Adapter, + render_errors: [ + formats: [html: ServerWeb.ErrorHTML, json: ServerWeb.ErrorJSON], + layout: false + ], + pubsub_server: Server.PubSub, + live_view: [signing_salt: "JSgdVVL6"], + http: [ip: {127, 0, 0, 1}, port: 4000], + secret_key_base: String.duplicate("a", 64), + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg|styles)$", + ~r/\#{notebook_path}$/ + ] + ] + ]} + ], + kino: [ + group_leader: Process.group_leader() + ], + phoenix: [ + template_engines: [neex: LiveViewNative.Engine] + ], + phoenix_template: [format_encoders: [swiftui: Phoenix.HTML.Engine]], + mime: [ + types: %{"text/swiftui" => ["swiftui"], "text/styles" => ["styles"]} + ], + live_view_native: [plugins: [LiveViewNative.SwiftUI]], + live_view_native_stylesheet: [ + content: [ + swiftui: [ + "lib/**/*swiftui*", + notebook_path + ] + ], + output: "priv/static/assets" + ] + ], + force: true + ) + ``` + """ + assert LivebooksToMarkdown.make_ex_doc_friendly(content, "filename.livemd") =~ + "[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%filename.livemd)" + end + + test "make_ex_doc_friendly/1 removes initial Kino boilerplate in smart cells" do + content = """ + require Server.Livebook + import Server.Livebook + import Kernel, except: [defmodule: 2] + + """ + + assert LivebooksToMarkdown.make_ex_doc_friendly(content, "filename.livemd") == """ + """ + end + + test "make_ex_doc_friendly/1 removes ending Kino boilerplate in LiveViewNative smart cells" do + url = Enum.random(["/", "/path", "path/subpath", "path/1"]) + + content = """ + |> Server.SmartCells.LiveViewNative.register("#{url}") + + import Server.Livebook, only: [] + import Kernel + :ok + """ + + assert LivebooksToMarkdown.make_ex_doc_friendly(content, "filename.livemd") == """ + """ + end + + test "make_ex_doc_friendly/1 removes ending Kino boilerplate in Render Component smart cells" do + content = """ + |> Server.SmartCells.RenderComponent.register() + + import Server.Livebook, only: [] + import Kernel + :ok + """ + + assert LivebooksToMarkdown.make_ex_doc_friendly(content, "filename.livemd") == """ + """ + end + + + test "make_ex_doc_friendly/1 convert details sections" do + content = """ +
+ What do these options mean? + + * **Product Name:** The name of the application. This can be any valid name. We've chosen `Guides`. + * **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recomends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using `com.example.your_name` where `your_name` is your organization or personal name. + * **Interface:**: Xcode generates an interface file that includes all your source code's internal and public declarations when using the Assistant editor, the Related Items, or the Navigate menu. Select `SwiftUI` since we're building a SwiftUI application. + * **Language:** Determines which language Xcode should use for the project. Select `Swift`. +
+ """ + result = LivebooksToMarkdown.make_ex_doc_friendly(content, "filename.livemd") + refute result =~ "details" + assert result =~ """ + ### What do these options mean? + + * **Product Name:** The name of the application. This can be any valid name. We've chosen `Guides`. + * **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recomends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using `com.example.your_name` where `your_name` is your organization or personal name. + * **Interface:**: Xcode generates an interface file that includes all your source code's internal and public declarations when using the Assistant editor, the Related Items, or the Navigate menu. Select `SwiftUI` since we're building a SwiftUI application. + * **Language:** Determines which language Xcode should use for the project. Select `Swift`. + """ + end +end From 2c703325ae77db22b7fa8c281e37f2cb371c9b8d Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Tue, 30 Apr 2024 13:32:53 -0400 Subject: [PATCH 02/62] add pretty: true config to live_view_native_stylesheet in livebook lessons --- guides/livebooks/create-a-swiftui-application.livemd | 1 + guides/livebooks/getting-started.livemd | 1 + guides/livebooks/interactive-swiftui-views.livemd | 1 + guides/livebooks/native-navigation.livemd | 1 + guides/livebooks/swiftui-views.livemd | 1 + 5 files changed, 5 insertions(+) diff --git a/guides/livebooks/create-a-swiftui-application.livemd b/guides/livebooks/create-a-swiftui-application.livemd index 06a322508..a883bd6b3 100644 --- a/guides/livebooks/create-a-swiftui-application.livemd +++ b/guides/livebooks/create-a-swiftui-application.livemd @@ -50,6 +50,7 @@ Mix.install( notebook_path ] ], + pretty: true, output: "priv/static/assets" ] ], diff --git a/guides/livebooks/getting-started.livemd b/guides/livebooks/getting-started.livemd index bc2c2dadc..9574c9b98 100644 --- a/guides/livebooks/getting-started.livemd +++ b/guides/livebooks/getting-started.livemd @@ -48,6 +48,7 @@ Mix.install( notebook_path ] ], + pretty: true, output: "priv/static/assets" ] ], diff --git a/guides/livebooks/interactive-swiftui-views.livemd b/guides/livebooks/interactive-swiftui-views.livemd index 7f25b39fd..738ebe70d 100644 --- a/guides/livebooks/interactive-swiftui-views.livemd +++ b/guides/livebooks/interactive-swiftui-views.livemd @@ -49,6 +49,7 @@ Mix.install( notebook_path ] ], + pretty: true, output: "priv/static/assets" ] ], diff --git a/guides/livebooks/native-navigation.livemd b/guides/livebooks/native-navigation.livemd index 0188e0d1d..c1dfdef38 100644 --- a/guides/livebooks/native-navigation.livemd +++ b/guides/livebooks/native-navigation.livemd @@ -48,6 +48,7 @@ Mix.install( notebook_path ] ], + pretty: true, output: "priv/static/assets" ] ], diff --git a/guides/livebooks/swiftui-views.livemd b/guides/livebooks/swiftui-views.livemd index 326f6198a..a89dfd630 100644 --- a/guides/livebooks/swiftui-views.livemd +++ b/guides/livebooks/swiftui-views.livemd @@ -48,6 +48,7 @@ Mix.install( notebook_path ] ], + pretty: true, output: "priv/static/assets" ] ], From a01e782bfb5dbb0ae3e2293bae3b3b522c1f7f72 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Tue, 30 Apr 2024 13:59:08 -0400 Subject: [PATCH 03/62] move livebooks --- .../create-a-swiftui-application.md | 213 ----- guides/ex_doc_notebooks/getting-started.md | 87 -- .../forms-and-validation.md | 640 --------------- .../interactive-swiftui-views.md | 756 ------------------ .../markdown_livebooks/native-navigation.md | 303 ------- guides/markdown_livebooks/stylesheets.md | 538 ------------- guides/markdown_livebooks/swiftui-views.md | 693 ---------------- lib/mix/tasks/livebooks_to_markdown.ex | 9 +- .../create-a-swiftui-application.livemd | 0 .../forms-and-validation.livemd | 56 +- .../getting-started.livemd | 0 .../interactive-swiftui-views.livemd | 0 .../markdown}/create-a-swiftui-application.md | 2 +- .../markdown}/forms-and-validation.md | 2 +- .../markdown}/getting-started.md | 2 +- .../markdown}/interactive-swiftui-views.md | 2 +- .../markdown}/native-navigation.md | 2 +- .../markdown}/stylesheets.md | 22 +- .../markdown}/swiftui-views.md | 2 +- .../native-navigation.livemd | 0 .../stylesheets.livemd | 0 .../swiftui-views.livemd | 0 mix.exs | 20 +- test/mix/tasks/livebooks_to_markdown_test.exs | 2 +- 24 files changed, 86 insertions(+), 3265 deletions(-) delete mode 100644 guides/ex_doc_notebooks/create-a-swiftui-application.md delete mode 100644 guides/ex_doc_notebooks/getting-started.md delete mode 100644 guides/markdown_livebooks/forms-and-validation.md delete mode 100644 guides/markdown_livebooks/interactive-swiftui-views.md delete mode 100644 guides/markdown_livebooks/native-navigation.md delete mode 100644 guides/markdown_livebooks/stylesheets.md delete mode 100644 guides/markdown_livebooks/swiftui-views.md rename {guides/livebooks => livebooks}/create-a-swiftui-application.livemd (100%) rename {guides/livebooks => livebooks}/forms-and-validation.livemd (95%) rename {guides/livebooks => livebooks}/getting-started.livemd (100%) rename {guides/livebooks => livebooks}/interactive-swiftui-views.livemd (100%) rename {guides/markdown_livebooks => livebooks/markdown}/create-a-swiftui-application.md (98%) rename {guides/ex_doc_notebooks => livebooks/markdown}/forms-and-validation.md (99%) rename {guides/markdown_livebooks => livebooks/markdown}/getting-started.md (97%) rename {guides/ex_doc_notebooks => livebooks/markdown}/interactive-swiftui-views.md (99%) rename {guides/ex_doc_notebooks => livebooks/markdown}/native-navigation.md (99%) rename {guides/ex_doc_notebooks => livebooks/markdown}/stylesheets.md (93%) rename {guides/ex_doc_notebooks => livebooks/markdown}/swiftui-views.md (99%) rename {guides/livebooks => livebooks}/native-navigation.livemd (100%) rename {guides/livebooks => livebooks}/stylesheets.livemd (100%) rename {guides/livebooks => livebooks}/swiftui-views.livemd (100%) diff --git a/guides/ex_doc_notebooks/create-a-swiftui-application.md b/guides/ex_doc_notebooks/create-a-swiftui-application.md deleted file mode 100644 index 134e8af89..000000000 --- a/guides/ex_doc_notebooks/create-a-swiftui-application.md +++ /dev/null @@ -1,213 +0,0 @@ - - -# Create a SwiftUI Application - -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%create-a-swiftui-application.livemd) - -## Overview - -This guide will teach you how to set up a SwiftUI Application for LiveView Native. - -Typically, we recommend using the `mix lvn.install` task as described in the [Installation Guide](https://hexdocs.pm/live_view_native/installation.html#5-enable-liveview-native) to add LiveView Native to a Phoenix project. However, we will walk through the steps of manually setting up an Xcode iOS project to learn how the iOS side of a LiveView Native application works. - -In future lessons, you'll use this iOS application to view iOS examples in the Xcode simulator (or a physical device if you prefer.) - -## Prerequisites - -First, make sure you have followed the [Getting Started](https://hexdocs.pm/live_view_native/getting_started.md) guide. Then evaluate the smart cell below and visit http://localhost:4000 to ensure the Phoenix server runs properly. You should see the text `Hello from LiveView!` - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - Hello, from LiveView Native! - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" -end -``` - -## Create the iOS Application - -Open Xcode and select Create New Project. - - - -![Xcode Create New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-create-new-project.png?raw=true) - - - -Select the `iOS` and `App` options to create an iOS application. Then click `Next`. - - - -![Xcode Create Template For New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-create-template-for-new-project.png?raw=true) - - - -Choose options for your new project that match the following image, then click `Next`. - -### What do these options mean? - -* **Product Name:** The name of the application. This can be any valid name. We've chosen `Guides`. -* **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recomends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using `com.example.your_name` where `your_name` is your organization or personal name. -* **Interface:**: The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. -* **Language:** Determines which language Xcode should use for the project. Select `Swift`. - - - - -![Xcode Choose Options For Your New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-choose-options-for-your-new-project.png?raw=true) - - - -Select an appropriate folder location where you would like to store the iOS project, then click `Create`. - - - -![Xcode select folder location](https://github.com/liveview-native/documentation_assets/blob/main/xcode-select-folder-location.png?raw=true) - - - -You should see the default iOS application generated by Xcode. - - - -![](https://github.com/liveview-native/documentation_assets/blob/main/default-xcode-app.png?raw=true) - -## Add the LiveView Client SwiftUI Package - -In Xcode from the project you just created, select `File -> Add Package Dependencies`. Then, search for `liveview-client-swiftui`. Once you have selected the package, click `Add Package`. - -The image below was created using version `0.2.0`. You should select whichever is the latest version of LiveView Native. - - - -![](https://github.com/liveview-native/documentation_assets/blob/main/add-liveview-swiftui-client-package-0.2.0.png?raw=true) - - - -Choose the Package Products for `liveview-client-swiftui`. Select `Guides` as the target for `LiveViewNative` and `LiveViewNativeStylesheet`. This adds both of these dependencies to your iOS project. - - - -![](https://github.com/liveview-native/documentation_assets/blob/main/select-package-products.png?raw=true) - - - -At this point, you'll need to enable permissions for plugins used by LiveView Native. -You should see the following prompt. Click `Trust & Enable All`. - - - -![Xcode some build plugins are disabled](https://github.com/liveview-native/documentation_assets/blob/main/xcode-some-build-plugins-are-disabled.png?raw=true) - - - -You'll also need to manually navigate to the error tab (shown below) and manually trust and enable packages. Click on each error to trigger a prompt. Select `Trust & Enable All` to enable the plugin. - -The specific plugins are subject to change. At the time of writing you need to enable `LiveViewNativeStylesheetMacros`, `LiveViewNativeMacros`, and `CasePathMacros` as shown in the images below. - - - -![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-liveview-native-stylesheet.png?raw=true) - - - -![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-liveview-native-macros.png?raw=true) - - - -![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-case-path-macros.png?raw=true) - -## Setup the SwiftUI LiveView - -The [ContentView](https://developer.apple.com/tutorials/swiftui-concepts/exploring-the-structure-of-a-swiftui-app#Content-view) contains the main view of our iOS application. - -Replace the code in the `ContentView` file with the following to connect the SwiftUI application and the Phoenix application. - - - -```swift -import SwiftUI -import LiveViewNative - -struct ContentView: View { - - var body: some View { - LiveView(.automatic( - development: .localhost(path: "/"), - production: .custom(URL(string: "https://example.com/")!) - )) - } -} - - -// Optionally preview the native UI in Xcode -#Preview { - ContentView() -} -``` - - - -The code above sets up the SwiftUI LiveView. By default, the SwiftUI LiveView connects to any Phoenix app running on http://localhost:4000. - - - - - -```mermaid -graph LR; - subgraph I[iOS App] - direction TB - ContentView - SL[SwiftUI LiveView] - end - subgraph P[Phoenix App] - LiveView - end - SL --> P - ContentView --> SL - - -``` - -## Start the Active Scheme - -Click the `start active scheme` button to build the project and run it on the iOS simulator. - -> A [build scheme](https://developer.apple.com/documentation/xcode/build-system) contains a list of targets to build, and any configuration and environment details that affect the selected action. For example, when you build and run an app, the scheme tells Xcode what launch arguments to pass to the app. -> -> * https://developer.apple.com/documentation/xcode/build-system - -After you start the active scheme, the simulator should open the iOS application and display `Hello from LiveView Native!`. If you encounter any issues see the **Troubleshooting** section below. - - - -
- -
- -## Troubleshooting - -If you encountered any issues with the native application, here are some troubleshooting steps you can use: - -* **Reset Package Caches:** In the Xcode application go to `File -> Packages -> Reset Package Caches`. -* **Update Packages:** In the Xcode application go to `File -> Packages -> Update to Latest Package Versions`. -* **Rebuild the Active Scheme**: In the Xcode application, press the `start active scheme` button to rebuild the active scheme and run it on the Xcode simulator. -* Update your [Xcode](https://developer.apple.com/xcode/) version if it is not already the latest version -* Check for error messages in the Livebook smart cells. - -You can also [raise an issue](https://github.com/liveview-native/live_view_native/issues/new) if you would like support from the LiveView Native team. diff --git a/guides/ex_doc_notebooks/getting-started.md b/guides/ex_doc_notebooks/getting-started.md deleted file mode 100644 index 63e640e5e..000000000 --- a/guides/ex_doc_notebooks/getting-started.md +++ /dev/null @@ -1,87 +0,0 @@ -# Getting Started - -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%getting-started.livemd) - -## Overview - -Our livebook guides provide step-by-step lessons to help you learn LiveView Native using Livebook. These guides assume that you already have some familiarity with Phoenix LiveView applications. - -You can read these guides online, or for the best experience we recommend you click on the "Run in Livebook" badge to import and run these guides locally with Livebook. - -Each guide can be completed independently, but we suggest following them chronologically for the most comprehensive learning experience. - -## Prerequisites - -To use these guides, you'll need to install the following prerequisites: - -* [Elixir/Erlang](https://elixir-lang.org/install.html) -* [Livebook](https://livebook.dev/) -* [Xcode](https://developer.apple.com/xcode/) - -While not necessary for our guides, we also recommend you install the following for general LiveView Native development: - -* [Phoenix](https://hexdocs.pm/phoenix/installation.html) -* [PostgreSQL](https://www.postgresql.org/download/) -* [LiveView Native VS Code Extension](https://github.com/liveview-native/liveview-native-vscode) - -## Hello World - -If you are not already running this guide in Livebook, click on the "Run in Livebook" badge at the top of this page to import this guide into Livebook. - -Then, you can evaluate the following smart cell and visit http://localhost:4000 to ensure this Livebook works correctly. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - Hello, from LiveView Native! - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns) do - ~H""" -

Hello from LiveView!

- """ - end -end -``` - -In an upcoming lesson, you'll set up an iOS application with Xcode so you can run code native examples. - -## Your Turn: Live Reloading - -Change `Hello from LiveView!` to `Hello again from LiveView!` in the above LiveView. Re-evaluate the cell and notice the application live reloads and automatically updates in the browser. - -## Kino LiveView Native - -To run a Phoenix Server setup with LiveView Native from within Livebook we built the [Kino LiveView Native](https://github.com/liveview-native/kino_live_view_native) library. - -Whenever you run one of our Livebooks, a server starts on localhost:4000. Ensure you have no other servers running on port 4000 - -Kino LiveView Native defines the **LiveView Native: LiveView** and **LiveViewNative: Render Component** smart cells within these guides. - -## Troubleshooting - -Some common issues you may encounter are: - -* Another server is already running on port 4000. -* Your version of Livebook needs to be updated. -* Your version of Elixir/Erlang needs to be updated. -* Your version of Xcode needs to be updated. -* This Livebook has cached outdated versions of dependencies - -Ensure you have the latest versions of all necessary software installed, and ensure no other servers are running on port 4000. - -To clear the cache, you can click the `Setup without cache` button revealed by clicking the dropdown next to the `setup` button at the top of the Livebook. - -If that does not resolve the issue, you can [raise an issue](https://github.com/liveview-native/live_view_native/issues/new) to receive support from the LiveView Native team. diff --git a/guides/markdown_livebooks/forms-and-validation.md b/guides/markdown_livebooks/forms-and-validation.md deleted file mode 100644 index 828e14a9c..000000000 --- a/guides/markdown_livebooks/forms-and-validation.md +++ /dev/null @@ -1,640 +0,0 @@ -# Forms and Validation - -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%forms-and-validation.livemd) - -## Overview - -The [LiveView Native Live Form](https://github.com/liveview-native/liveview-native-live-form) project makes it easier to build forms in LiveView Native. This project enables you to group different [Control Views](https://developer.apple.com/documentation/swiftui/controls-and-indicators) inside of a `LiveForm` and control them collectively under a single `phx-change` or `phx-submit` event handler, rather than with multiple different `phx-change` event handlers. - -Getting the most out of this material requires some understanding of the [Ecto](https://hexdocs.pm/ecto/Ecto.html) project and in particular a reasonably deep understanding of [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html). Review the linked Ecto documentation if you find any of the examples difficult to follow. - -## Installing LiveView Native Live Form - -To install LiveView Native Form, we need to add the `liveview-native-live-form` SwiftUI package to our iOS application. - -Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README and come back to this guide after you have finished the installation process. - -## Creating a Basic Form - -Once you have the LiveView Native Form package installed, you can use the `LiveForm` and `LiveSubmitButton` views to build forms more conveniently. - -Here's a basic example of a `LiveForm`. Keep in mind that `LiveForm` requires an `id` attribute. - - - -```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] - -defmodule Server.ExampleLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Placeholder - Submit - - """ - end - - @impl true - def handle_event("submit", params, socket) do - IO.inspect(params) - {:noreply, socket} - end -end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok -``` - -When a form is submitted, its data is sent as a map where each key is the 'name' attribute of the form's control views. Evaluate the example above in your simulator and you will see a map similar to the following: - - - -```elixir -%{"my-text" => "some value"} -``` - -In a real-world application you could use these params to trigger some application logic, such as inserting a record into the database. - -## Controls and Indicators - -We've already covered many individual controls and indicator views that you can use inside of forms. For more information on those, go to the [Interactive SwiftUI Views](https://hexdocs.pm/live_view_native/interactive-swiftui-views.html) guide. - - - -### Your Turn - -Create a form that has `TextField`, `Slider`, `Toggle`, and `DatePicker` fields. - -### Example Solution - -```elixir -defmodule Server.MultiInputFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Placeholder - - - - Submit - - """ - end - - @impl true - def handle_event("submit", params, socket) do - IO.inspect(params) - {:noreply, socket} - end -end -``` - - - - - -```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] - -defmodule Server.MultiInputFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - """ - end - - # You may use this handler to test your solution. - # You should not need to modify this handler. - @impl true - def handle_event("submit", params, socket) do - IO.inspect(params) - {:noreply, socket} - end -end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok -``` - -### Controlled Values - -Some control views such as the `Stepper` require manually displaying their value. In this case, we can store the form params in the socket and update them everytime the `phx-change` form binding submits an event. You can also use this pattern to provide default values. - -Evaluate the example below to see this in action. - - - -```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] - -defmodule Server.StepperLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, params: %{"my-stepper" => 1})} - end - - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - <%= @params["my-stepper"] %> - - """ - end - - @impl true - def handle_event("change", params, socket) do - IO.inspect(params) - {:noreply, assign(socket, params: params)} - end -end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok -``` - -### Secure Field - -For password entry, or anytime you want to hide a given value, you can use the [SecureField](https://developer.apple.com/documentation/swiftui/securefield) view. This field works mostly the same as a `TextField` but hides the visual text. - - - -```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] - -defmodule Server.SecureLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - Enter a Password - """ - end - - @impl true - def handle_event("change", params, socket) do - IO.inspect(params) - {:noreply, socket} - end -end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok -``` - -## Keyboard Types - -To format a `TextField` for specific input types we can use the [keyboardType](https://developer.apple.com/documentation/swiftui/view/keyboardtype(_:)) modifier. - -For a complete list of accepted keyboard types, see the [UIKeyboardType](https://developer.apple.com/documentation/uikit/uikeyboardtype) documentation. - -Below we've created several different common keyboard types. We've also included a generic `keyboard-*` to demonstrate how you can make a reusable class. - -```elixir -defmodule KeyboardStylesheet do - use LiveViewNative.Stylesheet, :swiftui - - ~SHEET""" - "number-pad" do - keyboardType(.numberPad) - end - - "email-address" do - keyboardType(.emailAddress) - end - - "phone-pad" do - keyboardType(.phonePad) - end - - "keyboard-" <> type do - keyboardType(to_ime(type)) - end - """ -end -``` - -Evaluate the example below to see the different keyboards as you focus on each input. If you don't see the keyboard, go to `I/O` -> `Keyboard` -> `Toggle Software Keyboard` to enable the software keyboard in your simulator. - - - -```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] - -defmodule Server.KeyboardLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use KeyboardStylesheet - - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Enter Phone - Enter Number - Enter Number - """ - end - - def render(assigns) do - ~H""" -

Hello from LiveView!

- """ - end -end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok -``` - -## Validation - -In this section, we'll focus mainly on using [Ecto Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) to validate data, but know that this is not the only way to validate data if you would like to write your own custom logic in the form event handlers, you absolutely can. - - - -### LiveView Native Changesets Coming Soon! - -LiveView Native Form doesn't currently natively support [Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) and [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) structs the way a traditional [Phoenix.Component.form](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1) does. However there is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/5) to add this behavior so this may change in the near future. As a result, this section is somewhat more verbose than will be necessary in the future, as we have to manually define much of the error handling logic that we expect will no longer be necessary in version `0.3` of LiveView Native. - -To make error handling easier, we've defined an `ErrorUtils` module below that will handle extracting the error message out of a Changeset. This will not be necessary in future versions of LiveView Native, but is a convenient helper for now. - -```elixir -defmodule ErrorUtils do - def error_message(errors, field) do - with {msg, opts} <- errors[field] do - Server.CoreComponents.translate_error({msg, opts}) - else - _ -> "" - end - end -end -``` - -For the sake of context, the `translate_message/2` function handles formatting Ecto Changeset errors. For example, it will inject values such as `count` into the string. - -```elixir -Server.CoreComponents.translate_error( - {"name must be longer than %{count} characters", [count: 10]} -) -``` - -### Changesets - -Here's a `User` changeset we're going to use to validate a `User` struct's `email` field. - -```elixir -defmodule User do - import Ecto.Changeset - defstruct [:email] - @types %{email: :string} - - def changeset(user, params) do - {user, @types} - |> cast(params, [:email]) - |> validate_required([:email]) - |> validate_format(:email, ~r/@/) - end -end -``` - -We're going to define an `error` class so errors will appear red and be left-aligned. - -```elixir -defmodule ErrorStylesheet do - use LiveViewNative.Stylesheet, :swiftui - - ~SHEET""" - "error" do - foregroundStyle(.red) - frame(maxWidth: .infinity, alignment: .leading) - end - """ -end -``` - -Then, we're going to create a LiveView that uses the `User` changeset to validate data. - -Evaluate the example below and view it in your simulator. We've included and `IO.inspect/2` call to view the changeset after submitting the form. Try submitting the form with different values to understand how those values affect the changeset. - - - -```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] - -defmodule Server.FormValidationLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use ErrorStylesheet - - @impl true - def mount(_params, _session, socket) do - user_changeset = User.changeset(%User{}, %{}) - {:ok, assign(socket, :user_changeset, user_changeset)} - end - - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Enter your email - - <%= ErrorUtils.error_message(@user_changeset.errors, :email) %> - - Submit - - """ - end - - @impl true - def handle_event("validate", params, socket) do - user_changeset = - User.changeset(%User{}, params) - # Preserve the `:action` field so errors do not vanish. - |> Map.put(:action, socket.assigns.user_changeset.action) - - {:noreply, assign(socket, :user_changeset, user_changeset)} - end - - def handle_event("submit", params, socket) do - user_changeset = - User.changeset(%User{}, params) - # faking a Database insert action - |> Map.put(:action, :insert) - # Submit the form and inspect the logs below to view the changeset. - |> IO.inspect(label: "Form Field Values") - - {:noreply, assign(socket, :user_changeset, user_changeset)} - end -end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok -``` - -In the code above, the `"sumbit"` and `"validate"` events update the changeset based on the current form params. This fills the `errors` field used by the `ErrorUtils` module to format the error message. - -After submitting the form, the `:action` field of the changeset has a value of `:insert`, so the red Text appears using the `:if` conditional display logic. - -In the future, this complexity will likely be handled by the `live_view_native_form` library, but for now this example exists to show you how to write your own error handling based on changesets if needed. - - - -### Empty Fields Send `"null"`. - -If you submit a form with empty fields, those fields may currently send `"null"`. There is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/6) to fix this bug, but it may affect your form behavior for now and require a temporary workaround until the issue is fixed. - -## Mini Project: User Form - -Taking everything you've learned, you're going to create a more complex user form with data validation and error displaying. We've defined a `FormStylesheet` you can use (and modify) if you would like to style your form. - -```elixir -defmodule FormStylesheet do - use LiveViewNative.Stylesheet, :swiftui - - ~SHEET""" - "error" do - foregroundStyle(.red) - frame(maxWidth: .infinity, alignment: .leading) - end - - "keyboard-" <> type do - keyboardType(to_ime(type)) - end - """ -end -``` - -### User Changeset - -First, create a `CustomUser` changeset below that handles data validation. - -**Requirements** - -* A user should have a `name` field -* A user should have a `password` string field of 10 or more characters. Note that for simplicity we are not hashing the password or following real security practices since our pretend application doesn't have a database. In real-world apps passwords should **never** be stored as a simple string, they should be encrypted. -* A user should have an `age` number field greater than `0` and less than `200`. -* A user should have an `email` field which matches an email format (including `@` is sufficient). -* A user should have a `accepted_terms` field which must be true. -* A user should have a `birthdate` field which is a date. -* All fields should be required - -### Example Solution - -```elixir -defmodule CustomUser do - import Ecto.Changeset - defstruct [:name, :password, :age, :email, :accepted_terms, :birthdate] - - @types %{ - name: :string, - password: :string, - age: :integer, - email: :string, - accepted_terms: :boolean, - birthdate: :date - } - - def changeset(user, params) do - {user, @types} - |> cast(params, Map.keys(@types)) - |> validate_required(Map.keys(@types)) - |> validate_length(:password, min: 10) - |> validate_number(:age, greater_than: 0, less_than: 200) - |> validate_acceptance(:accepted_terms) - end - - def error_message(changeset, field) do - with {msg, _reason} <- changeset.errors[field] do - msg - else - _ -> "" - end - end -end -``` - - - -```elixir -defmodule CustomUser do - # define the struct keys - defstruct [] - - # define the types - @types %{} - - def changeset(user, params) do - # Enter your solution - end -end -``` - -### LiveView - -Next, create the `CustomUserFormLive` Live View that lets the user enter their information and displays errors for invalid information upon form submission. - -**Requirements** - -* The `name` field should be a `TextField`. -* The `email` field should be a `TextField`. -* The `password` field should be a `SecureField`. -* The `age` field should be a `TextField` with a `.numberPad` keyboard or a `Slider`. -* The `accepted_terms` field should be a `Toggle`. -* The `birthdate` field should be a `DatePicker`. - -### Example Solution - -```elixir -defmodule Server.CustomUserFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use FormStylesheet - - @impl true - def mount(_params, _session, socket) do - changeset = CustomUser.changeset(%CustomUser{}, %{}) - - {:ok, assign(socket, :changeset, changeset)} - end - - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - name... - <.form_error changeset={@changeset} field={:name}/> - - email... - <.form_error changeset={@changeset} field={:email}/> - - age... - <.form_error changeset={@changeset} field={:age}/> - - password... - <.form_error changeset={@changeset} field={:password}/> - - Accept the Terms and Conditions: - <.form_error changeset={@changeset} field={:accepted_terms}/> - - Birthday: - <.form_error changeset={@changeset} field={:birthdate}/> - Submit - - """ - end - - @impl true - def handle_event("validate", params, socket) do - user_changeset = - CustomUser.changeset(%CustomUser{}, params) - |> Map.put(:action, socket.assigns.changeset.action) - - {:noreply, assign(socket, :changeset, user_changeset)} - end - - def handle_event("submit", params, socket) do - user_changeset = - CustomUser.changeset(%CustomUser{}, params) - |> Map.put(:action, :insert) - - {:noreply, assign(socket, :changeset, user_changeset)} - end - - # While not strictly required, the form_error component reduces code bloat. - def form_error(assigns) do - ~SWIFTUI""" - - <%= CustomUser.error_message(@changeset, @field) %> - - """ - end -end -``` - - - - - -```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] - -defmodule Server.CustomUserFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use FormStylesheet - - @impl true - def mount(_params, _session, socket) do - # Remember to provide the initial changeset - {:ok, socket} - end - - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - """ - end - - @impl true - # Write your `"validate"` event handler - def handle_event("validate", params, socket) do - {:noreply, socket} - end - - # Write your `"submit"` event handler - def handle_event("submit", params, socket) do - {:noreply, socket} - end -end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok -``` diff --git a/guides/markdown_livebooks/interactive-swiftui-views.md b/guides/markdown_livebooks/interactive-swiftui-views.md deleted file mode 100644 index 95e34addb..000000000 --- a/guides/markdown_livebooks/interactive-swiftui-views.md +++ /dev/null @@ -1,756 +0,0 @@ -# Interactive SwiftUI Views - -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%interactive-swiftui-views.livemd) - -## Overview - -In this guide, you'll learn how to build interactive LiveView Native applications using event bindings. - -This guide assumes some existing familiarity with [Phoenix Bindings](https://hexdocs.pm/phoenix_live_view/bindings.html) and how to set/access state stored in the LiveView's socket assigns. To get the most out of this material, you should already understand the `assign/3`/`assign/2` function, and how event bindings such as `phx-click` interact with the `handle_event/3` callback function. - -We'll use the following LiveView and define new render component examples throughout the guide. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - Hello, from LiveView Native! - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" -end -``` - -## Event Bindings - -We can bind any available `phx-*` [Phoenix Binding](https://hexdocs.pm/phoenix_live_view/bindings.html) to a SwiftUI Element. However certain events are not available on native. - -LiveView Native currently supports the following events on all SwiftUI views: - -* `phx-window-focus`: Fired when the application window gains focus, indicating user interaction with the Native app. -* `phx-window-blur`: Fired when the application window loses focus, indicating the user's switch to other apps or screens. -* `phx-focus`: Fired when a specific native UI element gains focus, often used for input fields. -* `phx-blur`: Fired when a specific native UI element loses focus, commonly used with input fields. -* `phx-click`: Fired when a user taps on a native UI element, enabling a response to tap events. - -> The above events work on all SwiftUI views. Some events are only available on specific views. For example, `phx-change` is available on controls and `phx-throttle/phx-debounce` is available on views with events. - -There is also a [Pull Request](https://github.com/liveview-native/liveview-client-swiftui/issues/1095) to add Key Events which may have been merged since this guide was published. - -## Basic Click Example - -The `phx-click` event triggers a corresponding [handle_event/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_event/3) callback function whenever a SwiftUI view is pressed. - -In the example below, the client sends a `"ping"` event to the server, and trigger's the LiveView's `"ping"` event handler. - -Evaluate the example below, then click the `"Click me!"` button. Notice `"Pong"` printed in the server logs below. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("ping", _params, socket) do - IO.puts("Pong") - {:noreply, socket} - end -end -``` - -### Click Events Updating State - -Event handlers in LiveView can update the LiveView's state in the socket. - -Evaluate the cell below to see an example of incrementing a count. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :count, 0)} - end - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("increment", _params, socket) do - {:noreply, assign(socket, :count, socket.assigns.count + 1)} - end -end -``` - -### Your Turn: Decrement Counter - -You're going to take the example above, and create a counter that can **both increment and decrement**. - -There should be two buttons, each with a `phx-click` binding. One button should bind the `"decrement"` event, and the other button should bind the `"increment"` event. Each event should have a corresponding handler defined using the `handle_event/3` callback function. - -### Example Solution - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - <%= @count %> - - - - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :count, 0)} - end - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("increment", _params, socket) do - {:noreply, assign(socket, :count, socket.assigns.count + 1)} - end - - def handle_event("decrement", _params, socket) do - {:noreply, assign(socket, :count, socket.assigns.count - 1)} - end -end -``` - - - - - -### Enter Your Solution Below - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - <%= @count %> - - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :count, 0)} - end - - @impl true - def render(assigns), do: ~H"" -end -``` - -## Selectable Lists - -`List` views support selecting items within the list based on their id. To select an item, provide the `selection` attribute with the item's id. - -Pressing a child item in the `List` on a native device triggers the `phx-change` event. In the example below we've bound the `phx-change` event to send the `"selection-changed"` event. This event is then handled by the `handle_event/3` callback function and used to change the selected item. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - Item <%= i %> - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, selection: "None")} - end - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("selection-changed", %{"selection" => selection}, socket) do - {:noreply, assign(socket, selection: selection)} - end -end -``` - -## Expandable Lists - -`List` views support hierarchical content using the [DisclosureGroup](https://developer.apple.com/documentation/swiftui/disclosuregroup) view. Nest `DisclosureGroup` views within a list to create multiple levels of content as seen in the example below. - -To control a `DisclosureGroup` view, use the `is-expanded` boolean attribute as seen in the example below. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - - Level 1 - Item 1 - Item 2 - Item 3 - - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :is_expanded, false)} - end - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("toggle", %{"is-expanded" => is_expanded}, socket) do - {:noreply, assign(socket, is_expanded: !is_expanded)} - end -end -``` - -### Multiple Expandable Lists - -The next example shows one pattern for displaying multiple expandable lists without needing to write multiple event handlers. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - - Level 1 - Item 1 - - Level 2 - Item 2 - - - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :expanded_groups, %{1 => false, 2 => false})} - end - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("toggle-" <> level, %{"is-expanded" => is_expanded}, socket) do - level = String.to_integer(level) - - {:noreply, - assign( - socket, - :expanded_groups, - Map.replace!(socket.assigns.expanded_groups, level, !is_expanded) - )} - end -end -``` - -## Controls and Indicators - -In Phoenix, the `phx-change` event must be applied to a parent form. However in SwiftUI there is no similar concept of forms. Instead, SwiftUI provides [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) views. We can apply the `phx-change` binding to any of these views. - -Once bound, the SwiftUI view will send a message to the LiveView anytime the control or indicator changes its value. - -The params of the message are based on the name of the [Binding](https://developer.apple.com/documentation/swiftui/binding) argument of the view's initializer in SwiftUI. - - - -### Event Value Bindings - -Many views use the `value` binding argument, so event params are generally sent as `%{"value" => value}`. However, certain views such as `TextField` and `Toggle` deviate from this pattern because SwiftUI uses a different `value` binding argument. For example, the `TextField` view uses `text` to bind its value, so it sends the event params as `%{"text" => value}`. - -When in doubt, you can connect the event handler and inspect the params to confirm the shape of map. - -## Text Field - -The following example shows you how to connect a SwiftUI [TextField](https://developer.apple.com/documentation/swiftui/textfield) with a `phx-change` event binding to a corresponding event handler. - -Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the server logs in the console below as a map of `%{"text" => value}`. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - Enter text here - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("type", params, socket) do - IO.inspect(params, label: "params") - {:noreply, socket} - end -end -``` - -### Storing TextField Values in the Socket - -The following example demonstrates how to set/access a TextField's value by controlling it using the socket assigns. - -This pattern is useful when rendering the TextField's value elsewhere on the page, using the `TextField` view's value in other event handler logic, or to set an initial value. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - Enter text here - - The current value: <%= @text %> - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :text, "initial value")} - end - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("type", %{"text" => text}, socket) do - {:noreply, assign(socket, :text, text)} - end - - @impl true - def handle_event("pretty-print", _params, socket) do - IO.puts(""" - ================== - #{socket.assigns.text} - ================== - """) - - {:noreply, socket} - end -end -``` - -## Slider - -This code example renders a SwiftUI [Slider](https://developer.apple.com/documentation/swiftui/slider). It triggers the change event when the slider is moved and sends a `"slide"` message. The `"slide"` event handler then logs the value to the console. - -Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the console below as a map of `%{"value" => value}`. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - Percent Completed - 0% - 100% - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("slide", params, socket) do - IO.inspect(params, label: "Slide Params") - {:noreply, socket} - end -end -``` - -## Stepper - -This code example renders a SwiftUI [Stepper](https://developer.apple.com/documentation/swiftui/stepper). It triggers the change event and sends a `"change-tickets"` message when the stepper increments or decrements. The `"change-tickets"` event handler then updates the number of tickets stored in state, which appears in the UI. - -Evaluate the example and increment/decrement the step. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - Tickets <%= @tickets %> - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :tickets, 0)} - end - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("change-tickets", %{"value" => tickets}, socket) do - {:noreply, assign(socket, :tickets, tickets)} - end -end -``` - -## Toggle - -This code example renders a SwiftUI [Toggle](https://developer.apple.com/documentation/swiftui/toggle). It triggers the change event and sends a `"toggle"` message when toggled. The `"toggle"` event handler then updates the `:on` field in state, which allows the `Toggle` view to be toggled on. Without providing the `is-on` attribute, the `Toggle` view could not be flipped on and off. - -Evaluate the example below and click on the toggle. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - On/Off - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :on, false)} - end - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("toggle", %{"is-on" => on}, socket) do - {:noreply, assign(socket, :on, on)} - end -end -``` - -## DatePicker - -The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :date, nil)} - end - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("pick-date", params, socket) do - IO.inspect(params, label: "Date Params") - {:noreply, socket} - end -end -``` - -### Parsing Dates - -The date from the `DatePicker` is in iso8601 format. You can use the `from_iso8601` function to parse this string into a `DateTime` struct. - -```elixir -iso8601 = "2024-01-17T20:51:00.000Z" - -DateTime.from_iso8601(iso8601) -``` - -### Your Turn: Displayed Components - -The `DatePicker` view accepts a `displayed-components` attribute with the value of `"hour-and-minute"` or `"date"` to only display one of the two components. By default, the value is `"all"`. - -You're going to change the `displayed-components` attribute in the example below to see both of these options. Change `"all"` to `"date"`, then to `"hour-and-minute"`. Re-evaluate the cell between changes and see the updated UI. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" - - def handle_event("pick-date", params, socket) do - {:noreply, socket} - end -end -``` - -## Small Project: Todo List - -Using the previous examples as inspiration, you're going to create a todo list. - -**Requirements** - -* Items should be `Text` views rendered within a `List` view. -* Item ids should be stored in state as a list of integers i.e. `[1, 2, 3, 4]` -* Use a `TextField` to provide the name of the next added todo item. -* An add item `Button` should add items to the list of integers in state when pressed. -* A delete item `Button` should remove the currently selected item from the list of integers in state when pressed. - -### Example Solution - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - Todo... - - - - <%= content %> - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, items: [], selection: "None", item_name: "", next_item_id: 1)} - end - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("type-name", %{"text" => name}, socket) do - {:noreply, assign(socket, :item_name, name)} - end - - def handle_event("add-item", _params, socket) do - updated_items = [ - {"item-#{socket.assigns.next_item_id}", socket.assigns.item_name} - | socket.assigns.items - ] - - {:noreply, - assign(socket, - item_name: "", - items: updated_items, - next_item_id: socket.assigns.next_item_id + 1 - )} - end - - def handle_event("delete-item", _params, socket) do - updated_items = - Enum.reject(socket.assigns.items, fn {id, _name} -> id == socket.assigns.selection end) - {:noreply, assign(socket, :items, updated_items)} - end - - def handle_event("selection-changed", %{"selection" => selection}, socket) do - {:noreply, assign(socket, selection: selection)} - end -end -``` - - - - - -### Enter Your Solution Below - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - # Define your mount/3 callback - - @impl true - def render(assigns), do: ~H"" - - # Define your render/3 callback - - # Define any handle_event/3 callbacks -end -``` diff --git a/guides/markdown_livebooks/native-navigation.md b/guides/markdown_livebooks/native-navigation.md deleted file mode 100644 index 52746e1df..000000000 --- a/guides/markdown_livebooks/native-navigation.md +++ /dev/null @@ -1,303 +0,0 @@ -# Native Navigation - -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%native-navigation.livemd) - -## Overview - -This guide will teach you how to create multi-page applications using LiveView Native. We will cover navigation patterns specific to native applications and how to reuse the existing navigation patterns available in LiveView. - -Before diving in, you should have a basic understanding of navigation in LiveView. You should be familiar with the [redirect/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#redirect/2), [push_patch/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_patch/2) and [push_navigate/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2) functions, which are used to trigger navigation from within a LiveView. Additionally, you should know how to define routes in the router using the [live/4](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Router.html#live/4) macro. - -## NavigationStack - -LiveView Native applications are generally wrapped in a [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack) view. This view usually exists in the `root.swiftui.heex` file, which looks something like the following: - - - -```elixir -<.csrf_token /> - - -
- Hello, from LiveView Native! -
-
-``` - -Notice the [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack) view wraps the template. This view manages the state of navigation history and allows for navigating back to previous pages. - -## Navigation Links - -We can use the [NavigationLink](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/navigationlink) view for native navigation, similar to how we can use the [.link](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#link/1) component with the `navigate` attribute for web navigation. - -We've created the same example of navigating between the `Main` and `About` pages. Each page using a `NavigationLink` to navigate to the other page. - -Evaluate **both** of the code cells below and click on the `NavigationLink` in your simulator to navigate between the two views. - - - -```elixir -defmodule ServerWeb.AboutLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - You are on the about page - - To Home - - """ - end -end - -defmodule ServerWeb.AboutLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" -end -``` - - - -```elixir -defmodule ServerWeb.HomeLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - You are on the main page - - To About - - """ - end -end - -defmodule ServerWeb.HomeLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" -end -``` - -The `destination` attribute works the same as the `navigate` attribute on the web. The current LiveView will shut down, and a new one will mount without re-establishing a new socket connection. - -## Push Navigation - -For LiveView Native views, we can still use the same [redirect/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#redirect/2), [push_patch/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_patch/2), and [push_navigate/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2) functions used in typical LiveViews. - -These functions are preferable over `NavigationLink` views when you want to share navigation handlers between web and native, and/or when you want to have more customized navigation handling. - -Evaluate **both** of the code cells below and click on the `Button` view in your simulator that triggers the `handle_event/3` navigation handler to navigate between the two views. - - - -```elixir -defmodule ServerWeb.MainLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - You are on the Main Page - - """ - end -end - -defmodule ServerWeb.MainLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("to-about", _params, socket) do - {:noreply, push_navigate(socket, to: "/about")} - end -end -``` - - - -```elixir -defmodule ServerWeb.AboutLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - You are on the About Page - - """ - end -end - -defmodule ServerWeb.AboutLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" - - @impl true - def handle_event("to-main", _params, socket) do - {:noreply, push_navigate(socket, to: "/")} - end -end -``` - -## Routing - -The `KinoLiveViewNative` smart cells used in this guide automatically define routes for us. Be aware there is no difference between how we define routes for LiveView or LiveView Native. - -The routes for the main and about pages might look like the following in the router: - - - -```elixir -live "/", Server.MainLive -live "/about", Server.AboutLive -``` - -## Native Navigation Events - -LiveView Native navigation mirrors the same navigation behavior you'll find on the web. - -Evaluate the example below and press each button. Notice that: - -1. `redirect/2` triggers the `mount/3` callback re-establishes a socket connection. -2. `push_navigate/2` triggers the `mount/3` callbcak and re-uses the existing socket connection. -3. `push_patch/2` does not trigger the `mount/3` callback, but does trigger the `handle_params/3` callback. This is often useful when using navigation to trigger page changes such as displaying a modal or overlay. - -You can see this for yourself using the following example. Click each of the buttons for redirect, navigate, and patch behavior. - - - -```elixir -# This module built for example purposes to persist logs between mounting LiveViews. -defmodule PersistantLogs do - def get do - :persistent_term.get(:logs) - end - - def put(log) when is_binary(log) do - :persistent_term.put(:logs, [{log, Time.utc_now()} | get()]) - end - - def reset do - :persistent_term.put(:logs, []) - end -end - -PersistantLogs.reset() - -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - - - - - - Socket ID<%= @socket_id %> - LiveView PID:<%= @live_view_pid %> - <%= for {log, time} <- Enum.reverse(@logs) do %> - - <%= Calendar.strftime(time, "%H:%M:%S") %>: - <%= log %> - - <% end %> - - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def mount(_params, _session, socket) do - PersistantLogs.put("MOUNT") - - {:ok, - assign(socket, - socket_id: socket.id, - connected: connected?(socket), - logs: PersistantLogs.get(), - live_view_pid: inspect(self()) - )} - end - - @impl true - def handle_params(_params, _url, socket) do - PersistantLogs.put("HANDLE PARAMS") - - {:noreply, assign(socket, :logs, PersistantLogs.get())} - end - - @impl true - def render(assigns), - do: ~H"" - - @impl true - def handle_event("redirect", _params, socket) do - PersistantLogs.reset() - PersistantLogs.put("--REDIRECTING--") - {:noreply, redirect(socket, to: "/")} - end - - def handle_event("navigate", _params, socket) do - PersistantLogs.put("---NAVIGATING---") - {:noreply, push_navigate(socket, to: "/")} - end - - def handle_event("patch", _params, socket) do - PersistantLogs.put("----PATCHING----") - {:noreply, push_patch(socket, to: "/")} - end -end -``` diff --git a/guides/markdown_livebooks/stylesheets.md b/guides/markdown_livebooks/stylesheets.md deleted file mode 100644 index fa99bdf4a..000000000 --- a/guides/markdown_livebooks/stylesheets.md +++ /dev/null @@ -1,538 +0,0 @@ -# Stylesheets - -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%stylesheets.livemd) - -## Overview - -In this guide, you'll learn how to use stylesheets to customize the appearance of your LiveView Native Views. You'll also learn about the inner workings of how LiveView Native uses stylesheets to implement modifiers, and how those modifiers style and customize SwiftUI Views. By the end of this lesson, you'll have the fundamentals you need to create beautiful native UIs. - -## The Stylesheet AST - -LiveView Native parses through your application at compile time to create a stylesheet AST representation of all the styles in your application. This stylesheet AST is used by the LiveView Native Client application when rendering the view hierarchy to apply modifiers to a given view. - -```mermaid -sequenceDiagram - LiveView->>LiveView: Create stylesheet - Client->>LiveView: Send request to "http://localhost:4000/?_format=swiftui" - LiveView->>Client: Send LiveView Native template in response - Client->>LiveView: Send request to "http://localhost:4000/assets/app.swiftui.styles" - LiveView->>Client: Send stylesheet in response - Client->>Client: Parses stylesheet into SwiftUI modifiers - Client->>Client: Apply modifiers to the view hierarchy -``` - -We've setup this Livebook to be included when parsing the application for modifiers. You can visit http://localhost:4000/assets/app.swiftui.styles to see the Stylesheet AST created by all of the styles in this Livebook and any other styles used in the `kino_live_view_native` project. - -LiveView Native watches for changes and updates the stylesheet, so those will be dynamically picked up and applied, You may notice a slight delay as the Livebook takes **5 seconds** to write it's contents to a file. - -## Modifiers - -SwiftUI employs **modifiers** to style and customize views. In SwiftUI syntax, each modifier is a function that can be chained onto the view they modify. LiveView Native has a minimal DSL (Domain Specific Language) for writing SwiftUI modifiers. - -Modifers can be applied through a LiveView Native Stylesheet and applying them through classes as described in the [LiveView Native Stylesheets](#liveview-native-stylesheets) section, or can be applied directly through the `class` attribute as described in the [Utility Styles](#utility-styles) section. - - - -### SwiftUI Modifiers - -Here's a basic example of making text red using the [foregroundStyle](https://developer.apple.com/documentation/swiftui/text/foregroundstyle(_:)) modifier: - -```swift -Text("Some Red Text") - .foregroundStyle(.red) -``` - -Many modifiers can be applied to a view. Here's an example using [foregroundStyle](https://developer.apple.com/documentation/swiftui/text/foregroundstyle(_:)) and [frame](https://developer.apple.com/documentation/swiftui/view/frame(width:height:alignment:)). - -```swift -Text("Some Red Text") - .foregroundStyle(.red) - .font(.title) -``` - - - -### Implicit Member Expression - -Implicit Member Expression in SwiftUI means that we can implicityly access a member of a given type without explicitly specifying the type itself. For example, the `.red` value above is from the [Color](https://developer.apple.com/documentation/swiftui/color) structure. - -```swift -Text("Some Red Text") - .foregroundStyle(Color.red) -``` - - - -### LiveView Native Modifiers - -The DSL (Domain Specific Language) used in LiveView Native drops the `.` dot before each modifier, but otherwise remains largely the same. We do not document every modifier separately, since you can translate SwiftUI examples into the DSL syntax. - -For example, Here's the same `foregroundStyle` modifier as it would be written in a LiveView Native stylesheet or class attribute, which we'll cover in a moment. - -```swift -foregroundStyle(.red) -``` - -There are some exceptions where the DSL differs from SwiftUI syntax, which we'll cover in the sections below. - -## Utility Styles - -In addition to introducing stylesheets, LiveView Native `0.3.0` also introduced Utility classes, which will be our prefered method for writing styles in these Livebook guides. - -The same SwiftUI syntax used inside of a stylesheet can be used directly inside of a `class` attribute. The example below defines the `foregroundStyle(.red)` modifier. Evaluate the example and view it in your simulator. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - Hello, from LiveView Native! - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" -end -``` - -### Multiple Modifiers - -You can write multiple modifiers, separate each by a space or newline character. - -```html -Hello, from LiveView Native! -``` - -For newline characters, you'll need to wrap the string in curly brackets `{}`. Using multiple lines can better organize larger amounts of modifiers. - -```html - -Hello, from LiveView Native! - -``` - -## Dynamic Class Names - -LiveView Native parses styles in your project to define a single stylesheet. You can find the AST representation of this stylesheet at http://localhost:4000/assets/app.swiftui.styles. This stylesheet is compiled on the server and then sent to the client. For this reason, class names must be fully-formed. For example, the following class using string interpolation is **invalid**. - -```html - -Invalid Example - -``` - -However, we can still use dynamic styles so long as the class names are fully formed. - -```html - -Red or Blue Text - -``` - -Evaluate the example below multiple times while watching your simulator. Notice that the text is dynamically red or blue. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - - Hello, from LiveView Native! - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" -end -``` - -## Modifier Order - -Modifier order matters. Changing the order that modifers are applied can have a significant impact on their behavior. - -To demonstrate this concept, we're going to take a simple example of applying padding and background color. - -If we apply the background color first, then the padding, The background is applied to original view, leaving the padding filled with whitespace. - - - -```elixir -background(.orange) -padding(20) -``` - -```mermaid -flowchart - -subgraph Padding - View -end - -style View fill:orange -``` - -If we apply the padding first, then the background, the background is applied to the view with the padding, thus filling the entire area with background color. - - - -```elixir -padding(20) -background(.orange) -``` - -```mermaid -flowchart - -subgraph Padding - View -end - -style Padding fill:orange -style View fill:orange -``` - -Evaluate the example below to see this in action. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - Hello, from LiveView Native! - Hello, from LiveView Native! - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" -end -``` - -## Injecting Views in Stylesheets - -SwiftUI modifiers sometimes accept SwiftUI views as arguments. Here's an example using the `clipShape` modifier with a `Circle` view. - -```swift -Image("logo") - .clipShape(Circle()) -``` - -However, LiveView Native does not support using SwiftUI views directly within a stylesheet. Instead, we have a few alternative options in cases like this where we want to use a view within a modifier. - - - -### Using Members on a Given Type - -We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, if you look at the [clipShape](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) documentation you'll notice it accepts the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. This type defines the [circle](https://developer.apple.com/documentation/swiftui/shape/circle) property which we can use since it's equivalent to the [Circle](https://developer.apple.com/documentation/swiftui/circle) view for our purposes. - -We can use `Shape.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. - -```swift -Image("logo") - .clipShape(Shape.circle) -``` - -Using implicit member expression, we can simplify this code to the following: - -```swift -Image("logo") - .clipShape(.circle) -``` - -Which is simple to convert to the LiveView Native DSL using the rules we've already learned. - - - -```elixir -"example-class" do - clipShape(.circle) -end -``` - - - -### Injecting a View - -For more complex cases, we can inject a view directly into a stylesheet. - -Here's an example where this might be useful. SwiftUI has modifers that represent a named content area for views to be placed within. These views can even have their own modifiers, so it's not enough to use a simple static property on the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. - -```swift -Image("logo") - .overlay(content: { - Circle().stroke(.red, lineWidth: 4) - }) -``` - -To get around this issue, we instead inject a view into the stylesheet. First, define the modifier and use an atom to represent the view that's going to be injected. - - - -```elixir -"overlay-circle" do - overlay(content: :circle) -end -``` - -Then use the `template` attribute on the view to be injected into the stylesheet. This view should be a child of the view with the given class. - -```html - - - -``` - -We can then apply modifiers to the child view through a class as we've already seen. - -## Custom Colors - -### SwiftUI Color Struct - -The SwiftUI [Color](https://developer.apple.com/documentation/swiftui/color) structure accepts either the name of a color in the asset catalog or the RGB values of the color. - -Therefore we can define custom RBG styles like so: - -```swift -foregroundStyle(Color(.sRGB, red: 0.4627, green: 0.8392, blue: 1.0)) -``` - -Evaluate the example below to see the custom color in your simulator. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - - Hello, from LiveView Native! - - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" -end -``` - -### Custom Colors in the Asset Catalogue - -Custom colors can be defined in the [Asset Catalogue](https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). Once defined in the asset catalogue of the Xcode application, the color can be referenced by name like so: - -```swift -foregroundStyle(Color("MyColor")) -``` - -Generally using the asset catalog is more performant and customizable than using custom RGB colors with the [Color](https://developer.apple.com/documentation/swiftui/color) struct. - - - -### Your Turn: Custom Colors in the Asset Catalog - -Custom colors can be defined in the asset catalog (https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). Generat - -To create a new color go to the `Assets` folder in your iOS app and create a new color set. - - - -![](https://github.com/liveview-native/documentation_assets/blob/main/asset-catalogue-create-new-color-set.png?raw=true) - - - -To create a color set, enter the RGB values or a hexcode as shown in the image below. If you don't see the sidebar with color options, click the icon in the top-right of your Xcode app and click the **Show attributes inspector** icon shown highlighted in blue. - - - -![](https://github.com/liveview-native/documentation_assets/blob/main/asset-catalogue-modify-my-color.png?raw=true) - - - -The defined color is now available for use within LiveView Native styles. However, the app needs to be re-compiled to pick up a new color set. - -Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - Hello, from LiveView Native! - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns), do: ~H"" -end -``` - -## LiveView Native Stylesheets - -In LiveView Native, we use `~SHEET` sigil stylesheets to organize modifers by classes using an Elixir-oriented DSL similar to CSS for styling web elements. - -We group modifiers together within a class that can be applied to an element. Here's an example of how modifiers can be grouped into a "red-title" class in a stylesheet: - - - -```elixir -~SHEET""" - "red-title" do - foregroundColor(.red) - font(.title) - end -""" -``` - -We're mostly using Utility styles for these guides, but the stylesheet module does contain some important configuration to `@import` the utility styles module. It can also be used to group styles within a class if you have a set of modifiers you're repeatedly using and want to group together. - - - -```elixir -defmodule ServerWeb.Styles.App.SwiftUI do - use LiveViewNative.Stylesheet, :swiftui - @import LiveViewNative.SwiftUI.UtilityStyles - - ~SHEET""" - "red-title" do - foregroundColor(.red) - font(.title) - end - """ -end -``` - -Since the Phoenix server runs in a dependency for these guides, you don't have direct access to the stylesheet module. - -## Apple Documentation - -You can find documentation and examples of modifiers on [Apple's SwiftUI documentation](https://developer.apple.com/documentation/swiftui) which is comprehensive and thorough, though it may feel unfamiliar at first for Elixir Developers when compared to HexDocs. - - - -### Finding Modifiers - -The [Configuring View Elements](https://developer.apple.com/documentation/swiftui/view#configuring-view-elements) section of apple documentation contains links to modifiers organized by category. In that documentation you'll find useful references such as [Style Modifiers](https://developer.apple.com/documentation/swiftui/view-style-modifiers), [Layout Modifiers](https://developer.apple.com/documentation/swiftui/view-layout), and [Input and Event Modifiers](https://developer.apple.com/documentation/swiftui/view-input-and-events). - -You can also find the same modifiers with LiveView Native examples on the [LiveView Client SwiftUI Docs](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/paddingmodifier). - -## Visual Studio Code Extension - -If you use Visual Studio Code, we strongly recommend you install the [LiveView Native Visual Studio Code Extension](https://github.com/liveview-native/liveview-native-vscode) which provides autocompletion and type information thus making modifiers significantly easier to write and lookup. - -## Your Turn: Syntax Conversion - -Part of learning LiveView Native is learning SwiftUI. Fortunately we can leverage the existing SwiftUI ecosystem and convert examples into LiveView Native syntax. - -You're going to convert the following SwiftUI code into a LiveView Native template. This example is inspired by the official [SwiftUI Tutorials](https://developer.apple.com/tutorials/swiftui/creating-and-combining-views). - - - -```elixir - VStack { - VStack(alignment: .leading) { - Text("Turtle Rock") - .font(.title) - HStack { - Text("Joshua Tree National Park") - Spacer() - Text("California") - } - .font(.subheadline) - - Divider() - - Text("About Turtle Rock") - .font(.title2) - Text("Descriptive text goes here") - } - .padding() - - Spacer() -} -``` - -### Example Solution - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - - Turtle Rock - - Joshua Tree National Park - - California - - - About Turtle Rock - Descriptive text goes here - - """ - end -end -``` - - - -Enter your solution below. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - - """ - end -end -``` diff --git a/guides/markdown_livebooks/swiftui-views.md b/guides/markdown_livebooks/swiftui-views.md deleted file mode 100644 index 6803a0906..000000000 --- a/guides/markdown_livebooks/swiftui-views.md +++ /dev/null @@ -1,693 +0,0 @@ -# SwiftUI Views - -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%swiftui-views.livemd) - -## Overview - -LiveView Native aims to use minimal SwiftUI code. All patterns for building interactive UIs are the same as LiveView. However, unlike LiveView for the web, LiveView Native uses SwiftUI templates to build the native UI. - -This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common uses of each view and give you practical examples you can use to build your own native UIs. This lesson is like a recipe book you can refer back to whenever you need an example of how to use a particular SwiftUI view. In addition, once you understand how to convert these views into the LiveView Native DSL, you should have the tools to convert essentially any SwiftUI View into the LiveView Native DSL. - -## Render Components - -LiveView Native `0.3.0` introduced render components to better encourage isolation of native and web templates and move away from co-location templates within the same LiveView module. - -Render components are namespaced under the main LiveView, and are responsible for defining the `render/1` callback function that returns the native template. - -For example, and `ExampleLive` LiveView module would have an `ExampleLive.SwiftUI` render component module for the native Template. - -This `ExampleLive.SwiftUI` render component may define a `render/1` callback function as seen below. - - - -```elixir -# Render Component -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - Hello, from LiveView Native! - """ - end -end - -# LiveView -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view - - @impl true - def render(assigns) do - ~H""" -

Hello from LiveView!

- """ - end -end -``` - -Throughout this and further material we'll re-define render components you can evaluate and see reflected in your Xcode iOS simulator. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - Hello, from a LiveView Native Render Component! - """ - end -end -``` - -### Embedding Templates - -Alternatively, you may omit the render callback and instead define a `.neex` (Native + Embedded Elixir) template. - -By default, the module above would look for a template in the `swiftui/example_live*` path relative to the module's location. You can see the `LiveViewNative.Component` documentation for further explanation. - -For the sake of ease when working in Livebook, we'll prefer defining the `render/1` callback. However, we recommend you generally prefer template files when working locally in Phoenix LiveView Native projects. - -## SwiftUI Views - -In SwiftUI, a "View" is like a building block for what you see on your app's screen. It can be something simple like text or an image, or something more complex like a layout with multiple elements. Views are the pieces that make up your app's user interface. - -Here's an example `Text` view that represents a text element. - -```swift -Text("Hamlet") -``` - -LiveView Native uses the following syntax to represent the view above. - - - -```elixir -Hamlet -``` - -SwiftUI provides a wide range of Views that can be used in native templates. You can find a full reference of these views in the SwiftUI Documentation at https://developer.apple.com/documentation/swiftui/. You can also find a shorthand on how to convert SwiftUI syntax into the LiveView Native DLS in the [LiveView Native Syntax Conversion Cheatsheet](https://hexdocs.pm/live_view_native/cheatsheet.cheatmd). - -## Text - -We've already seen the [Text](https://developer.apple.com/documentation/swiftui/text) view, but we'll start simple to get the interactive tutorial running. - -Evaluate the cell below, then in Xcode, Start the iOS application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native/create-a-swiftui-application.html) lesson and ensure you see the `"Hello, from LiveView Native!"` text. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - Hello, from LiveView Native! - """ - end -end -``` - -## HStack and VStack - -SwiftUI includes many [Layout](https://developer.apple.com/documentation/swiftui/layout-fundamentals) container views you can use to arrange your user Interface. Here are a few of the most commonly used: - -* [VStack](https://developer.apple.com/documentation/swiftui/vstack): Vertically arranges nested views. -* [HStack](https://developer.apple.com/documentation/swiftui/hstack): Horizontally arranges nested views. - -Below, we've created a simple 3X3 game board to demonstrate how to use `VStack` and `HStack` to build a layout of horizontal rows in a single vertical column.o - -Here's a diagram to demonstrate how these rows and columns create our desired layout. - -```mermaid -flowchart -subgraph VStack - direction TB - subgraph H1[HStack] - direction LR - 1[O] --> 2[X] --> 3[X] - end - subgraph H2[HStack] - direction LR - 4[X] --> 5[O] --> 6[O] - end - subgraph H3[HStack] - direction LR - 7[X] --> 8[X] --> 9[O] - end - H1 --> H2 --> H3 -end -``` - -Evaluate the example below and view the working 3X3 layout in your Xcode simulator. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - O - X - X - - - X - O - O - - - X - X - O - - - """ - end -end -``` - -### Your Turn: 3x3 board using columns - -In the cell below, use `VStack` and `HStack` to create a 3X3 board using 3 columns instead of 3 rows as demonstrated above. The arrangement of `X` and `O` does not matter, however the content will not be properly aligned if you do not have exactly one character in each `Text` element. - -```mermaid -flowchart -subgraph HStack - direction LR - subgraph V1[VStack] - direction TB - 1[O] --> 2[X] --> 3[X] - end - subgraph V2[VStack] - direction TB - 4[X] --> 5[O] --> 6[O] - end - subgraph V3[VStack] - direction TB - 7[X] --> 8[X] --> 9[O] - end - V1 --> V2 --> V3 -end -``` - -### Example Solution - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - - O - X - X - - - X - O - O - - - X - X - O - - - """ - end -end -``` - - - - - -### Enter Your Solution Below - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end -``` - -## Grid - -`VStack` and `HStack` do not provide vertical-alignment between horizontal rows. Notice in the following example that the rows/columns of the 3X3 board are not aligned, just centered. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - X - X - - - X - O - O - - - X - O - - - """ - end -end -``` - -Fortunately, we have a few common elements for creating a grid-based layout. - -* [Grid](https://developer.apple.com/documentation/swiftui/grid): A grid that arranges its child views in rows and columns that you specify. -* [GridRow](https://developer.apple.com/documentation/swiftui/gridrow): A view that arranges its children in a horizontal line. - -A grid layout vertically and horizontally aligns elements in the grid based on the number of elements in each row. - -Evaluate the example below and notice that rows and columns are aligned. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - XX - X - X - - - X - X - - - X - X - X - - - """ - end -end -``` - -## List - -The SwiftUI [List](https://developer.apple.com/documentation/swiftui/list) view provides a system-specific interface, and has better performance for large amounts of scrolling elements. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - Item 1 - Item 2 - Item 3 - - """ - end -end -``` - -### Multi-dimensional lists - -Alternatively we can separate children within a `List` view in a `Section` view as seen in the example below. Views in the `Section` can have the `template` attribute with a `"header"` or `"footer"` value which controls how the content is displayed above or below the section. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - -
- Header - Content - Footer -
-
- """ - end -end -``` - -## ScrollView - -The SwiftUI [ScrollView](https://developer.apple.com/documentation/swiftui/scrollview) displays content within a scrollable region. ScrollView is often used in combination with [LazyHStack](https://developer.apple.com/documentation/swiftui/lazyvstack), [LazyVStack](https://developer.apple.com/documentation/swiftui/lazyhstack), [LazyHGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid), and [LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid) to create scrollable layouts optimized for displaying large amounts of data. - -While `ScrollView` also works with typical `VStack` and `HStack` views, they are not optimal choices for large amounts of data. - - - -### ScrollView with VStack - -Here's an example using a `ScrollView` and a `HStack` to create scrollable text arranged horizontally. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - Item <%= n %> - - - """ - end -end -``` - -### ScrollView with HStack - -By default, the [axes](https://developer.apple.com/documentation/swiftui/scrollview/axes) of a `ScrollView` is vertical. To make a horizontal `ScrollView`, set the `axes` attribute to `"horizontal"` as seen in the example below. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - Item <%= n %> - - - """ - end -end -``` - -### Optimized ScrollView with LazyHStack and LazyVStack - -`VStack` and `HStack` are inefficient for large amounts of data because they render every child view. To demonstrate this, evaluate the example below. You should experience lag when you attempt to scroll. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - Item <%= n %> - - - """ - end -end -``` - -To resolve the performance problem for large amounts of data, you can use the Lazy views. Lazy views only create items as needed. Items won't be rendered until they are present on the screen. - -The next example demonstrates how using `LazyVStack` instead of `VStack` resolves the performance issue. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - Item <%= n %> - - - """ - end -end -``` - -## Spacers - -[Spacers](https://developer.apple.com/documentation/swiftui/spacer) take up all remaining space in a container. - -![Apple Documentation](https://docs-assets.developer.apple.com/published/189fa436f07ed0011bd0c1abeb167723/Building-Layouts-with-Stack-Views-4@2x.png) - -> Image originally from https://developer.apple.com/documentation/swiftui/spacer - -Evaluate the following example and notice the `Text` element is pushed to the right by the `Spacer`. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - This text is pushed to the right - - """ - end -end -``` - -### Your Turn: Bottom Text Spacer - -In the cell below, use `VStack` and `Spacer` to place text in the bottom of the native view. - -### Example Solution - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns, _interface) do - ~LVN""" - - - Hello - - """ - end -end -``` - - - - - -### Enter Your Solution Below - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end -``` - -## AsyncImage - -`AsyncImage` is best for network images, or images served by the Phoenix server. - -Here's an example of `AsyncImage` with a lorem picsum image from https://picsum.photos/400/600. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end -``` - -### Loading Spinner - -`AsyncImage` displays a loading spinner while loading the image. Here's an example of using `AsyncImage` without a URL so that it loads forever. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end -``` - -### Relative Path - -For images served by the Phoenix server, LiveView Native evaluates URLs relative to the LiveView's host URL. This way you can use the path to static resources as you normally would in a Phoenix application. - -For example, the path `/images/logo.png` evaluates as http://localhost:4000/images/logo.png below. This serves the LiveView Native logo. - -Evaluate the example below to see the LiveView Native logo in the iOS simulator. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end -``` - -## Image - -The `Image` element is best for system images such as the built in [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) or images placed into the SwiftUI [asset catalogue](https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). - - - -### System Images - -You can use the `system-image` attribute to provide the name of system images to the `Image` element. - -For the full list of SF Symbols you can download Apple's [Symbols 5](https://developer.apple.com/sf-symbols/) application. - -Evaluate the cell below to see an example using the `square.and.arrow.up` symbol. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end -``` - -### Your Turn: Asset Catalogue - -You can place assets in your SwiftUI application's asset catalogue. Using the asset catalogue for SwiftUI assets provide many benefits such as device-specific image variants, dark mode images, high contrast image mode, and improved performance. - -Follow this guide: https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs#Add-a-new-asset to create a new asset called Image. - -Then evaluate the following example and you should see this image in your simulator. For a convenient image, you can right-click and save the following LiveView Native logo. - -![LiveView Native Logo](https://github.com/liveview-native/documentation_assets/blob/main/logo.png?raw=true) - -You will need to **rebuild the native application** to pick up the changes to the assets catalogue. - - - -### Enter Your Solution Below - -You should not need to make changes to this cell. Set up an image in your asset catalogue named "Image", rebuild your native application, then evaluate this cell. You should see the image in your iOS simulator. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - """ - end -end -``` - -## Button - -A Button is a clickable SwiftUI View. - -The label of a button can be any view, such as a [Text](https://developer.apple.com/documentation/swiftui/text) view for text-only buttons or a [Label](https://developer.apple.com/documentation/swiftui/label) view for buttons with icons. - -Evaluate the example below to see the SwiftUI [Button](https://developer.apple.com/documentation/swiftui/button) element. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - """ - end -end -``` - -## Further Resources - -See the [SwiftUI Documentation](https://developer.apple.com/documentation/swiftui) for a complete list of SwiftUI elements and the [LiveView Native SwiftUI Documentation](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/) for LiveView Native examples of the SwiftUI elements. diff --git a/lib/mix/tasks/livebooks_to_markdown.ex b/lib/mix/tasks/livebooks_to_markdown.ex index a00373a4f..71d788c06 100644 --- a/lib/mix/tasks/livebooks_to_markdown.ex +++ b/lib/mix/tasks/livebooks_to_markdown.ex @@ -1,16 +1,17 @@ defmodule Mix.Tasks.LivebooksToMarkdown do @moduledoc "Generates ex_doc friendly markdown guides from Livebook notebooks" - @destination "guides/markdown_livebooks" + @source "livebooks" + @destination "livebooks/markdown" use Mix.Task def run(_args) do # clean up old notebooks File.rm_rf(@destination) File.mkdir(@destination) - File.ls!("guides/livebooks") |> Enum.filter(fn file_name -> file_name =~ ".livemd" end) + File.ls!(@source) |> Enum.filter(fn file_name -> file_name =~ ".livemd" end) |> Enum.each(fn file_name -> - ex_doc_friendly_content = make_ex_doc_friendly(File.read!("guides/livebooks/#{file_name}"), file_name) + ex_doc_friendly_content = make_ex_doc_friendly(File.read!("#{@source}/#{file_name}"), file_name) File.write!("#{@destination}/#{Path.basename(file_name, ".livemd")}.md", ex_doc_friendly_content) end) end @@ -23,7 +24,7 @@ defmodule Mix.Tasks.LivebooksToMarkdown do end defp replace_setup_section_with_badge(content, file_name) do - badge = "[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%#{file_name})" + badge = "[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Flivebooks%#{file_name})" String.replace(content, ~r/```elixir(.|\n)+?```/, badge, global: false) end diff --git a/guides/livebooks/create-a-swiftui-application.livemd b/livebooks/create-a-swiftui-application.livemd similarity index 100% rename from guides/livebooks/create-a-swiftui-application.livemd rename to livebooks/create-a-swiftui-application.livemd diff --git a/guides/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd similarity index 95% rename from guides/livebooks/forms-and-validation.livemd rename to livebooks/forms-and-validation.livemd index 415ed98dc..3da3134f3 100644 --- a/guides/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -1,23 +1,59 @@ # Forms and Validation ```elixir +notebook_path = __ENV__.file |> String.split("#") |> hd() + Mix.install( [ - {:kino_live_view_native, "0.2.1"}, - {:ecto, "~> 3.11"} + {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} ], config: [ + server: [ + {ServerWeb.Endpoint, + [ + server: true, + url: [host: "localhost"], + adapter: Phoenix.Endpoint.Cowboy2Adapter, + render_errors: [ + formats: [html: ServerWeb.ErrorHTML, json: ServerWeb.ErrorJSON], + layout: false + ], + pubsub_server: Server.PubSub, + live_view: [signing_salt: "JSgdVVL6"], + http: [ip: {127, 0, 0, 1}, port: 4000], + secret_key_base: String.duplicate("a", 64), + live_reload: [ + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg|styles)$", + ~r/#{notebook_path}$/ + ] + ] + ]} + ], + kino: [ + group_leader: Process.group_leader() + ], + phoenix: [ + template_engines: [neex: LiveViewNative.Engine] + ], + phoenix_template: [format_encoders: [swiftui: Phoenix.HTML.Engine]], + mime: [ + types: %{"text/swiftui" => ["swiftui"], "text/styles" => ["styles"]} + ], live_view_native: [plugins: [LiveViewNative.SwiftUI]], - live_view_native_stylesheet: [parsers: [swiftui: LiveViewNative.SwiftUI.RulesParser]], - phoenix_template: [ - format_encoders: [ - swiftui: Phoenix.HTML.Engine - ] + live_view_native_stylesheet: [ + content: [ + swiftui: [ + "lib/**/*swiftui*", + notebook_path + ] + ], + pretty: true, + output: "priv/static/assets" ] - ] + ], + force: true ) - -KinoLiveViewNative.start([]) ``` ## Overview diff --git a/guides/livebooks/getting-started.livemd b/livebooks/getting-started.livemd similarity index 100% rename from guides/livebooks/getting-started.livemd rename to livebooks/getting-started.livemd diff --git a/guides/livebooks/interactive-swiftui-views.livemd b/livebooks/interactive-swiftui-views.livemd similarity index 100% rename from guides/livebooks/interactive-swiftui-views.livemd rename to livebooks/interactive-swiftui-views.livemd diff --git a/guides/markdown_livebooks/create-a-swiftui-application.md b/livebooks/markdown/create-a-swiftui-application.md similarity index 98% rename from guides/markdown_livebooks/create-a-swiftui-application.md rename to livebooks/markdown/create-a-swiftui-application.md index e337fa751..fd5d7a4b7 100644 --- a/guides/markdown_livebooks/create-a-swiftui-application.md +++ b/livebooks/markdown/create-a-swiftui-application.md @@ -2,7 +2,7 @@ # Create a SwiftUI Application -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%create-a-swiftui-application.livemd) +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Flivebooks%create-a-swiftui-application.livemd) ## Overview diff --git a/guides/ex_doc_notebooks/forms-and-validation.md b/livebooks/markdown/forms-and-validation.md similarity index 99% rename from guides/ex_doc_notebooks/forms-and-validation.md rename to livebooks/markdown/forms-and-validation.md index c6db3323f..d3e708f75 100644 --- a/guides/ex_doc_notebooks/forms-and-validation.md +++ b/livebooks/markdown/forms-and-validation.md @@ -1,6 +1,6 @@ # Forms and Validation -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%forms-and-validation.livemd) +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Flivebooks%forms-and-validation.livemd) ## Overview diff --git a/guides/markdown_livebooks/getting-started.md b/livebooks/markdown/getting-started.md similarity index 97% rename from guides/markdown_livebooks/getting-started.md rename to livebooks/markdown/getting-started.md index ecd39aa8b..3f5d02d0f 100644 --- a/guides/markdown_livebooks/getting-started.md +++ b/livebooks/markdown/getting-started.md @@ -1,6 +1,6 @@ # Getting Started -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Fguides%livebooks%getting-started.livemd) +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Flivebooks%getting-started.livemd) ## Overview diff --git a/guides/ex_doc_notebooks/interactive-swiftui-views.md b/livebooks/markdown/interactive-swiftui-views.md similarity index 99% rename from guides/ex_doc_notebooks/interactive-swiftui-views.md rename to livebooks/markdown/interactive-swiftui-views.md index 4f5ddc2bb..fea035dbd 100644 --- a/guides/ex_doc_notebooks/interactive-swiftui-views.md +++ b/livebooks/markdown/interactive-swiftui-views.md @@ -1,6 +1,6 @@ # Interactive SwiftUI Views -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%interactive-swiftui-views.livemd) +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Flivebooks%interactive-swiftui-views.livemd) ## Overview diff --git a/guides/ex_doc_notebooks/native-navigation.md b/livebooks/markdown/native-navigation.md similarity index 99% rename from guides/ex_doc_notebooks/native-navigation.md rename to livebooks/markdown/native-navigation.md index e29631de7..27125721f 100644 --- a/guides/ex_doc_notebooks/native-navigation.md +++ b/livebooks/markdown/native-navigation.md @@ -1,6 +1,6 @@ # Native Navigation -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%native-navigation.livemd) +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Flivebooks%native-navigation.livemd) ## Overview diff --git a/guides/ex_doc_notebooks/stylesheets.md b/livebooks/markdown/stylesheets.md similarity index 93% rename from guides/ex_doc_notebooks/stylesheets.md rename to livebooks/markdown/stylesheets.md index 979af24f3..541b3b7f3 100644 --- a/guides/ex_doc_notebooks/stylesheets.md +++ b/livebooks/markdown/stylesheets.md @@ -1,6 +1,6 @@ # Stylesheets -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%stylesheets.livemd) +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Flivebooks%stylesheets.livemd) ## Overview @@ -23,7 +23,7 @@ sequenceDiagram We've setup this Livebook to be included when parsing the application for modifiers. You can visit http://localhost:4000/assets/app.swiftui.styles to see the Stylesheet AST created by all of the styles in this Livebook and any other styles used in the `kino_live_view_native` project. -LiveView Native watches for changes and updates the stylesheet, so those will be dynamically picked up and applied, You may notice a slight delay as the Livebook takes **5 seconds** to write it's contents to a file. +LiveView Native watches for changes and updates the stylesheet, so those will be dynamically picked up and applied, You may notice a slight delay as the Livebook takes **5 seconds** to write its contents to a file. ## Modifiers @@ -79,6 +79,8 @@ There are some exceptions where the DSL differs from SwiftUI syntax, which we'll In addition to introducing stylesheets, LiveView Native `0.3.0` also introduced Utility classes, which will be our prefered method for writing styles in these Livebook guides. +Utility styles are comperable to inline styles in HTML, which have been largely discouraged in the CSS community. We recommend Utility styles for now as the easiest way to prototype applications. But, we hope to replace Utility styles with a more mature styling framework in the future. + The same SwiftUI syntax used inside of a stylesheet can be used directly inside of a `class` attribute. The example below defines the `foregroundStyle(.red)` modifier. Evaluate the example and view it in your simulator. @@ -124,6 +126,16 @@ Hello, from LiveView Native! ``` + + +### Spaces + +At the time of writing, the parser for utility styles interprets space characters as a separator for each rule, thus you should not includes spaces in modifiers that might traditionally have a space. + +```html +Hello, from LiveView Native! +``` + ## Dynamic Class Names LiveView Native parses styles in your project to define a single stylesheet. You can find the AST representation of this stylesheet at http://localhost:4000/assets/app.swiftui.styles. This stylesheet is compiled on the server and then sent to the client. For this reason, class names must be fully-formed. For example, the following class using string interpolation is **invalid**. @@ -328,7 +340,7 @@ foregroundStyle(Color(.sRGB, red: 0.4627, green: 0.8392, blue: 1.0)) Evaluate the example below to see the custom color in your simulator. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -336,7 +348,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + Hello, from LiveView Native! """ @@ -457,7 +469,7 @@ You can find documentation and examples of modifiers on [Apple's SwiftUI documen The [Configuring View Elements](https://developer.apple.com/documentation/swiftui/view#configuring-view-elements) section of apple documentation contains links to modifiers organized by category. In that documentation you'll find useful references such as [Style Modifiers](https://developer.apple.com/documentation/swiftui/view-style-modifiers), [Layout Modifiers](https://developer.apple.com/documentation/swiftui/view-layout), and [Input and Event Modifiers](https://developer.apple.com/documentation/swiftui/view-input-and-events). -You can also find the same modifiers with LiveView Native examples on the [LiveView Client SwiftUI Docs](https://liveview-native.github.io/liveview-client-swiftui/documentation/liveviewnative/paddingmodifier). +You can also find more on modifiers with LiveView Native examples on the [liveview-client-swiftui](https://hexdocs.pm/live_view_native_swiftui) HexDocs. ## Visual Studio Code Extension diff --git a/guides/ex_doc_notebooks/swiftui-views.md b/livebooks/markdown/swiftui-views.md similarity index 99% rename from guides/ex_doc_notebooks/swiftui-views.md rename to livebooks/markdown/swiftui-views.md index 110ceb07b..421c5116a 100644 --- a/guides/ex_doc_notebooks/swiftui-views.md +++ b/livebooks/markdown/swiftui-views.md @@ -1,6 +1,6 @@ # SwiftUI Views -[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%swiftui-views.livemd) +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Flivebooks%swiftui-views.livemd) ## Overview diff --git a/guides/livebooks/native-navigation.livemd b/livebooks/native-navigation.livemd similarity index 100% rename from guides/livebooks/native-navigation.livemd rename to livebooks/native-navigation.livemd diff --git a/guides/livebooks/stylesheets.livemd b/livebooks/stylesheets.livemd similarity index 100% rename from guides/livebooks/stylesheets.livemd rename to livebooks/stylesheets.livemd diff --git a/guides/livebooks/swiftui-views.livemd b/livebooks/swiftui-views.livemd similarity index 100% rename from guides/livebooks/swiftui-views.livemd rename to livebooks/swiftui-views.livemd diff --git a/mix.exs b/mix.exs index a51fa6734..10827b868 100644 --- a/mix.exs +++ b/mix.exs @@ -3,6 +3,8 @@ defmodule LiveViewNative.SwiftUI.MixProject do @version "0.3.0-rc.1" @source_url "https://github.com/liveview-native/liveview-client-swiftui" + @livebooks_enabled System.get_env("LIVEBOOKS_ENABLED") + @gen_docs_enabled System.get_env("GEN_DOCS_ENABLED") def project do [ @@ -33,7 +35,7 @@ defmodule LiveViewNative.SwiftUI.MixProject do defp aliases do [ - docs: ["lvn.swiftui.gen.docs", "livebooks_to_markdown", "docs"] + docs: @gen_docs_enabled && ["lvn.swiftui.gen.docs"] || [] ++ ["livebooks_to_markdown", "docs"] ] end @@ -108,15 +110,15 @@ defmodule LiveViewNative.SwiftUI.MixProject do guides = Path.wildcard("guides/**/*.md") generated_docs = Path.wildcard("generated_docs/**/*.{md,cheatmd}") - livebooks = if System.get_env("LIVEBOOKS_ENABLED") do + livebooks = if @livebooks_enabled do [ - "guides/markdown_livebooks/getting-started.md", - "guides/markdown_livebooks/create-a-swiftui-application.md", - "guides/markdown_livebooks/swiftui-views.md", - "guides/markdown_livebooks/interactive-swiftui-views.md", - "guides/markdown_livebooks/stylesheets.md", - "guides/markdown_livebooks/native-navigation.md", - "guides/markdown_livebooks/forms-and-validation.md" + "livebooks/markdown/getting-started.md", + "livebooks/markdown/create-a-swiftui-application.md", + "livebooks/markdown/swiftui-views.md", + "livebooks/markdown/interactive-swiftui-views.md", + "livebooks/markdown/stylesheets.md", + "livebooks/markdown/native-navigation.md", + "livebooks/markdown/forms-and-validation.md" ] else [] diff --git a/test/mix/tasks/livebooks_to_markdown_test.exs b/test/mix/tasks/livebooks_to_markdown_test.exs index fe61cda57..308b2d05c 100644 --- a/test/mix/tasks/livebooks_to_markdown_test.exs +++ b/test/mix/tasks/livebooks_to_markdown_test.exs @@ -60,7 +60,7 @@ defmodule Mix.Tasks.LivebooksToMarkdownTest do ``` """ assert LivebooksToMarkdown.make_ex_doc_friendly(content, "filename.livemd") =~ - "[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%2Flive_view_native%2Fmain%2Fguides%livebooks%filename.livemd)" + "[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fliveview-native%liveview-client-swiftui%2Fmain%2Flivebooks%filename.livemd)" end test "make_ex_doc_friendly/1 removes initial Kino boilerplate in smart cells" do From df47852192ec3a9f411f3196681166b8dca28366 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Fri, 3 May 2024 10:29:27 -0400 Subject: [PATCH 04/62] Draft forms and validations reading --- livebooks/forms-and-validation.livemd | 929 ++++++++++++++++---------- 1 file changed, 578 insertions(+), 351 deletions(-) diff --git a/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd index 3da3134f3..5eb8d972a 100644 --- a/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -5,7 +5,10 @@ notebook_path = __ENV__.file |> String.split("#") |> hd() Mix.install( [ - {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} + # {:kino_live_view_native, github: "liveview-native/kino_live_view_native"}, + {:kino_live_view_native, path: "../kino_live_view_native"}, + {:ecto, "~> 3.11"}, + {:phoenix_ecto, "~> 4.5"} ], config: [ server: [ @@ -50,6 +53,17 @@ Mix.install( ], pretty: true, output: "priv/static/assets" + ], + # Ensures that app.js compiles to avoid the switch to longpolling + # when a LiveView doesn't exist yet + esbuild: [ + version: "0.17.11", + server_web: [ + args: + ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), + cd: Path.expand("../assets", __DIR__), + env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} + ] ] ], force: true @@ -60,447 +74,676 @@ Mix.install( The [LiveView Native Live Form](https://github.com/liveview-native/liveview-native-live-form) project makes it easier to build forms in LiveView Native. This project enables you to group different [Control Views](https://developer.apple.com/documentation/swiftui/controls-and-indicators) inside of a `LiveForm` and control them collectively under a single `phx-change` or `phx-submit` event handler, rather than with multiple different `phx-change` event handlers. -Getting the most out of this material requires some understanding of the [Ecto](https://hexdocs.pm/ecto/Ecto.html) project and in particular a reasonably deep understanding of [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html). Review the linked Ecto documentation if you find any of the examples difficult to follow. +Getting the most out of this material requires some understanding of the [Ecto](https://hexdocs.pm/ecto/Ecto.html) project and in particular a reasonably deep understanding of [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html). Review the Ecto documentation if you find any of the examples difficult to follow. ## Installing LiveView Native Live Form To install LiveView Native Form, we need to add the `liveview-native-live-form` SwiftUI package to our iOS application. -Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README and come back to this guide after you have finished the installation process. +Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README to add the `liveview-native-live-form` package to the SwiftUI application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native_swiftui/create-a-swiftui-application.html) guide. + +Come back to this guide and continue after you have finished the installation process. ## Creating a Basic Form -Once you have the LiveView Native Form package installed, you can use the `LiveForm` and `LiveSubmitButton` views to build forms more conveniently. +The LiveView Native `mix lvn.install` task generates a [core_components.swiftui.ex](https://github.com/liveview-native/liveview-client-swiftui/blob/main/priv/templates/lvn.swiftui.gen/core_components.ex) for native SwiftUI function components similar to the [core_components.ex](https://github.com/phoenixframework/phoenix/blob/main/priv/templates/phx.gen.live/core_components.ex) file generated in a traditional phoenix application for web function components. + +See Phoenix's [Components and HEEx](https://hexdocs.pm/phoenix/components.html) HexDoc documentation if you need a primer on function components. + +In the `core_components.swiftui.ex` file there's a `simple_form/1` component that is a similar abstraction to the `simple_form/1` component found in `core_components.ex`. + +First, we'll see how to use this abstraction at a basic level, then later we'll dive deeper into how forms work under the hood in LiveView Native. + + + +### A Basic Form + +This code below demonstrates how the basic skeleton of a native and web form that share event handlers for the `phx-submit` and `phx-change` handlers. + +We'll break down and understand the individual parts of this form in a moment. -Here's a basic example of a `LiveForm`. Keep in mind that `LiveForm` requires an `id` attribute. +For now, evaluate the following example. Open the native form in your simulator, and open the web form on http://localhost:4000/. Enter some text into both forms, then submit them. Watch the logs in the cell below to see the printed params.' - + ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook +require Server.Livebook +import Server.Livebook import Kernel, except: [defmodule: 2] -defmodule Server.ExampleLive do - use Phoenix.LiveView - use LiveViewNative.LiveView +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:value]} type="TextField" placeholder="Enter a value" /> + <:actions> + <.button type="submit"> + Ping + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Placeholder - Submit - + def mount(_params, _session, socket) do + {:ok, assign(socket, form: to_form(%{}, as: "my_form"))} + end + + @impl true + def render(assigns) do + ~H""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:value]} placeholder="Enter a value" /> + <:actions> + <.button type="submit"> + Ping + + + """ end @impl true def handle_event("submit", params, socket) do - IO.inspect(params) + IO.inspect(params, label: "Submitted") + {:noreply, socket} + end + + @impl true + def handle_event("validate", params, socket) do + IO.inspect(params, label: "Validating") {:noreply, socket} end end -|> KinoLiveViewNative.register("/", ":index") +|> Server.SmartCells.LiveViewNative.register("/") -import KinoLiveViewNative.Livebook, only: [] +import Server.Livebook, only: [] import Kernel :ok ``` -When a form is submitted, its data is sent as a map where each key is the 'name' attribute of the form's control views. Evaluate the example above in your simulator and you will see a map similar to the following: +After submitting both forms, notice that both the web and native params are the same shape:`%{"my_form" => %{"value" => "some text"}}`. This makes it easier to share event handlers for both web and native. - +Sharing event handlers hugely simplifies and speeds up the process of writing web and native application logic because you only have to write the logic once. Alternatively, if your web and native UI deviates significantly, you can also separate the event handlers. -```elixir -%{"my-text" => "some value"} -``` +## Breaking down a Basic Form -In a real-world application you could use these params to trigger some application logic, such as inserting a record into the database. +### Simple Form -## Controls and Indicators +The interface for the native `simple_form/1` and web `simple_form/1` is intentionally identical. -We've already covered many individual controls and indicator views that you can use inside of forms. For more information on those, go to the [Interactive SwiftUI Views](https://hexdocs.pm/live_view_native/interactive-swiftui-views.html) guide. +```heex +<.simple_form for={@form} id="form" phx-submit="submit"> + + +``` - +We'll go into the internal implementation details later on, but for now you can treat these components as functionally identical. Both require a unique `id` and accept the `for` attribute that contains the [Phoenix.HTML.Form] datastructure containing form fields, error messages, and other form data. -### Your Turn +If you need a refresher on forms in Phoenix, see the [Form Bindings](https://hexdocs.pm/phoenix_live_view/form-bindings.html) HexDoc documentation. -Create a form that has `TextField`, `Slider`, `Toggle`, and `DatePicker` fields. + -
-Example Solution +### Inputs -```elixir -defmodule Server.MultiInputFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView +Both web and native core components define a `input/1` function component. Inputs in the web form and native form differ since one is an abstraction on top of HTML elements and the other is an abstraction on top of SwiftUI Views. Therefore, they have different values for the `type` attribte that determines which input type to render. - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Placeholder - - - - Submit - - """ - end +On web, the `input/1` component accepts the following values for the `type` attribute. These reflect [html input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types). - @impl true - def handle_event("submit", params, socket) do - IO.inspect(params) - {:noreply, socket} - end -end + + +```elixir + attr :type, :string, + default: "text", + values: ~w(checkbox color date datetime-local email file hidden month number password + range radio search select tel text textarea time url week) ``` -
+On native, the `input/1` component accepts the following values for the `type` attribute. These reflect the SwiftUI Views from the [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) and [Text Input and Outputs](https://developer.apple.com/documentation/swiftui/text-input-and-output) sections. - + ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] +attr :type, :string, + default: "TextField", + values: ~w(TextFieldLink DatePicker MultiDatePicker Picker SecureField Slider Stepper TextEditor TextField Toggle hidden) +``` -defmodule Server.MultiInputFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView +## Changesets - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - """ - end +The [Phoenix.Component.to_form/2](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#to_form/2) function also supports Ecto changesets for form data and error validation. See [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html) for a refresher on changesets. Also see [Form Bindings](https://hexdocs.pm/phoenix_live_view/form-bindings.html) and [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) for a refresher on Phoenix Forms. - # You may use this handler to test your solution. - # You should not need to modify this handler. - @impl true - def handle_event("submit", params, socket) do - IO.inspect(params) - {:noreply, socket} +We'll use the following changeset to demonstrate how to validate data in a LiveView Native Live Form. + +```elixir +defmodule User do + import Ecto.Changeset + defstruct [:email] + @types %{email: :string} + + def changeset(user, params) do + {user, @types} + |> cast(params, [:email]) + |> validate_required([:email]) + |> validate_format(:email, ~r/@/) end end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok ``` -### Controlled Values +The [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) struct stores the changeset. The `simple_form/1` and `input/1` components for both web and native use the [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) struct and nested [Phoenix.HTML.FormField](https://hexdocs.pm/phoenix_html/Phoenix.HTML.FormField.html) structs to render form data and display errors. -Some control views such as the `Stepper` require manually displaying their value. In this case, we can store the form params in the socket and update them everytime the `phx-change` form binding submits an event. You can also use this pattern to provide default values. +For example, `:action` field in the changeset determines if errors should display or not. Here's an example we'll use in a moment of faking a database `:insert` action and storing the changeset information inside of a form. -Evaluate the example below to see this in action. +```elixir +User.changeset(%User{}, %{email: "test"}) +|> Map.put(:action, :insert) +|> Phoenix.Component.to_form() +``` + +Here's an example of how we can use Ecto changesets with the LiveView Native Live Form. Now when we submit or validate the form data we apply the changes to the changeset and store the new version of the form in the socket. The `simple_form/1` and `input/1` components use the form data to render content and display errors. - +Evaluate the cell below and open your iOS application. Submit the form with an invalid email. You should notice a `has invalid format` error appear. + + ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook +require Server.Livebook +import Server.Livebook import Kernel, except: [defmodule: 2] -defmodule Server.StepperLive do - use Phoenix.LiveView - use LiveViewNative.LiveView +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:email]} type="TextField" placeholder="Email" /> + <:actions> + <.button type="submit"> + Submit + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, params: %{"my-stepper" => 1})} + changeset = User.changeset(%User{}, %{}) + {:ok, assign(socket, form: to_form(changeset), check_errors: false)} end @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - <%= @params["my-stepper"] %> - + def render(assigns) do + ~H""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:email]} placeholder="Email" /> + <:actions> + <.button type="submit"> + Submit + + + """ end @impl true - def handle_event("change", params, socket) do - IO.inspect(params) - {:noreply, assign(socket, params: params)} + def handle_event("submit", %{"user" => params}, socket) do + changeset = + User.changeset(%User{}, params) + # Faking a Database insert action + |> Map.put(:action, :insert) + |> IO.inspect(label: "Form Field Values") + + {:noreply, assign(socket, form: to_form(changeset))} + end + + @impl true + def handle_event("validate", %{"user" => params}, socket) do + changeset = + User.changeset(%User{}, params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, form: to_form(changeset))} end end -|> KinoLiveViewNative.register("/", ":index") +|> Server.SmartCells.LiveViewNative.register("/") -import KinoLiveViewNative.Livebook, only: [] +import Server.Livebook, only: [] import Kernel :ok ``` -### Secure Field +## Keyboard Types + +The [keyboardType](https://developer.apple.com/documentation/swiftui/view/keyboardtype(_:)) modifier changes the type of keyboard for a TextField view. -For password entry, or anytime you want to hide a given value, you can use the [SecureField](https://developer.apple.com/documentation/swiftui/securefield) view. This field works mostly the same as a `TextField` but hides the visual text. +Evaluate the example below to see the different keyboards as you focus on each input. If you don't see the keyboard, go to `I/O` -> `Keyboard` -> `Toggle Software Keyboard` to enable the software keyboard in your simulator. - + ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook +require Server.Livebook +import Server.Livebook import Kernel, except: [defmodule: 2] -defmodule Server.SecureLive do - use Phoenix.LiveView - use LiveViewNative.LiveView +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - Enter a Password + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form"> + <.input field={@form[:number_pad]} type="TextField" class="keyboardType(.numberPad)"/> + <.input field={@form[:email_address]} type="TextField" class="keyboardType(.emailAddress)"/> + <.input field={@form[:phonePad]} type="TextField" class="keyboardType(.phonePad)"/> + <:actions> + <.button type="submit"> + Submit + + + """ end - - @impl true - def handle_event("change", params, socket) do - IO.inspect(params) - {:noreply, socket} - end end -|> KinoLiveViewNative.register("/", ":index") +|> Server.SmartCells.RenderComponent.register() -import KinoLiveViewNative.Livebook, only: [] +import Server.Livebook, only: [] import Kernel :ok ``` -## Keyboard Types +For a complete list of accepted keyboard types, see the [UIKeyboardType](https://developer.apple.com/documentation/uikit/uikeyboardtype) documentation. -To format a `TextField` for specific input types we can use the [keyboardType](https://developer.apple.com/documentation/swiftui/view/keyboardtype(_:)) modifier. +## Core Components -For a complete list of accepted keyboard types, see the [UIKeyboardType](https://developer.apple.com/documentation/uikit/uikeyboardtype) documentation. +Setting up a LiveView Native application using the generators creates a `core_components.swiftui.ex` file. If you have the [liveview-native-live-form](https://github.com/liveview-native/liveview-native-live-form) dependency, this file includes function components for building forms. -Below we've created several different common keyboard types. We've also included a generic `keyboard-*` to demonstrate how you can make a reusable class. +To better understand how to work with each core component, refer to the `core_components.swiftui.ex` file generated in a Phoenix LiveView Native project. For the core components used in this Livebook, refer to the [core_components.swiftui.ex](https://github.com/liveview-native/kino_live_view_native/blob/main/apps/server_web/lib/server_web/components/core_components.swiftui.ex) from the Kino LiveView Native project. -```elixir -defmodule KeyboardStylesheet do - use LiveViewNative.Stylesheet, :swiftui +We've already been using the two main functions, `simple_form/1` and `input/1`. These are abstractions on top of the native SwiftUI views and some custom views defined by the LiveView Native Live Form library. - ~SHEET""" - "number-pad" do - keyboardType(.numberPad) - end +in this section, we'll dive deeper into these abstractions so that you can build your own custom forms. - "email-address" do - keyboardType(.emailAddress) - end + - "phone-pad" do - keyboardType(.phonePad) - end +### Simple Form + +Here's the `simple_form/1` definition. - "keyboard-" <> type do - keyboardType(to_ime(type)) + + +```elixir + attr :for, :any, required: true, doc: "the datastructure for the form" + attr :as, :any, default: nil, doc: "the server side parameter to collect all input under" + + attr :rest, :global, + include: ~w(autocomplete name rel action enctype method novalidate target multipart), + doc: "the arbitrary HTML attributes to apply to the form tag" + + slot :inner_block, required: true + slot :actions, doc: "the slot for form actions, such as a submit button" + + def simple_form(assigns) do + ~LVN""" + <.form :let={f} for={@for} as={@as} {@rest}> +
+ <%= render_slot(@inner_block, f) %> +
+ <%= for action <- @actions do %> + <%= render_slot(action, f) %> + <% end %> +
+
+ + """ end +``` + +We show this to highlight the similarity between this form, and the one used in `core_components.ex`. + + + +```elixir +attr :for, :any, required: true, doc: "the datastructure for the form" +attr :as, :any, default: nil, doc: "the server side parameter to collect all input under" + +attr :rest, :global, + include: ~w(autocomplete name rel action enctype method novalidate target multipart), + doc: "the arbitrary HTML attributes to apply to the form tag" + +slot :inner_block, required: true +slot :actions, doc: "the slot for form actions, such as a submit button" + +def simple_form(assigns) do + ~H""" + <.form :let={f} for={@for} as={@as} {@rest}> +
+ <%= render_slot(@inner_block, f) %> +
+ <%= render_slot(action, f) %> +
+
+ """ end ``` -Evaluate the example below to see the different keyboards as you focus on each input. If you don't see the keyboard, go to `I/O` -> `Keyboard` -> `Toggle Software Keyboard` to enable the software keyboard in your simulator. + - +### Input + +The `type` attribute on the `input/1` component determines which View to render. Here's the same `input/1` definition. + + ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] +attr :id, :any, default: nil +attr :name, :any +attr :label, :string, default: nil +attr :value, :any + +attr :type, :string, + default: "TextField", + values: ~w(TextFieldLink DatePicker MultiDatePicker Picker SecureField Slider Stepper TextEditor TextField Toggle hidden) + +attr :field, Phoenix.HTML.FormField, + doc: "a form field struct retrieved from the form, for example: @form[:email]" + +attr :errors, :list, default: [] +attr :checked, :boolean, doc: "the checked flag for checkbox inputs" +attr :prompt, :string, default: nil, doc: "the prompt for select inputs" +attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2" +attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs" + +attr :min, :any, default: nil +attr :max, :any, default: nil + +attr :placeholder, :string, default: nil + +attr :readonly, :boolean, default: false + +attr :autocomplete, :string, + default: "on", + values: ~w(on off) + +attr :rest, :global, + include: ~w(disabled step) + +slot :inner_block + +def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do + assigns + |> assign(field: nil, id: assigns.id || field.id) + |> assign(:errors, Enum.map(field.errors, &translate_error(&1))) + |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end) + |> assign_new(:value, fn -> field.value end) + |> assign( + :rest, + Map.put(assigns.rest, :class, [ + Map.get(assigns.rest, :class, ""), + (if assigns.readonly or Map.get(assigns.rest, :disabled, false), do: "disabled-true", else: ""), + (if assigns.autocomplete == "off", do: "text-input-autocapitalization-never autocorrection-disabled", else: "") + ] |> Enum.join(" ")) + ) + |> input() +end +``` -defmodule Server.KeyboardLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use KeyboardStylesheet +The `input/1` function then continues to call a separate function definition depending on the `type` attribute. For example, here's the `"TextField"` definition: - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Enter Phone - Enter Number - Enter Number - """ - end + - def render(assigns) do - ~H""" -

Hello from LiveView!

- """ - end +```elixir +def input(%{type: "TextField"} = assigns) do + ~LVN""" + + <%= @placeholder || @label %> + <.error :for={msg <- @errors}><%= msg %> + + """ end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok ``` -## Validation +Here's a list of valid options with links to their documentation: + +* [TextFieldLink](https://developer.apple.com/documentation/swiftui/textfieldlink) +* [DatePicker](https://developer.apple.com/documentation/swiftui/datepicker) +* [MultiDatePicker](https://developer.apple.com/documentation/swiftui/multidatepicker) +* [Picker](https://developer.apple.com/documentation/swiftui/picker) +* [SecureField](https://developer.apple.com/documentation/swiftui/securefield) +* [Slider](https://developer.apple.com/documentation/swiftui/slider) +* [Stepper](https://developer.apple.com/documentation/swiftui/stepper) +* [TextEditor](https://developer.apple.com/documentation/swiftui/texteditor) +* [TextField](https://developer.apple.com/documentation/swiftui/textfield) +* [Toggle](https://developer.apple.com/documentation/swiftui/toggle) +* hidden -In this section, we'll focus mainly on using [Ecto Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) to validate data, but know that this is not the only way to validate data if you would like to write your own custom logic in the form event handlers, you absolutely can. +For more on the form compatible views see the [Interactive SwiftUI Views](https://hexdocs.pm/liveview-client-swiftui/interactive-swiftui-views.html) guide. -### LiveView Native Changesets Coming Soon! +### Core Components vs Views -LiveView Native Form doesn't currently natively support [Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) and [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) structs the way a traditional [Phoenix.Component.form](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1) does. However there is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/5) to add this behavior so this may change in the near future. As a result, this section is somewhat more verbose than will be necessary in the future, as we have to manually define much of the error handling logic that we expect will no longer be necessary in version `0.3` of LiveView Native. +SwiftUI Core Components attempts to make the API consistent and easy to remember between platforms. For that reason, we deviate somewhat from the interface used by SwiftUI. -To make error handling easier, we've defined an `ErrorUtils` module below that will handle extracting the error message out of a Changeset. This will not be necessary in future versions of LiveView Native, but is a convenient helper for now. +Let's take the Slider view as an example. The Slider view accepts the `min` and `max` attributes instead of `lowerBound` and `upperBound` because they better reflect the html [range](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range) slider. The component also accepts the `label` attribute instead of using children for the same reason. + + ```elixir -defmodule ErrorUtils do - def error_message(errors, field) do - with {msg, opts} <- errors[field] do - Server.CoreComponents.translate_error({msg, opts}) - else - _ -> "" - end + def input(%{type: "Slider"} = assigns) do + ~LVN""" + + + <%= @label %> + <%= @label %> + + <.error :for={msg <- @errors}><%= msg %> + + """ end -end ``` -For the sake of context, the `translate_message/2` function handles formatting Ecto Changeset errors. For example, it will inject values such as `count` into the string. + -```elixir -Server.CoreComponents.translate_error( - {"name must be longer than %{count} characters", [count: 10]} -) -``` +### Labels with Form Data -### Changesets +Sometimes you may wish to use data within the form separately as part of your UI. For example, let's say we want to have a Stepper view with a dynamic label based on the current step value. In these cases, you can access form data through the `@form.params`. -Here's a `User` changeset we're going to use to validate a `User` struct's `email` field. +Here's an example showing how to have a dynamic label based on the Stepper view's current value. Evaluate the example below and run it in your simulator. + + ```elixir -defmodule User do - import Ecto.Changeset - defstruct [:email] - @types %{email: :string} +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] - def changeset(user, params) do - {user, @types} - |> cast(params, [:email]) - |> validate_required([:email]) - |> validate_format(:email, ~r/@/) +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input + field={@form[:value]} + type="Stepper" + label={"Value: #{@form.params["value"]}"} + /> + <:actions> + <.button type="submit"> + Ping + + + + """ end end -``` -We're going to define an `error` class so errors will appear red and be left-aligned. +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view -```elixir -defmodule ErrorStylesheet do - use LiveViewNative.Stylesheet, :swiftui + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, form: to_form(%{"value" => 0}, as: "my_form"))} + end - ~SHEET""" - "error" do - foregroundStyle(.red) - frame(maxWidth: .infinity, alignment: .leading) + @impl true + def render(assigns), do: ~H"" + + @impl true + def handle_event("submit", params, socket) do + {:noreply, assign(socket, form: to_form(params, as: "my_form"))} + end + + @impl true + def handle_event("validate", %{"my_form" => params}, socket) do + {:noreply, assign(socket, form: to_form(params, as: "my_form"))} end - """ end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok ``` -Then, we're going to create a LiveView that uses the `User` changeset to validate data. +### Your Turn -Evaluate the example below and view it in your simulator. We've included and `IO.inspect/2` call to view the changeset after submitting the form. Try submitting the form with different values to understand how those values affect the changeset. +Create a form that has `TextField`, `Slider`, `Toggle`, and `DatePicker` fields. - +
+Example Solution ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] -defmodule Server.FormValidationLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use ErrorStylesheet + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:text]} type="TextField" placeholder="Enter a value" /> + <.input field={@form[:slider]} type="Slider"/> + <.input field={@form[:toggle]} type="Toggle"/> + <.input field={@form[:date_picker]} type="DatePicker"/> + <:actions> + <.button type="submit"> + Ping + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view @impl true def mount(_params, _session, socket) do - user_changeset = User.changeset(%User{}, %{}) - {:ok, assign(socket, :user_changeset, user_changeset)} + {:ok, assign(socket, form: to_form(%{}, as: "my_form"))} end @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Enter your email - - <%= ErrorUtils.error_message(@user_changeset.errors, :email) %> - - Submit - - """ + def render(assigns), do: "" + + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params, label: "Submitted") + {:noreply, socket} end @impl true def handle_event("validate", params, socket) do - user_changeset = - User.changeset(%User{}, params) - # Preserve the `:action` field so errors do not vanish. - |> Map.put(:action, socket.assigns.user_changeset.action) + IO.inspect(params, label: "Validating") + {:noreply, socket} + end +end +``` + +
+ + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] - {:noreply, assign(socket, :user_changeset, user_changeset)} +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + + <:actions> + <.button type="submit"> + Ping + + + + """ end +end +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, form: to_form(%{}, as: "my_form"))} + end + + @impl true + def render(assigns), do: ~H"" + + @impl true def handle_event("submit", params, socket) do - user_changeset = - User.changeset(%User{}, params) - # faking a Database insert action - |> Map.put(:action, :insert) - # Submit the form and inspect the logs below to view the changeset. - |> IO.inspect(label: "Form Field Values") + IO.inspect(params, label: "Submitted") + {:noreply, socket} + end - {:noreply, assign(socket, :user_changeset, user_changeset)} + @impl true + def handle_event("validate", params, socket) do + IO.inspect(params, label: "Validating") + {:noreply, socket} end end -|> KinoLiveViewNative.register("/", ":index") +|> Server.SmartCells.LiveViewNative.register("/") -import KinoLiveViewNative.Livebook, only: [] +import Server.Livebook, only: [] import Kernel :ok ``` -In the code above, the `"sumbit"` and `"validate"` events update the changeset based on the current form params. This fills the `errors` field used by the `ErrorUtils` module to format the error message. - -After submitting the form, the `:action` field of the changeset has a value of `:insert`, so the red Text appears using the `:if` conditional display logic. - -In the future, this complexity will likely be handled by the `live_view_native_form` library, but for now this example exists to show you how to write your own error handling based on changesets if needed. +### Native Views - - -### Empty Fields Send `"null"`. +The LiveView Native LiveForm library defines [a few custom SwiftUI views](https://github.com/liveview-native/liveview-native-live-form/tree/main/swiftui/Sources/LiveViewNativeLiveForm) such as `LiveForm` and `LiveSubmitButton`. Several core components use these components. -If you submit a form with empty fields, those fields may currently send `"null"`. There is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/6) to fix this bug, but it may affect your form behavior for now and require a temporary workaround until the issue is fixed. +Typically, you won't need to use these views directly and will instead rely upon the core components directly. ## Mini Project: User Form -Taking everything you've learned, you're going to create a more complex user form with data validation and error displaying. We've defined a `FormStylesheet` you can use (and modify) if you would like to style your form. +Taking everything you've learned, you're going to create a more complex user form with data validation and error displaying. -```elixir -defmodule FormStylesheet do - use LiveViewNative.Stylesheet, :swiftui - - ~SHEET""" - "error" do - foregroundStyle(.red) - frame(maxWidth: .infinity, alignment: .leading) - end - - "keyboard-" <> type do - keyboardType(to_ime(type)) - end - """ -end -``` + ### User Changeset @@ -537,18 +780,11 @@ defmodule CustomUser do {user, @types} |> cast(params, Map.keys(@types)) |> validate_required(Map.keys(@types)) + |> validate_format(:email, ~r/@/) |> validate_length(:password, min: 10) |> validate_number(:age, greater_than: 0, less_than: 200) |> validate_acceptance(:accepted_terms) end - - def error_message(changeset, field) do - with {msg, _reason} <- changeset.errors[field] do - msg - else - _ -> "" - end - end end ``` @@ -570,7 +806,7 @@ end ### LiveView -Next, create the `CustomUserFormLive` Live View that lets the user enter their information and displays errors for invalid information upon form submission. +Next, create a Live View that lets the user enter their information and displays errors for invalid information. **Requirements** @@ -585,113 +821,104 @@ Next, create the `CustomUserFormLive` Live View that lets the user enter their i Example Solution ```elixir -defmodule Server.CustomUserFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use FormStylesheet +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:name]} type="TextField" placeholder="name" /> + <.input field={@form[:email]} type="TextField" placeholder="email" /> + <.input field={@form[:password]} type="SecureField" placeholder="password" /> + <.input field={@form[:age]} type="TextField" placeholder="age" class="keyboardType(.numberPad)" /> + <.input field={@form[:accepted_terms]} type="Toggle"/> + <.input field={@form[:birthdate]} type="DatePicker"/> + + <:actions> + <.button type="submit"> + Submit + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view @impl true def mount(_params, _session, socket) do - changeset = CustomUser.changeset(%CustomUser{}, %{}) - - {:ok, assign(socket, :changeset, changeset)} + changeset = User.changeset(%CustomUser{}, %{}) + {:ok, assign(socket, form: to_form(changeset, as: :user))} end @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - name... - <.form_error changeset={@changeset} field={:name}/> - - email... - <.form_error changeset={@changeset} field={:email}/> - - age... - <.form_error changeset={@changeset} field={:age}/> - - password... - <.form_error changeset={@changeset} field={:password}/> - - Accept the Terms and Conditions: - <.form_error changeset={@changeset} field={:accepted_terms}/> - - Birthday: - <.form_error changeset={@changeset} field={:birthdate}/> - Submit - - """ - end + def render(assigns), do: ~H"" @impl true - def handle_event("validate", params, socket) do - user_changeset = + def handle_event("submit", %{"user" => params}, socket) do + changeset = CustomUser.changeset(%CustomUser{}, params) - |> Map.put(:action, socket.assigns.changeset.action) + # Faking a Database insert action + |> Map.put(:action, :insert) + |> IO.inspect(label: "Form Field Values") - {:noreply, assign(socket, :changeset, user_changeset)} + {:noreply, assign(socket, form: to_form(changeset, as: :user))} end - def handle_event("submit", params, socket) do - user_changeset = + @impl true + def handle_event("validate", %{"user" => params}, socket) do + IO.inspect(params) + changeset = CustomUser.changeset(%CustomUser{}, params) - |> Map.put(:action, :insert) - - {:noreply, assign(socket, :changeset, user_changeset)} - end + |> Map.put(:action, :validate) + |> IO.inspect() - # While not strictly required, the form_error component reduces code bloat. - def form_error(assigns) do - ~SWIFTUI""" - - <%= CustomUser.error_message(@changeset, @field) %> - - """ + {:noreply, assign(socket, form: to_form(changeset, as: :user))} end end ``` - + ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook +require Server.Livebook +import Server.Livebook import Kernel, except: [defmodule: 2] -defmodule Server.CustomUserFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use FormStylesheet +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] - @impl true - def mount(_params, _session, socket) do - # Remember to provide the initial changeset - {:ok, socket} + def render(assigns) do + ~LVN""" + + """ end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - """ + def mount(_params, _session, socket) do + # Remember to assign the form + {:ok, socket} end @impl true - # Write your `"validate"` event handler - def handle_event("validate", params, socket) do - {:noreply, socket} - end + def render(assigns), do: ~H"" - # Write your `"submit"` event handler - def handle_event("submit", params, socket) do - {:noreply, socket} - end + # Event handlers for form validation and submission go here end -|> KinoLiveViewNative.register("/", ":index") +|> Server.SmartCells.LiveViewNative.register("/") -import KinoLiveViewNative.Livebook, only: [] +import Server.Livebook, only: [] import Kernel :ok ``` From 667df60d41360cbb0a0a0ecadf0c132155d42c51 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Fri, 3 May 2024 17:43:14 -0400 Subject: [PATCH 05/62] grammar check create a swiftui app --- livebooks/create-a-swiftui-application.livemd | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/livebooks/create-a-swiftui-application.livemd b/livebooks/create-a-swiftui-application.livemd index a883bd6b3..59db8ab42 100644 --- a/livebooks/create-a-swiftui-application.livemd +++ b/livebooks/create-a-swiftui-application.livemd @@ -68,7 +68,7 @@ In future lessons, you'll use this iOS application to view iOS examples in the X ## Prerequisites -First, make sure you have followed the [Getting Started](https://hexdocs.pm/live_view_native/getting_started.md) guide. Then evaluate the smart cell below and visit http://localhost:4000 to ensure the Phoenix server runs properly. You should see the text `Hello from LiveView!` +First, make sure you have followed the [Getting Started](https://hexdocs.pm/live_view_native/getting_started.md) guide. Then, evaluate the smart cell below. Visit http://localhost:4000 to ensure the Phoenix server runs properly. You should see the text `Hello from LiveView!` @@ -111,7 +111,7 @@ Open Xcode and select Create New Project. -Select the `iOS` and `App` options to create an iOS application. Then click `Next`. +To create an iOS application, select the **iOS** and **App** options and click **Next**. @@ -119,15 +119,15 @@ Select the `iOS` and `App` options to create an iOS application. Then click `Nex -Choose options for your new project that match the following image, then click `Next`. +Choose options for your new project that match the following image, then click **Next**.
What do these options mean? -* **Product Name:** The name of the application. This can be any valid name. We've chosen `Guides`. -* **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recomends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using `com.example.your_name` where `your_name` is your organization or personal name. +* **Product Name:** The name of the application. This can be any valid name. We've chosen **Guides`. +* **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recommends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using **com.example.your_name** where **your_name** is your organization or personal name. * **Interface:**: The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. -* **Language:** Determines which language Xcode should use for the project. Select `Swift`. +* **Language:** Determines which language Xcode should use for the project. Select **Swift**.
@@ -136,7 +136,7 @@ Choose options for your new project that match the following image, then click ` -Select an appropriate folder location where you would like to store the iOS project, then click `Create`. +Select an appropriate folder location to store the iOS project, then click **Create.** @@ -152,9 +152,9 @@ You should see the default iOS application generated by Xcode. ## Add the LiveView Client SwiftUI Package -In Xcode from the project you just created, select `File -> Add Package Dependencies`. Then, search for `liveview-client-swiftui`. Once you have selected the package, click `Add Package`. +In Xcode from the project you just created, select **File -> Add Package Dependencies**. Then, search for `liveview-client-swiftui`. Once you have chosen the package, click **Add Package**. -The image below was created using version `0.2.0`. You should select whichever is the latest version of LiveView Native. +The image below displays `0.2.0`. You should select the latest version of LiveView Native. @@ -162,7 +162,7 @@ The image below was created using version `0.2.0`. You should select whichever i -Choose the Package Products for `liveview-client-swiftui`. Select `Guides` as the target for `LiveViewNative` and `LiveViewNativeStylesheet`. This adds both of these dependencies to your iOS project. +Choose the Package Products for `liveview-client-swiftui`. Select **Guides** as the target for `LiveViewNative` and `LiveViewNativeStylesheet` to add these dependencies to your iOS project. @@ -170,8 +170,7 @@ Choose the Package Products for `liveview-client-swiftui`. Select `Guides` as th -At this point, you'll need to enable permissions for plugins used by LiveView Native. -You should see the following prompt. Click `Trust & Enable All`. +At this point, you'll need to enable permissions for plugins used by LiveView Native. You should see the following prompt. Click **Trust & Enable All**. @@ -179,7 +178,7 @@ You should see the following prompt. Click `Trust & Enable All`. -You'll also need to manually navigate to the error tab (shown below) and manually trust and enable packages. Click on each error to trigger a prompt. Select `Trust & Enable All` to enable the plugin. +You'll also need to manually navigate to the error tab (shown below) to trust and enable packages. Click on each error to trigger a prompt. Select **Trust & Enable All** to enable the plugin. The specific plugins are subject to change. At the time of writing you need to enable `LiveViewNativeStylesheetMacros`, `LiveViewNativeMacros`, and `CasePathMacros` as shown in the images below. @@ -252,11 +251,11 @@ graph LR; Click the `start active scheme` button to build the project and run it on the iOS simulator. -> A [build scheme](https://developer.apple.com/documentation/xcode/build-system) contains a list of targets to build, and any configuration and environment details that affect the selected action. For example, when you build and run an app, the scheme tells Xcode what launch arguments to pass to the app. +> A [build scheme](https://developer.apple.com/documentation/xcode/build-system) contains a list of targets to build and any configuration and environment details that affect the selected action. When you build and run an app, the scheme tells Xcode what launch arguments to pass to the app. > > * https://developer.apple.com/documentation/xcode/build-system -After you start the active scheme, the simulator should open the iOS application and display `Hello from LiveView Native!`. If you encounter any issues see the **Troubleshooting** section below. +After you start the active scheme, the simulator should open the iOS application and display `Hello from LiveView Native!` If you encounter any issues, see the Troubleshooting section below. @@ -268,8 +267,8 @@ After you start the active scheme, the simulator should open the iOS application If you encountered any issues with the native application, here are some troubleshooting steps you can use: -* **Reset Package Caches:** In the Xcode application go to `File -> Packages -> Reset Package Caches`. -* **Update Packages:** In the Xcode application go to `File -> Packages -> Update to Latest Package Versions`. +* **Reset Package Caches:** In the Xcode application go to **File -> Packages -> Reset Package Caches**. +* **Update Packages:** In the Xcode application go to **File -> Packages -> Update to Latest Package Versions**. * **Rebuild the Active Scheme**: In the Xcode application, press the `start active scheme` button to rebuild the active scheme and run it on the Xcode simulator. * Update your [Xcode](https://developer.apple.com/xcode/) version if it is not already the latest version * Check for error messages in the Livebook smart cells. From d20310e7b4c7b3eaef6682d1c439f2a205ff3840 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Fri, 3 May 2024 17:43:21 -0400 Subject: [PATCH 06/62] grammar check getting started --- livebooks/getting-started.livemd | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/livebooks/getting-started.livemd b/livebooks/getting-started.livemd index 9574c9b98..0905a4791 100644 --- a/livebooks/getting-started.livemd +++ b/livebooks/getting-started.livemd @@ -60,9 +60,9 @@ Mix.install( Our livebook guides provide step-by-step lessons to help you learn LiveView Native using Livebook. These guides assume that you already have some familiarity with Phoenix LiveView applications. -You can read these guides online, or for the best experience we recommend you click on the "Run in Livebook" badge to import and run these guides locally with Livebook. +You can read these guides online on HexDocs, but for the best experience, we recommend clicking on the "Run in Livebook" badge to import and run the guide locally with Livebook. -Each guide can be completed independently, but we suggest following them chronologically for the most comprehensive learning experience. +You may complete guides individually, but we suggest following them chronologically for the most comprehensive learning experience. ## Prerequisites @@ -80,7 +80,7 @@ While not necessary for our guides, we also recommend you install the following ## Hello World -If you are not already running this guide in Livebook, click on the "Run in Livebook" badge at the top of this page to import this guide into Livebook. +If you are not already running this guide in Livebook, click on the "Run in Livebook" badge at the top of this page to import it. Then, you can evaluate the following smart cell and visit http://localhost:4000 to ensure this Livebook works correctly. @@ -119,19 +119,17 @@ import Kernel :ok ``` -In an upcoming lesson, you'll set up an iOS application with Xcode so you can run code native examples. +In an upcoming lesson, you'll set up an iOS application with Xcode to run native code examples. ## Your Turn: Live Reloading -Change `Hello from LiveView!` to `Hello again from LiveView!` in the above LiveView. Re-evaluate the cell and notice the application live reloads and automatically updates in the browser. +In the above LiveView, change `Hello from LiveView!` to `Hello again from LiveView!`. After making the change, reevaluate the cell. Notice that the application live reloads and automatically updates in the browser. ## Kino LiveView Native -To run a Phoenix Server setup with LiveView Native from within Livebook we built the [Kino LiveView Native](https://github.com/liveview-native/kino_live_view_native) library. +To run a Phoenix + LiveView Native application from within Livebook we built the [Kino LiveView Native](https://github.com/liveview-native/kino_live_view_native) library. -Whenever you run one of our Livebooks, a server starts on localhost:4000. Ensure you have no other servers running on port 4000 - -Kino LiveView Native defines the **LiveView Native: LiveView** and **LiveViewNative: Render Component** smart cells within these guides. +Whenever you run one of our Livebooks, a server starts on localhost:4000. Ensure no other servers are running on port 4000, or you may experience issues. ## Troubleshooting From e037da6fa49aa2d1c41711214b04a18e3ade70d9 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Fri, 3 May 2024 17:43:27 -0400 Subject: [PATCH 07/62] grammar check swiftui views --- livebooks/swiftui-views.livemd | 54 ++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/livebooks/swiftui-views.livemd b/livebooks/swiftui-views.livemd index a89dfd630..918396437 100644 --- a/livebooks/swiftui-views.livemd +++ b/livebooks/swiftui-views.livemd @@ -58,19 +58,19 @@ Mix.install( ## Overview -LiveView Native aims to use minimal SwiftUI code. All patterns for building interactive UIs are the same as LiveView. However, unlike LiveView for the web, LiveView Native uses SwiftUI templates to build the native UI. +LiveView Native aims to use minimal SwiftUI code and instead rely on the same patterns used in traditional Phoenix LiveView development as much as possible. We'll primarily use The LiveView Naive SwiftUI DSL (Domain Specific Language) to build the native template. -This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common uses of each view and give you practical examples you can use to build your own native UIs. This lesson is like a recipe book you can refer back to whenever you need an example of how to use a particular SwiftUI view. In addition, once you understand how to convert these views into the LiveView Native DSL, you should have the tools to convert essentially any SwiftUI View into the LiveView Native DSL. +This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common use cases and provide practical examples of how to build native UIs. This lesson is like a recipe book you can refer to whenever you need an example of a particular SwiftUI view. + +In addition, we'll cover the LiveView Native DSL and teach you how to convert SwiftUI examples into the LiveView Native DSL. Once you understand how to convert SwiftUI code into the LiveView Native DSL, you'll have the knowledge you need to learn from the plethora of [SwiftUI resources available](https://developer.apple.com/tutorials/swiftui/creating-and-combining-views). ## Render Components -LiveView Native `0.3.0` introduced render components to better encourage isolation of native and web templates and move away from co-location templates within the same LiveView module. +LiveView Native `0.3.0` introduced render components to encourage isolation of native and web templates. This pattern generally scales better than co-located templates within the same LiveView module. Render components are namespaced under the main LiveView, and are responsible for defining the `render/1` callback function that returns the native template. -For example, and `ExampleLive` LiveView module would have an `ExampleLive.SwiftUI` render component module for the native Template. - -This `ExampleLive.SwiftUI` render component may define a `render/1` callback function as seen below. +For example, in the cell below, the `ExampleLive` LiveView module has a corresponding `ExampleLive.SwiftUI` render component module for the native template. This `ExampleLive.SwiftUI` render component may define a `render/1` callback function, as seen below. @@ -109,7 +109,7 @@ import Kernel :ok ``` -Throughout this and further material we'll re-define render components you can evaluate and see reflected in your Xcode iOS simulator. +Throughout this and further material, we'll re-define render components you can evaluate and see reflected in your Xcode iOS simulator. @@ -134,17 +134,21 @@ import Kernel :ok ``` +In a Phoenix application, these two modules would traditionally be in separate files. + + + ### Embedding Templates -Alternatively, you may omit the render callback and instead define a `.neex` (Native + Embedded Elixir) template. +Instead of defining a `render/1` callback function, you may instead define a `.neex` (Native + Embedded Elixir) template. By default, the module above would look for a template in the `swiftui/example_live*` path relative to the module's location. You can see the `LiveViewNative.Component` documentation for further explanation. -For the sake of ease when working in Livebook, we'll prefer defining the `render/1` callback. However, we recommend you generally prefer template files when working locally in Phoenix LiveView Native projects. +In Livebook, we'll use the `render/1` callback. However, we recommend using template files for local Phoenix + LiveView Native applications. ## SwiftUI Views -In SwiftUI, a "View" is like a building block for what you see on your app's screen. It can be something simple like text or an image, or something more complex like a layout with multiple elements. Views are the pieces that make up your app's user interface. +In SwiftUI, a "View" is like a building block for what you see on your app's screen. It can be something simple like text, or something more complex like a layout with multiple elements. Here's an example `Text` view that represents a text element. @@ -162,11 +166,11 @@ LiveView Native uses the following syntax to represent the view above. SwiftUI provides a wide range of Views that can be used in native templates. You can find a full reference of these views in the SwiftUI Documentation at https://developer.apple.com/documentation/swiftui/. You can also find a shorthand on how to convert SwiftUI syntax into the LiveView Native DLS in the [LiveView Native Syntax Conversion Cheatsheet](https://hexdocs.pm/live_view_native/cheatsheet.cheatmd). -## Text +We're going to cover a non-exhaustive list of the most common views and show examples of how to use them in your LiveView Native application. -We've already seen the [Text](https://developer.apple.com/documentation/swiftui/text) view, but we'll start simple to get the interactive tutorial running. +## Text -Evaluate the cell below, then in Xcode, Start the iOS application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native/create-a-swiftui-application.html) lesson and ensure you see the `"Hello, from LiveView Native!"` text. +Let's get the interactive tutorial running. Evaluate the cell below, then in Xcode, Start the iOS application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native/create-a-swiftui-application.html) lesson and ensure you see the `"Hello, from LiveView Native!"` text. @@ -200,7 +204,7 @@ SwiftUI includes many [Layout](https://developer.apple.com/documentation/swiftui Below, we've created a simple 3X3 game board to demonstrate how to use `VStack` and `HStack` to build a layout of horizontal rows in a single vertical column.o -Here's a diagram to demonstrate how these rows and columns create our desired layout. +Here's a diagram demonstrating how these rows and columns create our desired layout. ```mermaid flowchart @@ -266,7 +270,7 @@ import Kernel ### Your Turn: 3x3 board using columns -In the cell below, use `VStack` and `HStack` to create a 3X3 board using 3 columns instead of 3 rows as demonstrated above. The arrangement of `X` and `O` does not matter, however the content will not be properly aligned if you do not have exactly one character in each `Text` element. +In the cell below, use `VStack` and `HStack` to create a 3X3 board using 3 **columns** instead of 3 rows. The arrangement of `X` and `O` does not matter. However, the UI will not align if you don't have exactly one character in each `Text` element. ```mermaid flowchart @@ -396,7 +400,7 @@ Fortunately, we have a few common elements for creating a grid-based layout. * [Grid](https://developer.apple.com/documentation/swiftui/grid): A grid that arranges its child views in rows and columns that you specify. * [GridRow](https://developer.apple.com/documentation/swiftui/gridrow): A view that arranges its children in a horizontal line. -A grid layout vertically and horizontally aligns elements in the grid based on the number of elements in each row. +A grid layout aligns elements in the grid vertically and horizontally based on the number of elements in each row. Evaluate the example below and notice that rows and columns are aligned. @@ -441,7 +445,7 @@ import Kernel ## List -The SwiftUI [List](https://developer.apple.com/documentation/swiftui/list) view provides a system-specific interface, and has better performance for large amounts of scrolling elements. +The SwiftUI [List](https://developer.apple.com/documentation/swiftui/list) view provides a system-specific interface and performs well with large numbers of scrolling elements (up to certain limits). @@ -509,7 +513,7 @@ import Kernel The SwiftUI [ScrollView](https://developer.apple.com/documentation/swiftui/scrollview) displays content within a scrollable region. ScrollView is often used in combination with [LazyHStack](https://developer.apple.com/documentation/swiftui/lazyvstack), [LazyVStack](https://developer.apple.com/documentation/swiftui/lazyhstack), [LazyHGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid), and [LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid) to create scrollable layouts optimized for displaying large amounts of data. -While `ScrollView` also works with typical `VStack` and `HStack` views, they are not optimal choices for large amounts of data. +While `ScrollView` also works with typical `VStack` and `HStack` views, they are not optimal for large amounts of data. @@ -609,9 +613,9 @@ import Kernel :ok ``` -To resolve the performance problem for large amounts of data, you can use the Lazy views. Lazy views only create items as needed. Items won't be rendered until they are present on the screen. +You can use the Lazy views to resolve the performance problem for large amounts of data. Lazy views only create items as needed, meaning the client won't render them until they are on the screen. -The next example demonstrates how using `LazyVStack` instead of `VStack` resolves the performance issue. +The following example demonstrates how using `LazyVStack` instead of `VStack` resolves the performance issue. Evaluate the cell and notice the improved performance in your simulator. @@ -732,7 +736,7 @@ import Kernel ## AsyncImage -`AsyncImage` is best for network images, or images served by the Phoenix server. +`AsyncImage` is best for network images or images the Phoenix server serves. Here's an example of `AsyncImage` with a lorem picsum image from https://picsum.photos/400/600. @@ -860,7 +864,7 @@ import Kernel ### Your Turn: Asset Catalogue -You can place assets in your SwiftUI application's asset catalogue. Using the asset catalogue for SwiftUI assets provide many benefits such as device-specific image variants, dark mode images, high contrast image mode, and improved performance. +You can place assets in your SwiftUI application's asset catalog. Using the asset catalogue for SwiftUI assets provide many benefits such as device-specific image variants, dark mode images, high contrast image mode, and improved performance. Follow this guide: https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs#Add-a-new-asset to create a new asset called Image. @@ -868,13 +872,13 @@ Then evaluate the following example and you should see this image in your simula ![LiveView Native Logo](https://github.com/liveview-native/documentation_assets/blob/main/logo.png?raw=true) -You will need to **rebuild the native application** to pick up the changes to the assets catalogue. +Make sure to **rebuild the native application** to build the app with the updated asset catalog. ### Enter Your Solution Below -You should not need to make changes to this cell. Set up an image in your asset catalogue named "Image", rebuild your native application, then evaluate this cell. You should see the image in your iOS simulator. +Do not make changes to the cell below. Set up an image in your asset catalogue named "Image", rebuild your native application, then evaluate this cell. You should see the image in your iOS simulator. @@ -904,7 +908,7 @@ import Kernel A Button is a clickable SwiftUI View. -The label of a button can be any view, such as a [Text](https://developer.apple.com/documentation/swiftui/text) view for text-only buttons or a [Label](https://developer.apple.com/documentation/swiftui/label) view for buttons with icons. +A button's label can be any view, such as a [Text](https://developer.apple.com/documentation/swiftui/text) view for text-only buttons or a [Label](https://developer.apple.com/documentation/swiftui/label) view for buttons with icons. Evaluate the example below to see the SwiftUI [Button](https://developer.apple.com/documentation/swiftui/button) element. From 9e85aedc2e3cae61944142578f94b15c00f88b77 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Tue, 7 May 2024 13:10:14 -0400 Subject: [PATCH 08/62] Use flags to control mix docs tasks and generate markdown livebooks --- ...own.ex => lvn.swiftui.gen.livemarkdown.ex} | 5 +- livebooks/interactive-swiftui-views.livemd | 70 +- .../markdown/create-a-swiftui-application.md | 33 +- livebooks/markdown/forms-and-validation.md | 891 +++++++++++------- livebooks/markdown/getting-started.md | 16 +- .../markdown/interactive-swiftui-views.md | 70 +- livebooks/markdown/swiftui-views.md | 54 +- mix.exs | 50 +- 8 files changed, 684 insertions(+), 505 deletions(-) rename lib/mix/tasks/{livebooks_to_markdown.ex => lvn.swiftui.gen.livemarkdown.ex} (94%) diff --git a/lib/mix/tasks/livebooks_to_markdown.ex b/lib/mix/tasks/lvn.swiftui.gen.livemarkdown.ex similarity index 94% rename from lib/mix/tasks/livebooks_to_markdown.ex rename to lib/mix/tasks/lvn.swiftui.gen.livemarkdown.ex index 71d788c06..781a3986e 100644 --- a/lib/mix/tasks/livebooks_to_markdown.ex +++ b/lib/mix/tasks/lvn.swiftui.gen.livemarkdown.ex @@ -1,10 +1,11 @@ - -defmodule Mix.Tasks.LivebooksToMarkdown do +defmodule Mix.Tasks.Lvn.Swiftui.Gen.Livemarkdown do @moduledoc "Generates ex_doc friendly markdown guides from Livebook notebooks" @source "livebooks" @destination "livebooks/markdown" use Mix.Task + require Logger def run(_args) do + Logger.info("RUNNING LIVEBOOK DOCS") # clean up old notebooks File.rm_rf(@destination) File.mkdir(@destination) diff --git a/livebooks/interactive-swiftui-views.livemd b/livebooks/interactive-swiftui-views.livemd index 738ebe70d..c181b9b33 100644 --- a/livebooks/interactive-swiftui-views.livemd +++ b/livebooks/interactive-swiftui-views.livemd @@ -65,7 +65,7 @@ This guide assumes some existing familiarity with [Phoenix Bindings](https://hex We'll use the following LiveView and define new render component examples throughout the guide. - + ```elixir require Server.Livebook @@ -120,7 +120,7 @@ In the example below, the client sends a `"ping"` event to the server, and trigg Evaluate the example below, then click the `"Click me!"` button. Notice `"Pong"` printed in the server logs below. - + ```elixir require Server.Livebook @@ -130,7 +130,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" """ @@ -163,7 +163,7 @@ Event handlers in LiveView can update the LiveView's state in the socket. Evaluate the cell below to see an example of incrementing a count. - + ```elixir require Server.Livebook @@ -173,7 +173,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" """ @@ -217,7 +217,7 @@ There should be two buttons, each with a `phx-click` binding. One button should defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" <%= @count %> @@ -258,7 +258,7 @@ end ### Enter Your Solution Below - + ```elixir require Server.Livebook @@ -268,7 +268,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assign) do ~LVN""" <%= @count %> @@ -303,7 +303,7 @@ import Kernel Pressing a child item in the `List` on a native device triggers the `phx-change` event. In the example below we've bound the `phx-change` event to send the `"selection-changed"` event. This event is then handled by the `handle_event/3` callback function and used to change the selected item. - + ```elixir require Server.Livebook @@ -313,7 +313,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" Item <%= i %> @@ -352,7 +352,7 @@ import Kernel To control a `DisclosureGroup` view, use the `is-expanded` boolean attribute as seen in the example below. - + ```elixir require Server.Livebook @@ -362,7 +362,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" @@ -404,7 +404,7 @@ import Kernel The next example shows one pattern for displaying multiple expandable lists without needing to write multiple event handlers. - + ```elixir require Server.Livebook @@ -414,7 +414,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" @@ -463,6 +463,8 @@ import Kernel ## Controls and Indicators +In Phoenix, the `phx-change` event + In Phoenix, the `phx-change` event must be applied to a parent form. However in SwiftUI there is no similar concept of forms. Instead, SwiftUI provides [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) views. We can apply the `phx-change` binding to any of these views. Once bound, the SwiftUI view will send a message to the LiveView anytime the control or indicator changes its value. @@ -483,7 +485,7 @@ The following example shows you how to connect a SwiftUI [TextField](https://dev Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the server logs in the console below as a map of `%{"text" => value}`. - + ```elixir require Server.Livebook @@ -493,9 +495,11 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" - Enter text here + + Enter text here + """ end end @@ -526,7 +530,7 @@ The following example demonstrates how to set/access a TextField's value by cont This pattern is useful when rendering the TextField's value elsewhere on the page, using the `TextField` view's value in other event handler logic, or to set an initial value. - + ```elixir require Server.Livebook @@ -536,7 +540,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" Enter text here @@ -586,7 +590,7 @@ This code example renders a SwiftUI [Slider](https://developer.apple.com/documen Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the console below as a map of `%{"value" => value}`. - + ```elixir require Server.Livebook @@ -596,7 +600,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" + ```elixir require Server.Livebook @@ -648,7 +652,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" + ```elixir require Server.Livebook @@ -702,7 +706,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" On/Off """ @@ -737,7 +741,7 @@ import Kernel The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. - + ```elixir require Server.Livebook @@ -747,7 +751,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" """ @@ -795,7 +799,7 @@ The `DatePicker` view accepts a `displayed-components` attribute with the value You're going to change the `displayed-components` attribute in the example below to see both of these options. Change `"all"` to `"date"`, then to `"hour-and-minute"`. Re-evaluate the cell between changes and see the updated UI. - + ```elixir require Server.Livebook @@ -805,7 +809,8 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + @impl true + def render(assigns) do ~LVN""" """ @@ -819,6 +824,7 @@ defmodule ServerWeb.ExampleLive do @impl true def render(assigns), do: ~H"" + @impl true def handle_event("pick-date", params, socket) do {:noreply, socket} end @@ -849,7 +855,7 @@ Using the previous examples as inspiration, you're going to create a todo list. defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" Todo... @@ -910,7 +916,7 @@ end ### Enter Your Solution Below - + ```elixir require Server.Livebook @@ -920,7 +926,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assig) do ~LVN""" """ diff --git a/livebooks/markdown/create-a-swiftui-application.md b/livebooks/markdown/create-a-swiftui-application.md index fd5d7a4b7..b27d45f9e 100644 --- a/livebooks/markdown/create-a-swiftui-application.md +++ b/livebooks/markdown/create-a-swiftui-application.md @@ -14,7 +14,7 @@ In future lessons, you'll use this iOS application to view iOS examples in the X ## Prerequisites -First, make sure you have followed the [Getting Started](https://hexdocs.pm/live_view_native/getting_started.md) guide. Then evaluate the smart cell below and visit http://localhost:4000 to ensure the Phoenix server runs properly. You should see the text `Hello from LiveView!` +First, make sure you have followed the [Getting Started](https://hexdocs.pm/live_view_native/getting_started.md) guide. Then, evaluate the smart cell below. Visit http://localhost:4000 to ensure the Phoenix server runs properly. You should see the text `Hello from LiveView!` @@ -48,7 +48,7 @@ Open Xcode and select Create New Project. -Select the `iOS` and `App` options to create an iOS application. Then click `Next`. +To create an iOS application, select the **iOS** and **App** options and click **Next**. @@ -56,14 +56,14 @@ Select the `iOS` and `App` options to create an iOS application. Then click `Nex -Choose options for your new project that match the following image, then click `Next`. +Choose options for your new project that match the following image, then click **Next**. ### What do these options mean? -* **Product Name:** The name of the application. This can be any valid name. We've chosen `Guides`. -* **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recomends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using `com.example.your_name` where `your_name` is your organization or personal name. +* **Product Name:** The name of the application. This can be any valid name. We've chosen **Guides`. +* **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recommends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using **com.example.your_name** where **your_name** is your organization or personal name. * **Interface:**: The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. -* **Language:** Determines which language Xcode should use for the project. Select `Swift`. +* **Language:** Determines which language Xcode should use for the project. Select **Swift**. @@ -72,7 +72,7 @@ Choose options for your new project that match the following image, then click ` -Select an appropriate folder location where you would like to store the iOS project, then click `Create`. +Select an appropriate folder location to store the iOS project, then click **Create.** @@ -88,9 +88,9 @@ You should see the default iOS application generated by Xcode. ## Add the LiveView Client SwiftUI Package -In Xcode from the project you just created, select `File -> Add Package Dependencies`. Then, search for `liveview-client-swiftui`. Once you have selected the package, click `Add Package`. +In Xcode from the project you just created, select **File -> Add Package Dependencies**. Then, search for `liveview-client-swiftui`. Once you have chosen the package, click **Add Package**. -The image below was created using version `0.2.0`. You should select whichever is the latest version of LiveView Native. +The image below displays `0.2.0`. You should select the latest version of LiveView Native. @@ -98,7 +98,7 @@ The image below was created using version `0.2.0`. You should select whichever i -Choose the Package Products for `liveview-client-swiftui`. Select `Guides` as the target for `LiveViewNative` and `LiveViewNativeStylesheet`. This adds both of these dependencies to your iOS project. +Choose the Package Products for `liveview-client-swiftui`. Select **Guides** as the target for `LiveViewNative` and `LiveViewNativeStylesheet` to add these dependencies to your iOS project. @@ -106,8 +106,7 @@ Choose the Package Products for `liveview-client-swiftui`. Select `Guides` as th -At this point, you'll need to enable permissions for plugins used by LiveView Native. -You should see the following prompt. Click `Trust & Enable All`. +At this point, you'll need to enable permissions for plugins used by LiveView Native. You should see the following prompt. Click **Trust & Enable All**. @@ -115,7 +114,7 @@ You should see the following prompt. Click `Trust & Enable All`. -You'll also need to manually navigate to the error tab (shown below) and manually trust and enable packages. Click on each error to trigger a prompt. Select `Trust & Enable All` to enable the plugin. +You'll also need to manually navigate to the error tab (shown below) to trust and enable packages. Click on each error to trigger a prompt. Select **Trust & Enable All** to enable the plugin. The specific plugins are subject to change. At the time of writing you need to enable `LiveViewNativeStylesheetMacros`, `LiveViewNativeMacros`, and `CasePathMacros` as shown in the images below. @@ -188,11 +187,11 @@ graph LR; Click the `start active scheme` button to build the project and run it on the iOS simulator. -> A [build scheme](https://developer.apple.com/documentation/xcode/build-system) contains a list of targets to build, and any configuration and environment details that affect the selected action. For example, when you build and run an app, the scheme tells Xcode what launch arguments to pass to the app. +> A [build scheme](https://developer.apple.com/documentation/xcode/build-system) contains a list of targets to build and any configuration and environment details that affect the selected action. When you build and run an app, the scheme tells Xcode what launch arguments to pass to the app. > > * https://developer.apple.com/documentation/xcode/build-system -After you start the active scheme, the simulator should open the iOS application and display `Hello from LiveView Native!`. If you encounter any issues see the **Troubleshooting** section below. +After you start the active scheme, the simulator should open the iOS application and display `Hello from LiveView Native!` If you encounter any issues, see the Troubleshooting section below. @@ -204,8 +203,8 @@ After you start the active scheme, the simulator should open the iOS application If you encountered any issues with the native application, here are some troubleshooting steps you can use: -* **Reset Package Caches:** In the Xcode application go to `File -> Packages -> Reset Package Caches`. -* **Update Packages:** In the Xcode application go to `File -> Packages -> Update to Latest Package Versions`. +* **Reset Package Caches:** In the Xcode application go to **File -> Packages -> Reset Package Caches**. +* **Update Packages:** In the Xcode application go to **File -> Packages -> Update to Latest Package Versions**. * **Rebuild the Active Scheme**: In the Xcode application, press the `start active scheme` button to rebuild the active scheme and run it on the Xcode simulator. * Update your [Xcode](https://developer.apple.com/xcode/) version if it is not already the latest version * Check for error messages in the Livebook smart cells. diff --git a/livebooks/markdown/forms-and-validation.md b/livebooks/markdown/forms-and-validation.md index d3e708f75..68fdb4fe4 100644 --- a/livebooks/markdown/forms-and-validation.md +++ b/livebooks/markdown/forms-and-validation.md @@ -6,447 +6,631 @@ The [LiveView Native Live Form](https://github.com/liveview-native/liveview-native-live-form) project makes it easier to build forms in LiveView Native. This project enables you to group different [Control Views](https://developer.apple.com/documentation/swiftui/controls-and-indicators) inside of a `LiveForm` and control them collectively under a single `phx-change` or `phx-submit` event handler, rather than with multiple different `phx-change` event handlers. -Getting the most out of this material requires some understanding of the [Ecto](https://hexdocs.pm/ecto/Ecto.html) project and in particular a reasonably deep understanding of [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html). Review the linked Ecto documentation if you find any of the examples difficult to follow. +Getting the most out of this material requires some understanding of the [Ecto](https://hexdocs.pm/ecto/Ecto.html) project and in particular a reasonably deep understanding of [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html). Review the Ecto documentation if you find any of the examples difficult to follow. ## Installing LiveView Native Live Form To install LiveView Native Form, we need to add the `liveview-native-live-form` SwiftUI package to our iOS application. -Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README and come back to this guide after you have finished the installation process. +Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README to add the `liveview-native-live-form` package to the SwiftUI application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native_swiftui/create-a-swiftui-application.html) guide. + +Come back to this guide and continue after you have finished the installation process. ## Creating a Basic Form -Once you have the LiveView Native Form package installed, you can use the `LiveForm` and `LiveSubmitButton` views to build forms more conveniently. +The LiveView Native `mix lvn.install` task generates a [core_components.swiftui.ex](https://github.com/liveview-native/liveview-client-swiftui/blob/main/priv/templates/lvn.swiftui.gen/core_components.ex) for native SwiftUI function components similar to the [core_components.ex](https://github.com/phoenixframework/phoenix/blob/main/priv/templates/phx.gen.live/core_components.ex) file generated in a traditional phoenix application for web function components. + +See Phoenix's [Components and HEEx](https://hexdocs.pm/phoenix/components.html) HexDoc documentation if you need a primer on function components. + +In the `core_components.swiftui.ex` file there's a `simple_form/1` component that is a similar abstraction to the `simple_form/1` component found in `core_components.ex`. + +First, we'll see how to use this abstraction at a basic level, then later we'll dive deeper into how forms work under the hood in LiveView Native. + + + +### A Basic Form + +This code below demonstrates how the basic skeleton of a native and web form that share event handlers for the `phx-submit` and `phx-change` handlers. + +We'll break down and understand the individual parts of this form in a moment. -Here's a basic example of a `LiveForm`. Keep in mind that `LiveForm` requires an `id` attribute. +For now, evaluate the following example. Open the native form in your simulator, and open the web form on http://localhost:4000/. Enter some text into both forms, then submit them. Watch the logs in the cell below to see the printed params.' - + ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] -defmodule Server.ExampleLive do - use Phoenix.LiveView - use LiveViewNative.LiveView + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:value]} type="TextField" placeholder="Enter a value" /> + <:actions> + <.button type="submit"> + Ping + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, form: to_form(%{}, as: "my_form"))} + end @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Placeholder - Submit - + def render(assigns) do + ~H""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:value]} placeholder="Enter a value" /> + <:actions> + <.button type="submit"> + Ping + + + """ end @impl true def handle_event("submit", params, socket) do - IO.inspect(params) + IO.inspect(params, label: "Submitted") {:noreply, socket} end -end -|> KinoLiveViewNative.register("/", ":index") -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok + @impl true + def handle_event("validate", params, socket) do + IO.inspect(params, label: "Validating") + {:noreply, socket} + end +end ``` -When a form is submitted, its data is sent as a map where each key is the 'name' attribute of the form's control views. Evaluate the example above in your simulator and you will see a map similar to the following: +After submitting both forms, notice that both the web and native params are the same shape:`%{"my_form" => %{"value" => "some text"}}`. This makes it easier to share event handlers for both web and native. - +Sharing event handlers hugely simplifies and speeds up the process of writing web and native application logic because you only have to write the logic once. Alternatively, if your web and native UI deviates significantly, you can also separate the event handlers. -```elixir -%{"my-text" => "some value"} -``` +## Breaking down a Basic Form -In a real-world application you could use these params to trigger some application logic, such as inserting a record into the database. +### Simple Form -## Controls and Indicators +The interface for the native `simple_form/1` and web `simple_form/1` is intentionally identical. -We've already covered many individual controls and indicator views that you can use inside of forms. For more information on those, go to the [Interactive SwiftUI Views](https://hexdocs.pm/live_view_native/interactive-swiftui-views.html) guide. +```heex +<.simple_form for={@form} id="form" phx-submit="submit"> + + +``` + +We'll go into the internal implementation details later on, but for now you can treat these components as functionally identical. Both require a unique `id` and accept the `for` attribute that contains the [Phoenix.HTML.Form] datastructure containing form fields, error messages, and other form data. + +If you need a refresher on forms in Phoenix, see the [Form Bindings](https://hexdocs.pm/phoenix_live_view/form-bindings.html) HexDoc documentation. -### Your Turn +### Inputs -Create a form that has `TextField`, `Slider`, `Toggle`, and `DatePicker` fields. +Both web and native core components define a `input/1` function component. Inputs in the web form and native form differ since one is an abstraction on top of HTML elements and the other is an abstraction on top of SwiftUI Views. Therefore, they have different values for the `type` attribte that determines which input type to render. -### Example Solution +On web, the `input/1` component accepts the following values for the `type` attribute. These reflect [html input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types). + + ```elixir -defmodule Server.MultiInputFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView + attr :type, :string, + default: "text", + values: ~w(checkbox color date datetime-local email file hidden month number password + range radio search select tel text textarea time url week) +``` - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Placeholder - - - - Submit - - """ - end +On native, the `input/1` component accepts the following values for the `type` attribute. These reflect the SwiftUI Views from the [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) and [Text Input and Outputs](https://developer.apple.com/documentation/swiftui/text-input-and-output) sections. - @impl true - def handle_event("submit", params, socket) do - IO.inspect(params) - {:noreply, socket} - end -end + + +```elixir +attr :type, :string, + default: "TextField", + values: ~w(TextFieldLink DatePicker MultiDatePicker Picker SecureField Slider Stepper TextEditor TextField Toggle hidden) ``` +## Changesets +The [Phoenix.Component.to_form/2](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#to_form/2) function also supports Ecto changesets for form data and error validation. See [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html) for a refresher on changesets. Also see [Form Bindings](https://hexdocs.pm/phoenix_live_view/form-bindings.html) and [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) for a refresher on Phoenix Forms. - +We'll use the following changeset to demonstrate how to validate data in a LiveView Native Live Form. ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] - -defmodule Server.MultiInputFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - """ - end +defmodule User do + import Ecto.Changeset + defstruct [:email] + @types %{email: :string} - # You may use this handler to test your solution. - # You should not need to modify this handler. - @impl true - def handle_event("submit", params, socket) do - IO.inspect(params) - {:noreply, socket} + def changeset(user, params) do + {user, @types} + |> cast(params, [:email]) + |> validate_required([:email]) + |> validate_format(:email, ~r/@/) end end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok ``` -### Controlled Values +The [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) struct stores the changeset. The `simple_form/1` and `input/1` components for both web and native use the [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) struct and nested [Phoenix.HTML.FormField](https://hexdocs.pm/phoenix_html/Phoenix.HTML.FormField.html) structs to render form data and display errors. + +For example, `:action` field in the changeset determines if errors should display or not. Here's an example we'll use in a moment of faking a database `:insert` action and storing the changeset information inside of a form. -Some control views such as the `Stepper` require manually displaying their value. In this case, we can store the form params in the socket and update them everytime the `phx-change` form binding submits an event. You can also use this pattern to provide default values. +```elixir +User.changeset(%User{}, %{email: "test"}) +|> Map.put(:action, :insert) +|> Phoenix.Component.to_form() +``` -Evaluate the example below to see this in action. +Here's an example of how we can use Ecto changesets with the LiveView Native Live Form. Now when we submit or validate the form data we apply the changes to the changeset and store the new version of the form in the socket. The `simple_form/1` and `input/1` components use the form data to render content and display errors. - +Evaluate the cell below and open your iOS application. Submit the form with an invalid email. You should notice a `has invalid format` error appear. + + ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] -defmodule Server.StepperLive do - use Phoenix.LiveView - use LiveViewNative.LiveView + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:email]} type="TextField" placeholder="Email" /> + <:actions> + <.button type="submit"> + Submit + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, params: %{"my-stepper" => 1})} + changeset = User.changeset(%User{}, %{}) + {:ok, assign(socket, form: to_form(changeset), check_errors: false)} end @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - <%= @params["my-stepper"] %> - + def render(assigns) do + ~H""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:email]} placeholder="Email" /> + <:actions> + <.button type="submit"> + Submit + + + """ end @impl true - def handle_event("change", params, socket) do - IO.inspect(params) - {:noreply, assign(socket, params: params)} + def handle_event("submit", %{"user" => params}, socket) do + changeset = + User.changeset(%User{}, params) + # Faking a Database insert action + |> Map.put(:action, :insert) + |> IO.inspect(label: "Form Field Values") + + {:noreply, assign(socket, form: to_form(changeset))} end -end -|> KinoLiveViewNative.register("/", ":index") -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok + @impl true + def handle_event("validate", %{"user" => params}, socket) do + changeset = + User.changeset(%User{}, params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, form: to_form(changeset))} + end +end ``` -### Secure Field +## Keyboard Types -For password entry, or anytime you want to hide a given value, you can use the [SecureField](https://developer.apple.com/documentation/swiftui/securefield) view. This field works mostly the same as a `TextField` but hides the visual text. +The [keyboardType](https://developer.apple.com/documentation/swiftui/view/keyboardtype(_:)) modifier changes the type of keyboard for a TextField view. - +Evaluate the example below to see the different keyboards as you focus on each input. If you don't see the keyboard, go to `I/O` -> `Keyboard` -> `Toggle Software Keyboard` to enable the software keyboard in your simulator. -```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] + -defmodule Server.SecureLive do - use Phoenix.LiveView - use LiveViewNative.LiveView +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - Enter a Password + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form"> + <.input field={@form[:number_pad]} type="TextField" class="keyboardType(.numberPad)"/> + <.input field={@form[:email_address]} type="TextField" class="keyboardType(.emailAddress)"/> + <.input field={@form[:phonePad]} type="TextField" class="keyboardType(.phonePad)"/> + <:actions> + <.button type="submit"> + Submit + + + """ end - - @impl true - def handle_event("change", params, socket) do - IO.inspect(params) - {:noreply, socket} - end end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok ``` -## Keyboard Types +For a complete list of accepted keyboard types, see the [UIKeyboardType](https://developer.apple.com/documentation/uikit/uikeyboardtype) documentation. -To format a `TextField` for specific input types we can use the [keyboardType](https://developer.apple.com/documentation/swiftui/view/keyboardtype(_:)) modifier. +## Core Components -For a complete list of accepted keyboard types, see the [UIKeyboardType](https://developer.apple.com/documentation/uikit/uikeyboardtype) documentation. +Setting up a LiveView Native application using the generators creates a `core_components.swiftui.ex` file. If you have the [liveview-native-live-form](https://github.com/liveview-native/liveview-native-live-form) dependency, this file includes function components for building forms. -Below we've created several different common keyboard types. We've also included a generic `keyboard-*` to demonstrate how you can make a reusable class. +To better understand how to work with each core component, refer to the `core_components.swiftui.ex` file generated in a Phoenix LiveView Native project. For the core components used in this Livebook, refer to the [core_components.swiftui.ex](https://github.com/liveview-native/kino_live_view_native/blob/main/apps/server_web/lib/server_web/components/core_components.swiftui.ex) from the Kino LiveView Native project. -```elixir -defmodule KeyboardStylesheet do - use LiveViewNative.Stylesheet, :swiftui +We've already been using the two main functions, `simple_form/1` and `input/1`. These are abstractions on top of the native SwiftUI views and some custom views defined by the LiveView Native Live Form library. - ~SHEET""" - "number-pad" do - keyboardType(.numberPad) - end +in this section, we'll dive deeper into these abstractions so that you can build your own custom forms. - "email-address" do - keyboardType(.emailAddress) - end + - "phone-pad" do - keyboardType(.phonePad) - end +### Simple Form + +Here's the `simple_form/1` definition. + + - "keyboard-" <> type do - keyboardType(to_ime(type)) +```elixir + attr :for, :any, required: true, doc: "the datastructure for the form" + attr :as, :any, default: nil, doc: "the server side parameter to collect all input under" + + attr :rest, :global, + include: ~w(autocomplete name rel action enctype method novalidate target multipart), + doc: "the arbitrary HTML attributes to apply to the form tag" + + slot :inner_block, required: true + slot :actions, doc: "the slot for form actions, such as a submit button" + + def simple_form(assigns) do + ~LVN""" + <.form :let={f} for={@for} as={@as} {@rest}> +
+ <%= render_slot(@inner_block, f) %> +
+ <%= for action <- @actions do %> + <%= render_slot(action, f) %> + <% end %> +
+
+ + """ end +``` + +We show this to highlight the similarity between this form, and the one used in `core_components.ex`. + + + +```elixir +attr :for, :any, required: true, doc: "the datastructure for the form" +attr :as, :any, default: nil, doc: "the server side parameter to collect all input under" + +attr :rest, :global, + include: ~w(autocomplete name rel action enctype method novalidate target multipart), + doc: "the arbitrary HTML attributes to apply to the form tag" + +slot :inner_block, required: true +slot :actions, doc: "the slot for form actions, such as a submit button" + +def simple_form(assigns) do + ~H""" + <.form :let={f} for={@for} as={@as} {@rest}> +
+ <%= render_slot(@inner_block, f) %> +
+ <%= render_slot(action, f) %> +
+
+ """ end ``` -Evaluate the example below to see the different keyboards as you focus on each input. If you don't see the keyboard, go to `I/O` -> `Keyboard` -> `Toggle Software Keyboard` to enable the software keyboard in your simulator. + + +### Input + +The `type` attribute on the `input/1` component determines which View to render. Here's the same `input/1` definition. - + ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] +attr :id, :any, default: nil +attr :name, :any +attr :label, :string, default: nil +attr :value, :any + +attr :type, :string, + default: "TextField", + values: ~w(TextFieldLink DatePicker MultiDatePicker Picker SecureField Slider Stepper TextEditor TextField Toggle hidden) + +attr :field, Phoenix.HTML.FormField, + doc: "a form field struct retrieved from the form, for example: @form[:email]" + +attr :errors, :list, default: [] +attr :checked, :boolean, doc: "the checked flag for checkbox inputs" +attr :prompt, :string, default: nil, doc: "the prompt for select inputs" +attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2" +attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs" + +attr :min, :any, default: nil +attr :max, :any, default: nil + +attr :placeholder, :string, default: nil + +attr :readonly, :boolean, default: false + +attr :autocomplete, :string, + default: "on", + values: ~w(on off) + +attr :rest, :global, + include: ~w(disabled step) + +slot :inner_block + +def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do + assigns + |> assign(field: nil, id: assigns.id || field.id) + |> assign(:errors, Enum.map(field.errors, &translate_error(&1))) + |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end) + |> assign_new(:value, fn -> field.value end) + |> assign( + :rest, + Map.put(assigns.rest, :class, [ + Map.get(assigns.rest, :class, ""), + (if assigns.readonly or Map.get(assigns.rest, :disabled, false), do: "disabled-true", else: ""), + (if assigns.autocomplete == "off", do: "text-input-autocapitalization-never autocorrection-disabled", else: "") + ] |> Enum.join(" ")) + ) + |> input() +end +``` -defmodule Server.KeyboardLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use KeyboardStylesheet +The `input/1` function then continues to call a separate function definition depending on the `type` attribute. For example, here's the `"TextField"` definition: - @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Enter Phone - Enter Number - Enter Number - """ - end + - def render(assigns) do - ~H""" -

Hello from LiveView!

- """ - end +```elixir +def input(%{type: "TextField"} = assigns) do + ~LVN""" + + <%= @placeholder || @label %> + <.error :for={msg <- @errors}><%= msg %> + + """ end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok ``` -## Validation +Here's a list of valid options with links to their documentation: + +* [TextFieldLink](https://developer.apple.com/documentation/swiftui/textfieldlink) +* [DatePicker](https://developer.apple.com/documentation/swiftui/datepicker) +* [MultiDatePicker](https://developer.apple.com/documentation/swiftui/multidatepicker) +* [Picker](https://developer.apple.com/documentation/swiftui/picker) +* [SecureField](https://developer.apple.com/documentation/swiftui/securefield) +* [Slider](https://developer.apple.com/documentation/swiftui/slider) +* [Stepper](https://developer.apple.com/documentation/swiftui/stepper) +* [TextEditor](https://developer.apple.com/documentation/swiftui/texteditor) +* [TextField](https://developer.apple.com/documentation/swiftui/textfield) +* [Toggle](https://developer.apple.com/documentation/swiftui/toggle) +* hidden -In this section, we'll focus mainly on using [Ecto Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) to validate data, but know that this is not the only way to validate data if you would like to write your own custom logic in the form event handlers, you absolutely can. +For more on the form compatible views see the [Interactive SwiftUI Views](https://hexdocs.pm/liveview-client-swiftui/interactive-swiftui-views.html) guide. -### LiveView Native Changesets Coming Soon! +### Core Components vs Views -LiveView Native Form doesn't currently natively support [Changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) and [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) structs the way a traditional [Phoenix.Component.form](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1) does. However there is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/5) to add this behavior so this may change in the near future. As a result, this section is somewhat more verbose than will be necessary in the future, as we have to manually define much of the error handling logic that we expect will no longer be necessary in version `0.3` of LiveView Native. +SwiftUI Core Components attempts to make the API consistent and easy to remember between platforms. For that reason, we deviate somewhat from the interface used by SwiftUI. -To make error handling easier, we've defined an `ErrorUtils` module below that will handle extracting the error message out of a Changeset. This will not be necessary in future versions of LiveView Native, but is a convenient helper for now. +Let's take the Slider view as an example. The Slider view accepts the `min` and `max` attributes instead of `lowerBound` and `upperBound` because they better reflect the html [range](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range) slider. The component also accepts the `label` attribute instead of using children for the same reason. + + ```elixir -defmodule ErrorUtils do - def error_message(errors, field) do - with {msg, opts} <- errors[field] do - Server.CoreComponents.translate_error({msg, opts}) - else - _ -> "" - end + def input(%{type: "Slider"} = assigns) do + ~LVN""" + + + <%= @label %> + <%= @label %> + + <.error :for={msg <- @errors}><%= msg %> + + """ end -end ``` -For the sake of context, the `translate_message/2` function handles formatting Ecto Changeset errors. For example, it will inject values such as `count` into the string. + -```elixir -Server.CoreComponents.translate_error( - {"name must be longer than %{count} characters", [count: 10]} -) -``` +### Labels with Form Data + +Sometimes you may wish to use data within the form separately as part of your UI. For example, let's say we want to have a Stepper view with a dynamic label based on the current step value. In these cases, you can access form data through the `@form.params`. -### Changesets +Here's an example showing how to have a dynamic label based on the Stepper view's current value. Evaluate the example below and run it in your simulator. -Here's a `User` changeset we're going to use to validate a `User` struct's `email` field. + ```elixir -defmodule User do - import Ecto.Changeset - defstruct [:email] - @types %{email: :string} +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] - def changeset(user, params) do - {user, @types} - |> cast(params, [:email]) - |> validate_required([:email]) - |> validate_format(:email, ~r/@/) + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input + field={@form[:value]} + type="Stepper" + label={"Value: #{@form.params["value"]}"} + /> + <:actions> + <.button type="submit"> + Ping + + + + """ end end -``` -We're going to define an `error` class so errors will appear red and be left-aligned. +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view -```elixir -defmodule ErrorStylesheet do - use LiveViewNative.Stylesheet, :swiftui + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, form: to_form(%{"value" => 0}, as: "my_form"))} + end + + @impl true + def render(assigns), do: ~H"" - ~SHEET""" - "error" do - foregroundStyle(.red) - frame(maxWidth: .infinity, alignment: .leading) + @impl true + def handle_event("submit", params, socket) do + {:noreply, assign(socket, form: to_form(params, as: "my_form"))} + end + + @impl true + def handle_event("validate", %{"my_form" => params}, socket) do + {:noreply, assign(socket, form: to_form(params, as: "my_form"))} end - """ end ``` -Then, we're going to create a LiveView that uses the `User` changeset to validate data. +### Your Turn -Evaluate the example below and view it in your simulator. We've included and `IO.inspect/2` call to view the changeset after submitting the form. Try submitting the form with different values to understand how those values affect the changeset. +Create a form that has `TextField`, `Slider`, `Toggle`, and `DatePicker` fields. - +### Example Solution ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] -defmodule Server.FormValidationLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use ErrorStylesheet + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:text]} type="TextField" placeholder="Enter a value" /> + <.input field={@form[:slider]} type="Slider"/> + <.input field={@form[:toggle]} type="Toggle"/> + <.input field={@form[:date_picker]} type="DatePicker"/> + <:actions> + <.button type="submit"> + Ping + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view @impl true def mount(_params, _session, socket) do - user_changeset = User.changeset(%User{}, %{}) - {:ok, assign(socket, :user_changeset, user_changeset)} + {:ok, assign(socket, form: to_form(%{}, as: "my_form"))} end @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - Enter your email - - <%= ErrorUtils.error_message(@user_changeset.errors, :email) %> - - Submit - - """ - end + def render(assigns), do: "" @impl true - def handle_event("validate", params, socket) do - user_changeset = - User.changeset(%User{}, params) - # Preserve the `:action` field so errors do not vanish. - |> Map.put(:action, socket.assigns.user_changeset.action) - - {:noreply, assign(socket, :user_changeset, user_changeset)} - end - def handle_event("submit", params, socket) do - user_changeset = - User.changeset(%User{}, params) - # faking a Database insert action - |> Map.put(:action, :insert) - # Submit the form and inspect the logs below to view the changeset. - |> IO.inspect(label: "Form Field Values") + IO.inspect(params, label: "Submitted") + {:noreply, socket} + end - {:noreply, assign(socket, :user_changeset, user_changeset)} + @impl true + def handle_event("validate", params, socket) do + IO.inspect(params, label: "Validating") + {:noreply, socket} end end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok ``` -In the code above, the `"sumbit"` and `"validate"` events update the changeset based on the current form params. This fills the `errors` field used by the `ErrorUtils` module to format the error message. -After submitting the form, the `:action` field of the changeset has a value of `:insert`, so the red Text appears using the `:if` conditional display logic. -In the future, this complexity will likely be handled by the `live_view_native_form` library, but for now this example exists to show you how to write your own error handling based on changesets if needed. + - - -### Empty Fields Send `"null"`. +```elixir +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] -If you submit a form with empty fields, those fields may currently send `"null"`. There is an [open issue](https://github.com/liveview-native/liveview-native-live-form/issues/6) to fix this bug, but it may affect your form behavior for now and require a temporary workaround until the issue is fixed. + def render(assigns) do + ~LVN""" + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + + <:actions> + <.button type="submit"> + Ping + + + + """ + end +end -## Mini Project: User Form +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view -Taking everything you've learned, you're going to create a more complex user form with data validation and error displaying. We've defined a `FormStylesheet` you can use (and modify) if you would like to style your form. + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, form: to_form(%{}, as: "my_form"))} + end -```elixir -defmodule FormStylesheet do - use LiveViewNative.Stylesheet, :swiftui + @impl true + def render(assigns), do: ~H"" - ~SHEET""" - "error" do - foregroundStyle(.red) - frame(maxWidth: .infinity, alignment: .leading) + @impl true + def handle_event("submit", params, socket) do + IO.inspect(params, label: "Submitted") + {:noreply, socket} end - "keyboard-" <> type do - keyboardType(to_ime(type)) + @impl true + def handle_event("validate", params, socket) do + IO.inspect(params, label: "Validating") + {:noreply, socket} end - """ end ``` +### Native Views + +The LiveView Native LiveForm library defines [a few custom SwiftUI views](https://github.com/liveview-native/liveview-native-live-form/tree/main/swiftui/Sources/LiveViewNativeLiveForm) such as `LiveForm` and `LiveSubmitButton`. Several core components use these components. + +Typically, you won't need to use these views directly and will instead rely upon the core components directly. + +## Mini Project: User Form + +Taking everything you've learned, you're going to create a more complex user form with data validation and error displaying. + + + ### User Changeset First, create a `CustomUser` changeset below that handles data validation. @@ -481,18 +665,11 @@ defmodule CustomUser do {user, @types} |> cast(params, Map.keys(@types)) |> validate_required(Map.keys(@types)) + |> validate_format(:email, ~r/@/) |> validate_length(:password, min: 10) |> validate_number(:age, greater_than: 0, less_than: 200) |> validate_acceptance(:accepted_terms) end - - def error_message(changeset, field) do - with {msg, _reason} <- changeset.errors[field] do - msg - else - _ -> "" - end - end end ``` @@ -514,7 +691,7 @@ end ### LiveView -Next, create the `CustomUserFormLive` Live View that lets the user enter their information and displays errors for invalid information upon form submission. +Next, create a Live View that lets the user enter their information and displays errors for invalid information. **Requirements** @@ -528,113 +705,95 @@ Next, create the `CustomUserFormLive` Live View that lets the user enter their i ### Example Solution ```elixir -defmodule Server.CustomUserFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use FormStylesheet +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.input field={@form[:name]} type="TextField" placeholder="name" /> + <.input field={@form[:email]} type="TextField" placeholder="email" /> + <.input field={@form[:password]} type="SecureField" placeholder="password" /> + <.input field={@form[:age]} type="TextField" placeholder="age" class="keyboardType(.numberPad)" /> + <.input field={@form[:accepted_terms]} type="Toggle"/> + <.input field={@form[:birthdate]} type="DatePicker"/> + + <:actions> + <.button type="submit"> + Submit + + + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view @impl true def mount(_params, _session, socket) do - changeset = CustomUser.changeset(%CustomUser{}, %{}) - - {:ok, assign(socket, :changeset, changeset)} + changeset = User.changeset(%CustomUser{}, %{}) + {:ok, assign(socket, form: to_form(changeset, as: :user))} end @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - name... - <.form_error changeset={@changeset} field={:name}/> - - email... - <.form_error changeset={@changeset} field={:email}/> - - age... - <.form_error changeset={@changeset} field={:age}/> - - password... - <.form_error changeset={@changeset} field={:password}/> - - Accept the Terms and Conditions: - <.form_error changeset={@changeset} field={:accepted_terms}/> - - Birthday: - <.form_error changeset={@changeset} field={:birthdate}/> - Submit - - """ - end + def render(assigns), do: ~H"" @impl true - def handle_event("validate", params, socket) do - user_changeset = + def handle_event("submit", %{"user" => params}, socket) do + changeset = CustomUser.changeset(%CustomUser{}, params) - |> Map.put(:action, socket.assigns.changeset.action) + # Faking a Database insert action + |> Map.put(:action, :insert) + |> IO.inspect(label: "Form Field Values") - {:noreply, assign(socket, :changeset, user_changeset)} + {:noreply, assign(socket, form: to_form(changeset, as: :user))} end - def handle_event("submit", params, socket) do - user_changeset = + @impl true + def handle_event("validate", %{"user" => params}, socket) do + IO.inspect(params) + changeset = CustomUser.changeset(%CustomUser{}, params) - |> Map.put(:action, :insert) - - {:noreply, assign(socket, :changeset, user_changeset)} - end + |> Map.put(:action, :validate) + |> IO.inspect() - # While not strictly required, the form_error component reduces code bloat. - def form_error(assigns) do - ~SWIFTUI""" - - <%= CustomUser.error_message(@changeset, @field) %> - - """ + {:noreply, assign(socket, form: to_form(changeset, as: :user))} end end ``` - + ```elixir -require KinoLiveViewNative.Livebook -import KinoLiveViewNative.Livebook -import Kernel, except: [defmodule: 2] +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] -defmodule Server.CustomUserFormLive do - use Phoenix.LiveView - use LiveViewNative.LiveView - use FormStylesheet + def render(assigns) do + ~LVN""" + + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view @impl true def mount(_params, _session, socket) do - # Remember to provide the initial changeset + # Remember to assign the form {:ok, socket} end @impl true - def render(%{format: :swiftui} = assigns) do - ~SWIFTUI""" - - """ - end - - @impl true - # Write your `"validate"` event handler - def handle_event("validate", params, socket) do - {:noreply, socket} - end + def render(assigns), do: ~H"" - # Write your `"submit"` event handler - def handle_event("submit", params, socket) do - {:noreply, socket} - end + # Event handlers for form validation and submission go here end -|> KinoLiveViewNative.register("/", ":index") - -import KinoLiveViewNative.Livebook, only: [] -import Kernel -:ok ``` diff --git a/livebooks/markdown/getting-started.md b/livebooks/markdown/getting-started.md index 3f5d02d0f..2978be837 100644 --- a/livebooks/markdown/getting-started.md +++ b/livebooks/markdown/getting-started.md @@ -6,9 +6,9 @@ Our livebook guides provide step-by-step lessons to help you learn LiveView Native using Livebook. These guides assume that you already have some familiarity with Phoenix LiveView applications. -You can read these guides online, or for the best experience we recommend you click on the "Run in Livebook" badge to import and run these guides locally with Livebook. +You can read these guides online on HexDocs, but for the best experience, we recommend clicking on the "Run in Livebook" badge to import and run the guide locally with Livebook. -Each guide can be completed independently, but we suggest following them chronologically for the most comprehensive learning experience. +You may complete guides individually, but we suggest following them chronologically for the most comprehensive learning experience. ## Prerequisites @@ -26,7 +26,7 @@ While not necessary for our guides, we also recommend you install the following ## Hello World -If you are not already running this guide in Livebook, click on the "Run in Livebook" badge at the top of this page to import this guide into Livebook. +If you are not already running this guide in Livebook, click on the "Run in Livebook" badge at the top of this page to import it. Then, you can evaluate the following smart cell and visit http://localhost:4000 to ensure this Livebook works correctly. @@ -56,19 +56,17 @@ defmodule ServerWeb.ExampleLive do end ``` -In an upcoming lesson, you'll set up an iOS application with Xcode so you can run code native examples. +In an upcoming lesson, you'll set up an iOS application with Xcode to run native code examples. ## Your Turn: Live Reloading -Change `Hello from LiveView!` to `Hello again from LiveView!` in the above LiveView. Re-evaluate the cell and notice the application live reloads and automatically updates in the browser. +In the above LiveView, change `Hello from LiveView!` to `Hello again from LiveView!`. After making the change, reevaluate the cell. Notice that the application live reloads and automatically updates in the browser. ## Kino LiveView Native -To run a Phoenix Server setup with LiveView Native from within Livebook we built the [Kino LiveView Native](https://github.com/liveview-native/kino_live_view_native) library. +To run a Phoenix + LiveView Native application from within Livebook we built the [Kino LiveView Native](https://github.com/liveview-native/kino_live_view_native) library. -Whenever you run one of our Livebooks, a server starts on localhost:4000. Ensure you have no other servers running on port 4000 - -Kino LiveView Native defines the **LiveView Native: LiveView** and **LiveViewNative: Render Component** smart cells within these guides. +Whenever you run one of our Livebooks, a server starts on localhost:4000. Ensure no other servers are running on port 4000, or you may experience issues. ## Troubleshooting diff --git a/livebooks/markdown/interactive-swiftui-views.md b/livebooks/markdown/interactive-swiftui-views.md index fea035dbd..361388cf8 100644 --- a/livebooks/markdown/interactive-swiftui-views.md +++ b/livebooks/markdown/interactive-swiftui-views.md @@ -10,7 +10,7 @@ This guide assumes some existing familiarity with [Phoenix Bindings](https://hex We'll use the following LiveView and define new render component examples throughout the guide. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -56,13 +56,13 @@ In the example below, the client sends a `"ping"` event to the server, and trigg Evaluate the example below, then click the `"Click me!"` button. Notice `"Pong"` printed in the server logs below. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" """ @@ -90,13 +90,13 @@ Event handlers in LiveView can update the LiveView's state in the socket. Evaluate the cell below to see an example of incrementing a count. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" """ @@ -134,7 +134,7 @@ There should be two buttons, each with a `phx-click` binding. One button should defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" <%= @count %> @@ -175,13 +175,13 @@ end ### Enter Your Solution Below - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assign) do ~LVN""" <%= @count %> @@ -211,13 +211,13 @@ end Pressing a child item in the `List` on a native device triggers the `phx-change` event. In the example below we've bound the `phx-change` event to send the `"selection-changed"` event. This event is then handled by the `handle_event/3` callback function and used to change the selected item. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" Item <%= i %> @@ -251,13 +251,13 @@ end To control a `DisclosureGroup` view, use the `is-expanded` boolean attribute as seen in the example below. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" @@ -294,13 +294,13 @@ end The next example shows one pattern for displaying multiple expandable lists without needing to write multiple event handlers. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" @@ -344,6 +344,8 @@ end ## Controls and Indicators +In Phoenix, the `phx-change` event + In Phoenix, the `phx-change` event must be applied to a parent form. However in SwiftUI there is no similar concept of forms. Instead, SwiftUI provides [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) views. We can apply the `phx-change` binding to any of these views. Once bound, the SwiftUI view will send a message to the LiveView anytime the control or indicator changes its value. @@ -364,15 +366,17 @@ The following example shows you how to connect a SwiftUI [TextField](https://dev Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the server logs in the console below as a map of `%{"text" => value}`. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" - Enter text here + + Enter text here + """ end end @@ -398,13 +402,13 @@ The following example demonstrates how to set/access a TextField's value by cont This pattern is useful when rendering the TextField's value elsewhere on the page, using the `TextField` view's value in other event handler logic, or to set an initial value. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" Enter text here @@ -449,13 +453,13 @@ This code example renders a SwiftUI [Slider](https://developer.apple.com/documen Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the console below as a map of `%{"value" => value}`. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" On/Off """ @@ -573,13 +577,13 @@ end The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" """ @@ -622,13 +626,14 @@ The `DatePicker` view accepts a `displayed-components` attribute with the value You're going to change the `displayed-components` attribute in the example below to see both of these options. Change `"all"` to `"date"`, then to `"hour-and-minute"`. Re-evaluate the cell between changes and see the updated UI. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + @impl true + def render(assigns) do ~LVN""" """ @@ -642,6 +647,7 @@ defmodule ServerWeb.ExampleLive do @impl true def render(assigns), do: ~H"" + @impl true def handle_event("pick-date", params, socket) do {:noreply, socket} end @@ -666,7 +672,7 @@ Using the previous examples as inspiration, you're going to create a todo list. defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assigns) do ~LVN""" Todo... @@ -727,13 +733,13 @@ end ### Enter Your Solution Below - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assigns, _interface) do + def render(assig) do ~LVN""" """ diff --git a/livebooks/markdown/swiftui-views.md b/livebooks/markdown/swiftui-views.md index 421c5116a..7d31b3807 100644 --- a/livebooks/markdown/swiftui-views.md +++ b/livebooks/markdown/swiftui-views.md @@ -4,19 +4,19 @@ ## Overview -LiveView Native aims to use minimal SwiftUI code. All patterns for building interactive UIs are the same as LiveView. However, unlike LiveView for the web, LiveView Native uses SwiftUI templates to build the native UI. +LiveView Native aims to use minimal SwiftUI code and instead rely on the same patterns used in traditional Phoenix LiveView development as much as possible. We'll primarily use The LiveView Naive SwiftUI DSL (Domain Specific Language) to build the native template. -This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common uses of each view and give you practical examples you can use to build your own native UIs. This lesson is like a recipe book you can refer back to whenever you need an example of how to use a particular SwiftUI view. In addition, once you understand how to convert these views into the LiveView Native DSL, you should have the tools to convert essentially any SwiftUI View into the LiveView Native DSL. +This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common use cases and provide practical examples of how to build native UIs. This lesson is like a recipe book you can refer to whenever you need an example of a particular SwiftUI view. + +In addition, we'll cover the LiveView Native DSL and teach you how to convert SwiftUI examples into the LiveView Native DSL. Once you understand how to convert SwiftUI code into the LiveView Native DSL, you'll have the knowledge you need to learn from the plethora of [SwiftUI resources available](https://developer.apple.com/tutorials/swiftui/creating-and-combining-views). ## Render Components -LiveView Native `0.3.0` introduced render components to better encourage isolation of native and web templates and move away from co-location templates within the same LiveView module. +LiveView Native `0.3.0` introduced render components to encourage isolation of native and web templates. This pattern generally scales better than co-located templates within the same LiveView module. Render components are namespaced under the main LiveView, and are responsible for defining the `render/1` callback function that returns the native template. -For example, and `ExampleLive` LiveView module would have an `ExampleLive.SwiftUI` render component module for the native Template. - -This `ExampleLive.SwiftUI` render component may define a `render/1` callback function as seen below. +For example, in the cell below, the `ExampleLive` LiveView module has a corresponding `ExampleLive.SwiftUI` render component module for the native template. This `ExampleLive.SwiftUI` render component may define a `render/1` callback function, as seen below. @@ -46,7 +46,7 @@ defmodule ServerWeb.ExampleLive do end ``` -Throughout this and further material we'll re-define render components you can evaluate and see reflected in your Xcode iOS simulator. +Throughout this and further material, we'll re-define render components you can evaluate and see reflected in your Xcode iOS simulator. @@ -62,17 +62,21 @@ defmodule ServerWeb.ExampleLive.SwiftUI do end ``` +In a Phoenix application, these two modules would traditionally be in separate files. + + + ### Embedding Templates -Alternatively, you may omit the render callback and instead define a `.neex` (Native + Embedded Elixir) template. +Instead of defining a `render/1` callback function, you may instead define a `.neex` (Native + Embedded Elixir) template. By default, the module above would look for a template in the `swiftui/example_live*` path relative to the module's location. You can see the `LiveViewNative.Component` documentation for further explanation. -For the sake of ease when working in Livebook, we'll prefer defining the `render/1` callback. However, we recommend you generally prefer template files when working locally in Phoenix LiveView Native projects. +In Livebook, we'll use the `render/1` callback. However, we recommend using template files for local Phoenix + LiveView Native applications. ## SwiftUI Views -In SwiftUI, a "View" is like a building block for what you see on your app's screen. It can be something simple like text or an image, or something more complex like a layout with multiple elements. Views are the pieces that make up your app's user interface. +In SwiftUI, a "View" is like a building block for what you see on your app's screen. It can be something simple like text, or something more complex like a layout with multiple elements. Here's an example `Text` view that represents a text element. @@ -90,11 +94,11 @@ LiveView Native uses the following syntax to represent the view above. SwiftUI provides a wide range of Views that can be used in native templates. You can find a full reference of these views in the SwiftUI Documentation at https://developer.apple.com/documentation/swiftui/. You can also find a shorthand on how to convert SwiftUI syntax into the LiveView Native DLS in the [LiveView Native Syntax Conversion Cheatsheet](https://hexdocs.pm/live_view_native/cheatsheet.cheatmd). -## Text +We're going to cover a non-exhaustive list of the most common views and show examples of how to use them in your LiveView Native application. -We've already seen the [Text](https://developer.apple.com/documentation/swiftui/text) view, but we'll start simple to get the interactive tutorial running. +## Text -Evaluate the cell below, then in Xcode, Start the iOS application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native/create-a-swiftui-application.html) lesson and ensure you see the `"Hello, from LiveView Native!"` text. +Let's get the interactive tutorial running. Evaluate the cell below, then in Xcode, Start the iOS application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native/create-a-swiftui-application.html) lesson and ensure you see the `"Hello, from LiveView Native!"` text. @@ -119,7 +123,7 @@ SwiftUI includes many [Layout](https://developer.apple.com/documentation/swiftui Below, we've created a simple 3X3 game board to demonstrate how to use `VStack` and `HStack` to build a layout of horizontal rows in a single vertical column.o -Here's a diagram to demonstrate how these rows and columns create our desired layout. +Here's a diagram demonstrating how these rows and columns create our desired layout. ```mermaid flowchart @@ -176,7 +180,7 @@ end ### Your Turn: 3x3 board using columns -In the cell below, use `VStack` and `HStack` to create a 3X3 board using 3 columns instead of 3 rows as demonstrated above. The arrangement of `X` and `O` does not matter, however the content will not be properly aligned if you do not have exactly one character in each `Text` element. +In the cell below, use `VStack` and `HStack` to create a 3X3 board using 3 **columns** instead of 3 rows. The arrangement of `X` and `O` does not matter. However, the UI will not align if you don't have exactly one character in each `Text` element. ```mermaid flowchart @@ -287,7 +291,7 @@ Fortunately, we have a few common elements for creating a grid-based layout. * [Grid](https://developer.apple.com/documentation/swiftui/grid): A grid that arranges its child views in rows and columns that you specify. * [GridRow](https://developer.apple.com/documentation/swiftui/gridrow): A view that arranges its children in a horizontal line. -A grid layout vertically and horizontally aligns elements in the grid based on the number of elements in each row. +A grid layout aligns elements in the grid vertically and horizontally based on the number of elements in each row. Evaluate the example below and notice that rows and columns are aligned. @@ -323,7 +327,7 @@ end ## List -The SwiftUI [List](https://developer.apple.com/documentation/swiftui/list) view provides a system-specific interface, and has better performance for large amounts of scrolling elements. +The SwiftUI [List](https://developer.apple.com/documentation/swiftui/list) view provides a system-specific interface and performs well with large numbers of scrolling elements (up to certain limits). @@ -373,7 +377,7 @@ end The SwiftUI [ScrollView](https://developer.apple.com/documentation/swiftui/scrollview) displays content within a scrollable region. ScrollView is often used in combination with [LazyHStack](https://developer.apple.com/documentation/swiftui/lazyvstack), [LazyVStack](https://developer.apple.com/documentation/swiftui/lazyhstack), [LazyHGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid), and [LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid) to create scrollable layouts optimized for displaying large amounts of data. -While `ScrollView` also works with typical `VStack` and `HStack` views, they are not optimal choices for large amounts of data. +While `ScrollView` also works with typical `VStack` and `HStack` views, they are not optimal for large amounts of data. @@ -446,9 +450,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do end ``` -To resolve the performance problem for large amounts of data, you can use the Lazy views. Lazy views only create items as needed. Items won't be rendered until they are present on the screen. +You can use the Lazy views to resolve the performance problem for large amounts of data. Lazy views only create items as needed, meaning the client won't render them until they are on the screen. -The next example demonstrates how using `LazyVStack` instead of `VStack` resolves the performance issue. +The following example demonstrates how using `LazyVStack` instead of `VStack` resolves the performance issue. Evaluate the cell and notice the improved performance in your simulator. @@ -541,7 +545,7 @@ end ## AsyncImage -`AsyncImage` is best for network images, or images served by the Phoenix server. +`AsyncImage` is best for network images or images the Phoenix server serves. Here's an example of `AsyncImage` with a lorem picsum image from https://picsum.photos/400/600. @@ -633,7 +637,7 @@ end ### Your Turn: Asset Catalogue -You can place assets in your SwiftUI application's asset catalogue. Using the asset catalogue for SwiftUI assets provide many benefits such as device-specific image variants, dark mode images, high contrast image mode, and improved performance. +You can place assets in your SwiftUI application's asset catalog. Using the asset catalogue for SwiftUI assets provide many benefits such as device-specific image variants, dark mode images, high contrast image mode, and improved performance. Follow this guide: https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs#Add-a-new-asset to create a new asset called Image. @@ -641,13 +645,13 @@ Then evaluate the following example and you should see this image in your simula ![LiveView Native Logo](https://github.com/liveview-native/documentation_assets/blob/main/logo.png?raw=true) -You will need to **rebuild the native application** to pick up the changes to the assets catalogue. +Make sure to **rebuild the native application** to build the app with the updated asset catalog. ### Enter Your Solution Below -You should not need to make changes to this cell. Set up an image in your asset catalogue named "Image", rebuild your native application, then evaluate this cell. You should see the image in your iOS simulator. +Do not make changes to the cell below. Set up an image in your asset catalogue named "Image", rebuild your native application, then evaluate this cell. You should see the image in your iOS simulator. @@ -668,7 +672,7 @@ end A Button is a clickable SwiftUI View. -The label of a button can be any view, such as a [Text](https://developer.apple.com/documentation/swiftui/text) view for text-only buttons or a [Label](https://developer.apple.com/documentation/swiftui/label) view for buttons with icons. +A button's label can be any view, such as a [Text](https://developer.apple.com/documentation/swiftui/text) view for text-only buttons or a [Label](https://developer.apple.com/documentation/swiftui/label) view for buttons with icons. Evaluate the example below to see the SwiftUI [Button](https://developer.apple.com/documentation/swiftui/button) element. diff --git a/mix.exs b/mix.exs index 10827b868..8d1b14151 100644 --- a/mix.exs +++ b/mix.exs @@ -1,10 +1,7 @@ defmodule LiveViewNative.SwiftUI.MixProject do use Mix.Project - @version "0.3.0-rc.1" @source_url "https://github.com/liveview-native/liveview-client-swiftui" - @livebooks_enabled System.get_env("LIVEBOOKS_ENABLED") - @gen_docs_enabled System.get_env("GEN_DOCS_ENABLED") def project do [ @@ -35,7 +32,7 @@ defmodule LiveViewNative.SwiftUI.MixProject do defp aliases do [ - docs: @gen_docs_enabled && ["lvn.swiftui.gen.docs"] || [] ++ ["livebooks_to_markdown", "docs"] + docs: &various_docs/1 ] end @@ -44,7 +41,7 @@ defmodule LiveViewNative.SwiftUI.MixProject do defp elixirc_paths(_), do: ignore_docs_task(["lib"]) defp ignore_docs_task(paths) do - Enum.flat_map(paths, fn(path) -> + Enum.flat_map(paths, fn path -> Path.wildcard("#{path}/**/*.ex") end) |> Enum.filter(&(!(&1 =~ "lvn.swiftui.gen.docs"))) @@ -59,7 +56,8 @@ defmodule LiveViewNative.SwiftUI.MixProject do {:floki, ">= 0.30.0", only: :test}, {:live_view_native, "~> 0.3.0-rc.1"}, {:live_view_native_stylesheet, "~> 0.3.0-rc.1", only: :test}, - {:live_view_native_test, github: "liveview-native/live_view_native_test", tag: "v0.3.0", only: :test}, + {:live_view_native_test, + github: "liveview-native/live_view_native_test", tag: "v0.3.0", only: :test}, {:nimble_parsec, "~> 1.3"} ] end @@ -110,32 +108,29 @@ defmodule LiveViewNative.SwiftUI.MixProject do guides = Path.wildcard("guides/**/*.md") generated_docs = Path.wildcard("generated_docs/**/*.{md,cheatmd}") - livebooks = if @livebooks_enabled do - [ - "livebooks/markdown/getting-started.md", - "livebooks/markdown/create-a-swiftui-application.md", - "livebooks/markdown/swiftui-views.md", - "livebooks/markdown/interactive-swiftui-views.md", - "livebooks/markdown/stylesheets.md", - "livebooks/markdown/native-navigation.md", - "livebooks/markdown/forms-and-validation.md" - ] - else - [] - end + livebooks = + [ + "livebooks/markdown/getting-started.md", + "livebooks/markdown/create-a-swiftui-application.md", + "livebooks/markdown/swiftui-views.md", + "livebooks/markdown/interactive-swiftui-views.md", + "livebooks/markdown/stylesheets.md", + "livebooks/markdown/native-navigation.md", + "livebooks/markdown/forms-and-validation.md" + ] ["README.md"] ++ guides ++ generated_docs ++ livebooks end defp groups_for_extras do guide_groups = [ - "Architecture": Path.wildcard("guides/architecture/*.md"), - "Livebooks": ~r/markdown_livebooks/ + Architecture: Path.wildcard("guides/architecture/*.md"), + Livebooks: ~r/markdown_livebooks/ ] generated_groups = Path.wildcard("generated_docs/*") - |> Enum.map(&({Path.basename(&1) |> String.to_atom(), Path.wildcard("#{&1}/*.md")})) + |> Enum.map(&{Path.basename(&1) |> String.to_atom(), Path.wildcard("#{&1}/*.md")}) guide_groups ++ generated_groups end @@ -151,4 +146,15 @@ defmodule LiveViewNative.SwiftUI.MixProject do } } end + + defp various_docs(args) do + {opts, _, _} = + OptionParser.parse(args, + strict: [skip_gen_doc: :boolean, skip_livebooks: :boolean] + ) + + unless opts[:skip_gen_doc], do: Mix.Task.run("lvn.swiftui.gen.docs") + unless opts[:skip_livebooks], do: Mix.Task.run("lvn.swiftui.gen.livemarkdown") + Mix.Task.run("docs") + end end From 74be999bb02e7646db7c0879c9c0d4633b77b1c8 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Tue, 7 May 2024 13:39:42 -0400 Subject: [PATCH 09/62] Move form package setup to the create a swiftui application section --- livebooks/create-a-swiftui-application.livemd | 14 +++++++++++++- livebooks/forms-and-validation.livemd | 8 -------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/livebooks/create-a-swiftui-application.livemd b/livebooks/create-a-swiftui-application.livemd index 59db8ab42..63769f43c 100644 --- a/livebooks/create-a-swiftui-application.livemd +++ b/livebooks/create-a-swiftui-application.livemd @@ -152,7 +152,9 @@ You should see the default iOS application generated by Xcode. ## Add the LiveView Client SwiftUI Package -In Xcode from the project you just created, select **File -> Add Package Dependencies**. Then, search for `liveview-client-swiftui`. Once you have chosen the package, click **Add Package**. +The [LiveView Client SwiftUI Package](https://github.com/liveview-native/liveview-client-swiftui) allows your SwiftUI client application to connect to a Phoenix LiveView server. + +To install the package, from Xcode from the project you just created, select **File -> Add Package Dependencies**. Then, search for `liveview-client-swiftui`. Once you have chosen the package, click **Add Package**. The image below displays `0.2.0`. You should select the latest version of LiveView Native. @@ -194,6 +196,16 @@ The specific plugins are subject to change. At the time of writing you need to e ![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-case-path-macros.png?raw=true) +## Add the LiveView Live Form Package + +The [LiveView Native Live Form](https://github.com/liveview-native/liveview-native-live-form) provides forms for LiveView Native SwiftUI. + +To install LiveView Native Form, we need to add the [liveview-native-live-form](https://github.com/liveview-native/liveview-native-live-form) SwiftUI package to our iOS application. The steps will be mostly similar to what you have already setup with the `liveview-client-swiftui` package. + +Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README to add the `liveview-native-live-form` package to the SwiftUI application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native_swiftui/create-a-swiftui-application.html) guide. + +Come back to this guide and continue after you have finished the installation process. + ## Setup the SwiftUI LiveView The [ContentView](https://developer.apple.com/tutorials/swiftui-concepts/exploring-the-structure-of-a-swiftui-app#Content-view) contains the main view of our iOS application. diff --git a/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd index 5eb8d972a..46b2c3c7e 100644 --- a/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -76,14 +76,6 @@ The [LiveView Native Live Form](https://github.com/liveview-native/liveview-nati Getting the most out of this material requires some understanding of the [Ecto](https://hexdocs.pm/ecto/Ecto.html) project and in particular a reasonably deep understanding of [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html). Review the Ecto documentation if you find any of the examples difficult to follow. -## Installing LiveView Native Live Form - -To install LiveView Native Form, we need to add the `liveview-native-live-form` SwiftUI package to our iOS application. - -Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README to add the `liveview-native-live-form` package to the SwiftUI application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native_swiftui/create-a-swiftui-application.html) guide. - -Come back to this guide and continue after you have finished the installation process. - ## Creating a Basic Form The LiveView Native `mix lvn.install` task generates a [core_components.swiftui.ex](https://github.com/liveview-native/liveview-client-swiftui/blob/main/priv/templates/lvn.swiftui.gen/core_components.ex) for native SwiftUI function components similar to the [core_components.ex](https://github.com/phoenixframework/phoenix/blob/main/priv/templates/phx.gen.live/core_components.ex) file generated in a traditional phoenix application for web function components. From 898b7620a514a661184e24e62b2b9b229c095eed Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Wed, 8 May 2024 14:58:30 -0400 Subject: [PATCH 10/62] Fix examples in the interactive swiftui views reading to use LiveForm --- livebooks/interactive-swiftui-views.livemd | 241 ++++++++++----------- 1 file changed, 118 insertions(+), 123 deletions(-) diff --git a/livebooks/interactive-swiftui-views.livemd b/livebooks/interactive-swiftui-views.livemd index c181b9b33..e1b5bd52a 100644 --- a/livebooks/interactive-swiftui-views.livemd +++ b/livebooks/interactive-swiftui-views.livemd @@ -258,7 +258,7 @@ end ### Enter Your Solution Below - + ```elixir require Server.Livebook @@ -268,7 +268,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assign) do + def render(assigns) do ~LVN""" <%= @count %> @@ -350,9 +350,9 @@ import Kernel `List` views support hierarchical content using the [DisclosureGroup](https://developer.apple.com/documentation/swiftui/disclosuregroup) view. Nest `DisclosureGroup` views within a list to create multiple levels of content as seen in the example below. -To control a `DisclosureGroup` view, use the `is-expanded` boolean attribute as seen in the example below. +To control a `DisclosureGroup` view, use the `isExpanded` boolean attribute as seen in the example below. - + ```elixir require Server.Livebook @@ -365,7 +365,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + Level 1 Item 1 Item 2 @@ -389,8 +389,8 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("toggle", %{"is-expanded" => is_expanded}, socket) do - {:noreply, assign(socket, is_expanded: !is_expanded)} + def handle_event("toggle", %{"isExpanded" => is_expanded}, socket) do + {:noreply, assign(socket, is_expanded: is_expanded)} end end |> Server.SmartCells.LiveViewNative.register("/") @@ -404,7 +404,7 @@ import Kernel The next example shows one pattern for displaying multiple expandable lists without needing to write multiple event handlers. - + ```elixir require Server.Livebook @@ -417,10 +417,10 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + Level 1 Item 1 - + Level 2 Item 2 @@ -443,14 +443,14 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("toggle-" <> level, %{"is-expanded" => is_expanded}, socket) do + def handle_event("toggle-" <> level, %{"isExpanded" => is_expanded}, socket) do level = String.to_integer(level) {:noreply, assign( socket, :expanded_groups, - Map.replace!(socket.assigns.expanded_groups, level, !is_expanded) + Map.replace!(socket.assigns.expanded_groups, level, is_expanded) )} end end @@ -461,31 +461,36 @@ import Kernel :ok ``` -## Controls and Indicators +## Forms -In Phoenix, the `phx-change` event +In Phoenix, form elements must be inside of a form. Phoenix only captures events if the element is wrapped in a form. However in SwiftUI there is no similar concept of forms. To bridge the gap, we built the [LiveView Native Live Form](https://github.com/liveview-native/liveview-native-live-form) library. This library provides several views to enable writing views in a single form. -In Phoenix, the `phx-change` event must be applied to a parent form. However in SwiftUI there is no similar concept of forms. Instead, SwiftUI provides [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) views. We can apply the `phx-change` binding to any of these views. +Phoenix Applications setup with LiveView native include a `core_components.ex` file. This file contains several components for building forms. Generally, We recommend using core components rather than the views. We're going to cover the views directly so you understand how to build forms from scratch and how we built the core components. However, in the [Forms and Validations](https://hexdocs.pm/live_view_native/forms-and-validation.html) reading we'll cover using core components. -Once bound, the SwiftUI view will send a message to the LiveView anytime the control or indicator changes its value. + -The params of the message are based on the name of the [Binding](https://developer.apple.com/documentation/swiftui/binding) argument of the view's initializer in SwiftUI. +### LiveForm - +The `LiveForm` view must wrap views to capture events from the `phx-change` or `phx-submit` event. The `phx-change` event sends a message to the LiveView anytime the control or indicator changes its value. The `phx-submit` event sends a message to the LiveView when a user clicks the `LiveSubmitButton`. The params of the message are based on the name of the [Binding](https://developer.apple.com/documentation/swiftui/binding) argument of the view's initializer in SwiftUI. -### Event Value Bindings +Here's some example boilerplate for a `LiveForm`. The `id` attribute is required. -Many views use the `value` binding argument, so event params are generally sent as `%{"value" => value}`. However, certain views such as `TextField` and `Toggle` deviate from this pattern because SwiftUI uses a different `value` binding argument. For example, the `TextField` view uses `text` to bind its value, so it sends the event params as `%{"text" => value}`. +```html + + + Button Text + +``` -When in doubt, you can connect the event handler and inspect the params to confirm the shape of map. + -## Text Field +### Basic Example using TextField -The following example shows you how to connect a SwiftUI [TextField](https://developer.apple.com/documentation/swiftui/textfield) with a `phx-change` event binding to a corresponding event handler. +The following example shows you how to connect a SwiftUI [TextField](https://developer.apple.com/documentation/swiftui/textfield) with a `phx-change` event and `phx-submit` binding to a corresponding event handler. -Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the server logs in the console below as a map of `%{"text" => value}`. +Evaluate the example below. Type into the text field and press submit on your iOS simulator. Notice the inspected `params` appear in the server logs in the console below as a map of `%{"my-input" => value}` based on the `name` attribute on the `TextField` view. - + ```elixir require Server.Livebook @@ -497,8 +502,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - - Enter text here + + Enter text here + Submit """ end @@ -507,13 +513,20 @@ end defmodule ServerWeb.ExampleLive do use ServerWeb, :live_view use ServerNative, :live_view + require Logger @impl true def render(assigns), do: ~H"" @impl true - def handle_event("type", params, socket) do - IO.inspect(params, label: "params") + def handle_event("change", params, socket) do + Logger.info("Change params: #{inspect(params)}") + {:noreply, socket} + end + + @impl true + def handle_event("submit", params, socket) do + Logger.info("Submitted params: #{inspect(params)}") {:noreply, socket} end end @@ -524,73 +537,38 @@ import Kernel :ok ``` -### Storing TextField Values in the Socket +### Event Handlers -The following example demonstrates how to set/access a TextField's value by controlling it using the socket assigns. +The `phx-change` and `phx-submit` event handlers should generally be bound to the LiveForm. However, you can also bind the event handlers directly to the input view if you want to separately handle a single view's change events. -This pattern is useful when rendering the TextField's value elsewhere on the page, using the `TextField` view's value in other event handler logic, or to set an initial value. - - + ```elixir -require Server.Livebook -import Server.Livebook -import Kernel, except: [defmodule: 2] - -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - Enter text here - - The current value: <%= @text %> - """ - end -end - -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view + + Enter text here + Submit + +``` - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :text, "initial value")} - end +## Controls and Indicators - @impl true - def render(assigns), do: ~H"" +SwiftUI organizes interactive views in the [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) section. You may refer to this documentation when looking for views that belong within a form. - @impl true - def handle_event("type", %{"text" => text}, socket) do - {:noreply, assign(socket, :text, text)} - end +We'll demonstrate how to work with a few common control and indicator views. - @impl true - def handle_event("pretty-print", _params, socket) do - IO.puts(""" - ================== - #{socket.assigns.text} - ================== - """) + - {:noreply, socket} - end -end -|> Server.SmartCells.LiveViewNative.register("/") +### Slider -import Server.Livebook, only: [] -import Kernel -:ok -``` +This code example renders a SwiftUI [Slider](https://developer.apple.com/documentation/swiftui/slider). It triggers the change event when the slider is moved and sends a `"slide"` message. The `"slide"` event handler then logs the value to the console. -## Slider +The [Slider](https://developer.apple.com/documentation/swiftui/slider) view uses **named content areas** `minumumValueLabel` and `maximumValueLabel`. The example below demonstrates how to represent these areas using the `template` attribute. -This code example renders a SwiftUI [Slider](https://developer.apple.com/documentation/swiftui/slider). It triggers the change event when the slider is moved and sends a `"slide"` message. The `"slide"` event handler then logs the value to the console. +This example also demonstrates how to use the params sent by the slider to store a value in the socket and use it elsewhere in the template. -Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the console below as a map of `%{"value" => value}`. +Evaluate the example and enter some text in your iOS simulator. - + ```elixir require Server.Livebook @@ -602,16 +580,18 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - - Percent Completed - 0% - 100% - + + + 0% + 100% + + + <%= @percentage %> """ end end @@ -620,13 +600,17 @@ defmodule ServerWeb.ExampleLive do use ServerWeb, :live_view use ServerNative, :live_view + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :percentage, 0)} + end + @impl true def render(assigns), do: ~H"" @impl true - def handle_event("slide", params, socket) do - IO.inspect(params, label: "Slide Params") - {:noreply, socket} + def handle_event("slide", %{"my-slider" => value}, socket) do + {:noreply, assign(socket, :percentage, value)} end end |> Server.SmartCells.LiveViewNative.register("/") @@ -636,13 +620,13 @@ import Kernel :ok ``` -## Stepper +### Stepper This code example renders a SwiftUI [Stepper](https://developer.apple.com/documentation/swiftui/stepper). It triggers the change event and sends a `"change-tickets"` message when the stepper increments or decrements. The `"change-tickets"` event handler then updates the number of tickets stored in state, which appears in the UI. Evaluate the example and increment/decrement the step. - + ```elixir require Server.Livebook @@ -654,14 +638,17 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - - Tickets <%= @tickets %> - + + + Tickets <%= @tickets %> + + """ end end @@ -679,7 +666,7 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("change-tickets", %{"value" => tickets}, socket) do + def handle_event("change-tickets", %{"my-stepper" => tickets}, socket) do {:noreply, assign(socket, :tickets, tickets)} end end @@ -690,13 +677,13 @@ import Kernel :ok ``` -## Toggle +### Toggle -This code example renders a SwiftUI [Toggle](https://developer.apple.com/documentation/swiftui/toggle). It triggers the change event and sends a `"toggle"` message when toggled. The `"toggle"` event handler then updates the `:on` field in state, which allows the `Toggle` view to be toggled on. Without providing the `is-on` attribute, the `Toggle` view could not be flipped on and off. +This code example renders a SwiftUI [Toggle](https://developer.apple.com/documentation/swiftui/toggle). It triggers the change event and sends a `"toggle"` message when toggled. The `"toggle"` event handler then updates the `:on` field in state, which allows the `Toggle` view to be toggled o through the `isOn` attribute. Evaluate the example below and click on the toggle. - + ```elixir require Server.Livebook @@ -708,7 +695,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - On/Off + + On/Off + """ end end @@ -726,7 +715,7 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("toggle", %{"is-on" => on}, socket) do + def handle_event("toggle", %{"my-toggle" => on}, socket) do {:noreply, assign(socket, :on, on)} end end @@ -737,11 +726,11 @@ import Kernel :ok ``` -## DatePicker +### DatePicker -The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. +The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. Evaluate the example below and select a date to see the date params appear in the console below. - + ```elixir require Server.Livebook @@ -753,7 +742,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + + + """ end end @@ -761,6 +752,7 @@ end defmodule ServerWeb.ExampleLive do use ServerWeb, :live_view use ServerNative, :live_view + require Logger @impl true def mount(_params, _session, socket) do @@ -772,7 +764,7 @@ defmodule ServerWeb.ExampleLive do @impl true def handle_event("pick-date", params, socket) do - IO.inspect(params, label: "Date Params") + Logger.info("Date Params: #{inspect(params)}") {:noreply, socket} end end @@ -795,11 +787,11 @@ DateTime.from_iso8601(iso8601) ### Your Turn: Displayed Components -The `DatePicker` view accepts a `displayed-components` attribute with the value of `"hour-and-minute"` or `"date"` to only display one of the two components. By default, the value is `"all"`. +The `DatePicker` view accepts a `displayedComponents` attribute with the value of `"hourAndMinute"` or `"date"` to only display one of the two components. By default, the value is `"all"`. -You're going to change the `displayed-components` attribute in the example below to see both of these options. Change `"all"` to `"date"`, then to `"hour-and-minute"`. Re-evaluate the cell between changes and see the updated UI. +You're going to change the `displayedComponents` attribute in the example below to see both of these options. Change `"all"` to `"date"`, then to `"hourAndMinute"`. Re-evaluate the cell between changes and see the updated UI. - + ```elixir require Server.Livebook @@ -809,10 +801,11 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - @impl true def render(assigns) do ~LVN""" - + + + """ end end @@ -825,7 +818,7 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("pick-date", params, socket) do + def handle_event("pick-date", _params, socket) do {:noreply, socket} end end @@ -857,7 +850,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Todo... + + Todo... + From 4232b5faee8a39a62fd03bd68eb7e06c9c2f2c57 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Wed, 8 May 2024 15:10:16 -0400 Subject: [PATCH 11/62] remove unused check_errors value in the socket for forms and validations reading --- livebooks/forms-and-validation.livemd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd index 46b2c3c7e..4953e4a83 100644 --- a/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -244,7 +244,7 @@ Here's an example of how we can use Ecto changesets with the LiveView Native Liv Evaluate the cell below and open your iOS application. Submit the form with an invalid email. You should notice a `has invalid format` error appear. - + ```elixir require Server.Livebook @@ -275,7 +275,7 @@ defmodule ServerWeb.ExampleLive do @impl true def mount(_params, _session, socket) do changeset = User.changeset(%User{}, %{}) - {:ok, assign(socket, form: to_form(changeset), check_errors: false)} + {:ok, assign(socket, form: to_form(changeset))} end @impl true From fbaf75fc1d04937fedb693a385e7910019d311d8 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Wed, 15 May 2024 16:00:27 -0400 Subject: [PATCH 12/62] update class -> style --- livebooks/forms-and-validation.livemd | 32 +-- .../markdown/create-a-swiftui-application.md | 14 +- livebooks/markdown/forms-and-validation.md | 41 +-- .../markdown/interactive-swiftui-views.md | 232 ++++++++--------- livebooks/markdown/stylesheets.md | 234 +++++++++--------- livebooks/stylesheets.livemd | 234 +++++++++--------- 6 files changed, 376 insertions(+), 411 deletions(-) diff --git a/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd index 4953e4a83..842720758 100644 --- a/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -5,8 +5,7 @@ notebook_path = __ENV__.file |> String.split("#") |> hd() Mix.install( [ - # {:kino_live_view_native, github: "liveview-native/kino_live_view_native"}, - {:kino_live_view_native, path: "../kino_live_view_native"}, + {:kino_live_view_native, github: "liveview-native/kino_live_view_native"}, {:ecto, "~> 3.11"}, {:phoenix_ecto, "~> 4.5"} ], @@ -325,7 +324,7 @@ The [keyboardType](https://developer.apple.com/documentation/swiftui/view/keyboa Evaluate the example below to see the different keyboards as you focus on each input. If you don't see the keyboard, go to `I/O` -> `Keyboard` -> `Toggle Software Keyboard` to enable the software keyboard in your simulator. - + ```elixir require Server.Livebook @@ -338,9 +337,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" <.simple_form for={@form} id="form"> - <.input field={@form[:number_pad]} type="TextField" class="keyboardType(.numberPad)"/> - <.input field={@form[:email_address]} type="TextField" class="keyboardType(.emailAddress)"/> - <.input field={@form[:phonePad]} type="TextField" class="keyboardType(.phonePad)"/> + <.input field={@form[:number_pad]} type="TextField" style="keyboardType(.numberPad)"/> + <.input field={@form[:email_address]} type="TextField" style="keyboardType(.emailAddress)"/> + <.input field={@form[:phonePad]} type="TextField" style="keyboardType(.phonePad)"/> <:actions> <.button type="submit"> Submit @@ -422,9 +421,9 @@ slot :actions, doc: "the slot for form actions, such as a submit button" def simple_form(assigns) do ~H""" <.form :let={f} for={@for} as={@as} {@rest}> -
+
<%= render_slot(@inner_block, f) %> -
+
<%= render_slot(action, f) %>
@@ -477,20 +476,7 @@ attr :rest, :global, slot :inner_block def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do - assigns - |> assign(field: nil, id: assigns.id || field.id) - |> assign(:errors, Enum.map(field.errors, &translate_error(&1))) - |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end) - |> assign_new(:value, fn -> field.value end) - |> assign( - :rest, - Map.put(assigns.rest, :class, [ - Map.get(assigns.rest, :class, ""), - (if assigns.readonly or Map.get(assigns.rest, :disabled, false), do: "disabled-true", else: ""), - (if assigns.autocomplete == "off", do: "text-input-autocapitalization-never autocorrection-disabled", else: "") - ] |> Enum.join(" ")) - ) - |> input() + # Input Definition end ``` @@ -823,7 +809,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do <.input field={@form[:name]} type="TextField" placeholder="name" /> <.input field={@form[:email]} type="TextField" placeholder="email" /> <.input field={@form[:password]} type="SecureField" placeholder="password" /> - <.input field={@form[:age]} type="TextField" placeholder="age" class="keyboardType(.numberPad)" /> + <.input field={@form[:age]} type="TextField" placeholder="age" style="keyboardType(.numberPad)" /> <.input field={@form[:accepted_terms]} type="Toggle"/> <.input field={@form[:birthdate]} type="DatePicker"/> diff --git a/livebooks/markdown/create-a-swiftui-application.md b/livebooks/markdown/create-a-swiftui-application.md index b27d45f9e..879cd7e20 100644 --- a/livebooks/markdown/create-a-swiftui-application.md +++ b/livebooks/markdown/create-a-swiftui-application.md @@ -88,7 +88,9 @@ You should see the default iOS application generated by Xcode. ## Add the LiveView Client SwiftUI Package -In Xcode from the project you just created, select **File -> Add Package Dependencies**. Then, search for `liveview-client-swiftui`. Once you have chosen the package, click **Add Package**. +The [LiveView Client SwiftUI Package](https://github.com/liveview-native/liveview-client-swiftui) allows your SwiftUI client application to connect to a Phoenix LiveView server. + +To install the package, from Xcode from the project you just created, select **File -> Add Package Dependencies**. Then, search for `liveview-client-swiftui`. Once you have chosen the package, click **Add Package**. The image below displays `0.2.0`. You should select the latest version of LiveView Native. @@ -130,6 +132,16 @@ The specific plugins are subject to change. At the time of writing you need to e ![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-case-path-macros.png?raw=true) +## Add the LiveView Live Form Package + +The [LiveView Native Live Form](https://github.com/liveview-native/liveview-native-live-form) provides forms for LiveView Native SwiftUI. + +To install LiveView Native Form, we need to add the [liveview-native-live-form](https://github.com/liveview-native/liveview-native-live-form) SwiftUI package to our iOS application. The steps will be mostly similar to what you have already setup with the `liveview-client-swiftui` package. + +Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README to add the `liveview-native-live-form` package to the SwiftUI application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native_swiftui/create-a-swiftui-application.html) guide. + +Come back to this guide and continue after you have finished the installation process. + ## Setup the SwiftUI LiveView The [ContentView](https://developer.apple.com/tutorials/swiftui-concepts/exploring-the-structure-of-a-swiftui-app#Content-view) contains the main view of our iOS application. diff --git a/livebooks/markdown/forms-and-validation.md b/livebooks/markdown/forms-and-validation.md index 68fdb4fe4..afb5933c6 100644 --- a/livebooks/markdown/forms-and-validation.md +++ b/livebooks/markdown/forms-and-validation.md @@ -8,14 +8,6 @@ The [LiveView Native Live Form](https://github.com/liveview-native/liveview-nati Getting the most out of this material requires some understanding of the [Ecto](https://hexdocs.pm/ecto/Ecto.html) project and in particular a reasonably deep understanding of [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html). Review the Ecto documentation if you find any of the examples difficult to follow. -## Installing LiveView Native Live Form - -To install LiveView Native Form, we need to add the `liveview-native-live-form` SwiftUI package to our iOS application. - -Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README to add the `liveview-native-live-form` package to the SwiftUI application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native_swiftui/create-a-swiftui-application.html) guide. - -Come back to this guide and continue after you have finished the installation process. - ## Creating a Basic Form The LiveView Native `mix lvn.install` task generates a [core_components.swiftui.ex](https://github.com/liveview-native/liveview-client-swiftui/blob/main/priv/templates/lvn.swiftui.gen/core_components.ex) for native SwiftUI function components similar to the [core_components.ex](https://github.com/phoenixframework/phoenix/blob/main/priv/templates/phx.gen.live/core_components.ex) file generated in a traditional phoenix application for web function components. @@ -175,7 +167,7 @@ Here's an example of how we can use Ecto changesets with the LiveView Native Liv Evaluate the cell below and open your iOS application. Submit the form with an invalid email. You should notice a `has invalid format` error appear. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -202,7 +194,7 @@ defmodule ServerWeb.ExampleLive do @impl true def mount(_params, _session, socket) do changeset = User.changeset(%User{}, %{}) - {:ok, assign(socket, form: to_form(changeset), check_errors: false)} + {:ok, assign(socket, form: to_form(changeset))} end @impl true @@ -247,7 +239,7 @@ The [keyboardType](https://developer.apple.com/documentation/swiftui/view/keyboa Evaluate the example below to see the different keyboards as you focus on each input. If you don't see the keyboard, go to `I/O` -> `Keyboard` -> `Toggle Software Keyboard` to enable the software keyboard in your simulator. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -256,9 +248,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" <.simple_form for={@form} id="form"> - <.input field={@form[:number_pad]} type="TextField" class="keyboardType(.numberPad)"/> - <.input field={@form[:email_address]} type="TextField" class="keyboardType(.emailAddress)"/> - <.input field={@form[:phonePad]} type="TextField" class="keyboardType(.phonePad)"/> + <.input field={@form[:number_pad]} type="TextField" style="keyboardType(.numberPad)"/> + <.input field={@form[:email_address]} type="TextField" style="keyboardType(.emailAddress)"/> + <.input field={@form[:phonePad]} type="TextField" style="keyboardType(.phonePad)"/> <:actions> <.button type="submit"> Submit @@ -335,9 +327,9 @@ slot :actions, doc: "the slot for form actions, such as a submit button" def simple_form(assigns) do ~H""" <.form :let={f} for={@for} as={@as} {@rest}> -
+
<%= render_slot(@inner_block, f) %> -
+
<%= render_slot(action, f) %>
@@ -390,20 +382,7 @@ attr :rest, :global, slot :inner_block def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do - assigns - |> assign(field: nil, id: assigns.id || field.id) - |> assign(:errors, Enum.map(field.errors, &translate_error(&1))) - |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end) - |> assign_new(:value, fn -> field.value end) - |> assign( - :rest, - Map.put(assigns.rest, :class, [ - Map.get(assigns.rest, :class, ""), - (if assigns.readonly or Map.get(assigns.rest, :disabled, false), do: "disabled-true", else: ""), - (if assigns.autocomplete == "off", do: "text-input-autocapitalization-never autocorrection-disabled", else: "") - ] |> Enum.join(" ")) - ) - |> input() + # Input Definition end ``` @@ -715,7 +694,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do <.input field={@form[:name]} type="TextField" placeholder="name" /> <.input field={@form[:email]} type="TextField" placeholder="email" /> <.input field={@form[:password]} type="SecureField" placeholder="password" /> - <.input field={@form[:age]} type="TextField" placeholder="age" class="keyboardType(.numberPad)" /> + <.input field={@form[:age]} type="TextField" placeholder="age" style="keyboardType(.numberPad)" /> <.input field={@form[:accepted_terms]} type="Toggle"/> <.input field={@form[:birthdate]} type="DatePicker"/> diff --git a/livebooks/markdown/interactive-swiftui-views.md b/livebooks/markdown/interactive-swiftui-views.md index 361388cf8..7580dda96 100644 --- a/livebooks/markdown/interactive-swiftui-views.md +++ b/livebooks/markdown/interactive-swiftui-views.md @@ -175,13 +175,13 @@ end ### Enter Your Solution Below - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assign) do + def render(assigns) do ~LVN""" <%= @count %> @@ -249,9 +249,9 @@ end `List` views support hierarchical content using the [DisclosureGroup](https://developer.apple.com/documentation/swiftui/disclosuregroup) view. Nest `DisclosureGroup` views within a list to create multiple levels of content as seen in the example below. -To control a `DisclosureGroup` view, use the `is-expanded` boolean attribute as seen in the example below. +To control a `DisclosureGroup` view, use the `isExpanded` boolean attribute as seen in the example below. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -260,7 +260,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + Level 1 Item 1 Item 2 @@ -284,8 +284,8 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("toggle", %{"is-expanded" => is_expanded}, socket) do - {:noreply, assign(socket, is_expanded: !is_expanded)} + def handle_event("toggle", %{"isExpanded" => is_expanded}, socket) do + {:noreply, assign(socket, is_expanded: is_expanded)} end end ``` @@ -294,7 +294,7 @@ end The next example shows one pattern for displaying multiple expandable lists without needing to write multiple event handlers. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -303,10 +303,10 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + Level 1 Item 1 - + Level 2 Item 2 @@ -329,44 +329,49 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("toggle-" <> level, %{"is-expanded" => is_expanded}, socket) do + def handle_event("toggle-" <> level, %{"isExpanded" => is_expanded}, socket) do level = String.to_integer(level) {:noreply, assign( socket, :expanded_groups, - Map.replace!(socket.assigns.expanded_groups, level, !is_expanded) + Map.replace!(socket.assigns.expanded_groups, level, is_expanded) )} end end ``` -## Controls and Indicators +## Forms -In Phoenix, the `phx-change` event +In Phoenix, form elements must be inside of a form. Phoenix only captures events if the element is wrapped in a form. However in SwiftUI there is no similar concept of forms. To bridge the gap, we built the [LiveView Native Live Form](https://github.com/liveview-native/liveview-native-live-form) library. This library provides several views to enable writing views in a single form. -In Phoenix, the `phx-change` event must be applied to a parent form. However in SwiftUI there is no similar concept of forms. Instead, SwiftUI provides [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) views. We can apply the `phx-change` binding to any of these views. +Phoenix Applications setup with LiveView native include a `core_components.ex` file. This file contains several components for building forms. Generally, We recommend using core components rather than the views. We're going to cover the views directly so you understand how to build forms from scratch and how we built the core components. However, in the [Forms and Validations](https://hexdocs.pm/live_view_native/forms-and-validation.html) reading we'll cover using core components. -Once bound, the SwiftUI view will send a message to the LiveView anytime the control or indicator changes its value. + -The params of the message are based on the name of the [Binding](https://developer.apple.com/documentation/swiftui/binding) argument of the view's initializer in SwiftUI. +### LiveForm - +The `LiveForm` view must wrap views to capture events from the `phx-change` or `phx-submit` event. The `phx-change` event sends a message to the LiveView anytime the control or indicator changes its value. The `phx-submit` event sends a message to the LiveView when a user clicks the `LiveSubmitButton`. The params of the message are based on the name of the [Binding](https://developer.apple.com/documentation/swiftui/binding) argument of the view's initializer in SwiftUI. -### Event Value Bindings +Here's some example boilerplate for a `LiveForm`. The `id` attribute is required. -Many views use the `value` binding argument, so event params are generally sent as `%{"value" => value}`. However, certain views such as `TextField` and `Toggle` deviate from this pattern because SwiftUI uses a different `value` binding argument. For example, the `TextField` view uses `text` to bind its value, so it sends the event params as `%{"text" => value}`. +```html + + + Button Text + +``` -When in doubt, you can connect the event handler and inspect the params to confirm the shape of map. + -## Text Field +### Basic Example using TextField -The following example shows you how to connect a SwiftUI [TextField](https://developer.apple.com/documentation/swiftui/textfield) with a `phx-change` event binding to a corresponding event handler. +The following example shows you how to connect a SwiftUI [TextField](https://developer.apple.com/documentation/swiftui/textfield) with a `phx-change` event and `phx-submit` binding to a corresponding event handler. -Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the server logs in the console below as a map of `%{"text" => value}`. +Evaluate the example below. Type into the text field and press submit on your iOS simulator. Notice the inspected `params` appear in the server logs in the console below as a map of `%{"my-input" => value}` based on the `name` attribute on the `TextField` view. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -374,8 +379,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - - Enter text here + + Enter text here + Submit """ end @@ -384,76 +390,57 @@ end defmodule ServerWeb.ExampleLive do use ServerWeb, :live_view use ServerNative, :live_view + require Logger @impl true def render(assigns), do: ~H"" @impl true - def handle_event("type", params, socket) do - IO.inspect(params, label: "params") + def handle_event("change", params, socket) do + Logger.info("Change params: #{inspect(params)}") + {:noreply, socket} + end + + @impl true + def handle_event("submit", params, socket) do + Logger.info("Submitted params: #{inspect(params)}") {:noreply, socket} end end ``` -### Storing TextField Values in the Socket +### Event Handlers -The following example demonstrates how to set/access a TextField's value by controlling it using the socket assigns. +The `phx-change` and `phx-submit` event handlers should generally be bound to the LiveForm. However, you can also bind the event handlers directly to the input view if you want to separately handle a single view's change events. -This pattern is useful when rendering the TextField's value elsewhere on the page, using the `TextField` view's value in other event handler logic, or to set an initial value. - - + ```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use ServerNative, [:render_component, format: :swiftui] - - def render(assigns) do - ~LVN""" - Enter text here - - The current value: <%= @text %> - """ - end -end + + Enter text here + Submit + +``` -defmodule ServerWeb.ExampleLive do - use ServerWeb, :live_view - use ServerNative, :live_view +## Controls and Indicators - @impl true - def mount(_params, _session, socket) do - {:ok, assign(socket, :text, "initial value")} - end +SwiftUI organizes interactive views in the [Controls and Indicators](https://developer.apple.com/documentation/swiftui/controls-and-indicators) section. You may refer to this documentation when looking for views that belong within a form. - @impl true - def render(assigns), do: ~H"" +We'll demonstrate how to work with a few common control and indicator views. - @impl true - def handle_event("type", %{"text" => text}, socket) do - {:noreply, assign(socket, :text, text)} - end + - @impl true - def handle_event("pretty-print", _params, socket) do - IO.puts(""" - ================== - #{socket.assigns.text} - ================== - """) +### Slider - {:noreply, socket} - end -end -``` +This code example renders a SwiftUI [Slider](https://developer.apple.com/documentation/swiftui/slider). It triggers the change event when the slider is moved and sends a `"slide"` message. The `"slide"` event handler then logs the value to the console. -## Slider +The [Slider](https://developer.apple.com/documentation/swiftui/slider) view uses **named content areas** `minumumValueLabel` and `maximumValueLabel`. The example below demonstrates how to represent these areas using the `template` attribute. -This code example renders a SwiftUI [Slider](https://developer.apple.com/documentation/swiftui/slider). It triggers the change event when the slider is moved and sends a `"slide"` message. The `"slide"` event handler then logs the value to the console. +This example also demonstrates how to use the params sent by the slider to store a value in the socket and use it elsewhere in the template. -Evaluate the example and enter some text in your iOS simulator. Notice the inspected `params` appear in the console below as a map of `%{"value" => value}`. +Evaluate the example and enter some text in your iOS simulator. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -461,16 +448,18 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - - Percent Completed - 0% - 100% - + + + 0% + 100% + + + <%= @percentage %> """ end end @@ -479,24 +468,28 @@ defmodule ServerWeb.ExampleLive do use ServerWeb, :live_view use ServerNative, :live_view + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :percentage, 0)} + end + @impl true def render(assigns), do: ~H"" @impl true - def handle_event("slide", params, socket) do - IO.inspect(params, label: "Slide Params") - {:noreply, socket} + def handle_event("slide", %{"my-slider" => value}, socket) do + {:noreply, assign(socket, :percentage, value)} end end ``` -## Stepper +### Stepper This code example renders a SwiftUI [Stepper](https://developer.apple.com/documentation/swiftui/stepper). It triggers the change event and sends a `"change-tickets"` message when the stepper increments or decrements. The `"change-tickets"` event handler then updates the number of tickets stored in state, which appears in the UI. Evaluate the example and increment/decrement the step. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -504,14 +497,17 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - - Tickets <%= @tickets %> - + + + Tickets <%= @tickets %> + + """ end end @@ -529,19 +525,19 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("change-tickets", %{"value" => tickets}, socket) do + def handle_event("change-tickets", %{"my-stepper" => tickets}, socket) do {:noreply, assign(socket, :tickets, tickets)} end end ``` -## Toggle +### Toggle -This code example renders a SwiftUI [Toggle](https://developer.apple.com/documentation/swiftui/toggle). It triggers the change event and sends a `"toggle"` message when toggled. The `"toggle"` event handler then updates the `:on` field in state, which allows the `Toggle` view to be toggled on. Without providing the `is-on` attribute, the `Toggle` view could not be flipped on and off. +This code example renders a SwiftUI [Toggle](https://developer.apple.com/documentation/swiftui/toggle). It triggers the change event and sends a `"toggle"` message when toggled. The `"toggle"` event handler then updates the `:on` field in state, which allows the `Toggle` view to be toggled o through the `isOn` attribute. Evaluate the example below and click on the toggle. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -549,7 +545,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - On/Off + + On/Off + """ end end @@ -567,17 +565,17 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("toggle", %{"is-on" => on}, socket) do + def handle_event("toggle", %{"my-toggle" => on}, socket) do {:noreply, assign(socket, :on, on)} end end ``` -## DatePicker +### DatePicker -The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. +The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. Evaluate the example below and select a date to see the date params appear in the console below. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -585,7 +583,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + + + """ end end @@ -593,6 +593,7 @@ end defmodule ServerWeb.ExampleLive do use ServerWeb, :live_view use ServerNative, :live_view + require Logger @impl true def mount(_params, _session, socket) do @@ -604,7 +605,7 @@ defmodule ServerWeb.ExampleLive do @impl true def handle_event("pick-date", params, socket) do - IO.inspect(params, label: "Date Params") + Logger.info("Date Params: #{inspect(params)}") {:noreply, socket} end end @@ -622,20 +623,21 @@ DateTime.from_iso8601(iso8601) ### Your Turn: Displayed Components -The `DatePicker` view accepts a `displayed-components` attribute with the value of `"hour-and-minute"` or `"date"` to only display one of the two components. By default, the value is `"all"`. +The `DatePicker` view accepts a `displayedComponents` attribute with the value of `"hourAndMinute"` or `"date"` to only display one of the two components. By default, the value is `"all"`. -You're going to change the `displayed-components` attribute in the example below to see both of these options. Change `"all"` to `"date"`, then to `"hour-and-minute"`. Re-evaluate the cell between changes and see the updated UI. +You're going to change the `displayedComponents` attribute in the example below to see both of these options. Change `"all"` to `"date"`, then to `"hourAndMinute"`. Re-evaluate the cell between changes and see the updated UI. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - @impl true def render(assigns) do ~LVN""" - + + + """ end end @@ -648,7 +650,7 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("pick-date", params, socket) do + def handle_event("pick-date", _params, socket) do {:noreply, socket} end end @@ -674,7 +676,9 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Todo... + + Todo... + diff --git a/livebooks/markdown/stylesheets.md b/livebooks/markdown/stylesheets.md index 541b3b7f3..05886f2a6 100644 --- a/livebooks/markdown/stylesheets.md +++ b/livebooks/markdown/stylesheets.md @@ -29,7 +29,7 @@ LiveView Native watches for changes and updates the stylesheet, so those will be SwiftUI employs **modifiers** to style and customize views. In SwiftUI syntax, each modifier is a function that can be chained onto the view they modify. LiveView Native has a minimal DSL (Domain Specific Language) for writing SwiftUI modifiers. -Modifers can be applied through a LiveView Native Stylesheet and applying them through classes as described in the [LiveView Native Stylesheets](#liveview-native-stylesheets) section, or can be applied directly through the `class` attribute as described in the [Utility Styles](#utility-styles) section. +Modifers can be applied through a LiveView Native Stylesheet and applying them through inline styles as described in the [LiveView Native Stylesheets](#liveview-native-stylesheets) section, or can be applied directly through the `style` attribute as described in the [Utility Styles](#utility-styles) section. @@ -67,7 +67,7 @@ Text("Some Red Text") The DSL (Domain Specific Language) used in LiveView Native drops the `.` dot before each modifier, but otherwise remains largely the same. We do not document every modifier separately, since you can translate SwiftUI examples into the DSL syntax. -For example, Here's the same `foregroundStyle` modifier as it would be written in a LiveView Native stylesheet or class attribute, which we'll cover in a moment. +For example, Here's the same `foregroundStyle` modifier as it would be written in a LiveView Native stylesheet or style attribute, which we'll cover in a moment. ```swift foregroundStyle(.red) @@ -77,13 +77,13 @@ There are some exceptions where the DSL differs from SwiftUI syntax, which we'll ## Utility Styles -In addition to introducing stylesheets, LiveView Native `0.3.0` also introduced Utility classes, which will be our prefered method for writing styles in these Livebook guides. +In addition to introducing stylesheets, LiveView Native `0.3.0` also introduced Utility styles, which will be our prefered method for writing styles in these Livebook guides. Utility styles are comperable to inline styles in HTML, which have been largely discouraged in the CSS community. We recommend Utility styles for now as the easiest way to prototype applications. But, we hope to replace Utility styles with a more mature styling framework in the future. -The same SwiftUI syntax used inside of a stylesheet can be used directly inside of a `class` attribute. The example below defines the `foregroundStyle(.red)` modifier. Evaluate the example and view it in your simulator. +The same SwiftUI syntax used inside of a stylesheet can be used directly inside of a `style` attribute. The example below defines the `foregroundStyle(.red)` modifier. Evaluate the example and view it in your simulator. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -91,7 +91,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! + Hello, from LiveView Native! """ end end @@ -107,56 +107,46 @@ end ### Multiple Modifiers -You can write multiple modifiers, separate each by a space or newline character. +You can write multiple modifiers separated by a semi-color `;`. ```html -Hello, from LiveView Native! +Hello, from LiveView Native! ``` -For newline characters, you'll need to wrap the string in curly brackets `{}`. Using multiple lines can better organize larger amounts of modifiers. +To include newline characters in your string wrap the string in curly brackets `{}`. Using multiple lines can better organize larger amounts of modifiers. ```html - Hello, from LiveView Native! ``` - - -### Spaces - -At the time of writing, the parser for utility styles interprets space characters as a separator for each rule, thus you should not includes spaces in modifiers that might traditionally have a space. - -```html -Hello, from LiveView Native! -``` - -## Dynamic Class Names +## Dynamic Style Names -LiveView Native parses styles in your project to define a single stylesheet. You can find the AST representation of this stylesheet at http://localhost:4000/assets/app.swiftui.styles. This stylesheet is compiled on the server and then sent to the client. For this reason, class names must be fully-formed. For example, the following class using string interpolation is **invalid**. +LiveView Native parses styles in your project to define a single stylesheet. You can find the AST representation of this stylesheet at http://localhost:4000/assets/app.swiftui.styles. This stylesheet is compiled on the server and then sent to the client. For this reason, class names must be fully-formed. For example, the following style using string interpolation is **invalid**. ```html - + Invalid Example ``` -However, we can still use dynamic styles so long as the class names are fully formed. +However, we can still use dynamic styles so long as the modifiers are fully formed. ```html - + Red or Blue Text ``` Evaluate the example below multiple times while watching your simulator. Notice that the text is dynamically red or blue. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -164,7 +154,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + Hello, from LiveView Native! """ @@ -227,7 +217,7 @@ style View fill:orange Evaluate the example below to see this in action. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -235,8 +225,8 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! - Hello, from LiveView Native! + Hello, from LiveView Native! + Hello, from LiveView Native! """ end end @@ -250,82 +240,6 @@ defmodule ServerWeb.ExampleLive do end ``` -## Injecting Views in Stylesheets - -SwiftUI modifiers sometimes accept SwiftUI views as arguments. Here's an example using the `clipShape` modifier with a `Circle` view. - -```swift -Image("logo") - .clipShape(Circle()) -``` - -However, LiveView Native does not support using SwiftUI views directly within a stylesheet. Instead, we have a few alternative options in cases like this where we want to use a view within a modifier. - - - -### Using Members on a Given Type - -We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, if you look at the [clipShape](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) documentation you'll notice it accepts the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. This type defines the [circle](https://developer.apple.com/documentation/swiftui/shape/circle) property which we can use since it's equivalent to the [Circle](https://developer.apple.com/documentation/swiftui/circle) view for our purposes. - -We can use `Shape.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. - -```swift -Image("logo") - .clipShape(Shape.circle) -``` - -Using implicit member expression, we can simplify this code to the following: - -```swift -Image("logo") - .clipShape(.circle) -``` - -Which is simple to convert to the LiveView Native DSL using the rules we've already learned. - - - -```elixir -"example-class" do - clipShape(.circle) -end -``` - - - -### Injecting a View - -For more complex cases, we can inject a view directly into a stylesheet. - -Here's an example where this might be useful. SwiftUI has modifers that represent a named content area for views to be placed within. These views can even have their own modifiers, so it's not enough to use a simple static property on the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. - -```swift -Image("logo") - .overlay(content: { - Circle().stroke(.red, lineWidth: 4) - }) -``` - -To get around this issue, we instead inject a view into the stylesheet. First, define the modifier and use an atom to represent the view that's going to be injected. - - - -```elixir -"overlay-circle" do - overlay(content: :circle) -end -``` - -Then use the `template` attribute on the view to be injected into the stylesheet. This view should be a child of the view with the given class. - -```html - - - -``` - -We can then apply modifiers to the child view through a class as we've already seen. - ## Custom Colors ### SwiftUI Color Struct @@ -340,7 +254,7 @@ foregroundStyle(Color(.sRGB, red: 0.4627, green: 0.8392, blue: 1.0)) Evaluate the example below to see the custom color in your simulator. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -348,7 +262,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + Hello, from LiveView Native! """ @@ -400,7 +314,7 @@ The defined color is now available for use within LiveView Native styles. Howeve Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -408,7 +322,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! + Hello, from LiveView Native! """ end end @@ -433,8 +347,8 @@ We group modifiers together within a class that can be applied to an element. He ```elixir ~SHEET""" "red-title" do - foregroundColor(.red) - font(.title) + foregroundColor(.red); + font(.title); end """ ``` @@ -450,14 +364,92 @@ defmodule ServerWeb.Styles.App.SwiftUI do ~SHEET""" "red-title" do - foregroundColor(.red) - font(.title) + foregroundColor(.red); + font(.title); end """ end ``` -Since the Phoenix server runs in a dependency for these guides, you don't have direct access to the stylesheet module. +You can apply these classes through the `class` attribute. + +```html +Red Title Text +``` + +## Injecting Views in Stylesheets + +SwiftUI modifiers sometimes accept SwiftUI views as arguments. Here's an example using the `clipShape` modifier with a `Circle` view. + +```swift +Image("logo") + .clipShape(Circle()) +``` + +However, LiveView Native does not support using SwiftUI views directly within a stylesheet. Instead, we have a few alternative options in cases like this where we want to use a view within a modifier. + + + +### Using Members on a Given Type + +We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, if you look at the [clipShape](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) documentation you'll notice it accepts the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. This type defines the [circle](https://developer.apple.com/documentation/swiftui/shape/circle) property which we can use since it's equivalent to the [Circle](https://developer.apple.com/documentation/swiftui/circle) view for our purposes. + +We can use `Shape.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. + +```swift +Image("logo") + .clipShape(Shape.circle) +``` + +Using implicit member expression, we can simplify this code to the following: + +```swift +Image("logo") + .clipShape(.circle) +``` + +Which is simple to convert to the LiveView Native DSL using the rules we've already learned. + + + +```elixir +"example-class" do + clipShape(.circle) +end +``` + + + +### Injecting a View + +For more complex cases, we can inject a view directly into a stylesheet. + +Here's an example where this might be useful. SwiftUI has modifers that represent a named content area for views to be placed within. These views can even have their own modifiers, so it's not enough to use a simple static property on the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. + +```swift +Image("logo") + .overlay(content: { + Circle().stroke(.red, lineWidth: 4) + }) +``` + +To get around this issue, we instead inject a view into the stylesheet. First, define the modifier and use an atom to represent the view that's going to be injected. + + + +```elixir +"overlay-circle" do + overlay(content: :circle) +end +``` + +Then use the `template` attribute on the view to be injected into the stylesheet. + +```html + + + +``` ## Apple Documentation @@ -515,15 +507,15 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - - Turtle Rock - + + Turtle Rock + Joshua Tree National Park California - About Turtle Rock + About Turtle Rock Descriptive text goes here """ diff --git a/livebooks/stylesheets.livemd b/livebooks/stylesheets.livemd index 0b93678ef..29a192cef 100644 --- a/livebooks/stylesheets.livemd +++ b/livebooks/stylesheets.livemd @@ -83,7 +83,7 @@ LiveView Native watches for changes and updates the stylesheet, so those will be SwiftUI employs **modifiers** to style and customize views. In SwiftUI syntax, each modifier is a function that can be chained onto the view they modify. LiveView Native has a minimal DSL (Domain Specific Language) for writing SwiftUI modifiers. -Modifers can be applied through a LiveView Native Stylesheet and applying them through classes as described in the [LiveView Native Stylesheets](#liveview-native-stylesheets) section, or can be applied directly through the `class` attribute as described in the [Utility Styles](#utility-styles) section. +Modifers can be applied through a LiveView Native Stylesheet and applying them through inline styles as described in the [LiveView Native Stylesheets](#liveview-native-stylesheets) section, or can be applied directly through the `style` attribute as described in the [Utility Styles](#utility-styles) section. @@ -121,7 +121,7 @@ Text("Some Red Text") The DSL (Domain Specific Language) used in LiveView Native drops the `.` dot before each modifier, but otherwise remains largely the same. We do not document every modifier separately, since you can translate SwiftUI examples into the DSL syntax. -For example, Here's the same `foregroundStyle` modifier as it would be written in a LiveView Native stylesheet or class attribute, which we'll cover in a moment. +For example, Here's the same `foregroundStyle` modifier as it would be written in a LiveView Native stylesheet or style attribute, which we'll cover in a moment. ```swift foregroundStyle(.red) @@ -131,13 +131,13 @@ There are some exceptions where the DSL differs from SwiftUI syntax, which we'll ## Utility Styles -In addition to introducing stylesheets, LiveView Native `0.3.0` also introduced Utility classes, which will be our prefered method for writing styles in these Livebook guides. +In addition to introducing stylesheets, LiveView Native `0.3.0` also introduced Utility styles, which will be our prefered method for writing styles in these Livebook guides. Utility styles are comperable to inline styles in HTML, which have been largely discouraged in the CSS community. We recommend Utility styles for now as the easiest way to prototype applications. But, we hope to replace Utility styles with a more mature styling framework in the future. -The same SwiftUI syntax used inside of a stylesheet can be used directly inside of a `class` attribute. The example below defines the `foregroundStyle(.red)` modifier. Evaluate the example and view it in your simulator. +The same SwiftUI syntax used inside of a stylesheet can be used directly inside of a `style` attribute. The example below defines the `foregroundStyle(.red)` modifier. Evaluate the example and view it in your simulator. - + ```elixir require Server.Livebook @@ -149,7 +149,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! + Hello, from LiveView Native! """ end end @@ -170,56 +170,46 @@ import Kernel ### Multiple Modifiers -You can write multiple modifiers, separate each by a space or newline character. +You can write multiple modifiers separated by a semi-color `;`. ```html -Hello, from LiveView Native! +Hello, from LiveView Native! ``` -For newline characters, you'll need to wrap the string in curly brackets `{}`. Using multiple lines can better organize larger amounts of modifiers. +To include newline characters in your string wrap the string in curly brackets `{}`. Using multiple lines can better organize larger amounts of modifiers. ```html - Hello, from LiveView Native! ``` - - -### Spaces - -At the time of writing, the parser for utility styles interprets space characters as a separator for each rule, thus you should not includes spaces in modifiers that might traditionally have a space. - -```html -Hello, from LiveView Native! -``` - -## Dynamic Class Names +## Dynamic Style Names -LiveView Native parses styles in your project to define a single stylesheet. You can find the AST representation of this stylesheet at http://localhost:4000/assets/app.swiftui.styles. This stylesheet is compiled on the server and then sent to the client. For this reason, class names must be fully-formed. For example, the following class using string interpolation is **invalid**. +LiveView Native parses styles in your project to define a single stylesheet. You can find the AST representation of this stylesheet at http://localhost:4000/assets/app.swiftui.styles. This stylesheet is compiled on the server and then sent to the client. For this reason, class names must be fully-formed. For example, the following style using string interpolation is **invalid**. ```html - + Invalid Example ``` -However, we can still use dynamic styles so long as the class names are fully formed. +However, we can still use dynamic styles so long as the modifiers are fully formed. ```html - + Red or Blue Text ``` Evaluate the example below multiple times while watching your simulator. Notice that the text is dynamically red or blue. - + ```elixir require Server.Livebook @@ -231,7 +221,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + Hello, from LiveView Native! """ @@ -299,7 +289,7 @@ style View fill:orange Evaluate the example below to see this in action. - + ```elixir require Server.Livebook @@ -311,8 +301,8 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! - Hello, from LiveView Native! + Hello, from LiveView Native! + Hello, from LiveView Native! """ end end @@ -331,82 +321,6 @@ import Kernel :ok ``` -## Injecting Views in Stylesheets - -SwiftUI modifiers sometimes accept SwiftUI views as arguments. Here's an example using the `clipShape` modifier with a `Circle` view. - -```swift -Image("logo") - .clipShape(Circle()) -``` - -However, LiveView Native does not support using SwiftUI views directly within a stylesheet. Instead, we have a few alternative options in cases like this where we want to use a view within a modifier. - - - -### Using Members on a Given Type - -We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, if you look at the [clipShape](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) documentation you'll notice it accepts the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. This type defines the [circle](https://developer.apple.com/documentation/swiftui/shape/circle) property which we can use since it's equivalent to the [Circle](https://developer.apple.com/documentation/swiftui/circle) view for our purposes. - -We can use `Shape.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. - -```swift -Image("logo") - .clipShape(Shape.circle) -``` - -Using implicit member expression, we can simplify this code to the following: - -```swift -Image("logo") - .clipShape(.circle) -``` - -Which is simple to convert to the LiveView Native DSL using the rules we've already learned. - - - -```elixir -"example-class" do - clipShape(.circle) -end -``` - - - -### Injecting a View - -For more complex cases, we can inject a view directly into a stylesheet. - -Here's an example where this might be useful. SwiftUI has modifers that represent a named content area for views to be placed within. These views can even have their own modifiers, so it's not enough to use a simple static property on the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. - -```swift -Image("logo") - .overlay(content: { - Circle().stroke(.red, lineWidth: 4) - }) -``` - -To get around this issue, we instead inject a view into the stylesheet. First, define the modifier and use an atom to represent the view that's going to be injected. - - - -```elixir -"overlay-circle" do - overlay(content: :circle) -end -``` - -Then use the `template` attribute on the view to be injected into the stylesheet. This view should be a child of the view with the given class. - -```html - - - -``` - -We can then apply modifiers to the child view through a class as we've already seen. - ## Custom Colors ### SwiftUI Color Struct @@ -421,7 +335,7 @@ foregroundStyle(Color(.sRGB, red: 0.4627, green: 0.8392, blue: 1.0)) Evaluate the example below to see the custom color in your simulator. - + ```elixir require Server.Livebook @@ -433,7 +347,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - + Hello, from LiveView Native! """ @@ -490,7 +404,7 @@ The defined color is now available for use within LiveView Native styles. Howeve Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator. - + ```elixir require Server.Livebook @@ -502,7 +416,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! + Hello, from LiveView Native! """ end end @@ -532,8 +446,8 @@ We group modifiers together within a class that can be applied to an element. He ```elixir ~SHEET""" "red-title" do - foregroundColor(.red) - font(.title) + foregroundColor(.red); + font(.title); end """ ``` @@ -549,14 +463,92 @@ defmodule ServerWeb.Styles.App.SwiftUI do ~SHEET""" "red-title" do - foregroundColor(.red) - font(.title) + foregroundColor(.red); + font(.title); end """ end ``` -Since the Phoenix server runs in a dependency for these guides, you don't have direct access to the stylesheet module. +You can apply these classes through the `class` attribute. + +```html +Red Title Text +``` + +## Injecting Views in Stylesheets + +SwiftUI modifiers sometimes accept SwiftUI views as arguments. Here's an example using the `clipShape` modifier with a `Circle` view. + +```swift +Image("logo") + .clipShape(Circle()) +``` + +However, LiveView Native does not support using SwiftUI views directly within a stylesheet. Instead, we have a few alternative options in cases like this where we want to use a view within a modifier. + + + +### Using Members on a Given Type + +We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, if you look at the [clipShape](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) documentation you'll notice it accepts the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. This type defines the [circle](https://developer.apple.com/documentation/swiftui/shape/circle) property which we can use since it's equivalent to the [Circle](https://developer.apple.com/documentation/swiftui/circle) view for our purposes. + +We can use `Shape.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. + +```swift +Image("logo") + .clipShape(Shape.circle) +``` + +Using implicit member expression, we can simplify this code to the following: + +```swift +Image("logo") + .clipShape(.circle) +``` + +Which is simple to convert to the LiveView Native DSL using the rules we've already learned. + + + +```elixir +"example-class" do + clipShape(.circle) +end +``` + + + +### Injecting a View + +For more complex cases, we can inject a view directly into a stylesheet. + +Here's an example where this might be useful. SwiftUI has modifers that represent a named content area for views to be placed within. These views can even have their own modifiers, so it's not enough to use a simple static property on the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. + +```swift +Image("logo") + .overlay(content: { + Circle().stroke(.red, lineWidth: 4) + }) +``` + +To get around this issue, we instead inject a view into the stylesheet. First, define the modifier and use an atom to represent the view that's going to be injected. + + + +```elixir +"overlay-circle" do + overlay(content: :circle) +end +``` + +Then use the `template` attribute on the view to be injected into the stylesheet. + +```html + + + +``` ## Apple Documentation @@ -615,15 +607,15 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - - Turtle Rock - + + Turtle Rock + Joshua Tree National Park California - About Turtle Rock + About Turtle Rock Descriptive text goes here """ From 9a809098589056c8341104ca611196d0a68337d9 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Tue, 21 May 2024 11:49:33 -0400 Subject: [PATCH 13/62] fix create a swiftui application bug --- livebooks/create-a-swiftui-application.livemd | 6 ++++-- livebooks/markdown/create-a-swiftui-application.md | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/livebooks/create-a-swiftui-application.livemd b/livebooks/create-a-swiftui-application.livemd index 63769f43c..6e34ae071 100644 --- a/livebooks/create-a-swiftui-application.livemd +++ b/livebooks/create-a-swiftui-application.livemd @@ -121,18 +121,20 @@ To create an iOS application, select the **iOS** and **App** options and click * Choose options for your new project that match the following image, then click **Next**. +![Xcode Choose Options For Your New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-choose-options-for-your-new-project.png?raw=true) +
What do these options mean? -* **Product Name:** The name of the application. This can be any valid name. We've chosen **Guides`. +* **Product Name:** The name of the application. This can be any valid name. We've chosen **Guides**. * **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recommends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using **com.example.your_name** where **your_name** is your organization or personal name. * **Interface:**: The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. * **Language:** Determines which language Xcode should use for the project. Select **Swift**. +
-![Xcode Choose Options For Your New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-choose-options-for-your-new-project.png?raw=true) diff --git a/livebooks/markdown/create-a-swiftui-application.md b/livebooks/markdown/create-a-swiftui-application.md index 879cd7e20..51a401dd0 100644 --- a/livebooks/markdown/create-a-swiftui-application.md +++ b/livebooks/markdown/create-a-swiftui-application.md @@ -58,17 +58,19 @@ To create an iOS application, select the **iOS** and **App** options and click * Choose options for your new project that match the following image, then click **Next**. +![Xcode Choose Options For Your New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-choose-options-for-your-new-project.png?raw=true) + ### What do these options mean? -* **Product Name:** The name of the application. This can be any valid name. We've chosen **Guides`. +* **Product Name:** The name of the application. This can be any valid name. We've chosen **Guides**. * **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recommends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using **com.example.your_name** where **your_name** is your organization or personal name. * **Interface:**: The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. * **Language:** Determines which language Xcode should use for the project. Select **Swift**. + -![Xcode Choose Options For Your New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-choose-options-for-your-new-project.png?raw=true) From 452a9036c90ea5eb4134f2210f05be87ddaae919 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Tue, 21 May 2024 12:47:49 -0400 Subject: [PATCH 14/62] use single quotes for Color syntax --- livebooks/markdown/stylesheets.md | 4 ++-- livebooks/stylesheets.livemd | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/livebooks/markdown/stylesheets.md b/livebooks/markdown/stylesheets.md index 05886f2a6..6f7c830d6 100644 --- a/livebooks/markdown/stylesheets.md +++ b/livebooks/markdown/stylesheets.md @@ -314,7 +314,7 @@ The defined color is now available for use within LiveView Native styles. Howeve Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -322,7 +322,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! + Hello, from LiveView Native! """ end end diff --git a/livebooks/stylesheets.livemd b/livebooks/stylesheets.livemd index 29a192cef..e9bc03868 100644 --- a/livebooks/stylesheets.livemd +++ b/livebooks/stylesheets.livemd @@ -404,7 +404,7 @@ The defined color is now available for use within LiveView Native styles. Howeve Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator. - + ```elixir require Server.Livebook @@ -416,7 +416,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! + Hello, from LiveView Native! """ end end From aa545973b91cd9f3c05e696421872616f587371a Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Tue, 21 May 2024 12:47:57 -0400 Subject: [PATCH 15/62] add vscode to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 61efa18f0..f70a33a71 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ live_view_native_swiftui-*.tar # Temporary files, for example, from tests. /tmp/ + +.vscode \ No newline at end of file From 8e8d08256dc160b3e6a58144a12f4ad6f473bf2c Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Tue, 21 May 2024 12:48:06 -0400 Subject: [PATCH 16/62] Draft syntax conversion cheatsheet --- guides/syntax_conversion.cheatmd | 227 +++++++++++++++++++++++++++++++ mix.exs | 4 +- 2 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 guides/syntax_conversion.cheatmd diff --git a/guides/syntax_conversion.cheatmd b/guides/syntax_conversion.cheatmd new file mode 100644 index 000000000..55ac7f30f --- /dev/null +++ b/guides/syntax_conversion.cheatmd @@ -0,0 +1,227 @@ +# SwiftUI to LiveView Native Conversion Cheat Sheet + +In this short guide, we'll cover the fundamental SwiftUI syntax you'll encounter in SwiftUI guides and documentation and how to convert that syntax into LiveView Native templates and stylesheets. We've omitted deeper explanations of each concept to keep this guide brief for use as a convenient cheat sheet. + +You may wish to bookmark this guide and return to it as needed. In the interest of quick reference, we've kept explanations short. We hope to provide more guides in the future that will help explain these concepts deeper. Stay tuned to the DockYard blog for more guides and subscribe to the [LiveView Native Newsletter](https://dockyard.com/newsletter) for the latest updates on LiveView Native development + +You can also find more documentation and guides on the [LiveView Native Hexdocs](https://hexdocs.pm/live_view_native/overview.html). + +## Views + +SwiftUI Views are the building blocks of user interfaces in Swift applications. They represent the visual elements of an app, such as buttons, text fields, and images, and are structured hierarchically to compose complex interfaces. In LiveView Native, we represent views using syntax similar to HTML tags. + +## + +{: .col-2} + +### SwiftUI + +```swift +Text("Hello, SwiftUI") +``` + +### LiveView Native + +```heex +Hello, SwiftUI +``` + +## Modifiers + +SwiftUI modifiers are functions used to modify the appearance, behavior, or layout of views declaratively. They enable developers to apply various transformations and adjustments to views, such as changing colors, fonts, sizes, and alignments or adding animations and gestures. These modifiers are chainable, allowing for complex and dynamic interfaces through multiple modifiers applied to a single view. + +In LiveView Native, we use stylesheets with the `class` attribute or the inline `style` attribute. To be more similar to CSS stylesheets, LiveView Native uses semi-colons `;` to split modifiers rather than the `.` used by SwiftUI. + +## + +{: .col-2} + +### SwiftUI + +```swift +Text("Hello, SwiftUI") + .font(.title) + .foregroundStyle(.blue) +``` + +### LiveView Native + +```elixir +Hello, SwiftUI +``` + +Spaces and using newline characters are optional to improve organization. + +```elixir +Hello, SwiftUI +``` + +## Attributes + +In SwiftUI, attributes are properties that define the appearance and behavior of views. Unlike modifiers, attributes set the initial properties of views, while modifiers dynamically modify or augment a view after it's created. Also, modifiers typically affect child views, whereas attributes only affect one view. In practice, attributes are more similar to parameters in a function, whereas modifiers are chainable functions that modify a view. + +## + +{: .col-2} + +### SwiftUI + +```swift +VStack(alignment: .leading) +``` + +### LiveView Native + +```heex + +``` + +## Unnamed Attributes + +In many SwiftUI Views, the first argument to the function is often an unnamed attribute. SwiftUI uses an underscore `_` to indicate the attribute is unnamed. Unnamed attributes are just optional syntax sugar to avoid passing in the name. + +In these cases, in LiveView Native, we use the attribute's name to provide the value. + +## + +{: .col-2} + +### SwiftUI + +Unnamed version + +```swift +Image("turtlerock") +``` + +Named version (equivalent to the above) + +```swift +Image(name: "turtlerock") +``` + +### LiveView Native + +```heex + +``` + +## + +### Finding the Attribute Name + +You can find the attributes to a view within the Topics section of the views documentation in the corresponding `init` definition. For example, here's the [Image Topics section](https://developer.apple.com/documentation/swiftui/image#creating-an-image) where you can find the [Image's init](https://developer.apple.com/documentation/swiftui/image/init(_:bundle:)) function definition. + +The init definition includes a `_ name` unnamed attribute whose value is a `String`. Here's the same snippet you can find in the documentation above. + +```swift +init( + _ name: String, + bundle: Bundle? = nil +) +``` + +## Views as Arguments + +SwiftUI Modifiers can accept views as arguments. Supporting views as arguments presents a challenge for LiveView Native as there's no equivalent in a CSS-inspired paradigm. It would be like having a CSS property accept HTML elements as a value. + +To support this pattern, LiveView Native represents SwiftUI Views using dot notation within a stylesheet. + +## + +{: .col-2} + +### SwiftUI + +```swift +Image(name: "turtlerock") + .clipShape(Circle()) +``` + +### LiveView Native + +Stylesheet + +```elixir +defmodule MyAppWeb.Styles.SwiftUI do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "clipShape:circle" do + clipShape(.circle) + end + """ +end +``` + +Template + +```heex + +``` + +## Named Content Areas + +SwiftUI Views can have content area modifiers that accept one or more views inside a closure (the curly `{}` brackets). Views within the named content area can even have their own modifiers. + +LiveView Native supports named content areas through the `template` attribute. The stylesheet specifies a name for the content area using an atom. The view's `template` attribute should match the atom used. + +## + +{: .col-2} + +### SwiftUI + +Unnamed version + +```swift +Image("turtlerock") + .overlay { + Circle().stroke(.white, lineWidth: 4) + } +``` + +Named version (equivalent to the above) + +```swift +Image("turtlerock") + .overlay { + content: Circle().stroke(.white, lineWidth: 4) + } +``` + +### LiveView Native + +Stylesheet + +```elixir +defmodule MyAppWeb.Styles.SwiftUI do + use LiveViewNative.Stylesheet, :swiftui + + ~SHEET""" + "overlay-circle" do + overlay(content: :circle) + end + "white-border" do + stroke(.white, lineWidth: 4) + end + """ +end +``` + +Template + +```heex + + + +``` + +## Conclusion + +Use this cheatsheet for reference whenever you're converting SwiftUI examples into LiveView Native code and you should have the tools you need to build Native UIs from SwiftUI examples. We strongly encourage you to bookmark this page as it will likely be helpful in the future. \ No newline at end of file diff --git a/mix.exs b/mix.exs index da7fc8c7a..77bb4ab16 100644 --- a/mix.exs +++ b/mix.exs @@ -124,13 +124,13 @@ defmodule LiveViewNative.SwiftUI.MixProject do "livebooks/markdown/forms-and-validation.md" ] - ["README.md"] ++ guides ++ generated_docs ++ livebooks + ["README.md", "guides/syntax_conversion.cheatmd"] ++ guides ++ generated_docs ++ livebooks end defp groups_for_extras do guide_groups = [ Architecture: Path.wildcard("guides/architecture/*.md"), - Livebooks: ~r/markdown_livebooks/ + Livebooks: Path.wildcard("livebooks/markdown/*.md") ] generated_groups = From 164aa395ed75469f21577abf67e1447edde78a48 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Tue, 21 May 2024 12:49:27 -0400 Subject: [PATCH 17/62] Replace ```html with ```heex --- livebooks/interactive-swiftui-views.livemd | 2 +- livebooks/markdown/interactive-swiftui-views.md | 2 +- livebooks/markdown/native-navigation.md | 2 +- livebooks/markdown/stylesheets.md | 12 ++++++------ livebooks/native-navigation.livemd | 2 +- livebooks/stylesheets.livemd | 12 ++++++------ 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/livebooks/interactive-swiftui-views.livemd b/livebooks/interactive-swiftui-views.livemd index e1b5bd52a..59dcf16bf 100644 --- a/livebooks/interactive-swiftui-views.livemd +++ b/livebooks/interactive-swiftui-views.livemd @@ -475,7 +475,7 @@ The `LiveForm` view must wrap views to capture events from the `phx-change` or ` Here's some example boilerplate for a `LiveForm`. The `id` attribute is required. -```html +```heex Button Text diff --git a/livebooks/markdown/interactive-swiftui-views.md b/livebooks/markdown/interactive-swiftui-views.md index 7580dda96..6b27c5d1f 100644 --- a/livebooks/markdown/interactive-swiftui-views.md +++ b/livebooks/markdown/interactive-swiftui-views.md @@ -356,7 +356,7 @@ The `LiveForm` view must wrap views to capture events from the `phx-change` or ` Here's some example boilerplate for a `LiveForm`. The `id` attribute is required. -```html +```heex Button Text diff --git a/livebooks/markdown/native-navigation.md b/livebooks/markdown/native-navigation.md index 27125721f..deced73c3 100644 --- a/livebooks/markdown/native-navigation.md +++ b/livebooks/markdown/native-navigation.md @@ -50,7 +50,7 @@ end Visit http://localhost:4000/?_format=swiftui. The `?_format` query parameter specifies the Phoenix server should respond with the swiftui template rather than the web template. You should see source code similar to the example below. We've replaced long tokens with `"some token"` for the sake of readability. -```html +```heex diff --git a/livebooks/markdown/stylesheets.md b/livebooks/markdown/stylesheets.md index 6f7c830d6..af66cda9f 100644 --- a/livebooks/markdown/stylesheets.md +++ b/livebooks/markdown/stylesheets.md @@ -109,13 +109,13 @@ end You can write multiple modifiers separated by a semi-color `;`. -```html +```heex Hello, from LiveView Native! ``` To include newline characters in your string wrap the string in curly brackets `{}`. Using multiple lines can better organize larger amounts of modifiers. -```html +```heex Invalid Example @@ -138,7 +138,7 @@ Invalid Example However, we can still use dynamic styles so long as the modifiers are fully formed. -```html +```heex Red or Blue Text @@ -373,7 +373,7 @@ end You can apply these classes through the `class` attribute. -```html +```heex Red Title Text ``` @@ -445,7 +445,7 @@ end Then use the `template` attribute on the view to be injected into the stylesheet. -```html +```heex diff --git a/livebooks/native-navigation.livemd b/livebooks/native-navigation.livemd index c1dfdef38..0c28b55f3 100644 --- a/livebooks/native-navigation.livemd +++ b/livebooks/native-navigation.livemd @@ -113,7 +113,7 @@ import Kernel Visit http://localhost:4000/?_format=swiftui. The `?_format` query parameter specifies the Phoenix server should respond with the swiftui template rather than the web template. You should see source code similar to the example below. We've replaced long tokens with `"some token"` for the sake of readability. -```html +```heex diff --git a/livebooks/stylesheets.livemd b/livebooks/stylesheets.livemd index e9bc03868..309ae447e 100644 --- a/livebooks/stylesheets.livemd +++ b/livebooks/stylesheets.livemd @@ -172,13 +172,13 @@ import Kernel You can write multiple modifiers separated by a semi-color `;`. -```html +```heex Hello, from LiveView Native! ``` To include newline characters in your string wrap the string in curly brackets `{}`. Using multiple lines can better organize larger amounts of modifiers. -```html +```heex Invalid Example @@ -201,7 +201,7 @@ Invalid Example However, we can still use dynamic styles so long as the modifiers are fully formed. -```html +```heex Red or Blue Text @@ -472,7 +472,7 @@ end You can apply these classes through the `class` attribute. -```html +```heex Red Title Text ``` @@ -544,7 +544,7 @@ end Then use the `template` attribute on the view to be injected into the stylesheet. -```html +```heex From 897daf50c13d2f28701ad5d1cd88b4f49529bf47 Mon Sep 17 00:00:00 2001 From: Brooklin Myers Date: Wed, 22 May 2024 15:11:32 -0400 Subject: [PATCH 18/62] Update livebooks/create-a-swiftui-application.livemd Co-authored-by: Carson Katri Signed-off-by: Brooklin Myers --- livebooks/create-a-swiftui-application.livemd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livebooks/create-a-swiftui-application.livemd b/livebooks/create-a-swiftui-application.livemd index 6e34ae071..2cd150a8b 100644 --- a/livebooks/create-a-swiftui-application.livemd +++ b/livebooks/create-a-swiftui-application.livemd @@ -128,7 +128,7 @@ Choose options for your new project that match the following image, then click * * **Product Name:** The name of the application. This can be any valid name. We've chosen **Guides**. * **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recommends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using **com.example.your_name** where **your_name** is your organization or personal name. -* **Interface:**: The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. +* **Interface:** The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. * **Language:** Determines which language Xcode should use for the project. Select **Swift**. From dc28b2bc56aea4e71919eb9861bcac56a3026bdd Mon Sep 17 00:00:00 2001 From: Brooklin Myers Date: Wed, 22 May 2024 15:14:48 -0400 Subject: [PATCH 19/62] Update livebooks/create-a-swiftui-application.livemd Co-authored-by: Carson Katri Signed-off-by: Brooklin Myers --- livebooks/create-a-swiftui-application.livemd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livebooks/create-a-swiftui-application.livemd b/livebooks/create-a-swiftui-application.livemd index 2cd150a8b..8a7a7ecd0 100644 --- a/livebooks/create-a-swiftui-application.livemd +++ b/livebooks/create-a-swiftui-application.livemd @@ -182,7 +182,7 @@ At this point, you'll need to enable permissions for plugins used by LiveView Na -You'll also need to manually navigate to the error tab (shown below) to trust and enable packages. Click on each error to trigger a prompt. Select **Trust & Enable All** to enable the plugin. +You'll also need to manually navigate to the error tab (shown below) to trust and enable packages. Click on each error to trigger a prompt. Select **Trust & Enable** to enable the plugin. The specific plugins are subject to change. At the time of writing you need to enable `LiveViewNativeStylesheetMacros`, `LiveViewNativeMacros`, and `CasePathMacros` as shown in the images below. From 3d806a4ea45148c0998d638f32266ff14f5d64b0 Mon Sep 17 00:00:00 2001 From: Brooklin Myers Date: Wed, 22 May 2024 15:17:24 -0400 Subject: [PATCH 20/62] Update livebooks/create-a-swiftui-application.livemd Co-authored-by: Carson Katri Signed-off-by: Brooklin Myers --- livebooks/create-a-swiftui-application.livemd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/livebooks/create-a-swiftui-application.livemd b/livebooks/create-a-swiftui-application.livemd index 8a7a7ecd0..b63ecb0f2 100644 --- a/livebooks/create-a-swiftui-application.livemd +++ b/livebooks/create-a-swiftui-application.livemd @@ -223,9 +223,9 @@ import LiveViewNative struct ContentView: View { var body: some View { - LiveView(.automatic( - development: .localhost(path: "/"), - production: .custom(URL(string: "https://example.com/")!) + #LiveView(.automatic( + development: .localhost, + production: URL(string: "https://example.com/")! )) } } From 89b3487c9ec0fdf651a903b6452f0e00185ff099 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Wed, 22 May 2024 15:28:12 -0400 Subject: [PATCH 21/62] resolve PR review comments --- lib/mix/tasks/lvn.swiftui.gen.livemarkdown.ex | 1 + livebooks/create-a-swiftui-application.livemd | 10 ++--- livebooks/forms-and-validation.livemd | 3 +- livebooks/getting-started.livemd | 3 +- livebooks/interactive-swiftui-views.livemd | 3 +- .../markdown/create-a-swiftui-application.md | 44 +++++++++---------- livebooks/markdown/forms-and-validation.md | 14 +++--- .../markdown/interactive-swiftui-views.md | 10 ++--- livebooks/markdown/stylesheets.md | 22 +++++----- livebooks/markdown/swiftui-views.md | 12 ++--- livebooks/native-navigation.livemd | 3 +- livebooks/stylesheets.livemd | 3 +- livebooks/swiftui-views.livemd | 3 +- 13 files changed, 68 insertions(+), 63 deletions(-) diff --git a/lib/mix/tasks/lvn.swiftui.gen.livemarkdown.ex b/lib/mix/tasks/lvn.swiftui.gen.livemarkdown.ex index 781a3986e..7bc781b77 100644 --- a/lib/mix/tasks/lvn.swiftui.gen.livemarkdown.ex +++ b/lib/mix/tasks/lvn.swiftui.gen.livemarkdown.ex @@ -39,6 +39,7 @@ defmodule Mix.Tasks.Lvn.Swiftui.Gen.Livemarkdown do """, "") |> String.replace(~r/\|\> Server\.SmartCells\.LiveViewNative\.register\(\".+\"\)\n\nimport Server\.Livebook, only: \[\]\nimport Kernel\n:ok\n/, "") |> String.replace(~r/\|\> Server\.SmartCells\.RenderComponent\.register\(\)\n\nimport Server\.Livebook, only: \[\]\nimport Kernel\n:ok\n/, "") + |> String.replace(~s[], "") end defp convert_details_sections(content) do diff --git a/livebooks/create-a-swiftui-application.livemd b/livebooks/create-a-swiftui-application.livemd index b63ecb0f2..f7ac99e7e 100644 --- a/livebooks/create-a-swiftui-application.livemd +++ b/livebooks/create-a-swiftui-application.livemd @@ -22,7 +22,8 @@ Mix.install( ], pubsub_server: Server.PubSub, live_view: [signing_salt: "JSgdVVL6"], - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {0, 0, 0, 0}, port: 4000], + check_origin: false, secret_key_base: String.duplicate("a", 64), live_reload: [ patterns: [ @@ -133,9 +134,6 @@ Choose options for your new project that match the following image, then click * - - - Select an appropriate folder location to store the iOS project, then click **Create.** @@ -204,7 +202,7 @@ The [LiveView Native Live Form](https://github.com/liveview-native/liveview-nati To install LiveView Native Form, we need to add the [liveview-native-live-form](https://github.com/liveview-native/liveview-native-live-form) SwiftUI package to our iOS application. The steps will be mostly similar to what you have already setup with the `liveview-client-swiftui` package. -Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README to add the `liveview-native-live-form` package to the SwiftUI application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native_swiftui/create-a-swiftui-application.html) guide. +Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README to add the `liveview-native-live-form` package to the SwiftUI application. Come back to this guide and continue after you have finished the installation process. @@ -263,7 +261,7 @@ graph LR; ## Start the Active Scheme -Click the `start active scheme` button to build the project and run it on the iOS simulator. +Click the run button to build the project and run it on the iOS simulator. Alternatively you may go to `Product` in the top menu then press `Run`. > A [build scheme](https://developer.apple.com/documentation/xcode/build-system) contains a list of targets to build and any configuration and environment details that affect the selected action. When you build and run an app, the scheme tells Xcode what launch arguments to pass to the app. > diff --git a/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd index 842720758..4b19dce2f 100644 --- a/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -22,7 +22,8 @@ Mix.install( ], pubsub_server: Server.PubSub, live_view: [signing_salt: "JSgdVVL6"], - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {0, 0, 0, 0}, port: 4000], + check_origin: false, secret_key_base: String.duplicate("a", 64), live_reload: [ patterns: [ diff --git a/livebooks/getting-started.livemd b/livebooks/getting-started.livemd index 0905a4791..8b0b13a8c 100644 --- a/livebooks/getting-started.livemd +++ b/livebooks/getting-started.livemd @@ -20,7 +20,8 @@ Mix.install( ], pubsub_server: Server.PubSub, live_view: [signing_salt: "JSgdVVL6"], - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {0, 0, 0, 0}, port: 4000], + check_origin: false, secret_key_base: String.duplicate("a", 64), live_reload: [ patterns: [ diff --git a/livebooks/interactive-swiftui-views.livemd b/livebooks/interactive-swiftui-views.livemd index 59dcf16bf..e7aef8f2e 100644 --- a/livebooks/interactive-swiftui-views.livemd +++ b/livebooks/interactive-swiftui-views.livemd @@ -21,7 +21,8 @@ Mix.install( ], pubsub_server: Server.PubSub, live_view: [signing_salt: "JSgdVVL6"], - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {0, 0, 0, 0}, port: 4000], + check_origin: false, secret_key_base: String.duplicate("a", 64), live_reload: [ patterns: [ diff --git a/livebooks/markdown/create-a-swiftui-application.md b/livebooks/markdown/create-a-swiftui-application.md index 51a401dd0..ff75aa1c7 100644 --- a/livebooks/markdown/create-a-swiftui-application.md +++ b/livebooks/markdown/create-a-swiftui-application.md @@ -42,19 +42,19 @@ end Open Xcode and select Create New Project. - + ![Xcode Create New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-create-new-project.png?raw=true) - + To create an iOS application, select the **iOS** and **App** options and click **Next**. - + ![Xcode Create Template For New Project](https://github.com/liveview-native/documentation_assets/blob/main/xcode-create-template-for-new-project.png?raw=true) - + Choose options for your new project that match the following image, then click **Next**. @@ -69,22 +69,22 @@ Choose options for your new project that match the following image, then click * - - + + Select an appropriate folder location to store the iOS project, then click **Create.** - + ![Xcode select folder location](https://github.com/liveview-native/documentation_assets/blob/main/xcode-select-folder-location.png?raw=true) - + You should see the default iOS application generated by Xcode. - + ![](https://github.com/liveview-native/documentation_assets/blob/main/default-xcode-app.png?raw=true) @@ -96,41 +96,41 @@ To install the package, from Xcode from the project you just created, select **F The image below displays `0.2.0`. You should select the latest version of LiveView Native. - + ![](https://github.com/liveview-native/documentation_assets/blob/main/add-liveview-swiftui-client-package-0.2.0.png?raw=true) - + Choose the Package Products for `liveview-client-swiftui`. Select **Guides** as the target for `LiveViewNative` and `LiveViewNativeStylesheet` to add these dependencies to your iOS project. - + ![](https://github.com/liveview-native/documentation_assets/blob/main/select-package-products.png?raw=true) - + At this point, you'll need to enable permissions for plugins used by LiveView Native. You should see the following prompt. Click **Trust & Enable All**. - + ![Xcode some build plugins are disabled](https://github.com/liveview-native/documentation_assets/blob/main/xcode-some-build-plugins-are-disabled.png?raw=true) - + You'll also need to manually navigate to the error tab (shown below) to trust and enable packages. Click on each error to trigger a prompt. Select **Trust & Enable All** to enable the plugin. The specific plugins are subject to change. At the time of writing you need to enable `LiveViewNativeStylesheetMacros`, `LiveViewNativeMacros`, and `CasePathMacros` as shown in the images below. - + ![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-liveview-native-stylesheet.png?raw=true) - + ![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-liveview-native-macros.png?raw=true) - + ![](https://github.com/liveview-native/documentation_assets/blob/main/trust-and-enable-case-path-macros.png?raw=true) @@ -150,7 +150,7 @@ The [ContentView](https://developer.apple.com/tutorials/swiftui-concepts/explori Replace the code in the `ContentView` file with the following to connect the SwiftUI application and the Phoenix application. - + ```swift import SwiftUI @@ -173,11 +173,11 @@ struct ContentView: View { } ``` - + The code above sets up the SwiftUI LiveView. By default, the SwiftUI LiveView connects to any Phoenix app running on http://localhost:4000. - + @@ -207,7 +207,7 @@ Click the `start active scheme` button to build the After you start the active scheme, the simulator should open the iOS application and display `Hello from LiveView Native!` If you encounter any issues, see the Troubleshooting section below. - +
diff --git a/livebooks/markdown/forms-and-validation.md b/livebooks/markdown/forms-and-validation.md index afb5933c6..c2a7012a0 100644 --- a/livebooks/markdown/forms-and-validation.md +++ b/livebooks/markdown/forms-and-validation.md @@ -18,7 +18,7 @@ In the `core_components.swiftui.ex` file there's a `simple_form/1` component tha First, we'll see how to use this abstraction at a basic level, then later we'll dive deeper into how forms work under the hood in LiveView Native. - + ### A Basic Form @@ -105,7 +105,7 @@ We'll go into the internal implementation details later on, but for now you can If you need a refresher on forms in Phoenix, see the [Form Bindings](https://hexdocs.pm/phoenix_live_view/form-bindings.html) HexDoc documentation. - + ### Inputs @@ -274,7 +274,7 @@ We've already been using the two main functions, `simple_form/1` and `input/1`. in this section, we'll dive deeper into these abstractions so that you can build your own custom forms. - + ### Simple Form @@ -338,7 +338,7 @@ def simple_form(assigns) do end ``` - + ### Input @@ -417,7 +417,7 @@ Here's a list of valid options with links to their documentation: For more on the form compatible views see the [Interactive SwiftUI Views](https://hexdocs.pm/liveview-client-swiftui/interactive-swiftui-views.html) guide. - + ### Core Components vs Views @@ -441,7 +441,7 @@ Let's take the Slider view as an example. The Slider view accepts the `min` and end ``` - + ### Labels with Form Data @@ -608,7 +608,7 @@ Typically, you won't need to use these views directly and will instead rely upon Taking everything you've learned, you're going to create a more complex user form with data validation and error displaying. - + ### User Changeset diff --git a/livebooks/markdown/interactive-swiftui-views.md b/livebooks/markdown/interactive-swiftui-views.md index 6b27c5d1f..9f2a5dbb4 100644 --- a/livebooks/markdown/interactive-swiftui-views.md +++ b/livebooks/markdown/interactive-swiftui-views.md @@ -171,7 +171,7 @@ end - + ### Enter Your Solution Below @@ -348,7 +348,7 @@ In Phoenix, form elements must be inside of a form. Phoenix only captures events Phoenix Applications setup with LiveView native include a `core_components.ex` file. This file contains several components for building forms. Generally, We recommend using core components rather than the views. We're going to cover the views directly so you understand how to build forms from scratch and how we built the core components. However, in the [Forms and Validations](https://hexdocs.pm/live_view_native/forms-and-validation.html) reading we'll cover using core components. - + ### LiveForm @@ -363,7 +363,7 @@ Here's some example boilerplate for a `LiveForm`. The `id` attribute is required ``` - + ### Basic Example using TextField @@ -428,7 +428,7 @@ SwiftUI organizes interactive views in the [Controls and Indicators](https://dev We'll demonstrate how to work with a few common control and indicator views. - + ### Slider @@ -733,7 +733,7 @@ end - + ### Enter Your Solution Below diff --git a/livebooks/markdown/stylesheets.md b/livebooks/markdown/stylesheets.md index af66cda9f..8acb999ed 100644 --- a/livebooks/markdown/stylesheets.md +++ b/livebooks/markdown/stylesheets.md @@ -31,7 +31,7 @@ SwiftUI employs **modifiers** to style and customize views. In SwiftUI syntax, e Modifers can be applied through a LiveView Native Stylesheet and applying them through inline styles as described in the [LiveView Native Stylesheets](#liveview-native-stylesheets) section, or can be applied directly through the `style` attribute as described in the [Utility Styles](#utility-styles) section. - + ### SwiftUI Modifiers @@ -50,7 +50,7 @@ Text("Some Red Text") .font(.title) ``` - + ### Implicit Member Expression @@ -61,7 +61,7 @@ Text("Some Red Text") .foregroundStyle(Color.red) ``` - + ### LiveView Native Modifiers @@ -288,7 +288,7 @@ foregroundStyle(Color("MyColor")) Generally using the asset catalog is more performant and customizable than using custom RGB colors with the [Color](https://developer.apple.com/documentation/swiftui/color) struct. - + ### Your Turn: Custom Colors in the Asset Catalog @@ -296,19 +296,19 @@ Custom colors can be defined in the asset catalog (https://developer.apple.com/d To create a new color go to the `Assets` folder in your iOS app and create a new color set. - + ![](https://github.com/liveview-native/documentation_assets/blob/main/asset-catalogue-create-new-color-set.png?raw=true) - + To create a color set, enter the RGB values or a hexcode as shown in the image below. If you don't see the sidebar with color options, click the icon in the top-right of your Xcode app and click the **Show attributes inspector** icon shown highlighted in blue. - + ![](https://github.com/liveview-native/documentation_assets/blob/main/asset-catalogue-modify-my-color.png?raw=true) - + The defined color is now available for use within LiveView Native styles. However, the app needs to be re-compiled to pick up a new color set. @@ -388,7 +388,7 @@ Image("logo") However, LiveView Native does not support using SwiftUI views directly within a stylesheet. Instead, we have a few alternative options in cases like this where we want to use a view within a modifier. - + ### Using Members on a Given Type @@ -418,7 +418,7 @@ Which is simple to convert to the LiveView Native DSL using the rules we've alre end ``` - + ### Injecting a View @@ -455,7 +455,7 @@ Then use the `template` attribute on the view to be injected into the stylesheet You can find documentation and examples of modifiers on [Apple's SwiftUI documentation](https://developer.apple.com/documentation/swiftui) which is comprehensive and thorough, though it may feel unfamiliar at first for Elixir Developers when compared to HexDocs. - + ### Finding Modifiers diff --git a/livebooks/markdown/swiftui-views.md b/livebooks/markdown/swiftui-views.md index 7d31b3807..d91090939 100644 --- a/livebooks/markdown/swiftui-views.md +++ b/livebooks/markdown/swiftui-views.md @@ -64,7 +64,7 @@ end In a Phoenix application, these two modules would traditionally be in separate files. - + ### Embedding Templates @@ -234,7 +234,7 @@ end - + ### Enter Your Solution Below @@ -379,7 +379,7 @@ The SwiftUI [ScrollView](https://developer.apple.com/documentation/swiftui/scrol While `ScrollView` also works with typical `VStack` and `HStack` views, they are not optimal for large amounts of data. - + ### ScrollView with VStack @@ -524,7 +524,7 @@ end - + ### Enter Your Solution Below @@ -610,7 +610,7 @@ end The `Image` element is best for system images such as the built in [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) or images placed into the SwiftUI [asset catalogue](https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). - + ### System Images @@ -647,7 +647,7 @@ Then evaluate the following example and you should see this image in your simula Make sure to **rebuild the native application** to build the app with the updated asset catalog. - + ### Enter Your Solution Below diff --git a/livebooks/native-navigation.livemd b/livebooks/native-navigation.livemd index 0c28b55f3..fa832f7f3 100644 --- a/livebooks/native-navigation.livemd +++ b/livebooks/native-navigation.livemd @@ -20,7 +20,8 @@ Mix.install( ], pubsub_server: Server.PubSub, live_view: [signing_salt: "JSgdVVL6"], - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {0, 0, 0, 0}, port: 4000], + check_origin: false, secret_key_base: String.duplicate("a", 64), live_reload: [ patterns: [ diff --git a/livebooks/stylesheets.livemd b/livebooks/stylesheets.livemd index 309ae447e..17ad8d9e9 100644 --- a/livebooks/stylesheets.livemd +++ b/livebooks/stylesheets.livemd @@ -20,7 +20,8 @@ Mix.install( ], pubsub_server: Server.PubSub, live_view: [signing_salt: "JSgdVVL6"], - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {0, 0, 0, 0}, port: 4000], + check_origin: false, secret_key_base: String.duplicate("a", 64), live_reload: [ patterns: [ diff --git a/livebooks/swiftui-views.livemd b/livebooks/swiftui-views.livemd index 918396437..8a7873b26 100644 --- a/livebooks/swiftui-views.livemd +++ b/livebooks/swiftui-views.livemd @@ -20,7 +20,8 @@ Mix.install( ], pubsub_server: Server.PubSub, live_view: [signing_salt: "JSgdVVL6"], - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {0, 0, 0, 0}, port: 4000], + check_origin: false, secret_key_base: String.duplicate("a", 64), live_reload: [ patterns: [ From ec906ffec69eb9056d48492cf9c075f4395014f0 Mon Sep 17 00:00:00 2001 From: Brooklin Myers Date: Wed, 22 May 2024 15:29:27 -0400 Subject: [PATCH 22/62] Update livebooks/stylesheets.livemd Co-authored-by: Carson Katri Signed-off-by: Brooklin Myers --- livebooks/stylesheets.livemd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livebooks/stylesheets.livemd b/livebooks/stylesheets.livemd index 17ad8d9e9..e6bc4db73 100644 --- a/livebooks/stylesheets.livemd +++ b/livebooks/stylesheets.livemd @@ -417,7 +417,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! + Hello, from LiveView Native! """ end end From e6e0c4baac0f04f96cc99fa01a3c7ac76afa1247 Mon Sep 17 00:00:00 2001 From: Brooklin Myers Date: Wed, 22 May 2024 15:38:21 -0400 Subject: [PATCH 23/62] Update livebooks/native-navigation.livemd Co-authored-by: Carson Katri Signed-off-by: Brooklin Myers --- livebooks/native-navigation.livemd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livebooks/native-navigation.livemd b/livebooks/native-navigation.livemd index fa832f7f3..ecac3edf8 100644 --- a/livebooks/native-navigation.livemd +++ b/livebooks/native-navigation.livemd @@ -305,8 +305,8 @@ LiveView Native navigation mirrors the same navigation behavior you'll find on t Evaluate the example below and press each button. Notice that: -1. `redirect/2` triggers the `mount/3` callback re-establishes a socket connection. -2. `push_navigate/2` triggers the `mount/3` callbcak and re-uses the existing socket connection. +1. `redirect/2` triggers the `mount/3` callback and re-establishes a socket connection. +2. `push_navigate/2` triggers the `mount/3` callback and re-uses the existing socket connection. 3. `push_patch/2` does not trigger the `mount/3` callback, but does trigger the `handle_params/3` callback. This is often useful when using navigation to trigger page changes such as displaying a modal or overlay. You can see this for yourself using the following example. Click each of the buttons for redirect, navigate, and patch behavior. From caf8c1905df0471d97050f39c75ec8eb3ffae4ff Mon Sep 17 00:00:00 2001 From: Brooklin Myers Date: Wed, 22 May 2024 15:39:20 -0400 Subject: [PATCH 24/62] Update livebooks/forms-and-validation.livemd Co-authored-by: Carson Katri Signed-off-by: Brooklin Myers --- livebooks/forms-and-validation.livemd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd index 4b19dce2f..1edb54f4f 100644 --- a/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -78,7 +78,7 @@ Getting the most out of this material requires some understanding of the [Ecto]( ## Creating a Basic Form -The LiveView Native `mix lvn.install` task generates a [core_components.swiftui.ex](https://github.com/liveview-native/liveview-client-swiftui/blob/main/priv/templates/lvn.swiftui.gen/core_components.ex) for native SwiftUI function components similar to the [core_components.ex](https://github.com/phoenixframework/phoenix/blob/main/priv/templates/phx.gen.live/core_components.ex) file generated in a traditional phoenix application for web function components. +The LiveView Native `mix lvn.install` task generates a [core_components.swiftui.ex](https://github.com/liveview-native/liveview-client-swiftui/blob/main/priv/templates/lvn.swiftui.gen/core_components.ex) file for native SwiftUI function components similar to the [core_components.ex](https://github.com/phoenixframework/phoenix/blob/main/priv/templates/phx.gen.live/core_components.ex) file generated in a traditional phoenix application for web function components. See Phoenix's [Components and HEEx](https://hexdocs.pm/phoenix/components.html) HexDoc documentation if you need a primer on function components. From 29aa01e087ca41865bd32483eee0fba0472aa875 Mon Sep 17 00:00:00 2001 From: Brooklin Myers Date: Wed, 22 May 2024 15:43:12 -0400 Subject: [PATCH 25/62] Update livebooks/forms-and-validation.livemd Co-authored-by: Carson Katri Signed-off-by: Brooklin Myers --- livebooks/forms-and-validation.livemd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd index 1edb54f4f..965a8563b 100644 --- a/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -94,7 +94,7 @@ This code below demonstrates how the basic skeleton of a native and web form tha We'll break down and understand the individual parts of this form in a moment. -For now, evaluate the following example. Open the native form in your simulator, and open the web form on http://localhost:4000/. Enter some text into both forms, then submit them. Watch the logs in the cell below to see the printed params.' +For now, evaluate the following example. Open the native form in your simulator, and open the web form on http://localhost:4000/. Enter some text into both forms, then submit them. Watch the logs in the cell below to see the printed params. From a5028d083e7ec3cabb9b02cd89683ff1480683de Mon Sep 17 00:00:00 2001 From: Brooklin Myers Date: Wed, 22 May 2024 15:43:19 -0400 Subject: [PATCH 26/62] Update livebooks/forms-and-validation.livemd Co-authored-by: Carson Katri Signed-off-by: Brooklin Myers --- livebooks/forms-and-validation.livemd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd index 965a8563b..14119bffb 100644 --- a/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -132,7 +132,7 @@ defmodule ServerWeb.ExampleLive do @impl true def render(assigns) do ~H""" - <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> <.input field={@form[:value]} placeholder="Enter a value" /> <:actions> <.button type="submit"> From 07da913243b2d1140a6b2b98b189591a0f037dbc Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Wed, 22 May 2024 16:20:02 -0400 Subject: [PATCH 27/62] PR review changes --- livebooks/forms-and-validation.livemd | 4 +-- livebooks/markdown/native-navigation.md | 20 ++++++------ livebooks/stylesheets.livemd | 41 ++++++++++++------------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd index 14119bffb..9cabcc829 100644 --- a/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -90,7 +90,7 @@ First, we'll see how to use this abstraction at a basic level, then later we'll ### A Basic Form -This code below demonstrates how the basic skeleton of a native and web form that share event handlers for the `phx-submit` and `phx-change` handlers. +The code below demonstrates a basic form that uses the same event handlers for the `phx-change` and `phx-submit` events on both the web and native versions of the form. We'll break down and understand the individual parts of this form in a moment. @@ -178,7 +178,7 @@ The interface for the native `simple_form/1` and web `simple_form/1` is intentio ``` -We'll go into the internal implementation details later on, but for now you can treat these components as functionally identical. Both require a unique `id` and accept the `for` attribute that contains the [Phoenix.HTML.Form] datastructure containing form fields, error messages, and other form data. +We'll go into the internal implementation details later on, but for now you can treat these components as functionally identical. Both require a unique `id` and accept the `for` attribute that contains the [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) datastructure containing form fields, error messages, and other form data. If you need a refresher on forms in Phoenix, see the [Form Bindings](https://hexdocs.pm/phoenix_live_view/form-bindings.html) HexDoc documentation. diff --git a/livebooks/markdown/native-navigation.md b/livebooks/markdown/native-navigation.md index deced73c3..792bd2ee2 100644 --- a/livebooks/markdown/native-navigation.md +++ b/livebooks/markdown/native-navigation.md @@ -215,7 +215,7 @@ You can see this for yourself using the following example. Click each of the but ```elixir # This module built for example purposes to persist logs between mounting LiveViews. -defmodule PersistantLogs do +defmodule PersistentLogs do def get do :persistent_term.get(:logs) end @@ -229,7 +229,7 @@ defmodule PersistantLogs do end end -PersistantLogs.reset() +PersistentLogs.reset() defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] @@ -261,22 +261,22 @@ defmodule ServerWeb.ExampleLive do @impl true def mount(_params, _session, socket) do - PersistantLogs.put("MOUNT") + PersistentLogs.put("MOUNT") {:ok, assign(socket, socket_id: socket.id, connected: connected?(socket), - logs: PersistantLogs.get(), + logs: PersistentLogs.get(), live_view_pid: inspect(self()) )} end @impl true def handle_params(_params, _url, socket) do - PersistantLogs.put("HANDLE PARAMS") + PersistentLogs.put("HANDLE PARAMS") - {:noreply, assign(socket, :logs, PersistantLogs.get())} + {:noreply, assign(socket, :logs, PersistentLogs.get())} end @impl true @@ -285,18 +285,18 @@ defmodule ServerWeb.ExampleLive do @impl true def handle_event("redirect", _params, socket) do - PersistantLogs.reset() - PersistantLogs.put("--REDIRECTING--") + PersistentLogs.reset() + PersistentLogs.put("--REDIRECTING--") {:noreply, redirect(socket, to: "/")} end def handle_event("navigate", _params, socket) do - PersistantLogs.put("---NAVIGATING---") + PersistentLogs.put("---NAVIGATING---") {:noreply, push_navigate(socket, to: "/")} end def handle_event("patch", _params, socket) do - PersistantLogs.put("----PATCHING----") + PersistentLogs.put("----PATCHING----") {:noreply, push_patch(socket, to: "/")} end end diff --git a/livebooks/stylesheets.livemd b/livebooks/stylesheets.livemd index e6bc4db73..64a124e3b 100644 --- a/livebooks/stylesheets.livemd +++ b/livebooks/stylesheets.livemd @@ -492,16 +492,16 @@ However, LiveView Native does not support using SwiftUI views directly within a ### Using Members on a Given Type -We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, if you look at the [clipShape](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) documentation you'll notice it accepts the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. This type defines the [circle](https://developer.apple.com/documentation/swiftui/shape/circle) property which we can use since it's equivalent to the [Circle](https://developer.apple.com/documentation/swiftui/circle) view for our purposes. +We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, the [Getting standard shapes](https://developer.apple.com/documentation/swiftui/shape#getting-standard-shapes) documentation describes methods for accessing standard shapes. For example, we can use `Circle.circle` for the circle shape. -We can use `Shape.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. +We can use `Circle.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. ```swift Image("logo") - .clipShape(Shape.circle) + .clipShape(Circle.circle) ``` -Using implicit member expression, we can simplify this code to the following: +However, in LiveView Native we only support using implicit member expression syntax, so instead of `Circle.circle`, we only write `.circle`. ```swift Image("logo") @@ -576,27 +576,23 @@ You're going to convert the following SwiftUI code into a LiveView Native templa ```elixir - VStack { - VStack(alignment: .leading) { - Text("Turtle Rock") - .font(.title) - HStack { - Text("Joshua Tree National Park") - Spacer() - Text("California") - } - .font(.subheadline) - - Divider() - - Text("About Turtle Rock") - .font(.title2) - Text("Descriptive text goes here") +VStack(alignment: .leading) { + Text("Turtle Rock") + .font(.title) + HStack { + Text("Joshua Tree National Park") + Spacer() + Text("California") } - .padding() + .font(.subheadline) - Spacer() + Divider() + + Text("About Turtle Rock") + .font(.title2) + Text("Descriptive text goes here") } +.padding() ```
@@ -608,6 +604,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" + Turtle Rock From 75c520ccb34806c5116c2c3e5ba2cc0aae39472d Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Wed, 22 May 2024 16:49:34 -0400 Subject: [PATCH 28/62] add config for physical devices --- livebooks/create-a-swiftui-application.livemd | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/livebooks/create-a-swiftui-application.livemd b/livebooks/create-a-swiftui-application.livemd index f7ac99e7e..0a8272acf 100644 --- a/livebooks/create-a-swiftui-application.livemd +++ b/livebooks/create-a-swiftui-application.livemd @@ -237,6 +237,30 @@ struct ContentView: View { +
+(Optional) configuration for using a physical device + +You may wish to use a physical device instead of a simulator. If so, you'll need to change the `development` configuration to use your machine's IP address instead of localhost as seen in the example below. + +```elixir +#LiveView( + .automatic( + development: URL(string: "http://192.168.1.xxx:4000")!, + production: URL(string: "https://example.com")! + ) +) +``` + +Make sure to replace `192.168.1.xxx` with your IP address. You can run the following command in the IEx shell to find your machine's IP address: + +```elixir +iex> IO.puts :os.cmd(~c[ipconfig getifaddr en0]) +``` + +
+ + + The code above sets up the SwiftUI LiveView. By default, the SwiftUI LiveView connects to any Phoenix app running on http://localhost:4000. From 0103c5eafb0c8bb259b278fc0e53302602eab2bf Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Thu, 23 May 2024 11:26:59 -0400 Subject: [PATCH 29/62] run gen doc commands --- .../markdown/create-a-swiftui-application.md | 40 ++++++++++++----- livebooks/markdown/forms-and-validation.md | 10 ++--- livebooks/markdown/native-navigation.md | 24 +++++------ livebooks/markdown/stylesheets.md | 43 +++++++++---------- livebooks/markdown/swiftui-views.md | 29 ++----------- livebooks/swiftui-views.livemd | 38 ++-------------- mix.lock | 2 +- 7 files changed, 76 insertions(+), 110 deletions(-) diff --git a/livebooks/markdown/create-a-swiftui-application.md b/livebooks/markdown/create-a-swiftui-application.md index ff75aa1c7..952aae79c 100644 --- a/livebooks/markdown/create-a-swiftui-application.md +++ b/livebooks/markdown/create-a-swiftui-application.md @@ -64,16 +64,13 @@ Choose options for your new project that match the following image, then click * * **Product Name:** The name of the application. This can be any valid name. We've chosen **Guides**. * **Organization Identifier:** A reverse DNS string that uniquely identifies your organization. If you don't have a company identifier, [Apple recommends](https://developer.apple.com/documentation/xcode/creating-an-xcode-project-for-an-app) using **com.example.your_name** where **your_name** is your organization or personal name. -* **Interface:**: The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. +* **Interface:** The Xcode user interface to use. Select **SwiftUI** to create an app that uses the SwiftUI app lifecycle. * **Language:** Determines which language Xcode should use for the project. Select **Swift**. - - - Select an appropriate folder location to store the iOS project, then click **Create.** @@ -118,7 +115,7 @@ At this point, you'll need to enable permissions for plugins used by LiveView Na -You'll also need to manually navigate to the error tab (shown below) to trust and enable packages. Click on each error to trigger a prompt. Select **Trust & Enable All** to enable the plugin. +You'll also need to manually navigate to the error tab (shown below) to trust and enable packages. Click on each error to trigger a prompt. Select **Trust & Enable** to enable the plugin. The specific plugins are subject to change. At the time of writing you need to enable `LiveViewNativeStylesheetMacros`, `LiveViewNativeMacros`, and `CasePathMacros` as shown in the images below. @@ -140,7 +137,7 @@ The [LiveView Native Live Form](https://github.com/liveview-native/liveview-nati To install LiveView Native Form, we need to add the [liveview-native-live-form](https://github.com/liveview-native/liveview-native-live-form) SwiftUI package to our iOS application. The steps will be mostly similar to what you have already setup with the `liveview-client-swiftui` package. -Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README to add the `liveview-native-live-form` package to the SwiftUI application you created in the [Create a SwiftUI Application](https://hexdocs.pm/live_view_native_swiftui/create-a-swiftui-application.html) guide. +Follow the [LiveView Native Form Installation Guide](https://github.com/liveview-native/liveview-native-live-form?tab=readme-ov-file#liveviewnativeliveform) on that project's README to add the `liveview-native-live-form` package to the SwiftUI application. Come back to this guide and continue after you have finished the installation process. @@ -159,9 +156,9 @@ import LiveViewNative struct ContentView: View { var body: some View { - LiveView(.automatic( - development: .localhost(path: "/"), - production: .custom(URL(string: "https://example.com/")!) + #LiveView(.automatic( + development: .localhost, + production: URL(string: "https://example.com/")! )) } } @@ -175,6 +172,29 @@ struct ContentView: View { +### (Optional) configuration for using a physical device + +You may wish to use a physical device instead of a simulator. If so, you'll need to change the `development` configuration to use your machine's IP address instead of localhost as seen in the example below. + +```elixir +#LiveView( + .automatic( + development: URL(string: "http://192.168.1.xxx:4000")!, + production: URL(string: "https://example.com")! + ) +) +``` + +Make sure to replace `192.168.1.xxx` with your IP address. You can run the following command in the IEx shell to find your machine's IP address: + +```elixir +iex> IO.puts :os.cmd(~c[ipconfig getifaddr en0]) +``` + + + + + The code above sets up the SwiftUI LiveView. By default, the SwiftUI LiveView connects to any Phoenix app running on http://localhost:4000. @@ -199,7 +219,7 @@ graph LR; ## Start the Active Scheme -Click the `start active scheme` button to build the project and run it on the iOS simulator. +Click the run button to build the project and run it on the iOS simulator. Alternatively you may go to `Product` in the top menu then press `Run`. > A [build scheme](https://developer.apple.com/documentation/xcode/build-system) contains a list of targets to build and any configuration and environment details that affect the selected action. When you build and run an app, the scheme tells Xcode what launch arguments to pass to the app. > diff --git a/livebooks/markdown/forms-and-validation.md b/livebooks/markdown/forms-and-validation.md index c2a7012a0..57974e255 100644 --- a/livebooks/markdown/forms-and-validation.md +++ b/livebooks/markdown/forms-and-validation.md @@ -10,7 +10,7 @@ Getting the most out of this material requires some understanding of the [Ecto]( ## Creating a Basic Form -The LiveView Native `mix lvn.install` task generates a [core_components.swiftui.ex](https://github.com/liveview-native/liveview-client-swiftui/blob/main/priv/templates/lvn.swiftui.gen/core_components.ex) for native SwiftUI function components similar to the [core_components.ex](https://github.com/phoenixframework/phoenix/blob/main/priv/templates/phx.gen.live/core_components.ex) file generated in a traditional phoenix application for web function components. +The LiveView Native `mix lvn.install` task generates a [core_components.swiftui.ex](https://github.com/liveview-native/liveview-client-swiftui/blob/main/priv/templates/lvn.swiftui.gen/core_components.ex) file for native SwiftUI function components similar to the [core_components.ex](https://github.com/phoenixframework/phoenix/blob/main/priv/templates/phx.gen.live/core_components.ex) file generated in a traditional phoenix application for web function components. See Phoenix's [Components and HEEx](https://hexdocs.pm/phoenix/components.html) HexDoc documentation if you need a primer on function components. @@ -22,11 +22,11 @@ First, we'll see how to use this abstraction at a basic level, then later we'll ### A Basic Form -This code below demonstrates how the basic skeleton of a native and web form that share event handlers for the `phx-submit` and `phx-change` handlers. +The code below demonstrates a basic form that uses the same event handlers for the `phx-change` and `phx-submit` events on both the web and native versions of the form. We'll break down and understand the individual parts of this form in a moment. -For now, evaluate the following example. Open the native form in your simulator, and open the web form on http://localhost:4000/. Enter some text into both forms, then submit them. Watch the logs in the cell below to see the printed params.' +For now, evaluate the following example. Open the native form in your simulator, and open the web form on http://localhost:4000/. Enter some text into both forms, then submit them. Watch the logs in the cell below to see the printed params. @@ -60,7 +60,7 @@ defmodule ServerWeb.ExampleLive do @impl true def render(assigns) do ~H""" - <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> <.input field={@form[:value]} placeholder="Enter a value" /> <:actions> <.button type="submit"> @@ -101,7 +101,7 @@ The interface for the native `simple_form/1` and web `simple_form/1` is intentio ``` -We'll go into the internal implementation details later on, but for now you can treat these components as functionally identical. Both require a unique `id` and accept the `for` attribute that contains the [Phoenix.HTML.Form] datastructure containing form fields, error messages, and other form data. +We'll go into the internal implementation details later on, but for now you can treat these components as functionally identical. Both require a unique `id` and accept the `for` attribute that contains the [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) datastructure containing form fields, error messages, and other form data. If you need a refresher on forms in Phoenix, see the [Form Bindings](https://hexdocs.pm/phoenix_live_view/form-bindings.html) HexDoc documentation. diff --git a/livebooks/markdown/native-navigation.md b/livebooks/markdown/native-navigation.md index 792bd2ee2..c09e457bf 100644 --- a/livebooks/markdown/native-navigation.md +++ b/livebooks/markdown/native-navigation.md @@ -205,8 +205,8 @@ LiveView Native navigation mirrors the same navigation behavior you'll find on t Evaluate the example below and press each button. Notice that: -1. `redirect/2` triggers the `mount/3` callback re-establishes a socket connection. -2. `push_navigate/2` triggers the `mount/3` callbcak and re-uses the existing socket connection. +1. `redirect/2` triggers the `mount/3` callback and re-establishes a socket connection. +2. `push_navigate/2` triggers the `mount/3` callback and re-uses the existing socket connection. 3. `push_patch/2` does not trigger the `mount/3` callback, but does trigger the `handle_params/3` callback. This is often useful when using navigation to trigger page changes such as displaying a modal or overlay. You can see this for yourself using the following example. Click each of the buttons for redirect, navigate, and patch behavior. @@ -215,7 +215,7 @@ You can see this for yourself using the following example. Click each of the but ```elixir # This module built for example purposes to persist logs between mounting LiveViews. -defmodule PersistentLogs do +defmodule PersistantLogs do def get do :persistent_term.get(:logs) end @@ -229,7 +229,7 @@ defmodule PersistentLogs do end end -PersistentLogs.reset() +PersistantLogs.reset() defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] @@ -261,22 +261,22 @@ defmodule ServerWeb.ExampleLive do @impl true def mount(_params, _session, socket) do - PersistentLogs.put("MOUNT") + PersistantLogs.put("MOUNT") {:ok, assign(socket, socket_id: socket.id, connected: connected?(socket), - logs: PersistentLogs.get(), + logs: PersistantLogs.get(), live_view_pid: inspect(self()) )} end @impl true def handle_params(_params, _url, socket) do - PersistentLogs.put("HANDLE PARAMS") + PersistantLogs.put("HANDLE PARAMS") - {:noreply, assign(socket, :logs, PersistentLogs.get())} + {:noreply, assign(socket, :logs, PersistantLogs.get())} end @impl true @@ -285,18 +285,18 @@ defmodule ServerWeb.ExampleLive do @impl true def handle_event("redirect", _params, socket) do - PersistentLogs.reset() - PersistentLogs.put("--REDIRECTING--") + PersistantLogs.reset() + PersistantLogs.put("--REDIRECTING--") {:noreply, redirect(socket, to: "/")} end def handle_event("navigate", _params, socket) do - PersistentLogs.put("---NAVIGATING---") + PersistantLogs.put("---NAVIGATING---") {:noreply, push_navigate(socket, to: "/")} end def handle_event("patch", _params, socket) do - PersistentLogs.put("----PATCHING----") + PersistantLogs.put("----PATCHING----") {:noreply, push_patch(socket, to: "/")} end end diff --git a/livebooks/markdown/stylesheets.md b/livebooks/markdown/stylesheets.md index 8acb999ed..de66ad371 100644 --- a/livebooks/markdown/stylesheets.md +++ b/livebooks/markdown/stylesheets.md @@ -322,7 +322,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! + Hello, from LiveView Native! """ end end @@ -392,16 +392,16 @@ However, LiveView Native does not support using SwiftUI views directly within a ### Using Members on a Given Type -We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, if you look at the [clipShape](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) documentation you'll notice it accepts the [Shape](https://developer.apple.com/documentation/swiftui/shape) type. This type defines the [circle](https://developer.apple.com/documentation/swiftui/shape/circle) property which we can use since it's equivalent to the [Circle](https://developer.apple.com/documentation/swiftui/circle) view for our purposes. +We can't use the [Circle](https://developer.apple.com/documentation/swiftui/circle) view directly. However, the [Getting standard shapes](https://developer.apple.com/documentation/swiftui/shape#getting-standard-shapes) documentation describes methods for accessing standard shapes. For example, we can use `Circle.circle` for the circle shape. -We can use `Shape.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. +We can use `Circle.circle` instead of the `Circle` view. So, the following code is equivalent to the example above. ```swift Image("logo") - .clipShape(Shape.circle) + .clipShape(Circle.circle) ``` -Using implicit member expression, we can simplify this code to the following: +However, in LiveView Native we only support using implicit member expression syntax, so instead of `Circle.circle`, we only write `.circle`. ```swift Image("logo") @@ -476,27 +476,23 @@ You're going to convert the following SwiftUI code into a LiveView Native templa ```elixir - VStack { - VStack(alignment: .leading) { - Text("Turtle Rock") - .font(.title) - HStack { - Text("Joshua Tree National Park") - Spacer() - Text("California") - } - .font(.subheadline) - - Divider() - - Text("About Turtle Rock") - .font(.title2) - Text("Descriptive text goes here") +VStack(alignment: .leading) { + Text("Turtle Rock") + .font(.title) + HStack { + Text("Joshua Tree National Park") + Spacer() + Text("California") } - .padding() + .font(.subheadline) - Spacer() + Divider() + + Text("About Turtle Rock") + .font(.title2) + Text("Descriptive text goes here") } +.padding() ``` ### Example Solution @@ -507,6 +503,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" + Turtle Rock diff --git a/livebooks/markdown/swiftui-views.md b/livebooks/markdown/swiftui-views.md index d91090939..25261eb5a 100644 --- a/livebooks/markdown/swiftui-views.md +++ b/livebooks/markdown/swiftui-views.md @@ -6,7 +6,7 @@ LiveView Native aims to use minimal SwiftUI code and instead rely on the same patterns used in traditional Phoenix LiveView development as much as possible. We'll primarily use The LiveView Naive SwiftUI DSL (Domain Specific Language) to build the native template. -This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common use cases and provide practical examples of how to build native UIs. This lesson is like a recipe book you can refer to whenever you need an example of a particular SwiftUI view. +This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common use cases and provide practical examples of how to build native UIs. This lesson is like a recipe book you can refer to whenever you need an example of a particular SwiftUI view. In addition, we'll cover the LiveView Native DSL and teach you how to convert SwiftUI examples into the LiveView Native DSL. Once you understand how to convert SwiftUI code into the LiveView Native DSL, you'll have the knowledge you need to learn from the plethora of [SwiftUI resources available](https://developer.apple.com/tutorials/swiftui/creating-and-combining-views). @@ -429,32 +429,11 @@ end ### Optimized ScrollView with LazyHStack and LazyVStack -`VStack` and `HStack` are inefficient for large amounts of data because they render every child view. To demonstrate this, evaluate the example below. You should experience lag when you attempt to scroll. - - - -```elixir -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - Item <%= n %> - - - """ - end -end -``` - -You can use the Lazy views to resolve the performance problem for large amounts of data. Lazy views only create items as needed, meaning the client won't render them until they are on the screen. +`VStack` and `HStack` are inefficient for large amounts of data because they render every child view. You can use the Lazy views to resolve the performance problem for large amounts of data. Lazy views only create items as needed, meaning the client won't render them until they are on the screen. The following example demonstrates how using `LazyVStack` instead of `VStack` resolves the performance issue. Evaluate the cell and notice the improved performance in your simulator. - + ```elixir defmodule ServerWeb.ExampleLive.SwiftUI do @@ -465,7 +444,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do ~LVN""" - Item <%= n %> + Item <%= n %> """ diff --git a/livebooks/swiftui-views.livemd b/livebooks/swiftui-views.livemd index 8a7873b26..efe3f93c1 100644 --- a/livebooks/swiftui-views.livemd +++ b/livebooks/swiftui-views.livemd @@ -61,7 +61,7 @@ Mix.install( LiveView Native aims to use minimal SwiftUI code and instead rely on the same patterns used in traditional Phoenix LiveView development as much as possible. We'll primarily use The LiveView Naive SwiftUI DSL (Domain Specific Language) to build the native template. -This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common use cases and provide practical examples of how to build native UIs. This lesson is like a recipe book you can refer to whenever you need an example of a particular SwiftUI view. +This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We'll cover common use cases and provide practical examples of how to build native UIs. This lesson is like a recipe book you can refer to whenever you need an example of a particular SwiftUI view. In addition, we'll cover the LiveView Native DSL and teach you how to convert SwiftUI examples into the LiveView Native DSL. Once you understand how to convert SwiftUI code into the LiveView Native DSL, you'll have the knowledge you need to learn from the plethora of [SwiftUI resources available](https://developer.apple.com/tutorials/swiftui/creating-and-combining-views). @@ -584,41 +584,11 @@ import Kernel ### Optimized ScrollView with LazyHStack and LazyVStack -`VStack` and `HStack` are inefficient for large amounts of data because they render every child view. To demonstrate this, evaluate the example below. You should experience lag when you attempt to scroll. - - - -```elixir -require Server.Livebook -import Server.Livebook -import Kernel, except: [defmodule: 2] - -defmodule ServerWeb.ExampleLive.SwiftUI do - use LiveViewNative.Component, - format: :swiftui - - def render(assigns, _interface) do - ~LVN""" - - - Item <%= n %> - - - """ - end -end -|> Server.SmartCells.RenderComponent.register() - -import Server.Livebook, only: [] -import Kernel -:ok -``` - -You can use the Lazy views to resolve the performance problem for large amounts of data. Lazy views only create items as needed, meaning the client won't render them until they are on the screen. +`VStack` and `HStack` are inefficient for large amounts of data because they render every child view. You can use the Lazy views to resolve the performance problem for large amounts of data. Lazy views only create items as needed, meaning the client won't render them until they are on the screen. The following example demonstrates how using `LazyVStack` instead of `VStack` resolves the performance issue. Evaluate the cell and notice the improved performance in your simulator. - + ```elixir require Server.Livebook @@ -633,7 +603,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do ~LVN""" - Item <%= n %> + Item <%= n %> """ diff --git a/mix.lock b/mix.lock index e018f17ab..030dc02cb 100644 --- a/mix.lock +++ b/mix.lock @@ -9,7 +9,7 @@ "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "live_view_native": {:git, "https://github.com/liveview-native/live_view_native.git", "65fc88252a21342116ed4ce4791e09c49d9d4c32", []}, "live_view_native_stylesheet": {:hex, :live_view_native_stylesheet, "0.3.0-rc.1", "6675fca5fbaf23805a6a7b4214c0600ad4f996f31a9dccb48fa765b3e2e93455", [:mix], [{:live_view_native, "~> 0.3.0-rc.1", [hex: :live_view_native, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "08e6f51b6708340f49d39e64f782dbfc12c73c889f234822cbbab4868e84862f"}, - "live_view_native_test": {:git, "https://github.com/liveview-native/live_view_native_test.git", "539ae931fa3936f3ee2f73ffa11f7100fe6554db", [branch: "main"]}, + "live_view_native_test": {:git, "https://github.com/liveview-native/live_view_native_test.git", "f36efa463e172df27d50ab0bcbd16f2e59e6c05b", [tag: "v0.3.0"]}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_eex": {:hex, :makeup_eex, "0.1.2", "93a5ef3d28ed753215dba2d59cb40408b37cccb4a8205e53ef9b5319a992b700", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.16 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.1.0 or ~> 1.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "6140eafb28215ad7182282fd21d9aa6dcffbfbe0eb876283bc6b768a6c57b0c3"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, From 6cab4fe7ba5843d33fc7cb99e547d66480de0300 Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Mon, 27 May 2024 14:37:07 -0400 Subject: [PATCH 30/62] change skip-gen-docs command name --- mix.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 77bb4ab16..0cad00864 100644 --- a/mix.exs +++ b/mix.exs @@ -155,10 +155,10 @@ defmodule LiveViewNative.SwiftUI.MixProject do defp various_docs(args) do {opts, _, _} = OptionParser.parse(args, - strict: [skip_gen_doc: :boolean, skip_livebooks: :boolean] + strict: [skip_gen_docs: :boolean, skip_livebooks: :boolean] ) - unless opts[:skip_gen_doc], do: Mix.Task.run("lvn.swiftui.gen.docs") + unless opts[:skip_gen_docs], do: Mix.Task.run("lvn.swiftui.gen.docs") unless opts[:skip_livebooks], do: Mix.Task.run("lvn.swiftui.gen.livemarkdown") Mix.Task.run("docs") end From 89af0fa664cfbf91e5aee8d959f4cc8ea0bf598d Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Thu, 30 May 2024 14:23:06 -0400 Subject: [PATCH 31/62] add link component explanation to native navigation guide --- livebooks/native-navigation.livemd | 137 +++++++++++++++++++++++++---- livebooks/stylesheets.livemd | 7 +- 2 files changed, 124 insertions(+), 20 deletions(-) diff --git a/livebooks/native-navigation.livemd b/livebooks/native-navigation.livemd index ecac3edf8..28f2f8ad5 100644 --- a/livebooks/native-navigation.livemd +++ b/livebooks/native-navigation.livemd @@ -134,7 +134,41 @@ We've created the same example of navigating between the `Main` and `About` page Evaluate **both** of the code cells below and click on the `NavigationLink` in your simulator to navigate between the two views. - + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.HomeLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the home page + + To about + + """ + end +end + +defmodule ServerWeb.HomeLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + + ```elixir require Server.Livebook @@ -148,7 +182,7 @@ defmodule ServerWeb.AboutLive.SwiftUI do ~LVN""" You are on the about page - To Home + To home """ end @@ -168,7 +202,15 @@ import Kernel :ok ``` - +The `destination` attribute works the same as the `navigate` attribute on the web. The current LiveView will shut down, and a new one will mount without re-establishing a new socket connection. + +## Link Component + +The [link](https://github.com/liveview-native/liveview-client-swiftui/blob/748389d11007503273a96d28c3f0915ee68584bb/lib/live_view_native/swiftui/component.ex#L196) component wraps the `NavigationLink` and `Link` view. It accepts both the `navigation` and `href` attributes depending on the type of navigation you want to trigger. `navigation` preserves the socket connection and is best used for navigation within the application. `href` uses the `Link` view to navigate to an external resource using the native browser. + +Evaluate **both** of the code cells below and click on the `NavigationLink` in your simulator to navigate between the two views. + + ```elixir require Server.Livebook @@ -180,10 +222,8 @@ defmodule ServerWeb.HomeLive.SwiftUI do def render(assigns) do ~LVN""" - You are on the main page - - To About - + You are on the home page + <.link navigate="about" >To about """ end end @@ -202,7 +242,70 @@ import Kernel :ok ``` -The `destination` attribute works the same as the `navigate` attribute on the web. The current LiveView will shut down, and a new one will mount without re-establishing a new socket connection. + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + You are on the about page + <.link navigate="home" >To home + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +The `href` attribute is best used for external sites that the device will open in the native browser. Evaluate the example below and click the link to navigate to https://www.google.com. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + <.link href="https://www.google.com">To Google + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` ## Push Navigation @@ -212,25 +315,25 @@ These functions are preferable over `NavigationLink` views when you want to shar Evaluate **both** of the code cells below and click on the `Button` view in your simulator that triggers the `handle_event/3` navigation handler to navigate between the two views. - + ```elixir require Server.Livebook import Server.Livebook import Kernel, except: [defmodule: 2] -defmodule ServerWeb.MainLive.SwiftUI do +defmodule ServerWeb.HomeLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] def render(assigns) do ~LVN""" - You are on the Main Page - + You are on the home page + """ end end -defmodule ServerWeb.MainLive do +defmodule ServerWeb.HomeLive do use ServerWeb, :live_view use ServerNative, :live_view @@ -249,7 +352,7 @@ import Kernel :ok ``` - + ```elixir require Server.Livebook @@ -261,8 +364,8 @@ defmodule ServerWeb.AboutLive.SwiftUI do def render(assigns) do ~LVN""" - You are on the About Page - + You are on the about page + """ end end @@ -309,7 +412,7 @@ Evaluate the example below and press each button. Notice that: 2. `push_navigate/2` triggers the `mount/3` callback and re-uses the existing socket connection. 3. `push_patch/2` does not trigger the `mount/3` callback, but does trigger the `handle_params/3` callback. This is often useful when using navigation to trigger page changes such as displaying a modal or overlay. -You can see this for yourself using the following example. Click each of the buttons for redirect, navigate, and patch behavior. +You can see this for yourself using the following example. Click each of the buttons for redirect, navigate, and patch behavior. Try to understand each navigation type, and which callback functions the navigation type triggers. diff --git a/livebooks/stylesheets.livemd b/livebooks/stylesheets.livemd index 64a124e3b..4d5672df9 100644 --- a/livebooks/stylesheets.livemd +++ b/livebooks/stylesheets.livemd @@ -5,7 +5,8 @@ notebook_path = __ENV__.file |> String.split("#") |> hd() Mix.install( [ - {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} + {:kino_live_view_native, path: "../kino_live_view_native"} + # {:kino_live_view_native, github: "liveview-native/kino_live_view_native"} ], config: [ server: [ @@ -405,7 +406,7 @@ The defined color is now available for use within LiveView Native styles. Howeve Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator. - + ```elixir require Server.Livebook @@ -417,7 +418,7 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - Hello, from LiveView Native! + Hello, from LiveView Native! """ end end From fb8060275fe3a1eb73cd7146a8c4398ce245951b Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Thu, 27 Jun 2024 10:40:09 -0400 Subject: [PATCH 32/62] review getting started,forms and validation, and interactive swiftui views --- livebooks/forms-and-validation.livemd | 9 +++++---- livebooks/getting-started.livemd | 2 +- livebooks/interactive-swiftui-views.livemd | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/livebooks/forms-and-validation.livemd b/livebooks/forms-and-validation.livemd index 9cabcc829..d01a06776 100644 --- a/livebooks/forms-and-validation.livemd +++ b/livebooks/forms-and-validation.livemd @@ -90,7 +90,7 @@ First, we'll see how to use this abstraction at a basic level, then later we'll ### A Basic Form -The code below demonstrates a basic form that uses the same event handlers for the `phx-change` and `phx-submit` events on both the web and native versions of the form. +The code below demonstrates a basic form that uses the same event handlers for the `phx-change` and `phx-submit` events on both the web and native versions of the form. We'll break down and understand the individual parts of this form in a moment. @@ -132,7 +132,7 @@ defmodule ServerWeb.ExampleLive do @impl true def render(assigns) do ~H""" - <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> + <.simple_form for={@form} id="form" phx-submit="submit" phx-change="validate"> <.input field={@form[:value]} placeholder="Enter a value" /> <:actions> <.button type="submit"> @@ -544,7 +544,7 @@ Sometimes you may wish to use data within the form separately as part of your UI Here's an example showing how to have a dynamic label based on the Stepper view's current value. Evaluate the example below and run it in your simulator. - + ```elixir require Server.Livebook @@ -585,7 +585,8 @@ defmodule ServerWeb.ExampleLive do def render(assigns), do: ~H"" @impl true - def handle_event("submit", params, socket) do + def handle_event("submit", %{"my_form" => params}, socket) do + IO.inspect(params, label: "PARAMS") {:noreply, assign(socket, form: to_form(params, as: "my_form"))} end diff --git a/livebooks/getting-started.livemd b/livebooks/getting-started.livemd index 8b0b13a8c..ab2c7bd3f 100644 --- a/livebooks/getting-started.livemd +++ b/livebooks/getting-started.livemd @@ -85,7 +85,7 @@ If you are not already running this guide in Livebook, click on the "Run in Live Then, you can evaluate the following smart cell and visit http://localhost:4000 to ensure this Livebook works correctly. - + ```elixir require Server.Livebook diff --git a/livebooks/interactive-swiftui-views.livemd b/livebooks/interactive-swiftui-views.livemd index e7aef8f2e..39f198839 100644 --- a/livebooks/interactive-swiftui-views.livemd +++ b/livebooks/interactive-swiftui-views.livemd @@ -476,7 +476,7 @@ The `LiveForm` view must wrap views to capture events from the `phx-change` or ` Here's some example boilerplate for a `LiveForm`. The `id` attribute is required. -```heex +```html Button Text @@ -912,7 +912,7 @@ end ### Enter Your Solution Below - + ```elixir require Server.Livebook @@ -922,7 +922,7 @@ import Kernel, except: [defmodule: 2] defmodule ServerWeb.ExampleLive.SwiftUI do use ServerNative, [:render_component, format: :swiftui] - def render(assig) do + def render(assigns) do ~LVN""" """ From d7403678b5e2ca4563c5a2354fe5d73b3651ceac Mon Sep 17 00:00:00 2001 From: BrooklinJazz Date: Thu, 27 Jun 2024 10:46:42 -0400 Subject: [PATCH 33/62] quick test for brian --- livebooks/native-navigation.livemd | 15 ++++- livebooks/stylesheets.livemd | 97 +++++++++++++++++++++++++++--- 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/livebooks/native-navigation.livemd b/livebooks/native-navigation.livemd index 28f2f8ad5..76bb5e7c4 100644 --- a/livebooks/native-navigation.livemd +++ b/livebooks/native-navigation.livemd @@ -414,7 +414,7 @@ Evaluate the example below and press each button. Notice that: You can see this for yourself using the following example. Click each of the buttons for redirect, navigate, and patch behavior. Try to understand each navigation type, and which callback functions the navigation type triggers. - + ```elixir require Server.Livebook @@ -488,7 +488,14 @@ defmodule ServerWeb.ExampleLive do @impl true def render(assigns), - do: ~H"" + do: ~H""" + + """ + + def handle_event("do-thing", _params, socket) do + IO.inspect("DOING THING") + {:noreply, socket} + end @impl true def handle_event("redirect", _params, socket) do @@ -513,3 +520,7 @@ import Server.Livebook, only: [] import Kernel :ok ``` + +```elixir +dle +``` diff --git a/livebooks/stylesheets.livemd b/livebooks/stylesheets.livemd index 4d5672df9..33a6bb4ed 100644 --- a/livebooks/stylesheets.livemd +++ b/livebooks/stylesheets.livemd @@ -62,6 +62,86 @@ Mix.install( In this guide, you'll learn how to use stylesheets to customize the appearance of your LiveView Native Views. You'll also learn about the inner workings of how LiveView Native uses stylesheets to implement modifiers, and how those modifiers style and customize SwiftUI Views. By the end of this lesson, you'll have the fundamentals you need to create beautiful native UIs. +## Test for Brian + +Hey Brian, I made this quick test to make it easier to jump in to debugging this i + + + +app.js isn't loading when going to http://localhost:4000 so click events aren't working on the web. + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), + do: ~H""" + + """ + + def handle_event("ping", _params, socket) do + IO.inspect("PING") + {:noreply, socket} + end +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + +Custom Colors aren't being picked up in the stylesheet at http://localhost:4000/assets/app.swiftui.styles + + + +```elixir +require Server.Livebook +import Server.Livebook +import Kernel, except: [defmodule: 2] + +defmodule ServerWeb.ExampleLive.SwiftUI do + use ServerNative, [:render_component, format: :swiftui] + + def render(assigns) do + ~LVN""" + Hello, from LiveView Native! + """ + end +end + +defmodule ServerWeb.ExampleLive do + use ServerWeb, :live_view + use ServerNative, :live_view + + @impl true + def render(assigns), do: ~H"" +end +|> Server.SmartCells.LiveViewNative.register("/") + +import Server.Livebook, only: [] +import Kernel +:ok +``` + ## The Stylesheet AST LiveView Native parses through your application at compile time to create a stylesheet AST representation of all the styles in your application. This stylesheet AST is used by the LiveView Native Client application when rendering the view hierarchy to apply modifiers to a given view. @@ -337,7 +417,7 @@ foregroundStyle(Color(.sRGB, red: 0.4627, green: 0.8392, blue: 1.0)) Evaluate the example below to see the custom color in your simulator. - + ```elixir require Server.Livebook @@ -349,9 +429,12 @@ defmodule ServerWeb.ExampleLive.SwiftUI do def render(assigns) do ~LVN""" - - Hello, from LiveView Native! - + + Hello + """ end end @@ -384,7 +467,7 @@ Generally using the asset catalog is more performant and customizable than using ### Your Turn: Custom Colors in the Asset Catalog -Custom colors can be defined in the asset catalog (https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). Generat +Custom colors can be defined in the asset catalog (https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). You're going to define a color in the asset catolog then evaluate the example below to see the color appear in your simulator. To create a new color go to the `Assets` folder in your iOS app and create a new color set. @@ -406,7 +489,7 @@ The defined color is now available for use within LiveView Native styles. Howeve Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator. - + ```elixir require Server.Livebook @@ -428,7 +511,7 @@ defmodule ServerWeb.ExampleLive do use ServerNative, :live_view @impl true - def render(assigns), do: ~H"" + def render(assigns), do: ~H"Hello" end |> Server.SmartCells.LiveViewNative.register("/") From 402409a202f421fe295b99564d28b66ce5ce589c Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 21 May 2024 10:53:16 -0400 Subject: [PATCH 34/62] Update to 5.3.2 of SwiftPhoenixClient --- Package.swift | 2 +- .../LiveViewNative/Coordinators/LiveSessionCoordinator.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 1f56ddc98..73b7b8695 100644 --- a/Package.swift +++ b/Package.swift @@ -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"), diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift index f3d32b5cc..722f00caa 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift @@ -418,7 +418,7 @@ public class LiveSessionCoordinator: 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() From 45a3c0d3453e2d5daa9c29d55a7f479848a9d50a Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Thu, 16 May 2024 12:05:23 -0400 Subject: [PATCH 35/62] Use `connecting` phase when channel is not connected, but socket is --- Sources/LiveViewNative/NavStackEntryView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/LiveViewNative/NavStackEntryView.swift b/Sources/LiveViewNative/NavStackEntryView.swift index 5d1ab3585..0b0351397 100644 --- a/Sources/LiveViewNative/NavStackEntryView.swift +++ b/Sources/LiveViewNative/NavStackEntryView.swift @@ -31,7 +31,7 @@ struct NavStackEntryView: View { private var phase: LiveViewPhase { switch coordinator.state { case .notConnected: - return .disconnected + return .connecting // `disconnected` phase only applies to the socket connection, not the channel. case .connecting: return .connecting case .connectionFailed(let error): From 200d36c0f7b1f6c973f424fec26aabf4ce84a2c4 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Thu, 16 May 2024 13:57:56 -0400 Subject: [PATCH 36/62] Add `setup` state --- .../Coordinators/LiveSessionCoordinator.swift | 18 +++++++++--------- .../Coordinators/LiveSessionState.swift | 10 ++++++---- .../Coordinators/LiveViewCoordinator.swift | 6 +++--- Sources/LiveViewNative/Live/LiveView.swift | 4 +++- Sources/LiveViewNative/NavStackEntryView.swift | 6 ++++-- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift index 722f00caa..bf51fe513 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift @@ -33,7 +33,7 @@ private let logger = Logger(subsystem: "LiveViewNative", category: "LiveSessionC @MainActor public class LiveSessionCoordinator: 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 @@ -103,11 +103,11 @@ public class LiveSessionCoordinator: ObservableObject { $navigationPath.scan(([LiveNavigationEntry](), [LiveNavigationEntry]()), { ($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 { @@ -151,7 +151,7 @@ public class LiveSessionCoordinator: 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. /// @@ -159,7 +159,7 @@ public class LiveSessionCoordinator: ObservableObject { /// - 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 @@ -252,7 +252,7 @@ public class LiveSessionCoordinator: ObservableObject { } self.socket?.disconnect() self.socket = nil - self.state = .notConnected + self.state = .disconnected } /// Forces the session to disconnect then connect. diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionState.swift b/Sources/LiveViewNative/Coordinators/LiveSessionState.swift index d3b2ade31..a399e4745 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionState.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionState.swift @@ -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 diff --git a/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift index f3f9b403f..5104ef190 100644 --- a/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift @@ -26,7 +26,7 @@ private let logger = Logger(subsystem: "LiveViewNative", category: "LiveViewCoor /// - ``handleEvent(_:handler:)`` @MainActor public class LiveViewCoordinator: ObservableObject { - @Published internal private(set) var internalState: LiveSessionState = .notConnected + @Published internal private(set) var internalState: LiveSessionState = .setup var state: LiveSessionState { internalState @@ -283,7 +283,7 @@ public class LiveViewCoordinator: 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 } } @@ -320,7 +320,7 @@ public class LiveViewCoordinator: ObservableObject { } await MainActor.run { [weak self] in self?.channel = nil - self?.internalState = .notConnected + self?.internalState = .disconnected } } diff --git a/Sources/LiveViewNative/Live/LiveView.swift b/Sources/LiveViewNative/Live/LiveView.swift index b57274b29..476e04a07 100644 --- a/Sources/LiveViewNative/Live/LiveView.swift +++ b/Sources/LiveViewNative/Live/LiveView.swift @@ -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(session: session)) diff --git a/Sources/LiveViewNative/NavStackEntryView.swift b/Sources/LiveViewNative/NavStackEntryView.swift index 0b0351397..27dbe5d88 100644 --- a/Sources/LiveViewNative/NavStackEntryView.swift +++ b/Sources/LiveViewNative/NavStackEntryView.swift @@ -30,12 +30,14 @@ struct NavStackEntryView: View { private var phase: LiveViewPhase { switch coordinator.state { - case .notConnected: - return .connecting // `disconnected` phase only applies to the socket connection, not the channel. + 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() } From ce4b7bcc5597809e8bf55d307565034ee2656da7 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Wed, 22 May 2024 16:06:19 -0400 Subject: [PATCH 37/62] Use `style` attribute for generated docs --- .../Subcommands/DocumentationExtensions.swift | 34 ++++++++----------- .../ModifierGenerator/Subcommands/List.swift | 2 +- .../Subcommands/Schema.swift | 2 +- lib/mix/tasks/lvn.swiftui.gen.docs.ex | 2 +- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/Sources/ModifierGenerator/Subcommands/DocumentationExtensions.swift b/Sources/ModifierGenerator/Subcommands/DocumentationExtensions.swift index 7760dbf53..3673344b7 100644 --- a/Sources/ModifierGenerator/Subcommands/DocumentationExtensions.swift +++ b/Sources/ModifierGenerator/Subcommands/DocumentationExtensions.swift @@ -16,7 +16,7 @@ extension ModifierGenerator { static let configuration = CommandConfiguration(abstract: "Output a list of the names of all available modifiers.") @Option( - help: "The `.swiftinterface` file from `/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-ios.swiftinterface`", + help: "The `.swiftinterface` file from `/Applications/Xcode.app/Contents/Developer/Platforms/XROS.platform/Developer/SDKs/XROS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-xros.swiftinterface`", transform: { URL(filePath: $0) } ) var interface: URL @@ -156,25 +156,19 @@ extension ModifierGenerator { } var result = "" + let style: String if parameters.isEmpty { - result.append(#""" - ```elixir - # stylesheet - "example" do - \#(name)() - end - ``` - """#) + style = #"\#(name)()"# } else { - result.append(#""" - ```elixir - # stylesheet - "example" do - \#(name)(\#(parameters.joined(separator: ", "))) - end - ``` - """#) + style = #"\#(name)(\#(parameters.joined(separator: ", ")))"# + } + + let quotedStyle: String + if style.contains(#"""# as Character) { + quotedStyle = #"'\#(style)'"# + } else { + quotedStyle = #""\#(style)""# } let changeEvent: String? = switch changeTracked.count { @@ -196,7 +190,7 @@ extension ModifierGenerator { ```html <%!-- template --%> - + ``` """#) } else { @@ -204,7 +198,7 @@ extension ModifierGenerator { ```html <%!-- template --%> - + \#(templates.map({ " \($0)" }).joined(separator: "\n")) ``` @@ -215,7 +209,7 @@ extension ModifierGenerator { ```html <%!-- template --%> - + \#(templates.map({ " \($0)" }).joined(separator: "\n")) ``` diff --git a/Sources/ModifierGenerator/Subcommands/List.swift b/Sources/ModifierGenerator/Subcommands/List.swift index e39f2a051..d86a2eb6d 100644 --- a/Sources/ModifierGenerator/Subcommands/List.swift +++ b/Sources/ModifierGenerator/Subcommands/List.swift @@ -9,7 +9,7 @@ extension ModifierGenerator { static let configuration = CommandConfiguration(abstract: "Output a list of the names of all available modifiers.") @Option( - help: "The `.swiftinterface` file from `/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-ios.swiftinterface`", + help: "The `.swiftinterface` file from `/Applications/Xcode.app/Contents/Developer/Platforms/XROS.platform/Developer/SDKs/XROS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-xros.swiftinterface`", transform: { URL(filePath: $0) } ) var interface: URL? diff --git a/Sources/ModifierGenerator/Subcommands/Schema.swift b/Sources/ModifierGenerator/Subcommands/Schema.swift index 0793b80ee..617b760fb 100644 --- a/Sources/ModifierGenerator/Subcommands/Schema.swift +++ b/Sources/ModifierGenerator/Subcommands/Schema.swift @@ -9,7 +9,7 @@ extension ModifierGenerator { static let configuration = CommandConfiguration(abstract: "Generate a `stylesheet-language.json` file compatible with the VS Code extension.") @Option( - help: "The `.swiftinterface` file from `/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-ios.swiftinterface`", + help: "The `.swiftinterface` file from `/Applications/Xcode.app/Contents/Developer/Platforms/XROS.platform/Developer/SDKs/XROS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-xros.swiftinterface`", transform: { URL(filePath: $0) } ) var interface: URL? diff --git a/lib/mix/tasks/lvn.swiftui.gen.docs.ex b/lib/mix/tasks/lvn.swiftui.gen.docs.ex index eef1f1c5d..3b1b3e2f2 100644 --- a/lib/mix/tasks/lvn.swiftui.gen.docs.ex +++ b/lib/mix/tasks/lvn.swiftui.gen.docs.ex @@ -7,7 +7,7 @@ defmodule Mix.Tasks.Lvn.Swiftui.Gen.Docs do # Using a temporary folder outside of the project avoids ElixirLS file watching issues defp temp_doc_folder, do: Path.join(System.tmp_dir!(), "temp_swiftui_docs") defp generate_swift_lvn_docs_command, do: ~c"xcodebuild docbuild -scheme LiveViewNative -destination generic/platform=iOS -derivedDataPath #{temp_doc_folder()} -skipMacroValidation -skipPackagePluginValidation" - @swiftui_interface_path "Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-ios.swiftinterface" + @swiftui_interface_path "Platforms/XROS.platform/Developer/SDKs/XROS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-xros.swiftinterface" defp generate_modifier_documentation_extensions(xcode_path), do: ~c(xcrun swift run ModifierGenerator documentation-extensions --interface "#{Path.join(xcode_path, @swiftui_interface_path)}" --output Sources/LiveViewNative/LiveViewNative.docc/DocumentationExtensions) @generate_documentation_extensions ~c(xcrun swift package plugin --allow-writing-to-package-directory generate-documentation-extensions) defp modifier_list(xcode_path), do: ~s(xcrun swift run ModifierGenerator list --interface "#{Path.join(xcode_path, @swiftui_interface_path)}" --modifier-search-path Sources/LiveViewNative/Stylesheets/Modifiers) From bd40afd9776b1b99dd1eec3952f4dbbf91a6cbe8 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Thu, 23 May 2024 11:43:24 -0400 Subject: [PATCH 38/62] Treat boolean attributes as true if present and != "false" --- Sources/LiveViewNative/Utils/DOM.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/LiveViewNative/Utils/DOM.swift b/Sources/LiveViewNative/Utils/DOM.swift index a94c9e153..1cd479403 100644 --- a/Sources/LiveViewNative/Utils/DOM.swift +++ b/Sources/LiveViewNative/Utils/DOM.swift @@ -80,9 +80,9 @@ public struct ElementNode { /// > The strings `"true"`/`"false"` are ignored, and only the presence of the attribute is considered. /// > A value of `"false"` would still return `true`. public func attributeBoolean(for name: AttributeName) -> Bool { - guard let attribute = attribute(named: name)?.value + guard let attribute = attribute(named: name) else { return false } - return attribute != "false" + return attribute.value != "false" } /// The text of this element. From 4925931bf766e2d166250e6f91415f648e409fe5 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Thu, 23 May 2024 16:20:05 -0400 Subject: [PATCH 39/62] Update core components to use `style` attribute --- .../lvn.swiftui.gen/core_components.ex | 67 +++++++++++-------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/priv/templates/lvn.swiftui.gen/core_components.ex b/priv/templates/lvn.swiftui.gen/core_components.ex index fd4c6bff7..faae29593 100644 --- a/priv/templates/lvn.swiftui.gen/core_components.ex +++ b/priv/templates/lvn.swiftui.gen/core_components.ex @@ -44,7 +44,7 @@ defmodule <%= inspect context.web_module %>.CoreComponents.<%= inspect context.m ## Examples - + <.input field={@form[:email]} type="TextField" /> <.input name="my-input" errors={["oh no!"]} /> @@ -95,10 +95,10 @@ defmodule <%= inspect context.web_module %>.CoreComponents.<%= inspect context.m |> assign_new(:value, fn -> field.value end) |> assign( :rest, - Map.put(assigns.rest, :class, [ - Map.get(assigns.rest, :class, ""), - (if assigns.readonly or Map.get(assigns.rest, :disabled, false), do: "disabled-true", else: ""), - (if assigns.autocomplete == "off", do: "text-input-autocapitalization-never autocorrection-disabled", else: "") + Map.put(assigns.rest, :style, [ + Map.get(assigns.rest, :style, ""), + (if assigns.readonly or Map.get(assigns.rest, :disabled, false), do: "disabled(true)", else: ""), + (if assigns.autocomplete == "off", do: "textInputAutocapitalization(.never) autocorrectionDisabled()", else: "") ] |> Enum.join(" ")) ) |> input() @@ -238,7 +238,7 @@ defmodule <%= inspect context.web_module %>.CoreComponents.<%= inspect context.m def error(assigns) do ~LVN""" - + <%%= render_slot(@inner_block) %> """ @@ -251,18 +251,17 @@ defmodule <%= inspect context.web_module %>.CoreComponents.<%= inspect context.m """ @doc type: :component - attr :class, :string, default: nil - slot :inner_block, required: true slot :subtitle slot :actions def header(assigns) do ~LVN""" - + <%%= render_slot(@inner_block) %> @@ -295,7 +294,7 @@ defmodule <%= inspect context.web_module %>.CoreComponents.<%= inspect context.m """ attr :id, :string, required: true attr :show, :boolean, default: false - attr :on_cancel, :string + attr :on_cancel, :string, default: nil slot :inner_block, required: true def modal(assigns) do @@ -303,7 +302,7 @@ defmodule <%= inspect context.web_module %>.CoreComponents.<%= inspect context.m @@ -337,7 +336,10 @@ defmodule <%= inspect context.web_module %>.CoreComponents.<%= inspect context.m <%% msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind) %>