Всем привет!
Нет, это не очередной пост в стиле «встречайте Swift и его возможности», а скорее краткий экскурс по практическому применению и тонкостях, где протоколо-ориентированность нового языка от Apple позволяет делать симпатичные и удобные вещи.
Отдельное приветствие тем, кто заглянул под хабра-кат. В последнее время много приходилось разрабатывать на `Swift`, в тоже время есть большой багаж по объектному С и какое-то желание выразить некоторые вещи кодом, которые я понял намного проще и элегантнее можно реализовать на новом языке.
Внимание: статья не является подробным обучающим материалам, а представляет практические моменты, которые лично меня впечатлили и я решил ими поделиться, наверное, большинству читателей они покажутся знакомыми, так что кто хочет что-то добавить — просим в комментарии.
Что ждать в этой статье?
- Пару вводных предложений (плюс небольшая полезная либка)
- Декорируем дополнительное поведение класса с Extension (немного кода)
- Создаем реиспользуемый элемент с помощью протокола и дефолтной имплементации (много кода)
- Протоколы и enum — может быть удобно (средне кода)
В чём мощь протоколов?
Во-первых, как все знают механизм протоколов позволяют реализовывать множественное наследование разнотипных протоколов одним объектом. Наследование нас ограничивает тем, что в цепочке наследников на n-ом шаге нельзя «влить» или «добавить» новое общее с каким-то другим объектом поведение.
Во-вторых, в Swift есть возможность добавить дефолтную имплементацию (реализацию по умолчанию) для указанного протокола. При этом протокол может иметь несколько реализаций по умолчаний в зависимости от класса или типа объекта, который его наследует.
В-третьих, протокол можно наследовать от протокола.
В-четвёртых, протоколы могут быть унаследованы не только классами (Class), но структурами (Struct) и перечислениями (Enum).
В-пятых, протоколы могу добавлять свойства.
В-шестых, можно добавлять реализацию по умолчанию и для системных протоколов, а при желании уже переопределять в конкретной классе.
В завершении добавлю, что протоколы позволяют делать код переиспользуемым в разных классах и структурах. Можно реализовывать частые задачи в них и подключать как декораторы в те файлы, где они необходимы.
Например, в каждом проекте есть необходимость обработать клик на UIView, чтобы каждый раз не писать лишний код делайте свой класс Tappable(код — тут)
Лично мне не хватает некоторой конвенции при наследовании протокола, чтобы явно были видны наследуемые методы и свойства (слышал такое есть в Ruby):
protocol FCActionProtocol { var actionButton: UIButton! {get set} func showActionView() } class FCController: FCActionProtocol { var actionButton: UIButton! // FCActionProtocol convenience func showActionView() {} }
Вот хотелось бы, чтобы actionButton и showActionView() подставлялись в автоматически генерируемую область.
Буду ждать с Swift 3.0
Декорируем дополнительное поведение класса с Extension
Итак, от теории к практике: жизненный кейс №1.
Представим, что у нас есть логика по view cycle у контроллера и логика по передачи модели к view. Внезапно у нас появляется новое расширение контроллера, куда нужно уместить логику по показу почтового клиента. С протоколами это легко:
class MyViewController: UIViewController { // a lot of code here } extension MyViewController: MFMailComposeViewControllerDelegate { func showMailController() { let mailComposeViewController = configuredMailComposeViewController() if MFMailComposeViewController.canSendMail() { self.presentViewController(mailComposeViewController, animated: true, completion: nil) } } func configuredMailComposeViewController() -> MFMailComposeViewController { let controller = MFMailComposeViewController() controller.mailComposeDelegate = self return controller // customize and set it here } // MARK: - MFMailComposeViewControllerDelegate func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) {} }
Очень радует, что в отличие от obj-c в Swift можно в расширении класса MyViewController указать новые наследуемые протоколы и реализовать их поведение.
Создаем реиспользуемый элемент с помощью протокола и дефолтной имплементации
Кейс №2: недавно в приложении на 2-ух экранах была одинаковая кнопка, которая вела к одинаковому сценарию — показу actionSheet с действиями, по одному из которых показывался почтовый клиент. Техническая задача заключалась в том, чтобы реализовать протокол с имплементацией и всей логикой внутри, так чтобы степень сложности его подключения и зависимостей была минимальной. Вот так выглядит код в проекте:
protocol FCActionProtocol { var actionButton: UIButton! {get set} var delegateHandler: FCActionProtocolDelegateHandler! {get set} mutating func showActionSheet() func showMailController() } class FCActionProtocolDelegateHandler : NSObject, MFMailComposeViewControllerDelegate { var delegate: FCActionProtocol! init(delegate: FCActionProtocol) { super.init() self.delegate = delegate } func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) { controller.dismissViewControllerAnimated(true, completion: nil) } } extension FCActionProtocol { mutating func showActionSheet() { delegateHandler = FCActionProtocolDelegateHandler(delegate: self) let actionController = UIAlertController(title: nil, message: nil, preferredStyle: .ActionSheet) actionController.addAction(UIAlertAction(title: NSLocalizedString("ActionClear", comment: ""), style: .Default) { (action) in }) actionController.addAction(UIAlertAction(title: NSLocalizedString("ActionWriteBack", comment: ""), style: .Default) { (action) in self.showMailController() }) if let controller = self as? UIViewController { controller.presentViewController(actionController, animated: true) {} } } func showMailController() { if MFMailComposeViewController.canSendMail() { let controller = MFMailComposeViewController() controller.mailComposeDelegate = delegateHandler (self as! UIViewController).navigationController!.presentViewController(controller, animated: true, completion: nil) } } }
Внимание! Идея кода в том, что есть протокол FCActionProtocol, который включает в себя кнопку, (actionButton) по нажатию на которую происходит показ листа с действиями (showActionSheet). Внутри по клику на элемент листа должен показаться почтовый клиент (showMailController). Для того, чтобы логику и обработку этого вызова не реализовывать в классе, который наследует наш протокол мы делаем дефолтную имплементацию внутри с помощью некоторой абстрактной сущности delegateHandler, которая создается внутри нашего расширения и делегатные методы уже почтового клиента обрабатываются экземпляром класса FCActionProtocolDelegateHandler.
В результате сложность добавления этого реиспользуемого action-листа заключается в следующем:
class FCMyController: FCActionProtocol { var actionButton: UIButton! // convenience FCActionProtocol var delegateHandler: FCActionProtocolDelegateHandler! // convenience FCActionProtocol }
Вся логика внутри. Нам нужно только проинициализировать и добавить кнопку. На мой взгляд, получилось красиво и лаконично.
Протоколы и enum — может быть удобно
Жизненный кейс №3: наша команда делала сервис по продаже авиабилетов онлайн. Мобильный клиент тесно общается с сервером и есть разные сценарии при которых делается обращения к API. Разделим их условно на поиск, бронирование билета и оплату. В каждом из этих процессов может произойти ошибка (на стороне сервера, клиента, протокола общения, валидации данных и так далее). Если при бронировании или поиске 500-ая с сервера еще не несёт ничего страшного, то, например, при оплате данные с внутреннего сервера могли уже уйти в платежный шлюз и нельзя клиенту просто показать ошибку, в то время как его деньги могли быть списаны с банковской карты.
Здесь протоколы могу позволить создать достаточно изящный код:
protocol Critical { func criticalStatus() -> (critical: Bool, message: String) } enum Error { case Search(code: Int) case Booking(code: Int) case Payment(code: Int) } extension Error : Critical { func criticalStatus() -> (critical: Bool, message: String) { switch self { case .Payment(let code) where code == 500: return (true, "Please contact us, because your payment could proceed") default: return (false, "Something went wrong. Please try later.") } } }
Теперь дёргаем наш код и оцениваем насколько всё плохо:
let error = Error.Payment(code: 500) if error.criticalStatus().critical { print("callcenter will solve it") }
Жалко, что в реальности проект представлял огромный пласт objective-c с кучей хаков для совместимости с Swift.
Надеюсь, что следующие проекты можно будет реализовывать, используя все возможности языка.
P.s. Надеюсь, кто-то начинающий заинтересуется Swift-ом и подходом разработки с использованием протоколов. Может быть кто-то из middle отметит для себя пару приёмов, которые он не использовал. А сеньоры не будут критиковать и поделятся в комментариях парой своих секретов и наработок. Всем спасибо.
ссылка на оригинал статьи http://habrahabr.ru/post/272005/
Добавить комментарий