-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Conversation
@@ -18,6 +18,10 @@ internal final class ProjectNavigatorPagesDataSource: NSObject, UIPageViewContro | |||
self.padControllers(toLength: self.playlist.count) | |||
} | |||
|
|||
internal func updatePlaylist(_ playlist: [Project]) { | |||
self.playlist = playlist |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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. | ||
*/ |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly related to #68?
There was a problem hiding this comment.
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.") | ||
|
There was a problem hiding this comment.
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.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
ok i think this bad boy is ready to be reviewed! cc @justinswart cuz we refactored |
There was a problem hiding this 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>) |
There was a problem hiding this comment.
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
👍
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 ?? [])) } |
There was a problem hiding this comment.
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) }
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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? 🤔
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) } |
There was a problem hiding this comment.
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)
}
There was a problem hiding this comment.
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) }
There was a problem hiding this comment.
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!
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! |
There was a problem hiding this 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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 ?? [])) } |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
np leave as is!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What?
Improvements to the swipe-between-projects experience.
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.