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

Developing modular apps on iOS

Developing modular apps on iOS

Slides from my talk at NSPresenter Madrid on February 28th

Pedro Piñera Buendía

February 26, 2018
Tweet

More Decks by Pedro Piñera Buendía

Other Decks in Technology

Transcript

  1. Developing modular apps on
    iOS
    NSPresenter Madrid - February 28th
    @pepibumur
    1

    View Slide

  2. About me
    — Production Engineer at
    Shopify.
    — Building tools for mobile
    developers.
    — Born and raised in the

    Murcia.
    — Suffering the

    in Berlin.

    #
    ppinera.es

    $
    @pepibumur

    %
    [email protected]
    2

    View Slide

  3. What/why/how
    should I modularize my app?
    @pepibumur 3

    View Slide

  4. What
    @pepibumur 4

    View Slide

  5. "Modularizing an app consists on
    organising your app code and
    resources in multiple modules"
    @pepibumur 5

    View Slide

  6. A module can be
    — A library: If it doesn't have resources.
    — A framework: If it has them.
    — Static: If it's linked at compile time.
    — Dynamic: If it's linked at startup time.
    @pepibumur 6

    View Slide

  7. Static linking - Compilation time
    Dynamic linking - Startup time
    @pepibumur 7

    View Slide

  8. Dynamic modules need to be
    copied into the product for
    dylib to link them
    @pepibumur 8

    View Slide

  9. Static modules not properly
    linked might lead to duplicated
    symbols
    @pepibumur 9

    View Slide

  10. Carthage & CocoaPods are two
    examples of tools that
    modularise your dependencies
    Carthage: Dynamic linking
    CocoaPods: Static & dynamic linking
    @pepibumur 10

    View Slide

  11. Most of apps are usually built
    and grown as monoliths
    1 target with the app source
    1 target with the tests
    (And maybe some extensions)
    @pepibumur 11

    View Slide

  12. Why
    @pepibumur 12

    View Slide

  13. If you are a freelancer
    You might want to reuse code
    @pepibumur 13

    View Slide

  14. If you have multiple products
    !⌚#
    You might want to reuse code
    (e.g. Shopify has 5 iOS apps)
    @pepibumur 14

    View Slide

  15. If you want to write weakly
    coupled components
    You can leverage modules interfaces
    (internal by default in Swift)
    @pepibumur 15

    View Slide

  16. If you want to speed up your
    workflow

    And spend less time waiting for the compiler
    @pepibumur 16

    View Slide

  17. The compile time is
    proportional to the size of the
    codebase
    @pepibumur 17

    View Slide

  18. But...
    Apple is improving the Swi! compiler
    The compiler builds incrementally (only clean builds
    should take more time)
    @pepibumur 18

    View Slide

  19. — ⌘ + B (10 minutes)
    — Change code + (⌘ + B) (4 Seconds)
    — Change something + (⌘ + B) - (Error)
    — ⌘ + K (I'm sure this helps)
    — ⌘ + B (10 minutes)
    @pepibumur 19

    View Slide

  20. — 30 developers.
    — 10 clean builds per day.
    — 10 minutes per clean build.
    1000 hours spent per month
    6.25 engineers
    @pepibumur 20

    View Slide

  21. !
    Fortunately, there's
    something you can do to avoid
    this
    @pepibumur 21

    View Slide

  22. How
    @pepibumur 22

    View Slide

  23. This is just one approach
    That works in my experience
    @pepibumur 23

    View Slide

  24. Layers
    Project
    Downsides
    @pepibumur 24

    View Slide

  25. Before anyone asks...
    @pepibumur 25

    View Slide

  26. Yes, to avoid the hassle of
    dealing with versioning or git
    submodules, all the modules in
    the same repository
    @pepibumur 26

    View Slide

  27. Microfeatures
    github.com/pepibumur/microfeatures-guidelines
    @pepibumur 27

    View Slide

  28. @pepibumur 28

    View Slide

  29. App module
    Navigation
    App lifecycle events
    Dependency injector
    @pepibumur 29

    View Slide

  30. // App
    import Core
    class Registry {
    static let client = Client(url: "https://shopify.com")
    static let analytics = Tracker(providers: [.firebase])
    static let logger = Logger()
    }
    @pepibumur 30

    View Slide

  31. import Product
    import Home
    // Module: App
    class HomeCoordinator: HomeDelegate {
    let navigationController: UINavigationController!
    // HomeDelegate
    func didSelectProduct(id: String) {
    let vc = ProductFeature(id: id, client: Registry.client, analytics: Registry.analytics)
    .viewController
    navigationController.pushViewController(productViewController)
    }
    }
    @pepibumur 31

    View Slide

  32. Features modules
    Can be product or service features
    @pepibumur 32

    View Slide

  33. import Core
    // Module: Product
    public class ProductFeature {
    public init(id: String, client: Client, analytics: Analytics) {
    // Initialization
    }
    // !! We just expose UIKit classes
    public var viewController: UIViewController {
    // Initialization & dependency injection
    }
    }
    @pepibumur 33

    View Slide

  34. import Core
    // Module: BackgroundSync
    public class BackgroundSyncer {
    public init(client: Client, store: Store) { /* Init */ }
    public func sync(completion: @escaping (Error?) -> Void)
    }
    // Module: App
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
    performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    Registry.backgroundSyncer.sync { error in
    // Notify completionHandler
    }
    }
    }
    @pepibumur 34

    View Slide

  35. Shared components are
    extracted into foundation
    modules
    @pepibumur 35

    View Slide

  36. // Module: Core
    public class Client: Clienting {}
    public class Logger: Logging {}
    public class Store: Storing {}
    public class SecureStore: SecureStoring {}
    // Module: CoreTesting
    public class MockClient: Clienting {
    public var executeCount: UInt!
    public var executeRequest: URLRequest!
    public var executeStub: (Any?, Error?)!
    }
    @pepibumur 36

    View Slide

  37. import XCTest
    import Core
    import CoreTesting //
    !
    @testable import BackgroundSync
    final class BackgroundSyncerTests: XCTestCase {
    var client: MockClient!
    Var store: MockStore!
    var subject: BackgroundSyncer!
    override func setUp() {
    super.setUp()
    client = MockClient()
    store = MockStore()
    subject = BackgroundSyncer(client: client, store: store)
    }
    }
    @pepibumur 37

    View Slide

  38. // Module: Testing
    import XCTest
    public extension XCTestCase {
    public func XCTAssertTapa(_ tapa: Tapa,
    identifier: String? = nil,
    tolerance: CGFloat = 0,
    file: StaticString = #file,
    line: UInt = #line) {
    // Asserts the quality of the tapa
    }
    }
    @pepibumur 38

    View Slide

  39. // Module: UI
    public class Color {}
    public class Font {}
    public class CustomView {}
    @pepibumur 39

    View Slide

  40. @pepibumur 40

    View Slide

  41. Examples app
    Allow developers to try out the features that they build
    @pepibumur 41

    View Slide

  42. 1 Workspace
    Multiple projects
    (fewer git conflicts per project)
    @pepibumur 42

    View Slide

  43. There are not so cool features
    that I didn't mention
    (until now)
    @pepibumur 43

    View Slide

  44. From the monolith to modules
    !
    @pepibumur 44

    View Slide

  45. 1. Extract build se!ings into config files.
    2. Extract foundation components/extensions.
    3. Build new features in modules.
    4. Gradually extract the existing features.

    Extracting is not easy

    @pepibumur 45

    View Slide

  46. Maintaining more than one
    Xcode project
    @pepibumur 46

    View Slide

  47. Tips to ease maintenance
    1. Extract settings into .xcconfig files (a reusable single
    source of truth).
    2. Automate the generation of Xcode projects (e.g. using
    XcodeGen).
    3. Just Debug and Release configurations.
    @pepibumur 47

    View Slide

  48. Continuous integration
    (Usually clean builds)
    @pepibumur 48

    View Slide

  49. Tips to speed up CI builds
    1. Enable parallelize builds on your schemes.
    2. Selective builds on CI.
    Modules:
    - name: Search
    path: Search/Sources/**
    build: xcodebuild -workspace Search.xcworkspace -scheme Search
    dependencies:
    - Core
    - name: Core
    path: Core/Sources/**
    build: xcodebuild -workspace Core.xcworkspace -scheme Core
    @pepibumur 49

    View Slide

  50. Catalisis (coming soon)
    — Command line tool.
    — Generates workspaces and projects for the module
    you are working on.
    — Caches modules to save time on CI and locally.
    — Offers an API to build/test modules.
    @pepibumur 50

    View Slide

  51. # Examples
    catalisis generate-xcodeproj App
    catalisis build Core
    catalisis test Orders
    @pepibumur 51

    View Slide

  52. Conclusions
    — Do it of you really need it.
    — (reuse or decouple code, save time)
    — More independent and productive teams.
    — Apple is also improving things.
    — It comes with some costs and lack of tooling (tools
    are optimized for monoliths)
    — A transition into modules is tough.
    @pepibumur 52

    View Slide

  53. Thanks!
    Slides: h!ps://goo.gl/KAC1EJ
    @pepibumur 53

    View Slide