Bubble Level 🫧

This demo got me excited 🫧

I started by duplicating the app so it wouldn’t be a Swift Package, instead a plain old vanilla app.
Moving around with the device was fun, observing how the ‘bubble’ moves in response to how I tilt and roll my device. Let’s dive into how this lesson is introduced by 🍎

Starting off, we are in the MotionDetector class. We get a brief introduction to the library CoreMotion. This handles how we hold the device, which orientation it is held in, a pedometer, etc. Next, we discuss CMMotionManager, an object that manages motion services, for example: accelerometer & gyroscope data.

Step 3 shares with us the Timer class. Timers help your app measure when to fire (or not fire) a certain method or action. In this case, our Timer updates the pitch, roll & zAcceleration (yaw) value for our user to read.

Then we are going to store measurements for the pitch, roll & zAcceleration on our device for the user to read. In the demo we see they are Published properties, which means when the value changes SwiftUI will immediately observe them and update itself.

Step 5 references onUpdate, which is a closure. Documentation states that is Group code that executes together without creating a named function. We can pass this around like a variable and inject it into parameters in functions. In this app, our closure is empty but if we wanted, we could place something like a print statement there to run to the console with the message: ‘Updated

Following is our start function controlling MotionManager & the Timer properties. Step 7 checks if the device can actually measure movement (what if this app were run on a Mac mini? 🧐) Next we clear the check, our device can measure movement so we want to start measuring motion updates. After that we configure our timer property to repeat and run the function called inside of its closure.

Step 10, the function we run in the closure mentioned in Step 9, is reviewed in detail. Our properties are updated here if our conditional statement is satisfied. If we get data from our motion manager, the roll, pitch, & zAcceleration properties are updated then it calls our onUpdate closure. Step 11 highlights the conditional statement inside the updateMotionData function: if we aren’t receiving data from the motionManager object, we skip everything inside the if statement. On step 12, we learn why there is _ in at the end of line 38: it is the start of our closure, and we are not capturing an object, so instead we use an underscore ( _ ) and we free the memory that would be used to store & use said object.

Steps 13 to 18 we dive into the function which is called on line 39, updateMotionData(). Desktops, laptops or Mac minis would not be able to continue after line 47. They lack the appropriate sensors. iPads & iPhones can run this. We will check if we can store the data from our motionManager instance running its deviceMotion method. If so, we are moving on to line 48. We have here our tuple which will store our roll & pitch from the currentOrientation property. The result of our if statement, data, will be inserted as a parameter to the currentOrientation property, using data’s own property ‘attitude’. Step 16 describes what data.attitude is responsible for. Next we learn about the data.userAcceleration property. This is a piece we will review, it is an extension on UIDeviceOrientation. On line 49 we have our @Published property zAcceleration which will capture the value passed from userAcceleration.z property. onUpdate, if you will recall, is our empty closure. We call this like a regular function, with two parentheses at the end.

Steps 19 & 20 stops monitoring our motion sensors and turns our timer off. We are also removing our observer and making the orientationObserver property nil. In the final step, we are releasing the memory stored from this object when deinit is called. Calling deinit will in turn call stop() which turns off our motionManager as mentioned in steps 19 & 20.

At the very bottom, from the extension UIDeviceOrientation, we are using the sensors on the device to return specific orientations from the custom function adjustedRollAndPitch. This has a parameter which takes in a CMAttitude, which will return a tuple of roll & pitch, but if we wanted we could return roll, pitch & yaw values.

In Section 2, we pivot to a new file: OrientationDataView. This View will display left & right tilt (roll) or forward & backward tilt (pitch). The MotionDetector object is used here as a property by using the @EnvironmentObject macro. Since this is an ObservableObject, when the device moves & our detector object updates its values, the new data will propagate and SwiftUI will change the views to reflect the new value. Two properties, pitchString & rollString, both pass a Double value using a custom extension: describeAsFixedLengthString().
Horizontal reflects the roll, and vertical is the pitch. Inside the body, we have two Texts, one for Horizontal & the other for Vertical. These values update when the detector property updates its pitch & roll values. We set the display to be a system font with a monospaced design, that way all numbers & letters take an equal amount of space per letter.

Section 3 we check out the BubbleLevel file. Still observing the MotionDetector object with the @EnviromentObject macro, our view will update whenever the values on the MotionDetector change.
We have here three Circles: first large grey created at the bottom, next the accent which will respond to moves, finally the crosshair circle that stays centered on the grey bottom circle. Steps 4, 5 & 6 cover the constant properties range & levelSize. Range is defined as Double.pi, this has the system pass its approximation for π. LevelSize is the constant for the actual size of our circle, so we can reuse that value in many places and not get confused. BubbleXPosition & bubbleYPosition control our accent-colored circle. The heavy lifting is done inside of each property, so when we reference the property, we get that calculating done automatically 🪄. The next properties, verticalLine & horizontalLine will be reused so we will store templates here. Inside the body, we have our bottom Circle. This has the largest size (levelSize from earlier), and its foregroundStyle is set here. Beneath these settings is our overlay, which contains information for what needs to be drawn above our first Circle. Introducing the ZStack which will layer our views on top of each other like a cake 🍰. The next circle is our accent color Circle, the one which moves responding to user motion. Finally our crosshairs Circle is drawn over that. VerticalLine & HorizontalLine is used to make our crosshairs. The verticalLine & horizontalLine pair are used again for the large grey circle, this time we reuse levelSize constant for placement.

Section 4 is short. Here we learn how to expand the Double type using an extension. We will take our motion updates from the MotionDetector, which returns a Double, and format them so we have a fixed number of two digits: tens, ones, and two fraction parts, tenths and hundredths, as a String. The formatted function is used by not only Double, but other types in Foundation, via the BinaryFloatingPoint protocol. This is also where Double.pi come from deep in Double extension, but I digress. 🤓 We want the value represented as a simple number, so we use the .number function. Next comes the .sign modifier. We always want to display a (+/-) to denote positive & negative values. Below that is what I feel is most significant, the .precision modifier. This is what keeps our value formatted at +00.00, -00.01, etc.

Here’s a GitHub link of this app updated to conform to the Observable protocol

Standard

Leave a comment