$30 off During Our Annual Pro Sale. View Details »

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. View Slide

  2. Radoslav Stankov
    @rstankov

    http://rstankov.com

    http://github.com/rstankov

    View Slide

  3. Vladimir Vladimirov
    @DeVladinci

    http://github.com/DeVladinci

    View Slide

  4. View Slide

  5. View Slide

  6. Version 1.0

    View Slide

  7. View Slide

  8. Pretty standard iOS app
    • Storyboards
    • Uses web API
    • Decent data layer
    • … like a Apple tutorial

    View Slide

  9. Version 2.0

    View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. View Slide

  17. class ViewController : PHParallaxViewController {

    override func viewDidLoad() {
    super.viewDidLoad()
    header = …
    content = …
    }
    }

    View Slide

  18. class ViewController : PHParallaxViewController {

    override func viewDidLoad() {
    super.viewDidLoad()
    header = …
    content = …
    }
    }

    View Slide

  19. class ViewController : PHParallaxViewController {

    override func viewDidLoad() {
    super.viewDidLoad()
    header = …
    content = …
    }
    }

    View Slide

  20. class ViewController : PHParallaxViewController {

    override func viewDidLoad() {
    super.viewDidLoad()
    header = …
    content = …
    }
    }

    View Slide

  21. class ViewController : PHParallaxViewController {

    override func viewDidLoad() {
    super.viewDidLoad()
    header = …
    content = …
    }
    }

    View Slide

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

    View Slide

  23. class PHCollectionHeaderView : UIView {
    // …
    }
    extension PHCollectionHeaderView : PHParallaxHeaderSource {
    var backgroundImageUrl: String {
    return collection.imagePath
    }
    var height: Int {
    return 40
    }
    }

    View Slide

  24. Parallax
    View Controller

    View Slide

  25. Parallax
    View Controller
    Parallax Header

    View Slide

  26. Parallax
    View Controller
    Parallax Header
    Custom Header

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. Parallax
    View Controller
    Parallax Header
    Custom Header
    Custom
    View Controller

    View Slide

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

    View Slide

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

    View Slide

  34. Parallax
    View Controller
    Parallax Header
    Custom Header
    Custom
    View Controller

    View Slide

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

    View Slide

  36. Parallax
    View Controller

    View Slide

  37. Parallax
    View Controller

    View Slide

  38. Parallax
    View Controller

    View Slide

  39. Parallax
    View Controller

    View Slide

  40. View Slide

  41. View Slide

  42. View Slide

  43. View Slide


  44. View Controller

    View Slide


  45. View Controller
    Table View

    View Slide


  46. View Controller
    Table View
    Cell

    View Slide


  47. View Controller
    Table View
    Cell
    State View

    View Slide

  48. UITableViewController +
    • Load data from server
    • Loading and empty state
    • Pull to refresh
    • Infinite scrolling
    • Dynamic cell height

    View Slide

  49. PHListView

    View Slide

  50. List Data Source
    Post Data Source
    List Cell
    Post Cell
    PHListViewController(dataSource: dataSource, cell: PHPostCell.Type)

    View Controller
    Table View
    Cell
    State View

    View Slide

  51. PHDataSource
    • numberOfSections
    • numberOfRowsInSection
    • dataOfIndexPath

    View Slide

  52. class PHListArrayDataSource : PHListDataSource {
    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]
    }
    }

    View Slide

  53. let dataSource = PHListArrayDataSource(listOfPosts)
    let vc = PHListViewController(dataSource: dataSource, cell: PHPostCell.Type)

    View Slide

  54. View Slide

  55. PHDataSource
    • initialLoad
    • loadNewer
    • loadOlder
    • moreToLoad

    View Slide

  56. typealias PHPostCallback = ([PHPost]) -> Void
    typealias PHPostFetch = (lastId: Int?, callback: PHPostCallback) -> Void
    class PHPostListDataSource : PHListArrayDataSource {
    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()
    }
    }
    }

    View Slide

  57. View Slide

  58. View Slide

  59. Unit tests
    • Test one object / function
    • Isolated
    • Fast
    • Edge cases, nitpickings

    View Slide

  60. 4 Phase Testing

    View Slide

  61. Setup
    4 Phase Testing

    View Slide

  62. Setup
    Action
    4 Phase Testing

    View Slide

  63. Setup
    Action
    Assertion
    4 Phase Testing

    View Slide

  64. Setup
    Action
    Assertion
    Teardown
    4 Phase Testing

    View Slide

  65. Setup
    Action
    Assertion
    Teardown
    4 Phase Testing

    View Slide

  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)
    }
    }

    View Slide

  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)
    }
    }

    View Slide

  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)
    }
    }

    View Slide

  69. View Slide

  70. UI tests
    • Tests a full feature
    • Slow
    • Fake API
    • KIF

    View Slide

  71. Fake Endpoint
    Fake Data
    Data Source
    API
    Endpoint

    View Slide

  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()
    }
    }

    View Slide

  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"],

    View Slide

  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"])
    }
    }

    View Slide

  75. View Slide

  76. View Slide

  77. https://developer.apple.com/videos/play/wwdc2015/406/
    UI Testing in Xcode

    View Slide

  78. Version 3.0

    View Slide

  79. View Slide

  80. class PHOpenPostAction {
    class func perform(withId id: Int) {
    let vc = PHPostDetailsViewController()
    vc.postId = postId
    PHShowViewControllerAction.perform(vc)
    }
    }

    View Slide

  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)
    }
    }

    View Slide

  82. PHShowViewControllerAction
    PHOpenPostAction

    View Slide

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

    View Slide

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

    View Slide

  85. class PHLoadContentAction {
    class func perform(loadContent: PHLoadContentLoad) {
    let loadingViewController = PHLoadingViewController()
    loadingViewController.loadContent = loadContent
    PHShowViewControllerAction.perform(loadingViewController)
    }
    }

    View Slide

  86. PHOpenPostAction
    PHShowViewControllerAction
    PHLoadAction

    View Slide

  87. PHOpenPostAction.perform(withId: 5)

    View Slide

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

    View Slide

  89. PHOpenPostAction
    PHOpenLinkAction
    PHShowViewControllerAction
    PHLoadAction

    View Slide

  90. View Slide

  91. View Slide

  92. View
    Store

    View Slide

  93. View
    Store

    View Slide

  94. View
    Store

    View Slide

  95. Action
    View
    Store

    View Slide

  96. Dispatcher
    Action
    View
    Store

    View Slide

  97. Dispatcher
    Action
    View
    Store

    View Slide

  98. Product Hunt for Mac

    View Slide

  99. View Slide

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

    View Slide

  101. Version 3.2

    View Slide

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

    View Slide

  103. Thanks !

    View Slide

  104. View Slide