"Common mistakes discovered through code-review" by Dmitriy Vorona

"Common mistakes discovered through code-review" by Dmitriy Vorona

During last few years Dmitriy has made numerous code-reviews and project audits and discovered lots of mistakes in iOS projects. He've gathered the most common ones and want to share them with you. They vary a lot, from stylistics to design, architecture and concept issues. In his talk Dmitry will show alternative ways to solve same problems along and avoid most common mistakes. This should improve quality of your projects, make them tidier and neater.

This talk was made for CocoaHeads Kyiv #11 which took place March 04 2017.

Db84cf61fdada06b63f43f310b68b462?s=128

CocoaHeads Ukraine

March 09, 2017
Tweet

Transcript

  1. Common mistakes discovered through code review Dima Vorona, iOS Dev

  2. Agenda

  3. Agenda • Style and semantics • Language • Code/SDK behavior

    understanding • No-tests-ever design • Database misuse • Files management • One target - one love • Model versioning
  4. Agenda • Style and semantics • Language • Code/SDK behavior

    understanding • No-tests-ever design • Database misuse • Files management • One target - one love • Model versioning
  5. Agenda • Style and semantics • Language • Code/SDK behavior

    understanding • No-tests-ever design • Database misuse • Files management • One target - one love • Model versioning
  6. Agenda • Style and semantics • Language • Code/SDK behavior

    understanding • No-tests-ever design • Database misuse • Files management • One target - one love • Model versioning
  7. Agenda • Style and semantics • Language • Code/SDK behavior

    understanding • No-tests-ever design • Database misuse • Files management • One target - one love • Model versioning
  8. Agenda • Style and semantics • Language • Code/SDK behavior

    understanding • No-tests-ever design • Database misuse • Files management • One target - one love • Model versioning
  9. Agenda • Style and semantics • Language • Code/SDK behavior

    understanding • No-tests-ever design • Database misuse • Files management • One target • Model versioning
  10. Agenda • Style and semantics • Language • Code/SDK behavior

    understanding • No-tests-ever design • Database misuse • Files management • One target • Model versioning
  11. Style and semantic

  12. Style inconsistency Does it really matter? Style and semantics

  13. Style inconsistency class SomeClass{ let my_property = 0 private(set)var anotherIntProperty:

    Int = 1 init(){} func apply(value: Int){ if (value<=self.anotherIntProperty && value >= my_property) { print("Applied") }else{ print("Not applied") } } func printHello() { if my_property > 0{ print("Hello") } } } Does it really matter? Style and semantics
  14. Style inconsistency Style and semantics Impact

  15. Style inconsistency • Distracts your attention to understand Style and

    semantics Impact
  16. Style inconsistency • Distracts your attention to understand • Encourages

    «I don’t give a shit» attitude (Broken windows theory) Style and semantics Impact
  17. Style inconsistency • Distracts your attention to understand • Encourages

    «I don’t give a shit» attitude (Broken windows theory) • Unprofessional look and feel Style and semantics Impact
  18. Style inconsistency Style and semantics Solution

  19. Style inconsistency • Understand importance of consistency (must!) Style and

    semantics Solution
  20. Style inconsistency • Understand importance of consistency (must!) • Define

    or use coding guidelines consistently per (at least) File / Project Style and semantics Solution
  21. Style inconsistency • Understand importance of consistency (must!) • Define

    or use coding guidelines consistently per (at least) File / Project • Discuss with team - PR rejection is not enough Style and semantics Solution
  22. Style inconsistency • Understand importance of consistency (must!) • Define

    or use coding guidelines consistently per (at least) File / Project • Discuss with team - PR rejection is not enough • Use automation tools: SwiftLint, SwiftFormat Style and semantics Solution
  23. Single process vs multiple terms Style and semantics

  24. Single process vs multiple terms protocol UserModelDelegate: class { func

    modelDidUpdate(_ model: UserModelDelegate) func model(_ model: UserModelDelegate, didFailWithError: Error) } class UserModel { weak var delegate: UserModelDelegate! var user: User { } var features: [Feature] { } func loadData() { /* call API */ } } Style and semantics
  25. Single process vs multiple terms protocol UserModelDelegate: class { func

    modelDidUpdate(_ model: UserModelDelegate) func model(_ model: UserModelDelegate, didFailWithError: Error) } class UserModel { weak var delegate: UserModelDelegate! var user: User { } var features: [Feature] { } func loadData() { /* call API */ } } Style and semantics
  26. Single process vs multiple terms protocol UserModelDelegate: class { func

    modelDidUpdate(_ model: UserModelDelegate) func model(_ model: UserModelDelegate, didFailWithError: Error) } class UserModel { weak var delegate: UserModelDelegate! var user: User { } var features: [Feature] { } func loadData() { /* call API */ } } // Later in UserViewController.swift @IBAction func refreshControlValueChanged(_ sender: UIRefreshControl) { model.loadData() } Style and semantics
  27. Single process vs multiple terms protocol UserModelDelegate: class { func

    modelDidUpdate{What?}(_ model: UserModelDelegate) func model(_ model: UserModelDelegate, didFailWithError: Error) } class UserModel { weak var delegate: UserModelDelegate! var user: User { } var features: [Feature] { } func loadData() { /* call API */ } } // Later in UserViewController.swift @IBAction func refreshControlValueChanged(_ sender: UIRefreshControl) { model.loadData() } Style and semantics
  28. Single process vs multiple terms protocol UserModelDelegate: class { func

    modelDidUpdate{What?}(_ model: UserModelDelegate) func model(_ model: UserModelDelegate, didFail{To do what?}WithError: Error) } class UserModel { weak var delegate: UserModelDelegate! var user: User { } var features: [Feature] { } func loadData() { /* call API */ } } // Later in UserViewController.swift @IBAction func refreshControlValueChanged(_ sender: UIRefreshControl) { model.loadData() } Style and semantics
  29. Single process vs multiple terms protocol UserModelDelegate: class { func

    modelDidUpdate{What?}(_ model: UserModelDelegate) func model(_ model: UserModelDelegate, didFail{To do what?}WithError: Error) } class UserModel { weak var delegate: UserModelDelegate! var user: User { } var features: [Feature] { } func loadData() { /* call API */ } } // Later in UserViewController.swift @IBAction func refreshControlValueChanged(_ sender: UIRefreshControl) { // Recurring action invokes one-off method model.loadData() } Style and semantics
  30. Single process vs multiple terms protocol UserModelDelegate: class { func

    modelDidUpdate{What?}(_ model: UserModelDelegate) func model(_ model: UserModelDelegate, didFail{To do what?}WithError: Error) } class UserModel { weak var delegate: UserModelDelegate! var user: User { } var features: [Feature] { } // False expectations: method can be used repeatedly func loadData() { /* call API */ } } // Later in UserViewController.swift @IBAction func refreshControlValueChanged(_ sender: UIRefreshControl) { // Recurring action invokes one-off method model.loadData() } Style and semantics
  31. Single process = single term protocol UserModelDelegate: class { func

    modelDidReload(_ model: UserModelDelegate) func model(_ model: UserModelDelegate, didFailToReloadWithError error: Error) } class UserModel { weak var delegate: UserModelDelegate! var user: User { } var features: [Feature] { } func reload() { /* call API */ } } // Later in UserViewController.swift @IBAction func refreshControlValueChanged(_ sender: UIRefreshControl) { model.reload() } Style and semantics
  32. Single process = single term protocol UserModelDelegate: class { func

    modelDidReload(_ model: UserModelDelegate) func model(_ model: UserModelDelegate, didFailToReloadWithError error: Error) } class UserModel { weak var delegate: UserModelDelegate! var user: User { } var features: [Feature] { } func reload() { /* call API */ } } // Later in UserViewController.swift @IBAction func refreshControlValueChanged(_ sender: UIRefreshControl) { model.reload() } Style and semantics
  33. Ignoring SDK's API design guidelines Style and semantics

  34. Ignoring SDK's API design guidelines extension Array where Element: Equatable

    { mutating func removeObject(_ object: Element) { if let index = self.index(of: object) { self.remove(at: index) } } mutating func removeObjectsInArray(_ array: [Element]) { for object in array { self.removeObject(object) } } } Style and semantics
  35. Ignoring SDK's API design guidelines extension Array where Element: Equatable

    { // Two terms describe single entity mutating func removeObject(_ object: Element) { if let index = self.index(of: object) { self.remove(at: index) } } mutating func removeObjectsInArray(_ array: [Element]) { for object in array { self.removeObject(object) } } } Style and semantics
  36. Ignoring SDK's API design guidelines // Array uses 'Element' term,

    not Object extension Array where Element: Equatable { // Two terms describe single entity mutating func removeObject(_ object: Element) { if let index = self.index(of: object) { self.remove(at: index) } } mutating func removeObjectsInArray(_ array: [Element]) { for object in array { self.removeObject(object) } } } Style and semantics
  37. Ignoring SDK's API design guidelines // Array uses 'Element' term,

    not Object extension Array where Element: Equatable { // Two terms describe single entity mutating func removeObject(_ object: Element) { if let index = self.index(of: object) { self.remove(at: index) } } // Array.append(contentsOf:) mutating func removeObjectsInArray(_ array: [Element]) { for object in array { self.removeObject(object) } } } Style and semantics
  38. Ignoring SDK's API design guidelines extension Array where Element: Equatable

    { mutating func remove(_ element: Element) { if let index = self.index(of: element) { self.remove(at: index) } } mutating func removeObjectsInArray(_ array: [Element]) { for object in array { self.removeObject(object) } } } Style and semantics
  39. Ignoring SDK's API design guidelines extension Array where Element: Equatable

    { mutating func remove(_ element: Element) { if let index = self.index(of: element) { self.remove(at: index) } } mutating func remove<C: Collection>(contentsOf elements: C) where C.Iterator.Element == Element { for element in elements { self.remove(element) } } } Style and semantics
  40. Ignoring SDK's API design guidelines // Factory method func createPhotosViewControllerForUser(user:

    User) -> PhotosViewController { return /* ... */ } Style and semantics
  41. Ignoring SDK's API design guidelines // Factory method func createPhotosViewControllerForUser(user:

    User) -> PhotosViewController { return /* ... */ } // But public protocol Collection : ... { ... func makeIterator() -> Iterator } Style and semantics
  42. Ignoring SDK's API design guidelines // Factory method func createPhotosViewControllerForUser(user:

    User) -> PhotosViewController { return /* ... */ } // But public protocol Collection : ... { ... func makeIterator() -> Iterator } // Begin names of factory methods with “make”, e.g. x.makeIterator() // Factory method func makePhotosViewController(user: User) -> PhotosViewController { return /* ... */ } Style and semantics
  43. Ignoring SDK's API design guidelines // NSDateFormatter-like class class PositionFormatter

    { func formattedPosition(_ position: Int) -> String } Style and semantics
  44. Ignoring SDK's API design guidelines // NSDateFormatter-like class class PositionFormatter

    { func formattedPosition(_ position: Int) -> String } // Comparing to class DateFormatter : Formatter { ... func string(from date: Date) -> String func date(from string: String) -> Date? ... } Style and semantics
  45. Ignoring SDK's API design guidelines // NSDateFormatter-like class class PositionFormatter

    { func string(from position: Int) -> String func position(from string: String) -> Int? } // Comparing to class DateFormatter : Formatter { ... func string(from date: Date) -> String func date(from string: String) -> Date? ... } Style and semantics
  46. Style and semantics • Every detail matters • Name should

    describe actual intent - don't give false expectations • Reuse existing API terms - don't pollute with new ones without a need • Follow vendor/recommended/any API design guidelines Links: • Swift API Design Guidelines: https://goo.gl/YT3LA5 • SE-0006: https://goo.gl/nX78X0 • How to name things: the hardest problem in programming: https://goo.gl/ZGEy81 • Hunting for great names in programming: https://goo.gl/H6zxcT • Erica Sadun: Swift protocol names: https://goo.gl/iM2re7 • Swift Protocol Naming Convention: https://goo.gl/2xnIxT Summary
  47. Style and semantics • Every detail matters • Name should

    describe actual intent - don't give false expectations • Reuse existing API terms - don't pollute with new ones without a need • Follow vendor/recommended/any API design guidelines Links: • Swift API Design Guidelines: https://goo.gl/YT3LA5 • SE-0006: https://goo.gl/nX78X0 • How to name things: the hardest problem in programming: https://goo.gl/ZGEy81 • Hunting for great names in programming: https://goo.gl/H6zxcT • Erica Sadun: Swift protocol names: https://goo.gl/iM2re7 • Swift Protocol Naming Convention: https://goo.gl/2xnIxT Summary
  48. Style and semantics • Every detail matters • Name should

    describe actual intent - don't give false expectations • Reuse existing API terms - don't pollute with new ones without a need • Follow vendor/recommended/any API design guidelines Links: • Swift API Design Guidelines: https://goo.gl/YT3LA5 • SE-0006: https://goo.gl/nX78X0 • How to name things: the hardest problem in programming: https://goo.gl/ZGEy81 • Hunting for great names in programming: https://goo.gl/H6zxcT • Erica Sadun: Swift protocol names: https://goo.gl/iM2re7 • Swift Protocol Naming Convention: https://goo.gl/2xnIxT Summary
  49. Style and semantics • Every detail matters • Name should

    describe actual intent - don't give false expectations • Reuse existing API terms - don't pollute with new ones without a need • Follow vendor/recommended/any API design guidelines Links: • Swift API Design Guidelines: https://goo.gl/YT3LA5 • SE-0006: https://goo.gl/nX78X0 • How to name things: the hardest problem in programming: https://goo.gl/ZGEy81 • Hunting for great names in programming: https://goo.gl/H6zxcT • Erica Sadun: Swift protocol names: https://goo.gl/iM2re7 • Swift Protocol Naming Convention: https://goo.gl/2xnIxT Summary
  50. Style and semantics • Every detail matters • Name should

    describe actual intent - don't give false expectations • Reuse existing API terms - don't pollute with new ones without a need • Follow vendor/recommended/any API design guidelines Links: • Swift API Design Guidelines: https://goo.gl/YT3LA5 • SE-0006: https://goo.gl/nX78X0 • How to name things: the hardest problem in programming: https://goo.gl/ZGEy81 • Hunting for great names in programming: https://goo.gl/H6zxcT • Erica Sadun: Swift protocol names: https://goo.gl/iM2re7 • Swift Protocol Naming Convention: https://goo.gl/2xnIxT Summary
  51. Language

  52. Optional.map .flatMap Still ignored Language

  53. Optional.map .flatMap // Unwrapped value required for the result if

    let title = user?.title { titleLabel.text = "Username: \(title)" } else { titleLabel.text = nil } Still ignored Language
  54. Optional.map .flatMap // Unwrapped value required for the result titleLabel.text

    = user?.title.map { "Username: \($0)" } Still ignored Language
  55. Optional.map .flatMap // Unwrapped value required for the result titleLabel.text

    = user?.title.map { "Username: \($0)" } // Ternary force unwrap func toString(_ value: Int?) -> String { return value != nil ? String(value!) : "empty string" } Still ignored Language
  56. Optional.map .flatMap // Unwrapped value required for the result titleLabel.text

    = user?.title.map { "Username: \($0)" } // Ternary force unwrap func toString(_ value: Int?) -> String { return value.map(String.init) ?? "" // or return value.map { String($0) } ?? "" } Still ignored Language
  57. Optional.map .flatMap // Unwrapped value required for the result titleLabel.text

    = user?.title.map { "Username: \($0)" } // Ternary force unwrap func toString(_ value: Int?) -> String { return value.map(String.init) ?? "" // or return value.map { String($0) } ?? "" } // Unwrapped value may produce optional value func imageData(at maybePath: String?) -> Data? { if let path = maybePath, let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { return data } else { return nil } } Still ignored Language
  58. Optional.map .flatMap // Unwrapped value required for the result titleLabel.text

    = user?.title.map { "Username: \($0)" } // Ternary force unwrap func toString(_ value: Int?) -> String { return value.map(String.init) ?? "" // or return value.map { String($0) } ?? "" } // Unwrapped value may produce optional value func imageData(at maybePath: String?) -> Data? { return maybePath.flatMap { try? Data(contentsOf: URL(fileURLWithPath:$0)) } } Language Still ignored
  59. No access control Language Discovered as

  60. No access control • @IBAction / @IBOutlet declared as internal

    • Properties that should be readonly for the client declared as readwrite • Utility methods declared as internal Language Discovered as
  61. No access control • @IBAction / @IBOutlet declared as internal

    • Properties with private setter by intent declared as readwrite • Utility methods declared as internal Language Discovered as
  62. No access control • @IBAction / @IBOutlet declared as internal

    • Properties with private setter by intent declared as readwrite • Most of the methods declared as internal Language Discovered as
  63. No access control Language Impact

  64. No access control • Gives false expectation to your teammates:

    no difference between public and private anymore • Breaks "black box" by exposing implementation • Encourages to use fast and dirty workarounds Language Impact
  65. No access control • Gives false expectation to your teammates:

    no difference between public and private anymore • Breaks "black box" by exposing implementation • Encourages to use fast and dirty workarounds Language Impact
  66. No access control • Gives false expectation to your teammates:

    no difference between public and private anymore • Breaks "black box" by exposing implementation • Encourages to use fast and dirty workarounds Language Impact
  67. No access control Language Solution

  68. No access control • @IBAction private func login(_ sender:) -

    still available to Interface Builder • use private(set) to hide mutability • Declare private first! Expose to public/ internal only when needed Language Solution
  69. No access control • @IBAction private func login(_ sender:) -

    still available to Interface Builder • use private(set) to hide mutability • Declare private first! Expose to public/ internal only when needed Language Solution
  70. No access control • @IBAction private func login(_ sender:) -

    still available to Interface Builder • use private(set) to hide mutability • Declare private first! Expose to public/ internal only when needed Language Solution
  71. Language Summary

  72. Language • Learn tools you're using everyday via: • Books

    • Blogs • Practice! Summary
  73. Code/SDK behavior understanding

  74. Wrong assumptions about layout cycle Code/SDK behavior understanding

  75. Wrong assumptions about layout cycle override func viewDidLoad() { super.viewDidLoad()

    titleLabel.preferredMaxLayoutWidth = imageView.bounds.width } Code/SDK behavior understanding
  76. Wrong assumptions about layout cycle override func viewDidLoad() { super.viewDidLoad()

    // Wrong. Size might be incorrect titleLabel.preferredMaxLayoutWidth = imageView.bounds.width } Code/SDK behavior understanding
  77. Awareness about layout cycle override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() titleLabel.preferredMaxLayoutWidth

    = imageView.bounds.width view.layoutIfNeeded() } Code/SDK behavior understanding
  78. Wrong assumptions about view lifecycle Code/SDK behavior understanding

  79. Wrong assumptions about view lifecycle class UserCell: UITableViewCell { @IBOutlet

    private var titleLabel: UILabel! // invoked on every tableView:cellForRowAtIndexPath: func apply(_ user: User) { titleLabel.textColor = UIColor.yellow titleLabel.font = UIFont.boldSystemFont(ofSize: 12) } } Code/SDK behavior understanding
  80. Wrong assumptions about view lifecycle class UserCell: UITableViewCell { @IBOutlet

    private var titleLabel: UILabel! // invoked on every tableView:cellForRowAtIndexPath: func apply(_ user: User) { // UI configuration performed on every reuse titleLabel.textColor = UIColor.yellow titleLabel.font = UIFont.boldSystemFont(ofSize: 12) } } Code/SDK behavior understanding
  81. Wrong assumptions about view lifecycle class UserCell: UITableViewCell { @IBOutlet

    private var titleLabel: UILabel! // invoked on every tableView:cellForRowAtIndexPath: func apply(_ user: User) { // UI configuration performed on every reuse titleLabel.textColor = UIColor.yellow titleLabel.font = UIFont.boldSystemFont(ofSize: 12) } // also found in layoutSubviews override func layoutSubviews() { super.layoutSubviews() // UI configuration performed during layout! titleLabel.textColor = UIColor.yellow titleLabel.font = UIFont.boldSystemFont(ofSize: 12) } } Code/SDK behavior understanding
  82. Awareness about view lifecycle class UserCell: UITableViewCell { @IBOutlet private

    var titleLabel: UILabel! override func awakeFromNib() { super.awakeFromNib() titleLabel.textColor = UIColor.yellow titleLabel.font = UIFont.boldSystemFont(ofSize: 12) } ... } Code/SDK behavior understanding
  83. Closures: unowned, weak or strong? Code/SDK behavior understanding

  84. Closures: unowned, weak or strong? @IBAction func doSomething() { DispatchQueue.main.async

    { [weak self] /* or [unowned self] */ in ... } } Code/SDK behavior understanding
  85. Closures: unowned, weak or strong? @IBAction func doSomething() { DispatchQueue.main.async

    { [weak self] /* or [unowned self] */ in ... } } func dismissSomething() { self.dismiss(animated: true) { [unowned self] in ... } } Code/SDK behavior understanding
  86. Closures: unowned, weak or strong? @IBAction func doSomething() { DispatchQueue.main.async

    { [weak self] /* or [unowned self] */ in ... } } func dismissSomething() { self.dismiss(animated: true) { [unowned self] in ... } } func animateChanges() { UIView.animate(withDuration: 0.2) { [weak self] in ... } } Code/SDK behavior understanding
  87. Closures: unowned, weak or strong? @IBAction func doSomething() { DispatchQueue.main.async

    { [weak self] /* or [unowned self] */ in ... } } func dismissSomething() { self.dismiss(animated: true) { [unowned self] in ... } } func animateChanges() { UIView.animate(withDuration: 0.2) { [weak self] in ... } } func saveCoreDataContext() { context.performBlockAndWait { [unowned self] in ... } // or context.performBlock { [weak self] in ... } } Code/SDK behavior understanding
  88. But why? Code/SDK behavior understanding

  89. But why? • Breaking retain cycle - OK • Unawareness

    of how particular code works • Ignorance of (non)deallocation impact Code/SDK behavior understanding
  90. But why? • Breaking retain cycle - OK • Unawareness

    of how particular code works • Ignorance of (non)deallocation impact Code/SDK behavior understanding
  91. But why? • Breaking retain cycle - OK • Unawareness

    of how particular code works • Ignorance of (non)deallocation impact Code/SDK behavior understanding
  92. Retain cycles are OK. Use `strong` Code/SDK behavior understanding

  93. Retain cycles are OK. Use `strong` • Temporary retain cycles

    are OK when: • it is clear that a cycle will be broken • guarantees correct app execution • Not all API leads to the permanent retain cycles: • dispatch_(a)sync on the main/global/any unowned queue • UIView.animate • MOC.performBlock(AndWait) Code/SDK behavior understanding
  94. Retain cycles are OK. Use `strong` • Temporary retain cycles

    are OK when: • it is clear that a cycle will be broken • guarantees correct app execution • Not all API leads to the permanent retain cycles: • dispatch_(a)sync on the main/global/any unowned queue • UIView.animate • MOC.performBlock(AndWait) Code/SDK behavior understanding
  95. Temporary retain cycles are OK data consistency Code/SDK behavior understanding

  96. Temporary retain cycles are OK class AbuseDetector { let networkAPI:

    API func reportAppState() { // collect usage data and send it to the server self.networkAPI.makeRequest { response in self.blockFurtherExecutionIfNeeded() } } } // usage AbuseDetector().reportAppState() data consistency Code/SDK behavior understanding
  97. Temporary retain cycles are OK class AbuseDetector { let networkAPI:

    API func reportAppState() { // collect usage data and send it to the server self.networkAPI.makeRequest { [unowned self] response in self.blockFurtherExecutionIfNeeded() } } } // usage AbuseDetector().reportAppState() data consistency Code/SDK behavior understanding
  98. Temporary retain cycles are OK class AbuseDetector { let networkAPI:

    API func reportAppState() { // collect usage data and send it to the server self.networkAPI.makeRequest { [unowned self] response in // crash! self.blockFurtherExecutionIfNeeded() } } } // usage AbuseDetector().reportAppState() data consistency Code/SDK behavior understanding
  99. Temporary retain cycles are OK class AbuseDetector { let networkAPI:

    API func reportAppState() { // collect usage data and send it to the server self.networkAPI.makeRequest { [weak self] response in // no crash self.blockFurtherExecutionIfNeeded() } } } // usage AbuseDetector().reportAppState() data consistency Code/SDK behavior understanding
  100. Temporary retain cycles are OK class AbuseDetector { let networkAPI:

    API func reportAppState() { // collect usage data and send it to the server self.networkAPI.makeRequest { [weak self] response in // no crash. But `self` was deallocated self.blockFurtherExecutionIfNeeded() } } } // usage AbuseDetector().reportAppState() data consistency Code/SDK behavior understanding
  101. Temporary retain cycles are OK class AbuseDetector { let networkAPI:

    API func reportAppState() { // collect usage data and send it to the server self.networkAPI.makeRequest { response in // Better to not rely on external env. and to guarantee response handling self.blockFurtherExecutionIfNeeded() } } } // usage AbuseDetector().reportAppState() data consistency Code/SDK behavior understanding
  102. Code/SDK behavior understanding • Seek to understand how and why

    code does its job • Learn tools you're using everyday • Layout cycle isn't that straightforward • Fundamentals understanding is a must • Apple Guides is worth reading Links • objc.io: Advanced autolayout toolbox: https://goo.gl/ebc9jd • Apple: Changing constrains: https://goo.gl/UbWlhI • Krakendev: weak and unowned refs in swift: https://goo.gl/6eqYco Summary
  103. Code/SDK behavior understanding • Seek to understand how and why

    code does its job • Learn tools you're using everyday • Layout cycle isn't that straightforward • Fundamentals understanding is a must • Apple Guides is worth reading Links • objc.io: Advanced autolayout toolbox: https://goo.gl/ebc9jd • Apple: Changing constrains: https://goo.gl/UbWlhI • Krakendev: weak and unowned refs in swift: https://goo.gl/6eqYco Summary
  104. Code/SDK behavior understanding • Seek to understand how and why

    code does its job • Learn tools you're using everyday • Layout cycle isn't that straightforward • Fundamentals understanding is a must • Apple Guides is worth reading Links • objc.io: Advanced autolayout toolbox: https://goo.gl/ebc9jd • Apple: Changing constrains: https://goo.gl/UbWlhI • Krakendev: weak and unowned refs in swift: https://goo.gl/6eqYco Summary
  105. Code/SDK behavior understanding • Seek to understand how and why

    code does its job • Learn tools you're using everyday • Layout cycle isn't that straightforward • Fundamentals understanding is a must • Apple Guides is worth reading Links • objc.io: Advanced autolayout toolbox: https://goo.gl/ebc9jd • Apple: Changing constrains: https://goo.gl/UbWlhI • Krakendev: weak and unowned refs in swift: https://goo.gl/6eqYco Summary
  106. Code/SDK behavior understanding • Seek to understand how and why

    code does its job • Learn tools you're using everyday • Layout cycle isn't that straightforward • Apple Guides is worth reading Links • objc.io: Advanced autolayout toolbox: https://goo.gl/ebc9jd • Apple: Changing constrains: https://goo.gl/UbWlhI • Krakendev: weak and unowned refs in swift: https://goo.gl/6eqYco Summary
  107. No-tests-ever design

  108. Logic and transformations done in Void methods No-tests-ever design

  109. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { let authorizationRequired = // perform some non-trivial check let credentialKey = // key depends on the domain/protocol/api/etc if authorizationRequired, let credential = Keychain.getValue(for: credentialKey) { let headers = // form custom HTTP headers // append to the request } } } No-tests-ever design
  110. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { let authorizationRequired = // perform some non-trivial check let credentialKey = // key depends on the domain/protocol/api/etc if authorizationRequired, let credential = Keychain.getValue(for: credentialKey) { let headers = // form custom HTTP headers // append to the request } } } // How to evaluate whether request requires authorization or not? No-tests-ever design
  111. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { let authorizationRequired = // perform some non-trivial check let credentialKey = // key depends on the domain/protocol/api/etc if authorizationRequired, let credential = Keychain.getValue(for: credentialKey) { let headers = // form custom HTTP headers // append to the request } } } // How to evaluate whether request requires authorization or not? // How to evaluate credential key? No-tests-ever design
  112. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { let authorizationRequired = // perform some non-trivial check let credentialKey = // key depends on the domain/protocol/api/etc if authorizationRequired, let credential = Keychain.getValue(for: credentialKey) { let headers = // form custom HTTP headers // append to the request } } } // How to evaluate whether request requires authorization or not? // How to evaluate credential key? // Hot to ensure that headers will be in the correct form? No-tests-ever design
  113. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { let authorizationRequired = // perform some non-trivial check let credentialKey = // key depends on the domain/protocol/api/etc if authorizationRequired, let credential = Keychain.getValue(for: credentialKey) { let headers = // form custom HTTP headers // append to the request } } } // How to evaluate whether request requires authorization or not? // How to evaluate credential key? // Hot to ensure that headers will be in the correct form? // Side effects! No-tests-ever design
  114. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { if let credential = credential(for: request) { for (key, value) in authorizationHeaders(for: credential) { // append to request headers } } } func shouldAuthorize(request: URLRequest) -> Bool { ... } func credential(for request: URLRequest) -> URLCredential? { if shouldAuthorize(request: request) { ... } else return nil } func headers(for credential: URLCredential) -> [String: String] { ... } } No-tests-ever design
  115. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { if let credential = credential(for: request) { for (key, value) in authorizationHeaders(for: credential) { // append to request headers } } } func shouldAuthorize(request: URLRequest) -> Bool { ... } func credential(for request: URLRequest) -> URLCredential? { if shouldAuthorize(request: request) { ... } else return nil } func headers(for credential: URLCredential) -> [String: String] { ... } } No-tests-ever design
  116. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { if let credential = credential(for: request) { for (key, value) in authorizationHeaders(for: credential) { // append to request headers } } } func shouldAuthorize(request: URLRequest) -> Bool { ... } func credential(for request: URLRequest) -> URLCredential? { if shouldAuthorize(request: request) { ... } else return nil } func headers(for credential: URLCredential) -> [String: String] { ... } } No-tests-ever design
  117. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { if let credential = credential(for: request) { for (key, value) in authorizationHeaders(for: credential) { // append to request headers } } } func shouldAuthorize(request: URLRequest) -> Bool { ... } func credential(for request: URLRequest) -> URLCredential? { if shouldAuthorize(request: request) { ... } else return nil } func headers(for credential: URLCredential) -> [String: String] { ... } } No-tests-ever design
  118. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { if let credential = credential(for: request) { for (key, value) in authorizationHeaders(for: credential) { // append to request headers } } } func shouldAuthorize(request: URLRequest) -> Bool { ... } func credential(for request: URLRequest) -> URLCredential? { if shouldAuthorize(request: request) { ... } else return nil } func headers(for credential: URLCredential) -> [String: String] { ... } } No-tests-ever design
  119. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { if let credential = credential(for: request) { for (key, value) in authorizationHeaders(for: credential) { // append to request headers } } } func shouldAuthorize(request: URLRequest) -> Bool { ... } func credential(for request: URLRequest) -> URLCredential? { if shouldAuthorize(request: request) { ... } else return nil } func headers(for credential: URLCredential) -> [String: String] { ... } } // Testable! No-tests-ever design
  120. No dependencies management No-tests-ever design

  121. No dependencies management class Keychain { class func storeValue(_ value:

    Any, for key: String) { ... } class func getValue(for key: String) -> Any? { ... } } // later class RequestAuthorizer { ... func credential(for request: URLRequest) -> URLCredential? { return Keychain.getValue(for: "key") } } No-tests-ever design
  122. No dependencies management class Keychain { class func storeValue(_ value:

    Any, for key: String) { ... } class func getValue(for key: String) -> Any? { ... } } // later class RequestAuthorizer { ... func credential(for request: URLRequest) -> URLCredential? { // No way to stub/mock/inject keychain easily return Keychain.getValue(for: "key") } } No-tests-ever design
  123. No dependencies management class Keychain { class func storeValue(_ value:

    Any, for key: String) { ... } class func getValue(for key: String) -> Any? { ... } } // later class RequestAuthorizer { ... func credential(for request: URLRequest) -> URLCredential? { // No way to stub/mock/inject keychain easily // No way to isolate tests environment return Keychain.getValue(for: "key") } } No-tests-ever design
  124. No dependencies management class Keychain { func storeValue(_ value: Any,

    for key: String) { ... } func getValue(for key: String) -> Any? { ... } } // later class RequestAuthorizer { ... func credential(for request: URLRequest) -> URLCredential? { return Keychain.getValue(for: "key") } } No-tests-ever design
  125. No dependencies management class Keychain { func storeValue(_ value: Any,

    for key: String) { ... } func getValue(for key: String) -> Any? { ... } } // later class RequestAuthorizer { init(keychain: Keychain) { ... } ... func credential(for request: URLRequest) -> URLCredential? { return keychain.getValue(for: "key") } } No-tests-ever design
  126. No dependencies management class Keychain { func storeValue(_ value: Any,

    for key: String) { ... } func getValue(for key: String) -> Any? { ... } } // later class RequestAuthorizer { init(keychain: Keychain) { ... } ... func credential(for request: URLRequest) -> URLCredential? { return keychain.getValue(for: "key") } } // Before let authorizer = RequestAuthorizer() // After. Hell for a legacy app let authorizer = RequestAuthorizer(keychain: Keychain()) No-tests-ever design
  127. No dependencies management class Keychain { func storeValue(_ value: Any,

    for key: String) { ... } func getValue(for key: String) -> Any? { ... } } // later class RequestAuthorizer { init(keychain: Keychain = Keychain.yourDefaultKeychain) { ... } ... func credential(for request: URLRequest) -> URLCredential? { return keychain.getValue(for: "key") } } // Before let authorizer = RequestAuthorizer() // After. Hell for a legacy app let authorizer = RequestAuthorizer(keychain: Keychain()) No-tests-ever design
  128. No dependencies management class Keychain { func storeValue(_ value: Any,

    for key: String) { ... } func getValue(for key: String) -> Any? { ... } } // later class RequestAuthorizer { init(keychain: Keychain = Keychain.yourDefaultKeychain) { ... } ... func credential(for request: URLRequest) -> URLCredential? { return keychain.getValue(for: "key") } } // Before let authorizer = RequestAuthorizer() // After let authorizer = RequestAuthorizer() No-tests-ever design
  129. Callee is responsible for async No-tests-ever design

  130. Callee is responsible for async class ResponseParser { func parseResponse<T>(_

    response: [String: Any], completion: (T) -> Void) { DispatchQueue.global().async { // parse completion(result) } } } No-tests-ever design
  131. Callee is responsible for async class ResponseParser { func parseResponse<T>(_

    response: [String: Any], completion: (T) -> Void) { // async is non-trivial for testing DispatchQueue.global().async { // parse completion(result) } } } No-tests-ever design
  132. Callee is responsible for async class ResponseParser { func parseResponse<T>(_

    response: [String: Any]) -> T { // trivial to test sync code } } No-tests-ever design Let the caller be responsible for it (IoC)
  133. Callee is responsible for async No-tests-ever design Use Executor pattern

  134. Callee is responsible for async class ResponseParser { init(executor: Executor)

    func parseResponse<T>(_ response: [String: Any], completion: (T) -> Void) { executor.execute { /* parse and completion */ } } } protocol Executor { func execute(_ work: (Void) -> Void) } No-tests-ever design Use Executor pattern
  135. Callee is responsible for async class ResponseParser { init(executor: Executor

    = DispatchQueueExecutor()) // default behavior func parseResponse<T>(_ response: [String: Any], completion: (T) -> Void) { executor.execute { /* parse and completion */ } } } protocol Executor { func execute(_ work: (Void) -> Void) } class DispatchQueueExecutor: Executor { func execute(_ work: (Void) -> Void) { ... } } No-tests-ever design Use Executor pattern
  136. Callee is responsible for async class ResponseParser { init(executor: Executor

    = DispatchQueueExecutor()) // default behavior func parseResponse<T>(_ response: [String: Any], completion: (T) -> Void) { executor.execute { /* parse and completion */ } } } protocol Executor { func execute(_ work: (Void) -> Void) } class DispatchQueueExecutor: Executor { func execute(_ work: (Void) -> Void) { ... } } // sync for testing class SyncExecutor: Executor { func execute(_ work: (Void) -> Void) { work() } } No-tests-ever design Use Executor pattern
  137. No-tests-ever design No-tests-ever design Summary

  138. No-tests-ever design No-tests-ever design • Designing with tests in mind:

    Summary
  139. No-tests-ever design No-tests-ever design • Designing with tests in mind:

    • Really improves your API and code Summary
  140. No-tests-ever design No-tests-ever design • Designing with tests in mind:

    • Really improves your API and code • Makes your code "tests ready" for free Summary
  141. No-tests-ever design No-tests-ever design • Designing with tests in mind:

    • Really improves your API and code • Makes your code "tests ready" for free • Doesn't require to write tests immediately Summary
  142. No-tests-ever design No-tests-ever design • Designing with tests in mind:

    • Really improves your API and code • Makes your code "tests ready" for free • Doesn't require to write tests immediately • Saves you from some potential bugs Summary
  143. Database misuse

  144. One db to rule them all Database misuse

  145. One db to rule them all • Faster to setup

    - zero cost Database usage
  146. One db to rule them all • Faster to setup

    - zero cost • Simpler to maintain in short-term Database usage
  147. One db to rule them all • Faster to setup

    - zero cost • Simpler to maintain in short-term • Bundled with MR_defaultContext() / Realm() Database usage
  148. One db to rule them all • Faster to setup

    - zero cost • Simpler to maintain in short-term • Bundled with MR_defaultContext() / Realm() • Leads to no-tests-ever design Database usage
  149. One db to rule them all • Faster to setup

    - zero cost • Simpler to maintain in short-term • Bundled with MR_defaultContext() / Realm() • Leads to no-tests-ever design • Tricky to manage data from different users Database usage
  150. One db to rule them all • Faster to setup

    - zero cost • Simpler to maintain in short-term • Bundled with MR_defaultContext() / Realm() • Leads to no-tests-ever design • Tricky to manage data from different users • Tricky to perform db cleanup Database usage
  151. Logout cleanup Database misuse

  152. Logout cleanup // UserA logged out Database misuse

  153. Logout cleanup // UserA logged out // We need to

    reset database for the UserB Database misuse
  154. Logout cleanup // UserA logged out // We need to

    reset database for the UserB let path = "Documents/Database.sqlite" FileManager.default.removeItem(atPath: path) // Are we good to go? Database misuse
  155. Logout cleanup // UserA logged out // We need to

    reset database for the UserB let path = "Documents/Database.sqlite" FileManager.default.removeItem(atPath: path) // Are we good to go? No! // How about: Database.sqlite-shm Database.sqlite-wal Database misuse
  156. Logout cleanup // UserA logged out // We need to

    reset database for the UserB let path = "Documents/Database.sqlite" FileManager.default.removeItem(atPath: path) // Are we good to go? No! // How about: Database.sqlite-shm Database.sqlite-wal // Or maybe Database_SUPPORT/_EXTERNAL_DATA/ Database misuse
  157. Logout cleanup // UserA logged out // We need to

    reset database for the UserB let path = "Documents/Database.sqlite" FileManager.default.removeItem(atPath: path) // Are we good to go? No! // How about: Database.sqlite-shm Database.sqlite-wal // Or maybe Database_SUPPORT/_EXTERNAL_DATA/ // Nobody cares! Database misuse
  158. Logout cleanup // UserA logged out // We need to

    reset database for the UserB let coordinator: NSPersistentStoreCoordinator // Choose a store you'd like to reset let store = coordinator.persistentStores.first Database misuse Use framework API!
  159. Logout cleanup // UserA logged out // We need to

    reset database for the UserB let coordinator: NSPersistentStoreCoordinator // Choose a store you'd like to reset let store = coordinator.persistentStores.first if let url = store.url { try coordinator.destroyPersistentStore(at: url, ofType: store.type) } // Add a new store using the same options Database misuse Use framework API!
  160. Logout/Login in concurrent world Database misuse

  161. Logout/Login in concurrent world // Statistics: started UserA's events upload

    Database misuse
  162. Logout/Login in concurrent world // Statistics: started UserA's events upload

    // UserA logged out Database misuse
  163. Logout/Login in concurrent world // Statistics: started UserA's events upload

    // UserA logged out // UserB logged in Database misuse
  164. Logout/Login in concurrent world // Statistics: started UserA's events upload

    // UserA logged out // UserB logged in // Statistics: received response Database misuse
  165. Logout/Login in concurrent world // Statistics: started UserA's events upload

    // UserA logged out // UserB logged in // Statistics: received response // Statistics: writing UserA's results to the Database Database misuse
  166. Logout/Login in concurrent world // Statistics: started UserA's events upload

    // UserA logged out // UserB logged in // Statistics: received response // Statistics: writing UserA's results to the Database // Results has been written to the UserB database or crashed due to the missing DB Database misuse
  167. Logout/Login in concurrent world Database misuse A better approach

  168. Logout/Login in concurrent world • Divide and conquer: Database misuse

    A better approach
  169. Logout/Login in concurrent world • Divide and conquer: • Keep

    database(s) per user Database misuse A better approach
  170. Logout/Login in concurrent world • Divide and conquer: • Keep

    database(s) per user • Keep service(s) per user Database misuse A better approach
  171. Logout/Login in concurrent world • Divide and conquer: • Keep

    database(s) per user • Keep service(s) per user • Keep folders on the disk per user Database misuse A better approach
  172. Logout/Login in concurrent world • Divide and conquer: • Keep

    database(s) per user • Keep service(s) per user • Keep folders on the disk per user • Prefer DI over MR_defaultContext / Realm() to gain control over your resources Database misuse A better approach
  173. Logout/Login in concurrent world Database and disk usage Ideally

  174. Logout/Login in concurrent world // UserA.Statistics: started events upload Database

    and disk usage Ideally
  175. Logout/Login in concurrent world // UserA.Statistics: started events upload //

    UserA logged out // UserB logged in Database and disk usage Ideally
  176. Logout/Login in concurrent world // UserA.Statistics: started events upload //

    UserA logged out // UserB logged in // UserA.Statistics: received response Database and disk usage Ideally
  177. Logout/Login in concurrent world // UserA.Statistics: started events upload //

    UserA logged out // UserB logged in // UserA.Statistics: received response // UserA.Statistics: writing results to the UserA.Database Database and disk usage Ideally
  178. Logout/Login in concurrent world // UserA.Statistics: started events upload //

    UserA logged out // UserB logged in // UserA.Statistics: received response // UserA.Statistics: writing results to the UserA.Database // UserA.Services are shutting down // UserA.Database removed or closed Database and disk usage Ideally
  179. Logout/Login in concurrent world // UserA.Statistics: started events upload //

    UserA logged out // UserB logged in // UserA.Statistics: received response // UserA.Statistics: writing results to the UserA.Database // UserA.Services are shutting down // UserA.Database removed or closed // Easy to achieve with DI Database and disk usage Ideally
  180. Files management

  181. Documents/ Common case Files management

  182. Documents/ Documents !"" Database.sqlite !"" Database.sqlite-shm !"" Database.sqlite-wal #"" images

    !"" 0e76292794888d4f1fa75fb3aff4ca27c58f56a6 !"" 7a85f4764bbd6daf1c3545efbbf0f279a6dc0beb #"" e5f26f9227e3ff4ece1431c2fc497c054d0e8269 Common case Files management
  183. Documents/ Documents !"" user_a $ !"" Database.sqlite $ !"" ...

    $ #"" ... #"" user_b !"" Database.sqlite !"" ... #"" ... With separated user data Files management
  184. Documents/ Documents !"" user_a $ !"" Database.sqlite $ !"" ...

    $ #"" ... #"" user_b !"" Database.sqlite !"" ... #"" ... // Are we done? With separated user data Files management
  185. Documents/ Documents !"" user_a $ !"" Database.sqlite $ !"" ...

    $ #"" ... !"" user_b $ !"" Database.sqlite $ !"" ... $ #"" ... #"" com.thirdPartySDK With separated user data Files management
  186. Documents/ Documents !"" user_a $ !"" Database.sqlite $ !"" ...

    $ #"" ... !"" user_b $ !"" Database.sqlite $ !"" ... $ #"" ... #"" com.thirdPartySDK FileManager.default.contentsOfDirectory(atPath: "Documents") // ["user_a", "user_b", "com.thirdPartySDK"] With separated user data Files management
  187. Documents/ Documents !"" com.bundle.id $ !"" metadata $ #"" users

    $ !"" a $ $ #"" ... $ #"" b $ #"" ... #"" com.thirdPartySDK With separated app data Files management
  188. Separated app data Files management How-to

  189. Separated app data • Use chdir-like approach Files management How-to

  190. Separated app data • Use chdir-like approach • Don't let

    services to choose location on their own Files management How-to
  191. Separated app data • Use chdir-like approach • Don't let

    services to choose location on their own • Pass "root" directory for every service Files management How-to
  192. Separated app data How-to Files management

  193. Separated app data struct Filepath { private(set) var url: URL

    mutating func append(_ component: String) { ... } func appending(_ component: String) -> Filepath { ... } } How-to Files management
  194. Separated app data struct Filepath { private(set) var url: URL

    mutating func append(_ component: String) { ... } func appending(_ component: String) -> Filepath { ... } } let userFilepath = Filepath(user: "user") How-to Files management
  195. Separated app data struct Filepath { private(set) var url: URL

    mutating func append(_ component: String) { ... } func appending(_ component: String) -> Filepath { ... } } let userFilepath = Filepath(user: "user") let databasePath = userFilepath.appending("Database.sqlite").url // Documents/com.bundle.id/users/user/Database.sqlite How-to Files management
  196. Files management When performed

  197. Files management • Simplifies any resources cleanup Files management When

    performed
  198. Files management • Simplifies any resources cleanup • Simplifies migration

    process Files management When performed
  199. Files management • Simplifies any resources cleanup • Simplifies migration

    process • Isolates resources from different services Files management When performed
  200. One target - One love

  201. One big, fat main target One target - one love

    Issues
  202. One big, fat main target • Easy to break layers

    boundaries • Can not fully utilize `internal` access level • Long compilation time (Swift only) • Single namespace One target - one love Issues
  203. One big, fat main target • Easy to break layers

    boundaries • Can not fully utilize `internal` access level • Long compilation time (Swift only) • Single namespace One target - one love Issues
  204. One big, fat main target • Easy to break layers

    boundaries • Can not fully utilize `internal` access level • Long compilation time (Swift only) • Single namespace One target - one love Issues
  205. One big, fat main target • Easy to break layers

    boundaries • Can not fully utilize `internal` access level • Long compilation time (Swift only) One target - one love Issues
  206. Several targets One target - one love Benefits

  207. Several targets • Forces you to write cleaner API •

    Better tests with @testable due to `internal` • Prevents you from leaking layers - no way to mix UI with Core.framework easily • Speeds up compilation (Swift only) One target - one love Benefits
  208. Several targets • Forces you to write cleaner API •

    Better tests with @testable due to `internal` • Prevents you from leaking layers - no way to mix UI with Core.framework easily • Speeds up compilation (Swift only) One target - one love Benefits
  209. Several targets • Forces you to write cleaner API •

    Better tests with @testable due to `internal` • Prevents you from leaking layers - no way to mix UI with Core.framework easily • Speeds up compilation (Swift only) One target - one love Benefits
  210. Several targets • Forces you to write cleaner API •

    Better tests with @testable due to `internal` • Prevents you from leaking layers - no way to mix UI with Core.framework easily • Speeds up compilation (Swift only) One target - one love Benefits
  211. Logic and transformations done in Void methods class RequestAuthorizer {

    func authorize(request: URLRequest) { ... } func shouldAuthorize(request: URLRequest) -> Bool { ... } func credential(for request: URLRequest) -> URLCredential? { ... } func headers(for credential: URLCredential) -> [String: String] { ... } } No-tests-ever design
  212. Logic and transformations done in Void methods // RequestAuthorizer.swift in

    your Core.framework to utilize @testable public class RequestAuthorizer { public func authorize(request: URLRequest) { ... } func shouldAuthorize(request: URLRequest) -> Bool { ... } func credential(for request: URLRequest) -> URLCredential? { ... } func headers(for credential: URLCredential) -> [String: String] { ... } } No-tests-ever design
  213. Model versioning

  214. Version number

  215. Version number • Identifier of the model scheme • Required

    during migration • Often is missing before first app update
  216. Version number • Identifier of the model scheme • Required

    during migration • Often is missing before first app update
  217. Version number • Identifier of the model scheme • Required

    during migration • Often is missing before first app update
  218. Migration example Model versioning

  219. Migration example func application(..., didFinishLaunchingWithOptions: ...) -> Bool { if

    let storedModelVersion = ..., storedModelVersion < desiredModelVersion { // perform migration } return true } Model versioning
  220. Migration example func application(..., didFinishLaunchingWithOptions: ...) -> Bool { if

    let storedModelVersion = ..., storedModelVersion < desiredModelVersion { // perform migration } return true } // What if storedModelVersion == nil, but we actually do have a data with old version? Model versioning
  221. Migration example func application(..., didFinishLaunchingWithOptions: ...) -> Bool { if

    let storedModelVersion = ..., storedModelVersion < desiredModelVersion { // perform migration } return true } // What if storedModelVersion == nil, but we actually do have a data with old version? func application(..., didFinishLaunchingWithOptions: ...) -> Bool { let storedModelVersion = UserDefaults.standard.integer(forKey: "version") if storedModelVersion == Keychain.hasCredentialInOldFormat { // we're running untracked old model version } ... return true } Model versioning
  222. Migration example func application(..., didFinishLaunchingWithOptions: ...) -> Bool { if

    let storedModelVersion = ..., storedModelVersion < desiredModelVersion { // perform migration } return true } // What if storedModelVersion == nil, but we actually do have a data with old version? func application(..., didFinishLaunchingWithOptions: ...) -> Bool { let storedModelVersion = UserDefaults.standard.integer(forKey: "version") if storedModelVersion == Keychain.hasCredentialInOldFormat { // we're running untracked old model version } ... return true } // Migration also can no be fully automated because of this special case Model versioning
  223. Model versioning Summary

  224. Model version • Define and store model version from the

    first release • Use integers for simplified comparison • Model version != app version (810 vs 1.3) Summary
  225. Model version • Define and store model version from the

    first release • Use integers for simplified comparison • Model version != app version (810 vs 1.3) Summary
  226. Model version • Define and store model version from the

    first release • Use integers for simplified comparison • Model version != app version (810 vs 1.3) Summary
  227. Q&A

  228. Thanks Dima Vorona, iOS Dev zen.dev@yandex.com