Туториалы
06 сентября 2015
Туториалы
06 сентября 2015
Swift Часть 3: Кортежи, протоколы, делегаты и табличный формат

Этот туториал начинается с того места, где был закончен второй . Но, если у вас нет проекта, пожалуйста, скачайте его здесь.

Поехали!

Сейчас, ваш калькулятор предлагает посчитанные чаевые, для каждого учтенного с них процента. Однако, как только вы выберете необходимое количество, которое хотите оставить, то вам будет необходимо посчитать общий счет, с учетом этих чаевых. Вот тут-то и вскипает голова!

Было бы неплохо, если бы ваш метод calcTipWithTipPct() возвращал два значения: количество чаевых и общий счет с чаевыми.

В Objective-C, если вы хотите вернуть два значения метода, вы должны либо создать объект с двумя свойствами для возвращаемых значений, либо возвращаться в словарь, содержащий два значения. В Swift есть альтернативный вариант - кортежи (еще можно услышать тюплы или таплы, а по английски tuples).

Чтобы вы поняли как работают кортежи, давайте немного с ними поиграем в playground'е. Создайте новый playground (или, если вы руководствуетесь советом из первой части, просто кликните по playground'у, который у вас в быстром запуске - Dock). Удалим все из playground'а и начнем с чистого листа.

Безымянные кортежи

Давайте создадим что-то, что называется «безымянным кортежем». Впишите следующее:


let tipAndTotal = (4.00, 25.19)

Тут, вы группируете два значения типа Double (tip и total) в одном значении кортежа. Здесь работает вывод типа, так как компилятор может вывести тип по значениям, которые вы присвоили. Альтернативно вы можете явно указать типы:


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

Для доступа к определенному элементу кортежа вы можете использовать индекс элемента или его имя. Добавим следующий код:


tipAndTotal.0
tipAndTotal.1

Вы должны увидеть 4.0 и 25.19 в сайдбаре вашей площадки. Используйте доступ по индексу только в крайнем случае, так как более читабельным вариантом является доступ по имени. Добавьте следующий код для проверки:


let (theTipAmt, theTotal) = tipAndTotal
theTipAmt
theTotal

Такой синтаксис позволяет создать новую константу, которая ссылается на каждый элемент кортежа по имени.

Названные кортежи

Безымянные кортежи работают нормально, но требуют дополнительного кода для доступа к их значениям.

Чаще всего удобнее использовать «названные кортежи», где вы даете имена вашим элементам кортежа во время объявления. Для теста давайте напишем еще несколько строк кода:


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 с которым вы работали, за исключением того, что сейчас он возвращает кортеж (tipAmt:Double, total:Double), вместо Double.

Можете скачать файл площадки, включащий весь код до этого момента. Теперь удалите все, что вы сделали в вашей площадке и начнем с чистого листа.

Полный прототип

Теперь, вы готовы использовать все выученное, в своем классе TipCalculatorModel.

До того, как вы начнете изменять свой проект Xcode TipCalculator, давайте определим изменения на площадке!

Скопируйте класс TipCalculatorModel из вашего проекта TipCalculator на площадку. Затем, измените calcTipWithTipPct так, как мы говорили ранее. Наконец, измените returnPossibleTips для того, чтобы возвращался словарь-ключ - Int. Значение стало кортежем, а не ключом Int, как было раньше, также появился Double.

Если вы хотите проверить свои силы, то самое время! Но если у вас что-то не получилось, то сравните с кодом ниже:


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()
    for possibleTip in possibleTipsInferred {
      let intPct = Int(possibleTip*100)
      retval[intPct] = calcTipWithTipPct(possibleTip)
    }
    return retval
 
  }
 
}

Заметка

Мне пришлось переключиться с короткой записи формата словаря ([x: y]()) на длинную (Dictionary()) потому, что компилятор не поддерживает кортеж в кортежах, в короткой форме.

Файл площадки на этот момент.

Теперь, давайте сохраним эту площадку и откроем новую. Но мы вернемся обратно чуточку позже.

Следующим шагом, является создание прототипа таблицы для нашего приложения. Но до того, как мы приступим, нам предстоит разобраться еще с двумя понятиями: протоколы и делегаты. Начнем с протоколов:

