Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Last active April 13, 2020 23:26
Show Gist options
  • Save IanKeen/8410c17a95d6574090b2ca4f229cf0d8 to your computer and use it in GitHub Desktop.
Save IanKeen/8410c17a95d6574090b2ca4f229cf0d8 to your computer and use it in GitHub Desktop.
Segue<Destination> to tame Storyboard segues
//
// Coordinator
//
// Created by Ian Keen.
// Copyright © 2018 Ian Keen. All rights reserved.
//
import ObjectiveC
import UIKit
public extension UIViewController {
/// Configures the swizzling required to hook into the segue mechanism
public static let enableCoordinatorSegues: Void = {
swizzle(UIViewController.self, #selector(prepare(for:sender:)), #selector(swizzled_prepare(for:sender:)))
}()
}
public struct Segue<Destination: UIViewController> {
public typealias Configure = (Destination) -> Void
public let identifier: String
public let configure: Configure?
public init(identifier: String, configure: Configure? = nil) {
self.identifier = identifier
self.configure = configure
}
}
public protocol Coordinator {
func performSegue<T>(_ segue: Segue<T>)
}
extension UINavigationController: Coordinator { }
extension UITabBarController: Coordinator { }
extension Coordinator where Self: UIViewController {
public func performSegue<T>(_ segue: Segue<T>) {
let box = SegueBox { viewController in
segue.configure?(viewController as! T)
}
performSegue(withIdentifier: segue.identifier, sender: box)
}
}
public extension UIViewController {
public func attemptSegue<T>(_ segue: Segue<T>) {
guard let controller = firstSeguePerformer(withIdentifier: segue.identifier) as? Coordinator
else { fatalError("Unable to find a Coordinator to execute segue '\(segue.identifier)'") }
controller.performSegue(segue)
}
}
private extension UIViewController {
func canPerformSegue(withIdentifier identifier: String) -> Bool {
guard let segues = value(forKey: "storyboardSegueTemplates") as? [NSObject]
else { return false }
return segues.contains(where: { ($0.value(forKey: "identifier") as? String) == identifier })
}
func firstSeguePerformer(withIdentifier identifier: String) -> UIViewController? {
return sequence(first: self, next: { $0.next as? UIViewController })
.first(where: { $0.canPerformSegue(withIdentifier: identifier) })
}
}
private var segueBoxRef = "segueBoxRef"
private extension UIViewController {
@objc func swizzled_prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let sender = sender as? SegueBox {
sender.configure(segue.destination)
}
swizzled_prepare(for: segue, sender: sender)
}
var segueBox: SegueBox? {
get { return objc_getAssociatedObject(self, &segueBoxRef) as? SegueBox }
set { objc_setAssociatedObject(self, &segueBoxRef, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
}
private class SegueBox {
let configure: (UIViewController) -> Void
init(configure: @escaping (UIViewController) -> Void) {
self.configure = configure
}
}
private func swizzle(_ `class`: AnyClass, _ originalSelector: Selector, _ swizzledSelector: Selector) {
let originalMethod = class_getInstanceMethod(`class`, originalSelector)!
let swizzledMethod = class_getInstanceMethod(`class`, swizzledSelector)!
let didAdd = class_addMethod(
`class`, originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod)
)
if didAdd {
class_replaceMethod(
`class`, swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod)
)
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
@IanKeen
Copy link
Author

IanKeen commented Jul 7, 2019

Usage

Somewhere, i.e. appdelegate

UIViewController.enableCoordinatorSegues

Define Segues..

extension Segue {
	static var foo: Segue<FooViewController> {
		return .init(identifier: "segue_id") { viewController in
			//
		}
	}

	static var bar(_ value: Baz) -> Segue<BarViewController> {
		return .init(identifier: "segue_id") { viewController in
			//
		}
	}
}

In your 'meta' VCs to perform segues

  • only if they need to manually trigger them
performSegue(.foo)

In your 'leaf' VCs to ask any 'meta' VCs to perform segues

  • will automatically trigger them in the first VC in the responder chain it finds that can perform the segue
attemptSegue(.foo)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment