0% found this document useful (0 votes)
60 views20 pages

Working With Data - Swift Recipes For iOS Developers - Real-Life Code From App Store Apps

Uploaded by

idontcare
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
60 views20 pages

Working With Data - Swift Recipes For iOS Developers - Real-Life Code From App Store Apps

Uploaded by

idontcare
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 20

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022

A. Nekrasov, Swift Recipes for iOS Developers


https://doi.org/10.1007/978-1-4842-8098-0_1

1. Working with Data


Alexander Nekrasov1
(1) Moscow, Russia
Data processing is a core feature of all computing devices. And iPhones are mobile computers. Your pro‐
file on social networks, cookies on websites, emails and SMS messages, even user interfaces – all of them
are data structures. For example, iOS storyboards have an XML format; you can open them in a text
editor.

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.

Conversion Between Data Types


All programming languages have basic data types and more complex structures – and Swift is not an exception. The basic
types in Swift are as follows:

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?

Safe Number Conversion


If you have several different number types, for example, Int and Double, you can convert them this way :

let d: Double = 1.0


let i = Int(d)

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:

let d: Double = 1000000000.0


let i = Int16(d)

Output: Fatal error: Double value cannot be converted to Int16 because the result would
be greater than Int16.max.

The solution is to use optional constructor init?(exactly:):

let d: Double = 1000000000.0


let i = Int16(exactly: d)

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:

It allows exact conversions – 10.9 won’t return nil anymore.


It rounds numbers following mathematical rules. For example, 10.9 will become 11.

If rounding is not a desired behavior, there’s another option – the floor function. For example:

var i = Int16(exactly: trunk(d))

Finally, let’s make it even more universal and allow d to be optional:

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 .

