跳转至内容

Swift 入门/Swift 进阶

来自 Wikibooks,开放世界中的开放书籍

Swift 进阶

[编辑 | 编辑源代码]

闭包表达式

[编辑 | 编辑源代码]

闭包表达式是未命名的匿名函数,可以与周围上下文中的值交互https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID94</ref>。它们是简短的类似函数的结构,没有声明或名称。在此代码片段中,您可以看到闭包表达式的通用语法。它以左花括号、参数列表和返回值开头。关键字in标记闭包主体开始的位置。在 return 语句之后,表达式以右花括号关闭。

// basic syntax
let add = {(int1: Int, int2: Int) -> Int in return int1 + int2}
var result = add(20,5)      // 25

如果闭包被分配给函数类型,则可以使用简写语法,因为 Swift 可以从上下文中推断数据类型。在最简短的版本中,您只需要告诉 Swift 如何处理参数。$0是第一个传入参数的简写语法,$1是第二个参数的简写语法,依此类推。

let subtract: (Int, Int) -> Int = { int1, int2 in return int1 - int2 }

let multiply: (Int, Int) -> Int = { $0 * $1 }

result = subtract(20,5)     // 15
result = multiply(20,5)     // 100

尾随闭包

[编辑 | 编辑源代码]

闭包可以作为参数传递给函数。尾随闭包可以用作函数的最后一个参数。它不是写在函数的括号内,而是写在括号之后。在下面的代码片段中,您可以看到如何将闭包传递给 Swift 的 map 函数。map函数可以作用于数组,然后对数组的每个项目执行闭包。在此示例中,map 函数检查年份是否为闰年。所有年份都转换为字符串,如果满足先决条件,则附加“是闰年”。

// create an Array with values from 1950 to 2020
var years = [Int]()
for year in stride(from: 1950, to: 2020, by: 1){
    years.append(year)
}

let leapYears = years.map{ (year) -> String in
    var output = ""
    let year = year
    if(year%400 == 0){
        output.append(String(year)+" is a leap year")
    }
    else if(year % 100 != 0 && year % 4 == 0){
        output.append(String(year)  + " is a leap year")
    }
    else{
        output.append(String(year) + " is not a leap year")
    }
    return output
}

for year in leapYears{
    print(year)
}

在 Swift 中,主要有两种类型的属性。存储属性用于存储与类或结构关联的变量和常量的值。计算属性不用于存储,而是用于计算值。

存储属性

[编辑 | 编辑源代码]

这些属性是类或结构实例的一部分。它可以具有变量或常量值。

struct Animal{
    let name: String
    let legs: Int
    var weight: Double
}

var sloth = Animal(name: "slothy", legs: 4, weight: 8.5)
print("Hi my name is \(sloth.name) and i have \(sloth.weight) kilos!")

sloth.weight = 9.2
print("Put one some weight... now i have \(sloth.weight) kilos :) !")

计算属性

[编辑 | 编辑源代码]

这些属性提供 getter 和 setter 来检索或设置变量的值。在此示例中,结构Circle有一个名为 diameter 的计算属性。它有一个 getter,返回 radius 的两倍值。setter 将 radius 的值更改为新diameter的一半。area是一个只读属性,这意味着它没有 setter。

struct Circle {
    var radius: Double
    var diameter: Double{
        get {
            let diameter = radius * 2
        return diameter
        }
        set(newDiameter){
            radius = newDiameter/2
        }
    }
    var area: Double{
        get{
            return radius * radius * Double.pi
        }
    }
}

var smallCircle = Circle(radius: 3)

let initialDiameter = smallCircle.diameter
smallCircle.diameter = 10
print("The circle now has a diameter of \(smallCircle.diameter), a radius of \(smallCircle.radius) and a area of \(smallCircle.area)")
// prints "The circle now has a diameter of 10.0, a radius of 5.0 and a area of 78.53"

