Pull to refresh

Swift! Protocol Oriented

Reading time 6 min
Views 24K
Всем привет!
Нет, это не очередной пост в стиле «встречайте Swift и его возможности», а скорее краткий экскурс по практическому применению и тонкостях, где протоколо-ориентированность нового языка от Apple позволяет делать симпатичные и удобные вещи.
image


Отдельное приветствие тем, кто заглянул под хабра-кат. В последнее время много приходилось разрабатывать на `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 отметит для себя пару приёмов, которые он не использовал. А сеньоры не будут критиковать и поделятся в комментариях парой своих секретов и наработок. Всем спасибо.
Tags:
Hubs:
+5
Comments 5
Comments Comments 5

Articles