Video #163: SwiftUI Navigation: Sheets & Popovers, Part 2
Episode: Video #163 Date: Oct 11, 2021 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep163-swiftui-navigation-sheets-popovers-part-2

Description
This week we’ll explore how to drive a sheet with optional state and how to facilitate communication between the sheet and the view presenting it. In the process we will discover a wonderful binding transformation for working with optionals.
Video
Cloudflare Stream video ID: 7abfcb89c4dc0a4618a4fb01bb4a36f0 Local file: video_163_swiftui-navigation-sheets-popovers-part-2.mp4 *(download with --video 163)*
References
- Discussions
- SwiftUI Navigation
- Crash in Binding's failable initializer
- WWDC 2021: Demystifying SwiftUI
- SE-0293: Extend Property Wrappers to Function and Closure Parameters
- 0163-navigation-pt4
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
The reason we’d want to do such a refactor is that putting the cancel and save hooks in the child view causes the view to be overly specified. Maybe we want to reuse that view in situations where the buttons have different labels, or different placement, or not visible at all. Supporting all of those use cases would mean we have to pass in all types of configuration data to describe exactly what kind of ItemView we want.
— 0:31
It would be far better if we just allowed whoever creates the ItemView to be responsible for attaching the action buttons, and then it would be free to use whatever labels or placement it wants.
— 0:48
Unfortunately this is not possible right now because the most up to date version of the item state lives only in the ItemView ’s @State field. The outside simply does not have access to that data.
— 1:06
So, these 3 problems are substantial enough for us to want to find another way to manage this state. In order to facilitate communication between these two views we simply need to get rid of @State and reach for a tool that allows for 2-way communication. @Binding over @State
— 1:26
And that tool is another property wrapper called @Binding . We can simply swap State out: struct ItemView: View { // @State var item: Item = Item( // name: "", color: nil, status: .inStock(quantity: 1) // ) @Binding var item: Item … }
— 1:42
And get rid of the custom initializer: struct ItemView: View { … // init( // item: Item, // onSave: @escaping (Item) -> Void, // onCancel: @escaping () -> Void // ) { // print("ItemView.init", item.name) // self._item = .init(wrappedValue: item) // self.onSave = onSave // self.onCancel = onCancel // } … var body: some View { // let _ = print("ItemView.body", item.name) … } }
— 1:48
The ItemView compiles just fine with this change because the syntax for using @State and @Binding are very similar, but if we scroll down to the preview it is no longer compiling because we need to pass it a binding of an item .
— 2:02
The simplest fix would be to introduce a constant binding: struct ItemView_Previews: PreviewProvider { static var previews: some View { NavigationView { ItemView( item: .constant( Item( name: "", color: nil, status: .inStock(quantity: 1) ) ), onSave: { _ in }, onCancel: { } ) } } }
— 2:16
This will get the preview building, but because the binding is constant, the item view will have no ability to write to it, leaving our preview in a state in which we can’t really interact with it.
— 2:31
Another thing we could do is cook up a wrapper view that can introduce some state so that we can derive a binding for the ItemView : struct ItemView_Previews: PreviewProvider { struct WrapperView: View { @State var item = Item( name: "", color: nil, status: .inStock(quantity: 1) ) var body: some View { ItemView(item: $item, onSave: { _ in }, onCancel: { }) } } static var previews: some View { NavigationView { WrapperView() } } }
— 2:31
It’s a bit of a bummer, though, to have to introduce this boilerplate just to get a working preview.
— 2:53
The only other place the compiler is complaining is where we display the item view from a sheet. Cannot convert value of type ‘Item’ to expected argument type ‘Binding<Item>’
— 2:59
However, we don’t have access to a binding of an item. The value passed to the closure is just a plain item.
— 3:12
So, when invoking the .sheet(item:) API we would like to hand it a binding of an optional, and once we are inside the view builder closure we prefer to have a binding of an honest value. We somehow need to transform our binding of an optional into a binding of an honest value.
— 3:23
It turns out that SwiftUI actually ships with a tool that can help us accomplish that. It’s not talked about too much, but the Binding type comes with a failable initializer that accepts a binding of an optional. Perhaps we can use that to get a binding of an honest item: .sheet(item: $viewModel.itemToAdd) { (item: Item) in if let $itemToAdd = Binding($viewModel.itemToAdd) { NavigationView { ItemView( item: $itemToAdd, onSave: { item in viewModel.add(item: item) }, onCancel: { viewModel.cancelButtonTapped() } ) } } }
— 4:04
Everything compiles, and if we run in the app we see that the modal pops up with the name pre-filled and everything seems to work just as it did before. But even cooler, if we tap the “Add” button we will see the modal sheet starts to animate up, and for a split second it is completely blank, but them a moment later the item name is pre-filled with “Bluetooth Keyboard”.
— 4:30
However, if we interact with the form by doing something as simple as focusing a text field, when we hit cancel the application crashes. 😬
— 4:39
It seems that there is a bug in the failable binding initializer, and we’ve filed feedback with Apple so hopefully it gets fixed soon. However, lucky for us, it’s straightforward to implement this failable initializer ourselves, and it won’t crash.
— 4:51
We can get the signature in place, which we will purposely make it slightly different from SwiftUI’s so that it isn’t ambiguous: extension Binding { init?(unwrap binding: Binding<Value?>) { … } }
— 5:16
We can start the initializer by checking if the binding currently holds a value, and if it does not we can fail the initializer by returning nil : guard let value = binding.wrappedValue else { return nil }
— 5:28
Once we get an honest value out of the binding it’s easy to return a binding of honest values since we can wrap that value in a brand new binding that communicates updates back to the original: extension Binding { init?(unwrap binding: Binding<Value?>) { guard let wrappedValue = binding.wrappedValue else { return nil } self.init( get: { wrappedValue }, set: { binding.wrappedValue = $0 } ) } }
— 5:48
Next we can update our sheet code to use this initializer instead of SwiftUI’s: .sheet(item: $viewModel.itemToAdd) { itemToAdd in if let $itemToAdd = Binding(unwrap: $viewModel.itemToAdd) { … } }
— 5:55
Now if we re-run the application we will see it works as we expect, even if we make changes to the form fields before tapping “Add”.
— 6:17
We can clean things up a bit, because the current if - let dance is a bit noisy, and we’re not even using the itemToAdd that gets passed to the view builder. In fact, we can even ignore it with an underscore. .sheet(item: $viewModel.itemToAdd) { _ in if let $itemToAdd = Binding(unwrap: $viewModel.itemToAdd) { … } }
— 6:35
So let’s define a custom version of sheet that hides away all this messiness: extension View { func sheet<Value, Content>( unwrap optionalValue: Binding<Value?>, @ViewBuilder content: @escaping (Binding<Value>) -> Content ) -> some View where Value: Identifiable, Content: View { sheet(item: optionalValue) { _ in if let value = Binding(unwrap: optionalValue) { content(value) } } } }
— 8:11
And now we can clean up where the item view is presented. .sheet(unwrap: $viewModel.itemToAdd) { $itemToAdd in NavigationView { ItemView( item: $itemToAdd, onSave: { viewModel.add(item: $0) }, onCancel: { viewModel.cancelButtonTapped() } ) } } Derived behavior: binding transformations
— 8:38
This is pretty awesome. We are able to cook up our own little view method that transforms a binding of an optional into a binding of an honest value, and then hand that off to a view builder so that we can present a sheet from it. And then any mutations the sheet makes to the binding are automatically played back to the view model, so we could even observe changes to its itemToAdd field in the view model if we wanted to layer on additional behavior based on whatever was happening inside the modal.
— 9:05
This is now the second time we’ve seen this pattern in this series of episodes, the first being when we implemented the IfCaseLet view, so let’s take a moment to slow down and understand this concept a little deeper.
— 9:41
These helpers are examples of what we call “ derived behavior ”, which is something we did a 5-part series exploring in depth a few months ago. We call it “derived” because we have the InventoryViewModel that holds all of the behavior and logic for the inventory list screen. A part of that behavior is to spin up a modal view that is presented. In order to do this we want to peel off a little bit of behavior from the InventoryViewModel , in this case a binding that is connected to the itemToAdd optional, and hand it off to the ItemView that is presented in the modal.
— 10:17
This is an extremely important concept. It is what allows us to decouple child domains from parent domains, which further allows us to modularize our applications so that disconnected screens can be built, tested and run in full isolation. SwiftUI provides some tools to aid in this, such as dynamic member lookup for deriving bindings for a field of a struct, and very recently a new tool was provided. As of Swift 5.5 and iOS 15 it is possible to transform a binding of a collection into a binding of a particular element in the collection.
— 10:17
All thanks to the new ForEach initializer and the ability to pass property wrappers to the arguments of closures, we can now work with a binding of an element if we have a binding of a collection: ForEach($collection) { $element in }
— 11:43
This is an example of SwiftUI allowing us to “derive” behavior. We can peel of a little binding of a single element by handing a binding of a collection to ForEach . It may not seem like it, but this is identical to what we are doing with the .sheet(unwrap:) operator, except it transforms a binding of an optional into a binding of an honest value. In fact, if you think of optionals as just collections of at most one element, then our .sheet(unwrap:) operator is kinda just a special case of the ForEach initializer.
— 12:17
We can even further see the similarities between these concepts if we put their signatures next to each other. If strip away some syntactic noise then we will see that the ForEach initializer is basically a function that takes a binding of a collection as well as a function that transforms bindings of the collection’s element into some view: // ForEach.init: (Binding<C>, (Binding<C.Element>) -> some View) -> ForEach
— 12:43
Similarly, the .sheet(unwrap:) operator is a function that takes a binding of an optional value as well as a function that transforms bindings of honest values into some view: // ForEach.init: (Binding<C>, (Binding<C.Element>) -> some View) -> ForEach // sheet(unwrap:): (Binding<V?>, (Binding<V>) -> some View) -> some View
— 13:02
These functions serve the same purpose, its just that one is suited for collections and the other is suited for optionals. This should lead you to believe that there is an entire world of binding transformations that we are possibly missing. We could transform bindings of results, or bindings of dictionaries, or bindings of trees, and who knows what else!
— 13:25
That was all a little theoretical, so let’s get back to the task at hand. Now that we have changed our view to use @Binding instead of @State we can now refactor the toolbar buttons out of the ItemView and into the InventoryView .
— 13:49
So, let’s remove those closures from the ItemView , since we want to move those hooks directly into the sheet: struct ItemView: View { @Binding var item: Item // let onSave: (Item) -> Void // let onCancel: () -> Void … }
— 14:03
And let’s stop adding toolbar items to the navigation bar: // .toolbar { // ToolbarItem(placement: .cancellationAction) { // Button("Cancel") { // onCancel() // } // } // ToolbarItem(placement: .primaryAction) { // Button("Save") { // onSave(item) // } // } // }
— 14:07
Instead, when constructing the sheet view we will set the toolbar of it directly and feed its actions to the view model: .sheet(unwrap: $viewModel.itemToAdd) { item in NavigationView { ItemView(item: item) .navigationBarTitle("Add") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { viewModel.cancelButtonTapped() } } ToolbarItem(placement: .primaryAction) { Button("Add") { viewModel.add(item: itemToAdd) } } } } }
— 15:02
So our desire to push more and more state and logic into the view model and to use the best domain modeling possible has continued to reap benefits. Our code is getting simpler, has fewer invalid states, views are freer to be reused in many places, and we’re building new tools for breaking down complex problems into simpler pieces. Adding an item row domain
— 15:30
This is all looking really good, but let’s not rest on our laurels just yet. We’ve got more we’ve got to do.
— 15:36
We are going to add two more buttons to each row of the list. One for editing that item and another for duplicating that item. And to make things even more interesting we will make the edit screen show up as a modal sheet whereas the duplicate screen will be displayed in a popover.
— 15:58
So, where do we start? If we follow the example we’ve already set for ourselves we may be tempted to pile more state into our InventoryViewModel . We could add an optional Item for the one we are potentially duplicating and an optional Item for the one we are potentially editing: class InventoryViewModel: ObservableObject { @Published var inventory: IdentifiedArrayOf<Item> @Published var itemToAdd: Item? @Published var itemToDelete: Item? @Published var itemToDuplicate: Item? @Published var itemToEdit: Item? … }
— 16:20
We would also need to update our initializer and add a bunch of new methods onto the view model for when the duplicate or edit buttons are tapped, as well as for when those changes are finally committed.
— 16:34
But there are a number of problems with pursuing this style. First of all, modeling data in this way leads to many invalid states. We are holding onto 4 optionals, which means technically there are 16 different states this view model could be in. All the values could be nil , which is fine, or one of the values could be non- nil , which is also fine, but also two, three or even all four of the values could be non- nil , and that makes no sense whatsoever. What would it mean for both the itemToAdd and the itemToDuplicate to be non- nil ? Does that mean two modal sheets should be displayed?
— 17:10
And even beyond the data modeling problem, this view model is starting to take on a lot of responsibilities. Some are appropriate, such as holding onto the list of items and handling the logic for adding a new item to the list. Others seem like they are better suited to be in the domain of a single row of the list rather than the whole list, such as the alert to delete and the logic for editing or duplicating an item.
— 17:38
So, we’re going to do a bit of refactoring real quick to properly model a domain for each row of the list, which will make it much easier for us to implement the edit and duplicate features. We’ll start by getting a basic view model into place that represents a single row of the list, and for right now it will hold onto an item: class ItemRowViewModel: ObservableObject { @Published var item: Item init(item: Item) { self.item = item } }
— 18:20
Then our InventoryViewModel will hold onto an array of ItemRowViewModel s rather than just plain Item s: class InventoryViewModel: ObservableObject { @Published var inventory: IdentifiedArrayOf<ItemRowViewModel> @Published var itemToAdd: Item? @Published var itemToDelete: Item? init( inventory: IdentifiedArrayOf<ItemRowViewModel> = [], itemToAdd: Item? = nil, itemToDelete: Item? = nil ) { self.inventory = inventory self.itemToAdd = itemToAdd self.itemToDelete = itemToDelete } … }
— 18:50
In order to put an ItemRowViewModel in an IdentifiedArray we must make the view model Identifiable , which we can implement by just forwarding its requirements to the underlying item : class ItemRowViewModel: Identifiable, ObservableObject { … var id: Item.ID { item.id } }
— 19:14
The .add method is not compiling, but this is easy to fix because we just need to create a view model to wrap the item before adding it to the inventory collection: func add(item: Item) { withAnimation { inventory.append(.init(item: item)) itemToAdd = nil } }
— 19:25
Next we have some errors in the view, but before fixing them let’s first extract all the content inside the ForEach into its own proper SwiftUI view since it now has a dedicated view model for its behavior. We can get the scaffolding of the view in place: struct ItemRowView: View { @ObservedObject var viewModel: ItemRowViewModel var body: some View { } }
— 19:52
And we can basically copy and paste everything from inside the ForEach to this new view, making a few small adjustments to go through the viewModel in order to access the item : struct ItemRowView: View { @ObservedObject var viewModel: ItemRowViewModel var body: some View { HStack { VStack(alignment: .leading) { Text(viewModel.item.name) switch viewModel.item.status { case let .inStock(quantity): Text("In stock: \(quantity)") case let .outOfStock(isOnBackOrder): Text( "Out of stock" + (isOnBackOrder ? ": on back order" : "") ) } } Spacer() if let color = viewModel.item.color { Rectangle() .frame(width: 30, height: 30) .foregroundColor(color.swiftUIColor) .border(Color.black, width: 1) } Button(action: { // viewModel.deleteButtonTapped(item: item) }) { Image(systemName: "trash.fill") } .padding(.leading) } .buttonStyle(.plain) .foregroundColor( viewModel.item.status.isInStock ? nil : Color.gray ) } }
— 20:06
The only tricky part is what to do about the delete button being tapped. We no longer have access to the entire collection of items, and so it’s not clear how we can remove the item, but we will look into that later.
— 20:17
We can now just construct a ItemRowView inside the ForEach : List { ForEach(viewModel.inventory) { itemRowViewModel in ItemRowView(viewModel: itemRowViewModel) } }
— 20:29
And we can even shorten it up a bit if we want: List { ForEach(viewModel.inventory, content: ItemRowView.init(viewModel:)) }
— 20:44
So, this refactor is seems to be promising, but we currently we’ve lost the delete functionality. Let’s re-implement it, but this time we can move some of the responsibilities into the item row domain so that it doesn’t clutter the inventory list domain.
— 20:56
For example, the inventory list domain no longer needs to be responsible for showing the alert to confirm the deletion of an item. All of that logic can now live in the row domain. So, we’ll remove that field from the InventoryViewModel : class InventoryViewModel: ObservableObject { @Published var inventory: IdentifiedArrayOf<ItemRowViewModel> @Published var itemToAdd: Item? // @Published var itemToDelete: Item? init( inventory: IdentifiedArrayOf<ItemRowViewModel> = [], itemToAdd: Item? = nil // itemToDelete: Item? = nil ) { self.inventory = inventory self.itemToAdd = itemToAdd // self.itemToDelete = itemToDelete } … }
— 21:18
We can also get rid of the deleteButtonTapped method because that logic will soon move to the ItemRowViewModel : // func deleteButtonTapped(item: Item) { // itemToDelete = item // }
— 21:23
And we get the InventoryView compiling again by just remove all the alert code: var body: some View { List { ForEach( viewModel.inventory, content: ItemRowView.init(viewModel:) ) } // .alert( // item: $viewModel.itemToDelete, // title: { Text($0.name) }, // actions: { item in // Button("Delete", role: .destructive) { // viewModel.delete(item: item) // } // }, // message: { _ in // Text("Are you sure you want to delete this item?") // } // ) … }
— 21:30
Now that we’ve removed all of the deletion alert responsibilities from the inventory list domain, let’s figure out how to get those responsibilities into the row domain. We can start by introducing the state that used to live in the inventory view. class ItemRowViewModel: Identifiable, ObservableObject { @Published var item: Item @Published var itemToDelete: Item? init( item: Item, itemToDelete: Item? = nil ) { self.item = item self.itemToDelete = itemToDelete } … }
— 21:39
But because we’re in the row domain, we already have access to the specific item being deleted. We Just need to to represent if the delete alert is presented or not, so we can even go back to just using a simple boolean instead: class ItemRowViewModel: Identifiable, ObservableObject { @Published var deleteItemAlertIsPresented: Bool @Published var item: Item init( deleteItemAlertIsPresented: Bool = false, item: Item ) { self.deleteItemAlertIsPresented = deleteItemAlertIsPresented self.item = item } … }
— 22:07
Already this is showing that by splitting up our domains into smaller and smaller pieces we are getting benefits. Since the row domain always has an item available to us we get to use the simpler API for showing alerts. This may seem like a small win, but we are going to pick up more and more of these as we add features.
— 22:32
While we’re in the view model, let’s go ahead and add an endpoint for when the delete button is tapped, which should show the alert: func deleteButtonTapped() { deleteItemAlertIsPresented = true }
— 22:42
And this is the endpoint we can call to from the button. Button(action: { viewModel.deleteButtonTapped() }) { Image(systemName: "trash.fill") }
— 22:51
Then in the ItemRowView we can show the alert when this boolean flips to true : .alert( viewModel.item.name, isPresented: $viewModel.deleteItemAlertIsPresented, actions: { Button("Delete", role: .destructive) { viewModel.deleteConfirmationButtonTapped() } }, message: { Text("Are you sure you want to delete this item?") } )
— 23:31
We just need to add that endpoint to the view model, though we can’t implement it just yet. func deleteConfirmationButtonTapped() { }
— 23:55
And notably, we are now using the alert API that comes with iOS 15 and not the helper we defined to work with optional state, and this is because in deriving behavior for a single row we were able to simplify how we modeled this data with a boolean instead.
— 24:09
If we ran things, an alert would show when the delete button is tapped, but of course the actual delete action wouldn’t do anything yet, because we need some way to communicate back to the parent view model that we want to delete the item.
— 24:29
We will handle this like we have handled similar things in the past, by adding a callback closure that the parent can hook into to implement logic for the action: class ItemRowViewModel: Identifiable, ObservableObject { … var onDelete: () -> Void = { } … func deleteConfirmationButtonTapped() { onDelete() } }
— 24:46
We can hook into this onDelete callback after we create the view model, and this is our opportunity to layer in the behavior necessary to remove the item from the inventory: func add(item: Item) { withAnimation { let viewModel = ItemRowViewModel(item: item) viewModel.onDelete = { [weak self] in self?.deleteItem(id: item) } inventory.append(viewModel) itemToAdd = nil } }
— 25:50
It’s a little unfortunate that we have to pay careful attention to retain cycles, but that’s just the cost of doing business with reference types.
— 25:56
Finally, to get our previews building we need to wrap each item in a view model and remove itemToDelete , since deletion has moved to the row. struct InventoryView_Previews: PreviewProvider { static var previews: some View { let keyboard = Item(name: "Keyboard", color: .blue, status: .inStock(quantity: 100)) NavigationView { InventoryView( viewModel: .init( inventory: [ .init(item: keyboard), .init( item: Item( name: "Charger", color: .yellow, status: .inStock(quantity: 20) ) ), .init( item: Item( name: "Phone", color: .green, status: .outOfStock(isOnBackOrder: true) ) ), .init( item: Item( name: "Headphones", color: .green, status: .outOfStock(isOnBackOrder: false) ) ), ], itemToAdd: keyboard ) ) } } }
— 26:26
We also need to fix the ContentView previews, and the app entry point.
— 26:33
If we run things we will see that everything appears to work just as it did before the refactor. We can add an item and delete it, but when we try to delete an existing row, we will see that we can’t. This is because we introduce this behavior when the add endpoint is invoked, but not on initialization.
— 27:43
One thing we could do is change the initializer to invoke add rather than assign the array directly: init( inventory: IdentifiedArrayOf<ItemRowViewModel> = [], itemToAdd: Item? = nil ) { self.inventory = [] self.itemToAdd = itemToAdd for itemRowViewModel in inventory { add(item: itemRowViewModel.item) } }
— 28:02
But this doesn’t seem right. The initializer is handed an array of view models and immediately discards the view model.
— 28:16
Worse, add is responsible for more than configuring communication between view models. It also nils out itemToAdd , which means we’re potentially trampling over state we just passed to the initializer.
— 28:30
Instead, we should introduce a private method that the initializer and add can share, that is only responsible for binding communication between the view models. We can call it bind : private func bind(itemRowViewModel: ItemRowViewModel) { itemRowViewModel.onDelete = { [weak self, id = itemRowViewModel.id] in _ = withAnimation { self?.inventory.remove(id: id) } } inventory.append(itemRowViewModel) }
— 29:39
And then, we can update the initializer and add : init( inventory: IdentifiedArrayOf<ItemRowViewModel> = [], itemToAdd: Item? = nil ) { self.inventory = [] self.itemToAdd = itemToAdd for itemRowViewModel in inventory { bind(itemRowViewModel: itemRowViewModel) } } … func add(item: Item) { withAnimation { bind(itemRowViewModel: .init(item: item)) itemToAdd = nil } }
— 30:00
And now when we run the app, we can delete any of the rows we start with. Next time: editing and duplicating items
— 30:18
We have finished our refactor, and though there were a few bumps along the way, we got through it, and it is already greatly simplifying our inventory list domain. The inventory list view and view model get to handle fewer responsibilities, and we push more domain specific responsibilities to the row view and row view model.
— 30:40
While it’s true that creating child view models is a little gnarly, the benefits from doing so are tremendous. If we wanted to, we could completely split the inventory list domain from the item row domain, putting them in completely separate modules and making it easier to build, test, and run them in isolation.
— 31:12
Don’t forget, but the whole reason we did this refactor was to add new features for editing and duplicating items to the row domain, so now let’s flex these muscles by adding more functionality to the row and show that it does not needlessly bloat the parent domain. We are going to add two more buttons to the row: one for editing the item, and one for duplicating the item. And just to make things interesting we are going to show the edit screen in a modal sheet and the duplicate screen in a popover. References SwiftUI Navigation Brandon Williams & Stephen Celis • Nov 16, 2021 After 9 episodes exploring SwiftUI navigation from the ground up, we open sourced a library with all new tools for making SwiftUI navigation simpler, more ergonomic and more precise. https://github.com/pointfreeco/swiftui-navigation Crash in Binding's failable initializer Brandon Williams and Stephen Celis We uncovered a crash in SwiftUI’s Binding initializer that can fail, and filed a feedback with Apple. We suggest other duplicate our feedback so that this bug is fixed as soon as possible. https://gist.github.com/stephencelis/3a232a1b718bab0ae1127ebd5fcf6f97 WWDC 2021: Demystifying SwiftUI Matt Ricketson, Luca Bernardi & Raj Ramamurthy • Jun 9, 2021 An in-depth explaining on view identity, lifetime, and more, and crucial to understanding how @State works. https://developer.apple.com/videos/play/wwdc2021/10022/ SE-0293: Extend Property Wrappers to Function and Closure Parameters Holly Borla & Filip Sakel • Oct 6, 2020 The proposal that added property wrapper support to function and closure parameters, unlocking the ability to make binding transformations even more powerful. https://github.com/apple/swift-evolution/blob/79b9c8f09450cf7f38d5479e396998e3888a17e4/proposals/0293-extend-property-wrappers-to-function-and-closure-parameters.md Collection: Derived Behavior Brandon Williams & Stephen Celis • May 17, 2021 Note The ability to break down applications into small domains that are understandable in isolation is a universal problem, and yet there is no default story for doing so in SwiftUI. We explore the problem space and solutions, in both vanilla SwiftUI and the Composable Architecture. https://www.pointfree.co/collections/case-studies/derived-behavior Downloads Sample code 0163-navigation-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 .