public extension Int8 {


var asInt16: Int16 {
Int16(self)
}
var asInt32: Int32 {
Int32(self)
}
var asInt64: Int64 {
Int64(self)
}
var asInt: Int {
Int(self)
}
var asUInt8: UInt8? {
UInt8(exactly: self)
}
var asUInt16: UInt16? {
UInt16(exactly: self)
}
var asUInt32: UInt32? {
UInt32(exactly: self)
}
var asUInt64: UInt64? {
UInt64(exactly: self)
}
var asUInt: UInt? {
UInt(exactly: self)
}
var asFloat: Float {
Float(self)
}
var asDouble: Double {
Double(self)
}
}
public extension UInt8 {
var asInt8: Int8? {
Int8(exactly: self)
}
var asInt16: Int16 {
Int16(self)
}
var asInt32: Int32 {
Int32(self)
}
var asInt64: Int64 {
Int64(self)
}
var asInt: Int {
Int(self)
}
var asUInt16: UInt16 {
UInt16(self)
}
var asUInt32: UInt32 {
UInt32(self)
}
var asUInt64: UInt64 {
UInt64(self)
}
var asUInt: UInt {
UInt(self)
}
var asFloat: Float {
Float(self)
}
var asDouble: Double {
Double(self)
}
}
public extension Int16 {
var asInt8: Int8? {
Int8(exactly: self)
}
var asInt32: Int32 {
Int32(self)
}
var asInt64: Int64 {
Int64(self)
}
var asInt: Int {
Int(self)
}
var asUInt8: UInt8? {
UInt8(exactly: self)
}
var asUInt16: UInt16? {
UInt16(exactly: self)
}
var asUInt32: UInt32? {
UInt32(exactly: self)
}
var asUInt64: UInt64? {
UInt64(exactly: self)
}
var asUInt: UInt? {
UInt(exactly: self)
}
var asFloat: Float {
Float(self)
}
var asDouble: Double {
Double(self)
}
}
public extension UInt16 {
var asInt8: Int8? {
Int8(exactly: self)
}
var asInt16: Int16? {
Int16(exactly: self)
}
var asInt32: Int32 {
Int32(self)
}
var asInt64: Int64 {
Int64(self)
}
var asInt: Int {
Int(self)
}
var asUInt8: UInt8? {
UInt8(exactly: self)
}
var asUInt32: UInt32 {
UInt32(self)
}
var asUInt64: UInt64 {
UInt64(self)
}
var asUInt: UInt {
UInt(self)
}
var asFloat: Float {
Float(self)
}
var asDouble: Double {
Double(self)
}
}
public extension Int32 {
var asInt8: Int8? {
Int8(exactly: self)
}
var asInt16: Int16? {
Int16(exactly: self)
}
var asInt64: Int64 {
Int64(self)
}
var asInt: Int {
Int(self)
}
var asUInt8: UInt8? {
UInt8(exactly: self)
}
var asUInt16: UInt16? {
UInt16(exactly: self)
}
var asUInt32: UInt32? {
UInt32(exactly: self)
}
var asUInt64: UInt64? {
UInt64(exactly: self)
}
var asUInt: UInt? {
UInt(exactly: self)
}
var asFloat: Float {
Float(self)
}
var asDouble: Double {
Double(self)
}
}
public extension UInt32 {
var asInt8: Int8? {
Int8(exactly: self)
}
var asInt16: Int16? {
Int16(exactly: self)
}
var asInt32: Int32? {
Int32(exactly: self)
}
var asInt64: Int64 {
Int64(self)
}
var asInt: Int? {
Int(exactly: self)
}
var asUInt8: UInt8? {
UInt8(exactly: self)
}
var asUInt16: UInt16? {
UInt16(exactly: self)
}
var asUInt64: UInt64 {
UInt64(self)
}
var asUInt: UInt {
UInt(self)
}
var asFloat: Float {
Float(self)
}
var asDouble: Double {
Double(self)
}
}
public extension Int64 {
var asInt8: Int8? {
Int8(exactly: self)
}
var asInt16: Int16? {
Int16(exactly: self)
}
var asInt32: Int32? {
Int32(exactly: self)
}
var asInt: Int? {
Int(exactly: self)
}
var asUInt8: UInt8? {
UInt8(exactly: self)
}
var asUInt16: UInt16? {
UInt16(exactly: self)
}
var asUInt32: UInt32? {
UInt32(exactly: self)
}
var asUInt64: UInt64? {
UInt64(exactly: self)
}
var asUInt: UInt? {
UInt(exactly: self)
}
var asFloat: Float {
Float(self)
}
var asDouble: Double {
Double(self)
}
}
public extension UInt64 {
var asInt8: Int8? {
Int8(exactly: self)
}
var asInt16: Int16? {
Int16(exactly: self)
}
var asInt32: Int32? {
Int32(exactly: self)
}
var asInt64: Int64? {
Int64(exactly: self)
}
var asInt: Int? {
Int(exactly: self)
}
var asUInt8: UInt8? {
UInt8(exactly: self)
}
var asUInt16: UInt16? {
UInt16(exactly: self)
}
var asUInt32: UInt32? {
UInt32(exactly: self)
}
var asUInt: UInt? {
UInt(exactly: self)
}
var asFloat: Float {
Float(self)
}
var asDouble: Double {
Double(self)
}
}
public extension Int {
var asInt8: Int8? {
Int8(exactly: self)
}
var asInt16: Int16? {
Int16(exactly: self)
}
var asInt32: Int32? {
Int32(exactly: self)
}
var asInt64: Int64 {
Int64(self)
}
var asUInt8: UInt8? {
UInt8(exactly: self)
}
var asUInt16: UInt16? {
UInt16(exactly: self)
}
var asUInt32: UInt32? {
UInt32(exactly: self)
}
var asUInt64: UInt64? {
UInt64(exactly: self)
}
var asUInt: UInt? {
UInt(exactly: self)
}
var asFloat: Float {
Float(self)
}
var asDouble: Double {
Double(self)
}
}
public extension UInt {
var asInt8: Int8? {
Int8(exactly: self)
}
var asInt16: Int16? {
Int16(exactly: self)
}
var asInt32: Int32? {
Int32(exactly: self)
}
var asInt64: Int64? {
Int64(exactly: self)
}
var asInt: Int? {
Int(exactly: self)
}
var asUInt8: UInt8? {
UInt8(exactly: self)
}
var asUInt16: UInt16? {
UInt16(exactly: self)
}
var asUInt32: UInt32? {
UInt32(exactly: self)
}
var asUInt64: UInt64 {
UInt64(self)
}
var asFloat: Float {
Float(self)
}
var asDouble: Double {
Double(self)
}
}
public extension Float {
var asInt8: Int8? {
Int8(exactly: self.rounded())
}
var asInt16: Int16? {
Int16(exactly: self.rounded())
}
var asInt32: Int32? {
Int32(exactly: self.rounded())
}
var asInt64: Int64? {
Int64(exactly: self.rounded())
}
var asInt: Int? {
Int(exactly: self.rounded())
}
var asUInt8: UInt8? {
UInt8(exactly: self.rounded())
}
var asUInt16: UInt16? {
UInt16(exactly: self.rounded())
}
var asUInt32: UInt32? {
UInt32(exactly: self.rounded())
}
var asUInt64: UInt64? {
UInt64(exactly: self.rounded())
}
var asUInt: UInt? {
UInt(exactly: self.rounded())
}
var asDouble: Double {
Double(self)
}
}
public extension Double {
var asInt8: Int8? {
Int8(exactly: self.rounded())
}
var asInt16: Int16? {
Int16(exactly: self.rounded())
}
var asInt32: Int32? {
Int32(exactly: self.rounded())
}
var asInt64: Int64? {
Int64(exactly: self.rounded())
}
var asInt: Int? {
Int(exactly: self.rounded())
}
var asUInt8: UInt8? {
UInt8(exactly: self.rounded())
}
var asUInt16: UInt16? {
UInt16(exactly: self.rounded())
}
var asUInt32: UInt32? {
UInt32(exactly: self.rounded())
}
var asUInt64: UInt64? {
UInt64(exactly: self.rounded())
}
var asUInt: UInt? {
UInt(exactly: self.rounded())
}
var asFloat: Float? {
Float(exactly: self)
}
}
Recipe 1-1 Safe Numeric Conversions

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….

