Swift 3.0 vs Swift 4.0: What are the Main Differences Between Swift 3 and Swift 4

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 (Tuples) 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 Tuples 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 CocoaErrors 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.