属性观察器

[编辑 | 编辑源代码]

属性观察器可用于观察属性的状态并响应更改。每当属性的值被设置时,就会调用它们。观察器有两种类型。willSet在存储值之前被调用,didSet在存储值之后被调用。

class EctsCounter{
    var ectsCount: Int = 0{
        willSet(newCount){
            print("About to set your count to \(newCount) points!")
        }
        didSet{
            print("Added \(ectsCount - oldValue) points!")
        }
    }
}

let counter = EctsCounter()
counter.ectsCount += 10
// About to set your count to 10 points!
// Added 10 points!
counter.ectsCount += 4
// About to set your count to 14 points!
// Added 4 points!

使用 GCD 实现并发

[编辑 | 编辑源代码]

调度框架包含许多语言特性、运行时库和系统增强功能,这些增强功能提高了对在具有多个内核的硬件上并发代码执行的支持。Grand Central Dispatch (GCD) 将工作提交到系统管理的调度队列。

并发与并行

[编辑 | 编辑源代码]

并行是描述在多核处理器上同时执行两个或多个线程的概念的术语。具有单个内核的设备可以通过时间切片实现并发。这是通过上下文切换在多个线程之间切换的过程。GCD 管理一个线程池。代码块可以添加到调度队列中,GCD 决定执行什么。

DispatchQueue 代表 GCD 提供的调度队列[1]。您提交到队列的任务按照 FIFO 顺序执行,这意味着第一个提交的任务将始终是第一个开始的任务。队列有两种类型。串行队列在任何时间只能执行一个任务。并发队列可以同时启动多个任务。它们按照添加的顺序启动,可以以任何顺序完成。调度由 GCD 管理,这意味着它控制任务何时启动。GCD 提供一个主队列,它是一个串行队列,并在主线程上运行。主队列上的任务会立即执行。将所有更改或更新 UI 的任务都放在主队列上是一个好习惯。这确保了 UI 始终具有响应能力。四个全局队列是并发的,由整个系统共享,并分为优先级 - 高、默认、低和后台。队列也可以由用户创建,并且可以是串行或并发。

优先级不是直接指定的,而是通过使用服务质量 (QoS) 类指定的。

.user-interactive描述必须立即完成的任务,否则用户体验会很糟糕。此 QoS 用于 UI 更新和事件处理。

.user-initiated表示可以异步执行并从 UI 启动的任务。这些任务被映射到高优先级队列,因为它用于用户等待立即结果或任务需要用户交互才能继续时。

.utility表示长时间运行的任务。此类用于 I/O、网络和计算。通常使用进度指示器来确保用户知道正在发生的事情。

.background 描述了用户通常意识不到的任务。它用于不需要用户交互且不紧急的任务,例如维护或预取。

在 iOS 中使用队列

[编辑 | 编辑源代码]

以下代码片段可以在 Github 上找到,并且可以作为 Xcode 项目导入。一旦按下“开始”按钮,函数 download(size: Int, label: UILabel, timeout: Int) 就会被调用两次,并使用不同的输入参数。在将它们放入全局队列后,它们会异步执行模拟。每次迭代后,数量都会增加 1。接下来,需要更新显示下载进度的标签。为此,需要将任务放回主队列,以确保其立即执行。短暂超时后,下一轮迭代开始。

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var task1: UILabel!
    @IBOutlet weak var task2: UILabel!
    
    @IBAction func start(_ sender: UIButton) {
        download(size: 70, label: task1, timeout: 10)
        download(size: 50, label: task2, timeout: 7)
    }
    
    func download(size: Int, label: UILabel, timeout: Int) -> Void{
    
        DispatchQueue.global(qos: .userInitiated).async {
            // puts the download simulation on a global queue
            var amount = 0
            for _ in stride(from: 0, to: size, by: 1){
                amount += 1
                
                DispatchQueue.main.async {
                /* All actions which change the UI have to be put back on the main queue. */
                    label.text = String(amount)
                }
                // sets a timeout
                usleep(useconds_t(timeout*10000))
            }
        }
    }
}

