Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swipe improvements #70

Merged
merged 36 commits into from
Feb 9, 2017
Merged

Swipe improvements #70

merged 36 commits into from
Feb 9, 2017

Conversation

theginbin
Copy link
Contributor

What?

Improvements to the swipe-between-projects experience.

  1. Endless swipe
  2. Return to the correct position on closing a project (Discovery, Profile, Search).
  3. Smoother transitioning between projects.
  4. Tracking "next" vs "previous" swipes.

swiping-3

The basic flow is: ProjectNavigatorViewController calls a delegate method when a page transition is complete with the current project index. The delegate view controller then can use that index to determine whether to load new projects. If new projects are loaded and the Project Navigator is being presented, the new projects are passed to it and its datasource is updated. The same delegate method is also triggering the scroll-to-position after the page transitions.

@@ -18,6 +18,10 @@ internal final class ProjectNavigatorPagesDataSource: NSObject, UIPageViewContro
self.padControllers(toLength: self.playlist.count)
}

internal func updatePlaylist(_ playlist: [Project]) {
self.playlist = playlist
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is almost too simple to be real, but I haven't seen any issues. Basically just replacing the UIPageViewController list of projects completely. Is it really this simple?

.comments(project.stats.commentsCount ?? 0, liveStreamSubpages.isEmpty ? .first : .middle),
.updates(project.stats.updatesCount ?? 0, .last)
.comments(project.stats.commentsCount as Int?, liveStreamSubpages.isEmpty ? .first : .middle),
.updates(project.stats.updatesCount as Int?, .last)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially had this here for doing animation in the subpage cell, but didn't need the animation after all. Decided to leave the coalescing to the view model because it felt better.

VideoViewController (viewDidLoad is the last lifecycle method called on this VC).
Option A: Keep the play button off the new screenshots. Option B: Figure out how to call viewDidAppear.
Let's chat.
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's chat.

@@ -124,6 +125,13 @@ internal final class SearchViewController: UITableViewController {
.observeValues { [weak self] in
self?.changeSearchFieldFocus(focus: $0, animated: $1)
}

self.viewModel.outputs.scrollToProjectRow
.observeForControllerAction() // NB: this also is required to prevent NSException.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noticed this was crashing with observeForUI. Something special about the Search controller?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly related to #68?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wasnt able to repro this crash so i started thinking maybe it got fixed somehow.

i just paired on this e547c7d with gina. we can use a feature flag to slowly ramp up how many people get the observeForUI version and if we don't see any crashes we can go 100%


// this works in practice, gotta figure out what i'm missing in the test.
self.hasAddedProjects.assertValues([true, true], "More projects are loaded.")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same test in DiscoveryPage and Profile, but it fails here (works in the sim/device though). Just need to step away for a hot min and think about what's missing.

@theginbin
Copy link
Contributor Author

Looks like after merging with master LiveStream updates, the project pamp seems to be loading a project sooner and is messing with how I set things up so gotta investigate...

)
.take(first: 1)
projectAndLiveStreamEvents,
self.viewWillAppearAnimatedProperty.signal.take(first: 1)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the reason we had the merge of these different appearance events, so we should chat about this @mbrandonw. I changed this to just viewWillAppear so that the skeleton of the project could appear while the main project cell loads. This way the subpage cell and title can show some content until the project gets refreshed instead of the blank space. Problem is, after merging with master, this is happening at lightning speed, as if the full project is being loaded much earlier than before. It's interfering with page swiping and locking up the UI. Need to look into it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have a solution, will chat you when i'm finished!

@theginbin
Copy link
Contributor Author

ok i think this bad boy is ready to be reviewed! cc @justinswart cuz we refactored ProjectPamphletViewModel where LiveStreamEvents are being fetched.

Copy link
Contributor

@justinswart justinswart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I like how you've changed the refreshing of project and fetching the live streams.

Made a couple small comments 👍

@@ -224,3 +192,29 @@ private func cookieFrom(refTag: RefTag, project: Project) -> HTTPCookie? {

return HTTPCookie(properties: properties)
}

private func fetchProjectAndLiveStreams(projectOrParam: Either<Project, Param>)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool! It's nice having the fetching bundled like this and I think it makes what we're doing clearer. This would just make project refreshes take slightly longer though right, i.e. max 5 secs if live stream fetch times out?

I like the added benefit of less emissions from that combineLatest 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the fetch does take slightly longer, but only when coming from a Push or other Param-based route (since in that case the project does not emit immediately with the prefix value). I noticed the UI was stuttering a bit on swipe after merging Live stuff so wanted to cut down on the number of times the view is rendered.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -55,7 +55,7 @@ internal struct MockLiveStreamService: LiveStreamServiceProtocol {

internal func fetchEvents(forProjectId projectId: Int, uid: Int?) ->
SignalProducer<LiveStreamEventsEnvelope, LiveApiError> {
if let error = self.fetchEventResult?.error {
if let error = self.fetchEventsForProjectResult?.error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops! Good catch 👍

.flatMapError { _ in SignalProducer(error: SomeError()) }
.timeout(after: 5, raising: SomeError(), on: AppEnvironment.current.scheduler)
.materialize()
.map { (project, .some($0.value?.liveStreamEvents ?? [])) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think this can just be

.map { (project, $0.value?.liveStreamEvents) }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah so this is really subtle and will cause some tracking tests to fail. We want to be explicit that this is an optional for the filtering logic to work when tracking the page view (i.e. don't track if LiveStreamEvents array is nil, wait until we have the full project data). Specifically on a timeout or when the load errors, we still want to track everything so we have to coalesce here. Make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha, ok cool thanks for explaining!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super subtle and glad tests helped make my intent clear! gina described it perfectly.

@@ -129,9 +95,11 @@ ProjectPamphletViewModelOutputs {

fileprivate let projectOrParamProperty = MutableProperty<Either<Project, Param>?>(nil)
fileprivate let refTagProperty = MutableProperty<RefTag?>(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projectOrParamProperty and refTagProperty look like they can be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yes, thanks

let projectAndLiveStreams = AppEnvironment.current.apiService.fetchProject(param: param)
.ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler)
.demoteErrors()
.flatMap { project -> SignalProducer<(Project, [LiveStreamEvent]?), NoError> in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this to be a switchMap potentially or should the fact that the calling site is in a switchMap be sufficient for discarding earlier emissions? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question. i believe that the earlier switchMap is sufficient, but I am no expert.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think the call-site being a switchMap should take care of it but perhaps @mbrandonw can confirm.

.take(first: 1)

Signal.combineLatest(project, trackLiveStreamEvents, refTag, cookieRefTag)
Signal.combineLatest(freshProjectAndLiveStreamsAndRefTag, cookieRefTag)
.map { ($0.0, $0.1, $0.2, $1) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More verbose but perhaps for readability we could do:

.map { projectAndLiveStreamsAndRefTag, cookieRefTag in
        let (project, liveStreamEvents, refTag) = projectAndLiveStreamsAndRefTag
        return (project, liveStreamEvents, refTag, cookieRefTag)
      }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about this, simpler, but shorter:

.map { (project: $0.0, liveStreamEvents: $0.1, refTag: $0.2, cookieRefTag: $1) }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes that is good, reads better!

@justinswart
Copy link
Contributor

Approved the couple things I noticed/had questions about but perhaps someone closer to this part of the app should still give it a once-over! :)

Nice work!

Copy link
Contributor

@mbrandonw mbrandonw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great job gin! can't believe how short and simple this was.

i only got a few small comments.

@@ -4,6 +4,8 @@ import Prelude
import UIKit

internal protocol ProjectNavigatorDelegate: class {
/// Call when a page view controller has completed transitioning.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit-picky, but this comment should be Called when... since we are describing when we invoke this method on the delegate, not when to call it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that makes sense. i think 90% of our delegate comments are "Call when" so we should consider updating across the board.

@@ -118,6 +127,10 @@ internal final class ProjectNavigatorViewController: UIPageViewController {
return self.viewControllers?.first
}

internal func updatePlaylist(_ playlist: [Project]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could add some docs here to describe that it is expected that the delegate call this anytime it gets an updated playlist of projects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure thing

.map { row, total in row >= total - 3 && row > 0 }
let isCloseToBottom = Signal.merge(
self.willDisplayRowProperty.signal.skipNil(),
self.transitionedToProjectRowAndTotalProperty.signal.skipNil()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice that you can just merge these!

@@ -52,6 +52,10 @@ internal final class DiscoveryProjectsDataSource: ValueCellDataSource {
return self[indexPath] as? Project
}

internal func indexPath(for projectRow: Int) -> IndexPath {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be renamed to something like indexPath(forProjectRow:) so that it's clear we are looking in the project section. right now at the call-site it looks like self.dataSource.indexPath(for: row) which isn't super clear what we are asking for.

@@ -37,6 +37,10 @@ internal final class SearchDataSource: ValueCellDataSource {
}
}

internal func indexPath(for projectRow: Int) -> IndexPath {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

.observeValues { [weak self] itemIndex in
guard let _self = self else { return }
_self.collectionView?.scrollToItem(at: _self.dataSource.indexPath(for: itemIndex), at: .top,
animated: false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we put at: .top on a newline so that all params are grouped together?

@@ -124,6 +125,13 @@ internal final class SearchViewController: UITableViewController {
.observeValues { [weak self] in
self?.changeSearchFieldFocus(focus: $0, animated: $1)
}

self.viewModel.outputs.scrollToProjectRow
.observeForControllerAction() // NB: this also is required to prevent NSException.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wasnt able to repro this crash so i started thinking maybe it got fixed somehow.

i just paired on this e547c7d with gina. we can use a feature flag to slowly ramp up how many people get the observeForUI version and if we don't see any crashes we can go 100%

@@ -4,11 +4,11 @@ import ReactiveSwift
import Result

public protocol ProjectNavigatorViewModelInputs {
/// Call with the config data give to the view.
/// Call with the config data to give to the view.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh this comment should prob be "Call with the config data given to the view."

let swipedToProjectAtIndexFromIndex = self.willTransitionToProjectAtIndexProperty.signal.skipNil()
.takePairWhen(
self.pageTransitionCompletedFromIndexProperty.signal.skipNil()
.filter { completed, _ in completed }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like we do self.pageTransitionCompletedFromIndexProperty.signal.skipNil().filter { completed, _ in completed } twice so it could maybe be pulled out into a more descriptive signal

.flatMapError { _ in SignalProducer(error: SomeError()) }
.timeout(after: 5, raising: SomeError(), on: AppEnvironment.current.scheduler)
.materialize()
.map { (project, .some($0.value?.liveStreamEvents ?? [])) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super subtle and glad tests helped make my intent clear! gina described it perfectly.

Copy link
Contributor

@mbrandonw mbrandonw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a few comments, lemme know what you think. we can merge after!

@@ -210,7 +213,7 @@ private func fetchProjectAndLiveStreams(projectOrParam: Either<Project, Param>)
.take(first: 1)
}

if let project = projectOrParam.left {
if let project = projectOrParam.left, shouldPrefix {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i really wish swift still had the where syntax. this reads kinda weird

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i hear ya. would you prefer something else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm kind of used to the comma at this point so i don't care.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry i was just thinking out loud. didnt have anything actionable

Signal.merge(self.viewDidLoadProperty.signal,
self.viewDidAppearAnimated.signal.filter(isTrue).ignoreValues()
Signal.merge(self.viewDidLoadProperty.signal.mapConst(true),
self.viewDidAppearAnimated.signal.filter(isTrue).mapConst(false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i still think we should not emit configData multiple times cause the input is only called a single time. we could move the Signal.merge to a takePairWhen in the freshProjectAndLiveStreamsAndRefTag signal. also helps localize the shouldPrefix logic to near where it's needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hear what you're saying, but I don't really see these as being different. The only thing we'd be doing is getting rid of the let config and moving the whole thing to freshProjectAndLiveStreamsAndRefTag. In fact, I feel safer with the combineLatest then the takePairWhen to be honest. We very often put config data into a let, not sure what makes this one different?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well after a few more minutes of thinking, i guess it's not a problem to change it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah just ignore me

cookieRefTag,
self.viewDidAppearAnimated.signal.ignoreValues()
)
.map { (project: $0.0, liveStreamEvents: $0.1, refTag: $0.2, cookieRefTag: $1, _: $2) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think the named tuple params are necessary cause it doesnt seem like we use em. we could just do .map { ($0.0, $0.1, $0.2, $1, $2) }, which is just a fancy unpack

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i named them b/c @justinswart thought it was more readable

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np leave as is!

Copy link
Contributor

@mbrandonw mbrandonw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10214009-116640902_17-s4-v1

@theginbin theginbin merged commit 7bb82ef into master Feb 9, 2017
@theginbin theginbin deleted the swipe-improvements branch February 9, 2017 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants