Swift 教程三: Tuples Protocols Delegates以及Table Views

Tags: swift ios

第一篇Swift教程中,我们学习了Swift语言的一些基本知识,并且创建了自己的小费计算器类。

第二篇Swift教程中,我们为小费计算器类创建了用户界面
在这篇Swift教程中,我们将介绍Swift的新数据类型:元组(Tuples)。我们还将介绍Swift的协议(Protocols)、委托(Delegates)和表格视图(Table views),并且将介绍如何在Playgrounds中创建用户界面原型。
完成这篇教程需要先完成第二篇Swift教程,如果你并未完成第二篇教程的话,请先下载示例工程,这可以节省你的时间。

入门

到目前为止,你的小费计算器根据每个小费百分比给出了一个建议的小费额。然而,当你支付小费时,你需要在脑中将小费与消费金额相加 - 这和我们制作消费计算器的目的有点背离!

如果calcTipWithTipPct()方法可以返回两个值:小费金额,消费+小费的总额,那不是更好吗?
在Objective-C中,如果你想使一个方法返回两个值,你要么做一个有两个属性的Objective-C的对象来作为返回值,要么你必须返回一个包含两个值的字典。在Swift中还有另一种方法:元组(Tuples)。

那么让我们开始使用元组(Tuples)吧,感受一下它们是如何工作的。在Xcode中创建一个新的Playground(或者,如果你按照我在第一篇Swift教程中的建议,只需点击您保存到您的Dock上的Playground即可)。删除Playground中的所有代码,好让我们可以从头开始编写。

未命名元组(Tuples)

让我们从未命名元组开始,在playground中输入下面的代码:

let tipAndTotal = (4.00, 25.19)

上面的代码将两个Double值(小费和消费金额)保存到一个元组中,上面的代码使用类型推断语法,因此编译器会根据初始值推断元组中变量的类型。当然,你也可以明确指定元组中每个元素的类型,参考下面代码:

let tipAndTotal:(Double, Double) = (4.00, 25.19)

可以通过索引和名称来访问元组中的值,下面的例子是使用索引访问元组值的代码:

tipAndTotal.0
tipAndTotal.1

这样你可以在playground边栏上看到4.00和25.19两个数值,通过索引访问元组相对较少,因为这种方式不如通过名称获得元组属性更简洁,添加下面代码到playground中:

let (theTipAmt, theTotal) = tipAndTotal
theTipAmt
theTotal

上面的代码创建两个常量用来保存元组中的每个值。

命名的元组

未命名元组可以工作的很好,但是正如你看到的,我们需要额外的代码才能通过名字访问每个元素。

更方便的方式是使用命名元组:在定义元组的时候为每个元素命名(有点像字典),添加以下代码到playground中:

let tipAndTotalNamed = (tipAmt:4.00, total:25.19)
tipAndTotalNamed.tipAmt
tipAndTotalNamed.total

看这样更简洁,在本文后续内容中我们都将使用这种元组。

最后,我还是使用类型推断语法来定义tipAndTotalNamed元组,如果你想明确指定类型,可以参考以下代码:

let tipAndTotalNamed:(tipAmt:Double, total:Double) = (4.00, 25.19)

请注意,当使用显式句法,在右手侧命名的变量是可选的。

返回元组

现在我们已经知道元组的基本用法了,那么让我们看看我们如何在我们的小费计算器中使用元组返回两个值。添加以下代码到playground中:

let total = 21.19
let taxPct = 0.06
let subtotal = total / (taxPct + 1)
func calcTipWithTipPct(tipPct:Double) -> (tipAmt:Double, total:Double) {
  let tipAmt = subtotal * tipPct
  let finalTotal = total + tipAmt
  return (tipAmt, finalTotal)
}
calcTipWithTipPct(0.20)

这个方法和之前编写的calcTipWithTipPct方法基本一样,除了返回值部分由之前的Double类型修改为现在的(tipAmt:Double, total:Double)。

这里是到目前为止我们的playground文件。现在删除playground文件中所有内容继续后续内容。

完整的原型

到目前为止,我们已经准备好将学到的内容集成到TipCalculatorModel类了。但是在修改TipCalculator XCode项目前,我们可以先在playground中进行修改。