错误处理

[编辑 | 编辑源代码]

在代码执行过程中,有很多情况下可能会发生错误。例如,当尝试从硬盘读取文件时,由于权限不足或文件不存在,可能会发生错误。Swift 提供了几种[2]处理代码执行期间错误的方法。

  • 函数中的错误可以传播到调用该函数的代码。
  • do-catch 语句
  • 可选值
  • 断言错误不会发生

使用抛出函数传播错误

[编辑 | 编辑源代码]

关键字 throws 用于函数声明中参数列表之后。此函数被称为“抛出函数”。在下面的示例中,使用枚举定义了两种可能的错误类型。每当调用 makeCoffee() 函数时,机器中的咖啡豆数量就会减少,并且计数器会增加。一旦咖啡豆用完,就会抛出 outOfBeans 错误。如果已经服务了特定数量的杯子,则会抛出 needsMaintenance 错误。

enum CoffeeMachineError: Error {
    case outOfBeans
    case needsMaintenance
}

class CoffeeMachine{
    var beans = 20
    var count = 1
    
    func makeCoffee() throws{
        if(count < 6){
            
            if(beans > 0){
                print("Enjoy your cup of coffee!")
                beans -= 1
                count += 1
            } else{
                throw CoffeeMachineError.outOfBeans
            }
        } else{
            throw CoffeeMachineError.needsMaintenance
        }
    }
}
var machine = CoffeeMachine()
for _ in stride(from: 0, to: 7, by: 1){
    try machine.makeCoffee()
}

Do-Catch 语句用于根据抛出的错误类型执行不同的语句。例如,这里捕获了 needsMaintenance 错误,机器会提示需要维护,并将计数器重置为 0。

var coffeeMachine = CoffeeMachine()
for run in stride(from: 0, to: 25, by: 1){
    do{
        try coffeeMachine.makeCoffee()
    } catch CoffeeMachineError.outOfBeans{
        print("Out of Beans!")
		
    } catch CoffeeMachineError.needsMaintenance{
        print("Machine needs Maintenance!")
        // Machine is maintained, counter gets set back to 0
        coffeeMachine.count = 0
    }
}

将错误转换为可选值

[编辑 | 编辑源代码]

使用 try? 关键字,错误会被转换为可选值。每当在执行过程中抛出错误时,表达式的值为 nil。在下面的代码片段中,一旦要返回的数字不再大于零,就会抛出错误。

enum DigitError: Error{
    case outOfDigitsError(String)
}

var currentDigit = 9
func  getDigit() throws -> Int {
    if(currentDigit > 0){
        let tmp = currentDigit
        currentDigit -= 1
        return tmp
    }
    else{
        throw DigitError.outOfDigitsError("Sorry, no digits left...")
    }
}

for _ in stride(from: 0, to: 10, by: 1){
    if let digit = try? getDigit(){
        print(digit)
    }
}


访问传感器数据

[编辑 | 编辑源代码]

Apple 的移动设备包含许多传感器,包括加速计、气压计、环境光传感器和计步器。iOS 开发人员可以使用 Swift 在他们的项目中访问这些传感器数据。

计步器

[编辑 | 编辑源代码]

例如,下面的代码片段显示了如何访问计步器以及如何检索数据[3]。例如,此传感器用于统计人的步数。此项目可以从 GitHub 下载。

import UIKit
import CoreMotion

@IBDesignable
class ViewController: UIViewController {
   
    
    @IBOutlet weak var stepsDisplay: UILabel!
    @IBOutlet weak var stepsToComplete: UILabel!
    
    let calendar = Calendar.current
    let todayDate = Date()
    var stepsToday: Int = 0
    let pedometer = CMPedometer()
    
