-
Notifications
You must be signed in to change notification settings - Fork 235
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
Bk/performance improvements #272
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -258,6 +258,10 @@ public final class CalendarView: UIView { | |
invalidateIntrinsicContentSize() | ||
} | ||
|
||
// Clear this so that we don't return early in our next layout pass even though our layout | ||
// region might not have changed. | ||
layoutRegion = nil | ||
|
||
self.content = content | ||
setNeedsLayout() | ||
|
||
|
@@ -496,6 +500,7 @@ public final class CalendarView: UIView { | |
private var anchorLayoutItem: LayoutItem? | ||
private var _visibleItemsProvider: VisibleItemsProvider? | ||
private var visibleItemsDetails: VisibleItemsDetails? | ||
private var layoutRegion: ClosedRange<LayoutItem.ItemType>? | ||
private var visibleViewsForVisibleItems = [VisibleItem: ItemView]() | ||
|
||
private var isAnimatedUpdatePass = false | ||
|
@@ -734,11 +739,14 @@ public final class CalendarView: UIView { | |
isAnimatedUpdatePass: isAnimatedUpdatePass) | ||
self.anchorLayoutItem = currentVisibleItemsDetails.centermostLayoutItem | ||
|
||
updateVisibleViews( | ||
withVisibleItems: currentVisibleItemsDetails.visibleItems, | ||
previouslyVisibleItems: visibleItemsDetails?.visibleItems ?? []) | ||
// If our first / last layout item hasn't changed, then we haven't scrolled enough to trigger | ||
// an update of visible views. This short-circuit greatly improves scroll performance. | ||
if currentVisibleItemsDetails.layoutRegion != layoutRegion { | ||
updateVisibleViews(withVisibleItems: currentVisibleItemsDetails.visibleItems) | ||
} | ||
|
||
visibleItemsDetails = currentVisibleItemsDetails | ||
layoutRegion = currentVisibleItemsDetails.layoutRegion | ||
Comment on lines
+744
to
+749
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here's the big optimization - when we're scrolling, we only need to |
||
|
||
let minimumScrollOffset = visibleItemsDetails?.contentStartBoundary.map { | ||
($0 - firstLayoutMarginValue).alignedToPixel(forScreenWithScale: scale) | ||
|
@@ -760,10 +768,7 @@ public final class CalendarView: UIView { | |
} | ||
} | ||
|
||
private func updateVisibleViews( | ||
withVisibleItems visibleItems: Set<VisibleItem>, | ||
previouslyVisibleItems _: Set<VisibleItem>) | ||
{ | ||
private func updateVisibleViews(withVisibleItems visibleItems: Set<VisibleItem>) { | ||
var viewsToHideForVisibleItems = visibleViewsForVisibleItems | ||
visibleViewsForVisibleItems.removeAll(keepingCapacity: true) | ||
|
||
|
@@ -1158,10 +1163,7 @@ extension CalendarView { | |
guard cachedAccessibilityElements == nil else { | ||
return cachedAccessibilityElements | ||
} | ||
guard | ||
let visibleItemsDetails, | ||
let visibleMonthRange | ||
else { | ||
guard let visibleItemsDetails, let visibleMonthRange else { | ||
return nil | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -188,12 +188,7 @@ public final class CalendarViewContent { | |
-> CalendarViewContent | ||
{ | ||
self.monthHeaderItemProvider = { [defaultMonthHeaderItemProvider] month in | ||
guard let itemModel = monthHeaderItemProvider(month) else { | ||
// If the caller returned nil, fall back to the default item provider | ||
return defaultMonthHeaderItemProvider(month) | ||
} | ||
|
||
return itemModel | ||
monthHeaderItemProvider(month) ?? defaultMonthHeaderItemProvider(month) | ||
} | ||
|
||
return self | ||
|
@@ -223,12 +218,7 @@ public final class CalendarViewContent { | |
-> CalendarViewContent | ||
{ | ||
self.dayOfWeekItemProvider = { [defaultDayOfWeekItemProvider] month, weekdayIndex in | ||
guard let itemModel = dayOfWeekItemProvider(month, weekdayIndex) else { | ||
// If the caller returned nil, fall back to the default item provider | ||
return defaultDayOfWeekItemProvider(month, weekdayIndex) | ||
} | ||
|
||
return itemModel | ||
dayOfWeekItemProvider(month, weekdayIndex) ?? defaultDayOfWeekItemProvider(month, weekdayIndex) | ||
} | ||
|
||
return self | ||
|
@@ -254,12 +244,7 @@ public final class CalendarViewContent { | |
-> CalendarViewContent | ||
{ | ||
self.dayItemProvider = { [defaultDayItemProvider] day in | ||
guard let itemModel = dayItemProvider(day) else { | ||
// If the caller returned nil, fall back to the default item provider | ||
return defaultDayItemProvider(day) | ||
} | ||
|
||
return itemModel | ||
dayItemProvider(day) ?? defaultDayItemProvider(day) | ||
} | ||
|
||
return self | ||
|
@@ -407,22 +392,20 @@ public final class CalendarViewContent { | |
|
||
/// The default `monthHeaderItemProvider` if no provider has been configured, | ||
/// or if the existing provider returns nil. | ||
private lazy var defaultMonthHeaderItemProvider: (Month) -> AnyCalendarItemModel = { [ | ||
calendar, | ||
monthHeaderDateFormatter | ||
] month in | ||
let firstDateInMonth = calendar.firstDate(of: month) | ||
let monthText = monthHeaderDateFormatter.string(from: firstDateInMonth) | ||
let itemModel = MonthHeaderView.calendarItemModel( | ||
invariantViewProperties: .base, | ||
content: .init(monthText: monthText, accessibilityLabel: monthText)) | ||
return itemModel | ||
} | ||
private lazy var defaultMonthHeaderItemProvider: (Month) -> AnyCalendarItemModel = | ||
{ [unowned self] month in | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be impossible to hit cc @calda There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you mean? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @calda I mean the unowned should be "safe" / there's no way to crash here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps -- although not sure I understand the motivation to change from capturing individual properties (guaranteed to be safe) to using You could feasibly construct a case that crashes by doing something like this: let content = CalendarViewContent(...)
let monthHeaderItemProvider = content.monthHeaderItemProvider
/// ... later, in a different scope, so content itself has been deallocated
monthBackgroundItemProvider(month) // 💥 crash because content no longer exists but there was an unowned reference to it so the safety depends on the usage of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would only be possible internally though, since Capturing the individual properties here causes them to be created immediately (which is expensive for date formatters). lazy var expensiveProperty = ExpensiveObject()
let foo = { [expensiveProperty] in
expensiveProperty.doSomething()
}
// later...
foo() In this example, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense -- I guess in a lot of typical usage we'd be initializing the |
||
let firstDateInMonth = calendar.firstDate(of: month) | ||
let monthText = monthHeaderDateFormatter.string(from: firstDateInMonth) | ||
let itemModel = MonthHeaderView.calendarItemModel( | ||
invariantViewProperties: .base, | ||
content: .init(monthText: monthText, accessibilityLabel: monthText)) | ||
return itemModel | ||
} | ||
|
||
/// The default `dayHeaderItemProvider` if no provider has been configured, | ||
/// or if the existing provider returns nil. | ||
private lazy var defaultDayOfWeekItemProvider: (Month?, Int) | ||
-> AnyCalendarItemModel = { [monthHeaderDateFormatter] _, weekdayIndex in | ||
private lazy var defaultDayOfWeekItemProvider: (Month?, Int) -> AnyCalendarItemModel = | ||
{ [unowned self] _, weekdayIndex in | ||
let dayOfWeekText = monthHeaderDateFormatter.veryShortStandaloneWeekdaySymbols[weekdayIndex] | ||
let itemModel = DayOfWeekView.calendarItemModel( | ||
invariantViewProperties: .base, | ||
|
@@ -432,7 +415,7 @@ public final class CalendarViewContent { | |
|
||
/// The default `dayItemProvider` if no provider has been configured, | ||
/// or if the existing provider returns nil. | ||
private lazy var defaultDayItemProvider: (Day) -> AnyCalendarItemModel = { [calendar, dayDateFormatter] day in | ||
private lazy var defaultDayItemProvider: (Day) -> AnyCalendarItemModel = { [unowned self] day in | ||
let date = calendar.startDate(of: day) | ||
let itemModel = DayView.calendarItemModel( | ||
invariantViewProperties: .baseNonInteractive, | ||
|
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.
we clear this in
setContent
, since we wantupdateVisibleViews(...)
to run on the next layout pass even if our layout region hasn't changed