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

Frameworks and Dependency Injection - A Tale of Swift

Frameworks and Dependency Injection - A Tale of Swift

Maintaining a big codebase during an extended period is always tricky, especially in app environments. Products evolve, companies pivot, vendors modify their SDK, new design patterns emerge and die... you can even decide to change the programming language!

The Tuenti iOS app has suffered a good chunk of changes. In the short term, this app will be at the center of Telefonica digitization, serving dozens of millions of Movistar, O2 and Vivo customers.

How do you prepare a legacy codebase for this? First, let’s continue migrating a massive Objective-C codebase to Swift. Modularize and isolate its components using frameworks. Test as much as possible to avoid costly regressions. And finally, design the glue that holds everything together.

Víctor Pimentel

November 23, 2018
Tweet

Other Decks in Programming

Transcript

  1. Agenda 1. What Do We Do? 2. iOS App: A

    Quick History 3. The Swift Revolution 4. Frameworks 5. Dependency Injection 6. Conclusion Víctor Pimentel Rodríguez @ Commit Conf 2018 2
  2. A Single Product 4 Next generation of main Telefónica's apps

    4 Mi Movistar 4 Meu Vivo 4 My O2/Mein O2 4 Tuenti 4 350 million customers Víctor Pimentel Rodríguez @ Commit Conf 2018 4
  3. How? 4 Single codebase 4 Backend: PHP, Java, Kotlin, Node.js,

    Erlang, etc 4 Native applications 4 iOS: Swift, Objective-C 4 Android: Java, Kotlin 4 Web: React Víctor Pimentel Rodríguez @ Commit Conf 2018 5
  4. iOS App: A Quick History 4 Tuenti original iOS app

    was released in 2008 4 Started by one developer, now more than 40 4 20 just last month 4 Great team follows good practices 4 100% Objective-C ! Víctor Pimentel Rodríguez @ Commit Conf 2018 7
  5. The Only Constant is Change 4 Company Owner ! 4

    Business Model " 4 New Products # 4 New Brands 4 New Programming Language Víctor Pimentel Rodríguez @ Commit Conf 2018 8
  6. 2015: Swift Releases 4 We want to use it but...

    4 Very early to adopt 4 Too many doubts about Swift future 4 >300K lines of pure Objective-C code 4 Interop is a pain 4 We need a PLAN Víctor Pimentel Rodríguez @ Commit Conf 2018 10
  7. Step 0 4 Investigate & play with our project 4

    Add very basic support for Swift 4 Project settings 4 Bridging header 4 Result: the important things work 4 Unit testing is the biggest pain in the Víctor Pimentel Rodríguez @ Commit Conf 2018 11
  8. Step 1 4 Write a one small feature 4 Candidate:

    Contacts Framework support 4 Just for the new iOS 9 4 Self-Contained 4 Easy to revert 4 Result: frustrating but satisfying Víctor Pimentel Rodríguez @ Commit Conf 2018 12
  9. Step 2 4 Write a big feature 4 Candidate: Kill

    Photos project 4 Full-stack 4 Very self-contained 4 Code with an expiration date 4 Result: way more fun, testing Víctor Pimentel Rodríguez @ Commit Conf 2018 13
  10. Step 3 4 Add support for mocks 4 Useful but

    restrictive 4 Everything must be NSObject 4 Weird errors when something is wrong 4 Result: Objective-C code with nicer syntax Víctor Pimentel Rodríguez @ Commit Conf 2018 14
  11. Final Step 4 All new components in Swift 4 Exceptions

    may apply 4 If you significantly change a component... 4 Migrate to Swift if possible 4 Continuously improve the platform 4 Result: See next slide Víctor Pimentel Rodríguez @ Commit Conf 2018 15
  12. Programmer Experience 4 Crazy slow compiling times (10X than before)

    4 Change one file, recompile 100.000 4 Crazy IDE (Xcode) stops working 4 Difficult to navigate 4 Schrödinger syntax hightlighting 4 Interop is very frustrating Víctor Pimentel Rodríguez @ Commit Conf 2018 20
  13. Main Culprit 4 Too many code in one target 4

    Bridging headers grew exponentially 4 Everything ends up interconnected 4 Import everything 4 Too many things exposed to Objective-C 4 Too many things exposed to Swift Víctor Pimentel Rodríguez @ Commit Conf 2018 21
  14. Frameworks to the Rescue 4 Break the import everything problem

    4 Bridging headers get very thin 4 Independent targets 4 Compile just the things you want! 4 But you need some work to do Víctor Pimentel Rodríguez @ Commit Conf 2018 22
  15. Organize Your Code 4 Lightweight platform frameworks 4 Common code,

    helpers, etc 4 Big independent feature frameworks 4 Containing from models to VCs 4 App target glues everything together Víctor Pimentel Rodríguez @ Commit Conf 2018 23
  16. Results 4 Improved compilation times 4 Recompilations are rare 4

    Xcode is very happy 4 New possibilities 4 Faster way of working 4 App targets can import a subset of frameworks Víctor Pimentel Rodríguez @ Commit Conf 2018 25
  17. Unit Testing Madness 4 In 2013 we started to unit

    test our code 4 Unit testing a legacy i OS app... not so great 4 Lots of singletons, hidden dependencies and side effects 4 You can use some frameworks 4 Code is very brittle and easy to break Víctor Pimentel Rodríguez @ Commit Conf 2018 27
  18. Meet Dependency Injection 4 It just means that you don't

    create any dependency 4 Your dependencies are passed to you 4 Usually as constructor parameters 4 Easier way for tests to control side effects 4 No hidden dependencies Víctor Pimentel Rodríguez @ Commit Conf 2018 28
  19. Dependency Injection Example class ChatPresenter { let getMessages: GetMessages init(getMessages:

    GetMessages) { self.getMessages = getMessages } } // Production code let presenter = ChatPresenter(getMessages: GetMessages(...)) // Test code let presenter = ChatPresenter(getMessages: GetMessagesMock(...)) Víctor Pimentel Rodríguez @ Commit Conf 2018 29
  20. Then Who Creates Objects? 4 Some strategy needed to build

    the object graph 4 We decide that only the AppDelegate know about it 4 One component creates every component in the app 4 Tedious code to write in Objective-C Víctor Pimentel Rodríguez @ Commit Conf 2018 30
  21. Typhoon 4 We chose this Objective-C framework 4 Assembly: everything

    is one class, split by extensions 4 So everything must live in the app target 4 Declare dynamically what you need to build Víctor Pimentel Rodríguez @ Commit Conf 2018 31
  22. Swift ! Typhoon 4 If you need to inject a

    Swift component 4 It must be visible to Objective-C 4 So, NSObject-only 4 Interop compromises 4 Costly to migrate to newer versions 4 Pain to write, no autocompletion help Víctor Pimentel Rodríguez @ Commit Conf 2018 32
  23. Ditch Dynamism 4 Why not write everything in pure Swift?

    4 Less boilerplate: type inference 4 Autocompletion helps more 4 Most errors are caught in compile time 4 Huge task: migrate thousands of Typhoon definitions 4 Go framework by framework! Víctor Pimentel Rodríguez @ Commit Conf 2018 33
  24. My First Manual Assembly class PlatformAssembly { // Singleton let

    storage = Storage() // Singleton with dependencies lazy var statisticsClient = StatisticsClient(storage: storage) // Fresh instance func apiClient() -> APIClient { return APIClient(...) } } Víctor Pimentel Rodríguez @ Commit Conf 2018 35
  25. My Second Manual Assembly class ChatAssembly { // Assemblies compose

    let platform: PlatformAssembly init(platform: PlatformAssembly) { self.platform = platform } lazy var getMessages = GetMessages(platform.apiClient()) func presenter() -> ChatPresenter { return ChatPresenter(getMessages: getMessages) } } Víctor Pimentel Rodríguez @ Commit Conf 2018 36
  26. My Third Manual Assembly class AppAssembly { // Same thing

    all the way! let platform = PlatformAssembly() lazy var chat = ChatAssembly(platform: platform) } Víctor Pimentel Rodríguez @ Commit Conf 2018 37
  27. AppDelegate Integration import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {

    // Simplest integration let assembly = AppAssembly() lazy var window = assembly.window } Víctor Pimentel Rodríguez @ Commit Conf 2018 38
  28. Providers: a Special Case 4 Sometimes an object may want

    to create several instances of another object 4 You can use the Factory pattern here... 4 But propagation of dependencies is harder 4 And we can avoid having objects that create objects other than the Assembly Víctor Pimentel Rodríguez @ Commit Conf 2018 39
  29. Object receives a Provider dependency class ChatNavigator { // Use

    of unowned is an important rule unowned let provider: ChatViewControllerProvider init(provider: ChatViewControllerProvider) { self.provider = provider } func navigate(toId chatId: ChatId) { let vc = provider.chatViewController(chatId: ChatId) ... } } Víctor Pimentel Rodríguez @ Commit Conf 2018 41
  30. Assembly Conforms to that Protocol class ChatAssembly { // Notice

    the self! lazy var chatNavigator = ChatNavigator(provider: self) } extension ChatAssembly: ChatViewControllerProvider { func chatViewController(withId chatId: ChatId) { return ChatViewController(..., chatId: chatId) } } Víctor Pimentel Rodríguez @ Commit Conf 2018 42
  31. Results 4 Improved compilation times again! 4 Overall less code,

    specially Objective-C 4 Less unwanted NSObjects 4 Each framework knows how to build itself 4 Less duplication between app/widgets 4 Xcode helps us now -> Happier programmers! Víctor Pimentel Rodríguez @ Commit Conf 2018 43
  32. Conclusions 4 It's a bumpy road 4 Not great tooling

    4 Time: compromises between business and tech 4 Don't be afraid, everything can be done! 4 Listen to the community 4 Tackle the lowest-hanging apple and Víctor Pimentel Rodríguez @ Commit Conf 2018 45
  33. Where To Go From Here? [x] Remove compilation flags other

    than DEBUG [x] Replace CocoaPods with Carthage [x] Move to multi-targets, add an app framework [ ] Move to plugin-based architecture [ ] Autogenerate assemblies ( Karumi blog) [ ] Migrate to React Native? Víctor Pimentel Rodríguez @ Commit Conf 2018 46