    @IBInspectable
    var targetSteps = 10000
    
    func getStartOfDay(from date: Date) -> Date{
        return Calendar.current.startOfDay(for: date)
    }
    
    func handler (_ data: CMPedometerData?, _ error: Error?) -> Void{
        let steps = data?.numberOfSteps
        stepsToday = steps as! Int
        DispatchQueue.main.async(execute: {
            // puts the closure into the Main Queue so it will be executed immediatly
            self.stepsDisplay.text = String(self.stepsToday)
            self.stepsToComplete.text = String(self.getStepsToGoal(target: self.targetSteps, actual: self.stepsToday))
        })   
    }
    
    func getStepsToGoal(target steps: Int, actual count: Int) -> Int{
        return steps - count
    }
    
    @IBAction func getSteps(_ sender: UIButton){
        if CMMotionActivityManager.isActivityAvailable(){
            //checks if Activity Data is available
            pedometer.queryPedometerData(from: getStartOfDay(from: todayDate), to: todayDate, withHandler: handler)
            //queries data from the pedometer.
        }
    }   
}


单元测试

[编辑 | 编辑源代码]

在本节中,我们将了解如何在 Swift 中实现简单的单元测试[4]。为此,我们将测试一个包含三个函数的简单类。该类包含两个整数值的变量以及三个可以对这些值进行加、减或乘运算的函数。

import Foundation

class Calculator {
    
    var a: Int
    var b: Int
    
    init(a:Int, b:Int){
        self.a = a
        self.b = b
     }
    
    func add(a:Int, b:Int) -> Int {
        return a + b
    }
    
    func sub(a:Int, b:Int) -> Int {
        return a - b
    }
    
    func mul(a:Int, b:Int) -> Int {
        return a * b
    }
}

接下来,让我们看看如何测试这个类。首先,我们必须导入 XCTest 测试框架和 Calculator。在测试函数 testAdd()testSub()testMul() 中,使用 Calculator 类的实例来调用 add、subtract 和 multiply 方法,并将结果与预期值进行比较。

import XCTest
@testable import Calculator

class CalculatorTests: XCTestCase {
    
    let calc = Calculator(a:0, b:0)
    
    override func setUp() {
        // called before every test method
        super.setUp()
    }
    
    override func tearDown() {
        // called at the end of every test method
        super.tearDown()
    }
    
    func testAdd() {
        XCTAssertEqual(calc.add(a: 1, b: 1), 2)
        XCTAssertEqual(calc.add(a: 1, b: 2), 3)
        XCTAssertEqual(calc.add(a: 5, b: 4), 9)
    }
    
    func testSub(){
        XCTAssertEqual(calc.sub(a: 5, b: 2), 3)
        XCTAssertEqual(calc.sub(a: 3, b: 3), 0)
        XCTAssertEqual(calc.sub(a: 6, b: 7), -1)
    }
    
    func testMul(){
        XCTAssertEqual(calc.mul(a: 2, b: 4), 8)
        XCTAssertEqual(calc.mul(a: 9, b: 9), 81)
        XCTAssertEqual(calc.mul(a: 0, b: 4), 0)
    }
}

参考文献

[编辑 | 编辑源代码]
  1. Kodeco | 2017 | Grand Central Dispatch 教程 | [在线][访问日期:2017 年 9 月 18 日] | https://www.kodeco.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2
  2. Apple Inc. | 2017 | 错误处理 | [在线][访问日期:2017 年 9 月 18 日] | https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html#//apple_ref/doc/uid/TP40014097-CH42-ID508
  3. Apple Inc. | 2017 | [在线][访问日期:2017 年 9 月 18 日] | https://developer.apple.com/documentation/coremotion/cmpedometer
  4. Nolan, G. (2017), Agile Swift: 使用敏捷工具和技术的 Swift 编程,Springer Science+Business Media New York,纽约
华夏公益教科书