├── .bundle └── config ├── spec ├── main_spec.rb ├── helpers │ ├── cdq.rb │ └── load_image.rb ├── ext │ ├── ui_image_view_spec.rb │ ├── ui_color_spec.rb │ ├── object_spec.rb │ ├── ui_view_controller_spec.rb │ └── ui_view_spec.rb ├── ruby_motion_query │ ├── traverse_spec.rb │ ├── app_spec.rb │ └── stylers │ │ └── ui_image_view_spec.rb ├── pro_motion │ ├── screen_module_spec.rb │ └── data_table_screen_spec.rb └── screens │ └── ui_collection_view.rb ├── lib ├── project │ ├── version.rb │ ├── pro_motion │ │ ├── support.rb │ │ ├── screen.rb │ │ ├── collection_screen.rb │ │ ├── data_table_screen.rb │ │ ├── data_table_search_delegate.rb │ │ ├── table.rb │ │ ├── data_table_searchable.rb │ │ └── data_table.rb │ ├── ruby_motion_query │ │ ├── traverse.rb │ │ ├── stylers │ │ │ └── ui_image_view.rb │ │ └── app.rb │ └── ext │ │ ├── ui_table_view_cell.rb │ │ ├── object.rb │ │ ├── ui_collection_view_cell.rb │ │ ├── kernel.rb │ │ ├── ui_color.rb │ │ ├── pm_delegate.rb │ │ ├── ui_image_view.rb │ │ ├── ui_view.rb │ │ └── ui_view_controller.rb └── redpotion.rb ├── resources ├── homer.jpeg ├── grumpy_cat.png ├── icon-60@2x.png ├── grumpy_cat@2x.png └── Default-568h@2x.png ├── app ├── test_objects │ ├── test_controller.rb │ ├── test_screen.rb │ ├── test_table_screen.rb │ ├── test_grouped_table_screen.rb │ ├── test_screen_stylesheet.rb │ ├── test_view.rb │ └── delegate_test_attributes.rb ├── views │ ├── hello_world_section.rb │ ├── collection_cell.rb │ ├── metal_table_cell.rb │ ├── task_cell.rb │ └── contributor_cell.rb ├── models │ └── contributor.rb ├── stylesheets │ ├── collection_cell_stylesheet.rb │ ├── tasks_screen_stylesheet.rb │ ├── example_controller_stylesheet.rb │ ├── metal_table_screen_stylesheet.rb │ ├── contributor_screen_stylesheet.rb │ ├── collection_screen_stylesheet.rb │ ├── application_stylesheet.rb │ └── home_screen_stylesheet.rb ├── screens │ ├── contributor_screen.rb │ ├── tasks_screen.rb │ ├── collection_screen.rb │ ├── metal_table_screen.rb │ └── home_screen.rb ├── shared │ └── contributors_module.rb ├── app_delegate.rb └── controllers │ └── example_controller.rb ├── docs ├── cookbook │ ├── timers.md │ ├── misc.md │ ├── actions.md │ ├── testing.md │ ├── app.md │ ├── format.md │ ├── fonts.md │ ├── accessibility.md │ ├── selecting.md │ ├── tagging.md │ ├── colors.md │ ├── device.md │ ├── attributes.md │ ├── debugging.md │ ├── project.md │ ├── troubleshooting.md │ ├── events_and_gestures.md │ ├── alerts_and_action_sheets.md │ ├── traversing.md │ ├── app_delegate.md │ ├── table_screens.md │ ├── animations.md │ ├── images.md │ ├── distribution.md │ ├── core_data.md │ ├── networking.md │ ├── layout_a_screen.md │ └── validations.md ├── contributing.md ├── index.md ├── quick_start.md ├── new_features_for_rmq_and_promotion.md └── redpotion_specific_features.md ├── Gemfile ├── templates ├── view │ ├── spec │ │ └── views │ │ │ └── name.rb │ └── app │ │ ├── stylesheets │ │ └── name_stylesheet.rb │ │ └── views │ │ └── name.rb ├── table_screen_cell │ ├── spec │ │ └── views │ │ │ └── name.rb │ └── app │ │ ├── stylesheets │ │ └── name_stylesheet.rb │ │ └── views │ │ └── name.rb ├── screen │ ├── spec │ │ └── screens │ │ │ └── name_screen_spec.rb │ └── app │ │ ├── stylesheets │ │ └── name_screen_stylesheet.rb │ │ └── screens │ │ └── name_screen.rb ├── collection_view_screen │ ├── spec │ │ ├── views │ │ │ └── name_cell_spec.rb │ │ └── screens │ │ │ └── name_screen_spec.rb │ └── app │ │ ├── views │ │ └── name_cell.rb │ │ ├── stylesheets │ │ ├── name_cell_stylesheet.rb │ │ └── name_screen_stylesheet.rb │ │ └── screens │ │ └── name_screen.rb └── metal_table_screen │ ├── spec │ └── screens │ │ └── name_screen_spec.rb │ └── app │ ├── views │ └── name_cell.rb │ ├── stylesheets │ └── name_screen_stylesheet.rb │ └── screens │ └── name_screen.rb ├── schemas ├── 0001_initial.rb └── 0002_add_city_names.rb ├── .gitignore ├── .travis.yml ├── Rakefile ├── LICENSE ├── redpotion.gemspec ├── README.md ├── mkdocs.yml └── bin └── potion /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_JOBS: 3 3 | BUNDLE_RETRY: '3' 4 | -------------------------------------------------------------------------------- /spec/main_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Application 'redpotion'" do 2 | end 3 | -------------------------------------------------------------------------------- /lib/project/version.rb: -------------------------------------------------------------------------------- 1 | module RedPotion 2 | VERSION = "1.7.1" 3 | end 4 | -------------------------------------------------------------------------------- /resources/homer.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/redpotion/HEAD/resources/homer.jpeg -------------------------------------------------------------------------------- /resources/grumpy_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/redpotion/HEAD/resources/grumpy_cat.png -------------------------------------------------------------------------------- /resources/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/redpotion/HEAD/resources/icon-60@2x.png -------------------------------------------------------------------------------- /resources/grumpy_cat@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/redpotion/HEAD/resources/grumpy_cat@2x.png -------------------------------------------------------------------------------- /spec/helpers/cdq.rb: -------------------------------------------------------------------------------- 1 | module Bacon 2 | class Context 3 | 4 | include CDQ 5 | 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/redpotion/HEAD/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /app/test_objects/test_controller.rb: -------------------------------------------------------------------------------- 1 | class TestController < UIViewController 2 | 3 | include DelegateTestAttributes 4 | 5 | end 6 | -------------------------------------------------------------------------------- /docs/cookbook/timers.md: -------------------------------------------------------------------------------- 1 | ```ruby 2 | app.after 2 { do_something } # after 2 seconds 3 | app.every 5 { do_something_repetitive } # every 5 seconds 4 | ``` 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | gem "motion-cocoapods" 5 | gem "cdq" 6 | gem "RedAlert" 7 | gem "newclear" 8 | gem "motion-redgreen" 9 | -------------------------------------------------------------------------------- /app/test_objects/test_screen.rb: -------------------------------------------------------------------------------- 1 | class TestScreen < PM::Screen 2 | 3 | include DelegateTestAttributes 4 | stylesheet TestScreenStylesheet 5 | 6 | end 7 | -------------------------------------------------------------------------------- /templates/view/spec/views/name.rb: -------------------------------------------------------------------------------- 1 | describe '<%= @name_camel_case %>' do 2 | 3 | before do 4 | end 5 | 6 | after do 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /spec/helpers/load_image.rb: -------------------------------------------------------------------------------- 1 | def load_image(path, type = "jpeg") 2 | NSData.dataWithContentsOfFile(NSBundle.mainBundle.pathForResource(path, ofType:type)) 3 | end 4 | -------------------------------------------------------------------------------- /docs/cookbook/misc.md: -------------------------------------------------------------------------------- 1 | ## Misc features 2 | 3 | ### blank? 4 | 5 | ``` 6 | nil.blank? => true 7 | [].blank? => true 8 | {}.blank? => true 9 | # etc 10 | ``` 11 | -------------------------------------------------------------------------------- /lib/project/pro_motion/support.rb: -------------------------------------------------------------------------------- 1 | module ProMotion 2 | module Support 3 | 4 | def app 5 | RubyMotionQuery::App 6 | end 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /schemas/0001_initial.rb: -------------------------------------------------------------------------------- 1 | schema "0001 initial" do 2 | entity "Contributor" do 3 | string :name, optional: false 4 | 5 | datetime :created_at 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /templates/table_screen_cell/spec/views/name.rb: -------------------------------------------------------------------------------- 1 | describe '<%= @name_camel_case %>' do 2 | 3 | before do 4 | end 5 | 6 | after do 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /templates/screen/spec/screens/name_screen_spec.rb: -------------------------------------------------------------------------------- 1 | describe '<%= @name_camel_case %>Screen' do 2 | 3 | before do 4 | end 5 | 6 | after do 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/test_objects/test_table_screen.rb: -------------------------------------------------------------------------------- 1 | class TestTableScreen < PM::TableScreen 2 | 3 | include DelegateTestAttributes 4 | def table_data; @table_data ||= []; end 5 | 6 | end 7 | -------------------------------------------------------------------------------- /lib/project/pro_motion/screen.rb: -------------------------------------------------------------------------------- 1 | module ProMotion 2 | class Screen < ViewController 3 | 4 | def app 5 | RubyMotionQuery::App 6 | end 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /templates/collection_view_screen/spec/views/name_cell_spec.rb: -------------------------------------------------------------------------------- 1 | describe '<%= @name_camel_case %>Cell' do 2 | 3 | before do 4 | end 5 | 6 | after do 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /spec/ext/ui_image_view_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'UIImageView' do 2 | 3 | it "should respond to remote_image=" do 4 | UIImageView.new.should.respond_to?(:remote_image=) 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /templates/collection_view_screen/spec/screens/name_screen_spec.rb: -------------------------------------------------------------------------------- 1 | describe '<%= @name_camel_case %>Screen' do 2 | 3 | before do 4 | end 5 | 6 | after do 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /templates/metal_table_screen/spec/screens/name_screen_spec.rb: -------------------------------------------------------------------------------- 1 | describe '<%= @name_camel_case %>Screen' do 2 | 3 | before do 4 | end 5 | 6 | after do 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/test_objects/test_grouped_table_screen.rb: -------------------------------------------------------------------------------- 1 | class TestGroupedTableScreen < PM::GroupedTableScreen 2 | 3 | include DelegateTestAttributes 4 | def table_data; @table_data ||= []; end 5 | 6 | end 7 | -------------------------------------------------------------------------------- /schemas/0002_add_city_names.rb: -------------------------------------------------------------------------------- 1 | schema "0002 add city names" do 2 | entity "Contributor" do 3 | string :name, optional: false 4 | string :city, optional: true 5 | 6 | datetime :created_at 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /docs/cookbook/actions.md: -------------------------------------------------------------------------------- 1 | ```ruby 2 | find(UILabel).send(:some_method, args) 3 | find(my_view).hide 4 | find(my_view).show 5 | find(my_view).toggle 6 | find(my_view).toggle_enabled 7 | find(my_text_field).focus # or .become_first_responder 8 | ``` 9 | -------------------------------------------------------------------------------- /app/views/hello_world_section.rb: -------------------------------------------------------------------------------- 1 | class HelloWorldSection < UIView 2 | 3 | def on_load 4 | apply_style :section 5 | 6 | append(UIButton, :section_button).on(:touch) do 7 | mp "Button touched" 8 | end 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /lib/project/ruby_motion_query/traverse.rb: -------------------------------------------------------------------------------- 1 | module RubyMotionQuery 2 | class RMQ 3 | 4 | # TODO Why doesn't this work? 5 | #alias :screen :view_controller 6 | def screen 7 | view_controller 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/test_objects/test_screen_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class TestScreenStylesheet < ApplicationStylesheet 2 | 3 | def root_view(st) 4 | st.background_color = color.white 5 | end 6 | 7 | def test_label(st) 8 | st.text = 'style from sheet' 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /spec/ruby_motion_query/traverse_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'RubyMotionQuery traverse' do 2 | 3 | it "should return the view_controller for screen" do 4 | rmq = RubyMotionQuery::RMQ.new 5 | 6 | rmq.screen.should.equal(rmq.view_controller) 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/test_objects/test_view.rb: -------------------------------------------------------------------------------- 1 | class TestView < UIView 2 | 3 | attr_reader :on_loaded 4 | attr_reader :on_styled_fired 5 | 6 | def on_load 7 | @on_loaded = true 8 | end 9 | 10 | def on_styled 11 | @on_styled_fired = true 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /docs/cookbook/testing.md: -------------------------------------------------------------------------------- 1 | RubyMotion ships with a built-in fork of [MacBacon](https://github.com/alloy/MacBacon), which is itself a fork of [Bacon](https://github.com/chneukirchen/bacon), a small pure-Ruby RSpec clone. 2 | 3 | [Bacon cheatsheet](https://github.com/jamonholmgren/bacon-cheat-sheet) 4 | -------------------------------------------------------------------------------- /app/views/collection_cell.rb: -------------------------------------------------------------------------------- 1 | class CollectionCell < UICollectionViewCell 2 | 3 | def on_load 4 | find(self).apply_style :collection_cell 5 | 6 | find(self.contentView).tap do |q| 7 | q.append(UILabel, :title).get.text = rand(100).to_s 8 | end 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /spec/ext/ui_color_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'UIColor' do 2 | 3 | it "should allow you to get a new color from an existing color with 'with'" do 4 | trans_black = UIColor.blackColor.with(alpha: 0.5) 5 | trans_black.should.equal(UIColor.colorWithRed(0, green: 0, blue: 0, alpha: 0.5)) 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /app/models/contributor.rb: -------------------------------------------------------------------------------- 1 | class Contributor < CDQManagedObject 2 | 3 | scope :starts_with_s, where(:name).begins_with('s').sort_by(:name) 4 | 5 | def cell 6 | { 7 | cell_class: ContributorCell, 8 | properties: { 9 | name: name 10 | } 11 | } 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /docs/cookbook/app.md: -------------------------------------------------------------------------------- 1 | ```ruby 2 | app.window 3 | app.delegate 4 | app.environment 5 | app.release? # .production? also 6 | app.test? 7 | app.development? 8 | app.version 9 | app.name 10 | app.identifier 11 | app.resource_path 12 | app.document_path 13 | app.reset_image_cache! # See images cookbook example 14 | ``` 15 | -------------------------------------------------------------------------------- /app/views/metal_table_cell.rb: -------------------------------------------------------------------------------- 1 | class MetalTableCell < UITableViewCell 2 | 3 | def on_load 4 | find(self.contentView).tap do |q| 5 | @name = q.append!(UILabel, :cell_name) 6 | end 7 | end 8 | 9 | def update(data) 10 | @name.text = "#{data[:name]} is #{data[:num]}" 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /app/views/task_cell.rb: -------------------------------------------------------------------------------- 1 | class TaskCell < PM::TableViewCell 2 | 3 | def on_load 4 | apply_style :cell 5 | 6 | find(self.contentView).tap do |q| 7 | @title = q.append!(UILabel, :cell_title) 8 | end 9 | end 10 | 11 | def my_title=(value) 12 | @title.text = value 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /templates/collection_view_screen/app/views/name_cell.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %>Cell < UICollectionViewCell 2 | 3 | def on_load 4 | find(self).apply_style :<%= @name %>_cell 5 | 6 | q = find(self.contentView) 7 | # Add your subviews, init stuff here 8 | # @foo = q.append!(UILabel, :foo) 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /templates/collection_view_screen/app/stylesheets/name_cell_stylesheet.rb: -------------------------------------------------------------------------------- 1 | module <%= @name_camel_case %>CellStylesheet 2 | 3 | def cell_size 4 | {w: 96, h: 96} 5 | end 6 | 7 | def <%= @name %>_cell(st) 8 | st.frame = cell_size 9 | st.background_color = color.random 10 | 11 | # Style overall view here 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /templates/metal_table_screen/app/views/name_cell.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %>Cell < UITableViewCell 2 | 3 | def on_load 4 | find(self.contentView).tap do |q| 5 | @name = q.append!(UILabel, :cell_name) 6 | end 7 | end 8 | 9 | def update(data) 10 | @name.text = "#{data[:name]} is #{data[:num]}" 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .repl_history 2 | build 3 | tags 4 | app/pixate_code.rb 5 | resources/*.nib 6 | resources/*.momd 7 | resources/*.storyboardc 8 | .DS_Store 9 | nbproject 10 | .redcar 11 | #*# 12 | *~ 13 | *.sw[po] 14 | *.gem 15 | .eprj 16 | .sass-cache 17 | .idea 18 | .dat*.* 19 | rm_spec_output 20 | resources/*.xcdatamodeld 21 | vendor 22 | site/ 23 | Gemfile.lock 24 | -------------------------------------------------------------------------------- /lib/project/ext/ui_table_view_cell.rb: -------------------------------------------------------------------------------- 1 | class UITableViewCell 2 | 3 | # You can use either rmq_build or on_load, not both. If you have both, on_load will be ignored, 4 | # you can however call it from rmq_build. They are the same, on_load follows the ProMotion style 5 | # and is recommended. 6 | def rmq_build 7 | self.on_load 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /lib/project/pro_motion/collection_screen.rb: -------------------------------------------------------------------------------- 1 | module ProMotion 2 | class CollectionScreen 3 | 4 | def set_stylesheet 5 | super.tap do 6 | if self.class.rmq_style_sheet_class 7 | self.collection_view.rmq.apply_style(:collection_view) if self.rmq.stylesheet.respond_to?(:collection_view) 8 | end 9 | end 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/project/ruby_motion_query/stylers/ui_image_view.rb: -------------------------------------------------------------------------------- 1 | module RubyMotionQuery 2 | module Stylers 3 | 4 | class UIImageViewStyler < UIViewStyler 5 | 6 | def remote_image=(value) 7 | @view.remote_image = value 8 | end 9 | 10 | # This is the same functionality as image= 11 | def placeholder_image=(value) 12 | @view.image = value 13 | end 14 | 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/ruby_motion_query/app_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'RubyMotionQuery app' do 2 | 3 | it "should return the current_view_controller for the current_screen" do 4 | expected = RubyMotionQuery::App.current_view_controller 5 | RubyMotionQuery::App.current_screen.should.equal(expected) 6 | end 7 | 8 | it "should retun cdq object when using app.data" do 9 | RubyMotionQuery::App.data.should == CDQ.cdq 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /app/stylesheets/collection_cell_stylesheet.rb: -------------------------------------------------------------------------------- 1 | module CollectionCellStylesheet 2 | 3 | def cell_size 4 | {w: 96, h: 96} 5 | end 6 | 7 | def collection_cell(st) 8 | st.frame = cell_size 9 | st.background_color = color.random 10 | 11 | # Style overall view here 12 | end 13 | 14 | def title(st) 15 | st.frame = :full 16 | st.color = color.white 17 | st.text_alignment = :center 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /app/stylesheets/tasks_screen_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class TasksScreenStylesheet < ApplicationStylesheet 2 | 3 | def root_view(st) 4 | st.background_color = color.gray 5 | end 6 | 7 | def cell(st) 8 | st.background_color = color.green 9 | end 10 | 11 | def cell_title(st) 12 | st.frame = {t: 5, w: 200, h: 20, centered: :horizontal} 13 | st.background_color = color.white 14 | st.color = color.red 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | 3 | 0. Create an issue in GitHub to make sure your PR will be accepted. 4 | 1. Fork it 5 | 2. Create your feature branch (`git checkout -b my-new-feature`) 6 | 3. Write tests for your changes 7 | 4. Make your changes 8 | 5. Document your changes in the `docs` folder 9 | 6. Commit your changes (`git commit -am 'Add some feature'`) 10 | 7. Push to the branch (`git push origin my-new-feature`) 11 | 8. Create new Pull Request 12 | -------------------------------------------------------------------------------- /docs/cookbook/format.md: -------------------------------------------------------------------------------- 1 | A performant way to format numbers and dates. 2 | 3 | ```ruby 4 | rmq.format.number(1232, '#,##0.##') 5 | rmq.format.date(Time.now, 'EEE, MMM d, ''yy') 6 | rmq.format.numeric_formatter(your_format_here) # returns cached numeric formatter 7 | rmq.format.date_formatter(your_format_here) # returns cached date formatter 8 | ``` 9 | See for more information about date format strings. 10 | -------------------------------------------------------------------------------- /lib/project/ext/object.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | 3 | def app 4 | rmq.app 5 | end 6 | 7 | def device 8 | rmq.device 9 | end 10 | 11 | def blank? 12 | self.respond_to?(:empty?) ? self.empty? : !self 13 | end 14 | 15 | def find(*args) # Do not alias this, strange bugs happen where classes don't have methods 16 | rmq(*args) 17 | end 18 | 19 | def find!(*args) 20 | rmq(*args).get 21 | end 22 | 23 | def screen 24 | rmq.screen 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /docs/cookbook/fonts.md: -------------------------------------------------------------------------------- 1 | ```ruby 2 | font.family_list # useful in console 3 | font.for_family('Helvetica') # useful in console 4 | 5 | font.system(12) 6 | font.font_with_name('Helvetica', 18) 7 | 8 | # Add a new standard font (usually in ApplicationStylesheet.rb) 9 | font_family = 'Helvetica Neue' 10 | font.add_named :large, font_family, 36 11 | font.add_named :medium, font_family, 24 12 | font.add_named :small, font_family, 18 13 | 14 | # then use them like so 15 | font.large 16 | font.small 17 | ``` 18 | -------------------------------------------------------------------------------- /lib/project/ext/ui_collection_view_cell.rb: -------------------------------------------------------------------------------- 1 | class UICollectionViewCell 2 | 3 | # You can use either rmq_build or on_load, not both. If you have both, on_load will be ignored, 4 | # you can however call it from rmq_build. They are the same, on_load follows the ProMotion style 5 | # and is recommended. 6 | def rmq_build 7 | self.on_load 8 | end 9 | 10 | def reused 11 | @reused 12 | end 13 | 14 | def prepareForReuse 15 | super 16 | @reused = true 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /lib/project/ext/kernel.rb: -------------------------------------------------------------------------------- 1 | if RUBYMOTION_ENV == "development" 2 | class TopLevel 3 | 4 | def live(interval = 1.0, debug=false) 5 | rmq_live_stylesheets interval, debug 6 | end 7 | 8 | def enable_live_stylesheets(interval) 9 | enable_rmq_live_stylesheets interval 10 | end 11 | 12 | def open(screen, args={}) 13 | find.screen.open(screen, args) 14 | end 15 | 16 | def close(args={}) 17 | find.screen.close(args) 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /templates/screen/app/stylesheets/name_screen_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %>ScreenStylesheet < ApplicationStylesheet 2 | 3 | # Add your view stylesheets here. You can then override styles if needed, 4 | # example: include FooStylesheet 5 | 6 | def setup 7 | # Add stylesheet specific setup stuff here. 8 | # Add application specific setup stuff in application_stylesheet.rb 9 | end 10 | 11 | def root_view(st) 12 | st.background_color = color.white 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /app/stylesheets/example_controller_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class ExampleControllerStylesheet < ApplicationStylesheet 2 | 3 | def setup 4 | # Add stylesheet specific setup stuff here. 5 | # Add application specific setup stuff in application_stylesheet.rb 6 | end 7 | 8 | def root_view(st) 9 | st.background_color = color.purple 10 | end 11 | 12 | def hello_world(st) 13 | st.frame = {w: 200, h: 30, centered: :both} 14 | st.color = color.black 15 | st.font = font.large 16 | st.text = "Hello world" 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /app/screens/contributor_screen.rb: -------------------------------------------------------------------------------- 1 | class ContributorScreen < PM::DataTableScreen 2 | 3 | title "RedPotion Contributors" 4 | stylesheet ContributorScreenStylesheet 5 | model Contributor 6 | refreshable 7 | searchable fields: [:name, :city] 8 | 9 | def on_load 10 | @refreshed = false 11 | end 12 | 13 | def on_refresh 14 | stop_refreshing 15 | @refreshed = true 16 | end 17 | 18 | # Remove if you are only supporting portrait 19 | def will_animate_rotate(orientation, duration) 20 | reapply_styles 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/project/ext/ui_color.rb: -------------------------------------------------------------------------------- 1 | class UIColor 2 | 3 | def with(options) 4 | type = CGSize.type[/(f|d)/] 5 | r, g, b, a = Pointer.new(type), Pointer.new(type), Pointer.new(type), Pointer.new(type) 6 | self.getRed(r, green: g, blue: b, alpha: a) 7 | 8 | r = options[:r] || options[:red] || r.value 9 | g = options[:g] || options[:green] || g.value 10 | b = options[:b] || options[:blue] || b.value 11 | a = options[:a] || options[:alpha] || a.value 12 | 13 | UIColor.colorWithRed(r, green: g, blue: b, alpha: a) 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /app/stylesheets/metal_table_screen_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class MetalTableScreenStylesheet < ApplicationStylesheet 2 | 3 | def setup 4 | end 5 | 6 | def root_view(st) 7 | st.background_color = color.gray 8 | end 9 | 10 | def cell_height 11 | 80 # Anything visual should be in the stylesheet 12 | end 13 | 14 | def cell(st) 15 | st.background_color = color.yellow 16 | end 17 | 18 | def cell_name(st) 19 | st.frame = {t: 5, w: 200, h: 20, centered: :horizontal} 20 | st.background_color = color.white 21 | st.color = color.blue 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /app/views/contributor_cell.rb: -------------------------------------------------------------------------------- 1 | class ContributorCell < ProMotion::TableViewCell 2 | 3 | def on_load 4 | apply_style :cell 5 | 6 | find(self.contentView).tap do |q| 7 | @title = q.append!(UILabel, :cell_title) 8 | @button = q.append!(UIButton, :github_button) 9 | end 10 | end 11 | 12 | def name=(value) 13 | @title.text = value 14 | # TODO: this is being called twice for each cell... 15 | @button.off.on(:tap) do 16 | url = NSURL.URLWithString("http://www.github.com/#{value}") 17 | UIApplication.sharedApplication.openURL(url) 18 | end 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /docs/cookbook/accessibility.md: -------------------------------------------------------------------------------- 1 | ## Accessibility 2 | 3 | ### motion-accessibility gem 4 | 5 | [motion-accessibility](https://github.com/austinseraphin/motion-accessibility) gem. 6 | 7 | In your gemfile: 8 | 9 | ``` 10 | gem "motion-accessibility" 11 | ``` 12 | 13 | This gem has RedPotion capabilities. You can test any selection for accessibility. For example: 14 | 15 | ``` 16 | it "has accessible buttons" do 17 | find(UIButton).should.be.accessible 18 | end 19 | ``` 20 | 21 | ### Future 22 | 23 | We're currently adding new features to RedPotion, such as disabling animations when VoiceOver is turned on. 24 | 25 | 26 | -------------------------------------------------------------------------------- /lib/redpotion.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | unless defined?(Motion::Project::Config) 4 | raise "This file must be required within a RubyMotion project Rakefile." 5 | end 6 | 7 | require 'ruby_motion_query' 8 | require 'ProMotion' 9 | require 'motion_print' 10 | require 'RedAlert' 11 | 12 | lib_dir_path = File.dirname(File.expand_path(__FILE__)) 13 | Motion::Project::App.setup do |app| 14 | insert_point = app.files.find_index { |file| file =~ /^(?:\.\/)?app\// } || 0 15 | 16 | Dir.glob(File.join(lib_dir_path, "project/**/*.rb")).reverse.each do |file| 17 | app.files.insert(insert_point, file) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/stylesheets/contributor_screen_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class ContributorScreenStylesheet < ApplicationStylesheet 2 | 3 | def root_view(st) 4 | st.background_color = color.white 5 | end 6 | 7 | def cell(st) 8 | st.background_color = color.white 9 | end 10 | 11 | def cell_title(st) 12 | st.frame = {t: 5, w: device_width / 2, h: 20, l: 5} 13 | st.background_color = color.white 14 | st.color = color.black 15 | end 16 | 17 | def github_button(st) 18 | st.text = "View profile" 19 | st.frame = {t: 5, w: device_width / 2, h: 20, fr: 5} 20 | st.color = color.orange 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode7.2 3 | before_install: 4 | - (ruby --version) 5 | - sudo chown -R travis ~/Library/RubyMotion 6 | - sudo mkdir -p ~/Library/RubyMotion/build 7 | - sudo chown -R travis ~/Library/RubyMotion/build 8 | - sudo motion update 9 | gemfile: 10 | - Gemfile 11 | cache: 12 | directories: 13 | - vendor/bundle 14 | - vendor/Pods 15 | install: 16 | - bundle install --jobs=3 --retry=3 17 | - pod setup > /dev/null 18 | - bundle exec rake clean 19 | - bundle exec rake pod:install > /dev/null 20 | script: 21 | - bundle exec rake spec output=test_unit 22 | env: 23 | global: 24 | - COCOAPODS_NO_REPO_UPDATE_OUTPUT=true 25 | -------------------------------------------------------------------------------- /spec/ext/object_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Object' do 2 | 3 | before { @subject = Object.new } 4 | 5 | it "should return the RMQ App when Object#app is called" do 6 | @subject.app.should.equal(RubyMotionQuery::App) 7 | end 8 | 9 | it "should return the RMQ Device when Object#device is called" do 10 | @subject.device.should.equal(RubyMotionQuery::Device) 11 | end 12 | 13 | it "find should be an alias for rmq" do 14 | @subject.find.is_a?(RubyMotionQuery::RMQ).should.be.true 15 | end 16 | 17 | it "should have access to the current screen" do 18 | controller = UIViewController.alloc.init 19 | controller.rmq.screen.should == controller 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /templates/metal_table_screen/app/stylesheets/name_screen_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %>ScreenStylesheet < ApplicationStylesheet 2 | 3 | def setup 4 | # Add stylesheet specific setup stuff here. 5 | # Add application specific setup stuff in application_stylesheet.rb 6 | end 7 | 8 | def root_view(st) 9 | st.background_color = color.gray 10 | end 11 | 12 | def cell(st) 13 | # Style overall cell here 14 | st.background_color = color.random 15 | end 16 | 17 | def cell_height 18 | 80 19 | end 20 | 21 | def cell_name(st) 22 | st.frame = {left: 5, top: 5, from_right: 10, from_bottom: 5} 23 | st.color = color.black 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/project/pro_motion/data_table_screen.rb: -------------------------------------------------------------------------------- 1 | module ProMotion 2 | class DataTableScreen < TableViewController 3 | 4 | include ProMotion::ScreenModule 5 | include ProMotion::DataTable 6 | 7 | class << self 8 | def model(value, opts = {}) 9 | if value.method_defined?(:cell) 10 | @opts = { 11 | model: value, 12 | scope: :all, 13 | }.merge(opts) 14 | else 15 | raise "#{value} must define the cell method" 16 | end 17 | end 18 | 19 | def data_model 20 | @opts[:model] 21 | end 22 | 23 | def data_scope 24 | @opts[:scope] 25 | end 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/shared/contributors_module.rb: -------------------------------------------------------------------------------- 1 | module ContributorsModule 2 | 3 | def contributors 4 | [{ 5 | name: 'twerth', 6 | city: "San Francisco" 7 | }, { 8 | name: 'squidpunch', 9 | city: nil 10 | }, { 11 | name: 'GantMan', 12 | city: "New Orleans, LA" 13 | }, { 14 | name: 'shreeve', 15 | city: nil 16 | }, { 17 | name: 'chunlea', 18 | city: 'Shandong, China' 19 | }, { 20 | name: 'markrickert', 21 | city: 'Everywhere, USA' 22 | }] 23 | end 24 | 25 | def init_contributors 26 | Contributor.destroy_all 27 | contributors.each do |c| 28 | Contributor.new(c) 29 | end 30 | cdq.save 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /templates/view/app/stylesheets/name_stylesheet.rb: -------------------------------------------------------------------------------- 1 | # To style this view include its stylesheet at the top of each controller's 2 | # stylesheet that is going to use it: 3 | # class SomeStylesheet < ApplicationStylesheet 4 | # include <%= @name_camel_case %>Stylesheet 5 | 6 | # Another option is to use your controller's stylesheet to style this view. This 7 | # works well if only one controller uses it. If you do that, delete the 8 | # view's stylesheet with: 9 | # rm app/stylesheets/<%= @name %>_stylesheet.rb 10 | 11 | module <%= @name_camel_case %>Stylesheet 12 | 13 | def <%= @name %>(st) 14 | st.frame = {l: 5, t: 100, w: 80, h: 40} 15 | st.background_color = color.light_gray 16 | 17 | # Style overall view here 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/project/pro_motion/data_table_search_delegate.rb: -------------------------------------------------------------------------------- 1 | class DataTableSeachDelegate 2 | attr_accessor :parent 3 | 4 | # UISearchControllerDelegate methods 5 | 6 | def willPresentSearchController(search_controller) 7 | parent.dt_searchDisplayControllerWillBeginSearch(search_controller) 8 | end 9 | 10 | def willDismissSearchController(search_controller) 11 | parent.dt_searchDisplayControllerWillEndSearch(search_controller) 12 | end 13 | 14 | # UISearchResultsUpdating protocol method 15 | def updateSearchResultsForSearchController(search_controller) 16 | search_string = search_controller.searchBar.text 17 | parent.dt_searchDisplayController(search_controller, shouldReloadTableForSearchString: search_string) if @_data_table_searching 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate < PM::Delegate 2 | 3 | include CDQ 4 | include ContributorsModule 5 | status_bar true, animation: :fade 6 | 7 | def on_load(app, options) 8 | cdq.setup 9 | 10 | return true if RUBYMOTION_ENV == 'test' 11 | 12 | init_contributors if Contributor.count < contributors.count 13 | 14 | open HomeScreen.new(nav_bar: true) 15 | end 16 | 17 | # Remove this if you are only supporting portrait 18 | def application(application, willChangeStatusBarOrientation: new_orientation, duration: duration) 19 | # Manually set RMQ's orientation before the device is actually oriented 20 | # So that we can do stuff like style views before the rotation begins 21 | device.orientation = new_orientation 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/project/ext/pm_delegate.rb: -------------------------------------------------------------------------------- 1 | # FXFormViewController is a top level class 2 | class FXFormViewController < UIViewController 3 | end 4 | 5 | # Defines which ProMotion classes handle delegates 6 | module ProMotion 7 | class ViewController < UIViewController 8 | 9 | def class_handles_delegates? 10 | true 11 | end 12 | 13 | end 14 | 15 | class TableViewController < UITableViewController 16 | 17 | def class_handles_delegates? 18 | true 19 | end 20 | 21 | end 22 | 23 | # Include the entire class declaration chain for people that may not have 24 | # the ProMotion-form gem installed. 25 | class FormViewController < FXFormViewController 26 | 27 | def class_handles_delegates? 28 | true 29 | end 30 | 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/example_controller.rb: -------------------------------------------------------------------------------- 1 | # It's prefered you use a screen. But you can also use normal controllers. If you want 2 | # to turn any controller into a screen, see metal_table_screen for an example. 3 | class ExampleController < UIViewController 4 | 5 | # You can't use any of the ProMotion screen stuff here, but you can use the RedPotion stuff, and 6 | # RedPotion adds stuff like on_load to normal controllers. It also has all the rmq shortcuts 7 | 8 | stylesheet ExampleControllerStylesheet 9 | 10 | def on_load 11 | # You cannot use title above 'stylesheet' because that is a PM feature 12 | self.title = "Example controller" 13 | 14 | append UILabel, :hello_world 15 | end 16 | 17 | def will_animate_rotate(orientation, duration) 18 | reapply_styles 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /lib/project/pro_motion/table.rb: -------------------------------------------------------------------------------- 1 | module ProMotion 2 | 3 | class TableScreen < TableViewController 4 | # Don't call super -- would result in two on_load calls to the cell. 5 | def on_cell_created(cell, data) 6 | self.rmq.build(cell) 7 | end 8 | end 9 | 10 | # This is duplicated from ProMotion in order to be call 11 | # make_data_table_searchable instead of make_searchable 12 | module Table 13 | def set_up_searchable 14 | if self.class.respond_to?(:get_searchable) && self.class.get_searchable 15 | if self.is_a?(ProMotion::DataTableScreen) 16 | self.make_data_table_searchable(content_controller: self, search_bar: self.class.get_searchable_params) 17 | else 18 | self.make_searchable 19 | end 20 | end 21 | end 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /docs/cookbook/selecting.md: -------------------------------------------------------------------------------- 1 | You can select a view using the following: 2 | 3 | * Constant 4 | * :a_tag 5 | * :a_style_name 6 | * my_view_instance 7 | * text: 'you can select via attributes also' 8 | * :another_tag, UILabel, text: 'an array' <- this is an "or", use .and for and 9 | 10 | The more common use is to select any view or views you have assigned to variables, then perform actions on them. For example: 11 | 12 | ```ruby 13 | view_1 = UIView.alloc.initWithFrame([[10,10],[100, 10]]) 14 | view_2 = UIView.alloc.initWithFrame([[10,20],[100, 10]]) 15 | @view_3 = append(UIView, :some_style).get 16 | 17 | find(view_1).layout(l: 20, t: 40, w: 80, h: 20) 18 | 19 | find(view_1, view_2, @view_3).hide 20 | a = [view_1, view_2, @view_3] 21 | 22 | find(a).distribute(:vertical, margin: 10) 23 | 24 | find(a).on(:tap) do |sender| 25 | puts 'Tapped' 26 | end 27 | ``` 28 | -------------------------------------------------------------------------------- /templates/view/app/views/name.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %> < UIView 2 | 3 | def on_load 4 | apply_style :<%= @name %> 5 | 6 | # Add subviews here, like so: 7 | # append UILabel, :label_style_here 8 | # -or- 9 | # @submit_button = append(UIButton, :submit).get 10 | # -or- 11 | # @submit_button = append! UIButton, :submit 12 | end 13 | 14 | end 15 | 16 | # To style this view include its stylesheet at the top of each controller's 17 | # stylesheet that is going to use it: 18 | # class SomeStylesheet < ApplicationStylesheet 19 | # include <%= @name_camel_case %>Stylesheet 20 | 21 | # Another option is to use your controller's stylesheet to style this view. This 22 | # works well if only one controller uses it. If you do that, delete the 23 | # view's stylesheet with: 24 | # rm app/stylesheets/<%= @name %>_stylesheet.rb 25 | -------------------------------------------------------------------------------- /app/stylesheets/collection_screen_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class CollectionScreenStylesheet < ApplicationStylesheet 2 | 3 | include CollectionCellStylesheet 4 | 5 | def setup 6 | # Add stylesheet specific setup stuff here. 7 | # Add application specific setup stuff in application_stylesheet.rb 8 | 9 | @margin = ipad? ? 12 : 8 10 | end 11 | 12 | def collection_view(st) 13 | st.view.contentInset = [@margin, @margin, @margin, @margin] 14 | st.background_color = color.white 15 | 16 | st.view.collectionViewLayout.tap do |cl| 17 | cl.itemSize = [cell_size[:w], cell_size[:h]] 18 | #cl.scrollDirection = UICollectionViewScrollDirectionHorizontal 19 | #cl.headerReferenceSize = [cell_size[:w], cell_size[:h]] 20 | cl.minimumInteritemSpacing = @margin 21 | cl.minimumLineSpacing = @margin 22 | #cl.sectionInsert = [0,0,0,0] 23 | end 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /app/screens/tasks_screen.rb: -------------------------------------------------------------------------------- 1 | class TasksScreen < PM::TableScreen 2 | 3 | title "Tasks" 4 | refreshable 5 | stylesheet TasksScreenStylesheet 6 | searchable placeholder: "Search tasks" 7 | 8 | def on_load 9 | end 10 | 11 | def on_refresh 12 | end 13 | 14 | def table_data 15 | [{ 16 | title: "Tasks", 17 | cells: [ 18 | { 19 | cell_class: TaskCell, 20 | properties: { 21 | my_title: "First task" 22 | }, 23 | search_text: "First task", 24 | }, 25 | { 26 | cell_class: TaskCell, 27 | properties: { 28 | my_title: "Second task" 29 | }, 30 | search_text: "Second task", 31 | } 32 | ] 33 | }] 34 | end 35 | 36 | # Remove if you are only supporting portrait 37 | def will_animate_rotate(orientation, duration) 38 | reapply_styles 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /docs/cookbook/tagging.md: -------------------------------------------------------------------------------- 1 | ```ruby 2 | # Add tags 3 | find(my_view).tag(:your_tag) 4 | find(my_view).clear_tags.tag(:your_new_tag) 5 | 6 | find(my_view).find(UILabel).tag(:selected, :customer) 7 | 8 | # You can use a tag or tags as selectors 9 | find(:selected).hide 10 | find(:your_tag).and(:selected).hide 11 | 12 | # Check a selection for tags 13 | find(your_view).has_tag?(:foo) 14 | 15 | # Untag 16 | find(your_view).untag(:foo) 17 | find(:your_tag).untag(:your_tag) 18 | find(view_a, view_b).untag(:foo, bar) 19 | 20 | # You can optionally store a value in the tag, which can be super useful in some rare situations 21 | find(my_view).tag(your_tag: 22) 22 | find(my_view).tag(your_tag: 22, your_other_tag: 'Hello world') 23 | 24 | # You can query the data from your tags if you have stored values in them 25 | find(my_view).tags(:your_tag) 26 | 27 | # You can also get a hash of tag and values by simply calling `tags` 28 | find(my_view).tags 29 | ``` 30 | -------------------------------------------------------------------------------- /templates/collection_view_screen/app/stylesheets/name_screen_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %>ScreenStylesheet < ApplicationStylesheet 2 | 3 | include <%= @name_camel_case %>CellStylesheet 4 | 5 | def setup 6 | # Add stylesheet specific setup stuff here. 7 | # Add application specific setup stuff in application_stylesheet.rb 8 | 9 | @margin = ipad? ? 12 : 8 10 | end 11 | 12 | def collection_view(st) 13 | st.view.contentInset = [@margin, @margin, @margin, @margin] 14 | st.background_color = color.white 15 | 16 | st.view.collectionViewLayout.tap do |cl| 17 | cl.itemSize = [cell_size[:w], cell_size[:h]] 18 | #cl.scrollDirection = UICollectionViewScrollDirectionHorizontal 19 | #cl.headerReferenceSize = [cell_size[:w], cell_size[:h]] 20 | cl.minimumInteritemSpacing = @margin 21 | cl.minimumLineSpacing = @margin 22 | #cl.sectionInset = [0,0,0,0] 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/test_objects/delegate_test_attributes.rb: -------------------------------------------------------------------------------- 1 | module DelegateTestAttributes 2 | 3 | attr_accessor :view_did_load_count 4 | attr_accessor :view_will_appear_count 5 | attr_accessor :view_did_appear_count 6 | attr_accessor :view_will_disappear_count 7 | attr_accessor :view_did_disappear_count 8 | 9 | def view_did_load 10 | self.view_did_load_count ||= 0 11 | self.view_did_load_count += 1 12 | end 13 | 14 | def view_will_appear(animated) 15 | self.view_will_appear_count ||= 0 16 | self.view_will_appear_count += 1 17 | end 18 | 19 | def view_did_appear(animated) 20 | self.view_did_appear_count ||= 0 21 | self.view_did_appear_count += 1 22 | end 23 | 24 | def view_will_disappear(animated) 25 | self.view_will_disappear_count ||= 0 26 | self.view_will_disappear_count += 1 27 | end 28 | 29 | def view_did_disappear(animated) 30 | self.view_did_disappear_count ||= 0 31 | self.view_did_disappear_count += 1 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /spec/pro_motion/screen_module_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'ScreenModule' do 2 | 3 | class TestScreenStylesheet < ApplicationStylesheet 4 | def root_view(st) 5 | st.background_color = color.white 6 | end 7 | end 8 | 9 | class TestScreen < PM::Screen 10 | stylesheet TestScreenStylesheet 11 | 12 | def grab_app 13 | app 14 | end 15 | end 16 | 17 | tests TestScreen 18 | 19 | it "should set the rmq stylesheet" do 20 | controller.rmq.stylesheet.should.not.be.nil 21 | end 22 | 23 | describe "not providing a stylesheet" do 24 | class TestScreenTwo < PM::Screen; end 25 | 26 | it "should not raise exception if the stylesheet is not defined in the class" do 27 | should.not.raise(RuntimeError) do 28 | TestScreenTwo.new.view_did_load 29 | end 30 | end 31 | 32 | it 'should return RubyMotionQuery::App when using app inside a screen' do 33 | TestScreen.new.grab_app.should == RubyMotionQuery::App 34 | end 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /templates/screen/app/screens/name_screen.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %>Screen < PM::<%= @screen_base %> 2 | 3 | title "Your title here" 4 | stylesheet <%= @name_camel_case %>ScreenStylesheet 5 | 6 | def on_load 7 | end 8 | <% if @screen_base == 'TableScreen' %> 9 | def table_data 10 | [] 11 | end 12 | <% end %> 13 | 14 | # You don't have to reapply styles to all UIViews, if you want to optimize, another way to do it 15 | # is tag the views you need to restyle in your stylesheet, then only reapply the tagged views, like so: 16 | # def logo(st) 17 | # st.frame = {t: 10, w: 200, h: 96} 18 | # st.centered = :horizontal 19 | # st.image = image.resource('logo') 20 | # st.tag(:reapply_style) 21 | # end 22 | # 23 | # Then in will_animate_rotate 24 | # find(:reapply_style).reapply_styles# 25 | 26 | # Remove the following if you're only using portrait 27 | def will_animate_rotate(orientation, duration) 28 | reapply_styles 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /templates/table_screen_cell/app/stylesheets/name_stylesheet.rb: -------------------------------------------------------------------------------- 1 | # To style this view include its stylesheet at the top of each controller's 2 | # stylesheet that is going to use it: 3 | # class SomeStylesheet < ApplicationStylesheet 4 | # include <%= @name_camel_case %>Stylesheet 5 | 6 | # Another option is to use your controller's stylesheet to style this view. This 7 | # works well if only one controller uses it. If you do that, delete the 8 | # view's stylesheet with: 9 | # rm app/stylesheets/<%= @name %>_stylesheet.rb 10 | 11 | module <%= @name_camel_case %>Stylesheet 12 | 13 | def <%= @name %>_height 14 | 40 15 | end 16 | 17 | def <%= @name %>(st) 18 | st.frame = {l: 5, t: 100, w: 80, h: <%= @name %>_height} 19 | st.background_color = color.light_gray 20 | 21 | # Style overall view here 22 | end 23 | 24 | def <%= @name %>_title(st) 25 | st.frame = {l: 10, fr: 0, centered: :vertical, h: 20} 26 | st.font = font.medium 27 | st.color = color.black 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /app/stylesheets/application_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class ApplicationStylesheet < RubyMotionQuery::Stylesheet 2 | 3 | def application_setup 4 | 5 | # Change the default grid if desired 6 | # rmq.app.grid.tap do |g| 7 | # g.num_columns = 12 8 | # g.column_gutter = 10 9 | # g.num_rows = 18 10 | # g.row_gutter = 10 11 | # g.content_left_margin = 10 12 | # g.content_top_margin = 74 13 | # g.content_right_margin = 10 14 | # g.content_bottom_margin = 10 15 | # end 16 | 17 | # An example of setting standard fonts and colors 18 | font_family = 'Helvetica Neue' 19 | font.add_named :large, font_family, 36 20 | font.add_named :medium, font_family, 24 21 | font.add_named :small, font_family, 16 22 | font.add_named :tiny, font_family, 13 23 | 24 | color.add_named :tint, '236EB7' 25 | color.add_named :translucent_black, color.from_rgba(0, 0, 0, 0.4) 26 | color.add_named :battleship_gray, '#7F7F7F' 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /docs/cookbook/colors.md: -------------------------------------------------------------------------------- 1 | ## Get colors 2 | 3 | ```ruby 4 | color.red 5 | color('#ffffff') 6 | color('ffffff') 7 | color(hex: '000', a: 0.5) 8 | color(128, 128, 128, 0.5) 9 | color(r: 128, g: 128, b: 128, a: 0.5) 10 | color(red: 128, green: 128, blue: 128, alpha: 0.5) 11 | color(h: 4, s: 3, b: 2, a: 1) 12 | ``` 13 | ## Add a new standard color 14 | 15 | ```ruby 16 | color.add_named :pitch_black, '#000000' 17 | # Or 18 | color.add_named :pitch_black, color.black 19 | 20 | # have color and you need to just adjust one of the values? 21 | color(base: color.black, a: 0.5) 22 | ``` 23 | 24 | Color has a `with` method. Allowing you to build a color from an existing color easily 25 | 26 | ```ruby 27 | # For example that time you want your existing color, but with a slight change 28 | color.my_custom_color.with(a: 0.5) 29 | ``` 30 | 31 | Added standard colors allow you to create your named colors, often in your app-wide `application_stylesheet.rb` or in any stylesheet's `application_setup` method, for easy use and single point of change. 32 | -------------------------------------------------------------------------------- /lib/project/ruby_motion_query/app.rb: -------------------------------------------------------------------------------- 1 | module RubyMotionQuery 2 | class App 3 | class << self 4 | 5 | def current_screen(root_view_controller = nil) 6 | current_view_controller root_view_controller 7 | end 8 | 9 | def data(*args) # Do not alias this 10 | CDQ.cdq(*args) 11 | end 12 | 13 | def reset_image_cache! 14 | if !!defined?(SDWebImageManager) 15 | image_cache = SDImageCache.sharedImageCache 16 | image_cache.clearMemory 17 | if image_cache.respond_to?(:clearDisk) 18 | # Support for SDWebImage v3.x 19 | image_cache.clearDisk 20 | else 21 | # Support for SDWebImage v4.x 22 | image_cache.deleteOldFiles 23 | end 24 | else 25 | puts "\n[RedPotion ERROR] tried to reset image cache without SDWebImage cocoapod. Please add this to your Rakefile: \n\napp.pods do\n pod \"SDWebImage\"\nend\n" 26 | end 27 | end 28 | 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /docs/cookbook/device.md: -------------------------------------------------------------------------------- 1 | ```ruby 2 | device.screen 3 | device.width # screen width 4 | device.height # screen height 5 | device.ipad? 6 | device.iphone? 7 | device.retina? 8 | ``` 9 | 10 | ## Phone size 11 | 12 | ```ruby 13 | device.three_point_five_inch? 14 | device.four_inch? 15 | device.four_point_seven_inch? 16 | device.five_point_five_inch? 17 | ``` 18 | 19 | ## iOS version 20 | 21 | ```ruby 22 | device.ios_version # "8.0" etc 23 | device.is_version? "8.0" # be specific on minor versions 24 | device.is_version? "8" # or just use the major version number 25 | device.ios_at_least? 8 # iOS must be 8.0 or higher for true 26 | device.ios_at_least? 8.1 # returns true if version is 8.1 or higher 27 | ``` 28 | 29 | ## Are you in the simulator? 30 | 31 | ```ruby 32 | device.simulator? 33 | ``` 34 | 35 | # Orientation 36 | 37 | ```ruby 38 | # return values are :unknown, :portrait, :portrait_upside_down, :landscape_left, 39 | # :landscape_right, :face_up, :face_down 40 | device.orientation 41 | device.landscape? 42 | device.portrait? 43 | ``` 44 | -------------------------------------------------------------------------------- /spec/screens/ui_collection_view.rb: -------------------------------------------------------------------------------- 1 | describe 'UICollectionViewController' do 2 | 3 | before do 4 | @controller = CollectionScreen.new 5 | end 6 | 7 | it "should have sections" do 8 | @controller.numberOfSectionsInCollectionView(@controller.collectionView).should == 1 9 | end 10 | 11 | it "should have cells" do 12 | @controller.collectionView(@controller.collectionView, numberOfItemsInSection: 0).should == 200 13 | end 14 | 15 | it "should have cells of the proper class" do 16 | path = NSIndexPath.indexPathForRow(0, inSection:0) 17 | cell = @controller.collectionView(@controller.collectionView, cellForItemAtIndexPath: path) 18 | 19 | cell.class.should == CollectionCell 20 | end 21 | 22 | it "should provide a reference back to the collection view from the cell" do 23 | path = NSIndexPath.indexPathForRow(0, inSection:0) 24 | cell = @controller.collectionView(@controller.collectionView, cellForItemAtIndexPath: path) 25 | 26 | cell.find.screen.should == @controller 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | $:.unshift("~/.rubymotion/rubymotion-templates") 4 | 5 | require "motion/project/template/ios" 6 | require "motion/project/template/gem/gem_tasks" 7 | require "bundler/setup" 8 | require "motion_print" 9 | require "webstub" 10 | 11 | Bundler.require 12 | 13 | Motion::Project::App.setup do |app| 14 | # Use `rake config' to see complete project settings. 15 | app.identifier = "com.infinitered.redpotion" 16 | app.name = "RedPotion" 17 | app.deployment_target = "8.0" 18 | 19 | app.icons = Dir.glob("resources/icon*.png").map{|icon| icon.split("/").last} 20 | 21 | app.device_family = [:iphone, :ipad] 22 | app.interface_orientations = [:portrait, :landscape_left, :landscape_right, :portrait_upside_down] 23 | 24 | app.redgreen_style = :full # test output 25 | 26 | app.info_plist['NSAppTransportSecurity'] = { 'NSAllowsArbitraryLoads' => true } # allow HTTP requests in tests 27 | 28 | app.pods do 29 | pod "SDWebImage", "~> 4.3" 30 | end 31 | end 32 | task :"build:simulator" => :"schema:build" 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014-2018 Infinite Red, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/cookbook/attributes.md: -------------------------------------------------------------------------------- 1 | RedPotion provides a variety of ways to update a view's data or attributes. All of which are chain-able. 2 | 3 | ### Data 4 | 5 | You can use **.data** to either set or retrieve the "data" of the selected views. Depending on the view type, it will be the most common attribute, such as .text for UITextField or .image for UIImageView. 6 | 7 | To set the data, you can do this 8 | 9 | ```ruby 10 | find(my_text_field).data('Foo') 11 | find(my_label).data('Bar') 12 | ``` 13 | 14 | To get the data: 15 | 16 | ```ruby 17 | find(my_text_field).data 18 | => 'Foo' 19 | 20 | # Returns an array if more than one view is selected 21 | find(UIView).data 22 | => ['Foo', 'Bar'] 23 | ``` 24 | 25 | This is chain-able. For example, let's say you need to set a label's title before applying a style, you could do this: 26 | 27 | ```ruby 28 | find.append(UILabel).data('Some long title').apply_style(:name_label) 29 | ``` 30 | 31 | You also use an **=** like so, but it's not chain-able: 32 | 33 | ```ruby 34 | find(my_text_field).data = 'foo' 35 | ``` 36 | 37 | ### Attr 38 | 39 | You can set any attribute: 40 | 41 | ```ruby 42 | find(my_text_field).attr(text: 'Foo') 43 | ``` 44 | 45 | ### Worse case scenario, you can use send: 46 | 47 | ```ruby 48 | find(UILabel).send(:some_method, args) 49 | ``` 50 | -------------------------------------------------------------------------------- /redpotion.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "project/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "redpotion" 7 | spec.authors = ["Infinite Red"] 8 | spec.email = ["hello@infinite.red"] 9 | spec.description = %q{RedPotion - The best combination of RubyMotion tools and libraries} 10 | spec.summary = %q{RedPotion combines RMQ, ProMotion, CDQ, AFMotion, and more for the perfect mix to develop in RubyMotion fast} 11 | spec.homepage = "http://redpotion.org" 12 | spec.license = "MIT" 13 | 14 | files = [] 15 | files << 'README.md' 16 | files.concat(Dir.glob('lib/**/*.rb')) 17 | files.concat(Dir.glob('templates/**/*.rb')) 18 | spec.files = files 19 | 20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 21 | spec.require_paths = ["lib"] 22 | spec.version = RedPotion::VERSION 23 | 24 | spec.executables << 'potion' 25 | 26 | spec.add_runtime_dependency "ruby_motion_query", ">= 1.7.0" 27 | spec.add_runtime_dependency "ProMotion", ">= 2.7.1" 28 | spec.add_runtime_dependency "motion_print" 29 | spec.add_runtime_dependency "motion-cocoapods" 30 | spec.add_runtime_dependency "RedAlert" 31 | spec.add_runtime_dependency "rake" 32 | spec.add_development_dependency "webstub" 33 | end 34 | -------------------------------------------------------------------------------- /templates/table_screen_cell/app/views/name.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %> < PM::TableViewCell 2 | 3 | def on_load 4 | apply_style :<%= @name %> 5 | 6 | content = find(self.contentView) 7 | @title = content.append! UILabel, :<%= @name %>_title 8 | 9 | # Add subviews here, like so: 10 | # content.append UILabel, :label_style_here 11 | # -or- 12 | # @submit_button = content.append(UIButton, :submit).get 13 | # -or- 14 | # @submit_button = content.append! UIButton, :submit 15 | end 16 | 17 | def title=(value) 18 | @title.text = value 19 | end 20 | def title 21 | @title 22 | end 23 | 24 | end 25 | 26 | __END__ 27 | 28 | You can use this like so in your table_screen: 29 | 30 | def table_data 31 | [ 32 | { 33 | title: "Section", 34 | cells: [ 35 | { cell_class: BarCell, height: stylesheet.bar_cell_height, title: "Foo"}, 36 | { cell_class: BarCell, height: stylesheet.bar_cell_height, title: "Bar"} 37 | ] 38 | } 39 | ] 40 | end 41 | 42 | 43 | To style this view include its stylesheet at the top of each controller's 44 | stylesheet that is going to use it: 45 | 46 | class SomeStylesheet < ApplicationStylesheet 47 | include <%= @name_camel_case %>Stylesheet 48 | 49 | Another option is to use your controller's stylesheet to style this view. This 50 | works well if only one controller uses it. If you do that, delete the 51 | view's stylesheet with: 52 | 53 | rm app/stylesheets/<%= @name %>_stylesheet.rb 54 | -------------------------------------------------------------------------------- /app/screens/collection_screen.rb: -------------------------------------------------------------------------------- 1 | class CollectionScreen < UICollectionViewController 2 | 3 | include ProMotion::ScreenModule 4 | 5 | stylesheet CollectionScreenStylesheet 6 | title 'Collection View' 7 | COLLECTION_CELL_ID = "CollectionCell" 8 | 9 | def self.new(args = {}) 10 | layout = UICollectionViewFlowLayout.alloc.init 11 | s = self.alloc.initWithCollectionViewLayout(layout) 12 | s.screen_init(args) if s.respond_to?(:screen_init) 13 | s 14 | end 15 | 16 | def on_load 17 | collectionView.tap do |cv| 18 | cv.registerClass(CollectionCell, forCellWithReuseIdentifier: COLLECTION_CELL_ID) 19 | cv.delegate = self 20 | cv.dataSource = self 21 | cv.allowsSelection = true 22 | cv.allowsMultipleSelection = false 23 | find(cv).apply_style :collection_view 24 | end 25 | end 26 | 27 | # Remove if you are only supporting portrait 28 | def will_animate_rotate(orientation, duration) 29 | reapply_styles 30 | end 31 | 32 | def numberOfSectionsInCollectionView(view) 33 | 1 34 | end 35 | 36 | def collectionView(view, numberOfItemsInSection: section) 37 | 200 38 | end 39 | 40 | def collectionView(view, cellForItemAtIndexPath: index_path) 41 | view.dequeueReusableCellWithReuseIdentifier(COLLECTION_CELL_ID, forIndexPath: index_path).tap do |cell| 42 | self.rmq.build(cell) unless cell.reused 43 | 44 | # Update cell's data here 45 | end 46 | end 47 | 48 | def collectionView(view, didSelectItemAtIndexPath: index_path) 49 | cell = view.cellForItemAtIndexPath(index_path) 50 | puts "Selected at section: #{index_path.section}, row: #{index_path.row}" 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /app/screens/metal_table_screen.rb: -------------------------------------------------------------------------------- 1 | class MetalTableScreen < UITableViewController 2 | 3 | include ProMotion::ScreenModule 4 | 5 | title "Down to the metal table" 6 | stylesheet MetalTableScreenStylesheet 7 | CELL_ID = "MetalTableCell" 8 | 9 | # Needed to be a ProMotion screen. Many controllers have different inits, so you need 10 | # to provide the correct one here 11 | def self.new(args = {}) 12 | s = self.alloc.initWithStyle(UITableViewStylePlain) 13 | s.screen_init(args) if s.respond_to?(:screen_init) 14 | s 15 | end 16 | 17 | def on_load 18 | load_data 19 | view.tap do |table| 20 | table.delegate = self 21 | table.dataSource = self 22 | end 23 | end 24 | 25 | def load_data 26 | @data = 0.upto(rand(1000)).map do |i| # Test data 27 | { 28 | name: %w(Lorem ipsum dolor sit amet consectetur adipisicing elit sed).sample, 29 | num: rand(100), 30 | } 31 | end 32 | end 33 | 34 | # Remove if you are only supporting portrait 35 | def will_animate_rotate(orientation, duration) 36 | reapply_styles 37 | end 38 | 39 | # Standard table stuff 40 | def tableView(table_view, numberOfRowsInSection: section) 41 | @data.length 42 | end 43 | 44 | def tableView(table_view, heightForRowAtIndexPath: index_path) 45 | stylesheet.cell_height 46 | end 47 | 48 | def tableView(table_view, cellForRowAtIndexPath: index_path) 49 | data_row = @data[index_path.row] 50 | 51 | cell = table_view.dequeueReusableCellWithIdentifier(CELL_ID) || begin 52 | create!(MetalTableCell, :cell, reuse_identifier: CELL_ID) 53 | end 54 | 55 | cell.update(data_row) 56 | cell 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /app/stylesheets/home_screen_stylesheet.rb: -------------------------------------------------------------------------------- 1 | class HomeScreenStylesheet < ApplicationStylesheet 2 | 3 | def root_view(st) 4 | st.background_color = color.white 5 | end 6 | 7 | def hello_world(st) 8 | st.frame = {w: 200, h: 30, centered: :both} 9 | st.color = color.black 10 | st.font = font.large 11 | st.text = "Hello world" 12 | end 13 | 14 | def section(st) 15 | st.frame = {t: 100, w: 200, h: 100, centered: :horizontal} 16 | st.background_color = color.light_gray 17 | end 18 | 19 | def section_button(st) 20 | st.frame = {l: 5, t: 5, w: 80, h: 20} 21 | st.text = "Button" 22 | st.background_color = color.blue 23 | end 24 | 25 | def open_table_button(st) 26 | st.frame = {centered: :horizontal, fb: 140, w: 200, h: 20} 27 | st.color = color.tint 28 | st.text = "Open table screen" 29 | end 30 | 31 | def open_metal_table_button(st) 32 | open_table_button st 33 | st.frame = {bp: 10} 34 | st.text = "Open metal table screen" 35 | end 36 | 37 | def open_data_table_button(st) 38 | open_table_button st 39 | st.frame = {bp: 10} 40 | st.text = "Open data table screen" 41 | end 42 | 43 | def open_collection_screen_button(st) 44 | open_table_button st 45 | st.frame = {bp: 10} 46 | st.text = "Open collection screen" 47 | end 48 | 49 | def open_example_controller_button(st) 50 | open_table_button st 51 | st.frame = {bp: 10} 52 | st.text = "Open example controller" 53 | end 54 | 55 | def grumpy_image(st) 56 | st.frame = { 57 | w: 200, 58 | h: 200, 59 | centered: :both 60 | } 61 | st.corner_radius = 100 62 | st.image = image.resource('grumpy_cat') 63 | st.remote_image = 'http://placehold.it/400x400' 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /docs/cookbook/debugging.md: -------------------------------------------------------------------------------- 1 | ## Fancy printing 2 | 3 | RedPotion uses [motion_print](https://github.com/OTGApps/motion_print) for fancy console output. 4 | 5 | Instead of `puts`, use `mp`: 6 | 7 | ```ruby 8 | > mp({b: "bee", a: 'a', see: 4}) 9 | 10 | { 11 | a => a, 12 | b => bee, 13 | see => 4 14 | } 15 | ``` 16 | ------ 17 | 18 | ## RMQ Debug 19 | 20 | Adding rmq_debug=true to rake turns on some debugging features that are too slow or verbose to include in a normal build. It's great for normal use in the simulator, but you'll want to leave it off if you're measuring performance. 21 | 22 | ``` 23 | rake rmq_debug=true 24 | ``` 25 | 26 | Use this to add your optional debugging code 27 | ```ruby 28 | RubyMotionQuery::RMQ.debugging? 29 | => true 30 | ``` 31 | 32 | You can even change the value from the REPL (useful for turning on and off features) 33 | ```ruby 34 | RubyMotionQuery::RMQ.debugging = true 35 | => true 36 | ``` 37 | 38 | `rmq.debug.colorize` - Often times, you may want high contrast on a selection of items, so you can lay them out or identify them on the screen. One common practice we use is to assign all the selected with a random background color. Since this is so common, we've created a shortcut method to help. 39 | ```ruby 40 | find().debug.colorize 41 | ``` 42 | 43 | Other Debugging Items 44 | ```ruby 45 | rmq.log :tree 46 | find.all.log 47 | find.all.log :wide 48 | 49 | find(Section).log :tree 50 | # 163792144 is the ID a button 51 | find(163792144).style{|st| st.background_color = rmq.color.blue} 52 | 53 | find(Section).children.and_self.log :wide 54 | 55 | find(UILabel).animations.blink 56 | 57 | # Show subview index and thus zorder of Section within Section's parent 58 | find(Section).parent.children.log 59 | ``` 60 | -------------------------------------------------------------------------------- /templates/collection_view_screen/app/screens/name_screen.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %>Screen < UICollectionViewController 2 | 3 | include ProMotion::ScreenModule 4 | 5 | title "Your title here" 6 | stylesheet <%= @name_camel_case %>ScreenStylesheet 7 | 8 | <%= @name.upcase %>_CELL_ID = "<%= @name_camel_case %>Cell" 9 | 10 | def self.new(args = {}) 11 | # Set layout 12 | layout = UICollectionViewFlowLayout.alloc.init 13 | s = self.alloc.initWithCollectionViewLayout(layout) 14 | s.screen_init(args) if s.respond_to?(:screen_init) 15 | s 16 | end 17 | 18 | def on_load 19 | collectionView.tap do |cv| 20 | cv.registerClass(<%= @name_camel_case %>Cell, forCellWithReuseIdentifier: <%= @name.upcase %>_CELL_ID) 21 | cv.delegate = self 22 | cv.dataSource = self 23 | cv.allowsSelection = true 24 | cv.allowsMultipleSelection = false 25 | find(cv).apply_style :collection_view 26 | end 27 | end 28 | 29 | def numberOfSectionsInCollectionView(view) 30 | 1 31 | end 32 | 33 | def collectionView(view, numberOfItemsInSection: section) 34 | 200 35 | end 36 | 37 | def collectionView(view, cellForItemAtIndexPath: index_path) 38 | view.dequeueReusableCellWithReuseIdentifier(<%= @name.upcase %>_CELL_ID, forIndexPath: index_path).tap do |cell| 39 | self.rmq.build(cell) unless cell.reused 40 | 41 | # Update cell's data here 42 | end 43 | end 44 | 45 | def collectionView(view, didSelectItemAtIndexPath: index_path) 46 | cell = view.cellForItemAtIndexPath(index_path) 47 | puts "Selected at section: #{index_path.section}, row: #{index_path.row}" 48 | end 49 | 50 | # Remove the following if you're only using portrait 51 | def will_animate_rotate(orientation, duration) 52 | reapply_styles 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /app/screens/home_screen.rb: -------------------------------------------------------------------------------- 1 | class HomeScreen < PM::Screen 2 | 3 | title "RedPotion" 4 | stylesheet HomeScreenStylesheet 5 | 6 | def on_load 7 | set_nav_bar_button :left, system_item: :camera, action: :nav_left_button 8 | set_nav_bar_button :right, title: "Right", action: :nav_right_button 9 | 10 | @hello_world_label = append!(UILabel, :hello_world) 11 | append HelloWorldSection # Section will handle its own styling 12 | 13 | append(UIButton, :open_table_button).on(:touch) do 14 | open TasksScreen.new(nav_bar: true) 15 | end 16 | 17 | append(UIButton, :open_metal_table_button).on(:touch) do 18 | open MetalTableScreen.new(nav_bar: true) 19 | end 20 | 21 | append(UIButton, :open_data_table_button).on(:touch) do 22 | open ContributorScreen.new(nav_bar: true) 23 | end 24 | 25 | append(UIButton, :open_collection_screen_button).on(:touch) do 26 | open CollectionScreen.new(nav_bar: true) 27 | end 28 | 29 | append(UIButton, :open_example_controller_button).on(:touch) do 30 | open ExampleController 31 | end 32 | end 33 | 34 | def nav_left_button 35 | mp 'Left button' 36 | append(UIImageView, :grumpy_image) 37 | end 38 | 39 | def nav_right_button 40 | mp 'Right button' 41 | end 42 | 43 | # You don't have to reapply styles to all UIViews, if you want to optimize, 44 | # another way to do it is tag the views you need to restyle in your stylesheet, 45 | # then only reapply the tagged views, like so: 46 | # def logo(st) 47 | # st.frame = {t: 10, w: 200, h: 96} 48 | # st.centered = :horizontal 49 | # st.image = image.resource('logo') 50 | # st.tag(:reapply_style) 51 | # end 52 | # 53 | # # Then in will_animate_rotate 54 | # find(:reapply_style).reapply_styles 55 | def will_animate_rotate(orientation, duration) 56 | reapply_styles 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /docs/cookbook/project.md: -------------------------------------------------------------------------------- 1 | ## What is the app name and indentifier? 2 | 3 | ```ruby 4 | app.name 5 | app.identifier 6 | ``` 7 | 8 | ## Can I find out info about for the project at runtime? 9 | 10 | This only works in development environment: 11 | ```ruby 12 | RubyMotionQuery::RMQ.build_time # Date/time when the app was last built 13 | RubyMotionQuery::RMQ.project_path 14 | ``` 15 | 16 | 17 | ## What are paths for the running app? 18 | 19 | ```ruby 20 | app.resource_path 21 | app.document_path 22 | ``` 23 | 24 | 25 | # Is the app running in Dev or Release (production)? 26 | 27 | ```ruby 28 | app.environment 29 | app.release? # .production? also 30 | app.test? 31 | app.development? 32 | ``` 33 | 34 | # Is the app in the simulator? 35 | 36 | ```ruby 37 | device.simulator? 38 | ``` 39 | 40 | ## What kind of device is the app running on? 41 | 42 | ```ruby 43 | device.ipad? 44 | device.iphone? 45 | ``` 46 | 47 | ## What size phone or other device is the app running on? 48 | 49 | ```ruby 50 | device.width # this does not change with orientation, use screen_width for that 51 | device.height # this does not change with orientation, use screen_height for that 52 | device.screen_width 53 | device.screen_height 54 | 55 | device.retina? 56 | device.three_point_five_inch? 57 | device.four_inch? 58 | device.four_point_seven_inch? 59 | device.five_point_five_inch? 60 | ``` 61 | 62 | ## Which OS is it running? 63 | 64 | ```ruby 65 | # Detect the iOS version 66 | device.ios_version # "8.0" etc 67 | device.is_version? "8.0" # be specific on minor versions 68 | device.is_version? "8" # or just use the major version number 69 | device.ios_at_least? 8 # iOS must be 8.0 or higher for true 70 | device.ios_at_least? 8.1 # returns true if version is 8.1 or higher 71 | ``` 72 | 73 | ## What orientation is it currently in? 74 | 75 | ```ruby 76 | device.landscape? 77 | device.portrait? 78 | 79 | # return values are :unknown, :portrait, :portrait_upside_down, :landscape_left, 80 | # :landscape_right, :face_up, :face_down 81 | device.orientation 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/cookbook/troubleshooting.md: -------------------------------------------------------------------------------- 1 | ## Troubleshooting 2 | 3 | ### The Nuclear Option 4 | 5 | Running `rake` was working perfectly last night, but this morning, you are getting mysterious errors, such as 6 | 7 | ````Simulator session started with error: Error Domain=NSPOSIXErrorDomain Code=3 "Failed to lookup the process ID of com.your_domain_here.my_awesome_app after successful launch. Perhaps it crashed after launch."```` 8 | 9 | Your environment might be borked. Try this: 10 | 11 | `rake newclear` 12 | 13 | The nuke task performs the following operations: 14 | 15 | ```` 16 | Cleaning Project... 17 | Delete ./build 18 | Delete ./resources/my_awesome_app.momd 19 | Delete /Users//.rvm/gems/ruby-2.1.1/gems/cdq-1.0.2/lib/../vendor/cdq/ext/build-iPhoneSimulator 20 | Clean ./Pods.xcodeproj for platform `iPhoneSimulator' 21 | Clean ./Pods.xcodeproj for platform `iPhoneOS' 22 | Delete vendor/Pods/build-iPhoneSimulator 23 | Delete /Users//Library/RubyMotion/build 24 | Delete vendor/Pods 25 | 26 | Resetting simulator... 27 | 28 | Bundling... 29 | 30 | Setting up cocoapods... 31 | 32 | Installing cocoapod dependencies... 33 | 34 | rake 35 | ```` 36 | 37 | Most of the items (other than rvm gems and `/Users//Library/RubyMotion/build`) that are deleted and cleaned exist within your current project directory. Nuking your project is a benign operation. since running `rake` rebuilds everything that was nuked, so give it a try. 38 | 39 | ### Corrupt/missing Cocoapods Specs repository 40 | 41 | You run `rake pm:install` on a freshly created redpotion app and it hangs on `Updating spec repo master`. Presumably, you've already run `pod setup` one time on your machine, so what gives? 42 | 43 | If you see an error message about pod not being able to find the master spec repo when you run `rake pm:install --verbose`, you can perform a clean pod setup: 44 | 45 | ```` 46 | > pod repo remove master 47 | > pod setup 48 | 49 | ```` 50 | Now you should be able to run rake pm:install. 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /lib/project/ext/ui_image_view.rb: -------------------------------------------------------------------------------- 1 | class UIImageView 2 | 3 | def remote_image=(args) 4 | url = args.respond_to?(:fetch) ? args.fetch(:url) : args 5 | callback = args.respond_to?(:fetch) ? args.fetch(:on_load, -> {}) : -> {} 6 | load_remote_image(url, callback) 7 | end 8 | 9 | private 10 | 11 | def load_remote_image(url, callback = nil) 12 | if defined?(SDWebImageManager) 13 | @remote_image_operations ||= {} 14 | # Cancel the previous remote operation if it exists 15 | operation = @remote_image_operations[("%p" % self)] 16 | if operation && operation.respond_to?(:cancel) 17 | operation.cancel 18 | @remote_image_operations[("%p" % self)] = nil 19 | end 20 | url = NSURL.URLWithString(url) unless url.is_a?(NSURL) 21 | @remote_image_operations[("%p" % self)] = load_remote_image_using_sdwebimage(url, callback) 22 | else 23 | puts "\n[RedPotion ERROR] tried to set remote_image without SDWebImage CocoaPod. Please add this to your Rakefile: \n\napp.pods do\n pod \"SDWebImage\"\nend\n" 24 | end 25 | end 26 | 27 | def load_remote_image_using_sdwebimage(url, callback = nil) 28 | manager = SDWebImageManager.sharedManager 29 | if manager.respond_to?('downloadWithURL:options:progress:completed') 30 | # Support for SDWebImage v3.x 31 | manager.downloadWithURL(url, 32 | options: SDWebImageRefreshCached, 33 | progress: nil, 34 | completed: -> image, error, cacheType, finished { 35 | Dispatch::Queue.main.async do 36 | self.image = image 37 | callback.call if callback 38 | end unless image.nil? 39 | } 40 | ) 41 | else 42 | # Support for SDWebImage v4.x 43 | manager.loadImageWithURL(url, 44 | options: SDWebImageRefreshCached, 45 | progress: nil, 46 | completed: -> image, imageData, error, cacheType, finished, imageURL { 47 | Dispatch::Queue.main.async do 48 | self.image = image 49 | callback.call if callback 50 | end unless image.nil? 51 | } 52 | ) 53 | end 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