从TipCalculator项目中拷贝TipCalculatorModel类到playground中,按照之前内容修改calcTipWithTipPct类。最后,将returnPossibleTips的返回字字典的值由Double改为Tuples。下面是完整的代码:

import Foundation
 
class TipCalculatorModel {
 
  var total: Double
  var taxPct: Double
  var subtotal: Double {
    get {
      return total / (taxPct + 1)
    }
  }
 
  init(total:Double, taxPct:Double) {
    self.total = total
    self.taxPct = taxPct
  }
 
  func calcTipWithTipPct(tipPct:Double) -> (tipAmt:Double, total:Double) {
    let tipAmt = subtotal * tipPct
    let finalTotal = total + tipAmt
    return (tipAmt, finalTotal)
  }
 
  func returnPossibleTips() -> [Int: (tipAmt:Double, total:Double)] {
 
    let possibleTipsInferred = [0.15, 0.18, 0.20]
    let possibleTipsExplicit:[Double] = [0.15, 0.18, 0.20]
 
    var retval = Dictionary<Int, (tipAmt:Double, total:Double)>()
    for possibleTip in possibleTipsInferred {
      let intPct = Int(possibleTip*100)
      retval[intPct] = calcTipWithTipPct(possibleTip)
    }
    return retval
 
  }
 
}
 
let tipCalc = TipCalculatorModel(total: 21.19, taxPct: 0.06)
tipCalc.returnPossibleTips()

协议(Protocols)

下一步是为应用程序制作表格视图的原型,但是在这之前我们先需要了解什么是协议(Protocols)什么是委托(Delegates),我们先从协议开始(Protocols)。

协议(Protocols)是通过一组方法来定义“契约(contract)”或者“接口(interface)”,增加下面代码到playground中:

protocol Speaker {
  func Speak()
}

这个协议只定义了Speak()方法,任何遵守或者符合这个协议的类必须实现Speak()方法,参考下面代码:

class Vicki: Speaker {
  func Speak() {
    println("Hello, I am Vicki!")
  }
}
 
class Ray: Speaker {
  func Speak() {
    println("Yo, I am Ray!")
  }
}

为了定义实现协议的类,你需要在类名后增加冒号,再列出需要实现协议(在类需要继承的类类名后,如果有的话)。上面的类未集成其他任何类,所以可以直接列出协议的名称。如果类实现中没有实现Speak()方法,会得到一个编译器错误。

下面代码是一个实现Speaker的类,同时继承其他类的示例:

class Animal {
}
class Dog : Animal, Speaker {
  func Speak() {
    println("Woof!")
  }
}

上面的Dog类同时继承与Animal类和实现Speaker协议,因此,在Dog类的类名后需要增加冒号(:),然后写继承的类名,然后是实现的协议。在Swift中一个类只能有一个父类,但可以实现多个协议(有点像java,只能有一个父类,但可以有多个接口)。

可选的协议

可以标注协议中的方法为可选的,要做到这一点需要按照下面代码修改Speaker协议:

@objc protocol Speaker {
  func Speak()
  optional func TellJoke()
}

如果定义协议中的方法为可选的,需要首先为协议增加@objc注解(即使你的类不需要与objective-c交互),然后为所有可选的方法增加optional注解。

下面的例子中,Vicki和Ray可以说笑话,但可悲的是不是一只狗,所以只需要在这两个类中实现TellJoke方法。

class Vicki: Speaker {
  func Speak() {
    println("Hello, I am Vicki!")
  }
  func TellJoke() {
    println("Q: What did Sushi A say to Sushi B?")
  }
}
 
class Ray: Speaker {
  func Speak() {
    println("Yo, I am Ray!")
  }
  func TellJoke() {
    println("Q: Whats the object-oriented way to become wealthy?")
  }
  func WriteTutorial() {
    println("I'm on it!")
  }
}

使用协议

我们已经定义了一个协议和几个实现协议的类,现在我们来看看如何使用协议,将下面的代码添加到playground中:

