Swift 4.0 与 Swift 3.0 - 差异和新功能

已发表: 2021-10-05

苹果发布著名的 Swift 语言新版本的那一天终于到来了。 我们应该从中得到什么? 在充满期待的同时,我们决定对Swift 4版本中将出现的新更新进行简要概述。

3 Swift 的基石。

作为一种非常棒的编写代码的语言,Swift 有其自身的优势,并且据称“比 Objective-C 语言更长寿”。

欢迎您阅读 Swift 和 Objective-C 之间的主要区别

Swift速度快类型安全并且非常具有表现力。 它可用于在手机和平​​板电脑、台式机和服务器上编写软件——显然,在所有运行代码的地方。 它欢迎您使用它 - 使用 Apple 的学习如何编码 Swift Playgrounds 应用程序或在 Xcode 中使用 Playgrounds,您可以立即看到您的工作结果,无需专心开发和运行应用程序第一名。 随着每个新的附加版本,它变得更好更快,Swift 4 版本就是这种情况。
准备好了?

Xcode 9 为 Swift 4 提供的另一个很棒的功能 - 您不必太担心即将到来的迁移,并且在阅读本文时您会弄清楚原因。

说到这里,让我们简要地探讨一下今年秋季为我们带来的糖果和 Swift 4 新功能。

入门

如果没有方便的 IDE,即  开发人员世界中的 Xcode,语言本身并不是很有用。 您可以从 Mac App Store 或 Apple Developer 网站的下载页面下载最新版本的 Xcode 9,但首先要确保您拥有一个有效的开发者帐户。 它非常稳定,因此您可以在日常编码例程中用它替换 Xcode 8。

您还可以使用 xcversion 安装多个版本的 Xcode

如果您正在开始一个新项目 - 您很高兴。 但是如果你已经有一个用 Swift 3.x 编写的项目 - 你必须经历一个迁移过程。

我们建议首先在 Playground 上试用 - 习惯使用新功能。

在阅读本文时,您会注意到“SE-____”格式的 Swift Evolution 提案链接。

迁移到 Swift 4

从 Swift 的一个主要版本到下一个版本的迁移一直非常激烈,尤其是从 Swift 2.x 到 3.0。 通常每个项目大约需要 1-2 天,但迁移到 Swift 4 更容易一些,并且可以更快地通过。

迁移前准备

Xcode 9 不仅支持 Swift 4,还支持过渡版本 3.2,因此您的项目编译应该没有任何困难。 这是可能的,因为 Swift 4 编译器和迁移工具支持这两种语言版本。 您可以为每个目标指定不同的 Swift 版本,如果某些第三方库尚未更新或者您的项目中有多个目标,这将非常有用。 然而,不仅仅是语言,SDK 也发生了一些变化,因此随着 Apple 继续完善 SDK API,很可能必须对您的代码应用一些更新......

Swift 迁移工具

与往常一样,Apple 提供了捆绑在 Xcode 中的 Swift 迁移工具,可以帮助从以前的 Swift 版本迁移。 您可以在 Xcode 中启动它,方法是转到Edit -> Convert -> To Current Swift Syntax...并选择要转换的目标。

然后会询问您要应用哪种 Objective-C 推理偏好:

由于附加更改在 Swift 4 中占主导地位,因此 Swift 迁移工具将为您管理大部分更改。

可可豆

大多数  开发人员在他们的项目中使用 CocoaPods 依赖项管理器,因为 Swift Package Manager 虽然改进得非常快,但还没有达到应有的成熟度。 如上所述,并非所有第三方库都已更新到 Swift 4,因此您在编译其中一些库时可能会看到错误。 一个可能的解决方案来解决这个问题,指定版本,雨燕3.2的哪些不是通过添加还没有被更新的豆荚post_install脚本你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

然后运行

pod install

现在您可以毫无错误地编译 pod。

让我们来看看 Swift 4 API 的更改和添加。

API 更改和添加

字符串

由于 SE-0163 提案, String现在符合Collection协议。 还记得 Swift 1.x 吗?

现在不需要characters数组属性,因为您可以直接遍历String

 let string = "Hello, Mind Studios!" for character in string { print(character) }

这也意味着您可以在String上使用任何Collection方法和属性,例如countisEmptymap()filter()index(of:)等等:

 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"

新的Substring类型

Swift 4 带来了新的Substring类型,它表示String Substring序列(如上面提到的 SE-0163 中所述)。

 // Split string into substrings let string = "Hello, Mind Studios!" let parts = string.split(separator: " ") // ["Hello,", "Mind", "Studios!"] type(of: parts.first!) // Substring.Type

StringSubstring现在都支持新的StringProtocol ,这使它们几乎相同且可互操作:

 var hello = parts.first! // Concatenate a String onto a Substring hello += " !" // "Hello, !" // Create a String from a Substring let helloDog = String(hello) // "Hello, !"

