$30 off During Our Annual Pro Sale. View Details »

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. 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
  2. "Modularizing an app consists on organising your app code and

    resources in multiple modules" @pepibumur 5
  3. 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
  4. Carthage & CocoaPods are two examples of tools that modularise

    your dependencies Carthage: Dynamic linking CocoaPods: Static & dynamic linking @pepibumur 10
  5. 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
  6. If you have multiple products !⌚# You might want to

    reuse code (e.g. Shopify has 5 iOS apps) @pepibumur 14
  7. If you want to write weakly coupled components You can

    leverage modules interfaces (internal by default in Swift) @pepibumur 15
  8. If you want to speed up your workflow ⏰ And

    spend less time waiting for the compiler @pepibumur 16
  9. But... Apple is improving the Swi! compiler The compiler builds

    incrementally (only clean builds should take more time) @pepibumur 18
  10. — ⌘ + B (10 minutes) — Change code +

    (⌘ + B) (4 Seconds) — Change something + (⌘ + B) - (Error) — ⌘ + K (I'm sure this helps) — ⌘ + B (10 minutes) @pepibumur 19
  11. — 30 developers. — 10 clean builds per day. —

    10 minutes per clean build. 1000 hours spent per month 6.25 engineers @pepibumur 20
  12. Yes, to avoid the hassle of dealing with versioning or

    git submodules, all the modules in the same repository @pepibumur 26
  13. // 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
  14. 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
  15. 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
  16. 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
  17. // 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
  18. 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
  19. // 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
  20. // Module: UI public class Color {} public class Font

    {} public class CustomView {} @pepibumur 39
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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