$30 off During Our Annual Pro Sale. View Details »

Taking Swift Generics To The Extreme

Taking Swift Generics To The Extreme

In this presentation, we're starting from Swift Generics basic examples, and diving into how we, at monday.com, took this powerful Swift feature and used it to create awesome UI/UX behaviors with generic views all over the app.

Avatar for Ariel Pollack

Ariel Pollack

August 14, 2019
Tweet

Other Decks in Programming

Transcript

  1. !2

  2. !3 280 Employees (TLV + NYC) 265,000 Weekly Active Paying

    Users 2.5x ARR (Annual Recurring Revenue) Every Year Top 10 Fastest Growing Private Companies in the World!
  3. !6 Each column type have it’s own: • Appearance •

    Capabilities • Editing options • Integrations with other features in the platform
  4. We had to be able to scale fast without compromising

    performance and capabilities !12
  5. !17 29 years old ☀ Tel Aviv 7 years iOS

    dev 23 years computers geek Me and New Feature: a Dad :)
  6. The Team !20 Amit Frishberg Android Eran Helft Product Manager

    Yuval Brand iOS Shani Frankel Design Sahar Weissman Android Yoni Levin Android Team Lead Nir Lachman iOS Ariel Pollack iOS Ronen Sabag Android
  7. Agenda • Generics Basics • Generics and protocols - ❤

    at first sight • Generic Views - how and why should you use it • Building a generic view! !21
  8. “Generic programming is a style of computer programming in which

    algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters.” Generic Programming @ Wikipedia !22
  9. public struct Array<Element> public func map<T>(_ transform: (Element) throws ->

    T) rethrows -> [T] open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable Foundations RxSwift public class Observable<Element> : ObservableType { public typealias E = Element } !23
  10. Concept Array Element Use Case [“Hey”, “Man”] String Use Case

    [1, 2, 3] Int Use Case [CGRect.zero] CGRect Illustration: “Gentle Generics” by John Sundell !24
  11. struct SimpleStorage { private var storedValues: [Int] = [] func

    getValues() -> [Int] { return self.storedValues } mutating func set(values: [Int]) { self.storedValues = values } } !25 Storing Numbers
  12. struct SimpleStorage { private var storedValues: [Int] = [] func

    getValues() -> [Int] { return self.storedValues } mutating func set(values: [Int]) { self.storedValues = values } } !26 Storing Numbers
  13. struct SimpleStorage { private var storedValues: [Int] = [] func

    getValues() -> [Int] { return self.storedValues } mutating func set(values: [Int]) { self.storedValues = values } } !27 Storing Numbers
  14. struct SimpleStorage { private var storedValues: [Int] = [] func

    getValues() -> [Int] { return self.storedValues } mutating func set(values: [Int]) { self.storedValues = values } } !28 Storing Numbers
  15. struct SimpleStorage { private var storedValues: [Int] = [] func

    getValues() -> [Int] { return self.storedValues } mutating func set(values: [Int]) { self.storedValues = values } } !29 Storing Numbers
  16. struct SimpleStorage<T> { private var storedValues: [T] = [] func

    getValues() -> [T] { return self.storedValues } mutating func set(values: [T]) { self.storedValues = values } } !30 Storing Elements
  17. struct SimpleStorage<T> { private var storedValues: [T] = [] func

    getValues() -> [T] { return self.storedValues } mutating func set(values: [T]) { self.storedValues = values } } !31 Storing Elements
  18. protocol Storage { associatedtype T func getValues() -> [T] mutating

    func set(values: [T]) } Concept: Storing Data !35
  19. protocol Storage { associatedtype T func getValues() -> [T] mutating

    func set(values: [T]) } Concept: Storing Data struct SimpleStorage<T> { // ... } !36
  20. protocol Storage { associatedtype T func getValues() -> [T] mutating

    func set(values: [T]) } Concept: Storing Data struct SimpleStorage<T>: Storage { // ... } !37
  21. protocol Storage { associatedtype T func getValues() -> [T] mutating

    func set(values: [T]) } Concept: Storing Data struct SimpleStorage<T>: Storage { // ... } !38
  22. Use Cases: Local DB protocol DBSavable { ... } //

    Storing values to a local db class DBStorage<T>: Storage !39
  23. Use Cases: Local DB protocol DBSavable { ... } //

    Storing values to a local db class DBStorage<T>: Storage where T: DBSavable { ... } !40
  24. Use Cases: Local DB protocol DBSavable { ... } //

    Storing values to a local db class DBStorage<T>: Storage where T: DBSavable { ... } // An item that can be stored to a local db struct User: DBSavable { ... } !41
  25. Use Cases: Local DB protocol DBSavable { ... } //

    Storing values to a local db class DBStorage<T>: Storage where T: DBSavable { ... } // An item that can be stored to a local db struct User: DBSavable { ... } // Subclass a defined generic type final class UsersDBStorage: DBStorage<User> { } !42
  26. Use Cases: Local DB protocol DBSavable { ... } //

    Storing values to a local db class DBStorage<T>: Storage where T: DBSavable { ... } // An item that can be stored to a local db struct User: DBSavable { ... } // Subclass a defined generic type final class UsersDBStorage: DBStorage<User> { func someUsersSpecificMethod() { let values = getValues() // values: [User] } } !43
  27. Use Cases: UserDefaults class UserDefaultsStorage<T> { where T: NSCoding {

    private let key: String private let userDefaults: UserDefaults init(key: String, userDefaults: UserDefaults = .standard) { self.key = key self.userDefaults = userDefaults } func getValues() -> [T] { guard let array = userDefaults.value(forKey: key) as? [T] else { return [] } return array } func set(values: [T]) { userDefaults.setValue(values, forKey: key) } } !44
  28. Use Cases: UserDefaults class UserDefaultsStorage<T> { where T: NSCoding {

    private let key: String private let userDefaults: UserDefaults init(key: String, userDefaults: UserDefaults = .standard) { self.key = key self.userDefaults = userDefaults } func getValues() -> [T] { guard let array = userDefaults.value(forKey: key) as? [T] else { return [] } return array } func set(values: [T]) { userDefaults.setValue(values, forKey: key) } } !45
  29. Use Cases: UserDefaults class UserDefaultsStorage<T>: Storage { where T: NSCoding

    { private let key: String private let userDefaults: UserDefaults init(key: String, userDefaults: UserDefaults = .standard) { self.key = key self.userDefaults = userDefaults } func getValues() -> [T] { guard let array = userDefaults.value(forKey: key) as? [T] else { return [] } return array } func set(values: [T]) { userDefaults.setValue(values, forKey: key) } } !46
  30. Use Cases: UserDefaults class UserDefaultsStorage<T>: Storage { where T: NSCoding

    { private let key: String private let userDefaults: UserDefaults init(key: String, userDefaults: UserDefaults = .standard) { self.key = key self.userDefaults = userDefaults } func getValues() -> [T] { guard let array = userDefaults.value(forKey: key) as? [T] else { return [] } return array } func set(values: [T]) { userDefaults.setValue(values, forKey: key) } } !47
  31. Use Cases: UserDefaults class UserDefaultsStorage<T>: Storage { where T: NSCoding

    { private let key: String private let userDefaults: UserDefaults init(key: String, userDefaults: UserDefaults = .standard) { self.key = key self.userDefaults = userDefaults } func getValues() -> [T] { guard let array = userDefaults.value(forKey: key) as? [T] else { return [] } return array } func set(values: [T]) { userDefaults.setValue(values, forKey: key) } } !48
  32. Use Cases: UserDefaults class UserDefaultsStorage<T>: Storage where T: NSCoding {

    private let key: String private let userDefaults: UserDefaults init(key: String, userDefaults: UserDefaults = .standard) { self.key = key self.userDefaults = userDefaults } func getValues() -> [T] { guard let array = userDefaults.value(forKey: key) as? [T] else { return [] } return array } func set(values: [T]) { userDefaults.setValue(values, forKey: key) } } !49
  33. Use Cases: UserDefaults Storing numbers to UserDefaults typealias NumbersStorage =

    UserDefaultsStorage<NSNumber> let visitedBoards = NumbersStorage(key: “visited_boards_ids”) !51
  34. Use Cases: UserDefaults Storing numbers to UserDefaults typealias NumbersStorage =

    UserDefaultsStorage<NSNumber> let visitedBoards = NumbersStorage(key: “visited_boards_ids”) let boardIds = [1, 2, 3].map(NSNumber.init) visitedBoards.set(values: boardIds) visitedBoards.getValues() // => [1, 2, 3] !52
  35. protocol Configurable { associatedtype T func configure(with data: T) }

    typealias ConfigurableView = UIView & Configurable !55
  36. class CardView<View>: ConfigurableView { where View: ConfigurableView { typealias Data

    = View.Data let containedView = View(frame: .zero) let lblTitle: UILabel = { let label = UILabel(frame: .zero) label.text = "I'm the title" return label }() // setup views somewhere… func configure(with data: Data) { containedView.configure(with: data) } } !56
  37. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    View.T let containedView = View(frame: .zero) let lblTitle: UILabel = { let label = UILabel(frame: .zero) label.text = "I'm the title" return label }() // setup views somewhere… func configure(with data: T) { containedView.configure(with: data) } } !57
  38. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    View.T let containedView = View(frame: .zero) let lblTitle: UILabel = { let label = UILabel(frame: .zero) label.text = "I'm the title" return label }() // setup views somewhere… func configure(with data: T) { containedView.configure(with: data) } } !58
  39. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    View.T let containedView = View(frame: .zero) let lblTitle: UILabel = { let label = UILabel(frame: .zero) label.text = "I'm the title" return label }() // setup views somewhere… func configure(with data: T) { containedView.configure(with: data) } } !59 ConfigurableView ConfigurableView
  40. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    View.T let containedView = View(frame: .zero) let lblTitle: UILabel = { let label = UILabel(frame: .zero) label.text = "I'm the title" return label }() // setup views somewhere… func configure(with data: T) { containedView.configure(with: data) } } !60 ConfigurableView ConfigurableView Data
  41. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    View.T let containedView = View(frame: .zero) let lblTitle: UILabel = { let label = UILabel(frame: .zero) label.text = "I'm the title" return label }() // setup views somewhere… func configure(with data: T) { containedView.configure(with: data) } } !61 ConfigurableView ConfigurableView Data
  42. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    View.T let containedView = View(frame: .zero) let lblTitle: UILabel = { let label = UILabel(frame: .zero) label.text = "I'm the title" return label }() // setup views somewhere… func configure(with data: T) { containedView.configure(with: data) } } !62
  43. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    View.T let containedView = View(frame: .zero) let lblTitle: UILabel = { let label = UILabel(frame: .zero) label.text = "I'm the title" return label }() // setup views somewhere… func configure(with data: T) { containedView.configure(with: data) } } !63
  44. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    View.T let containedView = View(frame: .zero) let lblTitle = UILabel(frame: .zero) // called in init func setupViews() { ... } func configure(with data: T) { lblTitle.text = data.title containedView.configure(with: data) } } !72
  45. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    View.T let containedView = View(frame: .zero) let lblTitle = UILabel(frame: .zero) // called in init func setupViews() { ... } func configure(with data: T) { lblTitle.text = data.title containedView.configure(with: data) } } !73
  46. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    (title: String, data: View.T) let containedView = View(frame: .zero) let lblTitle = UILabel(frame: .zero) // called in init func setupViews() { ... } func configure(with data: T) { lblTitle.text = data.title containedView.configure(with: data.data) } } !74
  47. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    (title: String, data: View.T) let containedView = View(frame: .zero) let lblTitle = UILabel(frame: .zero) // called in init func setupViews() { ... } func configure(with data: T) { lblTitle.text = data.title containedView.configure(with: data.data) } } !75
  48. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    (title: String, data: View.T) let containedView = View(frame: .zero) let lblTitle = UILabel(frame: .zero) // called in init func setupViews() { ... } func configure(with data: T) { lblTitle.text = data.title containedView.configure(with: data.data) } } !76
  49. class CardView<View>: ConfigurableView where View: ConfigurableView { typealias T =

    (title: String, data: View.T) let containedView = View(frame: .zero) let lblTitle = UILabel(frame: .zero) // called in init func setupViews() { ... } func configure(with data: T) { lblTitle.text = data.title containedView.configure(with: data.data) } } let imageCard = CardView<UIImageView>() imageCard.configure(with: ( title: "Image", data: UIImage(named: "swift")! )) !77
  50. Let’s build a generic menu • MenuView<OptionType> will present a

    list of options conforming to OptionType • Each option will have a title and an icon • It should be really easy to use !80
  51. protocol MenuOption { var icon: UIImage { get } var

    title: String { get } } !82 class MenuOptionCell: UITableViewCell, Configurable { let lblTitle = UILabel() let imgIcon = UIImageView() func setupViews() { ... } func configure(with option: MenuOption) { imgIcon.image = option.icon lblTitle.text = option.title } }
  52. protocol MenuOption { var icon: UIImage { get } var

    title: String { get } } !83 class MenuOptionCell: UITableViewCell, Configurable { let lblTitle = UILabel() let imgIcon = UIImageView() func setupViews() { ... } func configure(with option: MenuOption) { imgIcon.image = option.icon lblTitle.text = option.title } }
  53. class MenuView<OptionType> { UITableViewDataSource, UITableViewDelegate where OptionType: MenuOption { typealias

    SelectOptionClosure = (OptionType) -> Void // vars private var didSelectOption: SelectOptionClosure private var options: [OptionType] { didSet { tableView.reloadData() } } // views private lazy var tableView: UITableView = { ... }() init(didSelectOption: @escaping SelectOptionClosure) { ... } func configure(with options: [OptionType]) { self.options = options } // MARK: UITableViewDataSource & UITableViewDelegate // ... } !84
  54. class MenuView<OptionType>: ConfigurableView { UITableViewDataSource, UITableViewDelegate where OptionType: MenuOption {

    typealias SelectOptionClosure = (OptionType) -> Void // vars private var didSelectOption: SelectOptionClosure private var options: [OptionType] { didSet { tableView.reloadData() } } // views private lazy var tableView: UITableView = { ... }() init(didSelectOption: @escaping SelectOptionClosure) { ... } func configure(with options: [OptionType]) { self.options = options } // MARK: UITableViewDataSource & UITableViewDelegate // ... } !85 typealias T = [OptionType]
  55. class MenuView<OptionType>: ConfigurableView, UITableViewDataSource, UITableViewDelegate { where OptionType: MenuOption {

    typealias SelectOptionClosure = (OptionType) -> Void // vars private var didSelectOption: SelectOptionClosure private var options: [OptionType] { didSet { tableView.reloadData() } } // views private lazy var tableView: UITableView = { ... }() init(didSelectOption: @escaping SelectOptionClosure) { ... } func configure(with options: [OptionType]) { self.options = options } // MARK: UITableViewDataSource & UITableViewDelegate // ... } !86
  56. class MenuView<OptionType>: ConfigurableView, UITableViewDataSource, UITableViewDelegate { where OptionType: MenuOption {

    typealias SelectOptionClosure = (OptionType) -> Void // vars private var didSelectOption: SelectOptionClosure private var options: [OptionType] { didSet { tableView.reloadData() } } // views private lazy var tableView: UITableView = { ... }() init(didSelectOption: @escaping SelectOptionClosure) { ... } func configure(with options: [OptionType]) { self.options = options } // MARK: UITableViewDataSource & UITableViewDelegate // ... } !87
  57. class MenuView<OptionType>: ConfigurableView, UITableViewDataSource, UITableViewDelegate { where OptionType: MenuOption {

    typealias SelectOptionClosure = (OptionType) -> Void // vars private var didSelectOption: SelectOptionClosure private var options: [OptionType] { didSet { tableView.reloadData() } } // views private lazy var tableView: UITableView = { ... }() init(didSelectOption: @escaping SelectOptionClosure) { ... } func configure(with options: [OptionType]) { self.options = options } // MARK: UITableViewDataSource & UITableViewDelegate // ... } !88
  58. class MenuView<OptionType>: ConfigurableView, UITableViewDataSource, UITableViewDelegate where OptionType: MenuOption { typealias

    SelectOptionClosure = (OptionType) -> Void // vars private var didSelectOption: SelectOptionClosure private var options: [OptionType] { didSet { tableView.reloadData() } } // views private lazy var tableView: UITableView = { ... }() init(didSelectOption: @escaping SelectOptionClosure) { ... } func configure(with options: [OptionType]) { self.options = options } // MARK: UITableViewDataSource & UITableViewDelegate // ... } !89
  59. Use Case: Board Menu enum BoardMenuOption { case delete case

    rename } extension BoardMenuOption: MenuOption { var title: String { switch self { case .delete: return “Delete" case .rename: return "Rename" } } var icon: UIImage { switch self { case .delete: return UIImage(named: “trash") case .rename: return UIImage(named: “rename") } } } !91
  60. With a little refactor, we can make it work with

    different types of cells. !95
  61. class MenuView<OptionCell>: ConfigurableView, UITableViewDataSource, UITableViewDelegate where OptionCell: ConfigurableCell { typealias

    OptionType = OptionCell.Data // ... } typealias ConfigurableCell = UITableViewCell & Configurable !99
  62. class MenuView<OptionCell>: ConfigurableView, UITableViewDataSource, UITableViewDelegate where OptionCell: ConfigurableCell { typealias

    OptionType = OptionCell.T // ... } typealias ConfigurableCell = UITableViewCell & Configurable !100
  63. class MenuView<OptionCell>: ConfigurableView, UITableViewDataSource, UITableViewDelegate where OptionCell: ConfigurableCell { typealias

    OptionType = OptionCell.T // ... } typealias ConfigurableCell = UITableViewCell & Configurable !101
  64. typealias BoardMenu = MenuView<BoardMenuOption> let menuView = BoardMenu(didSelectOption: { option

    in print(option) }) menuView.configure(options: [.rename, .delete]) typealias ConfigurableCell = UITableViewCell & Configurable // before !102 class MenuView<OptionCell>: ConfigurableView, UITableViewDataSource, UITableViewDelegate where OptionCell: ConfigurableCell { typealias OptionType = OptionCell.T // ... }
  65. typealias BoardMenu = MenuView<BoardMenuOption> let menuView = BoardMenu(didSelectOption: { option

    in print(option) }) menuView.configure(options: [.rename, .delete]) typealias ConfigurableCell = UITableViewCell & Configurable // before !103 class MenuView<OptionCell>: ConfigurableView, UITableViewDataSource, UITableViewDelegate where OptionCell: ConfigurableCell { typealias OptionType = OptionCell.T // ... }
  66. typealias BoardMenu = MenuView<MenuOptionCell> let menuView = BoardMenu(didSelectOption: { option

    in print(option) }) menuView.configure(options: [.rename, .delete]) typealias ConfigurableCell = UITableViewCell & Configurable // after And it just works! !104 class MenuView<OptionCell>: ConfigurableView, UITableViewDataSource, UITableViewDelegate where OptionCell: ConfigurableCell { typealias OptionType = OptionCell.T // ... }
  67. Use Case: Status Menu struct Status { let title: String

    let backgroundColor: UIColor } !106
  68. Use Case: Status Menu struct Status { let title: String

    let backgroundColor: UIColor } class StatusCell: UITableViewCell, Configurable { let statusColorView = UIView() let lblTitle = UILabel() func setupViews() { ... } func configure(with status: Status) { lblTitle.text = status.title statusColorView.backgroundColor = status.backgroundColor } } !107
  69. Use Case: Status Menu struct Status { let title: String

    let backgroundColor: UIColor } class StatusCell: UITableViewCell, Configurable { let statusColorView = UIView() let lblTitle = UILabel() func setupViews() { ... } func configure(with status: Status) { lblTitle.text = status.title statusColorView.backgroundColor = status.backgroundColor } } !108
  70. Use Case: Status Menu struct Status { let title: String

    // “Done” let backgroundColor: UIColor // .doneGreen } class StatusCell: UITableViewCell, Configurable { let statusColorView = UIView() let lblTitle = UILabel() func setupViews() { ... } func configure(with status: Status) { lblTitle.text = status.title statusColorView.backgroundColor = status.backgroundColor } } !109
  71. let statuses = [ Status(title: “Working on it", backgroundColor: .workingOrange),

    Status(title: "Done", backgroundColor: .doneGreen), Status(title: "Stuck", backgroundColor: .stuckRed) ] !110
  72. let statuses = [ Status(title: “Working on it", backgroundColor: .workingOrange),

    Status(title: "Done", backgroundColor: .doneGreen), Status(title: "Stuck", backgroundColor: .stuckRed) ] typealias StatusMenu = MenuView<StatusCell> let menuView = StatusMenu(didSelectOption: { status in print(status.title) }) menuView.configure(options: statuses) !111
  73. let statuses = [ Status(title: “Working on it", backgroundColor: .workingOrange),

    Status(title: "Done", backgroundColor: .doneGreen), Status(title: "Stuck", backgroundColor: .stuckRed) ] typealias StatusMenu = MenuView<StatusCell> let menuView = StatusMenu(didSelectOption: { status in print(status.title) }) menuView.configure(options: statuses) !112
  74. • Super reusable views (shadow, appearance, …) • Super reusable

    behaviors (animations, transitions, …) • Consistent UI/UX • Scalable - change the whole look in one place What’s in it for me? And will make your design team love you even more !113
  75. Where to go from here? • Xcode Playground • Swift

    Generics Manifesto • Gentle Generics by John Sundell • David Hart - Swift Generics - The 5 Stages of PATs !114