重要的提示

SE-0163 有一个非常重要的说明:

 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.

这意味着Substring旨在用作String序列的临时存储。 如果要将其传递给某些方法或其他类 - 首先将其转换为String

 let substring: Substring = ... // Substring let string = String(substring) // String someMethod(string)

无论如何,Swift 的类型系统将帮助您不要将Substring传递到需要String地方(假设您没有使用 new StringProtocol作为参数类型)。

多行字符串文字

SE-0168 使用三个双引号"""为多行字符串文字引入了一种简单的语法,这意味着可以粘贴大多数文本格式(例如 JSON 或 HTML)或一些长文本而无需任何转义:

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

在字符串文字中转义换行符

SE-0182 添加了在多行字符串文字中使用反斜杠转义换行符的功能。

 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

改进的 Unicode 支持

Swift 4 支持 Unicode 9,这意味着 unicode 字符计数问题现已消失:

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

上面强调的所有更改和已实施的提案(以及更多其他提案)都源于一组广泛描述的功能,称为字符串宣言。

访问控制

Swift 3 为访问控制带来了一个非常矛盾的元素fileprivate访问修饰符,这可能真的很令人困惑。
以前, private访问级别修饰符用于对其他类型隐藏类型成员,私有成员只能通过类型定义中定义的方法和属性访问,而将相同的类型扩展放在一边,因为它们无法访问这些成员。
fileprivate可用于在同一文件中共享类型成员的访问权限,例如属性和方法。
事实上,当某些类型的扩展无法访问该类型的成员时,使用private导致问题,因此在这种情况下使用fileprivate是一个非常常见的解决方案,这导致了另一个问题:相同的文件也可以访问这些成员。

Swift 4 通过允许类型的扩展访问同一文件中的该类型的private成员来整理事情,如 SE-0169 中所述:

 struct User { private let firstName: String private let lastName: String } extension User: CustomStringConvertible { var description: String { return "User: \(firstName) \(lastName)" } }

字典和集合

Sequence初始化Dictionary

Dictionary现在可以用Sequence初始化,但并不是所有的序列都可以在这个初始化器中传递,只有那些包含元组(Key, Value) ,其中Key是 Dictionary 的键类型, Value表示字典值类型:

 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]

这里zip函数从 2 个序列创建一对( Tuple s),您可以在 Swift 标准库文档中阅读有关此函数的更多信息。

合并字典

通过将闭包传递给uniquingKeysWith参数,您可以指定在从Tuple序列创建字典时应如何处理重复键,该参数用于组合来自 2 个相同键的值。

示例 1:

 let duplicates = [("a", 1), ("b", 5), ("a", 3), ("b", 3)] let dictionary = Dictionary(duplicates, uniquingKeysWith: { (first, _) in return first }) // ["b": 5, "a": 1]

这里我们保留第一个值,忽略所有具有相同键的下一个值。

示例 2:

计算每个字符在字符串中出现的次数。

 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]

示例 3:

使用merge方法:

 let values = ["a": 1, "b": 5] var additionalValues = ["b": 3, "c": 2, "a": 3] additionalValues.merge(values, uniquingKeysWith: +) // ["b": 8, "c": 2, "a": 4]

带默认值的下标

以前,一种常见的做法是使用 nil 合并运算符在值为 nil 时给出默认值。

斯威夫特 3:

 let dict = ["a": 1, "b": 5] dict["c"] ?? 0 // 0

Swift 4 在下标上引入了新的default值(SE-0165 的一部分):

 let dict = ["a": 1, "b": 5] dict["c", default: 0] // 0, equals to `dict["c"] ?? 0` in Swift 3

您还可以在使用默认值下标的同时改变字典:

 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]

特定于字典的映射和过滤器

分组序列元素

协议中的受限关联类型

SE-0142 提案在关联类型声明中引入了条件子句。

 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

存档和序列化(编码/解码)

以前,为了序列化一些自定义类型,您必须使用旧的和众所周知的NSCoding协议。 问题在于,诸如structenum类的非类类型无法符合此协议,因此开发人员只需使用一些技巧,例如通过创建可以符合NSCoding的嵌套类来提供额外的兼容性层。

由于 SE-0166 - Codable协议的引入,Swift 4 为这个问题提供了一个非常方便的解决方案:

 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 }

在像这样的简单情况下,您只需要为所有自定义类型添加Codable协议一致性,编译器将为您完成所有魔术。 就是这样!

Codabletypealias对的组成DecodableEncodable协议,因此你可以声明,例如,只Decodable协议一致性,如果你想你的类型实例从JSON数据进行解码。

编码

如果要序列化或反序列化Codable值 - 您必须使用和编码器或解码器对象。 Swift 4 已经带有一组用于 JSON 和属性列表的编码器/解码器,以及用于在编码/解码过程中可能抛出的不同类型错误的新CocoaError s。 NSKeyedArchiverNSKeyedUnarchiver也支持Codable类型。

 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" } ] }

小菜一碟,不是吗?

解码

解码器用于从Data反序列化自定义Codable类型。 它不知道从数据本身解码哪种类型,因此您应该指定要解码的类型,例如, Employee[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.

自定义键名

在大多数情况下,我们在自定义 Swift 类型中使用的名称与表示此类型的 JSON 数据中的键不匹配。 要在自定义类型属性名称和 JSON 键之间创建映射,您可以创建一个名为CodingKeys的嵌套枚举,该枚举应符合CodingKey协议:

 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" } }

自定义解码

如果您有一个复杂的案例,您可以从Decodable协议实现您的自定义初始值Decodable

 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 = "" } } }

键值编码

Swift 4 带来的便利功能之一是 SE-0161 中描述的 Smart KeyPath。 与 Swift 3 #keyPath() ,后者不是强类型并且仅适用于 Objective-C 成员,Swift 4 KeyPath 是一个泛型类,这意味着键路径现在是强类型的。 让我们深入研究一些例子:

 struct User { var username: String }

键路径\<Type>.<path>的一般形式,其中<Type>是类型名称,而<path>是一个或多个属性的链,例如, \User.username

 let user = User(username: "max") let username = user[keyPath: \User.username] // "max"

如果它是可变的,您还可以通过此键路径写入新值:

 var user = User(username: "max") user[keyPath: \User.username] = "alex" // "alex"

关键路径不限于一级层次结构:

 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"

关键路径可以存储在一个变量中:

 let authorKeyPath = \Comment.author let usernameKeyPath = authorKeyPath.appending(path: \.username) let authorUsername = comment[keyPath: usernameKeyPath] // "max"

您还可以将键路径用于可选和计算属性:

 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"

尽管 SE-0161 突出显示了关键路径中的支持下标,但它们尚未实现:

 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

除了新的键路径之外,Swift 4 中也更新了键值观察 API。

 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

如果要停止观察,请调用invalidate()方法

nameObservation.invalidate() user.name = "Elina" // observer isn't get called

它在 deinited 时也会停止,所以如果你想保存它,请确保将它存储在财产或其他地方。

单边范围

SE-0172 引入了通过现有范围运算符的前缀/后缀版本创建的“单边”范围,以及新的RangeExpression协议,以简化采用不同类型范围的方法的创建。

无限序列

您可以使用单边范围来构造无限序列:

 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,"

在模式匹配中使用单边范围

let value = 5 switch value { case 1...: print("greater than zero") case 0: print("zero") case ..<0: print("less than zero") default: break }

通用下标

SE-0148 下标现在可以具有通用参数和返回类型

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

限制 Objective-C 推理

Swift 4 通过将 @objc 推理限制为仅适用于声明必须可用于 Objective-C (SE-0160) 的情况来最小化 @objc 推理。
如果您不使用它,这会通过不编译多余的 Objective-C 代码来减少您的应用程序的二进制大小,并为何时推断 @objc 提供更多控制。 NSObject 派生类不再推断 @objc。

但是在某些情况下,Swift 代码会继续进行隐式推理:

  • 具有@objc 属性的声明

  • 满足@objc 协议要求的声明

  • 具有@IBAction、@IBInspectable、@IBOutlet、@NSManaged、@GKInspectable 属性的声明

要为整个类启用 @objc 推理,您可以使用新的 @objcmembers 属性。
要禁用特定扩展或函数的 @objc 推理 - 添加新的 @nonobjc 属性。

组合类和协议

在 Swift 4 中,我们现在可以将协议与其他 Swift 类型组合在一起:

 User & Codable & CustomStringConvertible typealias MyType = User & Codable & CustomStringConvertible

Swift 4 的好处

Swift 4 的优势真的很大,因为它经常发生在 Apple 发布新语言版本时。 除了语言性能的提升之外,它还极大地稳定了迁移过程。 让我们回想起将 Swift 2.2 迁移到 3.0 的过程,我们回想起转移所有依赖项的复杂过程。 Swift 4.0 的变化让我们无需真正“重新定位”第三方库——你只需要更新 Swift 本身。

另外关于 Swift 4.0 与 3.0 的改进,编译后的二进制文件大小发生了变化,导致应用程序的大小减小; 例如,过去的移动应用程序重 20 MB,而在最新的 Swift 版本中,它大约需要 17 MB。 Swift 4 和 Swift 3 之间有一个基本的区别——错误修复已经发生,语言变得更加快速。

Swift 已经使用多年了,而且它会随着每个即将到来的更新而不断发展。 每一种新语言都会更新新的开发视角,之前未知的,我们期待探索新的 iOS 视野。

不要错过我们关于 iOS 开发的 MVP vs MVC vs MVVM vs VIPER 的文章。

由 Max Mashkov 和 Elina Bessarabova 撰写