Figure 1-1 Swift Playground

Conversion from Number to String and Back

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:

let price = 14.50


let priceString = String(format: "Price: $%.02f", price)
Sometimes, you need to be more flexible. While most countries use two digits for monetary units, others
need three digits. One Bahraini dinar equals 1000 fils, one Chinese yuan equals 10 jiao, and so on.

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.

public extension Double {


var asPrice: String {
guard let cents = (self * 100.0).asInt else {
return ""
}
return String(format: "%d.%02d", cents / 100, cents % 100)
}
}
public extension Int {
var asPrice: String {
String(format: "%d.%02d", self / 100, self % 100)
}
}
Recipe 1-2 Formatting Price

Usage

let price = 14.5


let priceString = "Price: $\(price.asPrice)"

Similarly, you can format any other data.

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 fast solution is the following:

public extension String.StringInterpolation {


mutating func appendInterpolation(_ value: Double) {
let formatter = NumberFormatter()
formatter.decimalSeparator = "."
formatter.maximumFractionDigits = 3
if let result = formatter.string(from: value as NSNumber) {
appendLiteral(result)
}
}
}

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

let str = "123.5"


let i = Int(str)
let d = Double(str)

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: ".").

Let’s wrap it up and make an easy-to-use String extension (Recipe 1-4).

