Tobias shares a full story on how to build a prototype for a custom UI that visualizes instantaneous sound level as well as sound level over time. The talk is highly interactive and covers how Swift and SwiftUI are perfect for making prototypes, particularly when building custom UI.
p. 1. (Check audio input)!!!!!!! Let’s take a look at how we can use Swift and SwiftUI to prototype custom UI components.
p. 2. We’re going to build a completely custom UI to be able to visualize the sound level, both showing the instantaneous sound level as well as the sound level over time. So let’s just jump straight into how we could build such a component and how Swift and SwiftUI can help us prototype it along the way.
p. 3. To show the instantaneous sound level we will build a gauge that looks similar to a speedometer in a car.
p. 4. We start out with a plain circle, and as a first change, we’ll try to add a border to it.
p. 5. We can do that with a stroke that follows the outline of the circle. You can see here that we’ve hardcoded the line width.
p. 6. To make the code more tweakable as well as readable, we’re going to move it into a separate property. This removes friction when finding what can be tweaked since we group all our constants in the same place at the top of our code. We also get the opportunity to explain our constants a bit, so here we call it the ‘thickness’ of our Gauge. Finally, it helps us being able to reuse the thickness value if or when it’s needed for other parts of this view.
p. 7. So the next thing we want in our prototype is to create a natural start and endpoint for our gauge. A speedometer typically does this by leaving a gap in the circle, so that’s what we’ve done here by trimming our circle before applying the stroke. Again, we’re keeping our constants in properties at the top of our view.
p. 8. A final step we can do to make our gauge more usable is to use vars instead of lets for our properties. This enables Swift to synthesize our initializers automatically. The initializer even supports our default values, and this flexibility enables us to tweak things locally, without giving up on customizability from the outside. In general, I like exposing many properties from small views like this, to avoid locking our self in too early. It leaves us open and flexible to change things in a later tweaking stage.
p. 9. What our UI needs now is some colors, so let’s add all of them. Or, at least most of them.
p. 10. Here we add a gradient that is masked out by our Gauge. When we look at the code here, it feels a bit wrong to me. Let me explain why. The gradient wraps our Gauge, and what we do mentally is that we’re applying some styling to the Gauge. So I think we should reverse this order and nesting.
p. 11. Let’s imagine that we want this kind of dot syntax instead. This isn’t the normal SwiftUI API,
p. 12. but we can implement it with small extension on the View type. As you can see it’s fairly straight forward to use a Swift language feature like Extensions to improve the usability of SwiftUI.
p. 13. The new dot syntax here also allows us to quickly comment the styling call for adding our gradient in and out as we please. When we remove developer friction like we did here, we enable ourselves to iterate more quickly and avoid blocking our creative process.
p. 14. It’s often also quite convenient to be able to toggle our styling on and off programmatically. Let’s say that this gradient depends on other tweaks in a prototype, and we want to be able to control whether or not it’s colorized, just like you can see in the code here. To be able to conditionally apply calls to View types in this convenient way, we’re going to extend SwiftUI again.
p. 15. We make an extension on the View type that takes a boolean value, as well as a transformation closure. Depending on our condition, we decide to transform the view itself or not. Note that everything is wrapped in a view Group, which is needed since the types of the two returns might be different. The two types are the transform of self, and then self itself. This also means that flipping the transformation on and off can’t animate nicely, since SwiftUI’s diffing algorithm used for animations, is based on the types themselves.
p. 16. In our case, we don’t mind about this trade-off since we’re NOT going to use it for user-facing changes, ONLY for tweaks in the prototyping process.
p. 17. For some parts of the tweaking process it can be useful to be able to tweak the prototype without changing the code, but instead to be able to tweak it interactively. For instance when we want to get feedback from our colleagues, or when we have a lot of parameters we want to iterate through simultaneously. Let’s say that we want to be able to tweak the thickness of our gauge this way.
p. 18. In this case, a slider is fitting, but in other cases, toggles and text fields can also be quite convenient. In general, we want to go for a standard input control, since we don’t want to spend to much time setting it up. In the code here we make a State property for the thickness of our Gauge and set up a slider to modify it. We add the slider right next to our prototype in our UI with a matching label and the current value and make it ‘BLUE’. Let me just move my cursor over to the other screen and show how easily we can change the thickness of gauge now.
p. 19. I tend to add sliders for many parameters at a time to be able to adjust them simultaneously. So here I’ve added another one that is ‘ORANGE’. It is for the dashing of the border so we can tweak the look of the gauge a bit. (Thickness ↑, Dash ↓) We can go for a thick look with narrow gaps. (Thickness ↓, Dash →) Or we can make it thinner with a more even spacing. I think we'll keep the last look here. We can really see how fast we can change the overall look, with very little friction, using these interactive controls.
p. 20. A real speedometer has labels to show what values different positions represent. To be able to position them, we’re going to take a look at layout. When we want to layout things for prototypes, we could prioritize quick over perfect, but we also sometimes want to try out something highly customized that can’t be done quickly. So we need to find a balance. The stack views and frame modifiers are our go-to thing for most things when dealing with layout. Here we start out with a ZStack.
p. 21. We add all the labels that we want to position around the gauge to our stack, and since they’re laid out along the Z-axis, their sizes and positions don't affect each other. This gives us the needed freedom for some custom layout of these labels.
p. 22. The next step is to use the frame modifier, which works a bit different that what we might expect. So to better understand how the frame call works, I’m filling the background of our labels with a color.
p. 23. Now we add our frame call where we specify that we want to have the label stretched infinitely in the vertical direction, and as we can see it stretches to the height of its outer view. I’ve also added a border ‘AFTER’ the frame call, so we can see how the size of the label ITSELF didn’t change here.
p. 24. By default, the frame call aligns its content in the center, but here we specify that we want the label to be aligned towards the top of the frame.
p. 25. And, as a last step, we can now rotate the framed labels using the built-in .rotationEffect call in SwiftUI.
p. 26. To see our final result we’ll remove the helper colors. It’s quite powerful how little math calculations we had to do here, even though our labels are laid out in a fairly customized way. That doing custom layout is THIS accessible in SwiftUI REALLY helps in building custom UI components.
p. 27. Let’s say that we want to fill the trimmed gap in our gauge with some brand text. We could do it in a similar way to what we just did before, by taking each character in our string and putting them in separate labels. Buuuut, the kerning is all off. So, in this case, we want more control, so we can do a more precise calculation of the angles we need for each string character. Let’s see at how we can use another SwiftUI feature to give us the sizes of each of the character label, so that we can fix our kerning issue.
p. 28. The first step is to use the background modifier that stretches the entire size of our text label.
p. 29. Secondly, we can use a GeometryReader to get the size of our background view. To be able to store this size to be used outside the scope of this GeometryReader we can use something called a PreferenceKey.
p. 30. First, we have to implement a PreferenceKey that can aggregate the sizes of each label for us. That is what you see implemented here.
p. 31. The PreferenceKey works by setting a preference on a view, but we don’t even have a view inside our GeometryReader. So we add a transparent one, and then we store our sizes into it.
p. 32. Now we can listen for our preference key on any outer view and use them as we want.
p. 33. That’s what we do here so we can lay out our text labels more precisely where we use the sizes of the individual character labels to calculate more precise angles. p. 34. When we remove our debug borders we can see that we now have much nicer spacing between the characters. From time to time, you might run into a wall when trying to do custom layout in SwiftUI, and this technique using the PreferenceKey can be a good way to climb those walls.
p. 35. Our prototype is coming along nicely, but so far it is very static. We’re building a prototype here that is going to be driven by sensor data, so we should make sure to prototype THAT aspect of it TOO. We’ll use some random data to mock our sound level. Creating the random values themselves couldn’t be easier in Swift.
p. 36. To be able to drive our prototype with our random values we create an ObservableObject. We set up a timer to provide a new random value at a suitable time interval, and publish them through a property.
p. 37. In the place where we use our Gauge, we create an @ObservedObject property with our Audio object, and now we can use the decibel level from it to drive the trim of our Gauge. No more steps are needed since SwiftUI now takes care of re-rendering our prototype with each new random value.
p. 38. Being able to quickly mock our data this way is very powerful, but we can also feed live sensor data to our prototype using the exact same technique. It just requires a bit more work, depending on what type of sensor data we want to use.
p. 39. For getting access to the microphone in my Macbook here, it only takes the work you see here, besides some settings for privacy entitlements. It’s not that much code, but of course, the sensor frameworks might not be familiar to us, so we need to consider if it will be worth it for a prototype.
p. 40. The pay off is that we can now test our prototype in a much more realistic way. You might be able to notice that the sound level now follows. WHAT. I’M. SAYING!
p. 41. Let’s say we’re done with our gauge and we want to visualize the history of our sound level next. We’re going to make a circular graph that is suitable for this radar style grid.
p. 42. To make our circular graph we need to do custom drawing and we can use the Shape type for that. Then the "ONLY" job for us is to create a Path given a certain rectangle.
p. 43. Based on that rectangle we can now set up and calculate our Path.
p. 44. You can see that we can find the center and our radius using the rectangle here, which we can then use to calculate the points on our circular graph.
p. 45. We’ll style the graph with a gradient so that the values in our graph match those of our gauge. As you might have noticed we use our handy little call extension from the very beginning of this talk, to apply our gradient here too.
p. 46. Animation literally means bringing things to life, so let’s try to finalize our radar graph with a little bit of that.
p. 47. The first thing we’ll do is to add a line on top of our graph to indicate the most recent sound level. Secondly, we fade the graph out a bit as we go back in history, to put emphasis on the most recent sound levels captured.
p. 48. Since both of these are rotated using modifiers that are within the same view scope, we can animate both of them with a single animation call. We animate linearly using a duration that matches how often we receive new sound levels, giving it this continuous scanning look.
p. 49. The movements in the Gauge is now starting to look a bit choppy right next to our smooth graph, so let’s add a simple animation to the Gauge too. (move to slider ↑) We can dial our animation in to match the slooow radar scanner, But I don’t think we should go that far. (slider: 0.2). Once you start tweaking your animations like this, you’ll soon realize how perfect SwiftUI is for prototyping animation heavy design, not even just UI.
p. 50. Ok, so we’re coming to an end and our prototype is ready for some final testing, so I want the help from every single one of you. Now, please. See how much noise you can make and if we can make it clip! (blow into microphone a bit to help out if needed) (nervous laughter) Perfect! So, we’ve seen how Swift and SwiftUI can empower us when prototyping custom UI and a few things we could do to make it fit our prototyping needs a bit better. For all the things we did here, we never had to leave Swift and SwiftUI. Not for custom layout, custom drawing, and last but not least: animations. Swift and SwiftUI is quite a powerful combination, and in particular when it comes to prototyping.
p. 51. A lot of folks have shared how to do things in SwiftUI, so a big thanks to all these people up here.
p. 52. THANK YOU!