var speaker:Speaker
speaker = Ray()
speaker.Speak()
// speaker.WriteTutorial() // error!
(speaker as Ray).WriteTutorial()
speaker = Vicki()
speaker.Speak()

上面代码中,定义了speader的类型为Speaker,因此只能调用Speaker协议中定义的方法(Speak),调用WriteTutorial方法会报错,即使speaker是指向Ray的一个实例。

如果想要调用WriteTutorial方法,需要将speaker强制类型转换为Ray。当然,speaker转换为Vicki类也一样,因为Vicki也实现了Speaker协议。

增加下面代码到playground中:

speaker.TellJoke?()
speaker = Dog()
speaker.TellJoke?()

由于TellJoke是协议中可选方法,因此在调用前需要先检查这个方法是否被实现。我们使用可选链(optional chaining)方法来检查协议中可选方法是否被实现,在调用方法的名字后括号前增加?,这样调用前会检查方法是否已被实现,如果方法未被实现那么会返回nil。

可选链(optional chaining)是检查可选方法是否实现的有效技术,另一个方式是使用if let (optional binding) ,之前的章节已经介绍过。

委托

委托是符合协议的简单变量,变量指向的类通常使用于事件通知或执行各项子任务。要理解委托,添加新的DateSimulator类到playground中,允许两个符合Speaker协议的类进行一次约会:

class DateSimulator {
 
  let a:Speaker
  let b:Speaker
 
  init(a:Speaker, b:Speaker) {
    self.a = a
    self.b = b
  }
 
  func simulate() {
    println("Off to dinner...")
    a.Speak()
    b.Speak()
    println("Walking back home...")
    a.TellJoke?()
    b.TellJoke?()
  }
}
 
let sim = DateSimulator(a:Vicki(), b:Ray())
sim.simulate()

上面代码假设你想在约会开始或结束是通知其他的类。同样,在应用可用或者不可用时如果希望进行提示委托是非常有用的,要做到这个,首先创建一个协议并定义需要通知的事件(在DateSimulator前面创建):

protocol DateSimulatorDelegate {
  func dateSimulatorDidStart(sim:DateSimulator, a:Speaker, b:Speaker)
  func dateSimulatorDidEnd(sim:DateSimulator, a: Speaker, b:Speaker)
}

创建协议的实现类(在 DateSimulatorDelegate后面)

class LoggingDateSimulator:DateSimulatorDelegate {
  func dateSimulatorDidStart(sim:DateSimulator, a:Speaker, b:Speaker) {
    println("Date started!")
  }
  func dateSimulatorDidEnd(sim:DateSimulator, a: Speaker, b: Speaker)  {
    println("Date ended!")
  }
}

很简单的方法,仅仅在控制台中输出监控到的事件。

在DateSimulator中增加一个新属性,属性的类型是刚才的DateSimulatorDelegate。

var delegate:DateSimulatorDelegate?

上面是一个委托的实现例子。委托,仅仅是一些实现了特定协议的类,用来接受通知事件或者根据行为执行一些特定的任务。

注意上面代码中delegate定义为可选的,因此无论delegate是否被赋值DateSimulator都会正常工作。

在 sim.simulate()前面添加以下代码,设置delegate指向LoggingDateSimulator:

sim.delegate = LoggingDateSimulator()

最后,修改 simulate()方法,在合适的地方调用委托,下面是完整的代码:

class DateSimulator {
 
  let a:Speaker
  let b:Speaker
  var delegate:DateSimulatorDelegate?
 
  init(a:Speaker, b:Speaker) {
    self.a = a
    self.b = b
  }
 
  func simulate() {
    delegate?.dateSimulatorDidStart(self, a:a, b: b)
    println("Off to dinner...")
    a.Speak()
    b.Speak()
    println("Walking back home...")
    a.TellJoke?()
    b.TellJoke?()
    delegate?.dateSimulatorDidEnd(self, a:a, b:b)
  }
}

表格视图(Table Views), 委托(Delegates)和数据源(Data Sources)


我们已经了解了协议和委托的概念,现在就可以在应用程序总使用表格视图了。

表格视图有一个属性叫做委托-你可以将其设置为实现的UITableViewDelegate的类,这是有一堆可选方法的协议,用来处理当行被选中,或者当表格视图进入编辑模式等类似事件。

