跳轉至內容

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,紐約
華夏公益教科書