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

Refactoring iOS Projects - Bangalore

Refactoring iOS Projects - Bangalore

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 Bangalore, India, on September 15th, 2016, at http://www.mobiledevelopersummit.com

Speech: https://akos.ma/blog/refactoring-ios-projects/

Adrian Kosmaczewski

September 15, 2016
Tweet

More Decks by Adrian Kosmaczewski

Other Decks in Technology

Transcript

  1. ! !ೕಜ$ಗ& '()ಕ+'ಂ- र ि फ्र ै क्ट र ि

    ं ग ! प र ि योजनाओं Refactoring ! Projects @akosma – @mobiledevsummit – September 15th 2016 © akosma 2016 1
  2. !⌚#$ !ೕಜ$ಗ& '()ಕ+'ಂ- र ि फ्र ै क्ट र ि

    ं ग !⌚#$ प र ि योजनाओं Refactoring !⌚#$ Projects @akosma – @mobiledevsummit – September 15th 2016 © akosma 2016 2
  3. 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
  4. —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
  5. 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
  6. !⌚#$ Specific Smells ! Swift and Cocoa Smells ! Class

    Design Smells ! Project Management Smells © akosma 2016 33
  7. 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
  8. 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
  9. weak vs. unowned —weak can be nil —unowned cannot be

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

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

    { [unowned obj] return obj.property // Yay! } © akosma 2016 44
  12. 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
  13. 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
  14. Programs must be written for people to read, and only

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

    mainView.addChild(button, at: origin) truck.removeBoxes(withLabel: "mobiledevsummit") // Use verbs for side effects friends.reverse() viewController.present(animated: true) 55 Source: WWDC 2016 session 403 © akosma 2016 50
  16. 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
  17. 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
  18. 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
  19. 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
  20. “There are only two hard things in Computer Science: cache

    invalidation and naming things.” Phil Karlton10 10 Source: Martin Fowler © akosma 2016 57
  21. 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
  22. 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
  23. 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
  24. 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
  25. Associated Objects 14 Do not if you can use instead:

    —Subclassing —Target-Action —Gesture recognizers —Delegation —NSNotification 14 Source: NSHipster © akosma 2016 64
  26. 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
  27. 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
  28. 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
  29. Empty States72 1. First use39 2. User cleared 3. Errors

    39 Source: Designing For The Empty States 72 Source: emptystat.es © akosma 2016 74
  30. 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
  31. 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
  32. AppState (2/3) class SomeController { var state = AppState.None {

    didSet { state.execute() } } func viewDidLoad() { // ... } © akosma 2016 77
  33. 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
  34. 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
  35. GKState and GKStateMachine (2/2) let active = ActiveState() let inactive

    = InactiveState() let stateMachine = GKStateMachine(states: [active, inactive]) stateMachine.enterState(InactiveState.self) © akosma 2016 80
  36. 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
  37. 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
  38. 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
  39. Learn more about MVVM "MVVM-C In Practice" Tomorrow 13:50, by

    Steve Scott, in the Main Hall © akosma 2016 85
  40. Learn more about React "React Native for Android" Today 17:10,

    by Anirudh Sundararaman, in the SD Hall © akosma 2016 86
  41. 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 88
  42. Lazy Loading Class class LazyLoader { var _backingField: String? var

    property: String? { get { if _backingField == nil { _backingField = "text" } return _backingField } set { _backingField = newValue } } } © akosma 2016 89
  43. 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 91
  44. 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 92
  45. “There are only two hard things in Computer Science: cache

    invalidation and naming things.” Phil Karlton10 10 Source: Martin Fowler © akosma 2016 94
  46. …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 97
  47. 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 99
  48. 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 100
  49. 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 102
  50. 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 103
  51. 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 104
  52. Extensions Explosion —19 iOS extensions —11 macOS extensions Check out

    Apple’s “Lister” application!99 99 Source: Apple Developer Library © akosma 2016 112
  53. 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 113
  54. 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 115
  55. Specify Pod Versions ✔ target 'MyApp' do pod 'AFNetworking', '~>

    3.0' pod 'FBSDKCoreKit', '~> 4.9' end ➡ Choose your pods wisely! © akosma 2016 116
  56. Problem —Controllers tied to Storyboard ➡ hard to reuse —Merge

    conflicts —Slow loading —Complex navigation © akosma 2016 124
  57. 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 125
  58. // 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 128
  59. Learn more about Storyboards "Pro Storyboard Techniques" Tomorrow 16:10, by

    Joe Keeley, in the Main Hall © akosma 2016 129
  60. Formula for Happiness©™ Minimum iOS Version to Support = (Current

    iOS Version Number) – 1 ! © akosma 2016 132
  61. Common Backend API Sins —SOAP, XML-RPC —Chatty design —Lack of

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

    design —Socket.io & GraphQL ☺ " 㲗 ‘ ↩ # © akosma 2016 135
  63. ! Swift and Cocoa Smells ! SwiftyLeaks " Hungarian Notation

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

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

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

    cool name and a description 2. Specify the refactoring 3. Share! © akosma 2016 141