public extension StringProtocol {


var asInt8: Int8? {
Int8(self)
}
var asUInt8: UInt8? {
UInt8(self)
}
var asInt16: Int16? {
Int16(self)
}
var asUInt16: UInt16? {
UInt16(self)
}
var asInt32: Int32? {
Int32(self)
}
var asUInt32: UInt32? {
UInt32(self)
}
var asInt64: Int64? {
Int64(self)
}
var asUInt64: UInt64? {
UInt64(self)
}
var asInt: Int? {
Int(self)
}
var asUInt: UInt? {
UInt(self)
}
var asDouble: Double? {
Double(self.replacingOccurrences(of: ",", with: "."))
}
var asFloat: Float? {
Float(self.replacingOccurrences(of: ",", with: "."))
}
}
Recipe 1-4 Parsing Strings to Numeric Types

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.

Recipe 1-5 follows the set of rules specified in Table 1-1.


Table 1-1 Any to Bool conversion rules

Parsed
Rules
type

Bool Return parsed value as is.

Int Return false if the value is 0, otherwise true.

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

// Any can't be extended, so it's a function


func parseAsBool(value: Any?) -> Bool? {
if let boolValue = value as? Bool {
return boolValue
} else if let intValue = value as? Int {
return intValue != 0
} else if value is Float || value is Double {
return nil
} else if let strValue = value as? String {
let strPrepared = strValue.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if strPrepared == "true" || strPrepared == "yes" {
return true
} else if strPrepared == "false" || strPrepared == "no" {
return false
} else {
return nil
}
} else {
return nil
}
}
Recipe 1-5 Extracting Boolean Value from a Variable
If you prefer all numeric types to be converted to Bool, you can use the isZero function instead of re‐
turning nil. If Double.isZero returns true, you return false. Otherwise, you return true .

String to Data and Back

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.

let data = Data()


let str = String(data: data, encoding: .utf8)

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:

let str = "I'm a string"


let data = str.data(using: .utf8)

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.

public extension StringProtocol {


var asUtf8Data: Data {
data(using: .utf8)!
}
}
public extension Data {
var asUtf8String: String? {
String(data: self, encoding: .utf8)
}
}
Recipe 1-6 Conversion Between String and Data

Dates, Timestamps, and ISO 8601 Format

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.

public extension Formatter {


static let iso8601withFractionalSeconds: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
return formatter
}()
}
public extension Date {
var iso8601String: String {
Formatter.iso8601withFractionalSeconds.string(from: self)
}
init?(iso8601String: String) {
guard let date = Formatter.iso8601withFractionalSeconds.date(from: iso8601String) else {
return nil
}
self = date
}
}
Recipe 1-7 Date to ISO 8601

Usage

let dateWrong = Date(iso8601String: "It's 5 o'clock")


// dateWrong is nil
let dateCorrect = Date(iso8601String: "2021-07-01T18:30:00.000+04:00")
// dateCorrect is a valid Date object

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.

Extracting Data from Dictionaries and Arrays


You can’t do much with simple data types. In real-life apps, more complex types are used. You can have a structure
containing different data types and different structures inside. It’s called Dictionary . Basically, a Dictionary is a set of
keys and values. They can be limited with particular data types. You’ll rarely see the keyword Dictionary in Swift code. A
more common syntax is

var dict: [String: Any] = [:]

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.

var arr: [String] = []

Complex Data Types


The composition of dictionaries and arrays allows you to create data structures of any complexity.

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.

Extracting Data from Dictionary

Let’s say we have a Dictionary :

var dict: [String: Any] = [


"name": "John",
"surname": "Doe",
"age": 30
]

It’s John’s birthday, so we need to make him one year older:

if var age = dict["age"] {


age += 1
dict["age"] = age
}

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:

if var age = dict["age"] as? Int {


age += 1
dict["age"] = age
}

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.