logo

2 | 3 |
4 | 5 | [![Gem Version](https://img.shields.io/gem/v/redpotion.svg?style=flat)](https://rubygems.org/gems/redpotion) 6 | [![Build Status](https://img.shields.io/travis/infinitered/redpotion.svg?style=flat)](https://travis-ci.org/infinitered/redpotion) 7 | 8 | # RedPotion 9 | 10 | We believe iPhone development should be clean, scalable, and fast with a language that developers not only enjoy, but actively choose. With the advent of Ruby for iPhone development the RubyMotion community has combined and tested the most active and powerful gems into a single package called **RedPotion** 11 | 12 | RedPotion combines [RMQ](http://rubymotionquery.com/), [ProMotion](https://github.com/infinitered/ProMotion), [CDQ](https://github.com/infinitered/cdq), [AFMotion](https://github.com/clayallsopp/afmotion), [MotionPrint](https://github.com/OTGApps/motion_print) and [MORE!](#full-listing-of-gems-and-pods-for-redpotion). It also adds new features to better integrate RMQ with ProMotion. The goal is simply to choose standard libraries and promote best practices, allowing you to develop iOS apps in record time. 13 | 14 | 15 | The **makers of RMQ and ProMotion** at [InfiniteRed](http://infinite.red) (web and mobile developers based in Portland, OR and San Francisco, CA) have teamed up with [David Larrabee](https://twitter.com/Squidpunch) to create the ultimate RubyMotion library. 16 | 17 | [![image](http://infinite.red/images/ir-logo.svg)](http://infinite.red) 18 | 19 | ProMotion for screens and RMQ for styles, animations, traversing, events, etc. 20 | 21 |
22 | 23 | ---------- 24 | 25 |
26 | 27 | [![image](http://ir_wp.s3.amazonaws.com/wp-content/uploads/sites/19/2015/04/rp_docs.png)](http://docs.redpotion.org) 28 | 29 | **Read the [RedPotion Documentation](http://docs.redpotion.org).** 30 | 31 | **Read the [RedPotion Quick Start Documentation](http://docs.redpotion.org/en/latest/quick_start/).** 32 | 33 | ## Premium Support 34 | 35 | [RedPotion](https://github.com/infinitered/redpotion), as an open source project, is free to use and always will be. [Infinite Red](https://infinite.red/) is now a [React Native](https://reactnative.dev) expert consultancy, helping companies build, optimize, launch, and support their React Native apps. Email us at [hello@infinite.red](mailto:hello@infinite.red) to get in touch with us for more details. 36 | -------------------------------------------------------------------------------- /lib/project/ext/ui_view.rb: -------------------------------------------------------------------------------- 1 | class UIView 2 | 3 | # You can use either rmq_build or on_load, not both. If you have both, on_load will be ignored, 4 | # you can however call it from rmq_build. They are the same, on_load follows the ProMotion style 5 | # and is recommended. 6 | def rmq_build 7 | on_load 8 | end 9 | 10 | def on_load 11 | end 12 | 13 | def on_styled 14 | end 15 | 16 | # You can user either rmq_style_applied or on_styled, not both. If you have both on_styled will be ignored, 17 | # you can however call it from rmq_style_applied. They are the same, on_styled follows the Promotion style 18 | # and is recommended. 19 | def rmq_style_applied 20 | on_styled 21 | end 22 | 23 | def append(view_or_constant, style=nil, opts = {}, &block) 24 | rmq(self).append(view_or_constant, style, opts, &block) 25 | end 26 | def append!(view_or_constant, style=nil, opts = {}, &block) 27 | rmq(self).append!(view_or_constant, style, opts, &block) 28 | end 29 | 30 | def prepend(view_or_constant, style=nil, opts = {}, &block) 31 | rmq(self).prepend(view_or_constant, style, opts, &block) 32 | end 33 | def prepend!(view_or_constant, style=nil, opts = {}, &block) 34 | rmq(self).prepend!(view_or_constant, style, opts, &block) 35 | end 36 | 37 | def create(view_or_constant, style=nil, opts = {}, &block) 38 | rmq(self).create(view_or_constant, style, opts, &block) 39 | end 40 | def create!(view_or_constant, style=nil, opts = {}, &block) 41 | rmq(self).create!(view_or_constant, style, opts, &block) 42 | end 43 | 44 | def build(view, style = nil, opts = {}, &block) 45 | rmq(self).build(view, style, opts, &block) 46 | end 47 | def build!(view, style = nil, opts = {}, &block) 48 | rmq(self).build!(view, style, opts, &block) 49 | end 50 | 51 | def on(event, args = {}, &block) 52 | rmq(self).on(event, args, &block) 53 | end 54 | 55 | def off(*events) 56 | rmq(self).off(*events) 57 | end 58 | 59 | def apply_style(style_name) 60 | rmq(self).apply_style(style_name) 61 | end 62 | 63 | def reapply_styles 64 | rmq(self).reapply_styles 65 | end 66 | 67 | def style(&block) 68 | rmq(self).style(&block) 69 | end 70 | 71 | def color 72 | rmq.color 73 | end 74 | 75 | def font 76 | rmq.font 77 | end 78 | 79 | def image 80 | rmq.image 81 | end 82 | 83 | def stylesheet 84 | rmq.stylesheet 85 | end 86 | 87 | def stylesheet=(value) 88 | rmq.stylesheet = value 89 | end 90 | 91 | end 92 | -------------------------------------------------------------------------------- /templates/metal_table_screen/app/screens/name_screen.rb: -------------------------------------------------------------------------------- 1 | class <%= @name_camel_case %>Screen < UITableViewController 2 | 3 | include ProMotion::ScreenModule 4 | 5 | title "Your title here" 6 | stylesheet <%= @name_camel_case %>ScreenStylesheet 7 | 8 | <%= @name.upcase %>_CELL_ID = "<%= @name_camel_case %>Cell" 9 | 10 | # Needed to be a ProMotion screen. Many controllers have different inits, so you need 11 | # to provide the correct one here 12 | def self.new(args = {}) 13 | s = self.alloc.initWithStyle(UITableViewStylePlain) 14 | s.screen_init(args) if s.respond_to?(:screen_init) 15 | s 16 | end 17 | 18 | def on_load 19 | load_data 20 | 21 | table = view 22 | table.delegate = self 23 | table.dataSource = self 24 | end 25 | 26 | def load_data 27 | @data = 0.upto(rand(100)).map do |i| # Test data 28 | { 29 | name: %w(Lorem ipsum dolor sit amet consectetur adipisicing elit sed).sample, 30 | num: rand(100), 31 | } 32 | end 33 | end 34 | 35 | # Standard table stuff 36 | def tableView(table_view, numberOfRowsInSection: section) 37 | @data.length 38 | end 39 | 40 | def tableView(table_view, heightForRowAtIndexPath: index_path) 41 | stylesheet.cell_height 42 | end 43 | 44 | def tableView(table_view, cellForRowAtIndexPath: index_path) 45 | data_row = @data[index_path.row] 46 | 47 | cell = table_view.dequeueReusableCellWithIdentifier(<%= @name.upcase %>_CELL_ID) || begin 48 | rmq.create(<%= @name_camel_case %>Cell, :cell, reuse_identifier: <%= @name.upcase %>_CELL_ID).get 49 | 50 | # If you want to change the style of the cell, you can do something like this: 51 | #rmq.create(<%= @name_camel_case %>Cell, :cell, reuse_identifier: <%= @name.upcase %>_CELL_ID, cell_style: UITableViewCellStyleSubtitle).get 52 | end 53 | 54 | cell.update(data_row) 55 | cell 56 | end 57 | 58 | # You don't have to reapply styles to all UIViews, if you want to optimize, another way to do it 59 | # is tag the views you need to restyle in your stylesheet, then only reapply the tagged views, like so: 60 | # def logo(st) 61 | # st.frame = {t: 10, w: 200, h: 96} 62 | # st.centered = :horizontal 63 | # st.image = image.resource('logo') 64 | # st.tag(:reapply_style) 65 | # end 66 | # 67 | # Then in will_animate_rotate 68 | # find(:reapply_style).reapply_styles# 69 | 70 | # Remove the following if you're only using portrait 71 | def will_animate_rotate(orientation, duration) 72 | reapply_styles 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /docs/cookbook/events_and_gestures.md: -------------------------------------------------------------------------------- 1 | ## On / Off 2 | 3 | To add an event, use .on, to remove it it, use .off 4 | 5 | ```ruby 6 | # Simple example 7 | append(UIView).on(:tap){|sender| find(sender).hide} 8 | 9 | # Adding an Event during creation 10 | view_q = append(UIView).on(:tap) do |sender, event| 11 | # do something here 12 | end 13 | 14 | # removing an Event 15 | view_q.off(:tap) 16 | 17 | # or you remove them all 18 | view_q.off 19 | 20 | # like everything in RMQ, this works on all items selected 21 | find(UIView).off(:tap) 22 | ``` 23 | 24 | ## RubyMotionQuery::Event 25 | 26 | In RMQ events and gestures are normalized with the same API. For example removing events or gestures is foo.off, and the appropriate thing happens. 27 | 28 | If you see Event, just remember that's either an event or gesture. I decided to call them Events 29 | 30 | ## Type of events and gestures 31 | 32 | ```ruby 33 | # Events on controls 34 | :touch 35 | :touch_up 36 | :touch_down 37 | :touch_start 38 | :touch_stop 39 | :change 40 | 41 | :touch_down_repeat 42 | :touch_drag_inside 43 | :touch_drag_outside 44 | :touch_drag_enter 45 | :touch_drag_exit 46 | :touch_up_inside 47 | :touch_up_outside 48 | :touch_cancel 49 | 50 | :value_changed 51 | 52 | :editing_did_begin 53 | :editing_changed 54 | :editing_did_change 55 | :editing_did_end 56 | :editing_did_endonexit 57 | 58 | :all_touch 59 | :all_editing 60 | 61 | :application 62 | :system 63 | :all 64 | 65 | # Gestures 66 | :tap 67 | :pinch 68 | :rotate 69 | :swipe 70 | :swipe_up 71 | :swipe_down 72 | :swipe_left 73 | :swipe_right 74 | :pan 75 | :long_press 76 | ``` 77 | 78 | ## Interesting methods of an RubyMotionQuery::Event: 79 | ```ruby 80 | foo.sender 81 | foo.event 82 | 83 | foo.gesture? 84 | foo.recognizer 85 | foo.gesture 86 | 87 | foo.location 88 | foo.location_in 89 | 90 | foo.sdk_event_or_recognizer 91 | ``` 92 | 93 | TODO, need many examples here 94 | 95 |   96 | 97 | ## Events and user interaction 98 | 99 | `.userInteractionEnabled` will be set to true when you add `.on` events (As of edge 0.7.1). If you are using an older version of RMQ, you can use `.enable_interaction` in a chain like so. 100 | 101 | ```ruby 102 | 103 | # this code allows you to place a tap event on an image 104 | append(UIImageView, :my_picture).enable_interaction.on(:tap) do |sender| 105 | puts "Imageview tapped" 106 | end 107 | 108 | ``` 109 | 110 | ## Custom events 111 | 112 | To add a custom event, use `.on` with a custom symbol. Call `.trigger` to trigger your block. 113 | 114 | ```ruby 115 | 116 | # this code allows you to add a custom event and trigger it 117 | append(UIView, :my_view).on(:custom_event) do |sender| 118 | puts "custom_event has been triggered" 119 | end 120 | find(:my_view).trigger(:custom_event) 121 | ``` 122 | 123 | ## RubyMotionQuery::Events 124 | 125 | The internal store of events in a UIView. It's rmq.events, you won't use it too often 126 | -------------------------------------------------------------------------------- /docs/cookbook/alerts_and_action_sheets.md: -------------------------------------------------------------------------------- 1 | Provided by the RedAlert Gem, which is included in RedPotion 2 | Screen Shot 3 | 4 | **Did you know that UIAlertView and UIActionSheet (as well as their respective delegate protocols) are deprecated in iOS 8?** 5 | 6 | Apple requests you start using the new `UIAlertController`. RedPotion gives you a clean way to use `UIAlertController`s that will automatically fail over to `UIAlertView` and `UIActionSheet` for iOS 7. 7 | 8 | ## Usage 9 | 10 | ```ruby 11 | 12 | # Simply do an alert 13 | app.alert("Minimal Alert") 14 | 15 | # Alert with callback 16 | app.alert("Alert with Block") { 17 | puts "Alert with Block worked!" 18 | } 19 | 20 | # Modify some snazzy options 21 | app.alert(title: "New Title", message: "Great message", animated: false) 22 | 23 | # Switch it to look like an ActionSheet by setting the style 24 | app.alert(title: "Hey there!", message: "My style is :sheet", style: :sheet) do |action_type| 25 | puts "You clicked #{action_type}" 26 | end 27 | 28 | # Utilize common templates 29 | app.alert(message: "Would you like a sandwich?", actions: :yes_no_cancel, style: :sheet) do |action_type| 30 | case action_type 31 | when :yes 32 | puts "Here's your Sandwich!" 33 | when :no 34 | puts "FINE!" 35 | end 36 | end 37 | ``` 38 | 39 | You can pass in symbols or strings and we'll build the buttons for you: 40 | 41 | ```ruby 42 | rmq.app.alert title: "Hey!", actions: [ "Go ahead", :cancel, :delete ] do |button_tag| 43 | case button_tag 44 | when :cancel then puts "Canceled!" 45 | when :delete then puts "Deleted!" 46 | when "Go ahead" then puts "Going ahead!" 47 | end 48 | end 49 | ``` 50 | 51 | You can even use the `make_button` helper to create custom UIAction buttons to add: 52 | ```ruby 53 | # Use custom UIAction buttons and add them 54 | taco = app.make_button("Taco") { 55 | puts "Taco pressed" 56 | } 57 | nacho = app.make_button(title: "Nacho", style: :destructive) { 58 | puts "Nacho pressed" 59 | } 60 | button_list = [taco, nacho] 61 | app.alert(title: "Actions!", message: "Actions created with `make_button` helper.", actions: button_list) 62 | ``` 63 | 64 | ## Available Templates 65 | 66 | Templates are provided [HERE](https://github.com/GantMan/RedAlert/blob/master/lib/project/button_templates.rb) 67 | * `:yes_no` = Simple yes and no buttons. 68 | * `:yes_no_cancel` = Yes/no buttons with a separated cancel button. 69 | * `:ok_cancel` = OK button with a separated cancel button. 70 | * `:delete_cancel` = Delete button (red) with a separated cancel button. 71 | 72 | _More to come:_ be sure to submit a pull-request with your button template needs. 73 | 74 | ## More info 75 | 76 | Feel free to read up on UIAlertController to see what all is wrapped up in this gem. 77 | * [Hayageek](http://hayageek.com/uialertcontroller-example-ios/) 78 | * [NSHipster](http://nshipster.com/uialertcontroller/) 79 | 80 | 81 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: RedPotion 2 | #theme: readthedocs 3 | pages: 4 | - [index.md, Docs, Home] 5 | - [quick_start.md, Docs, Quick start] 6 | - [new_features_for_rmq_and_promotion.md, Docs, "New features in RMQ and ProMotion"] 7 | - [redpotion_specific_features.md, Docs, "RedPotion specific features"] 8 | - [contributing.md, Docs, "Contributing"] 9 | - [cookbook/command_line.md, Cookbook, "Command-line and REPL tools"] 10 | - [cookbook/project.md, Cookbook, "Project info, are you in the simulator, dev, release, on what OS, what device, etc"] 11 | - [cookbook/misc.md, Cookbook, "Misc"] 12 | - [cookbook/device.md, Cookbook, "The device"] 13 | - [cookbook/app_delegate.md, Cookbook, "App delegate"] 14 | - [cookbook/app.md, Cookbook, "App object: access the app from anywhere"] 15 | - [cookbook/screens.md, Cookbook, "Screens: UIViewControllers"] 16 | - [cookbook/table_screens.md, Cookbook, "Table Screens"] 17 | - [cookbook/stylesheets.md, Cookbook, "Stylesheets: style and layout your screens and views"] 18 | - [cookbook/layout_a_screen.md, Cookbook, "Layout: Laying out a screen"] 19 | - [cookbook/stylers.md, Cookbook, "Styles and stylers"] 20 | - [cookbook/traversing.md, Cookbook, "Traversing: Finding your way around screens and views"] 21 | - [cookbook/selecting.md, Cookbook, "Selecting: finding views"] 22 | - [cookbook/events_and_gestures.md, Cookbook, "Events and Gestures"] 23 | - [cookbook/images.md, Cookbook, "Images, icons, and photos"] 24 | - [cookbook/fonts.md, Cookbook, "Fonts"] 25 | - [cookbook/colors.md, Cookbook, "Colors"] 26 | - [cookbook/animations.md, Cookbook, "Animating and animations"] 27 | - [cookbook/actions.md, Cookbook, "Actions: such as hiding, focusing, etc"] 28 | - [cookbook/attributes.md, Cookbook, "Attributes and data"] 29 | - [cookbook/tagging.md, Cookbook, "Tagging views"] 30 | - [cookbook/format.md, Cookbook, "Numbers and dates: formatting"] 31 | - [cookbook/alerts_and_action_sheets.md, Cookbook, "Alerts and ActionSheets"] 32 | - [cookbook/core_data.md, Cookbook, "Core Data and CDQ: Using a local database"] 33 | - [cookbook/timers.md, Cookbook, "Timers and delays. Do something every second"] 34 | - [cookbook/networking.md, Cookbook, "Networking, JSON, remote images"] 35 | - [cookbook/validations.md, Cookbook, "Validating data, text, text boxes, etc"] 36 | - [cookbook/debugging.md, Cookbook, "Debugging"] 37 | - [cookbook/testing.md, Cookbook, "Testing"] 38 | - [cookbook/accessibility.md, Cookbook, "Accessibility for users with particular needs"] 39 | - [cookbook/distribution.md, Cookbook, "Distribution"] 40 | - [cookbook/troubleshooting.md, Cookbook, "Troubleshooting"] 41 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |

logo

2 | 3 |
4 | 5 | [![Dependency Status](https://gemnasium.com/infinitered/redpotion.svg)](https://gemnasium.com/infinitered/redpotion) [![Build Status](https://travis-ci.org/infinitered/redpotion.svg)](https://travis-ci.org/infinitered/redpotion) [![Gem Version](https://badge.fury.io/rb/redpotion.png)](http://badge.fury.io/rb/redpotion) 6 | 7 | # RedPotion 8 | 9 | [![image](http://ir_public.s3.amazonaws.com/projects/redpotion/Octocat_100.png)**github repo**](https://github.com/infinitered/redpotion) 10 | 11 | We believe iPhone development should be clean, scalable, and fast with a language that developers not only enjoy, but actively choose. With the advent of Ruby for iPhone development the RubyMotion community has combined and tested the most active and powerful gems into a single package called **RedPotion**. 12 | 13 | RedPotion combines [RMQ](http://rubymotionquery.com/), [ProMotion](https://github.com/infinitered/ProMotion), [CDQ](https://github.com/infinitered/cdq), [AFMotion](https://github.com/clayallsopp/afmotion), [MotionPrint](https://github.com/OTGApps/motion_print) and [MORE!](#full-listing-of-gems-and-pods-for-redpotion). It also adds new features to better integrate RMQ with ProMotion. The goal is simply to choose standard libraries and promote best practices, allowing you to develop iOS apps in record time. 14 | 15 | --- 16 | 17 | ## Other documentation 18 | 19 | Until these docs are complete, please also use RMQ's and ProMotion's docs 20 | 21 | * [RMQ](http://rubymotionquery.com/documentation/) 22 | * [ProMotion](http://promotion.readthedocs.org/en/master/) 23 | 24 | --- 25 | 26 | 27 | The creators of **RMQ** and **ProMotion** at [Infinite Red](http://www.infinite.red) teamed up with [David Larrabee](https://twitter.com/Squidpunch) to create the ultimate RubyMotion library. 28 | 29 | [![image](https://ir_wp.s3.amazonaws.com/wp-content/uploads/sites/11/2013/08/InfiniteRed_logo_100h.png)](http://infinitered.com/) 30 | 31 | ProMotion for screens and RMQ for styles, animations, traversing, events, etc. 32 | 33 | ## Plugins and Add-ons 34 | 35 | You can use both RMQ Plugins and ProMotion Add-ons 36 | 37 | ![image](https://camo.githubusercontent.com/523372b371be1de2fb2cec421be423e2b89bcfd0/687474703a2f2f69725f77702e73332e616d617a6f6e6177732e636f6d2f77702d636f6e74656e742f75706c6f6164732f73697465732f31392f323031342f30392f726d715f706c7567696e2e706e67) 38 | 39 | ![image](http://ir_wp.s3.amazonaws.com/wp-content/uploads/sites/11/2014/11/ProMotion-addon-logo.png) 40 | 41 | ## Full listing of Gems and Pods for RedPotion 42 | 43 | **Gems** 44 | 45 | * [RMQ 1.7+](http://rubymotionquery.com/) 46 | * [ProMotion 2.5+](https://github.com/infinitered/ProMotion) 47 | * [CDQ](https://github.com/infinitered/cdq) 48 | * [AFMotion](https://github.com/clayallsopp/afmotion) 49 | * [newclear](https://github.com/IconoclastLabs/newclear) 50 | * [motion_print](https://github.com/OTGApps/motion_print) 51 | * [motion-cocoapods](https://github.com/HipByte/motion-cocoapods) 52 | * [RedAlert](https://github.com/GantMan/RedAlert) 53 | * (DEV) [webstub](https://github.com/nathankot/webstub) 54 | 55 | **Pods** 56 | 57 | * [SDWebImage](https://github.com/rs/SDWebImage) 58 | -------------------------------------------------------------------------------- /lib/project/pro_motion/data_table_searchable.rb: -------------------------------------------------------------------------------- 1 | module ProMotion 2 | module Table 3 | module Searchable 4 | 5 | def make_data_table_searchable(params={}) 6 | if params[:search_bar][:fields].nil? 7 | raise "ERROR: You must specify fields:[:example] for your searchable DataTableScreen. It should be an array of fields you want searched in CDQ." 8 | else 9 | @data_table_predicate_fields = params[:search_bar][:fields] 10 | end 11 | params[:delegate] = search_delegate 12 | params[:search_results_updater] = search_delegate 13 | 14 | make_searchable(params) 15 | end 16 | 17 | def search_fetch_controller 18 | @_search_fetch_controller ||= new_frc_with_search(search_string) 19 | end 20 | 21 | def new_frc_with_search(search_string) 22 | return if @data_table_predicate_fields.blank? 23 | 24 | # Create the predicate from the predetermined fetch scope. 25 | where = @data_table_predicate_fields.map{|f| "#{f} CONTAINS[cd] \"#{search_string}\"" }.join(" OR ") 26 | search_scope = fetch_scope.where(where) 27 | 28 | # Create the search FRC with the predicate and set delegate 29 | search = NSFetchedResultsController.alloc.initWithFetchRequest( 30 | search_scope.fetch_request, 31 | managedObjectContext: search_scope.context, 32 | sectionNameKeyPath: nil, 33 | cacheName: nil 34 | ) 35 | search.delegate = search_delegate 36 | 37 | # Perform the fetch 38 | error_ptr = Pointer.new(:object) 39 | unless search.performFetch(error_ptr) 40 | raise "Error performing fetch: #{error_ptr[2].description}" 41 | end 42 | 43 | search 44 | end 45 | 46 | def reset_search_frc 47 | # Update the filter, in this case just blow away the FRC and let 48 | # lazy evaluation create another with the relevant search info 49 | @_search_fetch_controller.delegate = nil unless @_search_fetch_controller.nil? 50 | @_search_fetch_controller = nil 51 | end 52 | 53 | def search_delegate 54 | @_search_delegate ||= begin 55 | d = DataTableSeachDelegate.new 56 | d.parent = WeakRef.new(self) 57 | d 58 | end 59 | end 60 | 61 | ######### iOS methods, headless camel case ####### 62 | 63 | def dt_searchDisplayControllerWillBeginSearch(controller) 64 | @_data_table_searching = true 65 | search_controller.delegate.will_begin_search if search_controller.delegate.respond_to? "will_begin_search" 66 | end 67 | 68 | def dt_searchDisplayControllerWillEndSearch(controller) 69 | @_data_table_searching = false 70 | @_search_fetch_controller.delegate = nil unless @_search_fetch_controller.nil? 71 | @_search_fetch_controller = nil 72 | @_data_table_search_string = nil 73 | search_controller.delegate.will_end_search if search_controller.delegate.respond_to? "will_end_search" 74 | update_table_data 75 | end 76 | 77 | def dt_searchDisplayController(controller, shouldReloadTableForSearchString:search_string) 78 | @_data_table_search_string = search_string 79 | reset_search_frc 80 | true 81 | end 82 | 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /docs/quick_start.md: -------------------------------------------------------------------------------- 1 | # Quick start 2 | 3 | ``` 4 | > gem install redpotion 5 | 6 | > potion new my_new_app 7 | > cd my_new_app 8 | > rake 9 | ``` 10 | 11 | ## Installation 12 | 13 | - `gem install redpotion` 14 | 15 | If you use rbenv 16 | 17 | - `rbenv rehash` 18 | 19 | add it to your `Gemfile`: 20 | 21 | - `gem 'redpotion'` 22 | 23 | 24 | ## Let's build something 25 | 26 | Let's start by creating our app, do this: 27 | 28 | ``` 29 | > potion new my_new_app 30 | > cd my_new_app 31 | > rake 32 | ``` 33 | 34 | Your app should be running now. Type `exit` in console to stop your app. 35 | 36 | Let's add a text field, a button, and an image to the main screen: 37 | 38 | Open the `home_screen.rb` file, then add this 39 | 40 | ```ruby 41 | @image_url = append!(UITextField, :image_url) 42 | 43 | append UIButton, :go_button 44 | 45 | @sample_image = append!(UIImageView, :sample_image) 46 | ``` 47 | 48 | Delete this line: 49 | 50 | ```ruby 51 | @hello_world = append!(UILabel, :hello_world) 52 | ``` 53 | 54 | Now we need to style them so you can see them on your screen. 55 | 56 | Open up `home_screen_stylesheet.rb`, then add this: 57 | 58 | ```ruby 59 | def image_url(st) 60 | st.frame = {left: 20, from_right: 20, top: 80, height: 30} 61 | st.background_color = color.light_gray 62 | end 63 | 64 | def go_button(st) 65 | st.frame = {below_prev: 10, from_right: 20, width: 40, height: 30} 66 | st.text = "go" 67 | st.background_color = color.blue 68 | st.color = color.white 69 | end 70 | 71 | def sample_image(st) 72 | st.frame = {left: 20, below_prev: 10, from_right: 20, from_bottom: 20} 73 | st.background_color = color.gray 74 | 75 | # an example of using the view directly 76 | st.view.contentMode = UIViewContentModeScaleAspectFit 77 | end 78 | ``` 79 | 80 | Now let's add the logic. When the user enters a URL to an image in the text field, then tap **Go**, it shows the picture in the image view below. 81 | 82 | Let's add the event to the go_button: 83 | 84 | Replace this: 85 | ```ruby 86 | append UIButton, :go_button 87 | ``` 88 | 89 | With this: 90 | ```ruby 91 | append(UIButton, :go_button).on(:touch) do |sender| 92 | @sample_image.remote_image = @image_url.text 93 | @image_url.resignFirstResponder # Closes keyboard 94 | end 95 | ``` 96 | 97 | You should end up with this `on_load` method: 98 | 99 | ```ruby 100 | def on_load 101 | set_nav_bar_button :left, system_item: :camera, action: :nav_left_button 102 | set_nav_bar_button :right, title: "Right", action: :nav_right_button 103 | 104 | @image_url = append!(UITextField, :image_url) 105 | 106 | append(UIButton, :go_button).on(:touch) do |sender| 107 | @sample_image.remote_image = @image_url.text 108 | @image_url.resignFirstResponder # Closes keyboard 109 | end 110 | 111 | @sample_image = append!(UIImageView, :sample_image) 112 | end 113 | ``` 114 | 115 | If you want to preview images which are under not secured (under HTTP), add the following line in the `Rakefile`: 116 | ```ruby 117 | app.info_plist['NSAppTransportSecurity'] = { 'NSAllowsArbitraryLoads' => true } 118 | ``` 119 | 120 | Now paste this URL in and hit **Go** 121 | `http://bit.ly/18iMhwc` 122 | 123 | You should have this: 124 | 125 | ![image](http://ir_wp.s3.amazonaws.com/wp-content/uploads/sites/11/2015/03/myapp_screenshot.jpg) 126 | -------------------------------------------------------------------------------- /docs/cookbook/traversing.md: -------------------------------------------------------------------------------- 1 | Moving around the subview tree. 2 | 3 | ## Screen, controller, and root_view 4 | 5 | ```ruby 6 | find.screen # alias to find.view_controller 7 | find.view_controller 8 | find.root_view # View of the view_controller 9 | ``` 10 | 11 | ## Window of the root_view 12 | 13 | ```ruby 14 | find.window 15 | ``` 16 | 17 | ## All subviews, subviews of subviews, etc for root_view: 18 | 19 | ```ruby 20 | find.all 21 | ``` 22 | 23 | ## Find 24 | 25 | Find all children/grandchildren/etc: 26 | 27 | ```ruby 28 | find(my_view).find # Different from children as it keeps on going down the tree 29 | ``` 30 | 31 | More commonly, you are searching for something: 32 | 33 | ```ruby 34 | find(my_view).find(UITextField) 35 | ``` 36 | 37 | ## Closest 38 | 39 | Closest is interesting, and very useful. It searches through parents/grandparents/etc and finds the **first** occurrence that matches the selectors: 40 | 41 | ```ruby 42 | find(my_view).closest(Section) 43 | ``` 44 | 45 | Let's say that someone clicked on a button in a table cell. You want to find and disable all buttons in that cell. So first you need to find the cell itself, and then find all buttons for that cell, then let's say we want to hide them. You'd do that like so: 46 | 47 | ```ruby 48 | find(sender).closest(UITableViewCell).find(UIButton).hide 49 | ``` 50 | 51 | ## Children of selected view, views, or root_view 52 | 53 | ```ruby 54 | find.children # All children (but not grandchildren) of root_view 55 | find(:section).children # All children of any view with the tag or stylename of :section 56 | ``` 57 | 58 | ### You can also add selectors 59 | 60 | ```ruby 61 | find(:section).children(UILabel) # All children (that are of type UILabel of any view with the tag or stylename of :section 62 | ``` 63 | 64 | ## Parent or parents of selected view(s) 65 | 66 | ```ruby 67 | find(my_view).parent # superview of my_view 68 | find(my_view).parents # superview of my_view, plus any grandparents, great-grandparents, etc 69 | find(UIButton).parent # all parents of all buttons 70 | ``` 71 | 72 | ## Siblings 73 | 74 | Find all your siblings: 75 | 76 | ```ruby 77 | find(my_view).siblings # All children of my_view's parent, minus my_view) 78 | ``` 79 | 80 | Get the sibling right next to the view, below the view: 81 | 82 | ```ruby 83 | find(my_view).next 84 | ``` 85 | 86 | Get the sibling right next to the view, above the view: 87 | 88 | ```ruby 89 | find(my_view).prev 90 | ``` 91 | 92 | ## And, not, back, and self 93 | 94 | These four could be thought of as Selectors, not Traversing. They kind of go in both, anywho. 95 | 96 | By default selectors are an OR, not an AND. This will return any UILabels and anything with text == '': 97 | 98 | ```ruby 99 | find(UILabel, text: "") # This is an OR 100 | ``` 101 | 102 | So if you want to do an **and**, do this: 103 | 104 | ```ruby 105 | find(UILabel).and(text: "") 106 | ``` 107 | 108 | Not works the same way: 109 | 110 | ```ruby 111 | find(UILabel).not(text: "") 112 | ``` 113 | 114 | Back is interesting, it moves you back up the chain one. In this example, we find all images that are inside test_view. Then we tag them as :foo. Now we want to find all labels in test_view and tag them :bar. So after the first tag, we go **back** up the chain to test_view, find labels, then tag them :bar: 115 | 116 | ```ruby 117 | find(test_view).find(UIImageView).tag(:foo).back.find(UILabel).tag(:bar) 118 | ``` 119 | 120 | ## Filter 121 | 122 | Filter is what everything else uses (parents, children, find, etc), you typically don't use it yourself. 123 | -------------------------------------------------------------------------------- /docs/cookbook/app_delegate.md: -------------------------------------------------------------------------------- 1 | ## Get to the app delegate from anywhere in the app 2 | 3 | ```ruby 4 | app.delegate.some_method_you_added 5 | ``` 6 | 7 | ## About the app delegate 8 | 9 | RedPotion uses the PM::Delegate from ProMotion, which has a nice API for your AppDelegate class. 10 | 11 | ```ruby 12 | # app/app_delegate.rb 13 | class AppDelegate < PM::Delegate 14 | status_bar false, animation: :none 15 | 16 | def on_load(app, options) 17 | open HomeScreen.new(nav_bar: true) 18 | end 19 | end 20 | ``` 21 | 22 | If you need to inherit from a different AppDelegate superclass, do this: 23 | 24 | ```ruby 25 | class AppDelegate < JHMyParentDelegate 26 | include PM::DelegateModule 27 | status_bar false, animation: :none 28 | 29 | def on_load(app, options) 30 | open HomeScreen.new(nav_bar: true) 31 | end 32 | end 33 | ``` 34 | 35 | ### Methods 36 | 37 | #### on_load(app, options) 38 | 39 | Main method called when starting your app. Open your first screen, tab bar, or split view here. 40 | 41 | ```ruby 42 | def on_load(app, options) 43 | open HomeScreen 44 | end 45 | ``` 46 | 47 | #### on_unload 48 | 49 | Fires when the app is about to terminate. Don't do anything crazy here, but it's a last chance 50 | to save state if necessary. 51 | 52 | ```ruby 53 | def on_unload 54 | # Unloading! 55 | end 56 | ``` 57 | 58 | #### will_load(app, options) 59 | 60 | Fired just before the app loads. Not usually necessary. 61 | 62 | #### will_deactivate 63 | 64 | Fires when the app is about to become inactive. 65 | 66 | #### on_activate 67 | 68 | Fires when the app becomes active. 69 | 70 | #### will_enter_foreground 71 | 72 | Fires just before the app enters the foreground. 73 | 74 | #### on_enter_background 75 | 76 | Fires when the app enters the background. 77 | 78 | #### open_tab_bar(*screens) 79 | 80 | Opens a UITabBarController with the specified screens as the root view controller of the current app. 81 | iOS doesn't allow opening a UITabBar as a sub-view. 82 | 83 | ```ruby 84 | def on_load(app, options) 85 | open_tab_bar HomeScreen, AboutScreen, ThirdScreen, HelpScreen 86 | end 87 | ``` 88 | 89 | #### open_split_screen(master, detail) 90 | 91 | **iPad apps only** 92 | 93 | Opens a UISplitScreenViewController with the specified screens as the root view controller of the current app 94 | 95 | ```ruby 96 | def on_load(app, options) 97 | open_split_screen MasterScreen, DetailScreen, 98 | icon: "split-icon", title: "Split Screen Title" # optional 99 | end 100 | ``` 101 | 102 | #### on_open_url(args = {}) 103 | 104 | Fires when the application is opened via a URL (utilizing [application:openURL:sourceApplication:annotation:](http://developer.apple.com/library/ios/#documentation/uikit/reference/UIApplicationDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/UIApplicationDelegate/application:openURL:sourceApplication:annotation:)). 105 | 106 | ```ruby 107 | def on_open_url(args = {}) 108 | args[:url] # => the URL used to fire the app (NSURL) 109 | args[:source_app] # => the bundle ID of the app that is launching your app (string) 110 | args[:annotation] # => hash with annotation data from the source app 111 | end 112 | ``` 113 | 114 | --- 115 | 116 | ### Class Methods 117 | 118 | #### status_bar 119 | 120 | Class method that allows hiding or showing the status bar. 121 | 122 | ```ruby 123 | class AppDelegate < PM::Delegate 124 | status_bar true, animation: :none # :slide, :fade 125 | end 126 | ``` 127 | 128 | #### tint_color 129 | 130 | Class method that allows you to set the application's global tint color for iOS 7 apps. 131 | 132 | ```ruby 133 | class AppDelegate < ProMotion::Delegate 134 | tint_color UIColor.greenColor 135 | end 136 | ``` 137 | 138 | -------------------------------------------------------------------------------- /docs/new_features_for_rmq_and_promotion.md: -------------------------------------------------------------------------------- 1 | # New generators to integrate RMQ & ProMotion nicely 2 | 3 | Our new generators allow you to create your ProMotion screen and stylesheet template to let you hit the ground running. Currently the following RedPotion generators exist: 4 | 5 | ``` 6 | potion g screen foo 7 | potion g table_screen foo 8 | potion g view foo 9 | 10 | # All rmq generators work with the potion command as well 11 | potion g model foo 12 | potion g shared foo 13 | potion g lib foo 14 | 15 | # rmq controller generators also still exist 16 | # but screens are preferred to get the redpotion value 17 | potion g controller foo 18 | potion g collection_view_controller foos 19 | potion g table_view_controller bars 20 | 21 | # RedPotion includes CDQ and afmotion by default, if you don't need these gems 22 | # we have provided command line tasks to remove either of them 23 | potion remove cdq 24 | potion remove afmotion 25 | ``` 26 | 27 | ## New features for RMQ 28 | 29 | ### `find` is aliased to `rmq` so you can use it for a more natural reading code: 30 | 31 | ```ruby 32 | find.all.hide 33 | find(my_view).children.nudge(right: 10) 34 | ``` 35 | 36 | ### You can use `app` directly in code, which is the same as `rmq.app` 37 | 38 | So you also get window, device, and delegate from that. 39 | 40 | ``` 41 | app.device 42 | app.window 43 | app.delegate 44 | ``` 45 | 46 | ### You can use the following in a UIView or Screen or UIViewController without prefacing it with `rmq`: 47 | 48 | ```ruby 49 | append 50 | append! 51 | prepend 52 | prepend! 53 | create 54 | create! 55 | build 56 | build! 57 | on 58 | apply_style 59 | reapply_styles 60 | style 61 | color 62 | image 63 | stylesheet 64 | stylesheet= 65 | ``` 66 | 67 | ### Stylesheet in your screens 68 | 69 | You can specify the stylesheet in your screen like so: 70 | 71 | ```ruby 72 | class HomeScreen < PM::Screen 73 | title "RedPotion" 74 | stylesheet HomeStylesheet 75 | 76 | def on_load 77 | end 78 | end 79 | ``` 80 | 81 | ### rmq_build can now be called on_load 82 | 83 | You can use either rmq_build or on_load, they do exactly the same thing. You can only use one or the other. `on_load` is preferred as it matches the screen's onload. 84 | 85 | ```ruby 86 | class Section < UIView 87 | def on_load 88 | apply_style :section 89 | 90 | append(UIButton, :section_button).on(:touch) do 91 | mp "Button touched" 92 | end 93 | end 94 | end 95 | ``` 96 | 97 | ### Remote image loading for UIImageView styler 98 | 99 | You can set `remote_image` to a URL string or an instance of `NSURL` and it will automatically fetch the image and set the image (with caching) using the power of [SDWebImage](https://github.com/rs/SDWebImage). 100 | 101 | ```ruby 102 | class MyStylesheet < ApplicationStylesheet 103 | def my_ui_image_view(st) 104 | # placeholder_image= is just an alias to image= 105 | # Set the placeholder image you want from your resources directory 106 | st.placeholder_image = image.resource("my_placeholder") 107 | # Set the remote URL. It will be applied to the UIImageView 108 | # when downloaded or retrieved from the local cache. 109 | st.remote_image = "http://www.rubymotion.com/img/rubymotion-logo.png" 110 | # or st.remote_image = NSURL.urlWithString(...) 111 | end 112 | end 113 | ``` 114 | 115 | In order to use this feature, you must add the `SDWebImage` cocoapod to your project: 116 | 117 | ```ruby 118 | app.pods do 119 | pod 'SDWebImage' 120 | end 121 | ``` 122 | 123 | ## New features for ProMotion 124 | 125 | * 2.2.0 added on_styled 126 | * 2.3.0 added on_load to match RedPotion 127 | * 2.4.0 added support for SDWebImage to replace JMImageCache 128 | * 2.5.0 added footer_views to TableScreens 129 | -------------------------------------------------------------------------------- /docs/cookbook/table_screens.md: -------------------------------------------------------------------------------- 1 | There are two types of templates in RedPotion for tables: 2 | 3 | * Metal tables: "Metal" meaning down to the metal. These are standard SDK tables, that are converted into ProMotion screens 4 | * ProMotion tables: These are easy to use and if you don't have tons of rows they would be a good choice. 5 | 6 | ## Metal tables 7 | 8 | ### To create 9 | 10 | ``` 11 | potion g table_screen foo 12 | ``` 13 | 14 | ## ProMotion tables (Table Screen) 15 | 16 | ProMotion::TableScreen allows you to easily create lists or "tables" as iOS calls them. It's a subclass of [UITableViewController](http://developer.apple.com/library/ios/#documentation/uikit/reference/UITableViewController_Class/Reference/Reference.html) and has all the goodness of [PM::Screen](https://github.com/infinitered/ProMotion/blob/master/docs/Reference/ProMotion%20Screen.md) with some additional magic to make the tables work beautifully. 17 | 18 | |Table Screens|Grouped Tables|Searchable|Refreshable| 19 | |---|---|---|---| 20 | |![ProMotion TableScreen](https://f.cloud.github.com/assets/1479215/1534137/ed71e864-4c90-11e3-98aa-ed96049f5407.png)|![Grouped Table Screen](https://f.cloud.github.com/assets/1479215/1589973/61a48610-5281-11e3-85ac-abee99bf73ad.png)|![Searchable](https://f.cloud.github.com/assets/1479215/1534299/20cc05c6-4c93-11e3-92ca-9ee39c044457.png)|![Refreshable](https://f.cloud.github.com/assets/1479215/1534317/5a14ef28-4c93-11e3-8e9e-f8c08d8464f8.png)| 21 | 22 | ### To create 23 | 24 | ``` 25 | potion g table_screen foo 26 | ``` 27 | 28 | [ProMotion table docs](https://github.com/infinitered/ProMotion/blob/master/docs/Reference/ProMotion%20TableScreen.md) 29 | 30 | 31 | ### Add a custom cell 32 | 33 | We almost always use custom cells, rather than rely on the default ones provided by the SDK. To create a custom cell (or many different cell types) for your table screen, do this: 34 | 35 | ``` 36 | potion g table_screen_cell bar_cell 37 | ``` 38 | 39 | Then follow the directions in the files that were created. 40 | 41 | ## ProMotion::DataTableScreen 42 | 43 | This is a feature that RubyMotion developers have wanted for some time now: easily binding a TableScreen to CoreData. 44 | 45 | Now all you have to do is use [CDQ](https://github.com/infinitered/cdq) to define your CoreData schema and implement the model like so: 46 | 47 | ```ruby 48 | schema "0001 initial" do 49 | entity "MyModel" do 50 | # Define anything you want to here 51 | string :name, optional: false 52 | integer32 :something_else, default: 5 53 | 54 | # These are special CDQ properties that get populated automatically. 55 | # They are not required, but are very helpful. 56 | datetime :created_at 57 | datetime :updated_at 58 | end 59 | end 60 | ``` 61 | 62 | ```ruby 63 | class MyModel < CDQManagedObject 64 | # Scopes are optional 65 | scope :sort_name, sort_by(:name) 66 | 67 | # This is just a ProMotion TableScreen cell definition 68 | def cell 69 | # Use the model's properties to populate data in the hash 70 | { 71 | title: name, 72 | subtitle: "Something else: #{something_else}" 73 | } 74 | end 75 | end 76 | ``` 77 | 78 | Then create you `DataTableScreen`: 79 | 80 | ```ruby 81 | class SomeModelScreen < PM::DataTableScreen 82 | title "Cool Implementation of CoreData" 83 | model MyModel, scope: :sort_name 84 | end 85 | ``` 86 | 87 | Once everything is in place, the new screen will mirror your CoreData database and build the cells based on the `cell` definition in the model. Whenever you update the data in CoreData the table will automatically update to reflect the new data! It automatically handles additions, deletions, and updates to existing model data. 88 | 89 | The `model` class method accepts an optional `scope:` parameter where you an specify a scope as defined in your model. If you _do not_ specify a scope or the scope is not sorted, the `DataTableScreen` will attempt to sort your model data by the following properties (in this order): `:updated_at`, `:created_at`, `:id`. If your model doesn't include any of these properties you should add a `sort_by(:property)` to the chosen scope or the DatTableScreen will not work. 90 | 91 | **NOTE:** `DataTableScreen` is a work in progress and some of the extensions to `TableScreen` like `searchable`, `refreshable`, `longpressable`, etc. are not yet available. 92 | -------------------------------------------------------------------------------- /docs/cookbook/animations.md: -------------------------------------------------------------------------------- 1 | ## Animating 2 | 3 | The most basic: 4 | 5 | ```ruby 6 | find.animate do 7 | find(UIButton).nudge r: 40 8 | end 9 | ``` 10 | 11 | A better way to do that is select something first. In this case `q` is an RMQ instance selecting all UIButtons: 12 | 13 | ```ruby 14 | find(UIButton).animate do |q| 15 | q.nudge r: 40 16 | end 17 | ``` 18 | Some more options, this time it is animating a selected view: 19 | 20 | ```ruby 21 | find(my_view).animate( 22 | duration: 0.3, 23 | animations: lambda{|q| 24 | q.move left: 20 25 | } 26 | ) 27 | ``` 28 | 29 | ```ruby 30 | # As an example, this is the implementation of .animations.throb 31 | find(selectors).animate( 32 | duration: 0.1, 33 | animations: -> (q) { 34 | q.style {|st| st.scale = 1.1} 35 | }, 36 | completion: -> (did_finish, q) { 37 | q.animate( 38 | duration: 0.4, 39 | animations: -> (cq) { 40 | cq.style {|st| st.scale = 1.0} 41 | } 42 | ) 43 | } 44 | ) 45 | 46 | # You can pass any options that animateWithDuration allows: options: YOUR_OPTIONS 47 | ``` 48 | 49 | # Built-in animations 50 | 51 | ```ruby 52 | find(my_view).animations.fade_in 53 | find(my_view).animations.fade_in(duration: 0.8) 54 | find(my_view).animations.fade_out 55 | find(my_view).animations.fade_out(duration: 0.8) 56 | 57 | find(my_view).animations.blink 58 | find(my_view).animations.throb 59 | find(my_view).animations.sink_and_throb 60 | find(my_view).animations.land_and_sink_and_throb 61 | find(my_view).animations.drop_and_spin # has optional param 'remove_view: true' 62 | 63 | find(my_view).animations.slide_in # default from_direction: :right 64 | find(my_view).animations.slide_in(from_direction: :left) # acceptable directions :left, :right, :top, :bottom 65 | 66 | find(my_view).animations.slide_out # default to_direction: :left, also accepts :right, :top, and :bottom 67 | find(my_view).animations.slide_out(remove_view: true) # removes the view from superview after animation 68 | find(my_view).animations.slide_out(to_direction: :top, remove_view: true) #combining options example 69 | 70 | find.animations.start_spinner 71 | find.animations.stop_spinner 72 | ``` 73 | 74 | Fade In 75 | 76 | fade_in 77 | 78 |   79 | 80 | Fade Out 81 | 82 | fade_out 83 | 84 | Blink 85 | 86 | blink 87 | 88 | Throb 89 | 90 | throb 91 | 92 | Sink and Throb 93 | 94 | sink_and_throb 95 | 96 | Land and Sink and Throb 97 | 98 | land_sink_throb 99 | 100 | Drop and Spin 101 | 102 | drop_spin 103 | 104 | Slide In 105 | slide_in 106 | 107 | 108 | Slide Out 109 | slide_out 110 | 111 | -------------------------------------------------------------------------------- /lib/project/ext/ui_view_controller.rb: -------------------------------------------------------------------------------- 1 | class UIViewController 2 | 3 | def append(view_or_constant, style=nil, opts = {}) 4 | self.rmq.append(view_or_constant, style, opts) 5 | end 6 | def append!(view_or_constant, style=nil, opts = {}) 7 | self.rmq.append!(view_or_constant, style, opts) 8 | end 9 | 10 | def prepend(view_or_constant, style=nil, opts = {}) 11 | self.rmq.prepend(view_or_constant, style, opts) 12 | end 13 | def prepend!(view_or_constant, style=nil, opts = {}) 14 | self.rmq.prepend!(view_or_constant, style, opts) 15 | end 16 | 17 | def create(view_or_constant, style=nil, opts = {}) 18 | self.rmq.create(view_or_constant, style, opts) 19 | end 20 | def create!(view_or_constant, style=nil, opts = {}) 21 | self.rmq.create!(view_or_constant, style, opts) 22 | end 23 | 24 | def build(view_or_constant, style = nil, opts = {}) 25 | self.rmq.build(view_or_constant, style, opts) 26 | end 27 | def build!(view_or_constant, style = nil, opts = {}) 28 | self.rmq.build!(view_or_constant, style, opts) 29 | end 30 | 31 | def reapply_styles 32 | self.rmq.all.reapply_styles 33 | end 34 | 35 | def color 36 | self.rmq.color 37 | end 38 | 39 | def font 40 | self.rmq.font 41 | end 42 | 43 | def image 44 | self.rmq.image 45 | end 46 | 47 | def stylesheet 48 | self.rmq.stylesheet 49 | end 50 | 51 | def stylesheet=(value) 52 | self.rmq.stylesheet = value 53 | end 54 | 55 | def self.stylesheet(style_sheet_class) 56 | @rmq_style_sheet_class = style_sheet_class 57 | end 58 | 59 | def self.rmq_style_sheet_class 60 | @rmq_style_sheet_class 61 | end 62 | 63 | def on_load 64 | end 65 | 66 | def view_did_load 67 | end 68 | 69 | alias :originalViewDidLoad :viewDidLoad 70 | def viewDidLoad 71 | set_stylesheet 72 | 73 | self.originalViewDidLoad 74 | unless pm_handles_delegates? 75 | unless self.class.included_modules.include?(ProMotion::ScreenModule) 76 | self.view_did_load 77 | end 78 | self.on_load 79 | end 80 | end 81 | 82 | def view_will_appear(animated) 83 | end 84 | 85 | def viewWillAppear(animated) 86 | unless pm_handles_delegates? 87 | self.view_will_appear(animated) 88 | end 89 | end 90 | 91 | def view_did_appear(animated) 92 | end 93 | 94 | def viewDidAppear(animated) 95 | unless pm_handles_delegates? 96 | self.view_did_appear(animated) 97 | end 98 | end 99 | 100 | def view_will_disappear(animated) 101 | end 102 | 103 | def viewWillDisappear(animated) 104 | unless pm_handles_delegates? 105 | self.view_will_disappear(animated) 106 | end 107 | end 108 | 109 | def view_did_disappear(animated) 110 | end 111 | 112 | def viewDidDisappear(animated) 113 | unless pm_handles_delegates? 114 | self.view_did_disappear(animated) 115 | end 116 | end 117 | 118 | def should_rotate(orientation) 119 | end 120 | 121 | def shouldAutorotateToInterfaceOrientation(orientation) 122 | self.should_rotate(orientation) 123 | end 124 | 125 | def should_autorotate 126 | true 127 | end 128 | 129 | def shouldAutorotate 130 | self.should_autorotate 131 | end 132 | 133 | def will_rotate(orientation, duration) 134 | end 135 | 136 | def willRotateToInterfaceOrientation(orientation, duration:duration) 137 | self.will_rotate(orientation, duration) 138 | end 139 | 140 | def on_rotate(orientation) 141 | end 142 | 143 | def didRotateFromInterfaceOrientation(orientation) 144 | self.on_rotate orientation 145 | end 146 | 147 | def will_animate_rotate(orientation, duration) 148 | end 149 | 150 | def willAnimateRotationToInterfaceOrientation(orientation, duration: duration) 151 | self.will_animate_rotate(orientation, duration) 152 | end 153 | 154 | protected 155 | 156 | def set_stylesheet 157 | if self.class.rmq_style_sheet_class 158 | self.rmq.stylesheet = self.class.rmq_style_sheet_class 159 | self.view.rmq.apply_style(:root_view) if self.rmq.stylesheet.respond_to?(:root_view) 160 | end 161 | end 162 | 163 | private 164 | 165 | def pm_handles_delegates? 166 | self.respond_to?(:class_handles_delegates?) && self.class_handles_delegates? 167 | end 168 | 169 | end 170 | -------------------------------------------------------------------------------- /docs/cookbook/images.md: -------------------------------------------------------------------------------- 1 | ## General images 2 | 3 | RedPotion provides various features for dealing with images. 4 | 5 | If you want to load an image from your **/resources** folder (_which is where they should be_), you can either load it and cache it (**imageNamed**) or load it and not cache it (**NSBundle.mainBundle.pathForResource**). ProMotion takes care of the details: 6 | 7 | ```ruby 8 | image.resource('foo') # /resources/foo@2x.png 9 | image.resource('foo', cached: false) 10 | # In a stylesheet 11 | st.background_image = image.resource('foo') 12 | ``` 13 | 14 | **Snapshot of a view** 15 | 16 | Lastly you can get an image of a view, meaning a "screenshot" of it: 17 | 18 | ```ruby 19 | my_image_view.image = image.from_view(some_view) 20 | ``` 21 | 22 | **Other examples** 23 | 24 | ```ruby 25 | RubyMotionQuery::ImageUtils.resource('logo') 26 | image.resource('logo') 27 | image.resource('subfolder/foo') 28 | image.resource_for_device('logo') # will look for 4inch or 3.5inch 29 | image.resource('logo', cached: false) 30 | 31 | image.resource_resizable('foo', left: 10, top: 10, right: 10, bottom: 10) 32 | 33 | image.from_view(my_view) 34 | ``` 35 | 36 | ------ 37 | 38 | ## Remote images 39 | 40 | You can set `remote_image` to a URL string or an instance of `NSURL` and it will automatically fetch the image and set the image (with caching) using the power of [SDWebImage](https://github.com/rs/SDWebImage). 41 | 42 | ```ruby 43 | class MyStylesheet < ApplicationStylesheet 44 | def my_ui_image_view(st) 45 | # placeholder_image= is just an alias to image= 46 | # Set the placeholder image you want from your resources directory 47 | st.placeholder_image = image.resource("my_placeholder") 48 | # Set the remote URL. It will be applied to the UIImageView 49 | # when downloaded or retrieved from the local cache. 50 | st.remote_image = "http://www.rubymotion.com/img/rubymotion-logo.png" 51 | # or st.remote_image = NSURL.urlWithString(...) 52 | end 53 | end 54 | ``` 55 | 56 | You can also set `remote_image` with a hash that includes `url` and `on_load` 57 | keys. The `on_load` key should contain a closure to execute once the remote 58 | image has finished loading. 59 | 60 | Extending on the example above: 61 | ```ruby 62 | class MyStylesheet < ApplicationStylesheet 63 | def my_ui_image_view(st) 64 | # ... 65 | st.remote_image = { url: 'http://bit.ly/18iMhwc', 66 | on_load: -> { puts 'Image finished loading!' } } 67 | # Or st.remote_image = { url: NSURL.urlWithString(...), 68 | # on_load: -> { puts 'Image finished loading!' } } 69 | end 70 | end 71 | ``` 72 | 73 | To assign a remote image to a UIImageView: 74 | 75 | ```ruby 76 | your_ui_image_view.remote_image = "http://bit.ly/18iMhwc" 77 | ``` 78 | 79 | We also provide an easy way to reset all images cached by SDWebImage. If you're using ProMotion screens, you should probably do this in your `on_memory_warning` method on each screen. If using standard `UIViewController` subclasses, add this to your `didReceiveMemoryWarning` method. 80 | 81 | ```ruby 82 | app.reset_image_cache! 83 | ``` 84 | 85 | ------ 86 | 87 | ## Capped Images 88 | 89 | Sometimes when you apply a background_image to a view you want the image to stretch to the size of the view without stretching the corners of the image, for example if you're making a rounded button. The SDK has a nice feature for this, called UIImage#resizableImageWithCapInsets. It stretches the center of your image, but not the corners. 90 | 91 | Let's say you want to create this, like we did in [Temple](http://app.temple.cx/): 92 | 93 | ![Bar](https://ir_wp.s3.amazonaws.com/wp-content/uploads/sites/18/2014/03/bar.png) 94 | 95 | The red bar grows horizontally. But it has rounded caps. So we created this image ![Cap image](https://ir_wp.s3.amazonaws.com/wp-content/uploads/sites/18/2014/03/bar_poor@2x.png), which is the caps, plus one pixel to stretch. Here it is blown up and I dimmed the 4 caps: 96 | 97 | ![Cap image](https://ir_wp.s3.amazonaws.com/wp-content/uploads/sites/18/2014/03/blown_up.png) 98 | 99 | Basically just the center line of it stretches, the other 4 quadrants do not. ProMotion makes this very easy. You create a UIImageView, then in the style (or anywhere) you set the image like so: 100 | 101 | ```ruby 102 | append(UIImageView, :your_style) 103 | 104 | # Then in your style 105 | st.image = image.resource_resizable('your_image', top: 4, left: 4, right: 4, bottom: 4) 106 | ``` 107 | 108 | The top, left, etc, tell which p 109 | -------------------------------------------------------------------------------- /docs/redpotion_specific_features.md: -------------------------------------------------------------------------------- 1 | # Features we've added to RedPotion that don't modify an existing gem 2 | 3 | This isn't a complete list, but here are some highlights. See the specific sections in the cookbook for everything. 4 | 5 | ## UIColor 6 | 7 | UIColor has a `with` method. Allowing you to build a color from an existing color easily 8 | 9 | ```ruby 10 | # for example that time you want your existing color, but with a slight change 11 | color.my_custom_color.with(a: 0.5) 12 | ``` 13 | 14 | ## remote_image 15 | 16 | See "Images, icons, and photos" in cookbook 17 | 18 | ## blank? 19 | 20 | ``` 21 | nil.blank? => true 22 | [].blank? => true 23 | {}.blank? => true 24 | # etc 25 | ``` 26 | 27 | ## Semantic methods 28 | 29 | * `app` aliases `rmq.app` 30 | * `device` aliases `rmq.device` 31 | * `find` aliases `rmq` so you can do stuff like: `find(:some_style).find(UIButton).hide` 32 | * `app.data` aliases cdq. 33 | * `screen` aliases `rmq.view_controller` 34 | * `live` aliases `rmq_live_stylesheets` 35 | * `enable_live_stylesheets` aliases `enable_rmq_live_stylesheets` 36 | * `on_load` aliases `rmq_build` in views. This is great as it now matches screens 37 | * `on_styled` aliases `rmq_style_applied` 38 | * `open` in the REPL aliases `find.screen.open(*)` 39 | * `close` in the REPL aliases `find.screen.close(*)` 40 | 41 | ## append, create, build, on, off, apply_style, etc inside of a view 42 | 43 | In RedPotion, if you call this inside of a custom view: 44 | 45 | ``` 46 | append(UIView) 47 | ``` 48 | 49 | That is the same as calling: 50 | 51 | ``` 52 | rmq(self).append(UIView) 53 | ``` 54 | 55 | This is true for all these: 56 | 57 | * append 58 | * create 59 | * build 60 | * append! 61 | * create! 62 | * build! 63 | * on 64 | * off 65 | * apply_style 66 | * reapply_styles 67 | * style 68 | * color 69 | * font 70 | * image 71 | 72 | ### ProMotion::DataTableScreen 73 | 74 | This is a feature that RubyMotion developers have wanted for some time now: easily binding a TableScreen to CoreData. 75 | 76 | Now all you have to do is use [CDQ](https://github.com/infinitered/cdq) to define your CoreData schema and implement the model like so: 77 | 78 | ```ruby 79 | schema "0001 initial" do 80 | entity "MyModel" do 81 | # Define anything you want to here 82 | string :name, optional: false 83 | integer32 :something_else, default: 5 84 | 85 | # These are special CDQ properties that get populated automatically. 86 | # They are not required, but are very helpful. 87 | datetime :created_at 88 | datetime :updated_at 89 | end 90 | end 91 | ``` 92 | 93 | ```ruby 94 | class MyModel < CDQManagedObject 95 | # Scopes need to be sorted. We'll try and figure out how you 96 | # want it sorted automatically if it's not, and give you a 97 | # warning in the REPL. 98 | scope :sort_name, sort_by(:name) 99 | 100 | # This is just a ProMotion TableScreen cell definition. 101 | # All cell options are available here. See the PM docs for details. 102 | def cell 103 | { 104 | # Use the model's properties to populate data in the hash 105 | title: name, 106 | subtitle: "Something else: #{something_else}" 107 | } 108 | end 109 | end 110 | ``` 111 | 112 | #### Then create your `DataTableScreen` 113 | 114 | The `model` class method accepts an optional `scope:` parameter where you an specify a scope as defined in your model. If you _do not_ specify a scope or the scope is not sorted, the `DataTableScreen` will attempt to sort your model data by the following properties (in this order): `:updated_at`, `:created_at`, `:id`. If your model doesn't include any of these properties you should add a `sort_by(:property)` to the chosen scope or the DataTableScreen will not work. 115 | 116 | ```ruby 117 | class SomeModelScreen < PM::DataTableScreen 118 | title "Cool Implementation of CoreData" 119 | model MyModel, scope: :sort_name 120 | end 121 | ``` 122 | 123 | You could also use a specific query like this: 124 | 125 | ```ruby 126 | class SomeModelScreen < PM::DataTableScreen 127 | title "Cool Implementation of CoreData" 128 | # Tells DataTableScreen what cell definitions to use. 129 | model MyModel 130 | 131 | def model_query 132 | # You can use this space to return any CDQTargetedQuery 133 | # This is useful because you can use relationships here, 134 | # so long as the result contains all `MyModel` objects. 135 | MyModel.where(:name).contains("Emily") 136 | .and(:something_else).gt(4) 137 | .sort_by(:name) 138 | end 139 | end 140 | ``` 141 | 142 | Once everything is in place, the new screen will mirror your CoreData database and build the cells based on the `cell` definition in the model. Whenever you update the data in CoreData the table will automatically update to reflect the new data! It automatically handles additions, deletions, and updates to existing model data. 143 | -------------------------------------------------------------------------------- /spec/ruby_motion_query/stylers/ui_image_view_spec.rb: -------------------------------------------------------------------------------- 1 | class StyleSheetForUIImageViewStylerTests < RubyMotionQuery::Stylesheet 2 | 3 | def ui_image_view_placeholder(st) 4 | st.placeholder_image = rmq.image.resource('grumpy_cat') 5 | end 6 | 7 | def ui_image_view_remote(st) 8 | ui_image_view_placeholder(st) 9 | st.remote_image = 'http://somehost/image' 10 | end 11 | 12 | def ui_image_view_remote_nsurl(st) 13 | ui_image_view_placeholder(st) 14 | st.remote_image = NSURL.URLWithString('http://somehost/image') 15 | end 16 | 17 | def ui_image_view_remote_no_placeholder(st) 18 | st.remote_image = 'http://somehost/image' 19 | end 20 | 21 | def ui_image_view_remote_fail(st) 22 | ui_image_view_placeholder(st) 23 | st.remote_image = 'http://somehost/image_fail' 24 | end 25 | 26 | end 27 | 28 | describe "RubyMotionQuery styler: UIImageView" do 29 | extend WebStub::SpecHelpers 30 | 31 | before do 32 | WebStub::Protocol.disable_network_access! 33 | @vc = UIViewController.alloc.init 34 | @vc.rmq.stylesheet = StyleSheetForUIImageViewStylerTests 35 | @view_klass = UIImageView 36 | 37 | @image = load_image('homer') 38 | @grumpy_cat = rmq.image.resource('grumpy_cat') 39 | @url = 'http://somehost/image' 40 | WebStub::API.stub_request(:get, @url).to_return(body: load_image('homer'), content_type: "image/jpeg") 41 | end 42 | 43 | after do 44 | WebStub::Protocol.enable_network_access! 45 | image_cache = SDWebImageManager.sharedManager.imageCache 46 | image_cache.clearMemory 47 | if image_cache.respond_to?(:clearDisk) 48 | # Support for SDWebImage v3.x 49 | image_cache.clearDisk 50 | else 51 | # Support for SDWebImage v4.x 52 | image_cache.deleteOldFiles 53 | end 54 | end 55 | 56 | it "should set a placeholder image" do 57 | view = @vc.rmq.append!(@view_klass, :ui_image_view_placeholder) 58 | view.image.should == @grumpy_cat 59 | end 60 | 61 | it "should set a remote image URL string" do 62 | view = @vc.rmq.append!(@view_klass, :ui_image_view_remote) 63 | view.image.should == @grumpy_cat 64 | 65 | wait 0.1 do 66 | view.image.should.not == @grumpy_cat 67 | end 68 | end 69 | 70 | it "should set a remote image with a NSURL instance" do 71 | view = @vc.rmq.append!(@view_klass, :ui_image_view_remote_nsurl) 72 | view.image.should == @grumpy_cat 73 | 74 | wait 0.1 do 75 | view.image.should.not == @grumpy_cat 76 | end 77 | end 78 | 79 | it "should keep the placeholder image when the remote image fails" do 80 | view = @vc.rmq.append!(@view_klass, :ui_image_view_remote_fail) 81 | view.image.should == @grumpy_cat 82 | 83 | wait 0.1 do 84 | view.image.should == @grumpy_cat 85 | end 86 | end 87 | 88 | it "should set a remote image and no placeholder" do 89 | view = @vc.rmq.append!(@view_klass, :ui_image_view_remote_no_placeholder) 90 | view.image.should == nil 91 | 92 | wait 0.1 do 93 | view.image.should.not == nil 94 | end 95 | end 96 | 97 | it "should fetch the image from memory" do 98 | view = @vc.rmq.append!(@view_klass, :ui_image_view_remote) 99 | view.image.should == @grumpy_cat 100 | 101 | wait 0.1 do 102 | view.image.should.not == @grumpy_cat 103 | view.image = nil 104 | view.image.should.be.nil 105 | view.apply_style(:ui_image_view_remote) 106 | # This should be instant since we have not cleared the cache 107 | view.image.should.not.be.nil 108 | end 109 | end 110 | 111 | # FIXME: failing to start with size of zero and failing to clear. Not sure why. 112 | # it "should clear the image cache" do 113 | # SDImageCache.sharedImageCache.getSize.should == 0.0 114 | 115 | # url = NSURL.URLWithString('http://somehost/image') 116 | # manager = SDWebImageManager.sharedManager 117 | # if manager.respond_to?('downloadWithURL:options:progress:completed') 118 | # # Support for SDWebImage v3.x 119 | # manager.downloadWithURL(url, 120 | # options: SDWebImageRefreshCached, 121 | # progress: nil, 122 | # completed: -> image, error, cacheType, finished { 123 | # SDImageCache.sharedImageCache.getSize.should > 0.0 124 | # rmq.app.reset_image_cache! 125 | # wait 0.1 do 126 | # SDImageCache.sharedImageCache.getSize.should == 0.0 127 | # end 128 | # } 129 | # ) 130 | # else 131 | # # Support for SDWebImage v4.x 132 | # manager.loadImageWithURL(url, 133 | # options: SDWebImageRefreshCached, 134 | # progress: nil, 135 | # completed: -> image, imageData, error, cacheType, finished, imageURL { 136 | # SDImageCache.sharedImageCache.getSize.should > 0.0 137 | # rmq.app.reset_image_cache! 138 | # wait 0.1 do 139 | # SDImageCache.sharedImageCache.getSize.should == 0.0 140 | # end 141 | # } 142 | # ) 143 | # end 144 | # end 145 | 146 | end 147 | -------------------------------------------------------------------------------- /docs/cookbook/distribution.md: -------------------------------------------------------------------------------- 1 | # Distribution 2 | 3 | When you're ready to distribute your RubyMotion app to other people for testing, or submit to the App Store, here is an overview of the steps you will take: 4 | 5 | 1. Create an App ID for your app 6 | 2. Create your app in iTunes Connect 7 | 3. Add users for testing 8 | 2. Create a distribution certificate and provisioning profile. 9 | 3. Build your app for distribution. 10 | 4. Upload to iTunes Connect. 11 | 5. Enable your build for your TestFlight users. 12 | 6. Once you've uploaded a build and determined that it is ready to submit to the App Store, you can submit the build for App Store review. 13 | 14 | ## Certificates & Provisioning Profiles 15 | 16 | In order to deploy your app to your test users, you need to sign your app using a provisioning profile with "App Store Distribution" enabled. Here are the steps you will take to create this profile: 17 | 18 | 1. Log into Apple Developer Center 19 | 2. Click on "Certificates, Identifiers & Profiles" 20 | 21 | ### Generating an App ID 22 | 23 | 1. Click on Identifiers 24 | 2. Click the plus-sign button to create a new App ID 25 | 3. Fill out the form to create your App ID. 26 | 4. Now that you have an App ID Prefix, Description, and Suffix, you can update the bundle identifier in your Rakefile. 27 | 5. Open your Rakefile and update the `app.identifier` option in the format of `prefix.description.suffix` (i.e. `com.mycompany.myapp`) 28 | 29 | ### Generating a Certificate 30 | 31 | 1. In the sidebar, click Certificates > All 32 | 2. Click the plus-sign button to create a new certificate 33 | 3. Select "App Store and Ad Hoc" as the type of certificate 34 | 4. Now you will generate a certificate signing request. 35 | 1. In spotlight, search for and open Keychain Access 36 | 2. From the Keychain Access menu, select Certificate Assistant > Request a Certificate from a Certificate Authority 37 | 3. Enter the email address that you used to log into the Apple Developer Center 38 | 4. Enter some description in the Common Name field. I used "MyApp Distribution" 39 | 5. Leave CA Email blank (even though is says required) 40 | 6. Select "Saved to disk" and save it somewhere. 41 | 5. Now upload the `.certSigningRequest` file that you just created. 42 | 6. Download the Certificate file that is created for you. 43 | 7. Now, double-click the file you just downloaded to open it, and store it, in the Keychain Access app. 44 | 8. Make note of the name associated with the Certificate. It should display in the format of "iPhone Distribution: NAME". 45 | 9. Open your Rakefile and update the `app.codesign_certificate` option with the value "iPhone Distribution: NAME", replacing NAME with the name displayed in Keychain Access. 46 | 47 | ### Generating a Provisioning Profile 48 | 49 | 2. In the sidebar, click on Provisioning Profiles 50 | 3. Click the plus-sign button to create a profile. 51 | 4. Select "App Store" Distribution 52 | 5. Select your App from the list of App IDs 53 | 5. Select the distribution certificate that we just created 54 | 6. Download the provisioning profile that it generated for you. 55 | 7. Create a new directory in your project called `signing`. Move this '.mobileprovision' file to that directory. 56 | 8. Open your Rakefile and update the `app.provisioning_profile` option with the relative location of that file. It should look something like this: "signing/MyApp_Distribution_Profile.mobileprovision" 57 | 58 | Now that you've completed all the steps necessary to build your app for distribution, follow these steps to build and upload your app to iTunes Connect. 59 | 60 | ## Building for Distribution 61 | 62 | To build your app for distribution, run the following command: 63 | 64 | rake archive:distribution 65 | 66 | ## Uploading to iTunes Connect 67 | 68 | The first step in preparing to upload to iTunes Connect is to create the App record in iTunes Connect. 69 | 70 | Once you've created the App record, there are two different ways that you can upload to iTunes Connect: use the Application Loader application to manually upload a build, or use the [`motion-appstore`](https://github.com/HipByte/motion-appstore) gem to upload from the command line. Note that the `motion-appstore` gem still uses the Application Loader application behind the scenes. 71 | 72 | ### Using the motion-appstore gem 73 | 74 | To upload the build to iTunes Connect, install the gem: 75 | 76 | gem install motion-appstore 77 | 78 | This will add the following commands: 79 | 80 | motion validate appleid@example.com 81 | motion upload appleid@example.com 82 | 83 | Use the `validate` command if you want to see if your build will upload successfully. Use the `upload` command to upload the build to iTunes Connect. Use the email address that you used to log into the Developer Center. 84 | 85 | ### Using Application Loader 86 | 87 | Alternatively, you can manually upload your build using Application Loader. 88 | 89 | 1. To launch Application Loader, use spotlight search, or from the Xcode menu, choose Open Developer Tool > Application Loader. 90 | 2. Sign In using your Developer Center credentials. 91 | 3. Choose "Deliver Your App" 92 | 4. Select the `.ipa` file from your `build/iPhoneOS-*-Release` directory (assuming you already ran `rake archive:distribution`) 93 | 5. Continue through the steps to upload your build. It will conclude with some messaging that makes it sound like your app has been uploaded to the App Store, but don't worry. Your build has simply been uploaded to iTunes Connect. 94 | 95 | The build will have an initial state of "Pending". Once the build transitions to a sort of ready state, it will allow you to enable it for TestFlight Beta Testing. 96 | 97 | ## Enable Build for TestFlight 98 | 99 | Now that you have uploaded your build to iTunes Connect, you can enable the build for TestFlight for distributing to test users. TestFlight allows two different types of testers: internal and external. 100 | 101 | * Internal testers are meant for other members of your team. However, you can simply invite anyone by their email address. If they don't have a Apple account, they will be asked to create a TestFlight user account. A team admin will need to add these users to the team and give them a "technical" role in order to be a TestFlight internal tester. 102 | * External testers are intended to be other people who are not on your team. However, your build must be approved by Apple before it can be sent to your External testers. 103 | 104 | Once you've added your test users, to enable your build for TestFlight beta testing: 105 | 106 | 1. Navigate to your app in iTunes Connect. 107 | 2. Navigate to the Prerelease tab. 108 | 3. Find the build that you want to enable for tesing and click the toggle on the right side of the page (you can only enable one build at a time). 109 | 110 | -------------------------------------------------------------------------------- /spec/ext/ui_view_controller_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'UIViewController' do 2 | 3 | class FakeView < UIView; end 4 | 5 | after do 6 | #cleanup between tests 7 | controller.find(FakeView).remove 8 | end 9 | 10 | shared 'proper delegate caller' do 11 | it 'should call view_will_appear only once' do 12 | controller.view_will_appear_count.should.equal(1) 13 | end 14 | 15 | it 'should call view_did_appear only once' do 16 | controller.viewDidAppear(true) 17 | 18 | controller.view_did_appear_count.should.equal(1) 19 | end 20 | 21 | it 'should call view_did_load only once' do 22 | controller.view_did_load_count.should.equal(1) 23 | end 24 | 25 | it 'should call view_will_disappear only once' do 26 | controller.viewWillDisappear(true) 27 | 28 | controller.view_will_disappear_count.should.equal(1) 29 | end 30 | 31 | it 'should call view_will_disappear only once' do 32 | controller.viewDidDisappear(true) 33 | 34 | controller.view_did_disappear_count.should.equal(1) 35 | end 36 | end 37 | 38 | describe 'when using a PM::Screen' do 39 | tests TestScreen 40 | 41 | behaves_like 'proper delegate caller' 42 | 43 | describe 'append' do 44 | it "should return a RMQ object" do 45 | appended = controller.append(UIView, nil, {}) 46 | appended.is_a?(RubyMotionQuery::RMQ).should.be.true 47 | end 48 | 49 | it "should create a new object class provided" do 50 | appended = controller.append(UIView, nil, {}) 51 | appended.get.is_a?(UIView).should.be.true 52 | end 53 | end 54 | 55 | describe "append!" do 56 | it "should return the appended object" do 57 | appended = controller.append!(UIView, nil, {}) 58 | appended.is_a?(UIView).should.be.true 59 | end 60 | end 61 | 62 | describe "prepend" do 63 | it "should return a RMQ object" do 64 | prepended = controller.prepend(UIView, nil, {}) 65 | prepended.is_a?(RubyMotionQuery::RMQ).should.be.true 66 | end 67 | 68 | it "should create a new object class provided" do 69 | prepended = controller.prepend(UIView, nil, {}) 70 | prepended.get.is_a?(UIView).should.be.true 71 | end 72 | end 73 | 74 | describe "prepend!" do 75 | it "should return the appended object" do 76 | prepended = controller.prepend!(UIView, nil, {}) 77 | prepended.is_a?(UIView).should.be.true 78 | end 79 | end 80 | 81 | describe "create" do 82 | it "should return a RMQ object" do 83 | created = controller.create(UIView, nil, {}) 84 | created.is_a?(RubyMotionQuery::RMQ).should.be.true 85 | end 86 | 87 | it "should create a new object class provided" do 88 | created = controller.create(UIView, nil, {}) 89 | created.get.is_a?(UIView).should.be.true 90 | end 91 | end 92 | 93 | describe "create!" do 94 | it "should return the appended object" do 95 | created = controller.create!(UIView, nil, {}) 96 | created.is_a?(UIView).should.be.true 97 | end 98 | end 99 | 100 | describe "build" do 101 | it "should return a RMQ object" do 102 | built = controller.build(UIView, nil, {}) 103 | built.is_a?(RubyMotionQuery::RMQ).should.be.true 104 | end 105 | 106 | it "should create a new object class provided" do 107 | built = controller.build(UIView, nil, {}) 108 | built.get.is_a?(UIView).should.be.true 109 | end 110 | end 111 | 112 | describe "build!" do 113 | it "should return the appended object" do 114 | built = controller.build!(UIView, nil, {}) 115 | built.is_a?(UIView).should.be.true 116 | end 117 | end 118 | 119 | describe "color" do 120 | it "should return rmq.color" do 121 | controller.color.should.equal(RubyMotionQuery::Color) 122 | end 123 | end 124 | 125 | describe "font" do 126 | it "should return rmq.font" do 127 | controller.font.should.equal(RubyMotionQuery::Font) 128 | end 129 | end 130 | 131 | describe "image" do 132 | it "should return rmq.image" do 133 | controller.image.should.equal(RubyMotionQuery::ImageUtils) 134 | end 135 | end 136 | 137 | describe "stylesheet" do 138 | it "should return rmq.stylesheet" do 139 | controller.stylesheet.should.equal(controller.rmq.stylesheet) 140 | end 141 | end 142 | 143 | describe "stylesheet=" do 144 | it "should set rmq.stylesheet" do 145 | controller.stylesheet = TestScreenStylesheet 146 | controller.rmq.stylesheet.is_a?(TestScreenStylesheet).should.be.true 147 | end 148 | end 149 | 150 | describe "reapply_styles" do 151 | it "should reapply styles" do 152 | @view = controller.append!(UILabel, :test_label) 153 | @view.text.should.equal('style from sheet') 154 | 155 | @view.text = nil 156 | @view.text.should.be.nil 157 | 158 | controller.reapply_styles 159 | @view.text.should.equal('style from sheet') 160 | end 161 | end 162 | 163 | describe "find" do 164 | it "should set use the proper context to find the children" do 165 | controller.append(UILabel).tag(:find_me) 166 | controller.find(:find_me).count.should.equal(1) 167 | end 168 | 169 | it "should properly work with multiple arguments as well" do 170 | controller.append(UILabel).tag(:first) 171 | controller.append(UILabel).tag(:second) 172 | controller.find(:first, :second).count.should.equal(2) 173 | end 174 | end 175 | 176 | describe "#find!" do 177 | it "should return the view when there is a single result" do 178 | btn = controller.append!(FakeView) 179 | controller.append(UIButton) 180 | 181 | controller.find!(FakeView).should.equal(btn) 182 | end 183 | 184 | it "should return an array of views when there is multiple results" do 185 | btn = controller.append!(FakeView) 186 | btn2 = controller.append!(FakeView) 187 | controller.append(UIButton) 188 | 189 | controller.find!(FakeView).should.equal([btn, btn2]) 190 | end 191 | end 192 | end 193 | 194 | describe 'when using a PM::TableScreen' do 195 | tests TestTableScreen 196 | 197 | behaves_like 'proper delegate caller' 198 | end 199 | 200 | describe 'when using a PM::GroupedTableScreen' do 201 | tests TestGroupedTableScreen 202 | 203 | behaves_like 'proper delegate caller' 204 | end 205 | 206 | describe 'when using a controller' do 207 | tests TestController 208 | 209 | behaves_like 'proper delegate caller' 210 | end 211 | 212 | end 213 | -------------------------------------------------------------------------------- /docs/cookbook/core_data.md: -------------------------------------------------------------------------------- 1 | RedPotion uses Core Data as its local data store. Specifically it uses the [CDQ](https://github.com/infinitered/cdq) gem. 2 | 3 | ## `app.data` 4 | 5 | The `cdq` command is aliased to `app.data` to give you a more semantic way to access local data. 6 | 7 | ## Schema 8 | 9 | Now go edit schemas/0001_initial.rb. There's some commented-out example code 10 | in it. Go ahead and uncomment the example entities and save the file. It should 11 | look like this: 12 | 13 | ```ruby 14 | schema "0001 initial" do 15 | 16 | # Examples: 17 | # 18 | entity "Person" do 19 | string :name, optional: false 20 | has_many :posts 21 | end 22 | 23 | entity "Post" do 24 | string :title, optional: false 25 | string :body 26 | 27 | datetime :created_at 28 | datetime :updated_at 29 | has_many :replies, inverse: "Post.parent" 30 | belongs_to :parent, inverse: "Post.replies" 31 | 32 | belongs_to :person 33 | end 34 | 35 | end 36 | ``` 37 | 38 | ## Models 39 | 40 | Now to generate models for the entities, Person and Post, that you've just set up 41 | in the schema. CDQ includes a handy generator to save a little time: 42 | 43 | ```bash 44 | $ cdq create model person 45 | $ cdq create model post 46 | ``` 47 | 48 | ## Creating Data 49 | 50 | You're now ready to run rake and start playing with your data. The simulator 51 | should come up cleanly. 52 | 53 | Once you have a console prompt, start creating some data: 54 | 55 | ```ruby 56 | marie = Person.create(name: "Marie Skłodowska") 57 | pierre = Person.create(name: "Pierre Curie") 58 | post = marie.posts.create(title: "First Post", body: "This stuff seems to be glowing.", created_at: Time.now) 59 | post.replies.create(title: "Marry Me!", body: "That is so freaking amazing!", person: pierre, created_at: Time.now) 60 | cdq.save 61 | ``` 62 | 63 | Note that unlike ActiveRecord, created_at will not (currently) get set 64 | automatically. 65 | 66 | Now quit the app and restart so you can verify that things are 67 | saving to disk correctly. 68 | 69 | ### An aside about data faults 70 | 71 | When you're looking around, you may notice something strange. Sometimes when 72 | you run a query, especially for the first time, you get a result like this: 73 | 74 | ```ruby 75 | (main)> Person.all.array 76 | => [ (entity: Person; id: 0x9285150 ; data: ), (entity: Person; id: 0x9285160 ; data: )] 77 | ``` 78 | 79 | Very compact. But other times, it might look like this: 80 | 81 | ```ruby 82 | (main)> Person.all.map(&:name) 83 | => ["Marie Skłodowska", "Pierre Curie"] 84 | (main)> Person.all.array 85 | => [ (entity: Person; id: 0x9285150 ; data: { 86 | name = "Marie Sk\U0142odowska"; 87 | posts = ""; 88 | }), (entity: Person; id: 0x9285160 ; data: { 89 | name = "Pierre Curie"; 90 | posts = ""; 91 | })] 92 | 93 | ``` 94 | 95 | Not so compact, but more useful. What's going on? Core Data is very smart 96 | about how much data to load, and won't go fetch the details of an object until 97 | they're used. So in the first example, I'd just loaded the app and hadn't used 98 | anything yet, so you see the text ```data: ``` in there indicating that 99 | it hasn't fetched the attributes from the SQLite database that actually holds 100 | all your data. In the second example, I deliberately loop through each object 101 | and ask it for data, so now when I print out the collection, it shouls you all the 102 | attributes. But you'll note that for posts, it still says "relationship fault", 103 | because we haven't asked it about posts yet. Turtles all the way down. 104 | 105 | ## Queries 106 | 107 | Now try some queries: 108 | 109 | ```ruby 110 | pierre = Person.where(:name).contains("Pierre").first 111 | marie = Person.where(:name).ne("Pierre Curie").first 112 | Post.where(:person).eq(pierre).array 113 | Post.sort_by(:title).first 114 | Post.sort_by(:created_at)[1] 115 | Post.count 116 | ``` 117 | 118 | Create some more data and then run the queries again, or try some new combinations. 119 | 120 | ## Cheatsheet 121 | 122 | ```ruby 123 | cdq.setup # Load the whole system 124 | 125 | cdq.contexts.current # The currently-active NSManagedObjectContext 126 | cdq.contexts.all # See all contexts on the stack 127 | cdq.contexts.new(type) # Create a new context and push it onto the stack 128 | 129 | cdq.save # Save all contexts on the stack 130 | ``` 131 | 132 | ## Object Lifecycle 133 | 134 | ### Creating 135 | ```ruby 136 | Author.create(name: "Le Guin", publish_count: 150, first_published: 1970) 137 | Author.create(name: "Shakespeare", publish_count: 400, first_published: 1550) 138 | Author.create(name: "Blake", publish_count: 100, first_published: 1778) 139 | cdq.save 140 | ``` 141 | 142 | ### Updating 143 | ```ruby 144 | author = Author.first 145 | author.name = "Ursula K. Le Guin" 146 | cdq.save 147 | ``` 148 | 149 | ### Deleting 150 | ```ruby 151 | author = Author.first 152 | author.destroy 153 | cdq.save 154 | ``` 155 | 156 | ## Queries 157 | 158 | ```ruby 159 | Author.where(:name).eq("Emily") 160 | Author.where(:name).not_equal("Emily") 161 | Author.limit(1) 162 | Author.offset(10) 163 | Author.where(:name).contains("A").offset(10).first 164 | 165 | # Conjuctions 166 | Author.where(:name).contains("Emily").and.contains("Dickinson") 167 | Author.where(:name).begins_with("E").or(:pub_count).eq(1) 168 | Post.where(:date).ge(start_date).and.le(end_date) # gt, ge (greater or equal), lt, le (less or equal) 169 | 170 | # Nested Conjuctions 171 | Author.where(:name).contains("Emily").and(cdq(:pub_count).gt(100).or.lt(10)) 172 | 173 | # Relationships 174 | Author.first.publications.offset(2).limit(1) 175 | cdq(emily_dickinson).publications.where(:type).eq('poetry') 176 | 177 | # Sorting 178 | Author.sort_by(:name) 179 | Author.sort_by(:name, order: :descending, case_insensitive: true) 180 | ``` 181 | 182 | ## Scopes 183 | 184 | ```ruby 185 | class Author < CDQManagedObject 186 | scope :prolific, where(:pub_count).gt(50) 187 | end 188 | ``` 189 | 190 | ## Operators 191 | 192 | Many short-form operators also have verbose equivalents. 193 | 194 | ``` 195 | eq (equal) 196 | ne (not_equal) 197 | lt (less_than) 198 | le (less_than_or_equal) 199 | gt (greater_than) 200 | ge (greater_than_or_equal) 201 | contains 202 | matches 203 | in 204 | begins_with 205 | ends_with 206 | between 207 | ``` 208 | 209 | ## Accessors 210 | 211 | These methods pull you out of CDQ-land and return actual objects or values. 212 | They go at the end of a query or scope and will force execution. 213 | 214 | ``` 215 | array (to_a) 216 | first 217 | map 218 | each 219 | [] 220 | ``` 221 | 222 | -------------------------------------------------------------------------------- /docs/cookbook/networking.md: -------------------------------------------------------------------------------- 1 | RedPotion uses the following for networking: 2 | 3 | * AFNetworking pod 4 | * AFMotion gem 5 | * SDWebImage 6 | 7 | ------- 8 | 9 | ## Remote images 10 | 11 | You can set `remote_image` to a URL string or an instance of `NSURL` and it will automatically fetch the image and set the image (with caching) using the power of [SDWebImage](https://github.com/rs/SDWebImage). 12 | 13 | You should always use `remote_image` for any image you download, it will cache it to memory and out to disk when it's appropriate. It does it async and is performant, even for a large table of images. 14 | 15 | ```ruby 16 | class MyStylesheet < ApplicationStylesheet 17 | def my_ui_image_view(st) 18 | # placeholder_image= is just an alias to image= 19 | # Set the placeholder image you want from your resources directory 20 | st.placeholder_image = image.resource("my_placeholder") 21 | # Set the remote URL. It will be applied to the UIImageView 22 | # when downloaded or retrieved from the local cache. 23 | st.remote_image = "http://www.rubymotion.com/img/rubymotion-logo.png" 24 | # or st.remote_image = NSURL.urlWithString(...) 25 | end 26 | end 27 | ``` 28 | 29 | To assign a remote image to a UIImageView: 30 | 31 | ```ruby 32 | your_ui_image_view.remote_image = "http://bit.ly/18iMhwc" 33 | ``` 34 | 35 | ------- 36 | 37 | ## Get HTML or JSON directly 38 | 39 | 40 | ```ruby 41 | AFMotion::HTTP.get("http://google.com") do |result| 42 | mp result.body 43 | end 44 | 45 | AFMotion::JSON.get("http://jsonip.com") do |result| 46 | mp result.object["ip"] 47 | end 48 | ``` 49 | 50 | ------- 51 | 52 | ## Setup a session client to reuse (best practice) 53 | 54 | ```ruby 55 | @client = AFMotion::SessionClient.build("https://alpha-api.app.net/") do 56 | session_configuration :default 57 | 58 | header "Accept", "application/json" 59 | 60 | response_serializer :json 61 | end 62 | ``` 63 | 64 | Then use it: 65 | 66 | ```ruby 67 | @client.get("stream/0/posts/stream/global") do |result| 68 | # result.operation is the AFURLConnectionOperation instance 69 | mp result.operation.inspect 70 | mp result.status_code 71 | 72 | if result.success? 73 | # result.object depends on the type of operation. 74 | # For JSON and PLIST, this is usually a Hash. 75 | # For XML, this is an NSXMLParser 76 | # For HTTP, this is an NSURLResponse 77 | # For Image, this is a UIImage 78 | p result.object 79 | 80 | elsif result.failure? 81 | # result.error is an NSError 82 | mp result.error.localizedDescription 83 | end 84 | end 85 | ``` 86 | 87 | The client support methods of the form `Client#get/post/put/patch/delete(url, request_parameters)`. The `request_parameters` is a hash containing your parameters to attach as the request body or URL parameters, depending on request type. For example: 88 | 89 | ```ruby 90 | @client.get("users", id: 1) do |result| 91 | ... 92 | end 93 | 94 | @client.post("users", name: "@clayallsopp", library: "AFMotion") do |result| 95 | ... 96 | end 97 | ``` 98 | 99 | #### Multipart Requests 100 | 101 | The session supports multipart form requests (i.e. for image uploading) - simply use `multipart_post` and it'll convert your parameters into properly encoded multipart data. For all other types of request data, use the `form_data` object passed to your callback: 102 | 103 | ```ruby 104 | # an instance of UIImage 105 | image = my_function.get_image 106 | data = UIImagePNGRepresentation(image) 107 | 108 | @client.multipart_post("avatars") do |result, form_data| 109 | if form_data 110 | # Called before request runs 111 | # see: https://github.com/AFNetworking/AFNetworking/wiki/AFNetworking-FAQ 112 | form_data.appendPartWithFileData(data, name: "avatar", fileName:"avatar.png", mimeType: "image/png") 113 | elsif result.success? 114 | ... 115 | else 116 | ... 117 | end 118 | end 119 | ``` 120 | 121 | This is an instance of [`AFMultipartFormData`](http://cocoadocs.org/docsets/AFNetworking/2.0.0/Protocols/AFMultipartFormData.html). 122 | 123 | If you want to track upload progress, you can add a third callback argument which returns the upload percentage between 0.0 and 1.0: 124 | 125 | ```ruby 126 | @client.multipart_post("avatars") do |result, form_data, progress| 127 | if form_data 128 | # Called before request runs 129 | # see: https://github.com/AFNetworking/AFNetworking/wiki/AFNetworking-FAQ 130 | form_data.appendPartWithFileData(data, name: "avatar", fileName:"avatar.png", mimeType: "image/png") 131 | elsif progress 132 | # 0.0 < progress < 1.0 133 | my_widget.update_progress(progress) 134 | else 135 | ... 136 | end 137 | ``` 138 | 139 | #### Headers 140 | 141 | You can set default HTTP headers using `@client.headers`, which is sort of like a `Hash`: 142 | 143 | ```ruby 144 | @client.headers["Accept"] 145 | #=> "application/json" 146 | 147 | @client.headers["Accept"] = "something_else" 148 | #=> "application/something_else" 149 | 150 | @client.headers.delete "Accept" 151 | #=> "application/something_else" 152 | ``` 153 | 154 | #### Client Building DSL 155 | 156 | The session client DSLs allows the following properties: 157 | 158 | - `header(header, value)` 159 | - `authorization(username: ___, password: ____)` for HTTP Basic auth, or `authorization(token: ____)` for Token based auth. 160 | - `request_serializer(serializer)`. Allows you to set an [`AFURLRequestSerialization`](http://cocoadocs.org/docsets/AFNetworking/2.0.0/Protocols/AFURLRequestSerialization.html) for all your client's requests, which determines how data is encoded on the way to the server. So if your API is always going to be JSON, you should set `operation(:json)`. Accepts `:json` and `:plist`, or any instance of `AFURLRequestSerialization` and must be called before calling `header` or `authorization` or else the [headers will not be applied](https://github.com/clayallsopp/afmotion/issues/78). 161 | - `response_serializer(serializer)`. Allows you to set an [`AFURLResponseSerialization`](http://cocoadocs.org/docsets/AFNetworking/2.0.0/Protocols/AFURLResponseSerialization.html), which determines how data is decoded once the server respnds. Accepts `:json`, `:xml`, `:plist`, `:image`, `:http`, or any instance of `AFURLResponseSerialization`. 162 | - `session_configuration(session_configuration, identifier = nil)`. Allows you to set the [`NSURLSessionConfiguration`](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionConfiguration_class/Reference/Reference.html#//apple_ref/occ/cl/NSURLSessionConfiguration). Accepts `:default`, `:ephemeral`, `:background` (with the `identifier` as a String), or an instance of `NSURLSessionConfiguration`. 163 | 164 | You can also configure your client by passing it as a block argument: 165 | 166 | ```ruby 167 | @client = AFMotion::SessionClient.build("https://alpha-api.app.net/") do |client| 168 | client.session_configuration :default 169 | 170 | client.header "Accept", @custom_header 171 | end 172 | ``` 173 | 174 | See all of AFMotion's [docs here](https://github.com/clayallsopp/afmotion). 175 | -------------------------------------------------------------------------------- /lib/project/pro_motion/data_table.rb: -------------------------------------------------------------------------------- 1 | module ProMotion 2 | module DataTable 3 | 4 | include TableClassMethods 5 | include ProMotion::Styling 6 | include ProMotion::Table 7 | include ProMotion::TableBuilder 8 | include ProMotion::TableDataBuilder 9 | include ProMotion::Table::Utils 10 | 11 | include ProMotion::Table::Searchable 12 | include ProMotion::Table::Refreshable 13 | include ProMotion::Table::Indexable 14 | include ProMotion::Table::Longpressable 15 | 16 | def table_view 17 | self.view 18 | end 19 | 20 | # This has to be defined in order to inherit everything from TableScreen 21 | def table_data 22 | [{cells:[]}] 23 | end 24 | 25 | def screen_setup 26 | set_up_fetch_controller 27 | 28 | set_up_header_footer_views 29 | set_up_searchable 30 | set_up_refreshable 31 | set_up_longpressable 32 | set_up_row_height 33 | end 34 | 35 | def set_up_fetch_controller 36 | error_ptr = Pointer.new(:object) 37 | fetch_controller.delegate = self 38 | 39 | unless fetch_controller.performFetch(error_ptr) 40 | raise "Error performing fetch: #{error_ptr[2].description}" 41 | end 42 | end 43 | 44 | def update_table_data(notification = nil) 45 | if notification.nil? 46 | table_view.reloadData 47 | else 48 | Dispatch::Queue.main.async do 49 | fetch_controller.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification) 50 | end 51 | end 52 | end 53 | 54 | def fetch_scope 55 | @_fetch_scope ||= begin 56 | if respond_to?(:model_query) 57 | data_with_scope = model_query 58 | else 59 | data_with_scope = data_model.send(data_scope) 60 | end 61 | 62 | if data_with_scope.sort_descriptors.blank? 63 | # Try to be smart about how we sort things if a sort descriptor doesn't exist 64 | attributes = data_model.send(:attribute_names) 65 | sort_attribute = nil 66 | [:updated_at, :created_at, :id].each do |att| 67 | sort_attribute ||= att if attributes.include?(att.to_s) 68 | end 69 | 70 | if sort_attribute 71 | 72 | unless data_scope == :all 73 | mp "The `#{data_model}` model scope `#{data_scope}` needs a sort descriptor. Add sort_by(:property) to your scope. Currently sorting by :#{sort_attribute}.", force_color: :yellow 74 | end 75 | data_model.send(data_scope).sort_by(sort_attribute) 76 | else 77 | # This is where the application says goodbye and dies in a fiery crash. 78 | mp "The `#{data_model}` model scope `#{data_scope}` needs a sort descriptor. Add sort_by(:property) to your scope.", force_color: :yellow 79 | end 80 | else 81 | data_with_scope 82 | end 83 | end 84 | end 85 | 86 | def fetch_controller 87 | if searching? 88 | search_fetch_controller 89 | else 90 | @fetch_controller ||= NSFetchedResultsController.alloc.initWithFetchRequest( 91 | fetch_scope.fetch_request, 92 | managedObjectContext: fetch_scope.context, 93 | sectionNameKeyPath: nil, 94 | cacheName: nil 95 | ) 96 | end 97 | end 98 | 99 | def on_cell_created(cell, data) 100 | # Do not call super here 101 | self.rmq.build(cell) 102 | end 103 | 104 | def cell_at(args = {}) 105 | index_path = args.is_a?(Hash) ? args[:index_path] : args 106 | c = object_at_index(index_path).cell 107 | set_data_cell_defaults(c) 108 | end 109 | 110 | def object_at_index(i) 111 | fetch_controller.objectAtIndexPath(i) 112 | end 113 | 114 | def data_model 115 | self.class.data_model 116 | end 117 | 118 | def data_scope 119 | self.class.data_scope 120 | end 121 | 122 | def searching? 123 | @_data_table_searching || false 124 | end 125 | 126 | def search_string 127 | @_data_table_search_string 128 | end 129 | 130 | def original_search_string 131 | search_string 132 | end 133 | 134 | def self.included(base) 135 | base.extend(TableClassMethods) 136 | end 137 | 138 | # UITableViewDelegate methods 139 | def numberOfSectionsInTableView(_) 140 | fetch_controller.sections.count 141 | end 142 | 143 | def tableView(table_view, numberOfRowsInSection: section) 144 | fetch_controller.sections[section].numberOfObjects 145 | end 146 | 147 | def tableView(table_view, didSelectRowAtIndexPath: index_path) 148 | data_cell = cell_at(index_path: index_path) 149 | table_view.deselectRowAtIndexPath(index_path, animated: true) unless data_cell[:keep_selection] == true 150 | trigger_action(data_cell[:action], data_cell[:arguments], index_path) if data_cell[:action] 151 | end 152 | 153 | def tableView(_, cellForRowAtIndexPath: index_path) 154 | params = index_path_to_section_index(index_path: index_path) 155 | data_cell = cell_at(index_path: index_path) 156 | return self.rmq.create(UITableViewCell) unless data_cell 157 | 158 | create_table_cell(data_cell) 159 | end 160 | 161 | def tableView(_, willDisplayCell: table_cell, forRowAtIndexPath: index_path) 162 | data_cell = cell_at(index_path: index_path) 163 | table_cell.send(:will_display) if table_cell.respond_to?(:will_display) 164 | table_cell.send(:restyle!) if table_cell.respond_to?(:restyle!) # Teacup compatibility 165 | end 166 | 167 | def tableView(table_view, heightForRowAtIndexPath: index_path) 168 | (object_at_index(index_path).cell[:height] || table_view.rowHeight).to_f 169 | end 170 | 171 | def controllerWillChangeContent(controller) 172 | # TODO - we should update the search results table when a new record is added 173 | # or deleted or changed. For now, when the data changes, the search doesn't 174 | # update. Closing the search will update the data and then searching again 175 | # will show the new or changed content. 176 | table_view.beginUpdates unless searching? 177 | end 178 | 179 | def controller(controller, didChangeObject: task, atIndexPath: index_path, forChangeType: change_type, newIndexPath: new_index_path) 180 | unless searching? 181 | case change_type 182 | when NSFetchedResultsChangeInsert 183 | table_view.insertRowsAtIndexPaths([new_index_path], withRowAnimation: UITableViewRowAnimationAutomatic) 184 | when NSFetchedResultsChangeDelete 185 | table_view.deleteRowsAtIndexPaths([index_path], withRowAnimation: UITableViewRowAnimationAutomatic) 186 | when NSFetchedResultsChangeUpdate 187 | table_view.reloadRowsAtIndexPaths([index_path], withRowAnimation: UITableViewRowAnimationAutomatic) 188 | end 189 | end 190 | end 191 | 192 | def controllerDidChangeContent(controller) 193 | table_view.endUpdates unless searching? 194 | end 195 | 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /docs/cookbook/layout_a_screen.md: -------------------------------------------------------------------------------- 1 | This example applies layouts in the stylesheet (thus why it's st.frame =), but you could also apply them using the .layout method. 2 | 3 | rect_example 4 | 5 | Notice the use of :left and :from_right together, instead of :width. This is very handy. For example if you want to place your view inside its superview (parent) and give a 5 pixel border, you would do this: 6 | 7 | ```ruby 8 | st.frame = {l: 5, t: 5, fr: 5, fb: 5} 9 | ``` 10 | 11 | 8, 9, and 10 are interesting. I placed 8 on the bottom, then put 9 above_prev: 5, and then 10 above_prev: 5. You could do this differently, but that's pretty slick. 12 | 13 | 14 | layout 15 | 16 | ## Techniques for dealing with multiple size devices 17 | 18 | ### Develop the app using the smallest device first 19 | 20 | You should build the app and do the layout on the smallest size device, which is usually an iPhone 4s or iPhone 5. 21 | 22 | When I design, I take the smaller sizes, then find views that I want to expand on larger devices. For example if you're designing a **compose email** screen, the part that you could expand would be the body of the email. Making everything just larger is poor design. The user has a bigger device to see more data and UI, not make it bigger (although there is a zoom mode, but that just makes the device have the same resolution as a smaller device. So this has no effect on how you'd do layout) 23 | 24 | If designed correctly, then the larger sizes will mostly work automatically (assuming you're using the relative layout features (such as :from_right and :below_prev). 25 | 26 | Lastly you can do small tweaks for certain size devices. 27 | 28 | Some designers will design every size exactly. In this case I'd still do the smallest first. 29 | 30 | ### 31 | 32 | In your stylesheets you can use the following to make tweaks for different devices: 33 | 34 | * `ipad?` 35 | * `iPhone?` 36 | * `device.width` screen width, orientation doesn't matter 37 | * `device.height` screen height, orientation doesn't matter 38 | * `three_point_five_inch?` 39 | * `four_inch?` 40 | * `four_point_seven_inch?` 41 | * `five_point_five_inch?` 42 | * `landscape?` 43 | * `portrait?` 44 | 45 | Here is an example in a stylesheet: 46 | 47 | ``` 48 | st.frame = {t: 10, bp: 10, fr: 10, h: four_inch? 150 : 180} 49 | ``` 50 | 51 | Or you can have completely different hashes per size and/or orientation: 52 | 53 | ``` 54 | st.frame = if four_inch? 55 | {left: 10, below_prev: 10, from_right: 10, height: 150} 56 | elsif four_point_seven_inch? 57 | {left: 15, below_prev: 15, from_right: 15, height: 150} 58 | else 59 | {left: 18, below_prev: 18, from_right: 18, height: 150} 60 | end 61 | ``` 62 | 63 | If you're not in your stylesheet, then you can get to these in `device`, for example: `device.iPad?`. 64 | 65 | 66 | 67 | ## Update a view(s) frame or bounds 68 | 69 | You can update the rectangle of a view in various ways, each way can take any of the types below (hash, array, CGRect, etc): 70 | 71 | ```ruby 72 | # In style 73 | st.frame = {l: 10, t: 80, w: 50, h: 20} 74 | 75 | # One or more views using .layout 76 | find(view_1, view_2).layout(l: 10, t: 80, w: 50, h: 20) 77 | append(UIButton, :button_style).layout(l: 10, t: 80, w: 50, h: 20) 78 | 79 | # One or more views using .frame 80 | find(view_1).frame = {l: 10, t: 80, w: 50, h: 20} 81 | 82 | # Using move or resize 83 | find(view_1).move(l: 10, t: 80) 84 | find(view_1).resize(w: 10, h: 80) 85 | 86 | # Animate it 87 | find(my_view_1).animate{|q| q.move(l: 80)} 88 | ``` 89 | 90 | You can update a frame or bounds with the following: 91 | 92 | * hash (**preferred**) 93 | * another RubyMotionQuery::Rect 94 | * array 95 | * array of array 96 | * CGPoint 97 | * CGSize 98 | * CGRect 99 | 100 | --- 101 | 102 | #### Hash 103 | 104 | The params are always applied in this order, regardless of the hash order: 105 | 106 | 1. grid 107 | 1. l, t, w, h 108 | 1. previous 109 | 1. from_right, from_bottom 110 | 1. right, bottom 111 | 1. left and from_right applied together (will change width) 112 | 1. top and from_bottom applied together (will change height) 113 | 114 | ##### Options for layout, move, size, frame, & bounds: 115 | 116 | * :full 117 | * :left :l 118 | * :right :r 119 | * :from_right :fr 120 | * :top :t 121 | * :bottom :b 122 | * :from_bottom :fb 123 | * :width :w 124 | * :height :h 125 | * :right_of_prev :rop 126 | * :left_of_prev :lop 127 | * :below_prev :bp 128 | * :above_prev :ap 129 | * :centered (:vertical, :horizontal, :both) 130 | 131 | #### Options for nudge: 132 | 133 | * :left :l 134 | * :right :r 135 | * :up :u 136 | * :down :d 137 | 138 | #### Values for each option can be: 139 | 140 | * signed integer 141 | * integer 142 | * float 143 | * string 144 | 145 | #### Strings are for the grid: 146 | 147 | * 'a1:b4' 148 | * 'a1' 149 | * 'a' 150 | * '1' 151 | * ':b4' 152 | 153 | #### Previous 154 | 155 | You can use `:below_prev: 10`, `right_of_prev: 20`, etc. **Prev** refers to the **previous sibling of this view**. 156 | 157 | #### Some examples 158 | 159 | ```ruby 160 | st.frame = {l: 10, t: 20, w: 100, h: 150} 161 | st.frame = {t: 20, h: 150, l: 10, w: 100} 162 | find(my_view).layout(l: 10, t: 20) 163 | find(my_view).move(h: 20) 164 | find(my_view).layout(l: :prev, t: 20, w: 100, h: 150) 165 | find(my_view).frame = {l: 10, below_prev: 10, w: prev, h: 150} 166 | find(my_view).frame = {left: 10, top: 20, width: 100, height: 150} 167 | find(my_view).frame = {l: 10, t: 10, fr: 10, fb: 10} 168 | find(my_view).frame = {width: 50, height: 20, centered: :both} 169 | find(my_view, my_other_view).frame = {grid: "b2", w: 100, h: 200} 170 | ``` 171 | 172 | ```ruby 173 | # In a style 174 | def some_style(st) 175 | st.frame = {l: 20, t: 20, w: 100, h: 50} 176 | end 177 | 178 | # Layout, move, or resize selected views 179 | find(your_view).layout(left: 20, top: 20, width: 100, height: 50) 180 | find(your_view).layout(l: 20) 181 | find(your_view).layout(left: 20) 182 | find(your_view).layout(l: 20, t: 20, r: 20, b: 20) 183 | find(your_view).layout(left: 20, top: 20, width: 100, height: 50) 184 | 185 | find(your_view).move(left: 20) # alias for layout 186 | find(your_view).move(l: 30, t: 50) # alias for layout 187 | find(your_view).resize(width: 100, height: 50) # alias for layout 188 | 189 | # Nudge pushes them in a direction 190 | find(your_view).nudge(d: 20) 191 | find(your_view).nudge(down: 20) 192 | find(your_view).nudge(l: 20, r: 20, u: 100, d: 50) 193 | find(your_view).nudge(left: 20, right: 20, up: 100, down: 50) 194 | 195 | # Other 196 | find(your_view).resize_to_fit_subviews 197 | find(your_view).resize_content_to_fit_subviews 198 | ``` 199 | 200 | ### Getting the Rect for a view or views 201 | 202 | ```ruby 203 | rect = find(view).frame 204 | rect.log 205 | puts rect 206 | puts rect.left 207 | 208 | # Use it directly 209 | find(my_view).move(l: find(my_other_view).frame.l) 210 | 211 | # If you select more than one view, you'l get an array of Rect objects 212 | a = find(UIView).frame 213 | ``` 214 | 215 | # Distributing views 216 | 217 | ```ruby 218 | find(UIButton).distribute 219 | find(UIButton).distribute(:vertical) 220 | find(UIButton).distribute(:horizontal) 221 | find(UIButton).distribute(:vertical, margin: 20) 222 | find(my_view, my_other_view, third_view).distribute(:vertical, margin: 10) 223 | find(UIButton).distribute(:vertical, margins: [5,5,10,5,10,5,10,20]) 224 | ``` 225 | -------------------------------------------------------------------------------- /docs/cookbook/validations.md: -------------------------------------------------------------------------------- 1 | RedPotion allows a RubyMotion validation library to help you to validate data in a way that makes life easy. 2 | 3 | There are two main aspects, so you can utilize RMQ validation to meet your needs: 4 |
    5 |
  • Validation Utility - Simple validation methods
  • 6 |
  • RMQ Validation Selection Rules - Dynamically apply validation rules to input
  • 7 |
8 | QUICK EXAMPLES: 9 | ```ruby 10 | # Examples of the Utility 11 | rmq.validation.valid?('https://www.infinitered.com', :url) #true 12 | rmq.validation.valid?(98.6, :number) #true 13 | 14 | # Examples of Selection Rules 15 | append(UITextField, :user).validates(:email) 16 | append(UITextField, :password).validates(:strong_password) 17 | append(UITextField, :pin).validates(:digits).validates(:length, exact_length: 5) 18 | 19 | find(UITextField).valid? # checks if selected is valid 20 | find(:password).clear_validations! #removes validations on selected 21 | ``` 22 | 23 | All validations are tied to RMQ Debugging mode. So you can turn them off in the application by entering debug mode. You can do so by starting your project with the flag set to true `rake rmq_debug=true` OR by setting the flag in the code/REPL with `RubyMotionQuery::RMQ.debugging = true`. 24 | 25 | This allows you to quickly disable validation in your entire application during debugging. 26 |

RMQ Validation Utility

27 | RMQ Comes with a simple utility to validate common forms of data input. Each validation type can support options specific to that validation rule. 28 | 29 | **The following validation types are baked in for the utility:** 30 |
    31 |
  • :email - Email address
  • 32 |
  • :url - URL (including http(s))
  • 33 |
  • :dateiso - ISO Date e.g. '2014-03-02'
  • 34 |
  • :number - Any Real number e.g. 62.5
  • 35 |
  • :digits - Any Natural numbers e.g. 65
  • 36 |
  • :ipv4 - IPV4 addresses
  • 37 |
  • :time - Military time
  • 38 |
  • :uszip - US Zip code with optional extended 4
  • 39 |
  • :ukzip - UK postal code
  • 40 |
  • :usphone - US 7 OR 10 digit phone number with dots, dashes, or spaces for separators
  • 41 |
  • :strong_password - Any string of at least 8 characters comprised of numbers, uppercase, and lowercase input
  • 42 |
  • :has_upper - True if any uppercase letters
  • 43 |
  • :has_lower - True if any lowercase letters
  • 44 |
  • :presence - True with any non-whitespace input
  • 45 |
  • :length - Allows you to validate length restrictions on input
  • 46 |
  • :custom - Validates using whatever is passed in the `regex` parameter
  • 47 |
48 | Most of these are built from a collection of `regular expressions` that are fed into a `regex_match?` method. You can send a pull-request or use the `custom` rule to handle your own validations. 49 | ```ruby 50 | # examples 51 | rmq.validation.valid?('test@test.com', :email) # returns true 52 | 53 | rmq.validation.valid?('5045558989', :usphone) # returns true 54 | 55 | rmq.validation.valid?('K1A 0B1', :uszip) # returns false 56 | 57 | # Length takes a wide selection of input to facilitate your length validation needs 58 | rmq.validation.valid?('test', :length, exact_length: 4) #true 59 | rmq.validation.valid?('test', :length, min_length: 4) #true 60 | rmq.validation.valid?('test', :length, max_length: 4) #true 61 | # ranges 62 | rmq.validation.valid?('test', :length, min_length: 2, max_length: 7) #true 63 | rmq.validation.valid?('test', :length, exact_length: 2..7) #true 64 | # strip whitespace 65 | rmq.validation.valid?(' test ', :length, max_length: 5, strip: true) #true 66 | 67 | # roll your own validation rule 68 | rmq.validation.valid?('nacho', :custom, regex: Regexp.new('nachos?')) #true (could also do /nachos?/ notation 69 | ``` 70 | 71 | ### Universal Options 72 | Some validation optinos are unique to that particular validation rule, but all validation rules have the following Universal Options: 73 | #### allow_blank 74 | By default all these tools are strict, but you can set the `allows_blank` option to true, which will cause a particular validation to return true IF it the input is blank. This can be useful in situations where data is optional. 75 |
e.g. Only validate weight is numeric if they entered a value for it.
76 | #### white_list 77 | In some situations you may have an outlying exception to the validation rule. Exceptions can be validated as true using the `white_list` parameter: 78 | ```ruby 79 | rmq.validation.valid?(some_url_input, :url, white_list: ['http://localhost:8080', 'http://localhost:3000']) 80 | # => true for 'http://localhost:3000' even though it's not going to pass URL (missing TLD) 81 | ``` 82 |

RMQ Validation Selection Rules

83 | For a more robust use of RMQ and validation, there's an arsenal of RMQ integrated validation options and events. ANY utility rule can be used in a selection validation. Simply chain the validation you'd like applied to the UIViews you've selected with RMQ. For example: 84 | ```ruby 85 | # attach validation rules to UIViews 86 | append(UITextField, :weight).validates(:digits) 87 | append(UITextField, :name).validates(:presence) 88 | 89 | # Later, you can check these fields to see if they have data that fits their associated validations 90 | find(UITextField).valid? 91 | ``` 92 | Using the selection rules gets you a lot of validation help. 93 | ```ruby 94 | # Get a list of all views that were valid/invalid (updates on every .valid? call) 95 | find(some_selection_criteria).invalid # returns all invalid views in the selected 96 | find(some_selection_criteria).valid # returns all valid views in the selected 97 | 98 | # you can remove validations just as easily 99 | find(some_selection).clear_validations! 100 | ``` 101 | You can attach and fire events on the UIViews in a block. 102 | ```ruby 103 | append(UITextField).validates(:digits). 104 | on(:valid) do |valid| 105 | puts "This field is valid" 106 | end. 107 | on(:invalid) do |invalid| 108 | puts "This field is invalid" 109 | end 110 | 111 | # The above will fire their events when validation gets called 112 | find.all.valid? 113 | # You can can call valid? during .on(:change) if you'd like your valid/invalid 114 | # blocks to run instantaneously with given input. 115 | ``` 116 | Also, you can even get friendly error messages 117 | ```ruby 118 | # start off with a validated input 119 | append(UITextField, :user).validates(:email) 120 | find.all.valid? #run validation check 121 | 122 | # Returns array of error messages for every invalid item in selection 123 | find.all.validation_errors 124 | # E.G ["Validation Error - input requires valid email."] 125 | ``` 126 | You can customize these error messages per validation rule by setting them in the stylesheet 127 | ```ruby 128 | # here in the stylesheet for this controller 129 | def user(st) 130 | st.validation_errors = { 131 | email: "Please check your user email, and try again" 132 | } 133 | end 134 | ``` 135 | 136 | ## How do I add my own validations? 137 | There are a few excellent ways to add your own validations. If you simply need to make exceptions for existing validations, consider looking into using `white_list`. If you need to use your own custom regex or validation code, then the following 2 methods are extremely useful. 138 | 139 | #### custom rule 140 | If your validation is specific to a single form, we suggest taking advantage of using the `custom` validation rule. 141 | ```ruby 142 | some_field = append(UITextField).validates(:custom, regex: /^test$/) 143 | some_field.data("test") 144 | some_field.valid? 145 | # => true 146 | ``` 147 | 148 | #### add_validator 149 | You can add your own validation with the `add_validator` method (Perhaps in your Application Stylesheet setup). Additionally, you're not limited to the bounds of a single regex. You can use the ruby methods you know and understand, as well as multiple parameters passed in `opts`. 150 | ```ruby 151 | rmq.validation.add_validator(:start_with) do |value, opts| 152 | value.start_with?(opts[:prefix]) 153 | end 154 | ``` 155 | You can then use your new validator in your application: 156 | ```ruby 157 | some_field = append(UITextField).validates(:start_with, prefix: 'x') 158 | some_field.data("test") 159 | some_field.valid? # => false 160 | some_field.data("xenophobia") 161 | some_field.valid? # => true 162 | ``` 163 | 164 | *If you find yourself needing a particular validation rule often, please patch back to RMQ's default rules with a Pull Request!* 165 | -------------------------------------------------------------------------------- /bin/potion: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "erb" 4 | require "rubygems" 5 | require 'open-uri' 6 | require_relative "../lib/project/version" 7 | 8 | class PotionCommandLine 9 | 10 | HELP_TEXT = %{ potion version #{RedPotion::VERSION} 11 | 12 | Some things you can do with the potion command: 13 | 14 | > potion new my_new_app 15 | > potion new my_new_app --skip-cdq # Setup application without CDQ 16 | > potion new my_new_app --skip-afmotion # Setup application without afmotion 17 | 18 | > potion g model foo 19 | > potion g screen foo 20 | > potion g table_screen foo 21 | > potion g table_screen_cell bar_cell 22 | > potion g metal_table_screen foo 23 | > potion g collection_view_screen 24 | > potion g view bar 25 | > potion g shared some_class_used_app_wide 26 | > potion g lib some_class_used_by_multiple_apps 27 | 28 | You can still create controllers, but you should create screens instead 29 | > potion g controller foos 30 | > potion g collection_view_controller foos 31 | > potion g table_view_controller bars 32 | 33 | You can remove CDQ or afmotion if your project does not use it 34 | > potion remove cdq 35 | > potion remove afmotion 36 | 37 | Misc 38 | > potion -h, --help 39 | > potion -v, --version 40 | 41 | Documentation 42 | > rmq docs 43 | > rmq docs query 44 | > rmq docs "background image" } 45 | 46 | class << self 47 | 48 | SKIP_FLAGS = [ "--skip-cdq", "--skip-afmotion" ] 49 | 50 | VALID_CREATE_TYPES = [ 51 | :screen, :table_screen, 52 | :table_screen_cell, 53 | :model, :controller, 54 | :metal_table_screen, 55 | :collection_view_screen, 56 | :view, 57 | :shared, 58 | :lib, 59 | :collection_view_controller, 60 | :table_view_controller 61 | ] 62 | 63 | def new(app_name, *options) 64 | if app_name.nil? 65 | puts "potion - Invalid command, try adding an application name. (ex: potion new my_app)\n" 66 | return 67 | end 68 | 69 | options.compact! 70 | #ensure_template_dir 71 | create_app(app_name, options) 72 | end 73 | 74 | def generate(template, *options) 75 | name = options.slice!(0) 76 | if VALID_CREATE_TYPES.include?(template.to_sym) 77 | insert_from_template(template, name) 78 | else 79 | puts "potion - Invalid command, do something like this: potion g controller my_controller\n" 80 | end 81 | end 82 | 83 | def include_skip_option?(options) 84 | (options.first && options.first.include?('--')) 85 | end 86 | 87 | def create(template_or_app_name, *options) 88 | # Dry Run option - TODO - change this to --dry_run to streamline 89 | if options.first == 'dry_run' 90 | @dry_run = true 91 | options.slice!(0) 92 | end 93 | 94 | if options.compact.empty? || include_skip_option?(options) 95 | new(template_or_app_name, *options) 96 | else 97 | generate(template_or_app_name, *options) 98 | end 99 | end 100 | 101 | def create_app(app_name, options) 102 | puts ' Creating app' 103 | 104 | `motion create --template=https://github.com/infinitered/redpotion-template.git #{app_name}` 105 | 106 | options.each do |option| 107 | if SKIP_FLAGS.include?(option) 108 | Dir.chdir("./#{app_name}") 109 | remove(option.split('-').last, true) 110 | Dir.chdir('..') 111 | end 112 | end 113 | 114 | ["bundle", "rake pod:install"].each do |command| 115 | puts "Running #{command}..." 116 | 117 | system("cd #{app_name}; #{command}") 118 | end 119 | 120 | puts %{ 121 | Complete. Things you can do: 122 | > rake spec 123 | > rake 124 | (main)> exit 125 | 126 | Then try these: 127 | > rake device_name='iPhone 4s' 128 | > rake device_name='iPhone 5s' 129 | > rake device_name='iPhone 5s' target=7.1 130 | > rake device_name='iPhone 6 Plus' 131 | > rake device_name='iPad Retina' 132 | > rake device 133 | 134 | Or for XCode 5.1 135 | > rake retina=3.5 136 | > rake retina=4 137 | > rake device_family=ipad} 138 | 139 | end 140 | 141 | def template_dir 142 | "#{Dir.home}/Library/RubyMotion/template/potion-template" 143 | end 144 | 145 | # TODO Disabling for now, this needs to get latest template 146 | def ensure_template_dir 147 | return if Dir.exists?(template_dir) 148 | `git clone https://github.com/infinitered/redpotion-template.git #{template_dir}` 149 | end 150 | 151 | def template_path(template_name) 152 | sub_path = "templates/#{template_name}/" 153 | 154 | # First check local directory, use that if it exists 155 | if Dir.exist?("#{Dir.pwd}/#{sub_path}") 156 | "#{Dir.pwd}/#{sub_path}" 157 | else # Then check the gem 158 | begin 159 | spec = Gem::Specification.find_by_name("redpotion") 160 | gem_root = spec.gem_dir 161 | "#{gem_root}/#{sub_path}" 162 | rescue Exception => e 163 | puts "potion - could not find template directory\n" 164 | nil 165 | end 166 | end 167 | end 168 | 169 | def insert_from_template(template_name, name) 170 | # TODO refactor this, it's less than wonderful 171 | if %w{metal_table_screen view collection_view_screen table_screen_cell}.include? template_name 172 | # Do nothing 173 | elsif template_name =~ /.*screen/ 174 | @screen_base = template_name.split('_').collect(&:capitalize).join 175 | template_name = 'screen' 176 | elsif template_name == "model" && cdq_included? 177 | puts `cdq create model #{name}` 178 | return 179 | else 180 | # we dont have templates for these fallback to RMQ 181 | puts `rmq create #{template_name} #{name}` 182 | return 183 | end 184 | 185 | puts "\n Creating #{template_name}: #{name}\n\n" 186 | 187 | return unless (@template_path = template_path(template_name)) 188 | files = Dir["#{@template_path}**/*"].select {|f| !File.directory? f} 189 | 190 | @name = name.gsub(/_?(screen|controller|stylesheet)/,'') 191 | @name_camel_case = @name.split('_').map{|word| word.capitalize}.join 192 | 193 | files.each do |template_file_path_and_name| 194 | @in_app_path = File.dirname(template_file_path_and_name).gsub(@template_path, '') 195 | @ext = File.extname(template_file_path_and_name) 196 | @file_name = File.basename(template_file_path_and_name, @ext) 197 | 198 | @new_file_name = @file_name.gsub('name', @name) 199 | @new_file_path_name = "#{Dir.pwd}/#{@in_app_path}/#{@new_file_name}#{@ext}" 200 | 201 | if @dry_run 202 | puts "\n Instance vars:" 203 | self.instance_variables.each{|var| puts " #{var} = #{self.instance_variable_get(var)}"} 204 | puts 205 | end 206 | 207 | if Dir.exist?(@in_app_path) 208 | puts " Using existing directory: #{@in_app_path}" 209 | else 210 | puts " \u0394 Creating directory: #{@in_app_path}" 211 | Dir.mkdir(@in_app_path) unless @dry_run 212 | end 213 | 214 | results = load_and_parse_erb(template_file_path_and_name) 215 | 216 | if File.exists?(@new_file_path_name) 217 | puts " X File exists, SKIPPING: #{@new_file_path_name}" 218 | else 219 | puts " \u0394 Creating file: #{@new_file_path_name}" 220 | File.open(@new_file_path_name, 'w+') { |file| file.write(results) } unless @dry_run 221 | end 222 | end 223 | 224 | puts "\n Done" 225 | end 226 | 227 | def load_and_parse_erb(template_file_name_and_path) 228 | template_file = File.open(template_file_name_and_path, 'r').read 229 | erb = ERB.new(template_file) 230 | erb.result(binding) 231 | end 232 | 233 | def remove(type, show_output=true) 234 | case type 235 | when 'cdq' 236 | remove_lines('Gemfile', /gem ('|")cdq('|")/, show_output) 237 | remove_lines('app/app_delegate.rb', /include CDQ|cdq.setup/, show_output) 238 | remove_lines('Rakefile', /schema:build/, show_output) 239 | File.delete('schemas/0001_initial.rb') if File.exists?('schemas/0001_initial.rb') 240 | File.delete('resources/cdq.yml') if File.exists?('resources/cdq.yml') 241 | File.delete('spec/helpers/cdq.rb') if File.exists?('spec/helpers/cdq.rb') 242 | Dir.delete('schemas') if Dir.exists?('schemas') 243 | when 'afmotion' 244 | remove_lines('Gemfile', /gem ('|")afmotion('|")/, show_output) 245 | else 246 | puts "potion - Invalid command option '#{type}', potion remove only works with cdq" 247 | end 248 | end 249 | 250 | def remove_lines(file, match, show_output) 251 | puts "Modifying #{file}" if show_output 252 | data = IO.readlines(file) 253 | File.open(file, 'w') do |f| 254 | data.each do |line| 255 | unless line =~ match 256 | f.puts line 257 | end 258 | end 259 | end 260 | end 261 | 262 | def cdq_included? 263 | return false unless File.exist?("Gemfile.lock") 264 | data = IO.readlines("Gemfile.lock") 265 | data.any?{|l| l.match(/\s.cdq\s\(/) } 266 | end 267 | 268 | end 269 | 270 | end 271 | 272 | # Process input, execute actions 273 | unless ARGV.length > 0 274 | puts "potion - Invalid command, do something like this: potion new my_new_app\n" 275 | puts PotionCommandLine::HELP_TEXT 276 | exit 277 | end 278 | 279 | action = ARGV[0] 280 | query = ARGV[1] 281 | 282 | case action 283 | when 'new' 284 | PotionCommandLine.new(ARGV[1], ARGV[2], ARGV[3]) 285 | when 'g', 'generate' 286 | PotionCommandLine.generate(ARGV[1], ARGV[2], ARGV[3]) 287 | when 'create' 288 | # create provided as an alias for backwards compatibility 289 | PotionCommandLine.create(ARGV[1], ARGV[2], ARGV[3]) 290 | when 'rmq_docs' 291 | if query 292 | query = URI::encode(query) 293 | url = "http://rubymotionquery.com?s=#{query}&post_type=document" 294 | `open #{url}` 295 | else 296 | `open http://rubymotionquery.com/documentation` 297 | end 298 | when 'remove' 299 | PotionCommandLine.remove(ARGV[1]) 300 | when '--help', '-h' 301 | puts PotionCommandLine::HELP_TEXT 302 | when '-v', '--version' 303 | puts RedPotion::VERSION 304 | else 305 | puts 'potion - Invalid action' 306 | puts PotionCommandLine::HELP_TEXT 307 | end 308 | -------------------------------------------------------------------------------- /spec/ext/ui_view_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'UIView' do 2 | 3 | it "should call on_load instead of rmq_build if rmq_build does not exist in the view" do 4 | rmq.create(TestView).get.on_loaded.should == true 5 | 6 | test_view = TestView.alloc.initWithFrame([[0,0],[10,10]]) 7 | test_view.on_loaded.should.be.nil 8 | rmq.build(test_view) 9 | test_view.on_loaded.should.be.true 10 | end 11 | 12 | it "should call on_styled instead of rmq_style_applied if rmq_style_applied does not exist in the view" do 13 | test_view = rmq.create!(TestView, :root_view) 14 | test_view.on_styled_fired.should.be.true 15 | test_view = TestView.alloc.initWithFrame([[0,0],[10,10]]) 16 | test_view.on_styled_fired.should.be.nil 17 | test_view.apply_style(:root_view) 18 | test_view.on_styled_fired.should.be.true 19 | end 20 | 21 | describe "#append" do 22 | before { @view = UIView.alloc.init } 23 | 24 | it "should return a RMQ object" do 25 | appended = @view.append(UIView, nil, {}) 26 | appended.is_a?(RubyMotionQuery::RMQ).should.be.true 27 | end 28 | 29 | it "should create a new object class provided" do 30 | appended = @view.append(UIView, nil, {}) 31 | appended.get.is_a?(UIView).should.be.true 32 | end 33 | 34 | it "inserts and then yields a block with the RMQ object" do 35 | block_called = false 36 | append = @view.append(UILabel, nil) do |mv| 37 | mv.should.be.kind_of(RubyMotionQuery::RMQ) 38 | mv.get.should.be.kind_of(UILabel) 39 | block_called = true 40 | end 41 | block_called.should == true 42 | 43 | end 44 | end 45 | 46 | describe "append!" do 47 | before { @view = UIView.alloc.init } 48 | 49 | it "should return the appended object" do 50 | appended = @view.append!(UIView, nil, {}) 51 | appended.is_a?(UIView).should.be.true 52 | end 53 | 54 | it "inserts and then yields a block with the created view" do 55 | block_called = false 56 | append = @view.append!(UILabel, nil) do |mv| 57 | mv.should.be.kind_of(UILabel) 58 | block_called = true 59 | end 60 | block_called.should == true 61 | end 62 | end 63 | 64 | describe "prepend" do 65 | before { @view = UIView.alloc.init } 66 | 67 | it "should return a RMQ object" do 68 | prepended = @view.prepend(UIView, nil, {}) 69 | prepended.is_a?(RubyMotionQuery::RMQ).should.be.true 70 | end 71 | 72 | it "should create a new object class provided" do 73 | prepended = @view.prepend(UIView, nil, {}) 74 | prepended.get.is_a?(UIView).should.be.true 75 | end 76 | 77 | it "prepends and then yields a block with the RMQ object" do 78 | block_called = false 79 | @view.prepend(UILabel, nil) do |mv| 80 | mv.should.be.kind_of(RubyMotionQuery::RMQ) 81 | mv.get.should.be.kind_of(UILabel) 82 | block_called = true 83 | end 84 | block_called.should == true 85 | end 86 | end 87 | 88 | describe "prepend!" do 89 | before { @view = UIView.alloc.init } 90 | 91 | it "should return the appended object" do 92 | prepended = @view.prepend!(UIView, nil, {}) 93 | prepended.is_a?(UIView).should.be.true 94 | end 95 | 96 | it "prepends and then yields a block with the created view" do 97 | block_called = false 98 | @view.prepend!(UILabel, nil) do |mv| 99 | mv.should.be.kind_of(UILabel) 100 | block_called = true 101 | end 102 | block_called.should == true 103 | end 104 | end 105 | 106 | describe "create" do 107 | before { @view = UIView.alloc.init } 108 | 109 | it "should return a RMQ object" do 110 | created = @view.create(UIView, nil, {}) 111 | created.is_a?(RubyMotionQuery::RMQ).should.be.true 112 | end 113 | 114 | it "should create a new object class provided" do 115 | created = @view.create(UIView, nil, {}) 116 | created.get.is_a?(UIView).should.be.true 117 | end 118 | 119 | it "creates and then yields a block with the RMQ object" do 120 | block_called = false 121 | @view.create(UILabel, nil) do |mv| 122 | mv.should.be.kind_of(RubyMotionQuery::RMQ) 123 | mv.get.should.be.kind_of(UILabel) 124 | block_called = true 125 | end 126 | block_called.should == true 127 | end 128 | end 129 | 130 | describe "create!" do 131 | before { @view = UIView.alloc.init } 132 | 133 | it "should return the appended object" do 134 | created = @view.create!(UIView, nil, {}) 135 | created.is_a?(UIView).should.be.true 136 | end 137 | 138 | it "creates and then yields a block with the created view" do 139 | block_called = false 140 | @view.create!(UILabel, nil) do |mv| 141 | mv.should.be.kind_of(UILabel) 142 | block_called = true 143 | end 144 | block_called.should == true 145 | end 146 | end 147 | 148 | describe "build" do 149 | before { @view = UIView.alloc.init } 150 | 151 | it "should return a RMQ object" do 152 | built = @view.build(UIView, nil, {}) 153 | built.is_a?(RubyMotionQuery::RMQ).should.be.true 154 | end 155 | 156 | it "should create a new object class provided" do 157 | built = @view.build(UIView, nil, {}) 158 | built.get.is_a?(UIView).should.be.true 159 | end 160 | 161 | it "builds and then yields a block with the RMQ object" do 162 | block_called = false 163 | existing_view = UILabel.new 164 | @view.build(existing_view) do |mv| 165 | mv.should.be.kind_of(RubyMotionQuery::RMQ) 166 | mv.get.should.be.kind_of(UILabel) 167 | mv.get.should == existing_view 168 | block_called = true 169 | end 170 | block_called.should == true 171 | end 172 | end 173 | 174 | describe "build!" do 175 | before { @view = UIView.alloc.init } 176 | 177 | it "should return the appended object" do 178 | built = @view.build!(UIView, nil, {}) 179 | built.is_a?(UIView).should.be.true 180 | end 181 | 182 | it "builds and then yields a block with the created view" do 183 | block_called = false 184 | @view.build!(UILabel) do |mv| 185 | mv.should.be.kind_of(UILabel) 186 | block_called = true 187 | end 188 | block_called.should == true 189 | end 190 | end 191 | 192 | describe "on" do 193 | before do 194 | @button = UIButton.alloc.init 195 | @button.on(:tap, {}) { } 196 | end 197 | 198 | it "should attach the event" do 199 | @button.rmq_data.events.has_event?(:tap).should.be.true 200 | end 201 | end 202 | 203 | describe "off" do 204 | before do 205 | @button = UIButton.alloc.init 206 | @button.on(:tap, {}) { } 207 | @button.on(:swipe, {}) { } 208 | @button.on(:value_changed, {}) { } 209 | end 210 | describe "removing a single event when many exist" do 211 | before do 212 | @button.off(:tap) 213 | end 214 | 215 | it "should detach the events" do 216 | @button.rmq_data.events.has_event?(:tap).should.be.false 217 | @button.rmq_data.events.has_event?(:swipe).should.be.true 218 | @button.rmq_data.events.has_event?(:value_changed).should.be.true 219 | end 220 | end 221 | describe "removing multiple events when many exist" do 222 | before do 223 | @button.off(:swipe, :value_changed) 224 | end 225 | 226 | it "should detach the events" do 227 | @button.rmq_data.events.has_event?(:tap).should.be.true 228 | @button.rmq_data.events.has_event?(:swipe).should.be.false 229 | @button.rmq_data.events.has_event?(:value_changed).should.be.false 230 | end 231 | end 232 | end 233 | 234 | describe "apply_style" do 235 | before do 236 | @view = UILabel.alloc.init 237 | @view.rmq.stylesheet = TestScreenStylesheet 238 | @view.apply_style(:test_label) 239 | end 240 | 241 | it "should style the view" do 242 | @view.text.should.equal("style from sheet") 243 | end 244 | end 245 | 246 | describe "style" do 247 | before do 248 | @view = UILabel.alloc.init 249 | 250 | @view.style do |st| 251 | st.text = "test style" 252 | end 253 | end 254 | 255 | it "should have styled the view" do 256 | @view.text.should.equal("test style") 257 | end 258 | end 259 | 260 | describe "color" do 261 | before { @view = UIView.alloc.init } 262 | 263 | it "should return rmq.color" do 264 | @view.color.should.equal(RubyMotionQuery::Color) 265 | end 266 | end 267 | 268 | describe "font" do 269 | before { @view = UIView.alloc.init } 270 | 271 | it "should return rmq.font" do 272 | @view.font.should.equal(RubyMotionQuery::Font) 273 | end 274 | end 275 | 276 | describe "image" do 277 | before { @view = UIView.alloc.init } 278 | 279 | it "should return rmq.image" do 280 | @view.image.should.equal(RubyMotionQuery::ImageUtils) 281 | end 282 | end 283 | 284 | describe "stylesheet" do 285 | before { @view = UIView.alloc.init } 286 | 287 | it "should return rmq.stylesheet" do 288 | @view.stylesheet.should.equal(@view.rmq.stylesheet) 289 | end 290 | end 291 | 292 | describe "stylesheet=" do 293 | before { @view = UIView.alloc.init } 294 | 295 | it "should set rmq.stylesheet" do 296 | @view.stylesheet = TestScreenStylesheet 297 | @view.rmq.stylesheet.is_a?(TestScreenStylesheet).should.be.true 298 | end 299 | end 300 | 301 | describe "find" do 302 | before do 303 | @view = UIView.alloc.init 304 | @view.append(UIButton) 305 | end 306 | 307 | it "should set use the proper context to find the children" do 308 | find(@view).find(UIButton).count.should.equal(1) 309 | end 310 | 311 | it "should properly work with multiple arguments as well" do 312 | @view.append(UILabel).tag(:something) 313 | find(@view).find(UIButton, :something).count.should.equal(2) 314 | end 315 | end 316 | 317 | describe "#find!" do 318 | before do 319 | @view = UIView.alloc.init 320 | end 321 | 322 | it "should return the view when there is a single result" do 323 | btn = @view.append!(FakeView) 324 | @view.append(UIButton) 325 | 326 | find(@view).find!(FakeView).should.equal(btn) 327 | end 328 | 329 | it "should return an array of views when there is multiple results" do 330 | btn = @view.append!(FakeView) 331 | btn2 = @view.append!(FakeView) 332 | @view.append(UIButton) 333 | 334 | find(@view).find!(FakeView).should.equal([btn, btn2]) 335 | end 336 | end 337 | 338 | end 339 | -------------------------------------------------------------------------------- /spec/pro_motion/data_table_screen_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'DataTableScreen' do 2 | 3 | extend ContributorsModule 4 | 5 | class TestDataTableScreen < ProMotion::DataTableScreen 6 | stylesheet ContributorScreenStylesheet 7 | model Contributor 8 | end 9 | 10 | class TestDataTableScreenScope < ProMotion::DataTableScreen 11 | stylesheet ContributorScreenStylesheet 12 | model Contributor, scope: :starts_with_s 13 | end 14 | 15 | class TestDataTableScreenRefreshable < ProMotion::DataTableScreen 16 | stylesheet ContributorScreenStylesheet 17 | model Contributor 18 | refreshable 19 | attr_accessor :refreshed 20 | 21 | def on_refresh 22 | @refreshed = true 23 | end 24 | end 25 | 26 | class TestDataTableScreenSearchableNoFields < ProMotion::DataTableScreen 27 | stylesheet ContributorScreenStylesheet 28 | model Contributor 29 | searchable 30 | end 31 | 32 | class TestDataTableScreenSearchable < ProMotion::DataTableScreen 33 | stylesheet ContributorScreenStylesheet 34 | model Contributor 35 | searchable fields: [:name] 36 | end 37 | 38 | class TestDataTableScreenModelQuery < ProMotion::DataTableScreen 39 | stylesheet ContributorScreenStylesheet 40 | model Contributor 41 | 42 | def model_query 43 | Contributor.where(:name).contains("er").sort_by(:name) 44 | end 45 | end 46 | 47 | before do 48 | class << self 49 | include CDQ 50 | end 51 | 52 | init_contributors 53 | end 54 | 55 | describe "using a model" do 56 | before do 57 | @controller = TestDataTableScreen.new 58 | @controller.on_load 59 | end 60 | 61 | it " - should default the scope to all, if its not included in the cell definition" do 62 | @controller.tableView(@controller.table_view, numberOfRowsInSection: 0).should == Contributor.count 63 | end 64 | 65 | it " - should initialize like a normal PM::TableScreen cell" do 66 | path = NSIndexPath.indexPathForRow(0, inSection:0) 67 | cell_data = @controller.cell_at(path) 68 | 69 | expected_keys = [:properties, :cell_style, :cell_identifier] 70 | (expected_keys & cell_data.keys).should == expected_keys 71 | 72 | @controller.tableView(@controller.table_view, cellForRowAtIndexPath: path).class.should == ContributorCell 73 | end 74 | 75 | it " - should sort by :created_at when the :all scope is not defined" do 76 | Contributor.sort_by(:created_at).each_with_index do |entity, index| 77 | path = NSIndexPath.indexPathForRow(index, inSection:0) 78 | cell_data = @controller.cell_at(path) 79 | cell_data[:properties][:name].should == entity.name 80 | end 81 | end 82 | end 83 | 84 | describe "using a scope" do 85 | before do 86 | @controller = TestDataTableScreenScope.new 87 | @controller.on_load 88 | end 89 | 90 | it "should properly use scopes to generate cells" do 91 | @controller.tableView(@controller.table_view, numberOfRowsInSection: 0).should == Contributor.where(:name).begins_with('s').count 92 | end 93 | 94 | it "should sort by the scope properly" do 95 | Contributor.where(:name).begins_with('s').sort_by(:name).each_with_index do |entity, index| 96 | path = NSIndexPath.indexPathForRow(index, inSection:0) 97 | cell_data = @controller.cell_at(path) 98 | cell_data[:properties][:name].should == entity.name 99 | end 100 | end 101 | end 102 | 103 | describe "using a model_query" do 104 | before do 105 | @controller = TestDataTableScreenModelQuery.new 106 | @controller.on_load 107 | end 108 | 109 | it "should have a sorted query for model data" do 110 | @controller.model_query.is_a?(CDQ::CDQTargetedQuery).should == true 111 | end 112 | 113 | it "should use the model_query to filter data properly" do 114 | # markrickert & twerth 115 | @controller.tableView(@controller.table_view, numberOfRowsInSection: 0).should == 2 116 | end 117 | 118 | it "should use the model_query to sort properly" do 119 | Contributor.where(:name).contains("er").sort_by(:name).each_with_index do |entity, index| 120 | path = NSIndexPath.indexPathForRow(index, inSection:0) 121 | cell_data = @controller.cell_at(path) 122 | cell_data[:properties][:name].should == entity.name 123 | end 124 | end 125 | end 126 | 127 | describe "live reloading" do 128 | before do 129 | @controller = TestDataTableScreen.new 130 | @controller.on_load 131 | end 132 | 133 | it "should delete cells when deleted form CoreData" do 134 | @controller.tableView(@controller.table_view, numberOfRowsInSection: 0).should == contributors.count 135 | Contributor.first.destroy 136 | cdq.save 137 | @controller.tableView(@controller.table_view, numberOfRowsInSection: 0).should == contributors.count - 1 138 | end 139 | 140 | it "should add cells when added to CoreData" do 141 | @controller.tableView(@controller.table_view, numberOfRowsInSection: 0).should == contributors.count 142 | Contributor.new(name: "clayallsopp") # a man can dream, can't he? 143 | cdq.save 144 | @controller.tableView(@controller.table_view, numberOfRowsInSection: 0).should == contributors.count + 1 145 | Contributor.new(name: "mattt") 146 | cdq.save 147 | @controller.tableView(@controller.table_view, numberOfRowsInSection: 0).should == contributors.count + 2 148 | end 149 | 150 | it "should update cells when data is changed in CoreData" do 151 | @controller.tableView(@controller.table_view, numberOfRowsInSection: 0).should == contributors.count 152 | 153 | path = NSIndexPath.indexPathForRow(2, inSection:0) 154 | cell_data = @controller.cell_at(path) 155 | name_to_change = cell_data[:properties][:name] 156 | 157 | # Change the name 158 | # Just append something to the name so we don't mess with 159 | # the order of the sorted cells. 160 | c = Contributor.where(name: name_to_change).first 161 | c.name = "#{name_to_change} new" 162 | 163 | cell_data = @controller.cell_at(path) 164 | cell_data[:properties][:name].should == "#{name_to_change} new" 165 | 166 | @controller.tableView(@controller.table_view, numberOfRowsInSection: 0).should == contributors.count 167 | end 168 | end 169 | 170 | describe "refreshable" do 171 | before do 172 | @controller = TestDataTableScreenRefreshable.new 173 | @controller.on_load 174 | end 175 | 176 | it "should be refreshable" do 177 | @controller.class.get_refreshable.should == true 178 | end 179 | 180 | it "should create a refresh object" do 181 | @controller.instance_variable_get("@refresh_control").should.be.kind_of UIRefreshControl 182 | end 183 | 184 | it "should respond to start_refreshing and end_refreshing" do 185 | @controller.respond_to?(:start_refreshing).should == true 186 | @controller.respond_to?(:end_refreshing).should == true 187 | end 188 | 189 | it "should call on_refresh" do 190 | @controller.refreshed.should.be.nil 191 | @controller.refreshView(UIRefreshControl.alloc.init) 192 | @controller.refreshed.should == true 193 | end 194 | end 195 | 196 | describe "searchable" do 197 | before do 198 | @controller = TestDataTableScreenSearchable.new 199 | @controller.on_load 200 | end 201 | 202 | # it "should raise an error when initializing without model field specifications" do 203 | # lambda do 204 | # no_fields = TestDataTableScreenSearchableNoFields.new 205 | # no_fields.on_load 206 | # end.should.raise 207 | # end 208 | 209 | it "should be searchable" do 210 | @controller.class.get_searchable.should == true 211 | end 212 | 213 | it "should have the correct delegate" do 214 | @controller.tableView.delegate.search_delegate.class.should == DataTableSeachDelegate 215 | end 216 | 217 | if UIDevice.currentDevice.systemVersion.to_f < 11.0 218 | it "should create a search header" do 219 | @controller.tableView.tableHeaderView.should.be.kind_of UISearchBar 220 | end 221 | 222 | it "should not hide the search bar initally by default" do 223 | @controller.tableView.contentOffset.should == CGPointMake(0,0) 224 | end 225 | end 226 | 227 | it "should have a delegate object that isn't ProMotion" do 228 | @controller.search_delegate.class.should.not == @controller.class 229 | @controller.search_delegate.is_a?(DataTableSeachDelegate).should == true 230 | end 231 | 232 | # fetchRequest failing in spec 233 | # it "should filter results in a new fetched results controller" do 234 | # frc = @controller.fetch_controller 235 | # frc.fetchRequest.predicate.class.should == NSTruePredicate 236 | # @controller.search_delegate.willPresentSearchController(@controller.search_controller) 237 | # @controller.search_controller.searchBar.text = "ma" 238 | # @controller.search_delegate.updateSearchResultsForSearchController(@controller.search_controller) 239 | # @controller.fetch_controller.should.not == frc 240 | # @controller.fetch_controller.fetchRequest.predicate.class.should == NSComparisonPredicate 241 | # end 242 | 243 | # it "should have the proper results for a search" do 244 | # search_string = "ma" 245 | 246 | # @controller.tableView(@controller, numberOfRowsInSection:0).should == Contributor.count 247 | # @controller.search_delegate.willPresentSearchController(@controller.search_controller) 248 | # @controller.search_controller.searchBar.text = search_string 249 | # @controller.search_delegate.updateSearchResultsForSearchController(@controller.search_controller) 250 | 251 | # searched = Contributor.where("name CONTAINS[cd] '#{search_string}' OR city CONTAINS[cd] '#{search_string}'") 252 | # @controller.tableView(@controller, numberOfRowsInSection:0).should == searched.count 253 | 254 | # @controller.search_delegate.willDismissSearchController(@controller.search_controller) 255 | # @controller.tableView(@controller, numberOfRowsInSection:0).should == Contributor.count 256 | # end 257 | end 258 | 259 | describe ".model" do 260 | it "should query the model that was provided to the screen" do 261 | TestDataTableScreen.model Contributor 262 | TestDataTableScreen.data_model.should.equal(Contributor) 263 | end 264 | 265 | it "should require the model provided defines the cell method" do 266 | class MissingCellMethod; end 267 | 268 | should.raise(RuntimeError) do 269 | TestDataTableScreen.model MissingCellMethod 270 | end 271 | end 272 | 273 | it "should accept an optional scope" do 274 | TestDataTableScreen.model Contributor, scope: :starts_with_s 275 | TestDataTableScreen.data_scope.should.equal(:starts_with_s) 276 | end 277 | end 278 | 279 | end 280 | --------------------------------------------------------------------------------