EP 109 · Composable SwiftUI Bindings · Jul 20, 2020 ·Members

Video #109: Composable SwiftUI Bindings: The Point

smart_display

Loading stream…

Video #109: Composable SwiftUI Bindings: The Point

Episode: Video #109 Date: Jul 20, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep109-composable-swiftui-bindings-the-point

Episode thumbnail

Description

It’s time to ask: “what’s the point?” If composing bindings is so important, then why didn’t Apple give us more tools to do it? To understand this we will explore how Apple handles these kinds of problems in their code samples, and compare it to what we have discovered in previous episodes.

Video

Cloudflare Stream video ID: cc73efff55fdd4ce3a44fc6b19f96bd2 Local file: video_109_composable-swiftui-bindings-the-point.mp4 *(download with --video 109)*

References

Transcript

0:05

So we are now accomplishing everything that we did with the unwrap binding helper, but in a more general fashion. We can instantly abstract over any case of any enum in order to show case-specific UI controls. As soon as the state flips from one enum case to a different enum our UI will instantly update and we’ll get bindings for the data in that case so that sub views can make changes to the data and have it instantly reflected in our model.

0:28

This is incredibly powerful. We have truly unlocked some new functionality in SwiftUI that was previously impossible to see with the tools Apple gave us. Apple simply does not make it easy for us to use enum for state in SwiftUI, and instead all of the tools are geared towards structs.

0:54

But, here on Point-Free we know the importance of putting structs and enums on equal footing. Once we have a tool designed for structs we should instantly start looking for how the equivalent tool looks like for enums, and once we have a tool designed for enums we should instantly start looking for how the equivalent tool looks like for structs. And this is what led us to discover a new way to transform bindings, which also led us to a better way to construct our view hierarchy. We can now model our domain exactly as we want, and we can have the view hierarchy naturally fall out of that domain expression rather than creating a bunch of escape hatches to project information out of the domain in an imprecise manner. Adding an inventory list feature

1:32

But let’s push these tools even further. Let’s add a new screen to our app that shows a list of inventory as well as a new flow that allows the user to add new inventory. This sounds straightforward enough, but we will again seen that doing it can wreak havoc on our nicely modeled domain.

1:51

Let’s start by getting a view model into place that will power the inventory list. At its bare essentials it will need to track an array of inventory that is currently being displayed: class InventoryViewModel: ObservableObject { @Published var inventory: [Item] init( inventory: [Item] = [] ) { self.inventory = inventory } }

2:39

And then we want a new view that can show a list of this inventory. Creating this view is pretty straightforward so we are not going to show all of the steps and instead just paste in the work: struct InventoryView: View { @ObservedObject var viewModel: InventoryViewModel var body: some View { NavigationView { List { ForEach(self.viewModel.inventory, id: \.self)) { item in HStack { VStack(alignment: .leading) { Text(item.name) if item.status.isInStock { Text("In stock: \(item.status.quantity)") } else { Text( "Out of stock" + ( item.status.isOnBackOrder ? ": on back order" : "" ) ) } } Spacer() item.color.map { color in Rectangle() .frame(width: 30, height: 30) .foregroundColor(color.toSwiftUIColor) .border(Color.black, width: 1) } } .buttonStyle(PlainButtonStyle()) .foregroundColor(item.status.isInStock ? nil : Color.gray) } } .navigationBarTitle("Inventory") } } }

3:10

This does the basics of displaying the list of each inventory item, and each row shows off some additional meta data like its color, and whether or not it is in stock.

3:42

We can also get a SwiftUI preview in place with a bunch of inventory in all combinations of state: struct InventoryView_Previews: PreviewProvider { static var previews: some View { InventoryView( viewModel: InventoryViewModel( inventory: [ Item( name: "Keyboard", color: .blue, status: .inStock(quantity: 100) ), Item( name: "Charger", color: .yellow, status: .inStock(quantity: 20) ), Item( name: "Phone", color: .green, status: .outOfStock(isOnBackOrder: true) ), Item( name: "Headphones", color: .green, status: .outOfStock(isOnBackOrder: false) ), ] ) ) } }

4:08

All of this code is the straightforward, not very interesting code. The interesting part is when we try to introduce a new feature that lets us add new inventory. And the reason this is interesting is because implementing this feature is not quite as straightforward as it seems at first.

4:23

