Video #293: Cross-Platform Swift: Navigation
Episode: Video #293 Date: Sep 2, 2024 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep293-cross-platform-swift-navigation

Description
We will introduce navigation APIs to our Wasm application, starting simply with an alert before ramping things up with a dialog tag that can be fully configurable from a value type that represents its state and actions.
Video
Cloudflare Stream video ID: 5e9c5921fa15d533fd3789d0dd8220a6 Local file: video_293_cross-platform-swift-navigation.mp4 *(download with --video 293)*
References
- Discussions
- Swift for WebAssembly - How To Use Swift In Your Web App
- Batteries Not Included: Beyond Xcode
- Swift WebAssembly + GoodNotes, a cross-platform story!
- 0293-cross-platform-pt4
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
So this is pretty incredible. The mere fact that we wanted our feature’s model to be cross platform friendly led us to improving the overall code the model. Rather than performing network requests directly in the model we have properly extracted that behavior out to a dependency that can be injected into the model. Stephen
— 0:36
And we now have a truly cross platform model that powers both the logic and behavior of our feature, and we are using it in a SwiftUI app, a UIKit app, and a Wasm app.
— 0:46
But let’s push things even further. So far we haven’t performed any navigation in the web app, but previously on iOS we presented the number fact in an alert and sheet in order to demonstrate state-driven navigation.
— 0:57
Let’s see what that looks like in Wasm. Basic navigation: alerts
— 1:01
Let’s start with the simplest kind of navigation, that of showing an alert with the loaded fact. Alerts in the browser are very basic. We can explore the API by calling the alert function right in the browser console: alert("Hello!")
— 1:15
That causes an alert to appear in the browser. And for whatever reason this alert function is actually defined on the window global instead of the document global: window.alert("Hello!")
— 1:33
One thing you may find surprising about this alert is that it actually stops all execution of JavaScript while it is presented. As we mentioned previously, JavaScript in the browser is single threaded, and alerts block that thread. So, these kinds of alerts serve a very basic purpose, and may not always be appropriate, but that are more than enough for what we want to do.
— 1:51
To show an alert from our Swift code we just need to go through JSObject.global.window : JSObject.global.window.alert("Hello from Swift!")
— 2:09
When this compiles and refreshes the browser we will see the alert appears immediately. And we can see that all JavaScript execution has halted because none of our counter UI renders until the alert is dismissed.
— 2:28
We want to show this alert when we detect the fact value flip to a non- nil value, which we need to do in the observe closure: observe { _ in if let fact = model.fact?.value { // Show alert } … } .store(in: &tokens)
— 2:49
Once we come across a non- nil fact we can then show the alert: observe { _ in if let fact = model.fact?.value { _ = JSObject.global.window.alert(fact) } … } .store(in: &tokens)
— 2:58
Once the browser refreshes we can give it a spin and see that it does seem to work. When the fact loads we get an alert.
— 3:05
However, this is not quite right. We aren’t clearing out the fact state after the alert is dismissed. This means that if the observe trailing closure is called again, say when the count changes, then if the fact is still non- nil another alert will be shown.
— 3:19
It’s not actually possible to show this problem right now because technically we clear out the fact when incrementing and decrementing: func incrementButtonTapped() { count += 1 fact = nil } func decrementButtonTapped() { count -= 1 fact = nil }
— 3:30
So it’s incidental that the bug does not show right now. But if we comment out the fact = nil lines: func incrementButtonTapped() { count += 1 // fact = nil } func decrementButtonTapped() { count -= 1 // fact = nil }
— 3:49
…now we can see the bug clear as day. Any state change causes the alert to show again.
— 3:55
Luckily this is easy to fix. We can simply clear the state out right after the alert is done showing. And because alerts completely block the execution of JavaScript, we can do this literally right after the line of code that shows the alert: if let fact = model.fact?.value { _ = JSObject.global.window.alert(fact) model.fact = nil } And we can see in the browser that the bug is now fixed. We can count up, get a fact, and then count up again, and the alert does not reappear until we request a fact again.
— 4:16
This is so simple it almost seems wrong. There’s no need for some complex hook into the lifecycle of the alert because truly the line of code after alert will not even be executed until the alert is dismissed.
— 4:31
However, it is quite a tricky bit of code to get right, and we will have to be on top of our game to make sure we always clear out the state after showing an alert. Wouldn’t it be better to have a helper that hides all of those details away from us so that we can show an alert in a more declarative way, rather than spelling out each little detail?
— 4:46
Well, it’s absolutely possible to do that, and we can even make the API look similar to what we do in SwiftUI. Let’s theorize what we would like the ideal call site to look like.
— 4:54
What if we had an alert function that took a binding to a piece of optional state that controlled whether or not the alert is showing: alert(item: $model.fact)
— 5:03
And we could derive this binding from our model by marking it as @UIBindable : @UIBindable var model = CounterModel()
— 5:10
And luckily for us our UIBinding and UIBindable types are both written in 100% pure Swift and so are immediately cross platform.
— 5:20
However, the fact property isn’t actually a string. It’s the Fact type that we created in order to make the state Identifiable , which was a requirement from SwiftUI. JavaScript doesn’t haven’t any such requirement.
— 5:35
So, we have to do a little bit more work to get access to the underlying string held inside the fact so that we can pass it to the JavaScript alert. So perhaps the alert function could take a trailing closure that allows you to further transform the optional item into the final string that is actually presented in the alert: alert(item: $model.fact) { $0.value }
— 5:46
Or we could even use key path syntax for specifying the closure: alert(item: $model.fact, message: \.value)
— 5:55
This now looks very similar to how we do things in SwiftUI and UIKit, and so let’s make this syntax a reality.
— 6:02
Let’s create a new Navigation.swift file and we’ll import our SwiftNavigation library in order to get access to the UIBinding type: import SwiftNavigation
— 6:15
We will define a top-level function called alert that takes a binding to some optional item, and then further a trailing closure that can turn a non-optional version of that item into a string for the message of the alert: func alert<Item>( item: UIBinding<Item?>, message: @escaping @Sendable (Item) -> String ) { }
— 6:39
Then, first thing we can do in the function is observe changes to the binding: observe { _ = item.wrappedValue }
— 6:51
The very act of accessing the bindings wrapped value means we will be notified anytime it changes.
— 6:57
And so we can unwrap the optional so that we know when to show the alert: if let unwrappedItem = item.wrappedValue { }
— 7:01
When the item becomes non- nil we will show the alert by applying the transformation to the unwrapped item: if let unwrappedItem = item.wrappedValue { _ = JSObject.global.window.alert(message(unwrappedItem)) }
— 7:24
And the finally, to tie the knot, we will automatically nil out the binding state once the alert is dismissed: if let unwrappedItem = item.wrappedValue { _ = JSObject.global.window.alert(message(unwrappedItem)) item.wrappedValue = nil }
— 7:34
And we are getting a warning about the unused observation token. Remember that we have to keep this object alive if we want the observation to stay alive. However, in this local scope we don’t have anywhere to store the token. So, we will pass the responsibility to the caller of this function by returning the token: func alert<Item>( item: UIBinding<Item?>, message: @escaping (Item) -> String ) -> ObservationToken { observe { _ in … } }
— 7:55
And amazingly that is all it takes to make our first navigation API for the web! And our theoretical syntax is compiling: alert(item: $model.fact, message: \.value) .store(in: &tokens)
— 8:14
We can give it a spin in the browser by counting up, clicking the “Fact” button, and then we will see an alert pop-up. And the moment we dismiss the alert the state will have been cleared out of the model so that we can be sure that our model stays in sync with the visual representation of the feature in the browser. Custom dialogs
— 8:37
This is pretty exciting stuff! We now have a very rudimentary form of navigation in our web app. We can show an alert in the browser when a piece of optional state becomes non- nil , and when the alert is dismissed the state will be automatically nil ’d out. This is very similar to how SwiftUI works, and how our navigation tools in UIKit work.
— 8:53
But also this form of navigation is perhaps a bit too simplistic. Browser alerts do serve a purpose, but it’s pretty rare that they are the appropriate tool to use. As we mentioned before, all JavaScript stops executing while the alert is open, and that’s probably not what we want most of the time. Brandon
— 9:09
Let’s explore another form of navigation. HTML has a special tag called <dialog> , and when it is shown it takes over the enter screen. By default the browser will even apply some basic styling to the dialog to make it look somewhat reasonable.
— 9:25
Let’s see what it takes to show our fact in a dialog instead of an alert. This will allow JavaScript to continue executing while the dialog is showing, but it will also bring new challenges.
— 9:38
Let’s dig it.
— 9:40
Let’s start by exploring how to show a dialog in HTML. We can create a <dialog> tag first: let dialog = document.createElement("dialog")
— 9:54
Then we can create a title tag and add it to the dialog: let title = document.createElement("h1") title.innerText = "Fact" dialog.appendChild(title)
— 10:17
Next we can create a message and add it to the dialog: let message = document.createElement("p") message.innerText = "0 is a good number." dialog.appendChild(message)
— 10:35
And then finally we can add the dialog to the document’s body and invoke the showModal method to present the model: document.body.appendChild(dialog) dialog.showModal()
— 10:52
That’s all it takes to a dialog that takes of the screen. It prevents clicking anything below the dialog.
— 11:01
Unlike alerts, this is not blocking JavaScript execution, which we can see by telling the dialog to close: dialog.close()
— 11:10
Now that we’re familiar with the APIs, let’s port this code to Swift so that we can drive these dialogs using our model state. var dialog = document.createElement("dialog") var title = document.createElement("h1") title.innerText = "Fact" _ = dialog.appendChild(title) var message = document.createElement("p") _ = dialog.appendChild(message) var closeButton = document.createElement("button") closeButton.innerText = "Close" _ = dialog.appendChild(closeButton) _ = document.body.appendChild(dialog) _ = dialog.showModal()
— 12:40
And when the browser refreshes we are immediately launched into a modal, though it’s not functional yet, we haven’t even hooked up the close button, though it is worth pointing out that the dialog can be dismissed by hitting the escape key.
— 13:34
So wouldn’t it be cool if we could display the fact loaded in a custom dialog like this rather than the system alert? And even better if we could drive the presentation and dismissal of the dialog from state.
— 13:55
Let’s first theorize the syntax we would want to use at the call site. The API can look similar to the alert one we have already created, but we will name it alertDialog since it is now powered by the dialog machinery of the browser: alertDialog
— 14:15
It will also take a binding to an optional item to drive the presenting and dismissal of the alert: alertDialog(item: $model.fact
— 14:26
But this time it will take two trailing closures, one for the title and one for the message: alertDialog(item: $model.fact) { _ in "Fact" } message: { fact in fact.value }
— 15:04
And as we’ve seen time and time again, this is going to return an observation token that we must store to keep the observation alive: alertDialog(item: $model.fact) { _ in "Fact" } message: { fact in fact.value } .store(in: &tokens)
— 15:17
This looks like a pretty great API, and it gives us more flexibility in the display of the alert. So let’s make this syntax a reality.
— 15:26
We’ll hop over to Navigation.swift again, where all of our helpers live, and we will sketch out the signature of this new tool: func alertDialog<Item>( item: UIBinding<Item?>, title: @escaping (Item) -> String, message: @escaping (Item) -> String ) -> ObservationToken { }
— 16:03
The first thing we can do in this function is create a stub of a dialog element that can be presented and dismissed dynamically.
— 16:20
We can create the element and add it to the body of the document by copying and pasting the earlier code we sketched out: let document = JSObject.global.document var dialog = document.createElement("dialog") var title = document.createElement("h1") _ = dialog.appendChild(title) var message = document.createElement("p") _ = dialog.appendChild(message) var closeButton = document.createElement("button") closeButton.innerText = "Close" _ = dialog.appendChild(closeButton) _ = document.body.appendChild(dialog)
— 17:11
But now let’s implement the logic of the close button to nil out the state of the binding in order to close the dialog: closeButton.onclick = .object( JSClosure { _ in item.wrappedValue = nil return .undefined } )
— 17:59
And further, to tap into the event of the user typing the escape key to close the dialog, we can override the oncancel event on the dialog, and we just need to nil out the binding state there too: dialog.oncancel = .object( JSClosure { _ in item.wrappedValue = nil return .undefined } )
— 18:34
Next we will start observing changes in the binding, and that will give us the observation token we need to return: return observe { item.wrappedValue }
— 18:54
When we detect the binding’s value becomes non- nil we will populate the title and message elements, but we have some shadowed variables and sendable warnings to contend with: @MainActor func alertDialog<Item>( item: UIBinding<Item?>, title titleFromItem: @escaping @Sendable (Item) -> String, message messageFromItem: @escaping @Sendable (Item) -> String ) -> ObservationToken { } And now we can configure the DOM elements accordingly and show the dialog: if let unwrappedItem = item.wrappedValue { title.innerText = .string(titleFromItem(unwrappedItem)) message.innerText = .string(messageFromItem(unwrappedItem)) _ = dialog.showModal() }
— 21:20
And when we detect the binding value goes back to nil we can dismiss the dialog: } else { _ = dialog.close() }
— 21:29
And that is all it takes! If we take the app for a spin in the browser we will see that it works just as before, but we are presenting the fact in a nicely styled dialog rather than a browser alert. This is a much friendlier UI to present to the user, and it allows JavaScript to continue executing in the background.
— 22:20
And the model and visual representation of the UI will stay in sync thanks to all of our hard work in the alertDialog helper. For example, if put a print statement when the fact property is set in the CounterModel : var fact: Fact? { didSet { print("Fact set", fact?.value) } }
— 23:04
…then we will see that nil is written to this state when tapping on the “Close” button in the dialog. And nil will also be written to the property if we press the escape key on the keyboard to close the dialog.
— 23:28
And further, if the state is programmatically nil ’d out while the dialog is presented it will automatically be dismissed. For example, suppose we slept for 2 seconds after populating the fact state, and then nil ’d it out again: fact = try await Fact(value: factClient.fetch(count)) try await Task.sleep(for: .seconds(2)) fact = nil
— 24:01
Now when we present the dialog, it will remain open for 2 seconds and then automatically close itself. This is showing that we are truly keeping the state of our model in sync with the visual representation of the UI in the browser.
— 24:19
And this style of navigation is quite similar to how we deal with things like sheets and popovers in SwiftUI and UIKit. Those navigation APIs allow you to present a self-contained view that takes over the entire screen, and the view can be dismissed by either updating the state to be nil or by the user performing an action such as swiping down on a sheet. This is exactly what is happening with this dialog, except instead of swiping down to dismiss the sheet Alert state
— 25:04
We now have a more complex form of navigation in our little web app. We can show a modal dialog that the user can dismiss by either click a button in the modal or tapping the escape key on the their keyboard. And this is starting to show that there are a lot of similarities between the kinds of things we do over in SwiftUI and UIKit.
— 25:25
But we can push things even further. Right now we have a bunch of code in the view for describing the various properties of the alert. There is a trailing closure that describes the title of the alert, then a trailing closure that describes the messages in the alert, and the fact that there’s a “Close” button is just hard coded in the library code. Stephen
— 25:43
What if we wanted to be able to describe all the properties of the alert in our core domain model, such as the title, message and buttons, and then have a very simply API for presenting and dismissing the alert from that state. Our SwiftNavigation library even comes with simple data types that can abstractly describe alerts, and then we have navigation helpers for presenting alerts in SwiftUI and UIKit from this data type. This allows you to encapsulate more of your logic in your observable model, which means more chances to share logic between platforms, and it even makes it possible to write unit tests for alerts.
— 26:20
Let’s see what it takes to start showing alert dialogs in the browser from this data type.
— 26:26
Currently we are doing the following in our view code to show an alert: alertDialog(item: $model.fact) { _ in "Fact" } message: { fact in fact.value } .store(in: &tokens)
— 26:29
Right now this isn’t such a big deal, but in the future there may be some logic necessary to construct the various parts of this alert. For example, there may be some logic that tweaks the title or message of the alert. Or maybe we want to incorporate action buttons into the alert so that when the user clicks a button it feeds back into our model so that we can react to it.
— 26:46
And for these reasons, and to make alerts more testable, we created a simple data type called AlertState that encapsulates all of the properties of an alert. It lives in our SwiftNavigation library, and it’s just a simple struct that provides a data description of an alert, such as its title, message and buttons: public struct AlertState<Action>: Identifiable { public let id: UUID public var buttons: [ButtonState<Action>] public var message: TextState? public var title: TextState … }
— 27:00
It is generic over an Action type, which represents the various buttons displayed in the alert. Typically this is an enum with a case for each kind of button, and when a button is tapped in the alert some kind of handler is invoked with the action so that logic can be executed.
— 27:15
Let’s use this data type to describe our alert rather than using a simple optional Fact : var fact: Fact? …which doesn’t actually describe any of the properties of the alert.
— 27:24
So, instead of that state, we will hold onto an optional AlertState : // var fact: Fact? public var alert: AlertState< >?
— 27:31
But what should the generic be?
— 27:33
Right now our alert doesn’t actually have any actions that can be taken. We add a “Close” button so that there’s some way of closing the dialog, but we don’t need to actually react to that in our model. The alertDialog took care of the work for us by nil ’ing out the state in the binding.
— 27:46
So for now we could just use Never for the action to represent that we have no actions for the alert: public var alert: AlertState<Never>?
— 27:51
Next we will update anywhere we were referencing the fact to instead reference the alert . For example, when clearing out the state: // fact = nil alert = nil
— 28:07
And then when loading the fact we will do this in two steps. First we will do the async work to load the fact: let fact = try await factClient.fetch(count)
— 28:15
And then we will populate the alert state with the title and message we want to show in the alert: alert = AlertState { TextState("Fact") } message: { TextState(fact) }
— 28:42
This looks very similar to how things were done in the view, but we are using simple data types like AlertState and TextState to describe the alert, and then the view will interpret this data to actually present the alert. And all of these data types are Equatable , which means we can write unit tests on all of this logic.
— 28:59
And technically there is another trailing closure we can provide to describe the buttons that are displayed in the alert: alert = AlertState { TextState("Fact") } actions: { } message: { TextState(fact) }
— 29:08
But ideally we could leave this blank for when we don’t have any actions that we care about, and hopefully the navigation helper we will soon implement can take care of putting a close button in for us when no buttons are specified. This is how SwiftUI works too.
— 29:25
That’s all it takes to update our model. And it’s worth mentioning that we could also go and update our SwiftUI and UIKit apps that use this same model so that they power their alerts off of this state too. But we will leave that as an exercise for the viewer.
— 29:38
Because what we are most interested in right now is displaying this alert in our web app. Let’s theorize a syntax. What if we had a version of alertDialog that allowed us to simply hand it a binding to some optional AlertState , and then it would take care of the rest: alertDialog($model.alert) .store(in: &tokens)
— 29:55
After all, the AlertState has everything needed to show the alert, such as the title, description and buttons. This would mean the view doesn’t have to worry at all about how to describe the alert. That has all been taken care of for us by the model.
— 30:08
Let’s hop over to the Navigation.swift file and make this function a reality. We can paste in the basic signature of the function: @MainActor func alertDialog<Action>( _ state: UIBinding<AlertState<Action>?> ) -> ObservationToken { } It just takes a UIBinding of some optional AlertState , and will return an ObservationToken just as all of our other helpers have.
— 30:45
This function will start just as the other alertDialog did, by getting a stub of a dialog element into place: let document = JSObject.global.document var dialog = document.createElement("dialog") _ = document.body.appendChild(dialog) var title = document.createElement("h1") _ = dialog.appendChild(title) var message = document.createElement("p") _ = dialog.appendChild(message) dialog.oncancel = .object( JSClosure { _ in state.wrappedValue = nil return .undefined } )
— 31:10
Next we can start up an observation and return the token: return observe { } And the work inside this observation will be similar to what we did in the other alertDialog , but there will also be a few new tricks.
— 31:17
First we will observe the changes in the binding by trying unwrap its value: if let alertState = state.wrappedValue { } else { }
— 31:30
If the unwrapping succeeds we can already update the title of the dialog: title.innerText = .string(String(state: alertState.title)) This is using a special String initializer for turning TextState into a simple string.
— 32:10
Next we want to update the message of the dialog with the message in the AlertState , but that state is optional. So we need to unwrap it first: message.innerText = .string( alertState.message.map { String(state: $0) } ?? "" )
— 32:42
And we have to be extra careful here because it’s possible to show an alert once with a message, and then later show another alert without a message. We wouldn’t want to accidentally leave an old message showing. To fix this we will just show or hide the message depending on if the state is nil : message.hidden = .boolean(alertState.message == nil)
— 33:05
Next we need to loop over all of the buttons in the alert so that we can add them to the dialog: for buttonState in alertState.buttons { }
— 33:16
We can create a button element and set its text with the button’s label: var button = document.createElement("button") button.innerText = .string(String(state: buttonState.label)) _ = dialog.appendChild(button)
— 33:44
And further we want to tap into the event of when the button is clicked so that we can clear out the state driving the alert: button.onclick = .object( JSClosure { _ in state.wrappedValue = nil return .undefined } )
— 34:00
But there is more to do in this closure. Remember that it is called when the button is clicked, and typically there is an action associated with each button so that we can handle the action when the button is clicked.
— 34:21
This means we need a handler function to be specified when using alertDialog so that we have a way to communicating to the caller that an alert button was clicked: @MainActor func alertDialog<Action>( _ state: UIBinding<AlertState<Action>?>, action handler: @escaping @Sendable (Action) -> Void ) -> ObservationToken {
— 34:40
And then we can invoke this handler with the action associated with the button clicked on: button.onclick = .object( JSClosure { _ in buttonState.withAction { action in guard let action else { return } handler(action) } state.wrappedValue = nil return .undefined } )
— 34:58
This is causing a cryptic compiler error, but the problem is just that buttonState needs to be Sendable to pass to an escaping closure, and ButtonState is conditionally sendable based on the sendability of its Action generic. So, let’s make the action Sendable : func alertDialog<Action: Sendable>(
— 35:18
Now this is compiling, but there are two big edge cases to think about when it comes to buttons.
— 35:22
First of all, an alert can be shown and dismissed multiple times during an application’s lifetime, and currently each time we will be appending more and more buttons. We need to clear out any previous buttons before adding the new ones.
— 35:39
To do this we can query the elements inside the dialog element that are buttons: dialog.querySelectorAll("button")
— 35:52
This returns a collection of buttons inside the dialog. We can then loop over this collection by using the JavaScript forEach operator: dialog.querySelectorAll("button").forEach
— 35:58
It’s a little confusing, but this forEach is calling down to a JavaScript function, not Swift’s native forEach method. And we can hand this forEach a JSClosure that will be invoked with each element in the collection: _ = dialog.querySelectorAll("button").forEach(JSClosure { arguments in return .undefined })
— 36:15
And in this closure we can remove each element: _ = dialog.querySelectorAll("button").forEach(JSClosure { arguments in arguments.first!.remove() })
— 36:28
That will now remove all buttons from the alert before we add the new buttons.
— 36:35
There’s one more edge case to think about. What do we want to do if no actions are specified by the AlertState ? That means there will be nothing for the user to click on to close the dialog. Sure they can still tap the escape key, but they also have to be aware of that.
— 36:49
We can take a page out of SwiftUI’s playbook here. In SwiftUI, if you present an alert and there is no button specified, it will automatically add an “OK” button for you. That way the user always has something to click on to dismiss the alert.
— 37:00
We can recreate this quite easily: if alertState.buttons.isEmpty { var button = document.createElement("button") button.innerText = "OK" button.onclick = .object( JSClosure { _ in state.wrappedValue = nil return .undefined } ) _ = dialog.appendChild(button) }
— 37:24
We are close to done with this implementation. All that is left is to finally show the modal after everything is configured: _ = dialog.showModal()
— 37:38
And we will need to close the modal when we detect the state go back to nil : _ = dialog.close()
— 37:44
And that is all it takes!
— 37:48
Unfortunately our theoretical syntax isn’t yet compiling: alertDialog($model.alert)
— 37:52
And that’s because we sketched this syntax before we realized that we needed a trailing closure to handle the action when a button is clicked: alertDialog($model.alert) { _ in }
— 38:03
However, our alert’s action is Never , and so this closure can’t actually ever be invoked.
— 38:10
For such a situation we can have an overload of alertDialog that doesn’t require a handler when the action is Never : @MainActor func alertDialog( _ state: UIBinding<AlertState<Never>?> ) -> ObservationToken { alertDialog(state, action: { _ in }) }
— 38:37
And now our theoretical syntax is compiling: alertDialog($model.alert) .store(in: &tokens)
— 38:48
We can give our app a spin in the browser to see that everything works just as we expect. There’s even an “OK” button in the alert even though we didn’t specific one in the AlertState .
— 38:55
And to make sure things are really working, let’s add some buttons to the alert. Let’s have an “OK” button for just dismissing the alert, but let’s also have a “Save” button: alert = AlertState { TextState("Fact") } actions: { ButtonState { TextState("OK") } ButtonState { TextState("Save") } } message: { TextState(fact) } …for a theoretical feature where we can save our favorite number facts and have them displayed in the app.
— 39:18
Running this in the browser will show that we now have two buttons in the dialog. Clicking either one dismisses the alert, and with a bit more work we could even implement this feature, but we won’t do that right now. Next time: UI controls
— 39:39
This is absolutely incredible. We are now powering a pretty complex web-based feature from our Swift code base. We have been able to share a ton of logic and behavior between different view paradigms and deployment platforms, and it’s all thanks to our dedicated effort to prioritize domain modeling over view-related concerns.
— 39:56
We have been very strict with ourselves to extract as much of the logic and behavior of our features into a dedicated observable model, rather than sprinkling that code throughout the view. And it is now paying off. Brandon
— 40:06
But there is one last feature in our CounterModel that we explored for UIKit that we have not yet recreated in our web app. Back when dealing with UIKit we wanted to show how one can bind properties of an observable model to various UI controls, such as steppers and text fields. And we further wanted to show how even focus of text fields could be controlled via state.
— 40:27
Let’s explore what this looks like from the perspective of the web and Wasm…next time! References Swift for WebAssembly - How To Use Swift In Your Web App Steven Van Impe A talk from SwiftCraft 2024: WebAssembly is a rapidly growing technology that provides great opportunities for Swift developers. This talk will introduce Swift developers to WebAssembly, and demonstrate how they can run Swift in the browser, call JavaScript from Swift to access the DOM, add Swift modules to web apps, and so much more. A live demo will show how a single Swift codebase can power not just an iOS app, but also a web app, and a back-end. https://www.youtube.com/watch?v=q0OdHVfz7r0 Batteries Not Included: Beyond Xcode Kabir Oberai A talk from Swift TO 2023: A confrontation of the notion that Xcode+macOS are the only way to develop apps for Apple platforms. We’ll discover what it takes to build apps in other IDEs like Visual Studio Code, as well as on non-Apple platforms, unveiling the secrets of cross-compilation to build, sign, and deploy iOS apps on Windows and Linux. https://www.youtube.com/watch?v=fQ9uU9RHnRM Swift WebAssembly + GoodNotes, a cross-platform story! Pedro Gómez • Nov 19, 2022 A talk from NSSpain 2022 discussing how Goodnotes uses Swift Wasm in their application: When a company implements 100% of the codebase in a language like Swift, there is a chance that in the future you may need to implement something that is not an iOS app with the same code, something like a web page maybe? This talk relates how we’ve reused most of the already implemented code in GoodNotes iOS app to create a web project we can reuse in different platforms, the technical approach we fo llowed, challenges and solutions applied. https://vimeo.com/751290710 Downloads Sample code 0293-cross-platform-pt4 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .