The day Apple releases a new version of the well-known Swift language has finally hit us. What should we expect from it? While trembling of anticipation, we have decided to present swift 3 vs swift 4 comparison and a tiny overview of the fresh updates that will be present in Swift 4 version.
3 Swift’s cornerstones.
As a fantastic language to write code on, Swift has its own benefits, and is claimed to “outlive” the Objective-C language.
You are welcome to read about the main differences between Swift and Objective-C
Swift is fast, type-safe and very expressive. It could be used to write software on phones and tablets, desktops and servers - apparently, on everything that runs code. It welcomes you to play with it - with Apple’s learn-how-to-code Swift Playgrounds app or using Playgrounds in Xcode you get to see the results of your work immediately, no need to put your nose down into developing and running an app at first place. With every new additive version it becomes better and faster, and that’s the case with Swift 4 version.
Ready-steady?
Another great feature that Xcode 9 possesses for Swift 4 - you don’t need to worry much about the upcoming migration and you will figure out why while reading this article.
Speaking of which, let’s explore briefly what bonbons and Swift 4 new features this fall brings us.
Getting Started
Language itself is not very useful without a handy IDE, which is Xcode in developers world. You can download latest version of Xcode 9 from Mac App Store or at Downloads page at Apple Developer site, but make sure that you have an active developer account firstly. It's pretty stable so you can replace Xcode 8 with it for your daily coding routines.
You can also install multiple versions of Xcode using xcversion
If you are starting a new project - you are good to go. But if you already have a project written in Swift 3.x - you have to go through a migration process.
We recommend to firstly try on the Playground - to get accustomed using new features.
You’ll notice links to Swift Evolution proposals in the 'SE-____' format as you’re reading this article.
Migrating to Swift 4 from Swift 3
Migration from one major version of Swift to the next one has always been pretty intense, especially from Swift 2.x to 3.0. Usually it takes about 1-2 days per project, but migration to Swift 4 from swift 3 is a bit easier and can be passed much faster.
Pre-migration preparation
Xcode 9 supports not only Swift 4, but a transitional version 3.2 as well, so your project should compile without any harsh difficulties. This is possible because Swift 4 compiler and migration tool support both versions of language. You can specify different Swift versions per target, this is very helpful if some third-party libraries didn't update yet or if you have multiple targets in your project. However, not just language, but the SDK has got some changes too, so it's very likely that some updates will have to be applied to your code as Apple continues to tide up SDK APIs...
Tool to migrate from swift 3 to swift 4
As always, Apple provides Swift migration tool bundled within Xcode which can help migrating from previous Swift version. You can launch it in Xcode by going to Edit -> Convert -> To Current Swift Syntax… and selecting which targets you want to convert.
You will than be asked which Objective-C inference preference you want to apply:
As the additive changes dominate in Swift 4, the Swift migration tool will manage most of the changes for you.
CocoaPods
Most developers use CocoaPods dependency manager for their projects as Swift Package Manager is not as mature as it could be, although it's improving very fast. As mentioned above, not all third-party libraries were updated to Swift 4 yet, so you could see errors while compiling some of them. One possible solution to fix this problem is specifying Swift version 3.2
for those pods which weren't updated yet by adding a post_install
script to you Podfile
:
old_swift_3_pods = [
'PodName1',
'PodName2',
]
post_install do |installer|
installer.pods_project.targets.each do |target|
if old_swift_3_pods.include? target.name
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '3.2'
end
end
end
end
Then run
pod install
Now you can compile pods without errors.
Let's examine about Swift 4 API changes and additions.
API changes and additions
Strings
String
now conforms to Collection
protocol thanks to SE-0163 proposal. Remember Swift 1.x?
There is no need in characters
array property now as you can iterate over String
directly:
let string = "Hello, Mind Studios!"
for character in string {
print(character)
}
This also means that you can use any Collection
methods and properties on String
, like count
, isEmpty
, map()
, filter()
, index(of:)
and many more:
string.count // No more `string.characters.count` 🙃
string.isEmpty // false
let index = string.index(of: " ") // 6
let reversedCollection = "abc".reversed()
let reversedString = String(reversedCollection) // "cba"
// String filtering
let string = "ni123n456iniASijasod! 78a9-kasd aosd0"
let numbersString = string.filter { Int(String($0)) != nil } // "1234567890"
New Substring
type
Swift 4 brings new Substring
type which represents a subsequence of String
(as described in SE-0163 mentioned above).
// Split string into substrings
let string = "Hello, Mind Studios!"
let parts = string.split(separator: " ") // ["Hello,", "Mind", "Studios!"]
type(of: parts.first!) // Substring.Type
Both String
and Substring
now support new StringProtocol
which makes them almost identical and interoperable:
var hello = parts.first!
// Concatenate a String onto a Substring
hello += " 🐶!" // "Hello, 🐶!"
// Create a String from a Substring
let helloDog = String(hello) // "Hello, 🐶!"
Important note
SE-0163 has a very important note:
Long-term storage of `Substring` instances is discouraged. A substring holds a reference to the entire storage of a larger string, not just to the portion it presents, even after the original string’s lifetime ends. Long-term storage of a substring may therefore prolong the lifetime of elements that are no longer otherwise accessible, which can appear to be memory leakage.
Which means that Substring
is intended to be used as a temp storage for String
subsequence. If you want to pass it to some methods or other classes - convert it to String
firstly:
let substring: Substring = ... // Substring
let string = String(substring) // String
someMethod(string)
Anyway, Swift's type system will help you not to pass Substring
somewhere where String
is expected (assuming that you are not using new StringProtocol
as parameter type).
Multi-line string literals
SE-0168 introduces a simple syntax for multi-line string literals using three double-quotes """
which means that most text formats (such as JSON or HTML) or some long text can be pasted in without any escaping:
let multilineString = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nam mattis lorem et leo laoreet fermentum.
Mauris pretium enim ac mi tempor viverra et fermentum nisl.
Sed diam nibh, posuere non lectus at, ornare bibendum erat.
Fusce mattis sem ac feugiat vulputate. Morbi at nunc maximus, vestibulum orci et, dictum neque.
Vestibulum vulputate augue ac libero vulputate vestibulum.
Nullam blandit et sapien non fermentum.
Proin mollis nisl at vulputate euismod.
"""
Escaping newlines in string literals
SE-0182 adds the ability to escape newlines in multi-line string literals with a backslash in the end of the line.
let escapedNewline = """
Line 1,
Line 2 \
next part of line 2,
Line 3
"""
print(escapedNewline)
Line 1,
Line 2 next part of line 2,
Line 3
Improved Unicode support
Swift 4 brings support for Unicode 9 which means that problems with unicode characters counting are now gone:
"👩💻".count // 1, in Swift 3: 2
"👍🏽".count // 1, in Swift 3: 2
"👧🏽".count // 1, in Swift 3: 2 - person + skin tone
"👨👩👧👦".count // 1, in Swift 3: 4
"🇺🇦🇺🇸🇮🇪".count // 3, in Swift 3: 1
All of the changes and implemented proposals highlighted above (as many more others) are taking their roots from a broadly described set of features called the String Manifesto.
Access control
Swift 3 brought a very contradictory element to Access control - fileprivate
access modifier which can be really confusing.
Previously, private
access level modifier was used to hide type members from other types and private members could only be accessed by methods and properties defined at type definition, leaving the same type extensions aside as they couldn't access those members.
fileprivate
could be used to share access for type members, such as properties and methods, within the same file.
In fact usage of private
led to a problem when extensions on some type didn't have access to members of that type, so using fileprivate
in under such circumstances was a very common solution, which has led to another the problem: other types in the same file could access those members too.
Swift 4 puts things in order by allowing extensions on the type to access private
members of that type in the same file as described in SE-0169:
struct User {
private let firstName: String
private let lastName: String
}
extension User: CustomStringConvertible {
var description: String {
return "User: \(firstName) \(lastName)"
}
}
Dictionary and Set
Initializing Dictionary
with Sequence
Dictionary
can be initialized with Sequence
now, but not all sequences could be passed in this initializer, only those containing tuples (Key, Value)
, where Key
is the key type of the Dictionary and Value
represents Dictionary value type:
let stocksIdentifiers = ["AAPL", "GOOGL", "NKE"]
let stocksValues = [158.28, 940.13, 53.73]
let pairs = zip(stocksIdentifiers, stocksValues)
let stocksValuesDict = Dictionary(uniqueKeysWithValues: pairs) // ["GOOGL": 940.13, "NKE": 53.73, "AAPL": 158.28]
Here zip
function creates a pair (Tuple
s) from 2 sequences, you can read more about this function in Swift Standard Library documentation.
Merging Dictionaries
You can specify how duplicate keys should be handled when creating a dictionary from a sequence of Tuple
s by passing a closure to uniquingKeysWith
parameter, which is used to combine values from 2 identical keys.
Example 1:
let duplicates = [("a", 1), ("b", 5), ("a", 3), ("b", 3)]
let dictionary = Dictionary(duplicates, uniquingKeysWith: { (first, _) in
return first
}) // ["b": 5, "a": 1]
Here we leave the first value ignoring all next values with the same key.
Example 2:
Counting how many times each character appears in string.
let string = "Hello!"
let pairs = Array(zip(string, repeatElement(1, count: string.count)))
let counts = Dictionary(pairs, uniquingKeysWith: +) // ["H": 1, "e": 1, "o": 1, "l": 2, "!": 1]
Example 3:
Using merge
method:
let values = ["a": 1, "b": 5]
var additionalValues = ["b": 3, "c": 2, "a": 3]
additionalValues.merge(values, uniquingKeysWith: +) // ["b": 8, "c": 2, "a": 4]
Subscript with default value
Previously, a common practice was to use the nil coalescing operator to give a default value in case the value is nil.
Swift 3:
let dict = ["a": 1, "b": 5]
dict["c"] ?? 0 // 0
Swift 4 introduces new default
value on subscripts (part of SE-0165):
let dict = ["a": 1, "b": 5]
dict["c", default: 0] // 0, equals to `dict["c"] ?? 0` in Swift 3
You can also mutate a dictionary while subscripting it with default value:
let string = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nam mattis lorem et leo laoreet fermentum.
Mauris pretium enim ac mi tempor viverra et fermentum nisl.
Sed diam nibh, posuere non lectus at, ornare bibendum erat.
Fusce mattis sem ac feugiat vulputate. Morbi at nunc maximus, vestibulum orci et, dictum neque.
Vestibulum vulputate augue ac libero vulputate vestibulum.
Nullam blandit et sapien non fermentum.
Proin mollis nisl at vulputate euismod.
"""
var wordsCountByLine = [Int: Int]()
let lines = string.split(separator: "\n")
for (index, line) in lines.enumerated() {
let lineWordsCount = line.split(separator: " ").count
wordsCountByLine[index, default: 0] += lineWordsCount
}
print(wordsCountByLine) // [2: 10, 4: 15, 5: 7, 6: 6, 7: 6, 0: 8, 1: 7, 3: 10]
Dictionary-specific map and filter
Grouping sequence elements
Constrained Associated Types in Protocols
Proposal SE-0142 introduces addition of conditional clauses to associated type declarations.
extension Sequence where Element: Numeric {
var sum: Element {
var result: Element = 0
for element in self {
result += element
}
return result
}
}
[1,2,3,4].sum
Archival & Serialization (Encoding / Decoding)
Previously, in order to serialize some custom type you'd have to use old and well known NSCoding
protocol. The problem is that non-class types such as struct
and enum
can't conform this protocol, so developers had nothing to do than use hacks like providing additional layer of compatibility by creating a nested class that could conform to NSCoding
.
Swift 4 has a very convenient solution to this problem thanks to SE-0166 - an introduction of Codable
protocol:
struct Employee: Codable {
let name: String
let age: Int
let role: Role
enum Role: String, Codable {
case manager
case developer
case admin
}
}
struct Company {
let name: String
let officeLocation: Location?
let employees: [Employee]
}
struct Location : Codable {
let latitude: Double
let longitude: Double
}
In simple case like this one all you need is to add Codable
protocol conformance to all your custom types, the compiler will do all the magic for you. That's it! 🚀
Codable
is a typealias
for a composition of Decodable
& Encodable
protocols, so you can declare, for example, only Decodable
protocol conformance if you want to decode your type instance from JSON data.
Encoding
If you want to serialize or deserialize a Codable
value - you have to use and encoder or decoder object. Swift 4 already comes with a set of encoders/decoders for JSON and property lists as well as new CocoaError
s for different types of errors that could be thrown during encoding/decoding. NSKeyedArchiver
& NSKeyedUnarchiver
also support Codable
types.
let employee = Employee(name: "Peter", age: 27, role: .manager)
let company = Company(name: "Awesome Company", officeLocation: nil, employees: [employee])
let encoder = JSONEncoder()
let companyData = try encoder.encode(company)
let string = String(data: companyData, encoding: .utf8)!
print(string)
>>>
{
"name" : "Awesome Company",
"employees" : [
{
"name" : "Peter",
"age" : 27,
"role" : "manager"
}
]
}
A piece of cake, isn't it? 🙃
Decoding
Decoder is used to deserialize custom Codable
type from Data
. It doesn't know which type to decode from data itself, so you should specify which type to decode, for example, Employee
or [Employee]
:
let decoder = JSONDecoder()
let jsonData = """
[
{
"name" : "Peter",
"age" : 27,
"role" : "manager"
},
{
"name" : "Alex",
"age" : 26,
"role" : "developer"
},
{
"name" : "Eugene",
"age" : 30,
"role" : "admin"
}
]
""".data(using: .utf8)!
let employees = try decoder.decode([Employee].self, from: jsonData)
If one of `Codable` type instances fails to decode, then whole collection will fail to decode.
Custom Key Names
In most cases names that we use in custom Swift types don't match the keys in JSON data that represents this type. To create a mapping between custom type properties names and JSON keys you can create a nested enum named CodingKeys
which should conform to CodingKey
protocol:
struct Country: Decodable {
let id: String
let name: String
let phoneCode: String
private enum CodingKeys: String, CodingKey {
case id = "alpha3"
case name
case phoneCode = "phone_code"
}
}
Custom Decoding
If you have a complex case you can implement your custom initializer from a Decodable
protocol:
struct Transaction {
let id: Int
let action: String
let source: String
let amount: Int
let state: TransactionState
let createdAt: Date
let authorName: String
enum TransactionState: String, Decodable {
case done
case canceled
case processed
}
}
extension Transaction: Decodable {
private enum CodingKeys: String, CodingKey {
case id
case action = "action_name"
case source = "source_name"
case amount
case state
case createdAt = "created_at"
case author
}
private enum AuthorKeys: String, CodingKey {
case fullName = "full_name"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
actionName = try container.decode(String.self, forKey: .action)
sourceName = try container.decode(String.self, forKey: .source)
let createdAtValue = try container.decode(Double.self, forKey: .createdAt)
createdAt = Date(timeIntervalSince1970: createdAtValue)
state = try container.decode(TransactionState.self, forKey: .state)
amount = try container.decodeIfPresent(Int.self, forKey: .amount) ?? 0
do {
let authorContainer = try container.nestedContainer(keyedBy: AuthorKeys.self, forKey: .author)
authorName = try authorContainer.decode(String.self, forKey: .fullName)
} catch {
authorName = ""
}
}
}
Key Value Coding
One of the handy Swift 4 features is Smart KeyPaths described in SE-0161. Unlike Swift 3 #keyPath()
, which is not strongly typed and works only for Objective-C members, Swift 4 KeyPath is a generic class, which means key paths are now strongly typed. Let's dive into some examples:
struct User {
var username: String
}
The general form of key path \<Type>.<path>
, where <Type>
is a type name, and <path>
is a chain of one or more property, for example, \User.username
:
let user = User(username: "max")
let username = user[keyPath: \User.username] // "max"
You can also write a new value by this key path if it's mutable:
var user = User(username: "max")
user[keyPath: \User.username] = "alex" // "alex"
Key paths are not limited to one level of hierarchy:
struct Comment {
let content: String
var author: User
}
let max = User(username: "max")
let comment = Comment(content: "Nice post!", author: max)
let authorUsername = comment[keyPath: \Comment.author.username] // "max"
Key paths can be stored in a variable:
let authorKeyPath = \Comment.author
let usernameKeyPath = authorKeyPath.appending(path: \.username)
let authorUsername = comment[keyPath: usernameKeyPath] // "max"
You can also use key paths for optional and computed properties:
struct Post {
let title: String
var comments: [Comment]
var topComment: Comment? {
return comments.first
}
}
let max = User(username: "max")
let alex = User(username: "alex")
var post = Post(title: "What's new in Swift 4", comments: [])
let topCommentAuthorUsernameKeyPath = \Post.topComment?.author.username
post[keyPath: topCommentAuthorUsernameKeyPath] // nil
let comment = Comment(content: "🚀", author: alex)
let anotherComment = Comment(content: "Nice post!", author: max)
post.comments = [comment, anotherComment]
post[keyPath: topCommentAuthorUsernameKeyPath] // "alex"
Despite that SE-0161 highlights support subscripts in key paths, they have not been implemented yet:
post.comments[keyPath: \.[0].content] // error: key path support for subscript components is not implemented
let firstCommentAuthorKeyPath = \Post.comments[0].author // error: key path support for subscript components is not implemented
KVO
In addition to new key paths, key-value observing API has been updated in Swift 4 too.
New KVO APIs depend on Objective-C runtime and works for `NSObject` subclasses only, so it can't be used for Swift structs and classes which don't inherit `NSObject`. In order to observe property it should be marked as `@objc dynamic var`.
class User: NSObject {
@objc dynamic var name: String
var username: String
init(name: String, username: String) {
self.name = name
self.userName = userName
super.init()
}
}
let user = User(name: "Max", username: "max")
let nameObservation = user.observe(\.name, options: [.new, .old]) { user, change in // NSKeyValueObservation
if let oldValue = change.oldValue, let newValue = change.newValue {
print("fullName has changed from \(oldValue) to \(newValue)")
} else {
print("fullName is now \(user.name)")
}
}
user.name = "Alex" // name has changed from Max to Alex
Call invalidate()
method if you want to stop observation
nameObservation.invalidate()
user.name = "Elina" // observer isn't get called
It's also stopped when deinited, so make sure to store it in property or somewhere else if you want to preserve it.
One-Sided Ranges
SE-0172 introduces "one-sided" ranges, created via prefix/postfix versions of the existing range operators, and a new RangeExpression
protocol to simplify the creation of methods that take different kinds of ranges.
Infinite Sequences
You can use a one-sided range to construct an infinite sequence:
let letters = ["a", "b", "c", "d"]
let numberedLetters = Array(zip(1..., letters)) // [(1, "a"), (2, "b"), (3, "c"), (4, "d")]
let string = "Hello, Mind Studios!"
let index = string.index(of: ",")!
string[..<index] // "Hello"
string[...index] // "Hello,"
Using One-Sided Ranges in Pattern Matching
let value = 5
switch value {
case 1...:
print("greater than zero")
case 0:
print("zero")
case ..<0:
print("less than zero")
default:
break
}
Generic Subscripts
SE-0148 Subscripts can now have generic arguments and return types 🚀
struct JSON {
let data: [String: Any]
subscript<T>(key: String) -> T? {
return data[key] as? T
}
}
let jsonDictionary: [String: Any] = [
"name": "Ukraine",
"flag": "🇺🇦",
"population": 42_500_000
]
let json = JSON(data: jsonDictionary)
let population: Int? = json["population"] // 42500600
extension Dictionary where Value == String {
subscript<T: RawRepresentable>(key: Key) -> T? where T.RawValue == Value {
guard let string = self[key] else {
return nil
}
return T(rawValue: string)
}
}
enum Color: String {
case red
case green
case blue
}
let dictionary = [1: "red"]
let color: Color? = dictionary[1] // red
Limiting Objective-C Inference
Swift 4 minimizes @objc inference by limiting it to only those cases when the declaration has to be available for Objective-C (SE-0160).
This decreases your app's binary size by not compiling redundant Objective-C code if you don't use it, and gives more control to when @objc will be inferred. NSObject derived classes no longer infer @objc.
But there are some situations in which Swift code will continue to have an implicit inference:
-
Declarations that have an @objc attribute
-
Declarations that satisfy a requirement of an @objc protocol
-
Declarations that have @IBAction, @IBInspectable, @IBOutlet, @NSManaged, @GKInspectable attributes
To enable @objc inference for an entire class you can use the new @objcmembers attribute.
To disable @objc inference for a specific extension or function - add the new @nonobjc attribute.
Composing Classes And Protocols
In Swift 4 we can now compose protocols together with other Swift types:
User & Codable & CustomStringConvertible
typealias MyType = User & Codable & CustomStringConvertible
Benefits Of Swift 4 Programming
The advantages of Swift 4 are really huge, as it often happens when Apple releases a new language version. Apart from the improved language performance, it has also stabilized the migration process greatly. Casting our minds back to the process of migrating Swift 2.2 to 3.0, we recall the perplex process of transferring all the dependencies. Swift 4.0 changes lets us leave the third-party libraries without actually “relocating” them - you just need to update Swift itself.
Also regarding the Swift 4.0 vs 3.0 improvements, the compiled binary files size has been changed, which has resulted in the decrease of app’s size; e. g. mobile application used to weigh 20 MB, and in the newest Swift-version it will take around 17 MB. And there is a basic difference between Swift 4 and Swift 3 - the bug fixing has happened, and language has become a bit more rapid.
It’s been years since Swift has been in use, and it continues to evolve with every coming update. With each new language renew new development perspectives, unknown before, arise and we look forward to exploring new iOS horizons.
Don't miss out on our article about MVP vs MVC vs MVVM vs VIPER for iOS development.
Written by Max Mashkov and Elina Bessarabova.