Rebuilding of Product Hunt for iOS

Rebuilding of Product Hunt for iOS

Learning from working on Product Hunt for iOS

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

May 14, 2016
Tweet

Transcript

  1. None
  2. Radoslav Stankov @rstankov http://rstankov.com http://github.com/rstankov

  3. Vladimir Vladimirov @DeVladinci http://github.com/DeVladinci

  4. None
  5. None
  6. Version 1.0

  7. None
  8. Pretty standard iOS app • Storyboards • Uses web API

    • Decent data layer • … like a Apple tutorial
  9. Version 2.0

  10. None
  11. None
  12. None
  13. None
  14. None
  15. None
  16. None
  17. class ViewController : PHParallaxViewController { 
 override func viewDidLoad() {

    super.viewDidLoad() header = … content = … } }
  18. class ViewController : PHParallaxViewController { 
 override func viewDidLoad() {

    super.viewDidLoad() header = … content = … } }
  19. class ViewController : PHParallaxViewController { 
 override func viewDidLoad() {

    super.viewDidLoad() header = … content = … } }
  20. class ViewController : PHParallaxViewController { 
 override func viewDidLoad() {

    super.viewDidLoad() header = … content = … } }
  21. class ViewController : PHParallaxViewController { 
 override func viewDidLoad() {

    super.viewDidLoad() header = … content = … } }
  22. class PHCollectionViewController : PHParallaxViewController { var collection = PHCollection() override

    func viewDidLoad() { super.viewDidLoad() header = PHCollectionHeaderView(collection) content = PHPostListViewController(collection) } }
  23. class PHCollectionHeaderView : UIView { // … } extension PHCollectionHeaderView

    : PHParallaxHeaderSource { var backgroundImageUrl: String { return collection.imagePath } var height: Int { return 40 } }
  24. Parallax View Controller

  25. Parallax View Controller Parallax Header

  26. Parallax View Controller Parallax Header Custom Header

  27. Parallax View Controller Parallax Header Custom Header Custom View Controller

  28. Parallax View Controller Parallax Header Custom Header Custom View Controller

  29. Parallax View Controller Parallax Header Custom Header Custom View Controller

  30. Parallax View Controller Parallax Header Custom Header Custom View Controller

    Background Image Height
  31. Parallax View Controller Parallax Header Custom Header Custom View Controller

  32. Parallax View Controller Parallax Header Custom Header Custom View Controller

  33. Parallax View Controller Parallax Header Custom Header Custom View Controller

    scrollView. panGestureRecognizer
  34. Parallax View Controller Parallax Header Custom Header Custom View Controller

  35. Parallax View Controller Parallax Header Custom Header Custom View Controller

  36. Parallax View Controller

  37. Parallax View Controller

  38. Parallax View Controller

  39. Parallax View Controller

  40. None
  41. None
  42. None
  43. None
  44. 
 View Controller

  45. 
 View Controller Table View

  46. 
 View Controller Table View Cell

  47. 
 View Controller Table View Cell State View

  48. UITableViewController + • Load data from server • Loading and

    empty state • Pull to refresh • Infinite scrolling • Dynamic cell height
  49. PHListView

  50. List Data Source Post Data Source List Cell Post Cell

    PHListViewController(dataSource: dataSource, cell: PHPostCell.Type) 
 View Controller Table View Cell State View
  51. PHDataSource • numberOfSections • numberOfRowsInSection • dataOfIndexPath

  52. class PHListArrayDataSource<T> : PHListDataSource<T> { var content = [T]() convenience

    init(_ content: [T]) { self.init() self.content = content } override func numberOfSections() -> Int { return 1 } override func numberOfRowsInSection(section: Int) -> Int { return content.count } override func dataAtIndexPath(indexPath: NSIndexPath) -> T? { return content[indexPath.row] } }
  53. let dataSource = PHListArrayDataSource<PHPost>(listOfPosts) let vc = PHListViewController(dataSource: dataSource, cell:

    PHPostCell.Type)
  54. None
  55. PHDataSource • initialLoad • loadNewer • loadOlder • moreToLoad

  56. typealias PHPostCallback = ([PHPost]) -> Void typealias PHPostFetch = (lastId:

    Int?, callback: PHPostCallback) -> Void class PHPostListDataSource : PHListArrayDataSource<PHPost> { private var fetch: PHPostFetch? convenience init(fetch: PHPostFetch) { self.init() self.fetch = fetch } override func initialLoad() { loadOlder() } override func loadOlder() { fetch?(lastId: content.last?.id) { (posts) in self.content += posts self.isThereMoreToLoad = !posts.isEmpty self.dataWasChanged() } } }
  57. None
  58. None
  59. Unit tests • Test one object / function • Isolated

    • Fast • Edge cases, nitpickings
  60. 4 Phase Testing

  61. Setup 4 Phase Testing

  62. Setup Action 4 Phase Testing

  63. Setup Action Assertion 4 Phase Testing

  64. Setup Action Assertion Teardown 4 Phase Testing

  65. Setup Action Assertion Teardown 4 Phase Testing

  66. class PHShareMessageTest: XCTestCase { func testBuildFromWithLiveEvent() { let event =

    PHTestFactory.liveEvent() let message = PHShareMessage(event) let expected = "I am chatting with @\(event.twitterName)" XCTAssertEqual(message.text, expected) } }
  67. class PHShareMessageTest: XCTestCase { func testBuildFromWithLiveEvent() { let event =

    PHTestFactory.liveEvent() let message = PHShareMessage(event) let expected = "I am chatting with @\(event.twitterName)" XCTAssertEqual(message.text, expected) } func testBuildFromWithPastLiveEvent() { let event = PHTestFactory.pastLiveEvent() let message = PHShareMessage(event) let expected = "Thanks @\(event.twitterName) for the LIVE Chat" XCTAssertEqual(message.text, expected) } }
  68. class PHShareMessageTest: XCTestCase { func testBuildFromWithLiveEvent() { let event =

    PHTestFactory.liveEvent() let message = PHShareMessage(event) let expected = "I am chatting with @\(event.twitterName)" XCTAssertEqual(message.text, expected) } func testBuildFromWithPastLiveEvent() { let event = PHTestFactory.pastLiveEvent() let message = PHShareMessage(event) let expected = "Thanks @\(event.twitterName) for the LIVE Chat" XCTAssertEqual(message.text, expected) } func testBuildFromWithUpcomingLiveEvent() { let event = PHTestFactory.upcomingLiveEvent() let message = PHShareMessage(event) let expected = "I signed up to chat with @\(event.twitterName)" XCTAssertEqual(message.text, expected) } }
  69. None
  70. UI tests • Tests a full feature • Slow •

    Fake API • KIF
  71. Fake Endpoint Fake Data Data Source API Endpoint

  72. class PHTestUITestCase : KIFTestCase { let tester // -> KIFUITestActor

    + ProductHunt extensions let fake // -> PHTestFake let endpoint // -> PHTestEndpoint override func setUp() { super.setUp() // fake endpoint setup // init state } override func tearDown() { // fake endpoint clear // reset state super.tearDown() } }
  73. class PHTestComments : PHTestUITestCase { func testCommentingFlow() { // setup

    let post = fake.post() tester.loginAsLoggedUser() // comment versions let comment = fake.comment(["user": tester.loggedUserId]) let updatedComment = comment.ph_merge(["body": "updated body"]) // stub requests endpoint.stubPost(post) endpoint.stubMethod(.Post, "/comments", comment) endpoint.stubMethod(.Patch, "/comments/\(comment["id"])", updatedComment) // start tester.openPost(post) tester.waitForViewWithAccessibilityLabel("No Comments") // test submit button appearing tester.clearTextFromAndThenEnterText(comment["body"], intoViewWithAccessibilityLabel: "Comment") // test creating a comment tester.tapViewWithAccessibilityLabel("Submit") tester.waitForViewWithAccessibilityLabel(comment["Body"]) // test updating a comment tester.tapViewWithAccessibilityLabel("Edit comment") tester.clearTextFromAndThenEnterText(updatedComment["body"],
  74. let updatedComment = comment.ph_merge(["body": "updated body"]) // stub requests endpoint.stubPost(post)

    endpoint.stubMethod(.Post, "/comments", comment) endpoint.stubMethod(.Patch, "/comments/\(comment["id"])", updatedComment) // start tester.openPost(post) tester.waitForViewWithAccessibilityLabel("No Comments") // test submit button appearing tester.clearTextFromAndThenEnterText(comment["body"], intoViewWithAccessibilityLabel: "Comment") // test creating a comment tester.tapViewWithAccessibilityLabel("Submit") tester.waitForViewWithAccessibilityLabel(comment["Body"]) // test updating a comment tester.tapViewWithAccessibilityLabel("Edit comment") tester.clearTextFromAndThenEnterText(updatedComment["body"], intoViewWithAccessibilityLabel: "Comment") tester.tapViewWithAccessibilityLabel("Submit") tester.waitForAbsenceOfViewWithAccessibilityLabel(comment["body"]) tester.waitForViewWithAccessibilityLabel(updatedComment["body"]) } }
  75. None
  76. None
  77. https://developer.apple.com/videos/play/wwdc2015/406/ UI Testing in Xcode

  78. Version 3.0

  79. None
  80. class PHOpenPostAction { class func perform(withId id: Int) { let

    vc = PHPostDetailsViewController() vc.postId = postId PHShowViewControllerAction.perform(vc) } }
  81. typealias Completion = () -> Void 
 class PHShowViewControllerAction {

    class func perform(vc: UIViewController, animated: Bool, completion: Completion?) { let delegate = UIApplication.sharedApplication().delegate as! PHAppDelegate var topVC = delegate.topViewController while (topVC.presentedViewController != nil) { topVC = topVC.presentedViewController } topVC.presentViewController(vc, animated: animated, completion: completion) } }
  82. PHShowViewControllerAction PHOpenPostAction

  83. Load Loading Show State Screen Not Found Error Timeout Error

    Success? true false
  84. Load Loading Show State Screen Not Found Error Timeout Error

    Success? true false
  85. class PHLoadContentAction { class func perform(loadContent: PHLoadContentLoad) { let loadingViewController

    = PHLoadingViewController() loadingViewController.loadContent = loadContent PHShowViewControllerAction.perform(loadingViewController) } }
  86. PHOpenPostAction PHShowViewControllerAction PHLoadAction

  87. PHOpenPostAction.perform(withId: 5)

  88. PHOpenLinkAction.perform("producthunt://post/1") PHOpenLinkAction.perform("producthunt://collection/1") PHOpenLinkAction.perform("producthunt://user/1")

  89. PHOpenPostAction PHOpenLinkAction PHShowViewControllerAction PHLoadAction

  90. None
  91. None
  92. View Store

  93. View Store

  94. View Store

  95. Action View Store

  96. Dispatcher Action View Store

  97. Dispatcher Action View Store

  98. Product Hunt for Mac

  99. None
  100. https://github.com/producthunt/producthunt-osx

  101. Version 3.2

  102. https://speakerdeck.com/rstankov/rebuilding-of-product-hunt-for-ios

  103. Thanks !

  104. None