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. 1.
  2. 4.
  3. 5.
  4. 7.
  5. 8.

    Pretty standard iOS app • Storyboards • Uses web API

    • Decent data layer • … like a Apple tutorial
  6. 10.
  7. 11.
  8. 12.
  9. 13.
  10. 14.
  11. 15.
  12. 16.
  13. 17.
  14. 18.
  15. 19.
  16. 20.
  17. 21.
  18. 22.

    class PHCollectionViewController : PHParallaxViewController { var collection = PHCollection() override

    func viewDidLoad() { super.viewDidLoad() header = PHCollectionHeaderView(collection) content = PHPostListViewController(collection) } }
  19. 23.

    class PHCollectionHeaderView : UIView { // … } extension PHCollectionHeaderView

    : PHParallaxHeaderSource { var backgroundImageUrl: String { return collection.imagePath } var height: Int { return 40 } }
  20. 40.
  21. 41.
  22. 42.
  23. 43.
  24. 48.

    UITableViewController + • Load data from server • Loading and

    empty state • Pull to refresh • Infinite scrolling • Dynamic cell height
  25. 50.

    List Data Source Post Data Source List Cell Post Cell

    PHListViewController(dataSource: dataSource, cell: PHPostCell.Type) 
 View Controller Table View Cell State View
  26. 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] } }
  27. 54.
  28. 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() } } }
  29. 57.
  30. 58.
  31. 59.

    Unit tests • Test one object / function • Isolated

    • Fast • Edge cases, nitpickings
  32. 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) } }
  33. 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) } }
  34. 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) } }
  35. 69.
  36. 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() } }
  37. 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"],
  38. 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"]) } }
  39. 75.
  40. 76.
  41. 79.
  42. 80.

    class PHOpenPostAction { class func perform(withId id: Int) { let

    vc = PHPostDetailsViewController() vc.postId = postId PHShowViewControllerAction.perform(vc) } }
  43. 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) } }
  44. 85.

    class PHLoadContentAction { class func perform(loadContent: PHLoadContentLoad) { let loadingViewController

    = PHLoadingViewController() loadingViewController.loadContent = loadContent PHShowViewControllerAction.perform(loadingViewController) } }
  45. 90.
  46. 91.
  47. 99.
  48. 103.
  49. 104.