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

Building 5 Calls for iOS

Building 5 Calls for iOS

A talk I gave in Barcelona. About 1/3 political, 1/3 technical, and 1/3 tactical.

023a6a37e8177cb2f84a236bbce643cf?s=128

Ben Scheirman

April 27, 2017
Tweet

Transcript

  1. Building

  2. Ben Scheirman benscheirman.com @subdigital

  3. ficklebits.com

  4. 260+ screencasts on iOS development nsscreencast.com

  5. None
  6. None
  7. None
  8. What do we do?

  9. None
  10. None
  11. None
  12. What do we do?

  13. What can we do?

  14. What can I do?

  15. None
  16. None
  17. United States Congress House of Representatives Senate 435 Members 100

    Members
  18. None
  19. None
  20. None
  21. 5calls.org

  22. None
  23. None
  24. None
  25. Launch ASAP

  26. None
  27. Wanted it to be a GOOD iOS app

  28. None
  29. github.com/5calls/ios

  30. None
  31. Let people help you!

  32. None
  33. Built on NSOperation Storyboards UITableView CocoaPods Vibrancy / Blur Dynamic

    Type Custom UIViewController Containment Local Push Notifications Fastlane Pantry Swift 3 Universal app
 (split view) Fabric / Crashlytics R.swift Buddybuild
  34. NSOperations class BaseOperation : Operation { override var isAsynchronous: Bool

    { return true } 
 private var _executing = false { willSet { willChangeValue(forKey: "isExecuting") } didSet { didChangeValue(forKey: "isExecuting") } } override var isExecuting: Bool { return _executing } private var _finished = false { willSet { willChangeValue(forKey: "isFinished") } didSet { didChangeValue(forKey: "isFinished") } } override var isFinished: Bool { return _finished } class BaseOperation : Operation { func execute() { fatalError("You must override this") } func finish() { _executing = false _finished = true } }
  35. NSOperations class FetchIssuesOperation : BaseOperation { let location: UserLocation? init(location:

    UserLocation?) { self.location = location }
 
 ... }
  36. NSOperations override func execute() { let task = session.dataTask(with: url)

    { 
 (data, response, error) in if let e = error { print("Error fetching issues: \ (e.localizedDescription)”) } else { self.handleResponse(data: data, response: response) } self.finish() } task.resume() }
  37. NSOperations self.issuesList = IssuesList(dictionary: json)

  38. NSOperations func fetchIssues(completion: @escaping (IssuesLoadResult) -> Void) { let operation

    = FetchIssuesOperation(location: userLocation) operation.completionBlock = { [weak self, weak operation] in if let issuesList = operation?.issuesList { self?.issuesList = issuesList DispatchQueue.main.async { completion(.success) } } else {
 // …
 }
 
 } OperationQueue.main.addOperation(operation) }
  39. NSOperations FetchIssuesOperation FetchStatsOperation ReportOutcomeOperation

  40. NSOperations Composable Dependencies Cancellable Control Concurrency Quality of Service

  41. NSOperations https://developer.apple.com/videos/play/wwdc2015/226/ WWDC Session 226 - Advanced NSOperations http://nsscreencast.com/ NSScreencast

    Episodes 175-177, 180
  42. Dynamic Type Dynamic Type Dynamic Type

  43. Using a custom font made this more difficult

  44. None
  45. None
  46. Getting Dynamic Type to work with UITableViewCells Define height with

    constraints
  47. Getting Dynamic Type to work with UITableViewCells

  48. Respond to Dynamic Type Changes UIContentSizeCategoryDidChangeNotification adjustsFontsForContentSizeCategory iOS 10.0

  49. None
  50. Getting dynamic fonts in code headline.font = UIFont.preferredFont( forTextStyle: UIFontTextStyleHeadline)

    subhead.font = UIFont.preferredFont( forTextStyle: UIFontTextStyleSubheadline) body.font = UIFont.preferredFont( forTextStyle: UIFontTextStyleBody)
  51. https://github.com/nickoneill/Pantry if let available: Bool = Pantry.unpack("promptAvailable") { completion(available: available)

    } else { anExpensiveOperationToDetermineAvailability({ (available) -> () in Pantry.pack(available, key: "promptAvailable", expires: .Seconds(60 * 10)) completion(available: available) }) }
  52. var autopersist: String? { set { if let newValue =

    newValue { Pantry.pack(newValue, key: "autopersist") } } get { return Pantry.unpack("autopersist") } } ...later... autopersist = "Hello!" // restart app, reboot phone, etc print(autopersist) // Hello!
  53. SIMPLE MODEL OBJECT…

  54. CONFORM TO STORABLE

  55. USAGE IS REALLY SIMPLE:

  56. CAREFUL YOU DON’T STORE USER DATA IN CACHES

  57. R.swift

  58. R.swift let icon = UIImage(named: "settings-icon") let icon = R.image.settingsIcon()

    becomes…
  59. R.swift let viewController = CustomViewController(nibName: "CustomView", bundle: nil) let viewController

    = CustomViewController(nib: R.nib.customView) becomes…
  60. R.swift let string = String(format: NSLocalizedString("welcome.withName", comment: ""), 
 locale:

    NSLocale.current, "Arthur Dent") let string = R.string.localizable.welcomeWithName("Arthur Dent") becomes…
  61. R.swift

  62. None
  63. None
  64. # Fastfile 
 desc "Runs all the tests" lane :test

    do scan(workspace: workspace, scheme: scheme) end
  65. $ fastlane test

  66. desc "Increments build number" lane :increment_build do increment_build_number(xcodeproj: xcodeproj) end

  67. None
  68. desc "Submit a new Beta Build to Apple TestFlight" lane

    :beta do ensure_git_status_clean increment_build commit_version_bump badge(shield: "Version-#{app_version}-red", shield_no_resize: true) gym(workspace: workspace, scheme: scheme) changelog = prompt_for_release_notes pilot(changelog: changelog) git_commit(path: 'fastlane/changelog.txt', message: 'Updated changelog.txt') reset_git_repo(files: app_icon_files) add_git_tag push_git_tags end
  69. desc "Submit a new Beta Build to Apple TestFlight" lane

    :beta do ensure_git_status_clean increment_build commit_version_bump badge(shield: "Version-#{app_version}-red", shield_no_resize: true) gym(workspace: workspace, scheme: scheme) changelog = prompt_for_release_notes pilot(changelog: changelog) git_commit(path: 'fastlane/changelog.txt', message: 'Updated changelog.txt') reset_git_repo(files: app_icon_files) add_git_tag push_git_tags end
  70. desc "Submit a new Beta Build to Apple TestFlight" lane

    :beta do ensure_git_status_clean increment_build commit_version_bump badge(shield: "Version-#{app_version}-red", shield_no_resize: true) gym(workspace: workspace, scheme: scheme) changelog = prompt_for_release_notes pilot(changelog: changelog) git_commit(path: 'fastlane/changelog.txt', message: 'Updated changelog.txt') reset_git_repo(files: app_icon_files) add_git_tag push_git_tags end
  71. desc "Submit a new Beta Build to Apple TestFlight" lane

    :beta do ensure_git_status_clean increment_build commit_version_bump badge(shield: "Version-#{app_version}-red", shield_no_resize: true) gym(workspace: workspace, scheme: scheme) changelog = prompt_for_release_notes pilot(changelog: changelog) git_commit(path: 'fastlane/changelog.txt', message: 'Updated changelog.txt') reset_git_repo(files: app_icon_files) add_git_tag push_git_tags end
  72. desc "Submit a new Beta Build to Apple TestFlight" lane

    :beta do ensure_git_status_clean increment_build commit_version_bump badge(shield: "Version-#{app_version}-red", shield_no_resize: true) gym(workspace: workspace, scheme: scheme) changelog = prompt_for_release_notes pilot(changelog: changelog) git_commit(path: 'fastlane/changelog.txt', message: 'Updated changelog.txt') reset_git_repo(files: app_icon_files) add_git_tag push_git_tags end
  73. def prompt_for_release_notes `open changelog.txt -W` File.read('changelog.txt') end

  74. desc "Submit a new Beta Build to Apple TestFlight" lane

    :beta do ensure_git_status_clean increment_build commit_version_bump badge(shield: "Version-#{app_version}-red", shield_no_resize: true) gym(workspace: workspace, scheme: scheme) changelog = prompt_for_release_notes pilot(changelog: changelog) git_commit(path: 'fastlane/changelog.txt', message: 'Updated changelog.txt') reset_git_repo(files: app_icon_files) add_git_tag push_git_tags end
  75. desc "Submit a new Beta Build to Apple TestFlight" lane

    :beta do ensure_git_status_clean increment_build commit_version_bump badge(shield: "Version-#{app_version}-red", shield_no_resize: true) gym(workspace: workspace, scheme: scheme) changelog = prompt_for_release_notes pilot(changelog: changelog) git_commit(path: 'fastlane/changelog.txt', message: 'Updated changelog.txt') reset_git_repo(files: app_icon_files) add_git_tag push_git_tags end
  76. Automatic Screenshots # Snapfile devices([
 "iPhone 7"
 "iPhone 7 Plus",


    "iPhone SE",
 "iPad Pro (12.9 inch)"
 ]) languages(["en-US"])
 
 workspace "./FiveCalls/FiveCalls.xcworkspace"
 scheme "FiveCallsUITests" output_directory "./screenshots"
 clear_previous_screenshots true
 launch_arguments(["-hasShownWelcomeScreen false"])
  77. Automatic Screenshots

  78. Deterministic Data? How do we ALWAYS choose the same item,

    despite the server changing?
  79. Stubs! {
 “issues”: [
 … ] 
 } issues.json INTERNET

    GET /issues UI TESTING? YES NO
  80. None
  81. None
  82. SeededURLSession SeededDataTask dataTask(with:completion:)

  83. None
  84. None
  85. HOST APP TESTS ENV

  86. TESTS

  87. None
  88. None
  89. None
  90. The Stats

  91. Week 1 6,300 downloads

  92. None
  93. None
  94. Month 1 76,916 downloads

  95. None
  96. All Time (4/21/2017) 105,000 downloads

  97. 44.2k Monthly Active Users 2.4k Daily Active Users

  98. None
  99. Over 1 million calls to congress

  100. Final Thoughts

  101. !

  102. Track Useful Analytics *but don’t be scummy

  103. None
  104. None
  105. None
  106. REDMAP

  107. None
  108. Software Development is an incredibly valuable skill.

  109. Software Development is an incredibly valuable skill. Use your powers

    for good, not evil.
  110. Ben Scheirman benscheirman.com @subdigital nsscreencast.com ¡Gracias!