To start, let’s add a new trailing navigation button for adding new inventory items: .navigationBarItems(trailing: Button("Add") { })

4:36

When this button is tapped we probably want to just forward that to a method on the view model so that the view model can run its logic: func addButtonTapped() { } … .navigationBarItems( trailing: Button("Add") { self.viewModel.addButtonTapped() } )

4:59

And so the question is: what should the view model do to represent that we want to add a new inventory item?

5:04

Well, we want to present the ItemView that we developed previously in a modal so that you can enter the details of the new inventory, and then either save the new item or cancel to discard. Remember that in order to construct an ItemView we have to initialize it with an honest binding of an honest Item . And in order to show a modal we can use one of two APIs: .sheet(item: <#Binding<Identifiable?>#>, content: <#(Identifiable) -> View#>) .sheet(isPresented: <#Binding<Bool>#>, content: <#() -> View#>)

5:30

The first API takes a binding of an optional so that when the binding’s value becomes non- nil , it will trigger the content closure, handing it an honest value, and whatever view is returned from that closure will be presented in a modal. Then, once the binding goes to nil it will trigger a dismissal of the modal.

5:46

The second API is similar, except it uses a simple boolean binding to determine whether or not if the modal is shown, and because the binding doesn’t hold data of any interest, its content closure is not handed any data, it just needs to return a view from nothing.

6:00

Both of these APIs are handy, and make it possible to get started with modals very easily, but they are missing a pretty significant piece of functionality. What if we want to pass along bindings to the modal view so that whatever actions the user performs in the modal can be reflected in the state of the parent view?

6:15

To see why this is what we want and why these APIs aren’t enough to do that, let’s try showing our ItemView in a modal. We need to introduce some new state to our view model that controls whether or not the modal is shown. We can do this by holding onto an optional Item value that represents the draft of the inventory item we want to add: @Published var draft: Item?

6:43

We can even fill in the addButtonTapped stub we created because we want it to set draft to be a non- nil value, which should then trigger the modal to be shown: func addButtonTapped() { self.draft = Item( name: "", color: nil, status: .inStock(quantity: 1) ) }

7:01

To make this state drives the modal we can use the sheet modifier that takes a binding of an optional: .sheet(item: self.$viewModel.draft) { draft in }

7:19

And Item must be Identifiable to work in this API. struct Item: Hashable, Identifiable { let id = UUID() … }

7:43

Inside the sheet closure we are handed a draft, but it’s of type Item . That is, it’s literally an item, not a binding to an item. That’s not super useful right now because our ItemView requires a binding: ItemView(item: <#Binding<Item>#>)

8:09

So how do we get a binding of an Item ? Well, we’ve already cooked up the helpers to do that, so we can just make use unwrap to perform that transformation: .sheet(item: self.$viewModel.draft) { draft in self.$viewModel.draft.unwrap().map { item in ItemView(item: item) } }

8:48

Though now we aren’t even using the draft provided to the closure so we can just ignore it: .sheet(item: self.$viewModel.draft) { _ in

8:54

If we run the preview now we will see that the modal shows when we tap the “Add” button, but we don’t have any way of saving the item.

9:17

We can add the buttons by chaining onto the ItemView we construct for the modal: .sheet(item: self.$viewModel.draft) { draft in self.$viewModel.draft.unwrap().map { item in NavigationView { ItemView(item: item) .navigationBarItems( leading: Button("Cancel") { }, trailing: Button("Save") { } ) } } }

9:59

Now when we run the preview we see the modal come up with the “Cancel” and “Save” buttons, but tapping them doesn’t do anything. That’s because we need to create new methods in our view model to implement that logic, and then invoke those endpoints from these closures: func cancelButtonTapped() { self.draft = nil } … func saveButtonTapped() { if let item = self.draft { self.inventory.append(item) } self.draft = nil } … ItemView(item: item) .navigationBarItems( leading: Button("Cancel") { self.viewModel.cancelButtonTapped() }, trailing: Button("Save") { self.viewModel.saveButtonTapped() } )

10:51

And now when we run the preview we see that hitting “Save” adds a new item to our inventory, and hitting “Cancel” will dismiss the modal without changing the state. It’s worth noting that cancelling performs a programatic dismissal of the modal. Just the very act of our draft state being nil ‘d out causes SwiftUI to dismiss the modal, and that’s really cool.

11:35

While this works just fine, I think we can make it a little nicer. It’s strange that we need to ignore the argument of the content closure, and strange that we have to transform the self.$viewModel.draft binding twice, once to show the sheet and then again to unwrap the binding.

11:53

What if we could cook up a sheet helper method that allowed allowed us to simultaneously show the sheet when the binding value becomes non- nil and also transform the binding into an honest value. That might look something like this: .sheet(unwrap: self.$viewModel.draft) { item in NavigationView { ItemView( item: item, onCancel: { self.viewModel.cancelButtonTapped() }, onSave: { self.viewModel.saveButtonTapped() } ) } }

12:20

And that’s looking much better. It’s removed the _ in and unwrap().map noise, and it’s more true to what we want to represent in this view. We’d like to safely unwrap the binding, show the sheet, and hand that binding off to the modal view so that it can safely make changes to the data it holds, all the while those mutations are being reflected in the parent.

12:41

But this isn’t compiling yet because we haven’t actually implemented this method, so let’s do that. It can done by adding a new method to the View protocol that takes a binding of an optional value, as well as a content closure that transforms a binding of an honest value into a view: extension View { func sheet<Content>( unwrap item: Binding<Item?>, @ViewBuilder content: @escaping (Binding<Item>) -> Content ) -> some View where Content: View { } } To implement this method we can simply do what we were doing before, which is to use the standard .sheet view modifier with an unwrap().map on the inside:

14:29

Technically this builds, but to generalize we shouldn’t be using our Item type in here, instead we should take any generic, identifiable Item : extension View { func sheet<Item, Content>( unwrap item: Binding<Item?>, @ViewBuilder content: @escaping (Binding<Item>) -> Content ) -> some View where Item: Identifiable, Content: View { self.sheet(item: item) { _ in item.unwrap().map(content) } } }

14:53

And now the app still compiles, and it works exactly as it did before. Adding an inventory duplication feature

15:12

But let’s kick up another notch! Let’s further add the ability to easily duplicate any existing inventory item. We will do this by adding a button to the item row in the list: Button(action: { }) { Image(systemName: "doc.on.doc.fill") } .padding(.leading)

15:50

And tapping this button will invoke an endpoint in the view model: Button(action: { self.viewModel.duplicate(item: item) }) {

15:59

And the view model method will simply start up a new draft that is a copy of the item we tapped on: func duplicate(item: Item) { self.draft = item }

16:12

This seems straightforward enough, but it’s slightly incorrect. Technically this draft will have the same id as the item passed in, and it will be bad for us to have multiple items with the same id in our inventory. To fix we can just construct an item from scratch: func duplicate(item: Item) { self.draft = Item( name: item.name, color: item.color, status: item.status ) }

16:32

We could also move this logic in a duplicate method on Item : // item.duplicate()

16:38

But we’ll keep it simple for now.

16:39

But, with that one very small change our new feature is already working. If we tap the icon on any row we will be presented with a modal ItemView that is already pre-filled with everything from the original item. And if we hit “Save” we get a new row in in our inventory, and if we hit “Cancel” everything is discarded.

17:09

So we think it’s pretty cool how we were able to leverage all the machinery we built up early in this series of episodes, things like the unwrap method and case path subscript. And all of this is only possible thanks to our unwavering resolve to make sure that enums are not left behind in the dust of structs. SwiftUI provides powerful tools for constructing views that are driven off of state that is modeled by structs, but sadly leaves enums out in the cold, even though they are crucial for proper domain modeling. What’s the point?

17:37

So, while this is all very cool, it’s time to end this series of episodes like we end every series of episodes: by asking “what’s the point?” This is our chance to prove to everyone that what we are doing is legitimately useful, and not just some highfalutin exploration that isn’t actually useful in everyday code.

17:54

And in this case it’s definitely worth asking because it seems like such an oversight that Apple would provide us the tools to properly model our domain types but wouldn’t provide the tools to then use those types with SwiftUI views. So, how does Apple suggest we attack these kinds of problems?

18:12

Well, unfortunately not many of Apple’s code samples cover these kinds of complex, real world problems. They tend to be more concerned with basic navigations and interactions. However, we did find a SwiftUI tutorial from 2019 that addressed a similar problem.

18:27

It’s from a session called “ Working with UI controls ”, and it deals with the concept of modeling a screen that can be put into an “edit mode”, where you can make changes to the data on screen, and then either save those changes or discard them. They even use the terminology of a “draft” to represent the temporary state of the screen while editing, and so they really are attacking essentially the same problem that we have just described.

18:51

However, they solve it quite differently. Rather than use an optional value that represents whether or not we are editing a draft they use a @State value to model some local, discardable state. Let’s try to recreate that style with our current application to see how it compares to what we have right now.

19:15

The crux of the idea is to not model the temporary, discardable state in the view model and instead use the @State property wrapper, which needs to be initialized immediately: struct InventoryView: View { @State var draft = Item( name: "", color: nil, status: .inStock(quantity: 1) ) @ObservedObject var viewModel: InventoryViewModel … }

19:59

And because we are no longer using an optional value to represent the draft, we must also introduce some additional state to keep track of whether or not we are in the mode of adding a new item: struct InventoryView: View { @State var draft = Item( name: "", color: nil, status: .inStock(quantity: 1) ) @State var isAdding = false @ObservedObject var viewModel: InventoryViewModel … }

20:17

Then we can drive the displaying of a sheet off of this boolean value: .sheet(isPresented: self.$isAdding) { NavigationView { ItemView(item: self.$draft) .navigationBarItems( leading: Button("Cancel") { }, trailing: Button("Save") { } ) } } We’ll wanna do something about those action closures at some point, but let’s take care of a few other things before doing that.

21:13

In order for the modal to ever show we have to mutate the isAdding field somehow. Previously we were doing that logic over in the view model, but we can no longer do that because the isAdding field is only available to us in the view. So, we need to tap into the action closure of the “Add” button so that we can do that mutation: .navigationBarItems( trailing: Button("Add") { self.isAdding = true // self.viewModel.addButtonTapped() } )

21:50

And we’ll wanna make sure to do the same with the duplicate button, except we also have to remember to mutate the draft to represent the item being duplicated: Button( action: { // self.viewModel.duplicate(item: item) self.isAdding = true self.draft = Item( name: item.name, color: item.color, status: item.status ) } ) { Image(systemName: "doc.on.doc.fill") } .padding([.leading])

22:29

And now we can fill in the onCancel and onSave closures when we create the ItemView . One thing different is that we need a new method on the view model for saving the item that takes the item to be saved. This is because we don’t have access to the item in the view model, it’s only local to the view. func saveButtonTapped(item: Item) { self.inventory.append(item) } … .sheet(isPresented: self.$isAdding) { NavigationView { ItemView(item: self.$draft) .navigationBarItems( leading: Button("Cancel") { self.isAdding = false }, trailing: Button("Save") { self.isAdding = false self.viewModel.saveButtonTapped(item: self.draft) } ) } }

23:24

And now when we run the preview we see that it seems to work.

23:47

Well, seems to. There are some subtle bugs. If we add an item and then try adding another we will see that when the modal comes up initially it still holds the data from the previous time the modal was shown. This is because we aren’t clearing the draft after it’s saved, so let’s do that: .sheet(isPresented: self.$isAdding) { NavigationView { ItemView(item: self.$draft) .navigationBarItems( leading: Button("Cancel") { self.isAdding = false }, trailing: Button("Save") { self.isAdding = false self.viewModel.saveButtonTapped(item: self.draft) self.draft = Item( name: "", color: nil, status: .inStock(quantity: 1) ) } ) ) } }

24:38

But that’s not all, if we bring up the item modal, make some changes, and then cancel, we will see that when we bring the modal back up it still has all the data. So we also need to make sure to clear the item when cancelling: .sheet(isPresented: self.$isAdding) { NavigationView { ItemView(item: self.$draft) .navigationBarItems( leading: Button("Cancel") { self.isAdding = false self.draft = Item( name: "", color: nil, status: .inStock(quantity: 1) ) }, trailing: Button("Save") { self.isAdding = false self.viewModel.saveButtonTapped(item: self.item) self.draft = Item( name: "", color: nil, status: .inStock(quantity: 1) ) } ) ) } }

25:00

We introduced a subtle glitch, though. When we tap “Cancel,” as the modal is dismissing we can see the state form reset as it’s being dismissed.

25:21

But this still isn’t quite right because we can also dismiss the modal by swiping down on it, and we won’t clear the data in that case. To see this, let’s tap the duplicate icon for an item, then swipe away the modal, and then tap “Add”, and we will see that the modal is erroneously pre-filled with data that shouldn’t be there.

25:41

To clear this state we need to further listen to the onDisappear of the modal so that we can clear yet more state: .onDisappear { self.draft = Item( name: "", color: nil, status: .inStock(quantity: 1) ) }

26:07

It’s a bit of a bummer that we need to clean up state in multiple places in the view. We didn’t have any of these problems when using the binding transformations because our model and presentation were 100% always in sync. The sheet only shows when the value is non- nil , and the moment it becomes nil the sheet is dismissed. There is no chance of accidentally showing the sheet with old, stale data because we need to construct a value to even show the sheet in the first place!

27:03

One possible work around all of these problems right now would be to just clear the draft right when we tap the “Add” button: .navigationBarItems( trailing: Button("Add") { self.isAdding = true self.draft = Item( name: "", color: nil, status: .inStock(quantity: 1) ) } )

27:52

But even this isn’t great because even after we save or cancel the draft we will have left over data hanging around in our view even though it isn’t used anymore. We may even accidentally refer to that data to perform some logic without knowing that the modal isn’t even currently showing. Whereas when our state is optional we will always have a single, precise way of knowing whether or not the modal is showing, rather than needing to check two different values, first the boolean and then the actual data.

28:26

What we are seeing all over again is that we are fighting against properly modeling the domain of this feature, and as a result we are having to do a bunch of clean up and a bunch of workarounds to get things working. The entire reason the choices we are making with how to implement this view is due to the fact that our domain is not modeled correctly. We have a draft value that is non-optional, and thus must always hold a valid Item value, even if the new item view isn’t even being shown.

28:54

Back in the day before we had proper representation for algebraic data types in our programming languages one would turn to so-called “sentinel” values to distinguish a special value from the rest. For example, when searching for the index of an element in an array, an API may decide to return a -1 to represent a “not found” index. You would need to do something like this if you didn’t have proper support for optional data types for which you could return nil to represent that an index was not found.

29:23

So, I suppose we could also cook up an “invalid” item: extension Item { static let invalid = Item( name: "Invalid", color: nil, status: .inStock(quantity: -1) ) }

29:39

And then use this use this as the default value of our draft: @State var draft = Item.invalid

29:49

And now I suppose we’d have some way of differentiating between between a valid draft that is currently being edited and one that had be invalidated. But the compiler isn’t doing anything to help us keep track of these states, it’s on us to remember that this distinction between values exists and to always uphold its invariants, in particular making sure to invalidate the draft when isAdding switches to false .

30:21

It’s impossible to know for certain that we got it 100% correct, we can only hope. Six months from now this feature could get a lot more complicated or we could forget all the intricacies of how we modeled this domain and it would be quite easy to mess it up. Or more likely, one of our colleagues may not know all the ins-and-outs of how this is designed, and since the compiler isn’t keep them in check they may introduce a subtle bug that breaks the invariants that we want to keep.

30:48

So, what we are seeing is that the official way Apple suggests to model this problem has its downsides, and leads us to a lot of the problems we saw at the beginning of this series of episodes when we were not using the correct tools for doing domain modeling. What we really want to do is just use an optional for the draft to simultaneously represent the idea that we may be in draft mode and we have a draft item we are operating on. But if we do that all of the tools for deriving bindings breaks down.

31:20

Whereas, on the other hand, the new tools we developed for this episode have allowed us to freely use enums for our domain modeling while still allowing us to construct our views in a sensible manner. In fact, having these tools at our disposal only makes it that much easier to refactor our domain to be even stronger, and to improve our application. Domain modeling: adding vs. duplicating

31:49

Let’s do one last change to our application to properly differentiate between the case of adding a new item versus duplicating an existing item. Right now our draft is held as a simple optional Item that determines whether or not the draft view is active: @Published var draft: Item?

31:59

Let’s beef it up to a proper enum so that we can differentiate between the case of adding a new item versus duplicating an existing item: // @Published var draft: Item? @Published var draft: Draft? … enum Draft: Identifiable { case adding(Item) case duplicating(Item) var id: UUID { switch self { case let .adding(item): return item.id case let .duplicating(item): return item.id } } }

32:53

This breaks a few things. First, in the view model we need to update some of our logical endpoints to work with this enum rather than the optional: func addButtonTapped() { // self.draft = Item( // name: "", // color: nil, // status: .inStock(quantity: 1) // ) self.draft = .adding( Item( name: "", color: nil, status: .inStock(quantity: 1) ) ) } … func saveButtonTapped() { switch self.draft { case let .some(.duplicating(item)), let .some(.adding(item)): self.inventory.append(item) case .none: break } self.draft = nil } … func duplicate(item: Item) { // self.draft = Item( // name: item.name, // color: item.color, // status: item.status // ) self.draft = .duplicating( Item( name: item.name, color: item.color, status: item.status ) ) }

34:25

And then in the view we can go back to the old code driven by our view model instead of local @State , and use our .sheet(unwrap:) helper to unwrap this optional draft to get a binding of an honest draft: .sheet(unwrap: self.$viewModel.draft) { draft in }

35:06

And then in this closure we can use our new case path subscripting tool to match against one of the two Draft cases, and that gives us the opportunity to add a little bit of extra customization, such as changing the navigation title of each view: .sheet(unwrap: self.$viewModel.draft) { draft in draft[/Draft.adding].map { item in NavigationView { ItemView(item: item) .navigationBarItems( leading: Button("Cancel") { self.viewModel.cancelButtonTapped() }, trailing: Button("Save") { self.viewModel.saveButtonTapped() } ) .navigationBarTitle("Add new item") } } draft[/Draft.duplicating].map { item in NavigationView { ItemView(item: item) .navigationBarItems( leading: Button("Cancel") { self.viewModel.cancelButtonTapped() }, trailing: Button("Save") { self.viewModel.saveButtonTapped() } ) .navigationBarTitle("Duplicate item") } } }

36:43

And with our preview running we can see each the navigation title reflecting each case, and everything else works just the same.

37:12

It’s really cool to see how all of these composition operators work together. We can first use the .sheet(unwrap:) operator to safely unwrap our binding of an optional, and then further chain onto it so that we can dive into each of the cases of the draft.

37:30

And so this is the point of this series of episodes. We cannot simply be beholden to whatever tools Apple gives us, we have to feel empowered to create the tools that we know are useful, and run with it. In particular, we know enums are a powerful way to model domains because they allow us to chip away at the invalid states until we are left with only the values that makes sense. But, if we make use of enums in SwiftUI we instantly run into road blocks and so we turned to case paths to unblock us.

38:05

And it’s worth nothing that even the new capabilities that function builders have obtained recently, such as being able to use if let , if case let and switch directly in a view, those things still do not solve the problem we are trying to solve here. They allow you to conditionally show a particular view, but they have nothing to say on how you construct bindings to hand over to those views so that any changes made in the view can instantly be reflected in the parent.

38:36

And perhaps the over-arching, meta “what’s the point” to this episode is that we have yet again seen the power of applying a particular pattern of thinking to programming. Over and over on Point-Free we have been on the search for new forms of composition, often leaning on the patterns we have previously learned, such as with map , zip , flatMap and pullback . And a great place to apply that knowledge is to find an area of Apple’s API that is highly tuned for structs and key paths, and try to understand what it would mean to use enums instead of structs and case paths instead of key paths. You may uncover some really interesting new forms of compositions and ways to model your data.

39:25

Well that’s it for this one, until next time! References Working with UI Controls Apple In this episode we recreated a technique that Apple uses in one of their SwiftUI code samples. In the sample Apple creates a UI to handle editing a profile with the ability to either save the changes or discard the changes: Note In the Landmarks app, users can create a profile to express their personality. To give users the ability to change their profile, you’ll add an edit mode and design the preferences screen. You’ll work with a variety of common user interface controls for data entry, and update the Landmarks model types whenever the user saves their changes. Follow the steps to build this project, or download the finished project to explore on your own. https://developer.apple.com/tutorials/swiftui/working-with-ui-controls CasePaths Brandon Williams & Stephen Celis CasePaths is one of our open source projects for bringing the power and ergonomics of key paths to enums. https://github.com/pointfreeco/swift-case-paths Collection: Enums and Structs Brandon Williams & Stephen Celis Enums are one of Swift’s most notable, powerful features, and as Swift developers we love them and are lucky to have them! By contrasting them with their more familiar counterpart, structs, we can learn interesting things about them, unlocking ergonomics and functionality that the Swift language could learn from. https://www.pointfree.co/collections/enums-and-structs Downloads Sample code 0109-composable-bindings-pt3 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 .