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

Building iOS apps at scale

Yusei Nishiyama
September 06, 2017

Building iOS apps at scale

Working with a large code base in a large distributed team involves a lot of challenges. You need to deal with complex workflows, slow build times, communications across different time zones, etc.

In this talk, Yusei will share how development teams can tackle these issues and speed up daily development. This talk will also cover the following topics:

- Workflow automation with Fastlane
- Code review with Danger and SwiftLint
- Collecting and visualizing code metrics with InfluxDB and Grafana
- Build time reduction
- Code modularization

Yusei Nishiyama

September 06, 2017
Tweet

More Decks by Yusei Nishiyama

Other Decks in Technology

Transcript

  1. • iOS, Android, Web • 67 countries • 21 languages

    • 60M monthly users (Japan) • 30M+ monthly users 
 (Outside Japan) https://info.cookpad.com/en https://cookpad.com/en
  2. Working at Cookpad is … • Working in a large

    team • Working with large code base
  3. Working at Cookpad is … • Working in a large

    team • Working with a large code base
  4. • Someone broke the build • We can’t release today

    because {name} is on vacation • Code review never finishes
  5. Sync dSYMs desc "Refresh dSYMs" lane :refresh_dsyms do version =

    get_version_number build_number = get_build_number # Download dSYM files from iTC into a temporary directory download_dsyms(version: version, build_number: build_number) # Upload them to Crashlytics upload_symbols_to_crashlytics # Delete the temporary dSYM files clean_build_artifacts end
  6. Release the App Spaceship::Tunes.login app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) edit_version = Spaceship::Application.find(app_identifier).edit_version

    version_to_release = edit_version.version UI.user_error! "No pending verion to release" unless edit_version.app_status == "Pending Developer Release" edit_version.release! UI.success "Version #{version_to_release} is released " slack( channel: "#release", attachment_properties: { pretext: ":appleinc: *Cookpad iOS #{version_to_release} - Released*” }) refresh_dsyms
  7. Check Labels desc "Check if pr is marked as beta

    distribution" private_lane :beta_distribution do labels.any? { |label| label == 'beta distribution' } end desc "Get labels of pull request" private_lane :labels do pr_number = ENV['PR_NUMBER'] repo = ENV['GITHUB_PULL_REQUEST_REPO'] result = github_api( http_method: 'GET', path: "/repos/#{repo}/issues/#{pr_number}/labels" ) result[:json].map { |label| label['name'] } end
  8. Notify a New Build desc "Notify new beta distribution" private_lane

    :notify_build do messages = "A new version is available" pr_number = ENV['PR_NUMBER'] repo = ENV['GITHUB_PULL_REQUEST_REPO'] github_api( http_method: 'POST', path: "/repos/#{repo}/issues/#{pr_number}/comments", body: { "body": message } ) end
  9. Danger Danger runs during your CI process, and gives teams

    the chance to automate common code review chores. This provides another logical step in your build, through this Danger can help lint your rote tasks in daily code review. You can use Danger to codify your teams norms. Leaving humans to think about harder problems. She does this by leaving messages inside your PRs based on rules that you create with the Ruby scripting language. Over time, as rules are adhered to, the message is amended to reflect the current state of the code review http://danger.systems/ruby/
  10. Dangerfile github.dismiss_out_of_range_messages({ error: false, warning: true, message: true, markdown: true

    }) xcode_summary.inline_mode = true xcode_summary.ignored_files = ['Pods/**'] xcode_summary.ignored_results { |result| result.message.start_with? 'ld' } log = File.join('logs', 'test.json') xcode_summary.report log if File.exist?(log) swiftlint.binary_path = './Pods/SwiftLint/swiftlint' swiftlint.lint_files inline_mode: true
  11. InfluxDB • Only purpose-built platform for any time-based data •

    Written in Go • Built-in HTTP API • SQL-like query language • Answer queries in real-time • Rich client libraries
  12. Post Code Coverage lane :post_coverage do rate = export_coverage if

    rate influxdb(table_name: table_name, values: {coverage: rate}) slack(message: "Code coverage: #{rate.to_f.round(4) * 100}%", channel: “#ios- notifications") end end private_lane :export_coverage do fastlane_require 'nokogiri' output_directory = 'coverage_report' reports = Dir.glob(File.join(ROOT_PATH, output_directory, '*.xml')) slather(use_bundle_exec: true, output_directory: output_directory) if reports.empty? nil else xml = Nokogiri::XML(File.open(reports.first)) coverage = xml.xpath('//coverage').first coverage.attr("line-rate").to_f end end
  13. Grafana • Rich metrics dashboard and editor • Fast rendering

    • Support InfluxDB as a data source • Query editor
  14. Working at Cookpad is … • Working in a large

    team • Working with a large code base
  15. Let’s improve the registration screen… • Implement • Tweak constraints

    • Test with older iOS versions • Fix tests • Wait for CI
  16. What I will “not” talk about • Annotate all types

    explicitly • Write as much as code in a single file • Get your Mac Pro
  17. fastlane/report.xml <?xml version="1.0" encoding="UTF-8"?> <testsuites> <testsuite name="fastlane.lanes"> <testcase classname="fastlane.lanes" name="00:

    default_platform" time="0.002078"> </testcase> <testcase classname="fastlane.lanes" name="01: bundle outdated fastlane --strict" time="8.59854"> </testcase> <testcase classname="fastlane.lanes" name="02: ensure_git_status_clean" time="0.017918"> </testcase> <testcase classname="fastlane.lanes" name="03: Switch to ios bump_build_number lane" time="0.001982"> </testcase> … <testcase classname="fastlane.lanes" name="17: hockey" time="263.575088"> </testcase> <testcase classname="fastlane.lanes" name="18: clean_build_artifacts" time="0.002326"> </testcase> </testsuite> </testsuites>
  18. Build Process • Dependency Manager (CocoaPods, Carthage) • Compile source

    files • Function A • Function B • … • Runs Scripts (Linter, Formatter)
  19. xcactivitylog • You can access build logs under DerivedData •

    xcactivitylog is just gzip. You can check the content as a plain text just by unzipping it
  20. Test with Real Data $ cloc
 
 github.com/AlDanial/cloc v 1.72

    T=3.40 s (153.3 files/s, 16646.6 lines/s) ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- Swift 497 8944 3122 43831 JSON 18 0 0 522 Objective C 3 37 5 166 C/C++ Header 4 19 6 27 ------------------------------------------------------------------------------- SUM: 522 9000 3133 44546 -------------------------------------------------------------------------------
  21. Test with Real Data • 40,000 lines of Swift code

    • CocoaPods • 38 direct dependencies and 60 libraries • Linter in build phases • CI archives the app and distributes it for internal testing
  22. F

  23. Carthage + github "ReactiveX/RxSwift" ~> 3.4 + github "RxSwiftCommunity/RxRealm" +

    github "realm/realm-cocoa" ~> 2.4 + github "danielgindi/Charts" ~> 3.0.2 - pod 'RxSwift', '3.3.1' - pod 'RxCocoa', '3.3.1' - pod 'RxRealm', '0.5.2' - pod 'RealmSwift', '2.4.4' Cartfile Podfile
  24. Prototyping with Playground • Test your idea in Playground 


    (interface, design, reproducing a bug, etc.) • Once your idea becomes concrete, move those code into the application project
  25. You need “existing” code 
 for prototyping class ViewController: UITableViewController

    { let myView = MyView() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .myColor view.addSubview(myView) } }
  26. git submodule • Simple (!= Easy) • Full control •

    Manual works • Complex workflow
  27. git subtree • An alternative to git submodule • Clones

    a subproject and merge it into the parent project • `git subtree push` goes through the commits and picking the changes that should go to a subproject
  28. git subtree • Requires no change to workflow • Need

    to learn a new merge strategy • Contributing back to upstream is a bit tricky
  29. Lesson Learned • Breaking existing implicit dependencies is hard •

    Starting from the single repository • Once modules become stable, consider • managing them in separated repositories • using pre-build versions
  30. Directory Structure . "## MainApp $ "## MainApp $ $

    "## AppDelegate.swift $ $ "## Info.plist $ $ &## ViewController.swift $ "## MainApp.xcodeproj $ &## MainAppTests $ "## Info.plist $ &## MainAppTests.swift "## Library $ "## Library $ $ "## Info.plist $ $ "## Library.h $ $ &## Library.swift $ "## Library.xcodeproj $ &## LibraryTests $ "## Info.plist $ &## LibraryTests.swift "## MainApp.xcworkspace "## Podfile "## Podfile.lock "## Pods &## README.md
  31. Podfile source 'https://github.com/CocoaPods/Specs.git' use_frameworks! platform :ios, '9.3' workspace 'MainApp.xcworkspace' target

    'MainApp' do project 'MainApp/MainApp.xcodeproj' # Dependencies of the MainApp end target 'Library' do project 'Library/Library.xcodeproj' # Dependencies of the Library end
  32. dylib Loading Time Total pre-main time: 4.2 seconds (100.0%) dylib

    loading time: 4.0 seconds (94.4%) rebase/binding time: 93.32 milliseconds (2.1%) ObjC setup time: 44.64 milliseconds (1.0%) initializer time: 98.30 milliseconds (2.3%)
  33. Recap • Automate daily workflows with CI and Fastlane •

    Reduce workload of code review with Danger • Visualize code metrics you want to improve • Revising project settings may improve build time dramatically • Code modularization improves build/index time and helps you getting well structured code/teams