Skip to content

Instantly share code, notes, and snippets.

@amelnychuck
Created July 27, 2017 23:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amelnychuck/4cc03b252d2f8ebf8a8b05dade7d07e5 to your computer and use it in GitHub Desktop.
Save amelnychuck/4cc03b252d2f8ebf8a8b05dade7d07e5 to your computer and use it in GitHub Desktop.
Example of why we need closures to replace the last instances of #selectors in Cocoa Touch
import UIKit
class TabBarViewController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: Keyboard Commands
override internal var keyCommands: [UIKeyCommand]? {
guard let controllers = viewControllers else {
return nil
}
var commands = [UIKeyCommand]()
for controller in controllers {
if let index = tabBar.items?.index(of: controller.tabBarItem) {
var pressedAction: Selector?
switch index {
case 0:
pressedAction = #selector(TabBarViewController.pressed1)
case 1:
pressedAction = #selector(TabBarViewController.pressed2)
case 2:
pressedAction = #selector(TabBarViewController.pressed3)
default:
break
}
if let action = pressedAction {
let command = UIKeyCommand(input: String(describing: index + 1), modifierFlags: .command, action: action, discoverabilityTitle: controller.tabBarItem.title!)
commands.append(command)
}
}
}
return commands
}
@objc func pressed1() {
selectedIndex = 0
}
@objc func pressed2() {
selectedIndex = 1
}
@objc func pressed3() {
selectedIndex = 2
}
}
@amelnychuck
Copy link
Author

If we were able to use closures, we could instead turn selectedIndex = 0, selectedIndex = 1, and selectedIndex = 2 in each corresponding method, to just be selectedIndex = self.viewControllers?.index(of: controller)—GREATLY simplifying the code, and making all arguments of UIKeyCommand possible to extract from context so if I wanted to add a 4th item to my tab bar, I wouldn't need to change or add a single line of code.

@leonardpauli
Copy link

I'd almost always advocate functional programming style and closures all the way, was expecting Apple to have block support for everything callback related when it was introduced in Objective-C way back.... even though that would require quite some code additions from their side...

However, with swift and bridging, they could've, possibly, added auto support / generation for closures-to-selectors, requiring no change on the massive amounts of API's from their side... (well, it is open-source, not sure they like auto-gen though...).

Nevertheless, the functionality you're asking for is still absolutely possible.

import UIKit

class TabBarViewController: UITabBarController {
  
  // MARK: Setup
  override func viewDidLoad() { super.viewDidLoad() }
  
  // MARK: Keyboard Commands
  var keyCommandsMap:[String:Int] = [:]
  override internal var keyCommands: [UIKeyCommand]? {
    guard let controllers = viewControllers else { return nil }
    
    var commands = [UIKeyCommand]()
    for controller in controllers {
      guard let index = tabBar.items?.index(of: controller.tabBarItem) else { continue }
      
      // ie. cmd-1 to go to tab nr 1
      let input = "\(index + 1)"
      let command = UIKeyCommand(
        input: input,
        modifierFlags: .command,
        action: #selector(TabBarViewController.pressed),
        discoverabilityTitle: controller.tabBarItem.title!
      )
      
      keyCommandsMap[input] = index
      commands.append(command)
    }
    
    return commands
  }
  
  @objc func pressed(sender: UIKeyCommand) {
    guard let index = keyCommandsMap[sender.input] else { return }
    selectedIndex = index
  }
  
}

Ps. guards is a great feature! This code is almost 50% less indented ;)

@leonardpauli
Copy link

Explored some additional choises; (I find syntax to be quite intriguing... pondering about writing my own...:) )

Swift; more functional

import UIKit

class TabBarViewController: UITabBarController {
  
  // MARK: Keyboard Commands
  var keyCommandsMap:[String:Int] = [:]
  override internal var keyCommands: [UIKeyCommand]? {
    let getInput = { (i: Int) -> String in
      let key = "\(i + 1)"
      return keyCommandsMap[key] = i
    }

    return viewControllers?.map {$0.tabBarItem}.map {(
        index: tabBar.items?.index(of: $0),
        item: $0
      )}.filter {$0.index != nil}.map {UIKeyCommand(
        input: getInput($0.index!),
        modifierFlags: .command,
        action: #selector(TabBarViewController.pressed),
        discoverabilityTitle: $0.item.title!
      )}
  }

  @objc func pressed(sender: UIKeyCommand) {
    guard let index = keyCommandsMap[sender.input] else { return }
    selectedIndex = index
  }
  
}

Swift; if closures were supported instead of selectors

import UIKit

class TabBarViewController: UITabBarController {
  
  // MARK: Keyboard Commands
  override internal var keyCommands: [UIKeyCommand]? {
    return viewControllers?.map {$0.tabBarItem}.map {(
        index: tabBar.items?.index(of: $0),
        item: $0
      )}.filter {$0.index != nil}.map {UIKeyCommand(
        input: "\($0.index! + 1)",
        modifierFlags: .command,
        discoverabilityTitle: $0.item.title!
      ) { [weak self] in self.selectedIndex = $0.index! }}
  }
  
}

LiteScript; Superset of ES7 JavaScript

import { UITabBarController, UIKeyCommand } from 'UIKit'

class TabBarViewController extends UITabBarController
  
  keyCommands() -get>
    this.viewControllers?.map(v-> v.tabBarItem)
      .map(item-> ({item, index: tabBar.items?.indexOf(item)}))
      .filter(({index})-> index)
      .map(({index, item})-> new UIKeyCommand({
        input: index+1+''
        modifierFlags: 'command'
        discoverabilityTitle: item.title
        action: ()=> this.selectedIndex = index
      })

Custom syntax; direct translation

import from UIKit: UITabBarController, UIKeyCommand

TabBarViewController inherits UITabBarController
  keyCommands: viewControllers?
    | map: .tabBarItem
    | map as item: {item, index: tabBar.items?.indexOf item}
    | filter: .index?
    | map: UIKeyCommand
      input: .index+1
      modifiers: :command
      title: .item.title
      on action: selectedIndex= .index

Custom syntax; how I think I would like to write it

import from UIKit: UITabBarController, UIKeyCommand

TabBarViewController inherits UITabBarController
  keyCommands: if viewControllers: iterate them:
    item: its tabBarItem
    index: tabBar.items?.indexOf item
    skip if !index
    yield UIKeyCommand
      input: index+1
      modifiers: :command
      title: item.title
      on action: selectedIndex= index

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