Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Introduction to Swift Keypaths

Introduction to Swift Keypaths

Keypaths were added in Swift 4. They're a fantastic feature but sometimes it feels difficult to find good situations for using them. However, when understood and used the right way, keypaths allow you to implement additional abstractions into your codebase that would be much harder with generics or protocols. In order to be able to do that, though, it is useful to intuitively understand when to apply the Keypath pattern.

This talk will first explain the different types of keypaths that exist and then go through real world examples to showcase how they can be leveraged in your own projects.

Benedikt Terhechte

March 21, 2019
Tweet

More Decks by Benedikt Terhechte

Other Decks in Programming

Transcript


  1. Introduction to Swift KeyPaths

    View Slide

  2. @terhechte
    お早う!
    2
    # Benedikt Terhechte
    # Twitter: @terhechte
    # Swift Guides: appventure.me
    # Swift Podcast: contravariance.rocks

    View Slide

  3. @terhechte
    What are KeyPaths
    # Swift 4
    # Type-Safe shortcuts to Read / Write properties
    # Composable
    3

    View Slide

  4. Not related to Objective-C's KeyPaths
    [object valueForKeyPath: @“user.address.city.zip"]
    object.valueForKey(#keyPath(Object.firstName))

    View Slide

  5. "
    What do we want to achieve?

    View Slide

  6. struct ProfileSettings {
    var displayName: String
    var shareUpdates: Bool
    var score: Float
    }
    struct PrivacySettings {
    var passcode: Bool
    var addByID: Bool
    var blackList: [String]
    }

    View Slide

  7. protocol SettingsEntry {
    ???
    }

    View Slide

  8. Abstract Over Types
    struct ProfileSettings {
    var displayName: String
    var shareUpdates: Bool
    var score: Float
    }
    struct PrivacySettings {
    var passcode: Bool
    var addByID: Bool
    var blackList: [String]
    }
    String, Bool, Float
    Bool, Bool, [String]

    View Slide

  9. @terhechte
    KeyPaths
    # Protocols make it difficult to abstract over very different
    types
    # KeyPaths allow to do this
    9

    View Slide

  10. @terhechte
    Goal
    # Develop generic app settings
    # Settings of any shape / type
    # To achieve that, we will learn about KeyPaths
    10

    View Slide

  11. @terhechte
    Agenda
    # Intro
    # KeyPath Theory
    # Practical Example
    # Tips And Tricks
    # KeyPath Libraries
    11

    View Slide

  12. Intro
    #

    View Slide

  13. Example:
    player[keyPath: \User.username] = “Link”
    struct User {
    var username: String
    }
    var player = User(username: “Mario”]

    View Slide

  14. Example:
    player[keyPath: \User.username] =
    “Link”

    View Slide

  15. let nameKeyPath = \User.username
    player[keyPath: nameKeyPath] = “Luigi”

    View Slide

  16. Abstract the access to the property "username" into
    a variable that can be moved and stored
    let nameKeyPath = \User.username

    View Slide

  17. \User.username
    KeyPath
    Root: User Value: String
    KeyPath

    View Slide

  18. Nesting:
    struct Address {
    let street: String
    }
    struct User {
    let address: Address
    }
    let x = \User.address.street.count
    KeyPath

    View Slide


  19. Theory

    View Slide

  20. Types of Keypaths
    ➡ ⏪ ↔ ⏺

    View Slide

  21. @terhechte
    KeyPath
    # Read-Only KeyPath with Root and Value
    # let properties
    21

    View Slide

  22. KeyPath
    struct User {
    let username: String
    }
    let kp: KeyPath = \User.username
    let player = User(username: “Mario”)
    print(player[keyPath: kp])
    // Error. Read Only KeyPath
    player[keyPath: kp] = “Luigi"

    View Slide

  23. @terhechte
    WritableKeyPath
    # Read / Write KeyPath
    # var properties
    23

    View Slide

  24. WritableKeyPath
    struct User { var username: String }
    var player = User(username: “Mario”)
    player[keyPath: \User.username] = “Luigi”

    View Slide

  25. @terhechte
    ReferenceWritableKeyPath
    # Read / Write KeyPath for Class types
    # Useful for mutating properties of let roots
    25

    View Slide

  26. @terhechte
    PartialKeyPath
    # KeyPaths of different Values with the same Root
    # Also Read-Only
    26

    View Slide

  27. PartialKeyPath
    // String
    let a: PartialKeyPath = \User.name
    // Float
    let c: PartialKeyPath = \User.quote
    // Int
    let b: PartialKeyPath = \User.age

    View Slide

  28. func acceptKeyPath (_ keyPath: PartialKeyPath) {

    }
    acceptKeyPath(\User.age)
    acceptKeyPath(\User.username)

    View Slide

  29. @terhechte
    AnyKeyPath
    # No Root, no Value
    # Type-Erased KeyPath.
    # Very useful to keep different types together
    29

    View Slide

  30. AnyKeyPath
    let keyPaths: [AnyKeyPath]
    = [\User.username, \String.count]
    KeyPath KeyPath
    AnyKeyPath

    View Slide

  31. @terhechte
    You can cast types back
    # AnyKeyPath as? WritableKeyPath
    # PartialKeyPath as? KeyPath
    31

    View Slide

  32. KeyPath Composition

    View Slide

  33. struct User {
    let address: Address
    }
    struct Address {
    let street: String
    }

    View Slide

  34. KeyPath Composition
    // User -> address
    let addressKeyPath = \User.address
    // Address -> street
    let streetKeyPath = \Address.street
    // User -> address -> street
    let userStreetKeyPath = addressKeyPath
    .appending(path: streetKeyPath)

    View Slide

  35. KeyPath Composition
    let userStreetKeyPath = addressKeyPath
    .appending(path: streetKeyPath)

    View Slide

  36. User ➜ address
    Address ➜ street
    User ➜ street
    +
    =

    View Slide

  37. ☑ Theory

    View Slide


  38. Practical Example

    View Slide

  39. Generic way of handling App
    Settings

    View Slide

  40. View Slide

  41. struct Settings {
    }

    View Slide

  42. struct Settings {
    var profileSettings: ProfileSettings
    var privacySettings: PrivacySettings
    }

    View Slide

  43. struct Settings {
    var profileSettings: ProfileSettings
    var privacySettings: PrivacySettings
    }
    struct ProfileSettings {
    var displayName: String
    var shareUpdates: Bool
    }

    View Slide

  44. struct Settings {
    var profileSettings: ProfileSettings
    var privacySettings: PrivacySettings
    }
    struct ProfileSettings {
    var displayName: String
    var shareUpdates: Bool
    }
    struct PrivacySettings {
    var passcode: Bool
    var addByID: Bool
    }

    View Slide

  45. Title
    Value / Type
    Subtitle

    View Slide

  46. Settings Entry Struct
    struct SettingsEntry {
    let keyPath: AnyKeyPath
    let title: String
    let subtitle: String
    let help: String
    ...
    }

    View Slide

  47. Settings Entry Struct
    struct SettingsEntry {
    let keyPath: AnyKeyPath let title: String
    let subtitle: String
    let help: String
    ...
    }

    View Slide

  48. Settings Entry Struct
    struct SettingsEntry {
    let keyPath: AnyKeyPath
    let title: String let subtitle: String
    let help: String
    ...
    }

    View Slide

  49. Simpler Demo
    struct SettingsEntry {
    let keyPath: AnyKeyPath
    let title: String
    }

    View Slide

  50. Allow types to return settings
    protocol SettingsProvider {
    var settings: [SettingsEntry] { get }
    }

    View Slide

  51. struct Settings {
    var profileSettings: ProfileSettings
    var privacySettings: PrivacySettings
    }

    View Slide

  52. extension Settings: SettingsProvider {
    var settings: [SettingsEntry] {
    return […]
    }
    }

    View Slide

  53. extension Settings: SettingsProvider {
    var settings: [SettingsEntry] {
    return [
    SettingsEntry(
    keyPath: \Settings.profileSettings,
    title: "Profile"),
    SettingsEntry(
    keyPath: \Settings.privacySettings,
    title: "Privacy")
    ]
    }
    }

    View Slide

  54. extension ProfileSettings: SettingsProvider {
    var settings: [SettingsEntry] {
    return […]
    }
    }

    View Slide

  55. extension ProfileSettings: SettingsProvider {
    var settings: [SettingsEntry] {
    return [
    SettingsEntry(
    keyPath: \ProfileSettings.displayName,
    title: "Display Name"),
    SettingsEntry(
    keyPath: \ProfileSettings.shareUpdates,
    title: "Share Profile Media Updates")
    ]
    }
    }

    View Slide

  56. extension PrivacySettings: SettingsProvider {
    var settings: [SettingsEntry] {
    return [
    SettingsEntry(
    keyPath: \PrivacySettings.addByID,
    title: "Allow add me by ID"),
    SettingsEntry(
    keyPath: \PrivacySettings.passcode,
    title: "Passcode Lock")
    ]
    }
    }

    View Slide

  57. func editSettings(provider: inout SettingsProvider) {
    }
    var appSettings = Settings()
    editSettings(provider: &appSettings)

    View Slide

  58. func editSettings(provider: inout SettingsProvider) {
    for setting in provider.settings {
    }
    }

    View Slide

  59. func editSettings(provider: inout SettingsProvider) {
    for setting in provider.settings {
    let value = provider[keyPath: setting.keyPath]
    ...
    }
    }

    View Slide

  60. Nested Providers
    Settings { profileSettings {
    displayName: String,
    shareUpdates: Bool
    },
    ...
    }

    View Slide

  61. Nested Providers
    Settings {
    profileSettings { displayName: String,
    shareUpdates: Bool
    },
    ...
    }

    View Slide

  62. Nested Providers
    Settings {
    profileSettings {
    displayName: String, shareUpdates: Bool
    },
    ...
    }

    View Slide

  63. func editSettings(provider: inout SettingsProvider) {
    for setting in provider.settings {
    let value = provider[keyPath: setting.keyPath]
    if let nested = value as? SettingsProvider {
    ...
    } else {
    ...
    }
    }
    }

    View Slide

  64. func editSettings(provider: inout SettingsProvider) {
    for setting in provider.settings {
    let value = provider[keyPath: setting.keyPath]
    if let nested = value as? SettingsProvider {
    for item in nested.settings {

    View Slide

  65. func editSettings(provider: inout SettingsProvider) {
    for setting in provider.settings {
    let value = provider[keyPath: setting.keyPath]
    if let nested = value as? SettingsProvider {
    for item in nested.settings {
    if let joined =
    keyPath.appending(path: item.keyPath) {

    View Slide

  66. func editSettings(provider: inout SettingsProvider) {
    for setting in provider.settings {
    let value = provider[keyPath: setting.keyPath]
    if let nested = value as? SettingsProvider {
    for item in nested.settings {
    if let joined =
    keyPath.appending(path: item.keyPath) {
    \Settings.PrivacySettings

    View Slide

  67. func editSettings(provider: inout SettingsProvider) {
    for setting in provider.settings {
    let value = provider[keyPath: setting.keyPath]
    if let nested = value as? SettingsProvider {
    for item in nested.settings {
    if let joined =
    keyPath.appending(path: item.keyPath) {
    \PrivacySettings.passcode

    View Slide

  68. func editSettings(provider: inout SettingsProvider) {
    for setting in provider.settings {
    let value = provider[keyPath: keyPath]
    if let nested = value as? SettingsProvider {
    for item in nested.settings {
    if let joined =
    keyPath.appending(path: item.keyPath) {
    updateSetting(keyPath: joined,
    title: item.title)
    ...
    }

    View Slide

  69. Settings ➜ privacySettings
    PrivacySettings ➜ passcode
    Settings ➜ passcode
    +
    =

    View Slide

  70. func editSettings(provider: inout SettingsProvider) {
    func updateSetting(keyPath: AnyKeyPath,
    title: String) {
    let value = provider[keyPath: setting.keyPath]
    if let nested = value as? SettingsProvider {
    for item in nested.settings {
    if let joined =
    keyPath.appending(path: item.keyPath) {
    updateSetting(keyPath: joined,
    title: item.title)
    ...
    }

    View Slide

  71. ☑ Settings: SettingsProvider {
    ☑ profileSettings: SettingsProvider {
    },
    ☑ privacySettings: SettingsProvider {
    },
    }

    View Slide

  72. ☑ Settings: SettingsProvider {
    ☑ profileSettings: SettingsProvider {
    displayName: String,
    shareUpdates: Bool
    },
    ☑ privacySettings: SettingsProvider {
    },
    }

    View Slide

  73. if let nested = value as? SettingsProvider {
    for item in nested.settings {
    if let joined =
    keyPath.appending(path: item.keyPath) {
    updateSetting(keyPath: joined,
    title: item.title)
    }
    }
    } else {
    }
    }

    View Slide

  74. if let nested = value as? SettingsProvider {
    for item in nested.settings {
    if let joined =
    keyPath.appending(path: item.keyPath) {
    updateSetting(keyPath: joined,
    title: item.title)
    }
    }
    } else {
    if let writable =
    keyPath as? WritableKeyPath {
    provider[keyPath: writable] = true
    }
    }

    View Slide

  75. if let nested = value as? SettingsProvider {
    for item in nested.settings {
    if let joined =
    keyPath.appending(path: item.keyPath) {
    updateSetting(keyPath: joined,
    title: item.title)
    }
    }
    } else {
    if let writable =
    keyPath as? WritableKeyPath {
    titleLabel.text = setting.title
    provider[keyPath: writable] = true
    }
    }

    View Slide

  76. Nested Providers
    Settings {
    profileSettings {
    displayName: String,
    shareUpdates: Bool },
    ...
    }

    View Slide

  77. The Final Code:

    View Slide

  78. func editSettings(provider: inout Root) {
    func updateSetting(keyPath: AnyKeyPath, title: String) {
    let value = provider[keyPath: keyPath]
    if let nestedProvider = value as? SettingsProvider {
    for item in nestedProvider.settings {
    if let joined = keyPath.appending(path: item.keyPath) {
    updateSetting(keyPath: joined, title: item.title)
    }
    }
    } else if let writable = keyPath as? WritableKeyPath {
    provider[keyPath: writable] = true
    }
    }
    for setting in provider.settings {
    updateSetting(keyPath: setting.keyPath, title: setting.title)
    }
    }

    View Slide

  79. What did we achieve?

    View Slide

  80. View Slide

  81. @terhechte
    Abstract over properties
    # Types of any shape can now be combined into our settings
    # Can have generic UI elements to read / write the types
    # They just need to describe themselves via [SettingEntry]
    81

    View Slide


  82. Three Tips for Using
    KeyPaths

    View Slide

  83. @terhechte
    1. Choose which Types to Erase
    # KeyPath = \User.age
    # PartialKeyPath = \User.age
    # AnyKeyPath = \User.age
    83

    View Slide

  84. @terhechte
    2. You can cast types back
    # AnyKeyPath as? WritableKeyPath
    # PartialKeyPath as? KeyPath
    84

    View Slide

  85. @terhechte
    3. KeyPaths conform to Hashable
    # Can be Keys in dictionaries
    # Useful to store more information about Keys
    85

    View Slide

  86. let meta: [PartialKeyPath: String] = [
    ]

    View Slide

  87. let meta: [PartialKeyPath: String] = [
    \User.username: “Your Name”,
    \User.age: “Your Age”
    ]

    View Slide

  88. let meta: [PartialKeyPath: String] = [
    \User.username: “Your Name”,
    \User.age: “Your Age”
    ]
    func renderTitle(keyPath: AnyKeyPath) {
    }
    renderTitle(\User.username)

    View Slide

  89. let meta: [PartialKeyPath: String] = [
    \User.username: “Your Name”,
    \User.age: “Your Age”
    ]
    func renderTitle(keyPath: AnyKeyPath) {
    if let title = meta[keyPath]
    titleField.text = title
    }
    }
    renderTitle(\User.username)

    View Slide


  90. KeyPath Libraries

    View Slide

  91. Kuery by @k_katsumi
    github.com/kishikawakatsumi/Kuery

    View Slide

  92. @terhechte
    Kuery
    # Type-Safe Core Data Queries
    # Core Data without strings
    92

    View Slide

  93. Kuery Example
    // Before:
    NSPredicate(format: "name == %@", “Mario")
    NSPredicate(format: "age > %@", 20)
    // After:
    Query(Person.self).filter(\Person.name == “Mario")
    Query(Person.self).filter(\Person.age > 20)
    github.com/kishikawakatsumi/Kuery

    View Slide

  94. KeyPathKit by @v_pradeilles
    github.com/vincent-pradeilles/KeyPathKit

    View Slide

  95. @terhechte
    KeyPathKit
    # Useful abstractions for easier KeyPath usage
    95

    View Slide

  96. KeyPathKit
    contacts.filter(where: \.lastName == “Webb"
    && \.age < 40)
    contacts.average(of: \.age).rounded()
    contacts.between(\.age, range: 20...30)
    contacts.groupBy(\.lastName)
    github.com/vincent-pradeilles/KeyPathKit

    View Slide

  97. Sorting
    users.sorted(by: .ascending(\.lastName),
    .descending(\.address.city.name))
    github.com/vincent-pradeilles/KeyPathKit

    View Slide


  98. Recap

    View Slide

  99. @terhechte
    What did we learn
    # KeyPaths are type-safe, type-erased, hashable and
    composable
    # Create abstractions not possible with protocols
    99

    View Slide

  100. @terhechte
    More Information
    # https://www.swiftbysundell.com/posts/the-power-of-key-paths-in-swift
    # https://www.klundberg.com/blog/swift-4-keypaths-and-you/
    # https://github.com/vincent-pradeilles/slides/blob/master/iosconfsg-2019-the-underestimated-
    power-of-keypaths.pdf
    # https://blog.slashkeys.com/practical-keypaths-in-swift-220da5ab5950
    # https://edit.theappbusiness.com/using-swift-keypaths-for-beautiful-user-preferences-c83c2f7ea7be
    # https://www.swiftbysundell.com/posts/the-power-of-key-paths-in-swift
    # https://github.com/makskovalko/FormValidation
    100

    View Slide

  101. @terhechte 101
    Simple Swift Guides: www.appventure.me

    View Slide

  102. RIANCE VARIANCE
    NTRA CONTRA
    A PODCAST
    www.contravariance.rocks
    @terhechte @BasThomas
    Swift Weekly Brief

    View Slide

  103. • German Social
    Network
    • Based in Hamburg
    • 15 Mio Users
    • Native Apps on all
    platforms
    We’re Hiring

    View Slide

  104. THANKS!
    ありがとう

    View Slide