表格视图还有另一个属性dataSource - 可以将其设置为符合UITableViewDataSource类。不同的是这个类不接受事件通知,表格视图通过它获取数据 - 比如显示多少行数据、每行显示什么数据。委托是可选的,但数据源是必需的。所以让我们先为小费计算器创建一个数据源。

playground非常酷的功能是,你可以创建和实时预览原型,这是一个伟大的方式,可以在把功能集成到项目前确保功能是OK的。

再次,检查playground中的TipCalculatorModel类是最新的,然后将下面代码添加到文件底部:

// 1
import UIKit
// 2
class TestDataSource : NSObject {
 
  // 3
  let tipCalc = TipCalculatorModel(total: 33.25, taxPct: 0.06)
  var possibleTips = Dictionary<Int, (tipAmt:Double, total:Double)>()
  var sortedKeys:[Int] = []
 
  // 4
  override init() {
    possibleTips = tipCalc.returnPossibleTips()
    sortedKeys = sorted(Array(possibleTips.keys))
    super.init()
  }
 
}

我们一段段的来讲解这段代码:

  1. 为了可以使用UIKit相关的类(如:UITableView),需要首先引入UIKit框架,如果这行报错的话,打开File Inspector (View\Utilities\Show File Inspector)并设置平台为IOS。

  2. 对实现UITableViewDataSource的了的要求是,类继承需要继承于NSObject(直接或间接都可以)

  3. 在这里初始化小费计算器,创建空数组保存可能的消费和排序键。需要注意的是需要定义possibleTips又如sortedKeys作为变量(不是常量),因为在实际的应用程序中会改变他们的值。

  4. 初始化方法,设置了两个变量的初始值。请注意,您需要用override标记这个方法,因为你是覆盖了NSObject的init方法。

现在,已经有了一个基础代码,让我们修改类来实现UITableViewDataSource。要做到这一点,需要在类声明的结尾添加数据源协议:

class TestDataSource: NSObject, UITableViewDataSource {

并增加两个新方法,下面是第一个:

func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
  return sortedKeys.count
}

上面是UITableViewDataSource的两个需要实现的方法之一。定义表视图的每个区块(Section)显示多少行数据。这个表格视图将只有1个区块(Section),所以这里返回sortedKeys数组的长度即可(即可能小费比例的个数)。

下面是另一个方法:

// 1
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
  // 2
  let cell = UITableViewCell(style: UITableViewCellStyle.Value2, reuseIdentifier: nil)
 
  // 3
  let tipPct = sortedKeys[indexPath.row]
  // 4
  let tipAmt = possibleTips[tipPct]!.tipAmt
  let total = possibleTips[tipPct]!.total
 
  // 5
  cell.textLabel.text = "\(tipPct)%:"
  cell.detailTextLabel.text = String(format:"Tip: $%0.2f, Total: $%0.2f", tipAmt, total)
  return cell
}

还是一段段说明:

  1. 这个方法会在表格视图的每一行上调用。你需要返回表示此行的视图(UITableViewCell的子类)。

  2. 使用内置样式创建UITableViewCell,或者通过自定义子类来创建自己的风格。这里使用UITableViewCellStyle.Value2来创建一个默认的样式表格视图单元格。

  3. 这个方法的一个参数是indexPath,这是表示当前表格视图的区块(Section)和行号的简单集合。由于只有一个区块(Section),所以使用行号从sortedKeys中取得合适的小费百分比

  4. 接下来,为表示小费比例的元组中每个元素创建变量。需要注意的是,当你访问一个字典中的元素是你会得到一个可选的值,因为对应的键在字典中可能没有值。但是,在这里我们确定对应的键在字典中肯定有值,因此使用!强制展开。

  5. 最后,内置的UITableViewCell有两个属性textLable和detailTextLabel对应显示标签,这几行代码就是设置对应属性用的。

收尾

打开TipCalculator项目,拷贝最新的TipCalculatorModel类覆盖已有的那个。

然后,打开 Main.storyboard选择Text View(结果区域)并删除它,从对象库(Object Library)拖动Table View(不是TableViewController)到视图控制器上,并设定以下属性: X=0, Y=187, Width=580, Height=293。

