• Stars
    star
    420
  • Rank 99,349 (Top 3 %)
  • Language
    Swift
  • License
    MIT License
  • Created about 6 years ago
  • Updated over 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

KeyPathKit is a library that provides the standard functions to manipulate data along with a call-syntax that relies on typed keypaths to make the call sites as short and clean as possible.

KeyPathKit

Build Status platforms pod Carthage compatible Swift Package Manager compatible

Context

Swift 4 has introduced a new type called KeyPath, with allows to access the properties of an object with a very nice syntax. For instance:

let string = "Foo"
let keyPathForCount = \String.count

let count = string[keyPath: keyPathForCount] // count == 3

The great part is that the syntax can be very concise, because it supports type inference and property chaining.

Purpose of KeyPathKit

Consequently, I thought it would be nice to leverage this new concept in order to build an API that allows to perform data manipulation in a very declarative fashion.

SQL is a great language for such manipulations, so I took inspiration from it and implemented most of its standard operators in Swift 4 using KeyPath.

But what really stands KeyPathKit appart from the competition is its clever syntax that allows to express queries in a very seamless fashion. For instance :

contacts.filter(where: \.lastName == "Webb" && \.age < 40)

Installation

CocoaPods

Add the following to your Podfile:

pod "KeyPathKit"

Carthage

Add the following to your Cartfile:

github "vincent-pradeilles/KeyPathKit"

Swift Package Manager

Create a file Package.swift:

// swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "YourProject",
    dependencies: [
        .package(url: "https://github.com/vincent-pradeilles/KeyPathKit.git", "1.0.0" ..< "2.0.0")
    ],
    targets: [
        .target(name: "YourProject", dependencies: ["KeyPathKit"])
    ]
)

Operators

Operator details

For the purpose of demonstrating the usage of the operators, the following mock data is defined:

struct Person {
    let firstName: String
    let lastName: String
    let age: Int
    let hasDriverLicense: Bool
    let isAmerican: Bool
}

let contacts = [
    Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
    Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
    Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
    Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
    Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
    Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
    Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)
]

and

Performs a boolean AND operation on a property of type Bool.

contacts.and(\.hasDriverLicense)
contacts.and(\.isAmerican)
false
true

average

Calculates the average of a numerical property.

contacts.average(of: \.age).rounded()
25

between

Filters out elements whose value for the property is not within the range.

contacts.between(\.age, range: 20...30)
// or
contacts.filter(where: 20...30 ~= \.age)
[Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

contains

Returns whether the sequence contains one element for which the specified boolean property or predicate is true.

contacts.contains(where: \.hasDriverLicense)
contacts.contains(where: \.lastName.count > 10)
true
false

distinct

Returns all the distinct values for the property.

contacts.distinct(\.lastName)
["Webb", "Elexson", "Zunino", "Alexson"]

drop

Returns a subsequence by skipping elements while a property of type Bool or a predicate evaluates to true, and returning the remaining elements.

contacts.drop(while: \.age < 40)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

filter

Filters out elements whose value is false for one (or several) boolean property.

contacts.filter(where: \.hasDriverLicense)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

Filter also works with predicates:

contacts.filter(where: \.firstName == "Webb")
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
 Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)]

filterIn

Filters out elements whose value for an Equatable property is not in a given Sequence.

contacts.filter(where: \.firstName, in: ["Alex", "John"])
[Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)]

filterLess

Filters out elements whose value is greater than a constant for a Comparable property.

contacts.filter(where: \.age, lessThan: 30)
// or
contacts.filter(where: \.age < 30)
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)]
contacts.filter(where: \.age, lessOrEqual: 30)
// or
contacts.filter(where: \.age <= 30)
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

filterLike

Filters out elements whose value for a string property does not match a regular expression.

contacts.filter(where: \.lastName, like: "^[A-Za-z]*son$")
[Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

filterMore

Filters out elements whose value is lesser than a constant for a Comparable property.

contacts.filter(where: \.age, moreThan: 30)
// or
contacts.filter(where: \.age > 30)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true)]
contacts.filter(where: \.age, moreOrEqual: 30)
// or
contacts.filter(where: \.age >= 30)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]

first

Returns the first element matching a predicate.

contacts.first(where: \.lastName == "Webb")
Optional(Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true))

groupBy

Groups values by equality on the property.

contacts.groupBy(\.lastName)
["Alexson": [Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true)], 
 "Webb": [Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)], 
 "Elexson": [Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)], 
 "Zunino": [Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true)]]

join

Joins values of two sequences in tuples by the equality on their respective property.

contacts.join(\.firstName, with: contacts, on: \.lastName)
// or
contacts.join(with: contacts, where: \.firstName == \.lastName)
[(Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true)), 
 (Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true)), 
 (Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true))]

Joining on more than one attribute is also supported:

contacts.join(with: contacts, .where(\.firstName, equals: \.lastName), .where(\.hasDriverLicense, equals: \.isAmerican))
// or
contacts.join(with: contacts, where: \.firstName == \.lastName, \.hasDriverLicense == \.isAmerican)

