Slide 1

Slide 1 text

Introduction to Swift KeyPaths

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

" What do we want to achieve?

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

protocol SettingsEntry { ??? }

Slide 8

Slide 8 text

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]

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Intro #

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Theory

Slide 20

Slide 20 text

Types of Keypaths ➡ ⏪ ↔ ⏺

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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"

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

func acceptKeyPath (_ keyPath: PartialKeyPath) { … } acceptKeyPath(\User.age) acceptKeyPath(\User.username)

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

KeyPath Composition

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

User ➜ address Address ➜ street User ➜ street + =

Slide 37

Slide 37 text

☑ Theory

Slide 38

Slide 38 text

Practical Example

Slide 39

Slide 39 text

Generic way of handling App Settings

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

struct Settings { }

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Title Value / Type Subtitle

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Settings Entry Struct struct SettingsEntry { let keyPath: AnyKeyPath <—\PrivacySettings.passcode let title: String let subtitle: String let help: String ... }

Slide 48

Slide 48 text

Settings Entry Struct struct SettingsEntry { let keyPath: AnyKeyPath let title: String <— “Lock with Passcode” let subtitle: String let help: String ... }

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Nested Providers Settings { <--- Here `[SettingsEntry]` profileSettings { displayName: String, shareUpdates: Bool }, ... }

Slide 61

Slide 61 text

Nested Providers Settings { profileSettings { <--- Here `[SettingsEntry]` displayName: String, shareUpdates: Bool }, ... }

Slide 62

Slide 62 text

Nested Providers Settings { profileSettings { displayName: String, <--- Here `String` shareUpdates: Bool }, ... }

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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 {

Slide 65

Slide 65 text

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) {

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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) ... }

Slide 69

Slide 69 text

Settings ➜ privacySettings PrivacySettings ➜ passcode Settings ➜ passcode + =

Slide 70

Slide 70 text

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) ... }

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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 { } }

Slide 74

Slide 74 text

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 } }

Slide 75

Slide 75 text

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 } }

Slide 76

Slide 76 text

Nested Providers Settings { profileSettings { displayName: String, shareUpdates: Bool <--- Set to true }, ... }

Slide 77

Slide 77 text

The Final Code:

Slide 78

Slide 78 text

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) } }

Slide 79

Slide 79 text

What did we achieve?

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

@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

Slide 82

Slide 82 text

Three Tips for Using KeyPaths

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

let meta: [PartialKeyPath: String] = [ ]

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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)

Slide 90

Slide 90 text

KeyPath Libraries

Slide 91

Slide 91 text

Kuery by @k_katsumi github.com/kishikawakatsumi/Kuery

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

@terhechte KeyPathKit # Useful abstractions for easier KeyPath usage 95

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

☑ Recap

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

@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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

THANKS! ありがとう