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

Rebuilding of Product Hunt for iOS

Rebuilding of Product Hunt for iOS

Learning from working on Product Hunt for iOS

Radoslav Stankov

May 14, 2016
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. Pretty standard iOS app • Storyboards • Uses web API

    • Decent data layer • … like a Apple tutorial
  2. class PHCollectionViewController : PHParallaxViewController { var collection = PHCollection() override

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

    : PHParallaxHeaderSource { var backgroundImageUrl: String { return collection.imagePath } var height: Int { return 40 } }
  4. UITableViewController + • Load data from server • Loading and

    empty state • Pull to refresh • Infinite scrolling • Dynamic cell height
  5. List Data Source Post Data Source List Cell Post Cell

    PHListViewController(dataSource: dataSource, cell: PHPostCell.Type) 
 View Controller Table View Cell State View
  6. 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] } }
  7. 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() } } }
  8. Unit tests • Test one object / function • Isolated

    • Fast • Edge cases, nitpickings
  9. 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) } }
  10. 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) } }
  11. 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) } }
  12. 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() } }
  13. 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"],
  14. 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"]) } }
  15. class PHOpenPostAction { class func perform(withId id: Int) { let

    vc = PHPostDetailsViewController() vc.postId = postId PHShowViewControllerAction.perform(vc) } }
  16. 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) } }
  17. class PHLoadContentAction { class func perform(loadContent: PHLoadContentLoad) { let loadingViewController

    = PHLoadingViewController() loadingViewController.loadContent = loadContent PHShowViewControllerAction.perform(loadingViewController) } }