Skip to content

Instantly share code, notes, and snippets.

@JasonCanCode
Last active August 3, 2018 15:51
Show Gist options
  • Save JasonCanCode/47069b64ec9159aba629bf61fd5c5fc2 to your computer and use it in GitHub Desktop.
Save JasonCanCode/47069b64ec9159aba629bf61fd5c5fc2 to your computer and use it in GitHub Desktop.
Menu Delegate for easily handling a side drawer nav
import UIKit
/// Fuctionality for a root view controller that handles the feature container view and the side menu drawer
protocol MenuDelegate: class {
var titleLabel: UILabel! { get } // To show feature name in mock nav bar
var containerView: UIView! { get }
var menuWidthConstraint: NSLayoutConstraint! { get }
var contentViewController: UIViewController! { get set }
func showTimeline()
func showProfile()
func showMessages()
func showSettings()
func logout()
}
extension MenuDelegate where Self: UIViewController {
func toggleMenu() {
let isMenuVisible = menuWidthConstraint.constant > 0
animateMenu(shouldShow: !isMenuVisible)
}
func animateMenu(shouldShow: Bool) {
let newWidth = shouldShow
? Constants.Menu.menuWidth
: 0
view.layoutIfNeeded()
menuWidthConstraint.constant = newWidth
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.25,
options: [.curveEaseInOut],
animations: { self.view.layoutIfNeeded() },
completion: nil)
}
func dismissSideMenu() {
animateMenu(shouldShow: false)
}
func newContentViewController<T: UIViewController>(forClass classType: T.Type) -> T? {
guard let nextViewController: T = StoryboardHelper.new() else {
return nil
}
return nextViewController
}
func replaceOrRefreshContent<T>(with nextViewController: T) where T: UIViewController, T: TopLevelViewControllerType {
if contentViewController is T,
let refreshableVC = contentViewController as? DataRefreshable {
// Refresh the current feature view when selected while already present
refreshableVC.refreshData()
} else {
replaceContent(with: nextViewController)
}
}
private func replaceContent<T>(with nextViewController: T) where T: UIViewController, T: TopLevelViewControllerType {
removeContent()
contentViewController = nextViewController
addChildViewController(nextViewController)
contentViewController!.view.frame = containerView.bounds
contentViewController!.didMove(toParentViewController: self)
containerView.addSubview(contentViewController!.view)
titleLabel.text = nextViewController.navigationTitle
view.layoutIfNeeded()
}
func removeContent() {
if let content = contentViewController {
content.willMove(toParentViewController: nil)
content.view.removeFromSuperview()
content.removeFromParentViewController()
self.contentViewController = nil
}
}
}
import UIKit
struct MenuModel {
let text: String
let image: UIImage?
let handler: () -> Void
init(text: String, image: UIImage? = nil, handler: () -> Void) {
self.text = text
self.image = image
self.handler = handler
}
}
protocol ParentNavigationDelegate: class {
func setNavBar(title: String)
}
import UIKit
/**
ParentViewController should be embedded in a UINavigationController in the Main storyboard. It will consist of two container views side by side. The left will contain a TableViewController for the SideMenuViewController and the right (`containerView`) will be the width of the device and house the `contentViewController`. A toolbar sits atop the containerView as a mock nav bar for the root views.
*/
class ParentViewController: UIViewController, MenuDelegate {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var menuWidthConstraint: NSLayoutConstraint!
weak var contentViewController: UIViewController!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
menuWidthConstraint.constant = 0
navigationController?.isNavigationBarHidden = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.isNavigationBarHidden = false
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "embedInitialContent" {
contentViewController = segue.destination
} else if let menuVC = segue.destination as? SideMenuViewController {
menuVC.delegate = self
}
}
// MARK: - Navigation
func showTimeline() {
if let nextViewController = newContentViewController(forClass: TimelineViewController.self) {
replaceContent(with: nextViewController)
}
dismissSideMenu()
}
func showProfile() {
if let nextViewController = newContentViewController(forClass: ProfileViewController.self) {
replaceContent(with: nextViewController)
}
dismissSideMenu()
}
func showMessages() {
if let nextViewController = newContentViewController(forClass: MessagesViewController.self) {
replaceContent(with: nextViewController)
}
dismissSideMenu()
}
func showSettings() {
if let nextViewController = newContentViewController(forClass: SettingsViewController.self) {
replaceContent(with: nextViewController)
}
dismissSideMenu()
}
func logout() {
repository.logout()
performSegue(withIdentifier: "unwindToLogin", sender: nil)
}
}
// MARK: - Navigation Bar Handling
extension ParentViewController: MenuDelegate, ParentNavigationDelegate {
@IBAction private func menuButtonPressed(_ sender: UIBarButtonItem) {
toggleMenu()
}
func setNavBar(title: String) {
titleLabel.text = title
}
}
import UIKit
class SideMenuViewController: UIViewController {
@IBOutlet weak private var tableView: UITableView!
@IBOutlet weak private var versionLabel: UILabel!
weak var delegate: MenuDelegate?
/// Generated dynamically to show/hide elements depending on user state
private var cellModels: [MenuModel] {
guard let delegate = delegate else {
return []
}
var models: [MenuModel] = []
models.append((text: "Timeline", handler: delegate.showTimeline))
models.append((text: "Profile", handler: delegate.showProfile))
models.append((text: "Messages", handler: delegate.showMessages))
models.append((text: "Settings", handler: delegate.showSettings))
models.append((text: "Log Out", handler: delegate.logout))
return models
}
var versionLabelText: String {
guard let versionNumberString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
let buildNumberString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
return ""
}
return String(format: "Version %@ (%@)", versionNumberString, buildNumberString)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
versionLabel.text = versionLabelText
}
/* If you have a dynamic side menu you can update it here
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if delegate?.isMenuVisible == true {
// Update cells as menu is being revealed
tableView.reloadData()
}
}
*/
}
// MARK: - Table Handling
extension SideMenuViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MenuItemCell", for: indexPath)
let model = cellModels[indexPath.row]
cell.textLabel?.text = model.text
cell.imageView?.image = model.image
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let model = cellModels[indexPath.row]
model.handler()
}
}
import UIKit
/// Identifies a root View Controller for a menu feature. Since a custom nav bar replacement is used, this is needed to set the title.
protocol TopLevelViewControllerType {
var navigationTitle: String { get }
}
@JasonCanCode
Copy link
Author

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