droidlabs/motion-prime

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

Summary

Maintainability
C
1 day
Test Coverage
motion_require 'base_section'
module MotionPrime
  class AbstractCollectionSection < Section
    include HasStyleChainBuilder

    attr_accessor :collection_element, :did_appear
    attr_reader :decelerating

    before_render :render_collection

    delegate :set_options, :update_options, to: :collection_element, allow_nil: true

    %w[table_data
      fixed_table_data
      table_view
      reset_table_data
      async_table_data
      reload_table_data
      table_delegate
      table_styles
      table_styles_base
      prepare_table_cell_sections
      table_element_options
      render_table].each do |table_method|
      define_method table_method do |*args|
        Prime.logger.info "##{table_method} is deprecated: #{caller[0]}"
        send(table_method.gsub('table', 'collection'), *args)
      end
    end

    def initialize(*args)
      super
      @_cached_cells = {}
    end

    # Return sections which will be used to render as collection cells.
    #
    # This method should be redefined in your collection section and must return array.
    # @return [Array<Prime::Section>] array of sections
    def collection_data
      @model || []
    end

    # Returns cached version of collection data
    #
    # @return [Array<Prime::Section>] cached array of sections
    def data
      @data || set_collection_data
    end

    # IMPORTANT: when you use #map in collection_data,
    # then #dealloc of Prime::Section will not be called to section created on that #map.
    # We did not find yet why this happening, for now just using hack.
    def fixed_collection_data
      collection_data.to_enum.to_a
    end

    def dealloc
      Prime.logger.dealloc_message :collection, self, @collection_element.try(:view).to_s
      @collection_delegate.try(:clear_delegated)
      @collection_element.try(:view).try(:setDataSource, nil)
      super
    end

    # Reset all collection data and reload collection view
    #
    # @return [Boolean] true
    def reload_data
      reset_collection_data
      reload_collection_data
    end

    # Alias for reload_data
    #
    # @return [Boolean] true
    def reload
      reload_data
    end

    # Reload collection view data
    #
    # @return [Boolean] true
    def reload_collection_data
      collection_view.reloadData
      true
    end

    # Reload collection view if data was empty before.
    #
    # @return [Boolean] true if reload was happened
    def refresh_if_needed
      @data.nil? ? reload_collection_data : false
    end

    # Reset all collection data.
    #
    # @return [Boolean] true
    def reset_collection_data
      @did_appear = false
      # FIXME: simetimes @data is a section
      Array.wrap(@data).flatten.each do |section|
        next unless element = section.container_element
        element.update_options(reuse_identifier: nil)
        element.view.try(:removeFromSuperview)
      end
      set_data!(nil)
      @data_stamp = nil
      true
    end

    def collection_styles_base
      raise "Implement #collection_styles_base"
    end

    def collection_styles
      type = collection_styles_base

      base_styles = Array.wrap(type)
      item_styles = [name.to_sym]
      item_styles += Array.wrap(@styles) if @styles.present?
      {common: base_styles, specific: item_styles}
    end

    def cell_section_styles(section)
      # type = [`cell`, `header`, `field`]

      # UserFormSection example: field :email, type: :string
      # form_name = `user`
      # type = `field`
      # field_name = `email`
      # field_type = `string_field`

      # CategoriesTableSection example: table is a `CategoryTableSection`, cell is a `CategoryTitleSection`, element :icon, type: :image
      # table_name = `categories`
      # type = `cell` (always true)
      # table_cell_section_name = `title`
      type = section.respond_to?(:cell_type) ? section.cell_type : 'cell'
      suffixes = [type]
      if section.is_a?(BaseFieldSection)
        suffixes << section.default_name
      end

      styles = {}
      # table: base_table_<type>
      # form: base_form_<type>, base_form_<field_type>
      styles[:common] = build_styles_chain(collection_styles[:common], suffixes)
      if section.is_a?(BaseFieldSection)
        # form cell: _<type>_<field_name> = `_field_email`
        suffixes << :"#{type}_#{section.name}" if section.name
      elsif section.respond_to?(:cell_section_name) # cell section came from table
        # table cell: _<table_cell_section_name> = `_title`
        suffixes << section.cell_section_name
      end
      # table: <table_name>_table_<type>, <table_name>_table_<table_cell_section_name> = `categories_table_cell`, `categories_table_title`
      # form: <form_name>_form_<type>, <form_name>_form_<field_type>, user_form_<type>_email = `user_form_field`, `user_form_string_field`, `user_form_field_email`
      styles[:specific] = build_styles_chain(collection_styles[:specific], suffixes)

      if section.container_options_styles.present?
        styles[:specific] += Array.wrap(section.container_options_styles)
      end
      styles
    end

    def collection_element_options
      container_options.except(:styles, :height, :hidden).merge({
        section: self.weak_ref,
        styles: collection_styles.values.flatten,
        delegate: collection_delegate,
        data_source: collection_delegate
      })
    end

    def render_collection
      raise "Implement #render_collection"
    end

    def collection_view
      collection_element.try(:view)
    end

    def hide
      collection_view.try(:hide)
    end

    def show
      collection_view.try(:show)
    end

    def render_cell(index)
      raise "Implement #render_cell"
    end

    def on_cell_render(cell, index); end
    def on_appear; end
    def on_click(index); end

    def cell_name(index)
      record = cell_section_by_index(index)
      "cell_#{record.object_id}_#{@data_stamp[record.object_id]}"
    end

    def cell_sections_for_group(section)
      raise "Implement #cell_sections_for_group"
    end

    def cell_section_by_index(index)
      cell_sections_for_group(index.section)[index.row]
    end

    def cell_for_index(index)
      cell = cached_cell(index) || render_cell(index)
      # run table view is appeared callback if needed
      if !@did_appear && index.row == cell_sections_for_group(index.section).size - 1
        on_appear
      end
      cell.is_a?(UIView) ? cell : cell.view
    end

    def height_for_index(index)
      section = cell_section_by_index(index)
      section.create_elements
      section.container_height
    end

    def scroll_view_will_begin_dragging(scroll)
      @decelerating = true
    end

    def scroll_view_will_begin_decelerating(scroll); end

    def scroll_view_did_end_scrolling_animation(scroll); end

    def scroll_view_did_end_decelerating(scroll)
      @decelerating = false
      display_pending_cells
    end

    def scroll_view_did_scroll(scroll); end

    def update_pull_to_refresh_after_scroll(scroll)
      return unless refresh_view = collection_view.try(:pullToRefreshView)
      return refresh_view.alpha = 1 if refresh_view.state == SVPullToRefreshStateLoading

      current_offset = scroll.contentOffset.y
      table_inset = collection_view.contentInset.top
      refresh_offset = refresh_view.yOrigin
      alpha = [[-(current_offset + table_inset)/refresh_view.size.height, 0].max, 1].min

      refresh_view.alpha = alpha
    end

    def scroll_view_did_end_dragging(scroll, willDecelerate: will_decelerate)
      display_pending_cells unless @decelerating = will_decelerate
    end

    def on_input_change(text_field); end
    def on_input_edit_begin(text_field); end
    def on_input_edit_end(text_field); end
    def on_input_return(text_field)
      text_field.resignFirstResponder
    end
    def on_input_did_change(text_field); end

    private
      def set_data!(new_data)
        Array.wrap(@preloader_queue).each { |queue| queue[:state] = :cancelled }
        @data = new_data
      end

      def cached_cell(index)
      end

      def display_pending_cells
        collection_view.visibleCells.each do |cell_view|
          if cell_view.section && cell_view.section.pending_display
            cell_view.section.display
          end
        end
      end

      def set_collection_data
        sections = fixed_collection_data
        prepare_collection_cell_sections(sections)
        set_data!(sections)
        reset_data_stamps
        create_section_elements
        @data
      end

      def prepare_collection_cell_sections(cells)
        Array.wrap(cells.flatten).each do |cell|
          Prime::Config.prime.cell_section.mixins.each do |mixin|
            cell.class.send(:include, mixin) unless (class << cell; self; end).included_modules.include?(mixin)
          end

          cell.screen ||= screen
          cell.collection_section ||= self.weak_ref if cell.respond_to?(:collection_section=)
        end
      end

      def container_element_options_for(index)
        cell_section = cell_section_by_index(index)
        {
          reuse_identifier: cell_name(index),
          parent_view: collection_view,
          bounds: {height: cell_section.container_height}
        }
      end

      def set_data_stamp(section_ids)
        @data_stamp ||= {}
        [*section_ids].each do |id|
          @data_stamp[id] = Time.now.to_f
        end
      end

      def reset_data_stamps
        keys = data.map(&:object_id)
        set_data_stamp(keys)
      end

      def create_section_elements
        data.flatten.each(&:create_elements)
      end
  end
end