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

Swift Smart KeyPath

Avatar for Chiaote Ni Chiaote Ni
September 21, 2019

Swift Smart KeyPath

Avatar for Chiaote Ni

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