Протокол - это список методов, которые определяют "контракт" или "интерфейс". Давайте добавим следующие строки в код для наглядности:


protocol Speaker {
  func Speak()
}

Этот протокол объявляет единственный метод Speak.

Любой класс, который соответствует этому протоколу, должен имплементировать этот метод. Давайте попробуем это сделать, добавив два класса, которые соответствуют протоколу Speaker:


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

Для того, чтобы класс соответствовал протоколу, вы должны поставить двоеточие после его имени, далее, перечислить все протоколы (после имени класса - все то, от чего наследует класс, если таковое есть). Такие классы не наследуют от других классов, так что просто можете перечислить протоколы напрямую.

И кстати, если вы не включите функцию Speak, то вы получите ошибку компиляции.

Теперь, давайте попробуем то же самое, только добавим еще наследование от другого класса:


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

В этом примере, Dog наследует от Animal, так что когда вы объявляете Dog, вы должны поставить двоеточие, после чего идет имя класса от которого наследуют, а затем список протоколов. Вы можете наследовать только от одного класса в Swift, но вы можете соответствовать любому числу протоколов.

Опциональные протоколы

Вы можете обозначить метод в протоколе как опциональный. Чтобы попробовать, замените протокол Speaker следующим кодом:


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

Если у вас произошла ошибка, то добавьте следующую строку на самый верх вашей площадки:


import Foundation

Если вы хотите создать протокол с опциональными методами, то вам нужно создать его с префиксом @objc (даже, если ваш класс никак не связан с Objective-C). После этого вы можете ставить префикс optional у любого метода, который должен быть опциональным.

Обратите внимание, что сейчас нет ошибки компилятора, для вашего класса Person или класса Dog.

В этом примере, Ray и Vicki расскажут шутку. Так что имлементируем этот метод только для двух классов:


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

Обратите внимание, что когда вы имплементируете протокол, то при необходимости, ваш класс может иметь больше одного метода, а не только тот, что указан в протоколе. Здесь класс Ray имеет один дополнительный метод.

Кстати, а вы не догадались случайно до ответов на эти шуточные вопросы?

Q: What did Sushi A say to Sushi B? A: Wasabi!

Q: Whats the object-oriented way to become wealthy? A: Inheritence!

Использование протоколов

На данный момент, вы уже создали протокол и несколько классов, которые их используют, давайте теперь попробуем использовать их. Добавьте следующий код на площадку:


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

Смотрите, вместо того, чтобы объявлять speaker как Ray, вы объявляете его как Speaker. Это значит, вы можете только вызывать методы для speaker, который существует в протоколе Speaker, так что вызов WriteTutorial вызовет ошибку, даже не смотря на то, что speaker - это тип Ray. Вы можете вызвать WriteTutorial, если вы временно отправите speaker обратно к Ray, как вы можете наблюдать тут.

Также обратите внимание, что вы можете присвоить speaker Vicki, так как Vicki, так же подчиняется Speaker.

Теперь, добавьте эти строки для эксперимента с опциональным методом:


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

Помните, что метод TellJoke - опциональный метод, и перед тем как его вызвать, нужно проверить его существование.

Эти строки используют технику последовательности опционала. Если вы поставите вопросительный знак ? после имени метода, то прежде чем вызвать метод, будет проводиться проверка на его существование. Если он не существует, то он будет вести себя так, как метод возвращающий nil.

Последовательность опционала - полезная техника, когда вы хотите проверить существование значения перед тем, как его использовать. Является альтернативой опциональной связке (if let). Именно эту технику мы и будем использовать до конца данного туториала и во всех остальных.

Делегаты

Делегат - переменная, которая подчиняется протоколу, который в свою очередь используется классом, для оповещения событий или выполнения различных подзадач. Лучше всего представить, что босс дает миньону задачу сообщать ему о каких-либо событиях, или просто просит выполнить его что-либо.

Чтобы наглядно понять, что я имею в виду, добавьте новый класс DateSimulator на вашу площадку. Он позволяет вашим двум классам, которые соответствуют 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, которое принимает класс, который подчиняется этому протоколу:


var delegate:DateSimulatorDelegate?

