Working With Data - Swift Recipes For iOS Developers - Real-Life Code From App Store Apps
Working With Data - Swift Recipes For iOS Developers - Real-Life Code From App Store Apps
That’s why it’s important to learn how to handle data as a starting point. We will work with the Data class
representing binary data; we will convert data types and handle conversion errors, extract data from
Dictionary and Array (which are Swift representations of XML, JSON, and other interchange formats),
and we will create these structures to pass data to APIs. In the end, we’ll talk about serialization and dese‐
rialization, which are extremely important concepts in data processing.
Int – An integer number. For example, 1, 15, or -1536. Depending on the architecture, it can be a 32-bit
or a 64-bit number. All modern Apple devices have 64-bit processors. On these platforms, the Int range
goes from −263 to 263 − 1. If you need to use a bigger or smaller range, you can add a width integer (the
amount of memory bits needed to store a value) to the type name. Examples are Int16 and Int64.
Unsigned integers have the UInt type.
Float and Double – A floating-point number, 32-bit and 64-bit, respectively. These types are actively
used in UI development. All coordinates on the screen are floating points. Float range is 1.2 * 10-38 to
3.4 * 1038. Double range is 2.3 * 10-308 to 1.7 * 10308. An example of a floating-point number is
3.14159265359.
Bool – A Boolean value, which can take only one of two values: true or false. Used for logical expres‐
sions and storing simple values (yes/no).
String – Textual data. In Swift, it’s a Unicode text string, which means it may contain text in all lan‐
guages and even emojis.
Character – One character. In Swift, it’s a 16-bit value.
Besides the basic types, there are hundreds of more complex types, which are structures containing basic
types or other structures.
What do we mean by data conversion? Swift is a strictly typed language, and while languages like C or
JavaScript allow scenarios like this: a = 1 if (a) ..., in Swift, it’s not acceptable. You can use Boolean
variables only inside an if statement, and you can only assign Double value to Double variables, not even
to the Float ones. It helps to avoid many mistakes but forces developers to perform data conversion
manually. There are more complex examples of data conversion. For example, 1 and "1" are not the
same thing, even though it may seem so at first glance. The first case is an integer value, while the second
one is a text (String or Character). To convert one type to another, you need to write additional code.
And it’s important not to forget about possible problems. What if the value in a String is too big for Int?
What if it’s not a number? What if it’s "1", true or false?
In the preceding example, the variable i will have the value 1. If the value of d doesn’t fit the range of i, you’ll get... a
crash:
Output: Fatal error: Double value cannot be converted to Int16 because the result would
be greater than Int16.max.
The variable i will be optional and will have the Int16? type, and instead of crashing the app, it will become nil.
There’s one serious problem left – init?(exactly:) returns a non-nil value only if the floating-point value doesn’t have
fractional parts. If d is 10.5, the conversion will return nil. This problem has a beautiful solution - the rounded() method
. It solves two problems:
If rounding is not a desired behavior, there’s another option – the floor function. For example:
extension Double {
var asInt16: Int16? {
Int16(exactly: self.rounded())
}
}
let d1: Double? = 1000000000.0
let d2: Double? = 10.9
let i1 = d1?.asInt16 // nil
let i2 = d2?.asInt16 // 11
In Recipe 1-1, we make an extension of the Double class. It automatically works on Double?. It’s simple, 100% safe,
and universal .
This recipe allows you to safely convert one numeric type to another as well as chaining such conver‐
sions. For example, if you have an Int, but function is presented as Double extension, this chaining solves
the problem: x.asDouble.process.asInt .
NoteThe best way to learn is to try. And the best way to try Swift code is Xcode Playground (Figure 1-1). To
create a new playground, open Xcode, open File menu, choose New and Playground….
The easiest way to convert numbers of any type to a String is a string interpolation . In Swift, you can include any variable
inside a String by adding \( before a variable name and ) after. It works even with expressions and functions. For example:
let age = 30
let str = "Your age is \(age). Next year you'll be \(age+1)"
It’s more complicated if you need to format it. If you need to get a string with a price, you probably want
to have two digits for the monetary part.
There are two ways. You can use the String(format:_:...) constructor. The first argument is a template; others are
variables. If you are familiar with the C-family programming languages, you know the sprintf function. It works exactly
the same way. Example:
Depending on the app functionality, you can write formatters for all necessary data types. If you use
Double to store prices, you can write an extension to format it and use it wherever you need. Such exten‐
sion is shown in Recipe 1-2.
NoteIt’s not a good idea to use Double for prices. Even though they have high precision, losing 0.000...1
may change your visible price from 1.00 to 0.99. A more reliable way is to use simple Ints and keep prices
in monetary units. Alternatively, you can use more complex data structures storing as two Int variables
– for full units and monetary units.
Usage
Another way to format numbers is extending String.StringInterpolation. It gives you access to inter‐
polation logic. Typically, it’s used for dates and custom types, but you can use it for numbers as well.
Unless the app has scientific purposes, you won’t need more than three decimals for doubles.
The preceding extension limits the amount of fraction digits to three and adds decimal separators.
But it’s fast only to write when you use it in UITableView or UICollectionView; it may cause perfor‐
mance issues. Why? Because we create and set up NumberFormatter every time.
The correct solution is to create a lazy variable, which will create our NumberFormatter once, and then reuse it every
time. We can’t create variables in extensions, so we’ll have to create a helper class as shown in Recipe 1-3.
class MyFormatters {
static var formatterWithThreeFractionDigits: NumberFormatter = {
let formatter = NumberFormatter()
formatter.decimalSeparator = "."
formatter.maximumFractionDigits = 3
return formatter
}()
}
public extension String.StringInterpolation {
mutating func appendInterpolation(_ value: Double) {
let formatter = MyFormatters.formatterWithThreeFractionDigits
if let result = formatter.string(from: value as NSNumber) {
appendLiteral(result)
}
}
}
Recipe 1-3
Custom String Interpolation
NoteAll static variables are lazy, so you don’t have to add lazy keyword. Even more, you can’t do it.
What about the opposite conversion? If we get a String, how can we convert it to Int or Double to perform numeric
operations? A simple answer is
This code is safe because it returns optionals. i in the preceding example will be nil, while d will be 123.5. It’s a
really good working code, but there are two problematic situations:
1.If str is optional, it won’t compile. This can be solved with a nil-coalescing operator (str ?? ""). If str
is nil, it will be replaced with an empty string. As empty strings can’t be parsed as a number (neither
Double nor Int), the result will be nil.
2.In some locales, decimal separator is “,” instead of “.”. This can be a problem when we parse user in‐
put. A quick fix is replacing commas with dots: .replacingOccurrences(of: ",", with: ".").
Boolean Conversions
Even though Bool is the simplest type with only two values, parsing it can be a challenge. To understand
why, try to figure out if 1 is true or false. Most developers will say that 1 is undoubtedly true because 0
is false and any other value is true. This is how it works under the hood in Swift and most other pro‐
gramming languages. In some cases, 0 may mean success (no error), and other values – error code. Keep
it in mind if you have such case, but we’ll assume that Int should convert to false if it’s 0, and to true
otherwise .
Other questions are if "yes" or "true" should be parsed as true and what to do with custom values like
5 or "success". It depends on the source of the data. You should read the documentation of the API or li‐
brary you use and decide accordingly.
Parsed
Rules
type
Float or Return nil to avoid confusion. Booleans should never be represented as floating-point
Double numbers.
Return true if the value is "true" or "yes"; return false if the value is "false" or
String
"no"; return nil in all other cases. String comparison should be case-insensitive.
Other
Return nil.
types
String and Data are highly used types in Swift. Data has a buffer of bytes without any particular seman‐
tics. String is basically the same, but Swift interprets this buffer as a sequence of characters, including
special symbols, such as spaces and newlines.
Depending on the use case, you may need to turn String to Data or vice versa.
If data can’t be parsed using specified encoding (UTF-8 in this case), this code will return nil. For exam‐
ple, if you read a JPEG file from an iPhone storage (or download it from the Internet) and send it to a
String constructor, it will always return nil.
The conversion from String to Data rarely fails with UTF-8 encoding:
But potentially you may need to get it encoded into ASCII (American Standard Code for Information
Interchange), which was the standard before UTF was introduced. ASCII uses 7 bits per symbol and sup‐
ports only 128 characters, including special ones. Arabic, Japanese, or Cyrillic symbols can’t be encoded
this way, so the conversion will fail. Data will be nil. Conversion of UTF8 data is shown in Recipe 1-6.
We will discuss the String type and its features in more details in the second chapter.
We often need to represent dates as a string . Swift has an integrated Date structure. It represents both
date and time, it supports time zones, and it’s used as an argument or return type in many functions and
methods. The biggest problem of using Date is that it’s an internal Swift format – you can’t pass it to a
server; you have to convert it to a String, Int, or another universal format.
One of the standards is called Timestamp or UNIX Timestamp . It’s an integer value, indicating the amount
of seconds or milliseconds since epoch. Epoch is the midnight of January 1, 1970. In Swift, timestamps are
represented as Double; they have an integer part (seconds) and a fractional part (fractions of seconds).
These two functions – one method and one constructor – convert Date to Double and back:
let date = Date()
let timestamp = date.timeIntervalSince1970
let restoredDate = Date(timeIntervalSince1970: timestamp)
The biggest problem in timestamps is the lack of information about time zones. Swift Date returns the
amount of seconds since January 1, 1970, UTC (Coordinated Universal Time). So if you convert it to a Date
object, you need to make correct time zone conversion.
Another way to represent time is the ISO 8601 format . ISO stands for International Organization for
Standardization. Everything starting with ISO means that it’s a standard.
Swift doesn’t offer a conversion option between ISO 8601 String and Date out of the box, but it’s rather easy to
implement it using the DateFormatter class as shown in Recipe 1-7.
Usage
Please note that in a debug session, you’ll see time in your local time zone, which is what you usually
want to show to the user.
This code created a Dictionary variable. Each key must be a String. The value can have any type, in‐
cluding another dictionary.
In case if you need an ordered list of values of the same type, you can use an Array. It doesn’t have keys but uses
indexes instead.
It’s easy to spot the similarity with JSON (JavaScript Object Notation) structures . JSON is the most popular
format for API requests and responses. When a mobile app requests a reading of the weather forecast,
user info, or any other data from the server, it’s almost guaranteed it gets JSON.
Parsing JSON structures from String or Data returned by API will be discussed later. Let’s focus on ex‐
tracting data from Dictionary and Array now.
And here we have a problem – the code won’t compile. age is not a number; it’s Any. We can’t increment Any; it doesn’t
make sense. Type casting could solve the problem:
And this is a working code, except for one situation. API responses are usually generated by scripting, not
strictly typed languages, like JavaScript. There’s a big chance that age will be "30" instead of 30. The op‐
posite problem is also possible. If the text string consists of digits, it can be parsed as a number. For exam‐
ple, a bank card number can be parsed as a big Int, and you may need it as a String. Or user’s password
123456 can be parsed as an Int instead of String.
As we already know, Swift type casting doesn’t turn Int into String automatically. And Bool variables are completely
uncertain. Dictionary extension from Recipe 1-8 will help us to solve this problem.
It would be better to confirm that there’s element 0 though. A more complex task is to get typed data from
[Any], and we get this type of Array from the parsers.
The logic is the same as for Dictionary, but we need to check if the element with a given index exists as well. If it
doesn’t, we return nil. The same as if we have a data type conflict. Recipe 1-9 shows the full Array extension.
Summary
In this chapter, we showed that even simple data types require attention, especially when it comes to
type conversion and extracting data from complex types like dictionaries and arrays.
In the next chapter, we’ll review such concepts as serialization, data exchange, and data semantics, when
number is not just a set of bits and bytes, but length, weight, or distance.