public extension Dictionary where Key: ExpressibleByStringLiteral {


func getInt8(_ key: String, defVal: Int8? = nil) -> Int8? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let ival = val as? Int8 {
return ival
}
if let dval = val as? Double {
return dval.asInt8
}
if let sval = val as? String {
return sval.asInt8 ?? defVal
}
return defVal
}
func getUInt8(_ key: String, defVal: UInt8? = nil) -> UInt8? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let ival = val as? UInt8 {
return ival
}
if let dval = val as? Double {
return dval.asUInt8
}
if let sval = val as? String {
return sval.asUInt8 ?? defVal
}
return defVal
}
func getInt16(_ key: String, defVal: Int16? = nil) -> Int16? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let ival = val as? Int16 {
return ival
}
if let dval = val as? Double {
return dval.asInt16
}
if let sval = val as? String {
return sval.asInt16 ?? defVal
}
return defVal
}
func getUInt16(_ key: String, defVal: UInt16? = nil) -> UInt16? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let ival = val as? UInt16 {
return ival
}
if let dval = val as? Double {
return dval.asUInt16
}
if let sval = val as? String {
return sval.asUInt16 ?? defVal
}
return defVal
}
func getInt32(_ key: String, defVal: Int32? = nil) -> Int32? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let ival = val as? Int32 {
return ival
}
if let dval = val as? Double {
return dval.asInt32
}
if let sval = val as? String {
return sval.asInt32 ?? defVal
}
return defVal
}
func getUInt32(_ key: String, defVal: UInt32? = nil) -> UInt32? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let ival = val as? UInt32 {
return ival
}
if let dval = val as? Double {
return dval.asUInt32
}
if let sval = val as? String {
return sval.asUInt32 ?? defVal
}
return defVal
}
func getInt64(_ key: String, defVal: Int64? = nil) -> Int64? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let ival = val as? Int64 {
return ival
}
if let dval = val as? Double {
return dval.asInt64
}
if let sval = val as? String {
return sval.asInt64 ?? defVal
}
return defVal
}
func getUInt64(_ key: String, defVal: UInt64? = nil) -> UInt64? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let ival = val as? UInt64 {
return ival
}
if let dval = val as? Double {
return dval.asUInt64
}
if let sval = val as? String {
return sval.asUInt64 ?? defVal
}
return defVal
}
func getInt(_ key: String, defVal: Int? = nil) -> Int? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let ival = val as? Int {
return ival
}
if let dval = val as? Double {
return dval.asInt
}
if let sval = val as? String {
return sval.asInt ?? defVal
}
return defVal
}
func geUtInt(_ key: String, defVal: UInt? = nil) -> UInt? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let ival = val as? UInt {
return ival
}
if let dval = val as? Double {
return dval.asUInt
}
if let sval = val as? String {
return sval.asUInt ?? defVal
}
return defVal
}
func getFloat(_ key: String, defVal: Float? = nil) -> Float? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let fval = val as? Float {
return fval
}
if let ival = val as? Int {
return ival.asFloat
}
if let sval = val as? String {
return sval.asFloat ?? defVal
}
return defVal
}
func getDouble(_ key: String, defVal: Double? = nil) -> Double? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let dval = val as? Double {
return dval
}
if let ival = val as? Int {
return ival.asDouble
}
if let sval = val as? String {
return sval.asDouble ?? defVal
}
return defVal
}
func getString(_ key: String, defVal: String? = nil) -> String? {
let val = self[key as! Key]
if val == nil {
return defVal
}
if let sval = val as? String {
return sval.trimmingCharacters(in: .whitespacesAndNewlines)
}
return defVal
}
func getBool(_ key: String, defVal: Bool? = nil) -> Bool? {
let val = self[key as! Key]
if val == nil {
return defVal
}
return parseAsBool(value: val) ?? defVal
}
}

Recipe 1-8 Extracting Data from Dictionary

Extracting Data from Array

Extracting data from typed Array is straightforward:

let arr: [String] = ["one", "two", "three"]


let firstElement = arr[0]

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.

