Pull to refresh

Swift улучшаем performSegueWithIdentifier или удобный роутер со сторибордами

Reading time 3 min
Views 11K
Редкий разработчик под iOS или OS X не использовал сториборды и еще меньше программистов не передавали данные между экранами.
Все мы знаем метод performSegueWithIdentifier и трудности работы с ним.

Начав проект на Swift в один момент мне стало обидно: «Почему мы должны в строго-типизированном языке использовать обертку для передачи данных?»
Через пару минут сформировалось видение решения и в скором времени реализация.

Долго думал, стоит ли писать об этом, поскольку материал крайне небольшой, но эти 50 строк могут вам очень сильно помочь

menuController?.performSegueWithIdentifier(changeItemIdentifier, sender: nil) { segue in
    let controller = segue.destinationViewController as! ChangeMenuItemController
    controller.viewModel.sourceMenuItem = item
}


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

Разберемся как это работает? Не претендую на уникальность, но мне кажется подход интересным.


1) Создаем метод аналогичный performSegueWithIdentifier, но с дополнительным параметром closure (configurate)
2) Сохраняем configurate
3) Вызываем метод performSegueWithIdentifier
4) В момент вызова prepareForSegue осталось только вызвать configurate

Можно написать класс, однако намного продуктивнее написать категорию.
Тонким моментом является сохранение configurate и вызов его из prepareForSegue (который важно не сломать своими действиями).
Для сохранения будем использовать ассоциативные ссылки. Вместо prepareForSegue будет вызываться наш метод, откуда будет вызываться configurate и уже потом вызываться оригинальный метод.

Верхнеуровневый код
typealias ConfiguratePerformSegue = (UIStoryboardSegue) -> ()
func performSegueWithIdentifier(identifier: String, sender: AnyObject?, configurate: ConfiguratePerformSegue?) {
    swizzlingPrepareForSegue()
    configuratePerformSegue = configurate
    performSegueWithIdentifier(identifier, sender: sender)
}



Реализация очень небольшая, и проще приложить код под спойлер
Реализация
class Box {
    let value: Any
    init(_ value: Any) {
        self.value = value
    }
}

extension UIViewController {
    struct AssociatedKey {
        static var ClosurePrepareForSegueKey = "ClosurePrepareForSegueKey"
        static var token: dispatch_once_t = 0
    }
    
    typealias ConfiguratePerformSegue = (UIStoryboardSegue) -> ()
    func performSegueWithIdentifier(identifier: String, sender: AnyObject?, configurate: ConfiguratePerformSegue?) {
        swizzlingPrepareForSegue()
        configuratePerformSegue = configurate
        performSegueWithIdentifier(identifier, sender: sender)
    }
    
    private func swizzlingPrepareForSegue() {
        dispatch_once(&AssociatedKey.token) {
            let originalSelector = #selector(UIViewController.prepareForSegue(_:sender:))
            let swizzledSelector = #selector(UIViewController.closurePrepareForSegue(_:sender:))
            
            let instanceClass = UIViewController.self
            let originalMethod = class_getInstanceMethod(instanceClass, originalSelector)
            let swizzledMethod = class_getInstanceMethod(instanceClass, swizzledSelector)
            
            let didAddMethod = class_addMethod(instanceClass, originalSelector,
                                               method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
            
            if didAddMethod {
                class_replaceMethod(instanceClass, swizzledSelector,
                                    method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
            } else {
                method_exchangeImplementations(originalMethod, swizzledMethod)
            }
        }
    }
    
    func closurePrepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        configuratePerformSegue?(segue)
        closurePrepareForSegue(segue, sender: sender)
        configuratePerformSegue = nil
    }
    
    var configuratePerformSegue: ConfiguratePerformSegue? {
        get {
            let box = objc_getAssociatedObject(self, &AssociatedKey.ClosurePrepareForSegueKey) as? Box
            return box?.value as? ConfiguratePerformSegue
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKey.ClosurePrepareForSegueKey, Box(newValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
    }
}



На этом все. Это очень маленький хак, который работает и на objc, и на Swift.
Это не только не нарушает написанный вами код ранее, но и позволяет комбинировать оба подхода или же полностью перейти на конфигурирование из блоков.
Так же это позволяет вынести зависимость переходов между экранами в специальный Router класс и использовать вместе со сторибордами

upd: после общения выложил в виде pod'а + использование с автоприведением destination UIViewController.
cocoapods.org/pods/PureSegue
Tags:
Hubs:
+9
Comments 6
Comments Comments 6

Articles