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

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

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

Adrian Kosmaczewski

July 16, 2016
Tweet

More Decks by Adrian Kosmaczewski

Other Decks in Technology

Transcript

  1. 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
  2. —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
  3. 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
  4. !⌚#$ Specific Smells ! Swift and Cocoa Smells ! Class

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

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

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

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

    incidentally for machines to execute. © akosma 2016 49
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. “There are only two hard things in Computer Science: cache

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

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

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

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

    = InactiveState() let stateMachine = GKStateMachine(states: [active, inactive]) stateMachine.enterState(InactiveState.self) © akosma 2016 80
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. “There are only two hard things in Computer Science: cache

    invalidation and naming things.” Phil Karlton10 10 Source: Martin Fowler © akosma 2016 92
  42. …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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. Extensions Explosion —19 iOS extensions —11 macOS extensions Check out

    Apple’s “Lister” application!99 99 Source: Apple Developer Library © akosma 2016 110
  49. 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
  50. 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
  51. Specify Pod Versions ✔ target 'MyApp' do pod 'AFNetworking', '~>

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

    conflicts —Slow loading —Complex navigation © akosma 2016 122
  53. 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
  54. // 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
  55. Formula for Happiness©™ Minimum iOS Version to Support = (Current

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

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

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

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

    Controller & App Delegate # Forgotten Memory Warnings $ Long Switch Statement % Long Method Names & Excessive Curiosity © akosma 2016 136
  60. ! 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
  61. Got More Smells? Call to action! 1. Give smells a

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