map

Maps elements to their values of the property.

contacts.map(\.lastName)
["Webb", "Elexson", "Webb", "Zunino", "Alexson", "Webb", "Elexson"]

mapTo

Maps a sequence of properties to a function. This is, for instance, useful to extract a subset of properties into a structured type.

struct ContactCellModel {
    let firstName: String
    let lastName: String
}

contacts.map(\.lastName, \.firstName, to: ContactCellModel.init)
[ContactCellModel(firstName: "Webb", lastName: "Charlie"), 
 ContactCellModel(firstName: "Elexson", lastName: "Alex"), 
 ContactCellModel(firstName: "Webb", lastName: "Charles"), 
 ContactCellModel(firstName: "Zunino", lastName: "Alex"), 
 ContactCellModel(firstName: "Alexson", lastName: "Alex"), 
 ContactCellModel(firstName: "Webb", lastName: "John"), 
 ContactCellModel(firstName: "Elexson", lastName: "Webb")]

max

Returns the element with the greatest value for a Comparable property.

contacts.max(by: \.age)
contacts.max(\.age)
Optional(Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true))
Optional(45)

min

Returns the element with the minimum value for a Comparable property.

contacts.min(by: \.age)
contacts.min(\.age)
Optional(Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true))
Optional(8)

or

Performs a boolean OR operation on an property of type Bool.

contacts.or(\.hasDriverLicense)
true

patternMatching

Allows the use of predicates inside a switch statement:

switch person {
case \.firstName == "Charlie":
    print("I'm Charlie!")
    fallthrough
case \.age < 18:
    print("I'm not an adult...")
    fallthrough
default:
    break
}

prefix

Returns a subsequence containing the initial, consecutive elements for whose a property of type Bool or a predicate evaluates to true.

contacts.prefix(while: \.age < 40)
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
 Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true)]

sum

Calculates the sum of the values for a numerical property.

contacts.sum(of: \.age)
177

sort

Sorts the elements with respect to a Comparable property.

contacts.sorted(by: \.age)
[Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true)]

It's also possible to specify the sorting order, to sort on multiple criteria, or to do both.

contacts.sorted(by: .ascending(\.lastName), .descending(\.age))
[Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true), 
 Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), 
 Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true)]

Author

Thanks

A big thank you to JΓ©rΓ΄me Alves (elegantswift.com) for coming up with the right modelization to allow sorting on multiple properties with heterogenous type.

More Repositories

1

swift-tips

A collection useful tips for the Swift language
Swift
979
star
2

locatable

A micro-framework that leverages Swift Property Wrappers to implement the Service Locator pattern
Swift
117
star
3

weakable-self

A Swift micro-framework to easily deal with weak references to self inside closures
Swift
78
star
4

KeyPathTesting

A Swift framework that defines a KeyPath-based syntax for writing unit tests
Swift
71
star
5

AutoMocker

AutoMocker is a Swift framework that leverages the type system to let you easily create mocked instances of your data types.
Swift
43
star
6

slides

37
star
7

ios-memes

31
star
8

learning-swiftui-when-you-already-know-uikit

Learning SwiftUI, when you already know UIKit
Swift
19
star
9

live-ios-app-architecture

18
star
10

kotlin-tips

A collection useful tips for the Kotlin language
Kotlin
13
star
11

live-async-await-ios13

Swift
11
star
12

workshop-nsspain-2019

Leveraging KeyPaths to their full potential
Swift
10
star
13

live-async-algorithms

9
star
14

NSAttributedStringBuilder

A micro-framework that exposes a function builder to compose attributed strings
Swift
8
star
15

how-to-master-combine-like-a-pro

Swift
8
star
16

euler-kotlin

Standard mathematical operations implemented through type extensions and infix functions
Kotlin
7
star
17

swift-playgrounds-background-remover-app

This is the repository with the code for this livestream πŸ‘‰ https://www.youtube.com/watch?v=gLn_0nan-pw
Swift
5
star
18

experimenting-with-createml-resources

Swift
3
star
19

live-building-your-first-swiftui-app

Swift
3
star
20

learning-uikit-when-you-already-know-swiftui

Swift
3
star
21

swift-island-2023

Swift
2
star
22

how-to-master-modern-swift-syntaxes-keypath

Swift
2
star
23

parallelize-blocking-api

Swift
2
star
24

live-discovering-swift-macros

Swift
1
star
25

swift-playgrounds

Swift
1
star
26

LocaLint

A command-line tool to lint the usage of iOS localized Strings
Swift
1
star
27

reflexive-factory

Swift
1
star
28

live-discovering-swift-data

Swift
1
star
29

loader-table-view

Swift
1
star
30

how-to-get-started-with-swift-macros

How to get started with Swift Macros
Swift
1
star
31

from-async-rx-api

Swift
1
star
32

vincent-pradeilles.github.io

CSS
1
star