App Frameworks • iOS, macOS, tvOS • 31:56
The Foundation framework, which includes the root object class, classes representing basic data types such as strings and byte arrays, and collection classes for storing other objects, has added new support for specifying Units and Measurements. Understand how to model your measurements and convert within dimensions. Learn from the experts how to use and surface Units and Measurements in your interfaces.
Speaker: Daphne Larose
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
Hi, my name is Daphne Larose. I'm a software engineer on the Foundation Team. Welcome to my talk. Thanks to those of you for sticking around. I know it's kind of late in the day. So, I wanted to start by thinking about apps that we commonly associate with measurements. The first thing that comes to mind for me, I don't know about you, are converter apps. Makes sense, convert one unit to another. But I wonder though, if there are other apps that also use measurements but maybe in a less obvious way.
I don't know, but when I think about, you know, measurements, it's interesting, right, because we use them all the time. We just don't normally think about them as explicitly, and they can pop up in really surprising ways, one of those ways being when they're not in terms of units that are common for whatever our current context is.
So, let's say I'm in France, and I'm using an app that is calculating road distance, and I expect the measurement to show up in terms of kilometers, but they show up in terms of miles. It's like, hmm. It makes for kind of a jarring, not so great user experience. And so in this talk, we're going to introduce to you a new suite of APIs that helps you ensure that this never happens in your app.
So, if we think again about this question of, like, other apps that we associate with measurements, I had mentioned converter apps, which is still true. But another example could be games, right? So, today we're talking about this game that I'm working on. It's called Jammin' in the Streetz with a z because z's are cool, and I want my game to be cool.
So, the premise of the game is that we have to get our player to dance from level to level. I have some pretty set goals for this game. One, I need this game to be super fun. Like, the most fun a game has ever been, ever. Two, tons of emoji, because I love emoji. Three, it needs to be available worldwide. I want it everywhere.
So, when we think about some of things that this game would include, right, every round would be called a jam session, and within this jam session, it's tracking the total amount of time that it takes our player to get from level to level, the distance that they've traveled, the number of dance movements they've performed, and maybe even, like, the rate at which they're traveling. But let's think about the things that I had just mentioned.
Notice anything? They're all measureable. They're quantifiable objects, right? And so, if we were stop this presentation right now, whip out a computer, and try to code this up, how would we represent this? Well, we could represent them all as doubles. That's true. Easy. The problem with that though is we're actually missing some context when we do that. And so let's look at an example.
So we have our little player. I told you, lots of emoji, I'm a big fan. And our little player has moonwalked, because why not, and they've moonwalked a certain distance. Now, if we stored this distance as 5, it's like, hmm, okay, but what does five actually mean? It brings up this question of five what? But if we say five feet, it's like oh, okay. This actually makes sense. It now has context in a physical space.
And so now, let's talk about the way that, or the API that Foundation is introducing to accurately represent this entire context. So, you have a new struct. It's called Measurement, and it's generic on the UnitType, and we'll get into what a UnitType is in a little bit. Contains a unit as well as a value, and an initializer. So, what's cool about this is that now you're actually able to represent a measurement fully with full context.
And so if we go back to our little player, let's say in the game we are not only keeping track of distance traveled, but we're also keeping track of the distance that the player has left to go. How would we do this with this new API? Well, now we can represent distance traveled as a measurement of five feet. We could also represent distance to go as a measurement of, let's say, six feet. And so now, you can actually do some really cool things with them. You can add them together to get a total distance, which is a measurement that represents 11 feet.
You can multiply them and get, you know, the value that you expect. You could also divide them, and get a similar value, which is awesome. So if we go back to thinking about a unit, what are some things that we tend to associate with a unit? Well, first off, every unit has a symbol, hands down.
Units can have a dimension, and so an example of this would be for the dimension length. It could use, like, feet is a unit of length, right? Units could also be equivalent to each other, so one foot could be equivalent to approximately 0.3 meters. And so now we have APIs to actually represent a unit object.
Now, if you're paying close attention, before I had said that every unit has a symbol, and so in this API we're representing that here. We don't necessarily represent the dimension in here or the equivalents because not every unit can be equivalent to another one, and not every unit is dimensional. But at the very least, every unit has a symbol, so we're representing that here.
But if we go back to this concept of a dimension, what is a dimension? What does that actually mean? Well, dimensions are categories of units that can be expressed with different kinds of units. So, when we think of length, as an example, length can be represented as kilometers, feet, miles, what have you. Dimensions also always have a base unit, so going back to the length example, the base unit of length is meter. You can also perform conversions within dimensions, within a single dimension. So you can convert kilometers to feet, vice versa, meters to miles, et cetera.
So now we have API to represent a dimension, and so you see here that dimension subclass is Unit, which means that it inherently acquires the symbol property. We have a converter property, and I'll go into what a unit converter is in a little bit, but essentially it defines the conversion of the unit to and from its base unit. We have an initializer, which takes the symbol and a converter instance, and as I've mentioned, every dimension has a base unit. So now we have a class property that will return that unit for you.
The important thing to remember here, though, is that every instance of a dimension is in and of itself a unit. Now, with this new API that we're introducing, the cool thing is that now we are providing to you some 170-plus units that you can just use out of the box. You don't even have to define them yourself. You can just use them, like, after this presentation.
And most of these units are in accordance with the International System of Units, so they're units that you're already used to seeing, which is really cool. Now let's look at an example. We have the dimension of length, and it has a whole suite of units that are class properties representing this dimension.
And so, if we go back to this, you know, instances of a dimension are, in fact, units, what happens is any time you call these properties you're getting back an instance of unit length. The difference between each of these instances, that one, their symbols are different, and two, the converters that define their definition are different.
So, just an overview of all of the classes that we're providing to you, all the different unit types. Remember I told you there's like 170-plus of them, so, you know, play with them to your hearts' content. But we have some pretty cool ones here, some common ones, like area, mass, temperature, length. But also, some kind of uncommon ones like illuminance and electric current, but sounds really fun if play with them.
So, let's go back to our distance traveled and our distance to go measurements. So, this is code we've already seen. Now, we're going to comment the last two lines out because we're going to redefine them. So, we're leaving distanceTraveled alone, but now we're defining distanceToGo with the same value of six, but instead of passing in feet as the unit, we're passing in kilometers.
Now, this is where things start to get a little hairy, because we're now adding distanceTraveled and distanceToGo together, but one's in feet and one's in kilometers. What does this mean for totalDistance? Is it in terms of feet? It is in terms of kilometers? Will this stage just blow up in five seconds? I don't know.
But, phew, no, it won't. Actually what happened is that the result is in terms of the base unit, which is meters. And so what's cool about this is that Measurement is handling all of this implicitly for you. You don't have to worry about it at all. You could just use the operators as you would with regular scalar values and you get the results that you expect.
So, now let's say, and again, I wanted to be able to track the player at certain points and, for the sake of the demo we're just printing out strings, but maybe I want to do something funkier, add more functionality at some point. But the key here is that I want to be able to compare these measurements, and now you can. So Measurement has support for comparison operators for you to be able to do this. And so if we were to friend distanceMarker, we would get what we expect, which is barely started, because Measurement was properly able to deduce that distanceTraveled is less than distanceToGo.
So, let's talk more about defining a unit because we haven't really delved into that yet. As previously mentioned, units are always defined in terms of their base unit. There are methods that describe this conversion to and from the base unit side, but we'll talk about that a little bit more when we go into UnitConverter.
The important thing, though, to remember with this is that conversion can only happen within a single dimension. So, when you think of length, for example, you can convert kilometers to feet, but would you try to convert kilometers to seconds? Conceptually that doesn't actually make a lot of sense. And so, in this case, with Measurement, if you tried to do that, it would throw in Objective-C or it would be a fatal error in Swift.
So, remember I had mentioned that we provided like 170 plus units for you out of the box. Chances are, you won't need to define your own custom units, but if you do, that's the only time you have to think about defining them, which is pretty cool. You can actually just use them, the units that are already provided for you out of the box, but if you are creating a custom unit, then you can think about how the converter would be set up. And so, the cool thing is that Measurement can handle the conversion for you implicitly even with your custom unit. It just auto-magically works. So, let's talk about some custom units. We're making some custom units for the game.
We have a jamz unit here, because I want to be able to calculate total time in a jam session in terms of this unit called jamz. Seconds is a little boring for me. But if you see here, we have this UnitConverterLinear object. What is that? We haven't really talked about that yet. So let's go back to talking about conversions a little bit more.
So again, to and from the base unit. UnitConverter is a root class that defines two methods that describe this conversion. So baseUnitValue(fromValue), and value(fromBaseUnitValue). UnitConverterLinear overrides those two methods and defines them linearly. So, for all the math people out there, it's in terms of A X plus B, where A is the coefficient and B is the constant.
So, if we go back to this jamz unit, we see here that this coefficient is 30, a scalar value of 30, so we're saying that one jamz unit is equivalent to 30 seconds and so now, in our linear function, we have 30 times whatever the jamz value would be. So, if a jam session was four total jamz, four jamz, then if we wanted to convert it to seconds, it would be four times 30 to give us 120 seconds, and you could do the reverse. 120 divided by 30 to get the jamz value.
Yeah. So let's say I wanted to define other custom units, for length, for example, because I don't want to calculate distance traveled in terms of feet or meters, I wanted to do something a little bit more interesting, like hopz. So, here, same concept. One hopz is equivalent to 0.75 meters and so we're defining the formulas for that here, and same with some other custom units that I would create for this. So, you start to get the idea.
So, if you recall correctly, when I was talking about the jam session, one of the measurements that I had named was the number of dance movements performed. Unfortunately, the international system of units does not have a dimension that recognizes dance movements. I'm not entirely sure why, but it's a thing that we are defining here today.
And so, we have UnitDanceMove with a base unit of wackyArmMovements. And so you see here that one wackyArmMovement is equivalent to another wackyArmMovement, so what this means, another way to think about how you define your units is how many of the base unit make up this particular unit. So, here's one-to-ones so our coefficient is one.
Let's say we wanted to define a robot movement. It's like, approximately like four wackyArmMovements, I think [laughter]. cabbagePatch is like three wackyArmMovements. Sure. And of course, no dance movement dimension is complete without jazzHands, which is about two wackyArmMovements. I think it's pretty accurate, we'll stick with that for now.
And so now, let's go back and actually create this jam session that I had outlined earlier. So distance traveled, this is in terms of steps taken, which will be in terms of this unit hopz that we created. jamTime will be in jamz, naturally, and dance moves will be in terms of the robot. So our player will be roboting the entire time, of course.
And so the dance rate will actually be terms of meters per second, but if you recall, none of the other measurements were in meters or in seconds, so how are we going to derive that value? Well, we can take stepsTaken and convert it to meters. We can also take the jamTime and convert them to seconds, and now we can actually return a measurement that's in terms of meters per second. And so the cool thing here is that in very few lines of code we were actually able to completely define our jamSession.
So, now we know how to actually represent measurements and units as model objects, which is pretty cool. But I told you, though, that I want this game available everywhere. Now to do that we actually have to format these measurements. That's where things get a little tricky. So, if we have our player, and let's say instead of dancing for only five feet, they danced the robot for five kilometers. Hard core. If we wanted to represent this across the world, how would this look? Well, in Canada, we could actually just write it as it was previously written, which would be five kilometers.
If we were to try to represent this in Chinese, however, we'd actually have to translate the unit. In Arabic, we'd have to translate the unit and change the number representation and make sure that our right-to-left ordering is correct. So, all of this is more logic that I'd have to add manually to my app.
And then finally, in the U.S., we're like, "Kilometers? What are those? What are those things?" So, then, not only do I have to handle conversion, just in terms of my calculations, but I also have to handle conversions just for formatting, which is additional logic I have to add to my app.
So, what's the solution here? You let Foundation do all the work for you. We have a new formatter called MeasurementFormatter. It formatters both measurements and units, and its locale-aware, so you don't have to worry about any of this. So let's take a look at it. Its subclass is Formatter. If you're familiar with any of our other formatters, same concept. It has the unitOptions property, and we'll talk a little bit more about unitOptions in a second. It also has unitStyle. So, again, if you're familiar with the other formatter, short, medium, long.
It has a locale that's setable. Now, chances are, you're just going to default to the current locale of the user, which is what this locale will always default to, but if you needed to set it explicitly, you can. It also takes a custom numberFormatter. So, let's say you wanted your value in your measurement to be represented in terms of scientific notation, you can provide a custom numberFormatter to do that for you. It also has methods that take in both a Measurement object and a Unit object.
So let's talk a little bit more about the unit options. The cool thing is that out-of-the-box, by default the formatter formats according to the preferred unit of your user's locale. So, you don't even have to think about it. It also takes into account things like purpose. So, you know, if you're calculating a length in terms of road distance verses in terms of person height, you're going to want to use different units depending on the context. So I'll take a look at some of the options that MeasurementFormatter provides.
One is provided unit, and so, let's say we have a case where we want to pass in a measurement of five kilometers, but our locale is the U.S. Now normally in the U.S., we would, for road distance for example, change it to miles because that's what's common for us here. But if you set provided unit, it'll ensure that whatever unit you pass in is the unit that actually gets formatted.
There's also an action called natural scale. So, this is great in particular for, like, UI stuff. So, if your, you know, your app is running on the watch, and you're really concerned about screen real estate, then instead of putting in a thousand meters, which would take up a bulk of your screen, you can actually have it formatted to one kilometer.
Another is temperature without unit. So let's say you have a measurement that represents 90 degrees Fahrenheit, but you don't want the Fahrenheit unit to actually show. You can set this to get the result that you were expecting. So, let's play around with some examples. We have our formatter here, and we have our original distance measurement that's in five kilometers, because that's how far our player danced. And now we want to get a resulting string from that.
You'll see that the result is in miles and the coolest thing about this is that in three lines of code, not only did we get the result that we expected, but we didn't have to do anything to the formatter, just out-of-the-box without setting anything, it knew exactly what to do.
Now let's say that we give it our custom unit, the hopz unit. And Measurement, MeasurementFormatter that has no conception, no idea that hopz is actually a unit, but we create this hopz distance, and we pass it to the formatter, and it's actually still able to do the conversion implicitly on our behalf. We don't have to do anything. Now let's say though, you know, we have these custom units and we actually want people to see them, so we'll set provided unit. We'll give it a measurement that has our hopz unit again.
And now the result will be in terms of that unit. Now this case is a particularly interesting case because not only are we providing a custom unit, but we're also providing a custom unit that's within a custom dimension, right? And so at this point, it's like, not really sure, what will MeasurementFormatter do? Oh, well, it'll do exactly what we expect, which is pretty awesome. So, now, I'm going to hand it over to another member of the Foundation team, and he's going to show how measurements are used and can be used and formatted in the High Scores feature of this game. Thanks [applause].
Thank you, Daphne. So, my name is Peter Hosey. I am also an engineer on the Foundation team, and like Daphne said, this is our High Score list, where we're showing a list of the levels in the game. As you tap on each one, you see some basic facts about the level, the name, a picture of it. You see some important information about playing the level, things you need to know.
And you even see your statistics of how well you've done in the game. You see things like your high score, how many wacky arm movements you've done. You see how far you've danced. You see how fast you've danced. But these values are all just numbers. They lack dimension.
So we don't know, like, how far is 6811? Now you could implement an entire unit system yourself. You could start small by just tacking a unit onto the end of each of these numbers. You could maybe build out a little more, and like, have a unit conversion system that will understand different locales and translate the unit names automatically, and that's a lot of work, isn't it? Like Daphne said, let Foundation do the work do the work for you. We now have Measurement and Unit, and MeasurementFormatter types in Foundation, so let's use them in our game.
So we're going to start by creating the custom units that Daphne showed you, some of them anyway. We've got a couple of length units, we've got our speed unit, and we have our four custom dance movements that do not come with Foundation. Now that we've got our custom units, we can bring these into our model, which is this levels struct, which has our basic facts about the level and includes the player's statistics, which are just numbers.
So let's change them to measurements. So, now we have a measurement of dance moves, a measurement of length, and a measurement of speed, and as we change the properties, so much we change the initializer. So, now it's possible to create a level with measurements, and we want to do that in the List ViewController, which is a controller for this view here, this is the list.
Our List ViewController queries are synchronization API, which returns a JSON list of dictionaries, one per level, containing all of this information. And particularly includes the player's statistics as just numbers. So, we want to create measurements around these numbers, so now this a number of wackyArmMovements. This is a number of Hopz. This is a number of hopzPerJamz.
Now that this information is in our model and we've created it as measurements in our List ViewController, and that's the only change that you had to make in the List ViewController. Now we can go over to the Detail ViewController, which is what shows this view here, and shows this for every one of these levels, and we can use our new measurements here.
So, we already have one formatter in place. This is a NumberFormatter, and this is what does this padding to six digits with zeros, and we want to keep that, but we want to build on top of it. We want to make this show the units. So we're going to add two more formatters, and I'll explain why to in a moment.
Now that we've created them, we go to the same place we're already configuring the number formatter, which is in our viewedDidLoad method. This is inherited from UIViewController, and here we're overwriting it. We configure the highScore NumberFormatter with our minimum integer digits, and now we're going to configure our MeasurementFormatter to use our provided unit, which is wackyArmMovements, which you can't tell here, can you? And use our NumberFormatter so that we continue to pad to six digits. And I mentioned we created two new MeasurementFormatters, so let's configure the other one. This one we want to use the StandardNumberFormatting, so we're not going to set this NumberFormatter, but we're still going to set it to use the provided unit.
And now if I run that, oops, oh. So, I've configured the formatters. Now I need to actually use the strings that they'll give me. So we're already talking to one formatter, and we say sting from this value. Well that worked fine when this was a number, but now it's a measurement.
So now we need to talk to the MeasurementFormatter. Now, it's a simple one-word change. Otherwise, it's exactly the same. Works the same for any formatter. The other two we're creating string directly to the number, and you can imagine how this is not very good for your localization effort.
Here too we want to use a formatter. So, we'll use our other formatter, and it's, again, the same thing. We do string from, in this case a string from Measurement, and this returns a string, and we pass that to our label. So now we can run the app.
And now we see our custom units show up. So this is a start, but, we're still not really providing real world context, which is our goal to start with. We need to show these in real world units. So we could do some conversion logic of our own, but MeasurementFormatter can do that for us.
So, we're going to create one more MeasurementFormatter, and as we have the custom units, MeasurementFormatter, we're also going to have the Locale-Aware MeasurementFormatter. As we create it, same as we configure it, except we don't' actually need to do anything, because MeasurementFormatter out-of-the-box converts automatically to the unit that the player expects for their locale.
Now, this is where it's going to be a little bit tricky, so bear with me a moment. We're currently talking to one formatter, asking it for one string, and passing it directly to the label. So what we're going to do instead, is we're going to talk to the customUnitsMeasurement Formatter first, get its string for this distance. Then we talk to the localeAwareMeasurement Formatter and get its string for this distance.
And then we'll use a Swift string interpolation to put these two things together and generate one string that we will pass to the label, and we did this for the distance and we do it also for the dance rate. And that's all we have to do. In order to show both our custom units and the units that the player expects for their locale, their real world distance.
[ Applause ]
But we don't need to stop there, because remember the goal here is to have this game all over the world in every country. So miles is great in the United States, we're in the United States. We see miles. But we want to make sure this works in every country. So I'm going to make use of an Xcode feature that's part of your scheme, so I'm going to edit my scheme here.
I'm going to duplicate the scheme, and when it duplicates the scheme. It's going to ask me for a name for it, so I'm going to give my scheme a very distinctive name. And now, having named my scheme, I'm going to make one simple change, under the run verb, Options tab, Application Region. I'm going to our testing into an exotic locale, like, say Canada.
And now, I'm going to run with this scheme, and with no code change to the app, with no configuration changes in the simulator, just changing the scheme, we can now see that in Canada, this shows kilometers. That's all you have to do to make this work with our new Measurement and Unit and MeasurementFormatter types in Foundation. Thank you [applause]. Thank you, Daphne.
Thanks so much, Peter. So, obviously this game is going to be a huge hit. I'm, like, super pumped about it. Let's wrap up real quick though. So, we just saw throughout this whole talk and in the demo how Measurements and Units are now model objects that we can use in our apps, which is really awesome.
We also saw that it's super easy to format them and they require very little work on our part, which is cool. And the best part is that we get all of this very powerful localization for free, without having to specify any objects, or any options, just right out of the box.
So, now you don't actually have to suddenly become a polyglot or, you know, change all the logic and coding in your app to be able to support this. You could just use it as-is. If you'd like more information, you should check out the link. These sessions are actually already passed, but if you're super interested in it, I would recommend checking out the videos, and thank you so much.
[ Applause ]