public extension Array {


func getInt8(_ idx: Int, defVal: Int8? = nil) -> Int8? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let ival = val as? Int8 {
return ival
}
if let dval = val as? Double {
return dval.asInt8
}
if let sval = val as? String {
return sval.asInt8 ?? defVal
}
return defVal
}
func getUInt8(_ idx: Int, defVal: UInt8? = nil) -> UInt8? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let ival = val as? UInt8 {
return ival
}
if let dval = val as? Double {
return dval.asUInt8
}
if let sval = val as? String {
return sval.asUInt8 ?? defVal
}
return defVal
}
func getInt16(_ idx: Int, defVal: Int16? = nil) -> Int16? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let ival = val as? Int16 {
return ival
}
if let dval = val as? Double {
return dval.asInt16
}
if let sval = val as? String {
return sval.asInt16 ?? defVal
}
return defVal
}
func getUInt16(_ idx: Int, defVal: UInt16? = nil) -> UInt16? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let ival = val as? UInt16 {
return ival
}
if let dval = val as? Double {
return dval.asUInt16
}
if let sval = val as? String {
return sval.asUInt16 ?? defVal
}
return defVal
}
func getInt32(_ idx: Int, defVal: Int32? = nil) -> Int32? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let ival = val as? Int32 {
return ival
}
if let dval = val as? Double {
return dval.asInt32
}
if let sval = val as? String {
return sval.asInt32 ?? defVal
}
return defVal
}
func getUInt32(_ idx: Int, defVal: UInt32? = nil) -> UInt32? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let ival = val as? UInt32 {
return ival
}
if let dval = val as? Double {
return dval.asUInt32
}
if let sval = val as? String {
return sval.asUInt32 ?? defVal
}
return defVal
}
func getInt64(_ idx: Int, defVal: Int64? = nil) -> Int64? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let ival = val as? Int64 {
return ival
}
if let dval = val as? Double {
return dval.asInt64
}
if let sval = val as? String {
return sval.asInt64 ?? defVal
}
return defVal
}
func getUInt64(_ idx: Int, defVal: UInt64? = nil) -> UInt64? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let ival = val as? UInt64 {
return ival
}
if let dval = val as? Double {
return dval.asUInt64
}
if let sval = val as? String {
return sval.asUInt64 ?? defVal
}
return defVal
}
func getInt(_ idx: Int, defVal: Int? = nil) -> Int? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let ival = val as? Int {
return ival
}
if let dval = val as? Double {
return dval.asInt
}
if let sval = val as? String {
return sval.asInt ?? defVal
}
return defVal
}
func geUtInt(_ idx: Int, defVal: UInt? = nil) -> UInt? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let ival = val as? UInt {
return ival
}
if let dval = val as? Double {
return dval.asUInt
}
if let sval = val as? String {
return sval.asUInt ?? defVal
}
return defVal
}
func getFloat(_ idx: Int, defVal: Float? = nil) -> Float? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let fval = val as? Float {
return fval
}
if let ival = val as? Int {
return ival.asFloat
}
if let sval = val as? String {
return sval.asFloat ?? defVal
}
return defVal
}
func getDouble(_ idx: Int, defVal: Double? = nil) -> Double? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let dval = val as? Double {
return dval
}
if let ival = val as? Int {
return ival.asDouble
}
if let sval = val as? String {
return sval.asDouble ?? defVal
}
return defVal
}
func getString(_ idx: Int, defVal: String? = nil) -> String? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
if let sval = val as? String {
return sval.trimmingCharacters(in: .whitespacesAndNewlines)
}
return defVal
}
func getBool(_ idx: Int, defVal: Bool? = nil) -> Bool? {
if idx < 0 || idx >= count {
return nil
}
let val = self[idx]
return parseAsBool(value: val) ?? defVal
}
}

Recipe 1-9 Extracting Data from Array

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.

You might also like