点击Interface Builder底部右侧的第四个按钮自动布局界面,然后点击Clear Constraints和Add Missing Constraints to View Controller。

最后,选择Table View并选择第6个检查器(Connections Inspector),你会看到的数据源和委托两个条目 - 拖动右边的按钮到文档大纲(Document Outline)中的视图控制器上。现在你的视图控制器设置了相关数据源和委托(与之前用编程方法设置的效果是一样的)。

最后,确保你的助理编辑器( Assistant Editor)已打开并显示ViewController.swift。拖动TableView到最下面的outlet下方。一个弹出会出现 - 输入tableView的名称,然后单击连接。

现在开始修改代码。打开ViewController.swift然后标记类标实现UITableViewDataSource:

class ViewController: UIKit.UIViewController, UITableViewDataSource {

在tipCalc下方增加两个新变量:

var possibleTips = Dictionary<Int, (tipAmt:Double, total:Double)>()
var sortedKeys:[Int] = []

用下面代码修改 calculateTapped() 方法:

@IBAction func calculateTapped(sender : AnyObject) {
  tipCalc.total = Double((totalTextField.text as NSString).doubleValue)
  possibleTips = tipCalc.returnPossibleTips()
  sortedKeys = sorted(Array(possibleTips.keys))
  tableView.reloadData()
}

这段代码重新加载possibleTips、sortedKeys和触发器以及表格视图数据。

删除,设置refreshUI()中设置resultsTextView的那行代码。

从playground中拷贝两个TableView方法到类中,并且删除无用的注释。下面是完整的代码

import UIKit
 
class ViewController: UIKit.UIViewController, UITableViewDataSource {
 
  @IBOutlet var totalTextField : UITextField!
  @IBOutlet var taxPctSlider : UISlider!
  @IBOutlet var taxPctLabel : UILabel!
  @IBOutlet var resultsTextView : UITextView!
  @IBOutlet var tableView: UITableView!
 
  let tipCalc = TipCalculatorModel(total: 33.25, taxPct: 0.06)
  var possibleTips = Dictionary<Int, (tipAmt:Double, total:Double)>()
  var sortedKeys:[Int] = []
 
  func refreshUI() {
    totalTextField.text = String(format: "%0.2f", tipCalc.total)
    taxPctSlider.value = Float(tipCalc.taxPct) * 100.0
    taxPctLabel.text = "Tax Percentage (\(Int(taxPctSlider.value))%)"
  }
 
  override func viewDidLoad() {
    super.viewDidLoad()
    refreshUI()
  }
 
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }
 
  @IBAction func calculateTapped(sender : AnyObject) {
    tipCalc.total = Double((totalTextField.text as NSString).doubleValue)
    possibleTips = tipCalc.returnPossibleTips()
    sortedKeys = sorted(Array(possibleTips.keys))
    tableView.reloadData()
  }
 
  @IBAction func taxPercentageChanged(sender : AnyObject) {
    tipCalc.taxPct = Double(taxPctSlider.value) / 100.0
    refreshUI()
  }
  @IBAction func viewTapped(sender : AnyObject) {
    totalTextField.resignFirstResponder()
  }
 
  func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
    return sortedKeys.count
  }
 
  func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
    let cell = UITableViewCell(style: UITableViewCellStyle.Value2, reuseIdentifier: nil)
 
    let tipPct = sortedKeys[indexPath.row]
    let tipAmt = possibleTips[tipPct]!.tipAmt
    let total = possibleTips[tipPct]!.total
 
    cell.textLabel.text = "\(tipPct)%:"
    cell.detailTextLabel.text = String(format:"Tip: $%0.2f, Total: $%0.2f", tipAmt, total)
    return cell
  }
 
}

构建、运行,然后开始使用你的新小费计算器了。


版权归属于英文原文:Swift Tutorial Part 3: Tuples, Protocols, Delegates, and Table Views

翻译:www.4byte.cn



本文链接:http://www.4byte.cn/learning/119999/swift-jiao-cheng-san-tuples-protocols-delegates-yi-ji-table-views.html