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

Swift Smart KeyPath

Chiaote Ni
September 21, 2019

Swift Smart KeyPath

Chiaote Ni

September 21, 2019
Tweet

More Decks by Chiaote Ni

Other Decks in Programming

Transcript

  1. Proposal: SE-0161 Implemented (Swift 4) Defer way to get /

    set property With type information (#keyPath( ) only return ‘Any’)
 Applicable to any type (#keyPath( ) only applicable to NSObjects) \UIView.frame
  2. Proposal: SE-0161 Implemented (Swift 4) Defer way to get /

    set property With type information (#keyPath( ) only return ‘Any’)
 Applicable to any type (#keyPath( ) only applicable to NSObjects) let keyPath = \UIView.frame view[keyPath: keyPath] \UIView.frame
  3. Proposal: SE-0161 Implemented (Swift 4) Defer way to get /

    set property With type information (#keyPath( ) only return ‘Any’)
 Applicable to any type (#keyPath( ) only applicable to NSObjects) let keyPath = \UIView.frame view[keyPath: keyPath] \UIView.frame
  4. Proposal: SE-0161 Implemented (Swift 4) Defer way to get /

    set property With type information (#keyPath( ) only return ‘Any’)
 Applicable to any type (#keyPath( ) only applicable to NSObjects) let keyPath = \UIView.frame view[keyPath: keyPath] let frame = view[keyPath: path] \UIView.frame
  5. Proposal: SE-0161 Implemented (Swift 4) Defer way to get /

    set property With type information (#keyPath( ) only return ‘Any’)
 Applicable to any type (#keyPath( ) only applicable to NSObjects) let keyPath = \UIView.frame view[keyPath: keyPath] let frame = view[keyPath: path] \UIView.frame //frame: CGRect
  6. Proposal: SE-0161 Implemented (Swift 4) Defer way to get /

    set property With type information (#keyPath( ) only return ‘Any’)
 Applicable to any type (#keyPath( ) only applicable to NSObjects) let keyPath = \UIView.frame view[keyPath: keyPath] let frame = view[keyPath: path] struct User { var id: Int } \UIView.frame //frame: CGRect
  7. Proposal: SE-0161 Implemented (Swift 4) Defer way to get /

    set property With type information (#keyPath( ) only return ‘Any’)
 Applicable to any type (#keyPath( ) only applicable to NSObjects) let keyPath = \UIView.frame view[keyPath: keyPath] let frame = view[keyPath: path] struct User { var id: Int } \User.id \UIView.frame //frame: CGRect
  8. \UIView.frame.size \UIView.frame.width struct User { var id: Int } ->

    ReferenceWritableKeyPath -> KeyPath -> WritableKeyPath \User.id
  9. struct Ticket { var price: Int } let tickets =

    [Int](0...5) .compactMap{ Ticket(price: $0) } // [ {price 0}, ..., {price 5} ]
  10. extension Array { } func sum<T: Numeric> (of keyPath: KeyPath<Element,

    T>) -> T { return reduce(0, {$0 += $1[keyPath: keyPath]} ) }
  11. extension Array { } func sum<T: Numeric> (of keyPath: KeyPath<Element,

    T>) -> T { return reduce(0, {$0 += $1[keyPath: keyPath]} ) } tickets.sum(of: \.price) // 15
  12. extension Array { } func sorted<T: Comparable> (_ keyPath: KeyPath<Element,

    T>, by comparation: (T, T) -> Bool) -> [Element] { return sorted(by: { comparation($0[keyPath: keyPath], $1[keyPath: keyPath]) }) }
  13. extension Array { } func sorted<T: Comparable> (_ keyPath: KeyPath<Element,

    T>, by comparation: (T, T) -> Bool) -> [Element] { return sorted(by: { comparation($0[keyPath: keyPath], $1[keyPath: keyPath]) }) } tickets.sorted(\.price, by: >) // {price 5}, ..., {price 1}
  14. extension Array { } func calculate<T: Comparable> (_ keyPath: KeyPath<Element,

    T>, with operation: (T, T) -> T) -> T? { guard !isEmpty else { return nil } var copy = self let initValue = copy.removeFirst()[keyPath: keyPath] return copy.reduce(initValue, { operation($0, $1[keyPath: keyPath]) }) }
  15. extension Array { } func calculate<T: Comparable> (_ keyPath: KeyPath<Element,

    T>, with operation: (T, T) -> T) -> T? { guard !isEmpty else { return nil } var copy = self let initValue = copy.removeFirst()[keyPath: keyPath] return copy.reduce(initValue, { operation($0, $1[keyPath: keyPath]) }) } tickets.calculate(\.price, with: +) // 15
  16. let pswdField: UITextField = .init() pswdField.text = "1234" pswdField .check(\.text,

    with: { $0?.isEmpty == false }) .check(\.text, with: { $0?.count ?? 0 >= 6 }) .check(\.text, with: { $0 != "123456" }) .validate() // false
  17. extension Validation { var currentResult: Bool? { get { return

    objc_getAssociatedObject(self, &AssociaKeys.currResult) as? Bool } set { objc_setAssociatedObject(self, &AssociaKeys.currResult, newValue, .OBJC_ASSOCIATION_RETAIN)} } } extension NSObject: Validation { fileprivate struct AssociaKeys { static var currResult = "currResult" } } protocol Validation: NSObject { }
  18. func check<T>(_ target: KeyPath<Self, T>, with condition: (T) -> Bool)

    -> Self { if currentResult == false { return self } else { self.currentResult = condition(self[keyPath: target]) return self } } protocol Validation: NSObject { } extension Validation { }
  19. func check<T>(_ target: KeyPath<Self, T>, with condition: (T) -> Bool)

    -> Self { if currentResult == false { return self } else { self.currentResult = condition(self[keyPath: target]) return self } } func validate() -> Bool { if let validate = currentResult { currentResult = nil print(validate) return validate } else { return true } } protocol Validation: NSObject { } extension Validation { }
  20. let pswdField: UITextField = .init() pswdField .check(\.text, with: { $0?.isEmpty

    == false }) .check(\.text, with: { $0?.count ?? 0 >= 6 }) .check(\.text, with: { $0 != "123456" }) .validate()
  21. let pswdField: UITextField = .init() pswdField .check(\.text, with: { $0?.isEmpty

    == false }) .check(\.text, with: { $0?.count ?? 0 >= 6 }) .check(\.text, with: { $0 != "123456" }) .check(\.frame.size.width, with: { $0 >= 100 }) .validate()
  22. let pswdField: UITextField = .init() pswdField .check(\.text, with: { $0?.isEmpty

    == false }) .check(\.text, with: { $0?.count ?? 0 >= 8 }) .check(\.text, with: { $0 != "123456" }) // do something else ... pswdField.text = "123456" // do something else again … pswdField.validate() // false
  23. struct ValidationStash<ClassType, T> { var condition: (T) -> Bool var

    keypath: KeyPath<ClassType, T> } protocol Validation: class { associatedtype ClassType associatedtype ValueType var stashes: [ValidationStash <ClassType, ValueType?>]? { set get } } extension Validation { func check( _ target: KeyPath<ClassType, ValueType?>, with condition: @escaping (ValueType? ) -> Bool) -> Self { //... } func validate() -> Bool { //... } }
  24. struct ValidationStash<ClassType, T> { var condition: (T) -> Bool var

    keypath: KeyPath<ClassType, T> } protocol Validation: class { associatedtype ClassType associatedtype ValueType var stashes: [ValidationStash <ClassType, ValueType?>]? { set get } } extension Validation { func check( _ target: KeyPath<ClassType, ValueType?>, with condition: @escaping (ValueType? ) -> Bool) -> Self { //... } func validate() -> Bool { //... } }
  25. func check( _ target: KeyPath<ClassType, ValueType?>, with condition: @escaping (ValueType?

    ) -> Bool) -> Self { let model = ValidationStash(condition: condition, keypath: target) if var array = stashes { array.append(model) stashes = array } else { stashes = [model] } return self }
  26. func validate() -> Bool { for stash in stashes ??

    [] { guard let self = self as? ClassType else { continue } let target = self[keyPath: stash.keypath] let result = stash.condition(target) if result == false { return result } } return true }
  27. extension UITextField: Validation { typealias ClassType = UITextField typealias ValueType

    = String var stashes: [ValidationStash<ClassType, ValueType?>]? { get { return objc_getAssociatedObject(self, &AssociatedKeys.array) as? [ValidationStash] } set { objc_setAssociatedObject(self, &AssociatedKeys.array, newValue, .OBJC_ASSOCIATION_RETAIN)} } fileprivate struct AssociatedKeys { static var array = “AssociatedKeys_array" } }
  28. userField.validate() // false pswdField.validate() // true let userField: UITextField =

    .init() userField .check(\.text, with: { $0?.isEmpty == false }) .check(\.text, with: { $0?.count ?? 0 >= 8 }) .check(\.text, with: { $0 != "123456" }) let pswdField: UITextField = .init() pswdField .check(\.text, with: { $0?.isEmpty == false }) .check(\.text, with: { $0?.count ?? 0 >= 6 }) .check(\.text, with: { $0 != "123456" }) userField.text = "123456" pswdField.text = "demo123456"
  29. userField.validate() // false pswdField.validate() // true let userField: UITextField =

    .init() userField .check(\.text, with: { $0?.isEmpty == false }) .check(\.text, with: { $0?.count ?? 0 >= 8 }) .check(\.text, with: { $0 != "123456" }) let pswdField: UITextField = .init() pswdField .check(\.text, with: { $0?.isEmpty == false }) .check(\.text, with: { $0?.count ?? 0 >= 6 }) .check(\.text, with: { $0 != "123456" }) userField.text = "123456" pswdField.text = "demo123456"
  30. userField.validate() // false pswdField.validate() // true let userField: UITextField =

    .init() userField .check(\.text, with: { $0?.isEmpty == false }) .check(\.text, with: { $0?.count ?? 0 >= 8 }) .check(\.text, with: { $0 != "123456" }) let pswdField: UITextField = .init() pswdField .check(\.text, with: { $0?.isEmpty == false }) .check(\.text, with: { $0?.count ?? 0 >= 6 }) .check(\.text, with: { $0 != "123456" }) userField.text = "123456" pswdField.text = "demo123456"
  31. var label = UILabel() .set(\.text, to: "Hello World.") .set(\.font, to:

    .systemFont(ofSize: 14)) .set(\.lineBreakMode, to: .byWordWrapping) .set(\.numberOfLines, to: 0) .set(\.textColor, to: .darkGray) func set<T>(_ keyPath: ReferenceWritableKeyPath<Self, T>, to value: T) -> Self { self[keyPath: keyPath] = value return self } Reference: http://www.swifttube.co/video/the-underestimated-power-of- keypaths
  32. var label = UILabel() .set(\.text, to: "Hello World.") .set(\.font, to:

    .systemFont(ofSize: 14)) .set(\.lineBreakMode, to: .byWordWrapping) .set(\.numberOfLines, to: 0) .set(\.textColor, to: .darkGray) func set<T>(_ keyPath: ReferenceWritableKeyPath<Self, T>, to value: T) -> Self { self[keyPath: keyPath] = value return self } Reference: http://www.swifttube.co/video/the-underestimated-power-of- keypaths Easy to implement Fluent Interface!
  33. var label = UILabel() .set(\.text, to: "Hello World.") .set(\.font, to:

    .systemFont(ofSize: 14)) .set(\.lineBreakMode, to: .byWordWrapping) .set(\.numberOfLines, to: 0) .set(\.textColor, to: .darkGray) func set<T>(_ keyPath: ReferenceWritableKeyPath<Self, T>, to value: T) -> Self { self[keyPath: keyPath] = value return self } Reference: http://www.swifttube.co/video/the-underestimated-power-of- keypaths Easy to implement Fluent Interface! -> Self -> Self -> Self -> Self -> Self
  34. descLabel .translatesAutoresizingMaskIntoConstraints = false let constraints = [ descLabel.widthAnchor .constraint(equalTo:

    view.widthAnchor, multiplier: 0.9, constant: 10), descLabel.heightAnchor .constraint(greaterThanOrEqualTo: view.heightAnchor), descLabel.centerYAnchor .constraint(equalTo: view.centerYAnchor, constant: 0), descLabel.centerXAnchor .constraint(equalTo: view.centerXAnchor, constant: 0) ] NSLayoutConstraint.activate(constraints)
  35. nameLabel.translatesAutoresizingMaskIntoConstraints = false nameLabel.widthAnchor.constraint(equalTo: view.widthAnchor) .isActive = true nameLabel.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor).isActive

    = true nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
  36. nameLabel.translatesAutoresizingMaskIntoConstraints = false nameLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2, constant: 10).isActive =

    true nameLabel.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor, multiplier: 0.2).isActive = true nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true
  37. nameLabel .setAnchor(\.widthAnchor, .equal, to: view.widthAnchor, constant: 10, multiplier: 0.2) .setAnchor(\.heightAnchor,

    .greaterThanOrEqual, to: view.heightAnchor, multiplier: 0.2) .setAnchor(\.topAnchor, .equal, to: view.topAnchor) .setAnchor(\.centerXAnchor, .equal, to:view.centerXAnchor)
  38. let topConstraint = label.setConstraint(\.topAnchor, .equal, to: view.topAnchor) nameLabel .setAnchor(\.widthAnchor, .equal,

    to: view.widthAnchor, constant: 10, multiplier: 0.2) .setAnchor(\.heightAnchor, .greaterThanOrEqual, to: view.heightAnchor, multiplier: 0.2) .setAnchor(\.topAnchor, .equal, to: view.topAnchor) .setAnchor(\.centerXAnchor, .equal, to:view.centerXAnchor)
  39. ( _ keyPath: KeyPath<UIView, LayoutType>, _ relation: NSLayoutConstraint.Relation, to anchor:

    LayoutType, ) -> NSLayoutConstraint { translatesAutoresizingMaskIntoConstraints = false return constraint } constant: CGFloat = 0, multiplier: CGFloat? = nil, priority: UILayoutPriority = .required if let multiplier = multiplier, //... } else { //... } @discardableResult func setConstraint let constraint: NSLayoutConstraint constraint.priority = priority constraint.isActive = true <LayoutType: NSLayoutAnchor<AnchorType>, AnchorType>
  40. ( _ keyPath: KeyPath<UIView, LayoutType>, _ relation: NSLayoutConstraint.Relation, to anchor:

    LayoutType, ) -> NSLayoutConstraint { translatesAutoresizingMaskIntoConstraints = false return constraint } constant: CGFloat = 0, multiplier: CGFloat? = nil, priority: UILayoutPriority = .required if let multiplier = multiplier, //... } else { //... } @discardableResult func setConstraint let constraint: NSLayoutConstraint constraint.priority = priority constraint.isActive = true <LayoutType: NSLayoutAnchor<AnchorType>, AnchorType>
  41. ( _ keyPath: KeyPath<UIView, LayoutType>, _ relation: NSLayoutConstraint.Relation, to anchor:

    LayoutType, ) -> NSLayoutConstraint { translatesAutoresizingMaskIntoConstraints = false return constraint } constant: CGFloat = 0, multiplier: CGFloat? = nil, priority: UILayoutPriority = .required if let multiplier = multiplier, //... } else { //... } @discardableResult func setConstraint let constraint: NSLayoutConstraint constraint.priority = priority constraint.isActive = true <LayoutType: NSLayoutAnchor<AnchorType>, AnchorType>
  42. ( _ keyPath: KeyPath<UIView, LayoutType>, _ relation: NSLayoutConstraint.Relation, to anchor:

    LayoutType, ) -> NSLayoutConstraint { translatesAutoresizingMaskIntoConstraints = false return constraint } constant: CGFloat = 0, multiplier: CGFloat? = nil, priority: UILayoutPriority = .required if let multiplier = multiplier, //... } else { //... } @discardableResult func setConstraint let constraint: NSLayoutConstraint constraint.priority = priority constraint.isActive = true <LayoutType: NSLayoutAnchor<AnchorType>, AnchorType>
  43. if let multiplier = multiplier, let dimension = self[keyPath: keyPath]

    as? NSLayoutDimension, let anchor = anchor as? NSLayoutDimension { //... } else { //... } to anchor: LayoutType, constant: CGFloat = 0, multiplier: CGFloat? = nil, priority: UILayoutPriority = .required @discardableResult ( _ keyPath: KeyPath<UIView, LayoutType>, _ relation: NSLayoutConstraint.Relation, to anchor: LayoutType, ) -> NSLayoutConstraint { translatesAutoresizingMaskIntoConstraints = false constant: CGFloat = 0, multiplier: CGFloat? = nil, priority: UILayoutPriority = .required @discardableResult func setConstraint let constraint: NSLayoutConstraint constraint.priority = priority constraint.isActive = true <LayoutType: NSLayoutAnchor<AnchorType>, AnchorType>
  44. if let multiplier = multiplier, let dimension = self[keyPath: keyPath]

    as? NSLayoutDimension, let anchor = anchor as? NSLayoutDimension { //... } else { //... } to anchor: LayoutType, constant: CGFloat = 0, multiplier: CGFloat? = nil, priority: UILayoutPriority = .required @discardableResult ( _ keyPath: KeyPath<UIView, LayoutType>, _ relation: NSLayoutConstraint.Relation, to anchor: LayoutType, ) -> NSLayoutConstraint { translatesAutoresizingMaskIntoConstraints = false constant: CGFloat = 0, multiplier: CGFloat? = nil, priority: UILayoutPriority = .required @discardableResult func setConstraint let constraint: NSLayoutConstraint constraint.priority = priority constraint.isActive = true <LayoutType: NSLayoutAnchor<AnchorType>, AnchorType>
  45. if let multiplier = multiplier, let dimension = self[keyPath: keyPath]

    as? NSLayoutDimension, let anchor = anchor as? NSLayoutDimension { switch relation { //NSLayoutDimension //... } } else { switch relation { //NSLayoutAnchor //... } }
  46. switch relation { //NSLayoutDimension case .equal: constraint = dimension .constraint(equalTo:

    anchor, multiplier:multiplier, constant: constant) case .greaterThanOrEqual: constraint = dimension .constraint(greaterThanOrEqualTo: anchor, multiplier: multiplier, constant: constant) case .lessThanOrEqual: constraint = dimension .constraint(lessThanOrEqualTo: anchor, multiplier: multiplier, constant: constant) }
  47. switch relation { //NSLayoutAnchor case .equal: constraint = self[keyPath: keyPath]

    .constraint(equalTo: anchor, constant: constant) case .greaterThanOrEqual: constraint = self[keyPath: keyPath] .constraint(greaterThanOrEqualTo: anchor, constant: constant) case .lessThanOrEqual: constraint = self[keyPath: keyPath] .constraint(lessThanOrEqualTo: anchor, constant: constant) }
  48. @discardableResult <LayoutType: NSLayoutAnchor<AnchorType>, AnchorType>( ) -> Self { setConstraint(keyPath, relation,

    to: anchor, constant: constant, multiplier: multiplier, priority: priority) return self } _ keyPath: KeyPath<UIView, LayoutType>, _ relation: NSLayoutConstraint.Relation, to anchor: LayoutType, constant: CGFloat = 0, multiplier: CGFloat? = nil, priority: UILayoutPriority = .required func setAnchor
  49. @discardableResult <LayoutType: NSLayoutAnchor<AnchorType>, AnchorType>( ) -> Self { setConstraint(keyPath, relation,

    to: anchor, constant: constant, multiplier: multiplier, priority: priority) return self } _ keyPath: KeyPath<UIView, LayoutType>, _ relation: NSLayoutConstraint.Relation, to anchor: LayoutType, constant: CGFloat = 0, multiplier: CGFloat? = nil, priority: UILayoutPriority = .required func setAnchor
  50. nameLabel.translatesAutoresizingMaskIntoConstraints = false nameLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2, constant: 10) .isActive

    = true nameLabel.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor, multiplier: 0.2).isActive = true nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true descLabel.translatesAutoresizingMaskIntoConstraints = false descLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9, constant: 10) .isActive = true descLabel.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor) .isActive = true descLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true descLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true
  51. nameLabel .setAnchor(\.widthAnchor, .equal, to: view.widthAnchor, constant: 10, multiplier: 0.2) .setAnchor(\.heightAnchor,

    .greaterThanOrEqual, to: view.heightAnchor, multiplier: 0.2) .setAnchor(\.centerXAnchor, .equal, to: view.centerXAnchor) .setAnchor(\.topAnchor, .equal, to: view.topAnchor) descLabel .setAnchor(\.widthAnchor, .equal, to: view.widthAnchor, multiplier: 0.2) .setAnchor(\.heightAnchor, .greaterThanOrEqual, to: view.heightAnchor) .setAnchor(\.centerXAnchor, .equal, to: view.centerXAnchor) .setAnchor(\.centerYAnchor, .equal, to: view.centerYAnchor)
  52. • Swift KeyPath is a defer way to get /

    set property with Type safety
  53. • Swift KeyPath is a defer way to get /

    set property with Type safety • Better Key-Value Coding for Swift (Compared with #KeyPath( ) )
  54. • Swift KeyPath is a defer way to get /

    set property with Type safety • Better Key-Value Coding for Swift • Create interface more flexible than ever (Compared with #KeyPath( ) )
  55. • Swift KeyPath is a defer way to get /

    set property with Type safety • Better Key-Value Coding for Swift • Create interface more flexible than ever (Compared with #KeyPath( ) ) • Easy to implement Fluent Interface!
  56. WWDC17 - What's New in Foundation https://developer.apple.com/videos/play/wwdc2017/212/ Proposal: SE-0161 https://github.com/apple/swift-evolution/blob/master/proposals/0161-key-paths.md

    Proposal SE-0252 https://github.com/apple/swift-evolution/blob/master/proposals/0252-keypath- dynamic-member-lookup.md Introduction to Swift Keypaths - Benedikt Terhechte
 http://www.swifttube.co/video/the-underestimated-power-of-keypaths The underestimated power of KeyPaths - Vincent Pradeilles http://www.swifttube.co/video/the-underestimated-power-of-keypaths FluentInterface https://martinfowler.com/bliki/FluentInterface.html Builder, Fluent Interface and classic builder https://medium.com/@sawomirkowalski/design-patterns-builder-fluent-interface- and-classic-builder-d16ad3e98f6c Reference
  57. Proposal SE-0249 https://github.com/apple/swift-evolution/blob/master/proposals/0249-key-path-literal- function-expressions.md KeyPathKit - Vincent Pradeilles https://github.com/vincent-pradeilles/KeyPathKit The

    power of key paths in Swift - Sundell https://www.swiftbysundell.com/articles/the-power-of-key-paths-in-swift/ Using Swift KeyPaths for Beautiful User Preferences - Kane https://edit.theappbusiness.com/using-swift-keypaths-for-beautiful-user- preferences-c83c2f7ea7be More Information