Changes/improvements in Dictionary
Many proposals were made to enhance the Dictionaries and make them more powerful. In certain scenarios, Dictionaries might behave in an unexpected manner, and for this reason, many suggestions were made to change the way Dictionaries currently work in certain situations.
Let’s take a look at an example. Filtering returns the same data type; in Swift 3, if you used a filter operation on a Dictionary, the return type of the result would be a tuple (with key/value labels) and not a Dictionary. Consider this example:
let people = ["Tom": 24, "Alex": 23, "Rex": 21, "Ravi": 43]
let middleAgePeople = people.filter { $0.value > 40 }
After the execution, you cannot use middleAgePeople["Ravi"]
since the returned result is not a Dictionary. Instead, you have to follow the tuple syntax to access the desired value because the return type is tuple, middleAgePeople[0].value
, which is not implicitly expected.
Thanks to the new release, the current scenario has now changed as the new return type is a Dictionary. This will break any existing implementation in which you had written your code based on the return type, expecting it to be a tuple.
Similarly, while working with Dictionaries, the map()
operation never worked the way most developers expected, since the return type could be a single value while you passed in a key-value tuple. Let's look at the following example:
let ages = people.map { $0.value * 2 }
This remains the same in Swift 4, but there is the addition of a new method mapValues()
, which will prove to be of more use as it allows values passed to the method to be transformed and spit out as a Dictionary with original keys.
For example, the following code will round off and convert all the given ages to Strings, place them into a new Dictionary with the exact same keys, that is, Tom, Alex, Rex, and Ravi:
let ageBrackets = people.mapValues { "\($0 / 10) 's age group" }
Note
Mapping Dictionary keys is not safe as we might end up creating duplicates.
Grouping initializer is the new addition to the Dictionary that converts a sequence into a Dictionary of sequences grouped as per your ambition. Continuing our people example, we can use people.keys
to get back an array of people names and then group them by their first letter, like this:
let groupedPeople = Dictionary(grouping: people.keys) { $0.prefix(1) }
print(groupedPeople)
This will output the following:
["T": ["Tom"], "A": ["Alex"], "R": ["Rex", “Ravi”]]
Here, T
, A
, and R
are initializers to the distinct names. For instance, consider that you had one more name in the Dictionary, say "Adam" aged 55:
["Tom": 24, "Alex": 23, "Rex": 21, "Ravi": 43, "Adam": 55]
In this case, the groupedPeople
array might look something like this:
["T": ["Tom"], "A": ["Alex", "Adam"], "R": ["Rex", “Ravi”]]
Alternatively, we can group people based on the length of their names, as shown:
let groupedPeople = Dictionary(grouping: people.keys) { $0.count }
print(groupedPeople)
This will output the following:
[3: ["Tom","Rex"], 4: ["Alex", "Ravi","Adam"]]
Key-based subscript with default value
To understand this change, let’s first try to cite why it was required in the first place; let's take a look at the following code example:
let peopleDictionary : [String: AnyObject] = ...
var name = "Unknown"
if let apiName = peopleDictionary["name"] as? String {
name = apiName
}
Basically, our goal is to get the name of the user from some Dictionary (probably coming from some API) and in case it doesn't exist, we just want to keep the default name.
There are two problems with that approach. The first is the fact that we've probably got more than just a name field, and we end up with repetitive "if let" statements that are basically just making our code less readable.
The second problem is that just for the sake of unwrapping a value, we need to come up with some artificial name for the temporary assignment (and hey, we are not good at naming stuff anyway).
So the question now is, can we do better?
The previous solution would be to use generics or extensions to modify the behavior of the existing libraries used to write some generic method to retrieve the desired value, but with Swift 4, it's now possible to access a Dictionary key and provide a default value to use if the key is missing:
let name = peopleDictionary["name", default: "Anonymous"]
We can write the same thing using nil coalescing; you can alternatively use Swift 3 to write this line:
let name = peopleDictionary["name"] ?? "Anonymous"
However, that does not work if you try to modify the value in the Dictionary rather than just reading it. Accessing the key in the Dictionary returns an optional rather than an exact value and for this reason, we can't modify a Dictionary value in place, but with Swift 4, you can write much more maintainable and succinct code, as follows:
var friends = ["Deapak", "Alex", "Ravi", "Deapak"]
var closeFriends = [String: Int]()
for friend in friends {
closeFriends[friend, default: 0] += 1
}
The preceding loop in code loops over each entry in the friends
array and populates the count of each entry in the closeFriends
Dictionary. Since we know that the Dictionary will always have a value, we can modify it in one line of code.
Convert tuples to Dictionary
With Swift 4, you can now create a new unique Dictionary from an array of tuples consisting of duplicate keys. Let's take an example of an array of tuples with duplicate keys:
let tupleWithDuplicateKeys = [("one", 1), ("one", 2), ("two", 2), ("three", 3), ("four", 4), ("five", 5)]
Also, you want to convert this array into Dictionary, so you can do this:
let dictionaryWithNonDuplicateKeys = Dictionary(tupleWithDuplicateKeys, uniquingKeysWith: { (first, _) in first })
Now if you try to print dictionaryWithNonDuplicateKeys;
print(dictionaryWithNonDuplicateKeys)
, the output will be as illustrated:
["three": 3, "four": 4, "five": 5, "one": 1, "two": 2],
This is along with all the duplicate keys removed in the resulting Dictionary.
Convert arrays to Dictionary
You can create a new Dictionary by mapping two sequences one is to one or by mapping a sequence of keys and values according to a custom logic; let’s take a look at both the methods:
- Mapping two sequences (arrays) one is to one: Consider that you have two sequences
personNames
and ages as shown here:
let personNames = ["Alex", "Tom", "Ravi", "Raj", "Moin"]
let ages = [23, 44, 53, 14, 34]
You can create a Dictionary contacts by joining these two arrays, as follows:
let contacts = Dictionary(uniqueKeysWithValues: zip(personNames, ages))
The output will be this:
["Tom": 44, "Raj": 14, "Moin": 34, "Ravi": 53, "Alex": 23]
- Create a new Dictionary by mapping an array of keys and values according to a custom logic. Suppose you have two arrays- one with Strings representing all the odd numbers in words and other one with integers from 1 to 10:
let oddKeys = ["one", "three", "five", "seven", "nine"]
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- Now, consider that you want to create a Dictionary in which you want to map the String values to corresponding int values; you can do this as follows:
numbers = numbers.filter { $0 % 2 != 0 }
let oddDictionary = Dictionary(uniqueKeysWithValues: zip(oddKeys, numbers))
print(oddDictionary)
["six": 6, "four": 4, "eight": 8, "ten": 10, "two": 2]
Easy, isn’t it!
Swift 4 allows us to initialize a Dictionary from a sequence with duple existence of entries and manage the duplicates easily. Suppose you have an array of friends as follows:
var friends = ["Deapak", "Alex", "Ravi", "Deapak"]
Also suppose that you want to create a Dictionary with all the friends, remove duplicates, and just maintain the count of the number of occurrences that occurred in the initial friends array; you can do this by initiating a new Dictionary, as follows:
let friendsWithMultipleEntries = Dictionary(zip(friends, repeatElement(1, count: friends.count)), uniquingKeysWith: +)
The output will be the following:
["Deapak": 2, "Ravi": 1, "Alex": 1],
This helps you avoid overwriting key-value pairs, without putting in a word. The preceding code besides the shorthand +
, uses zip to fix duplicate keys by adding the two contrasting values.
Note
zip(_:_:)
creates a sequence of pairs built out of two underlying sequences.
Dictionary and sequence now have the capacity to explicitly, unambiguously reserve capacity
.
Suppose you have a sequence of friends with an initial capacity of 4:
friends.capacity // 4
You can now reserve the capacity by doing this:
friends.reserveCapacity(20)
This reserves a minimum of 20 segments of capacity:
friends.capacity // 20
Reallocating memory to objects can be an expensive task, and if you have an idea about how much space it will require to store an object, then using reserveCapacity(_:)
can be an easy and simple way to increase performance.
Swift 4 brings in a number of modifications to the Dictionary, 12 to be exact as per the official Apple developers guide, and a number of additions that we will discuss in subsequent sections:
Changes/improvements in Strings
Undoubtedly, the String is one of the majorly used data types in all the programming languages. Apparently, it is the data type that mankind understands better. Strings are important to the extent that they have the ability to significantly change our perception of how difficult or simple it is to learn a programming language. Hence, it becomes really important to follow any development to this data type. Strings received a major overhaul with Swift 4, making them collections of characters. In the earlier versions, several times, Swift, with its complicated way of handling subStrings and characters, went overboard in advocating accuracy over convenience.
Bid bye to string.characters
As part of the changes, this is one of the most welcomed changes. This change eliminates the necessity for a characters array on String, which means now you can reverse them, loop over them character-by-character, map()
and flatMap()
them, and more than anything, you can now iterate directly over a String object:
let vowels = "AEIOU"
for char in vowels {
print(char)
}
This prints the following:
A , E , I , O, U
Here, you not only get logical iteration through String, but also specific understanding for collection and sequence:
vowels.count , result is 5, no need of vowels.characters.count
vowels.isEmpty , result is false
vowels.dropFirst() , result is "EIOU"
String(vowels.reversed()) , result is "UOIEA"
There is a small improvement to the way characters behave—now you can obtain the UnicodeScalarView
straight from the character, whereas earlier, instantiation of a new String
was needed.
Back in the days of Swift 1, Strings were a collection. In Swift 2, collection conformance was dropped because some of the behavior was thought to differ strongly enough from other collection types. Swift 4 reverses this change, so Strings are collection types again. One of the examples was described in the previous section where you were able to traverse over String vowels just like a normal array. In Swift 3, despite not being a collection, you could perform slicing operations on a String. Slicing a String in Swift 3 returned a String as well. This String was a String's own SubSequence
, which led to some memory issues and one of the bigger changes in Swift 4 is the introduction of the subString
types to remove these issues. For example, consider that we do this:
let secondIndex = vowels.index(after: vowels.startindex)
let subString = vowels[secondIndex...]
On doing this, if you inspect the type of subString
, you will notice that it is String.SubSequence
and not a String. With the existence of two types of String and SubSequence
for adding functionality to Strings in your code base, you have to extend both types individually, which is quite cumbersome. Let's take a look at the following example:
We will add an extension to the Character
type to determine whether a character is in uppercase:
extension Character {
var isUpperCase : Bool {
return String(self) == String(self).uppercased()
}
}
Using this, let's define a stripped uppercase method on String:
extension String {
func strippedUppercase() -> String {
return self.filter({ !$0.isUppercase})
}
}
So now that we have this method, we can use it on a String. So we can say that vowels.strippedUppercase()
will return an empty String since all the characters in the vowels String are already uppercase.
If we grab a slice of the String though, that is, subString
that we got earlier in the execution and use subString.strippedUppercase()
, we get an error as subString
is not a String anymore.
Does this mean that we need to extend the subString
type and add this strippedUppercase()
method as well? Thankfully NO!
Swift 4 also introduces String protocol. Both String and subString
affirms to this new String protocol type. So anywhere we use extend String in our code base, we should now extend String protocol instead to ensure that subString
gets the same behavior. So let's move the strippedUppercase()
method into an extension of String protocol:
extension StringProtocol {
func strippedUppercase() -> String {
return self.filter({ !$0.isUppercase})
}
}
When we do this, we get an error because we need to be aware of what self means inside the method; self can now mean either a String or subString
. To ensure that we always account for this, we will always convert self, make it a String instance, and then do the work we need. So if self is already a String, nothing happens and the function does not throw an error, but if it is a subString,
we will make it a String and then call filter, and the preceding function works just fine:
extension StringProtocol {
func strippedUppercase() -> String {
return String(self).filter({ !$0.isUppercase })
}
}
There are more changes in String, but this should be the most used part that we might use from day to day.
Changed interpretation of grapheme clusters
An additional big advancement is the way String interprets grapheme clusters. Conformity of Unicode 9 gives resolution to this.
The use of extended grapheme clusters for character values in Swift 4 means that concatenation and modification of Strings may cause no affect on a resulting String's character count.
For example, if you append a COMBINING ACUTE ACCENT (U+0301) to the end of the String initialized to "cafe", the resulting String will have a character count of 4, and the fourth character will be "e", not e':
var word = "cafe"
print("total chars in \(word) is \(word.count)")
It prints "total chars in cafe is 4":
word += "\u{301}" // COMBINING ACUTE ACCENT, U+0301
print("totalchars in \(word) is \(word.count)")
It prints "total chars in café is 4", whereas the count would increase by 1 to reflect 5 as a result of print statement earlier.
Note
Sequence of one or more Unicode scalars that when combined generate a single human-readable character is known as an extended grapheme cluster.
Similar to Dictionaries, the total number of modifications made to String API can be summed up by the following image:
In the next section, we will discuss new additions to the existing libraries and functionalities available in Swift.
JSON encoding and decoding
Everyone's favorite change to Swift 4 is of course the latest way to parse JSON. If you have been part of the Swift community for a while now, you'll know that every month we have a new hot way to parse JSON or so it feels, and now finally, we have an official way, so let's take a look. Let's talk about the Why first; Swift didn't ship with any native archival or serialization APIs and instead benefited from the Objective-C implementations available to it. While this allowed us to achieve certain end goals, it was both restrictive in that only NSObject subclasses can benefit, and we had to implement solutions for value types and as always, the existing solutions, NSJSONSerialization
and so on, are far from Swift like.
The goal, therefore, for this proposal was to allow native archival and serialization of Swift Enums and Structs and to do that in a type safe way. Let's introduce some sample JSON to our playground to see how it works in practice and as an added benefit, we get to work with Swift 4's multiline String literal syntax:
import Foundation
let exampleJson = """
{
"name": "Photography as a Science",
"release_date": "2017-11-12T14:00:00Z",
"authors": [
{
"name": "Noor Jones"
},
{
"name": "Larry Page"
}
]
}
"""
We cannot convert this raw JSON in a datatype, so let's say as follows:
let json = exampleJson.data(using: .utf8)!
Now, let's define a type, and we will keep it simple so that we can see how the new API works:
struct Book {
let name: String
}
We want to decode the JSON into an instance of this struct Book
and doing that in Swift 4 is super easy. First, we will make our model conform to our new Codable
protocol, after which all we need to do is the following:
struct Book: Codable{
let name: String
}
let photographyBook = try! JSONDecoder().decode(Book.self, from: json)
If we match the values on the instance, we can see that they match the value in the JSON data and that's it, easy isn't it?
Multiline String literals
With earlier versions of Swift, you had to use \n
in your Strings to add line breaks, which meant if the String was very long, then your code would start looking ugly with heaps of \n
sprinkled across it. Proposal SEO163 introduces multiline literals to Swift with a very simple syntax. Long Strings or multiline Strings are Strings delimited by triple quotes, that is, """
so we can say as follows:
let paragraph = """
This is a paragraph to demonstrate an example of multi-line String literals and the use in the latest Swift 4 syntax!
"""
So you have to end the multiline String literals with triple quotes as well, as shown in the preceding code. The nice part about these multiline String literals is that they can contain newlines, single quotes, nested, and unescaped double quotes without the need to escape them. As an example, you can include some sample JSON to test against without escaping every single quote.
Another important change introduced by Swift 4 is that of smarter key paths. Swift key paths are strongly typed and enforce a compile time check and remove a common runtime error.
You write a key path by starting with a backslash: `\Book.title`
. Every type automatically gets a `[keyPath: …]`
subscript to get or set the value at the specified key path:
struct Book {
var title = ""
let price : Float
}
let titleKeyPath = \Book.name
let mathsBook = Book(name: "Algebra", price: 10.50)
mathsBook[keyPath: titleKeyPath]
The value in the earlier mentioned keyPath
is "Algebra"
.
The titleKeyPath
object defines a citation to the name property. Then, it can be used as a subscript on that object. You can store and manipulate key paths. For example, you can append additional segments to a key path to drill down further. Key paths are composed of a root, and then you can drill down by following a combination of properties and subscripts.
If you change the variable of mathsBook
from let to var, a specific property can also be modified through the keyPath
subscript syntax:
mathsBook[keyPath: titleKeyPath] = "Trigonometry"
let newTitle = mathsBook[keyPath: titleKeyPath]
The value in the mentioned keyPath
is "Trigonometry"
.
Swift 4 makes it optional to provide a starting index or finishing index of ranges, as used with earlier versions of Swift.
With earlier versions of Swift, you had to do the following to use ranges:
let contactNames = [“Alex”, “Ravi”, “Moin”, “Vlad”, “Mark”]
let firstTwoContacts = contactNames[0..<2]
let lastThreeContacts = contactNames[2..<contactNames.count]
print(firstTwoContacts)
print(lastThreeContacts)
You will get the result as follows:
["Alex", "Ravi"] , for [0..<2]
["Moin", "Vlad", "Mark"], for [2..<contactNames.count]
However, with Swift 4, you no longer have to be constrained to lower bounds or upper bounds of the range mentioned in the for
loop mentioned earlier in the code example. You can now use a one sided range where the missing side will automatically be treated as the start or end of a sequence:
let firstTwoContacts = contactNames[..<2]
let lastThreeContacts = contactNames[2...]
print(firstTwoContacts)
print(lastThreeContacts)
You will get the result as shown:
["Alex", "Ravi"] , for [..<2]
["Moin", "Vlad", "Mark"] for [2...]
Pattern matching with one sided ranges.
Pattern matching works really well with one sided ranges in switch statements, but you should be mindful of the one hitch that it has.
While writing a switch case, be careful to add a default case since you have to make your switch case exhaustive and since one sided ranges are infinite now, adding a default case becomes mandatory:
let selectedNumber = 7
switch selectedNumber {
case ..<0 :
print("You have selected a negative number.")
case 0... :
print("You have selected a positive number")
default :
break
}
Here, note that we have already covered all the scenarios in the first 2 cases:
- Case 1: All negative numbers up to -1
- Case 2: All positive numbers from 0 onward
Hence, we simply break out the switch statement in the default case.
The swap(_:_: )
method in Swift 3 works on "pass by reference principle" and swaps two elements of a given array on the spot. In pass by reference, actual memory addresses are used rather than values:
var integerArray = [1, 2, 4, 3, 5]
swap(integerArray [2], integerArray [3])
As you can see, the parameters are passed as in out parameters, which means the actual references or placeholder addresses are accessible directly inside the function. On the other hand, Swift 4’s swapAt(_:_:)
works on “pass by value” principle and only the corresponding indices are passed to the function to be swapped:
integerArray.swapAt(2, 3)
The swap(_:_:)
function will not be seen in Swift 4, because it will be deprecated and removed, and you have a couple of approaches to replace it. The first approach uses a temporary constant as follows:
let temp = a
a = b
b = temp
The second takes advantage of Swift's built in tuples, as follows:
(b, a) = (a, b)
With earlier versions of Swift, behavior of NSNumber
might be unexpected and casting to Uint8 might result in absurd results:
let number1 = NSNumber(value: 1000)
let number1ConvertedToUInt = number1 as? UInt8
In this scenario, logically, the value inside number1ConvertedToUInt
should be nil, but this was not the case and the value would be 1000% 255, that is, 232 instead. This is because the maximum value that a UInt can hold is 255. Fortunately, this behavior has been resolved in Swift 4 and now if you execute the same code in Swift 4, you should expect the value of number1ConvertedToUInt
to be nil.
Directly access unicode scalars of characters
Swift 4 allows direct access to unicode scalars associated with characters:
let character: Character = “A”
let unicodeScalar = character.unicodeScalars
Done! Easy, isn’t it? Before you would have to convert the character to a String first and then try to access unicode scalar.