Refactoring iOS Projects - Ukraine

Refactoring iOS Projects - Ukraine

For many developers, refactoring is a menu in their IDE. The truth is that there are tons of refactoring techniques, most of them described in detail by Martin Fowler in his book of that name. In this session we are going to learn simple yet effective techniques to refactor large iOS codebases in order to make them more testable, to adapt them to be eventually rewritten in Swift, and to make them as "future proof" as possible.

Presentation given in Dnipropetrovsk, Ukraine, on July 16th, 2016, at https://uamobi.tech

53619e4417778923cc65a51683e850a0?s=128

Adrian Kosmaczewski

July 16, 2016
Tweet

Transcript

  1. Рефакторинг ! Проектів Refactoring ! Projects @akosma – @uaMobiTech –

    July 16th 2016 © akosma 2016 1
  2. Рефакторинг !⌚#$ Проектів Refactoring !⌚#$ Projects @akosma – @uaMobiTech –

    July 16th 2016 © akosma 2016 2
  3. © akosma 2016 3

  4. © akosma 2016 4

  5. Thank you ! More information at developer.apple.com © akosma 2016

    5
  6. © akosma 2016 6

  7. Sorry! ! © akosma 2016 7

  8. © akosma 2016 8

  9. © akosma 2016 9

  10. Thank you ! More information at jetbrains.com/objc © akosma 2016

    10
  11. © akosma 2016 11

  12. Seriously? ! © akosma 2016 12

  13. Live and Let IDE ! © akosma 2016 13

  14. Big Topic ! © akosma 2016 14

  15. © akosma 2016 15

  16. More ! © akosma 2016 16

  17. © akosma 2016 17

  18. Yeah baby !" © akosma 2016 18

  19. © akosma 2016 19

  20. What is it? ! © akosma 2016 20

  21. Refactoring (noun): a change made to the internal structure of

    software to make it easier to understand and cheaper to modify without changing its observable behavior. © akosma 2016 21
  22. Why to? ! © akosma 2016 22

  23. —To improve the design of software —To make it easier

    to understand —To make it easier to find and fix bugs —To program faster —To annoy your manager © akosma 2016 23
  24. When to? ⏰ © akosma 2016 24

  25. —When you add features —When fixing bugs —During code reviews

    © akosma 2016 25
  26. When not to? ⛔ © akosma 2016 26

  27. © akosma 2016 27

  28. 1. Add Tests 2. Refactor © akosma 2016 28

  29. © akosma 2016 29

  30. © akosma 2016 30

  31. How to? ! © akosma 2016 31

  32. Bad Smells !68 —Duplicated code —Long methods —Large class —Long

    parameter list —Divergent change —Shotgun surgery —Feature envy —Data clumps —Primitive obsession —Switch statements —Parallel inheritance hierarchies —Lazy class —Speculative generality —Temporary field —Message chains —Middle Man —Inappropriate intimacy —Alternative classes with different interfaces —Incomplete library class —Data class —Refused bequest —Comments 68 Source: “Refactoring” by Martin Fowler (1999) © akosma 2016 32
  33. !⌚#$ Specific Smells ! Swift and Cocoa Smells ! Class

    Design Smells ! Project Management Smells © akosma 2016 33
  34. ! Swift and Cocoa Smells © akosma 2016 34

  35. !" SwiftyLeaks © akosma 2016 35

  36. © akosma 2016 36

  37. Memory Leaks in Swift Reference Cycles23: —Instances —Closures 23 Source:

    Apple Developer Library © akosma 2016 37
  38. Instance Reference Cycles (1/2) class Person { let name: String

    var apartment: Apartment? } class Apartment { let unit: String var tenant: Person? // Ouch! } © akosma 2016 38
  39. © akosma 2016 39

  40. Instance Reference Cycles (2/2) class Person { let name: String

    var apartment: Apartment? } class Apartment { let unit: String weak var tenant: Person? // Yay! } © akosma 2016 40
  41. © akosma 2016 41

  42. weak vs. unowned —weak can be nil —unowned cannot be

    nil —unowned ➡ always nonoptional types © akosma 2016 42
  43. Closure Reference Cycles (1/2) let obj = SomeObject() obj.blockMember =

    { return obj.property // Ouch! } © akosma 2016 43
  44. Closure Reference Cycles (2/2) let obj = SomeObject() obj.blockMember =

    { [unowned obj] return obj.property // Yay! } © akosma 2016 44
  45. Closures are reference types⾠ © akosma 2016 45

  46. !" Hungarian Notation © akosma 2016 46

  47. protocol IUnknown { func QueryInterface (_ riid: REFIID, _ ppvObject:

    LPVOID) -> HRESULT func AddRef() -> ULONG func Release() -> ULONG } protocol IDispatch : IUnknown { func GetTypeInfoCount(pctinfo: UInt) -> HRESULT func GetTypeInfo(_ iTInfo: UInt, _ lcid: LCID, _ ppTInfo: ITypeInfo) -> HRESULT func GetIDsOfNames(_ riid: REFIID, _ rgszNames: OLECHAR, _ cNames: UInt, _ lcid: LCID, _ rgDispId: DISPID) -> HRESULT func Invoke(_ dispIdMember: DISPID, _ riid: REFIID, _ lcid: LCID, _ wFlags: WORD, _ pDispParams: DISPPARAMS, _ pVarResult: VARIANT, _ pExcepInfo: EXCEPINFO, _ puArgErr: UInt32) -> HRESULT } © akosma 2016 47
  48. let oPerson = Server.CreateObject("Person") var bBusy = true if (bBusy)

    { bBusy = false let riid = CFUUIDGetUUIDBytes(CFUUIDCreate(kCFAllocatorDefault)) let ppvObject = UnsafeMutablePointer<Void>(allocatingCapacity: 10) let result = oPerson.QueryInterface(riid, ppvObject) if result == 0x80090032 { bBusy = true oPerson.Invoke(0, riid, riid, 0, riid, nil, nil, 0) } } © akosma 2016 48
  49. Programs must be written for people to read, and only

    incidentally for machines to execute. © akosma 2016 49
  50. Swift API design guidelines55 // Make APIs read grammatically friends.remove(ted)

    mainView.addChild(button, at: origin) truck.removeBoxes(withLabel: "uaMobiTech") // Use verbs for side effects friends.reverse() viewController.present(animated: true) 55 Source: WWDC 2016 session 403 © akosma 2016 50
  51. Swift API design guidelines55 // Use nouns for returned values

    button.backgroundTitle(for: .disabled) friends.suffix(3) // Mutating and non-mutating method pairs x.reverse() // mutating let y = x.reversed() // non-mutating dir.appendPathComponent(".txt") // mutating let newDir = dir.appendingPathComponent(".txt") // non-mutating 55 Source: WWDC 2016 session 403 © akosma 2016 51
  52. Swift 㲗 Objective-C Two different signatures: one for Swift, another

    for Objective-C @objc() exports Swift API ➡ Objective-C NS_SWIFT_NAME() Objective-C APIs ➡ Swift © akosma 2016 52
  53. !" Objective-C Nostalgia © akosma 2016 53

  54. Migrate to Swift3 NS_ASSUME_NONNULL_BEGIN #define let __auto_type const #define var

    __auto_type @property(nonatomic, nonnull, copy) NSString *name; @property(nonatomic, nullable, weak) id<UITableViewDelegate> delegate; - (nullable NSArray *)arrayWithWord:(nonnull NSString *)word { // NSArray<NSString *> * _Nonnull words = @[@"one", @"two", word]; __auto_type _Nonnull words = @[@"one", @"two", word]; return words; } NS_ASSUME_NONNULL_END 3 Source: Swift Blog © akosma 2016 54
  55. Caveats —Objective-C cannot subclass a Swift class —…unless marked @objc

    —Tuples, Swift enums and structs are not accessible from Objective-C. © akosma 2016 55
  56. !" Homemade Cache © akosma 2016 56

  57. “There are only two hard things in Computer Science: cache

    invalidation and naming things.” Phil Karlton10 10 Source: Martin Fowler © akosma 2016 57
  58. NSURLCache let config = URLSessionConfiguration.default config.requestCachePolicy = .returnCacheDataElseLoad let memoryCapacity

    = 10 * 1024 * 1024; let diskCapacity = 20 * 1024 * 1024; let cache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: nil) URLCache.shared = cache © akosma 2016 58
  59. !" Tagged Views © akosma 2016 59

  60. Do not viewWithTag() override func tableView(_ tableView: UITableView, cellForRowAt indexPath:

    IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let object = objects[indexPath.row] as! NSDate cell.textLabel!.text = object.description let switchControl = cell.viewWithTag(1) as! UISwitch // Ouch! switchControl.setOn(false, animated: false) return cell } © akosma 2016 60
  61. Custom View Class override func tableView(_ tableView: UITableView, cellForRowAt indexPath:

    IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomViewCell let object = objects[indexPath.row] as! NSDate cell.titleLabel.text = object.description cell.switchControl.setOn(true, animated: false) // Yay! return cell } © akosma 2016 61
  62. !" Illicit Association © akosma 2016 62

  63. Associated Objects 86 extension NSWhatever { private struct Keys {

    static var Value = "AssociatedValue" } var associatedString: String? { get { return objc_getAssociatedObject(self, &Keys.Value) as? String } set { objc_setAssociatedObject(self, &Keys.Value, newValue as NSString?, .OBJC_ASSOCIATION_COPY_NONATOMIC) } } } var obj = NSWhatever() obj.associatedString = "Some other string" 86 Source: NSHipster © akosma 2016 63
  64. Associated Objects 14 Do not if you can use instead:

    —Subclassing —Target-Action —Gesture recognizers —Delegation —NSNotification 14 Source: NSHipster © akosma 2016 64
  65. © akosma 2016 65

  66. ! Class Design Smells © akosma 2016 66

  67. !" Class Struggle © akosma 2016 67

  68. class or struct? ! (1/2) protocol Modifiable { // protocols

    FTW! var value : Int { get set } mutating func increase() } extension Modifiable { mutating func increase() { value += 100 } } © akosma 2016 68
  69. class or struct? ! (2/2) struct TypeOne : Modifiable {

    var value = 0 } class TypeTwo : Modifiable { var value = 0 } var v1, v2, r1, r2 : Modifiable v1 = TypeOne(); v2 = v1 // Value type, copied r1 = TypeTwo(); r2 = r1 // Reference type, not copied v2.increase(); assert(v1.value != v2.value) r2.increase(); assert(r1.value == r2.value) © akosma 2016 69
  70. Reference Types ! —Inheritance —Reference semantics —Use Swift types in

    Objective-C Value Types ! —Always implement Equatable on your own76 —More testable architectures —Immutable by default 76 Source: WWDC 2015 Session 414 © akosma 2016 70
  71. !" Massive View Controller (MVC) and Massive App Delegate (MAD)

    © akosma 2016 71
  72. © akosma 2016 72

  73. Class Breakdown ! Maximum 400 lines ! © akosma 2016

    73
  74. Empty States72 1. First use39 2. User cleared 3. Errors

    39 Source: Designing For The Empty States 72 Source: emptystat.es © akosma 2016 74
  75. State Machines12 —AppState enumeration with associated lambdas —GKState and GKStateMachine

    Other Approaches —Model-View-ViewModel (MVVM) —Protocols & Extensions —Dependency Injection —ReSwift, PromiseKit, RxSwift, ReactKit, ReactiveCocoa… 12 Source: Github © akosma 2016 75
  76. AppState (1/3) enum AppState { case None case Active(action: Block)

    // typealias Block = (Void) -> Void case Inactive(action: Block) func execute() { switch self { case .Active(let block): block() case .Inactive(let block): block() default: return } } } © akosma 2016 76
  77. AppState (2/3) class SomeController { var state = AppState.None {

    didSet { state.execute() } } func viewDidLoad() { // ... } © akosma 2016 77
  78. AppState (3/3) func viewDidLoad() { super.viewDidLoad() let inactive = AppState.Inactive

    { [unowned self] () in self.view.backgroundColor = UIColor.red() } let active = AppState.Active { [unowned self] () in self.view.backgroundColor = UIColor.green() } // ... self.state = inactive } © akosma 2016 78
  79. GKState and GKStateMachine (1/2) import GameplayKit class ActiveState: GKState {

    override func isValidNextState(_ stateClass: AnyClass) -> Bool { if stateClass == InactiveState.self { return true } return false } override func didEnter(withPreviousState previousState: GKState?) { // change UI elements, etc, etc... } override func willExit(withNextState nextState: GKState) { // ... } } © akosma 2016 79
  80. GKState and GKStateMachine (2/2) let active = ActiveState() let inactive

    = InactiveState() let stateMachine = GKStateMachine(states: [active, inactive]) stateMachine.enterState(InactiveState.self) © akosma 2016 80
  81. © akosma 2016 81

  82. Model – View – ViewModel (MVVM 1/2) class DetailViewModel: NSObject

    { let model : Model init(withModel model: Model) { self.model = model } var displayText : String { return self.model.description } } © akosma 2016 82
  83. Model – View – ViewModel (MVVM 2/2) class DetailViewController: UIViewController

    { @IBOutlet weak var label: UILabel! var viewModel : DetailViewModel? override func viewDidLoad() { super.viewDidLoad() self.label.text = self.viewModel?.displayText } } © akosma 2016 83
  84. Martin Fowler Says68 —Extract Class (149) —Extract Subclass (330) —Extract

    Interface (341) —Duplicate Observed Data (189) 68 Source: “Refactoring” by Martin Fowler (1999) © akosma 2016 84
  85. !" Forgotten Memory Warnings © akosma 2016 85

  86. Memory Warnings func applicationDidReceiveMemoryWarning(_ application: UIApplication) { // ... on

    the app delegate... } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // ... on every controller... } // ... and anywhere else let center = NotificationCenter.default() let notification = NSNotification.Name.UIApplicationDidReceiveMemoryWarning let selector = #selector(SomeClass.handler(_:)) center.addObserver(self, selector: selector, name: notification, object: nil) © akosma 2016 86
  87. Lazy Loading Class class LazyLoader { var _backingField: String? var

    property: String? { get { if _backingField == nil { _backingField = "text" } return _backingField } set { _backingField = newValue } } } © akosma 2016 87
  88. !" Long Switch Statement © akosma 2016 88

  89. Pattern Matching44 switch (indexPath.section, indexPath.row) { case (0, let row):

    cell.backgroundColor = UIColor.lightGray() // ... case (let section, 0) where section % 2 == 0: cell.backgroundColor = UIColor.brown() // ... case let (3, row) where validate(row): cell.backgroundColor = UIColor.blue() // ... default: cell.backgroundColor = UIColor.white() } 44 Source: Ash Furrow © akosma 2016 89
  90. Martin Fowler Says68 —Extract Method (110) —Move Method (142) —Replace

    Type Code With Subclasses (223) —Replace Type Code With State/Strategy (227) —Replace Conditional with Polymorphism (255) 68 Source: “Refactoring” by Martin Fowler (1999) © akosma 2016 90
  91. !" Long Method Names © akosma 2016 91

  92. “There are only two hard things in Computer Science: cache

    invalidation and naming things.” Phil Karlton10 10 Source: Martin Fowler © akosma 2016 92
  93. Cocoa Awards™ ! © akosma 2016 93

  94. Cocoa Awards™ ! Longest Method Name Category95 95 Source: Quora

    and Github © akosma 2016 94
  95. …and the winner is… // QCPlugInContext class - (id) outputImageProviderFromBufferWithPixelFormat:(NSString*)format

    pixelsWide:(NSUInteger)width pixelsHigh:(NSUInteger)height baseAddress:(const void*)baseAddress bytesPerRow:(NSUInteger)rowBytes releaseCallback:(QCPlugInBufferReleaseCallback)callback releaseContext:(void*)context colorSpace:(CGColorSpaceRef)colorSpace shouldColorMatch:(BOOL)colorMatch; © akosma 2016 95
  96. © akosma 2016 96

  97. Martin Fowler Says68 —Replace Parameter with Method (292) —Preserve Whole

    Object (288) —Introduce Parameter Object (295) 68 Source: “Refactoring” by Martin Fowler (1999) © akosma 2016 97
  98. Parameter Objects in Cocoa // Map snapshots let options =

    MKMapSnapshotOptions() options.region = mapView.region options.size = mapView.frame.size let snapshotter = MKMapSnapshotter(options: options) // URL Sessions let config = URLSessionConfiguration() config.httpShouldSetCookies = false config.timeoutIntervalForRequest = 2.0 let session = URLSession(configuration: config) © akosma 2016 98
  99. !" Excessive Curiosity © akosma 2016 99

  100. Excessive User Interface Idiom Checks // Only if you need

    compatibility with iPhone OS 3.2... really? if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad { // ... iPad-only code } let idiom = UIDevice.current().userInterfaceIdiom if idiom == UIUserInterfaceIdiom.phone { // ... iPhone-only code } © akosma 2016 100
  101. Excessive OS Version Checks46 let ver = OperatingSystemVersion(majorVersion: 10, minorVersion:

    0, patchVersion: 0) if ProcessInfo().isOperatingSystemAtLeast(ver) { ... } let os = ProcessInfo().operatingSystemVersion switch (os.majorVersion, os.minorVersion, os.patchVersion) { case (10, _, _): print("iOS >= 10.0.0") default: print("Previous versions") } 46 Source: NSHipster © akosma 2016 101
  102. Behave Appropriately 1. Encapsulate features into subclasses 2. Last resort:

    check feature existence if let feature = NSClassFromString("INIntent") { print("Intents.framework (SiriKit) available") } © akosma 2016 102
  103. © akosma 2016 103

  104. ! Project Management Smells © akosma 2016 104

  105. !" Invisible Documentation © akosma 2016 105

  106. If you haven’t written documentation you haven’t done your job

    © akosma 2016 106
  107. © akosma 2016 107

  108. !" Folder Clusterfuck © akosma 2016 108

  109. © akosma 2016 109

  110. Extensions Explosion —19 iOS extensions —11 macOS extensions Check out

    Apple’s “Lister” application!99 99 Source: Apple Developer Library © akosma 2016 110
  111. Conventions for Xcode28 —Create folders for each platform: iOS, macOS,

    watchOS, tvOS —Target folders inside platform folders —Shared folder for cross-platform files —Separate items into Assets and Source directories 28 Source: The.Swift.Dev © akosma 2016 111
  112. !" Cocoapods Galore © akosma 2016 112

  113. Cocoapods Best Practices —Check in Pods folder, Podfile and Podfile.lock.

    —pod outdated in a separate branch once a week —Use rbenv to control Ruby installations —Team uses the same version of Cocoapods © akosma 2016 113
  114. Specify Pod Versions ✔ target 'MyApp' do pod 'AFNetworking', '~>

    3.0' pod 'FBSDKCoreKit', '~> 4.9' end ➡ Choose your pods wisely! © akosma 2016 114
  115. !" Rogue Compiler Warnings © akosma 2016 115

  116. New in Xcode8 8 Source: WWDC 2016 Session 410 ©

    akosma 2016 116
  117. © akosma 2016 117

  118. !" User Discrimination © akosma 2016 118

  119. ♿"#$%& '()*+㊙ -./012 © akosma 2016 119

  120. !" Interface Builder Attack © akosma 2016 120

  121. © akosma 2016 121

  122. Problem —Controllers tied to Storyboard ➡ hard to reuse —Merge

    conflicts —Slow loading —Complex navigation © akosma 2016 122
  123. Solution —Use storyboards only for navigation —Use XIB files only

    for UI design Steps: 1. Menu Editor -> Refactor to Storyboard… 2. ViewController.swift + ViewController.xib 3. Remove view outlet from Storyboard in Controller © akosma 2016 123
  124. © akosma 2016 124

  125. © akosma 2016 125

  126. // Instantiating through the Storyboard if let controller = self.storyboard?.instantiateViewController(withIdentifier:

    "Ctrl") { // ... } // Instantiating manually // This constructor automatically loads XIB file let controller = ViewController() © akosma 2016 126
  127. !" iOS Nostalgia © akosma 2016 127

  128. © akosma 2016 128

  129. Formula for Happiness©™ Minimum iOS Version to Support = (Current

    iOS Version Number) – 1 ! © akosma 2016 129
  130. !" Backend API Anarchy © akosma 2016 130

  131. Common Backend API Sins —SOAP, XML-RPC —Chatty design —Lack of

    Control —QOS, Security, Performance… ! " ↩ # © akosma 2016 131
  132. Repent Of Your Sins —Build your own API Proxy —Chunky

    design —Socket.io & GraphQL ☺ " 㲗 ‘ ↩ # © akosma 2016 132
  133. © akosma 2016 133

  134. Summary © akosma 2016 134

  135. ! Swift and Cocoa Smells ! SwiftyLeaks " Hungarian Notation

    # Objective-C Nostalgia $ Homemade Cache % Tagged View & Illicit Association © akosma 2016 135
  136. ! Class Design Smells ! Class Struggle " Massive View

    Controller & App Delegate # Forgotten Memory Warnings $ Long Switch Statement % Long Method Names & Excessive Curiosity © akosma 2016 136
  137. ! Project Management Smells ! Invisible Documentation " Folder Clusterfuck

    # Cocoapods Galore $ Rogue Compiler Warnings % User Discrimination & Interface Builder Attack ' iOS Nostalgia ( Backend API Anarchy © akosma 2016 137
  138. Got More Smells? Call to action! 1. Give smells a

    cool name and a description 2. Specify the refactoring 3. Share! © akosma 2016 138
  139. Дякую! ! Thanks! ! More information at about.me/akosma © akosma

    2016 139
  140. Questions❔ © akosma 2016 140