motion-prime/sections/_async_table_mixin.rb
module Prime
module AsyncTableMixin
extend ::MotionSupport::Concern
included do
class_attribute :async_data_options
end
# Returns true if table section have enabled async data. False by defaul.
#
# @return [Boolean] is async data enabled.
def async_data?
self.class.async_data_options
end
def table_element_options
options = super
if async_data? && self.class.async_data_options.has_key?(:estimated_cell_height)
options[:estimated_cell_height] = self.class.async_data_options[:estimated_cell_height]
end
options
end
# Reset async loaded table data and preloader queue.
#
# @return [Boolean] true
def reset_collection_data
super # must be before to update fixed_collection_data
@async_loaded_data = async_data? ? fixed_collection_data : nil
@preloader_next_starts_from = nil
end
def height_for_index(index)
section = cell_section_by_index(index)
unless section
Prime.logger.debug "could not find section with index #{index} for #{self.to_s}"
return 0
end
preload_section_by_index(index: index)
section.container_height
end
def render_cell(index)
preload_sections_after(index)
super
end
def on_async_data_loaded; end
def on_queue_preloaded(queue_id, loaded_index); end
def on_cell_section_preloaded(section, index); end
# Preloads sections after rendering cell in current sheduled index or given index.
# TODO: probably should be in separate class.
#
# @param from_index [NSIndexPath] Value of first index to load if current sheduled index not exists.
# @return [NSIndexPath, Boolean] Index of next sheduled index.
def preload_sections_after(from_index, load_limit = nil)
return unless async_data?
service = preloader_index_service
load_limit ||= self.class.async_data_options.try(:[], :preload_cells_count)
if @preloader_next_starts_from
index_to_start_preloading = service.sum_index(@preloader_next_starts_from, load_limit ? -load_limit/2 : 0)
# should we start preload based on index of rendered cell
return false if service.compare_indexes(from_index, index_to_start_preloading) < 0
end
# adjust start/finish points based on current queues
current_group = from_index.section
left_to_load_in_group = cell_sections_for_group(current_group).count - from_index.row
load_count = [left_to_load_in_group, load_limit].compact.min
to_index = service.sum_index(from_index, load_count - 1)
@preloader_next_starts_from = to_index
Array.wrap(@preloader_queue).each do |queue_info|
# cancelled and dealloc are left from prev data
next unless [:in_progress, :completed].include?(queue_info[:state])
# filter by current group
next unless queue_info[:from_index].section == current_group
# reject not started threads
next if queue_info[:to_index].nil? && queue_info[:state] != :in_progress
if from_index.row >= queue_info[:from_index].row
from_index = NSIndexPath.indexPathForRow([from_index.row, queue_info[:to_index].try(:row).try(:+, 1), (queue_info[:target_index] if queue_info[:state] == :in_progress).try(:row).try(:+, 1)].compact.max, inSection: current_group)
else
to_index = NSIndexPath.indexPathForRow([to_index.row, queue_info[:from_index].try(:row).try(:-, 1)].compact.min, inSection: current_group)
end
end
load_count = to_index.row - from_index.row + 1
preload_sections_schedule_from(from_index, load_count) if load_count > 0
# quota_left = (load_limit || 0) - load_count
# if quota_left > 0 && cell_sections_for_group(current_group + 1).any?
# preload_sections_after(NSIndexPath.indexPathForRow(0, inSection: current_group + 1), quota_left)
# end
end
# Schedules preloading sections starting with given index with given limit.
# TODO: probably should be in separate class.
#
# @param index [NSIndexPath] Value of first index to load.
# @param load_count [Integer] Count of sections to load.
# @return [Integer] Queue ID
def preload_sections_schedule_from(index, load_count)
service = preloader_index_service
@preloader_queue ||= []
# TODO: we do we need to keep screen ref too?
queue_id = @preloader_queue.count
@preloader_queue[queue_id] = {
state: :in_progress,
target_index: service.sum_index(index, load_count-1),
from_index: index
}
refs = strong_references
BW::Reactor.schedule(queue_id) do |queue_id|
result = load_count.times do |offset|
break if @preloader_queue[queue_id][:state] == :cancelled
unless refs.all?(&:weakref_alive?)
@preloader_queue[queue_id][:state] = :dealloc
break
end
self.performSelectorOnMainThread(:"preload_section_by_index:", withObject: {
index: index,
queue_id: queue_id,
success: proc { |section|
on_cell_section_preloaded(section, index)
}.weak!
}, waitUntilDone: true)
@preloader_queue[queue_id][:to_index] = index
unless offset == load_count - 1
index = service.sum_index(index, 1)
end
true
end
if result
@preloader_queue[queue_id][:state] = :completed
on_queue_preloaded(queue_id, index)
end
end
queue_id
end
def preloader_index_service
TableDataIndexes.new(@data)
end
private
def set_collection_data
sections = load_sections_async
prepare_collection_cell_sections(sections)
set_data!(sections)
reset_data_stamps
@data
end
def load_sections_async
@async_loaded_data || begin
ref_key = allocate_strong_references
BW::Reactor.schedule_on_main do
@async_loaded_data = fixed_collection_data
set_data!(nil)
reload_collection_data
on_async_data_loaded
release_strong_references(ref_key)
end
[]
end
end
def preload_section_by_index(options)
index = options[:index]
if queue_id = options[:queue_id]
return unless @preloader_queue[queue_id][:state] == :in_progress
end
section = cell_section_by_index(index)
create_elements = section.create_elements
no_container = !section.container_element
must_load = create_elements && no_container && async_data?
if must_load # perform only if just loaded
section.load_container_with_elements(container: container_element_options_for(index))
options[:success].call(section) if options[:success]
end
end
def create_section_elements; end
module ClassMethods
def inherited(subclass)
super
subclass.async_data_options = self.async_data_options.try(:clone)
end
def set_async_data_options(options = {})
self.async_data_options = options
end
end
end
end