droidlabs/motion-prime

View on GitHub
motion-prime/sections/_async_table_mixin.rb

Summary

Maintainability
C
1 day
Test Coverage
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