Вот это и есть пример делегата. Еще раз, делегат - это некоторый класс, который реализует протокол, позволяющий оповещать о каких-либо событиях или выполнять какие-то конкретные указания.

Вы наверное обратили внимание на то, что мы сделали опционал. Таким образом, DateSimulator должен работать нормально и при созданном делегате, и при отсутствии делегата.

Прямо перед строкой sim.simulate() установите переменную вашему 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)
  }
}

Файл playground с самого начала и до этого момента.

Теперь, сохраните свой файл и давайте вернемся к площадке, которую мы сохранили раньше для того, чтобы модернизировать наш класс TipCalculatorModel. Самое время вписать его в таблицу!

Таблицы, делегаты и источники данных

Сейчас, вы понимаете концепции протоколов и делегатов, и готовы к использованию таблиц (table views) вашего приложения.

Так уж получилось, что у таблиц (table views) есть свойство delegate, и вы можете установить его для класса, который подчиняется UITableViewDelegates. Это протокол с кучей опциональных методов. К примеру, есть метод, который определяет какой ряд выбран в таблице или, когда запущено ее редактирование.

Так же, таблицы имеют дополнительное свойство dataSource, которое вы так же можете установить для класса, который подчиняется UITableViewDataSource. Разница в том, что это свойство не уведомляет класс, а запрашивает данные. Например, сколько рядов в таблице.

Свойство delegate опционально, но dataSource обязательно. Что ж, начнем с того, что зададим источник данных для вашего калькулятора чаевых.

Одна из крутых штук игровых площадок в том, что вы можете прототипизировать(это слово-опционал 8]) и визуализировать элементы (к примеру UITableView). Это удобный способ посмотреть на работу элемента до того, как интегрировать его в проект.

Вновь убедитесь, что вы на площадке, с обновленным и улучшенным классом TipCalculatorModel. Добавьте этот код в самый низ файла:


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

Давайте все последовательно разберем.

  • Для использования таких классов UIKit как UITableView, сначала вам нужно импортировать фреймворк UIKit. Если у вас появляется ошибка, то идите в File Inspector (View\Utilities\Show File Inspector) и установите Platform на iOS.
  • Одно из требований реализации UITableViewDataSource в том, что ваш класс расширяет NSObject (непосредственно, либо через промежуточные классы).
  • Здесь инициализируем калькулятор и создаем пустой словарь возможных чаевых, а также массив отсортированных ключей. Обратите внимание, что possibleTips и sortedKeys - переменные, а не константы, потому что пользователю необходимо, чтобы значения менялись.
  • Метод init - присваиваете двум переменным исходные значения. Обратите внимание, что вы должны обозначить метод как override, так как, вы переписываете метод класса NSObject.

У вас есть база, что ж давайте подчиним наш класс протоколу UITableViewDataSource. Чтобы это сделать, мы должны прописать имя-протокол в объявлении класса:


class TestDataSource: NSObject, UITableViewDataSource {

Затем, добавьте вот этот новый метод:

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

Это один из двух необходимых методов, которые мы должны реализовать для подчинения протоколу UITableViewDataSource. Он запрашивает у вас количество рядов в каждой секции этой таблицы. Эта таблица, будет иметь всего одну секцию, так что вы возвращаете количество значений в 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
}

Идем как обычно, секция за секцией:

  • Этот метод вызывается для каждой строки-таблицы. Вы должны вернуть view (элемент), который является рядом таблицы, который в свою очередь, является подклассом UITableViewCell.
  • Вы можете создать ячейки таблицы со встроенным стилем или создать свой собственный подкласс своего стиля. Если быть конкретным, здесь мы создаем ячейки со стандартным стилем UITableViewCellStyle.Value2. Мы можем сократить эту запись до .Value2, благодаря выводу типов, но мы оставим длинный вариант для большей очевидности.
  • Один из параметров этого метода indexPath, который является простой коллекцией рядов и секций для этой ячейки. Так как у нас всего одна секция, то вы используете ряд для вытаскивания подходящего процента чаевых для отображения из массива sortedKeys.
  • Далее, вы хотите создать переменную для каждого элемента в кортеже, для процентов чаевых. Вспомните из предыдущего урока что, когда вы получите доступ к элементу в словаре, у вас появится опциональное значение, так как, вполне возможно, что нет ничего в словаре для данного конкретного ключа. Тем не менее, вы уверены, что для этого ключа что-то есть. В таком случае, используйте восклицательный знак (!) для принудительного извлечения значения.
  • Встроенный UITableViewCell имеет два встроенных свойства для описания ярлыка или этикетки: textLabel, detailTextLabel. Как раз здесь, вы можете их установить и вернуть ячейку.

И наконец, вы можете протестировать ваш табличный вид приложения, добавив следующий код в конец вашего файла playground (площадки):


let testDataSource = TestDataSource()
let tableView = UITableView(frame:CGRect(x: 0, y: 0, width: 320, height: 320), style:.Plain)
tableView.dataSource = testDataSource
tableView.reloadData()

Это создает вид таблицы заданного размера, и устанавливает источник данных для нового класса. Затем, он вызывает reloadData(), чтобы обновить вид таблицы.

Наведите вашу мышь на боковую панель площадки, на последнюю строку, и нажмите на появившийся глаз. Вы должны увидеть симпатичное превью вашей таблицы, с вашими значениями чаевых!

Файл playground'а до этого момента

Мы закончили с прототипом вашего нового и улучшенного TipCalculatorModel, и новым табличным видом приложения. Теперь, самое время интегрировать ваш код в проект!

Последние штрихи

Откройте ваш проект TipCalculator и скопируйте в него TipCalculatorModel, поверх вашей существующей реализации класса. 

Только не копируйте строчки нашего текста, которые в конце.

Откройте Main.storyboard, выберите ваш Table View (зона результатов) и удалите ее. Из библиотеки перетащите Table View (а не Table View Controller) в ваш View (в схеме документа). Установите X=0, Y=192, Width=600, Height=408.

Перенастройте расположение элементов, нажав на третью кнопку нижней правой части Interface Builder'а и нажмите Clear Constraints, потом Add Missing Constaints to View Controller.

Наконец, выделите ваш табличный вид (или table view) и выберите шестую вкладку инспектора (connection inspector). Вы увидите dataSource: перетащите кружочек на Tip Calculator в схеме документа.

Теперь, повторите тот же самый процесс для delegate: перетащите кружочек на Tip Calculator в схеме документа.

Теперь, ваш view controller (Tip Calculator) настроен не только как источник данных, но и как делегат, подобно тому, как вы ранее устанавливали тестовый класс в виде источника данных таблицы.

Убедитесь, что Assistant Editor открыт и показывает ViewController.swift. Перетащите с зажатым ctrl от table view в ViewController.swift под ваш последний outlet.

Выскочит окошко. В графе Name впишите tableView и нажмите Connect. Теперь вы можете обращаться к table view в вашем коде.

Октройте ViewController.swift и поставьте ваш класс как подчиняющийся UITableViewDataSource:


class ViewController: UIKit.UIViewController, UITableViewDataSource {

Затем, добавьте две переменные ниже tipCalc:


var possibleTips = Dictionary()
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, после чего срабатывает перезагрузка table view.

Удалите строку которая устанавливает resultsTextView в refreshUI().

Скопируйте два метода table view с вашей площадки (playground) в ваш класс, и удалите все комментарии из класса.

Конечный вариант ViewController.swift:


import UIKit
 
class UIViewController {
}
 
class ViewController: UIKit.UIViewController, UITableViewDataSource {
 
  @IBOutlet var totalTextField : UITextField!
  @IBOutlet var taxPctSlider : UISlider!
  @IBOutlet var taxPctLabel : UILabel!
  @IBOutlet var resultsTextView : UITextView!
  let tipCalc = TipCalculatorModel(total: 33.25, taxPct: 0.06)
  @IBOutlet weak var tableView: UITableView!
  var possibleTips = Dictionary()
  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. И, для более подробного изучения языка, вы можете пройти наши курсы!

Урок подготовил: Иван Акулов

Источник урока: http://www.raywenderlich.com/75289/swift-tutorial-part-3-tuples-protocols-delegates-table-views


Оцените статью
0
0
0
0
0

Чтобы добавить комментарий, авторизуйтесь
Войти
Безумова Виола
Пишет и переводит статьи для SwiftBook