├── .gitignore ├── .travis.yml ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── README.md ├── ROADMAP.md ├── Rakefile ├── app └── app_delegate.rb ├── bin └── prime ├── files ├── Gemfile ├── Rakefile ├── app │ ├── app_delegate.rb │ ├── config │ │ └── base.rb │ ├── environment.rb │ ├── models │ │ └── .gitkeep │ ├── screens │ │ └── application.rb │ ├── sections │ │ └── .gitkeep │ └── styles │ │ └── .gitkeep └── resources │ ├── Default-568h@2x.png │ ├── Default.png │ ├── Default@2x.png │ └── Icon.png ├── generators ├── generator.rb ├── model_generator.rb ├── scaffold_generator.rb ├── screen_generator.rb ├── table_generator.rb └── templates │ ├── cell.rb │ ├── model.rb │ ├── scaffold │ ├── cell.rb │ ├── form.rb │ ├── model.rb │ ├── screen.rb │ ├── show.rb │ ├── styles.rb │ └── table.rb │ ├── screen.rb │ └── table.rb ├── lib └── motion-prime.rb ├── motion-prime.gemspec ├── motion-prime ├── api_client.rb ├── config │ ├── base.rb │ └── config.rb ├── core_ext │ ├── hash.rb │ ├── kernel.rb │ ├── nil_class.rb │ └── time.rb ├── delegate │ ├── _base_mixin.rb │ ├── _navigation_mixin.rb │ └── app_delegate.rb ├── elements │ ├── _content_padding_mixin.rb │ ├── _content_text_mixin.rb │ ├── _text_mixin.rb │ ├── base_element.rb │ ├── button.rb │ ├── collection_view_cell.rb │ ├── draw.rb │ ├── draw │ │ ├── _draw_background_mixin.rb │ │ ├── image.rb │ │ ├── label.rb │ │ └── view.rb │ ├── error_message.rb │ ├── google_map.rb │ ├── image.rb │ ├── label.rb │ ├── map.rb │ ├── page_view_controller.rb │ ├── progress_hud.rb │ ├── spinner.rb │ ├── table_header.rb │ ├── table_view.rb │ ├── table_view_cell.rb │ ├── text_field.rb │ ├── text_view.rb │ ├── view_with_section.rb │ └── web_view.rb ├── env.rb ├── helpers │ ├── has_authorization.rb │ ├── has_class_factory.rb │ ├── has_normalizer.rb │ ├── has_search_bar.rb │ ├── has_style_chain_builder.rb │ ├── has_style_options.rb │ └── has_styles.rb ├── models │ ├── _association_mixin.rb │ ├── _base_mixin.rb │ ├── _dirty_mixin.rb │ ├── _filter_mixin.rb │ ├── _finder_mixin.rb │ ├── _nano_bag_mixin.rb │ ├── _sync_mixin.rb │ ├── _timestamps_mixin.rb │ ├── association_collection.rb │ ├── errors.rb │ ├── exceptions.rb │ ├── json.rb │ ├── model.rb │ ├── store.rb │ └── store_extension.rb ├── prime.rb ├── screens │ ├── _aliases_mixin.rb │ ├── _base_mixin.rb │ ├── _navigation_mixin.rb │ ├── _orientations_mixin.rb │ ├── _sections_mixin.rb │ ├── controllers │ │ ├── navigation_controller.rb │ │ └── tab_bar_controller.rb │ ├── extensions │ │ ├── _indicators_mixin.rb │ │ └── _navigation_bar_mixin.rb │ └── screen.rb ├── sections │ ├── _async_form_mixin.rb │ ├── _async_table_mixin.rb │ ├── _cell_section_mixin.rb │ ├── _delegate_mixin.rb │ ├── _draw_section_mixin.rb │ ├── _section_with_container_mixin.rb │ ├── abstract_collection.rb │ ├── base_section.rb │ ├── collection │ │ └── collection_delegate.rb │ ├── form.rb │ ├── form │ │ ├── base_field_section.rb │ │ ├── date_field_section.rb │ │ ├── form_delegate.rb │ │ ├── form_header_section.rb │ │ ├── password_field_section.rb │ │ ├── select_field_section.rb │ │ ├── static_field_section.rb │ │ ├── string_field_section.rb │ │ ├── submit_field_section.rb │ │ ├── switch_field_section.rb │ │ └── text_field_section.rb │ ├── grid.rb │ ├── header.rb │ ├── page_view.rb │ ├── page_view │ │ └── page_view_delegate.rb │ ├── tabbed.rb │ ├── table.rb │ └── table │ │ ├── refresh_mixin.rb │ │ └── table_delegate.rb ├── services │ ├── base_computed_options.rb │ ├── element_computed_options.rb │ ├── logger.rb │ ├── section_computed_options.rb │ └── table_data_indexes.rb ├── styles │ ├── _mixins.rb │ ├── base.rb │ └── form.rb ├── support │ ├── _control_content_alignment.rb │ ├── _key_value_store.rb │ ├── _padding_attribute.rb │ ├── consts.rb │ ├── mp_button.rb │ ├── mp_collection_cell_with_section.rb │ ├── mp_label.rb │ ├── mp_search_bar_custom.rb │ ├── mp_spinner.rb │ ├── mp_table_cell_content_view.rb │ ├── mp_table_cell_with_section.rb │ ├── mp_table_header_with_section.rb │ ├── mp_table_view.rb │ ├── mp_text_field.rb │ ├── mp_text_view.rb │ ├── mp_view_controller.rb │ ├── mp_view_with_section.rb │ ├── temp_fixes.rb │ └── ui_view.rb ├── version.rb └── views │ ├── _frame_calculator_mixin.rb │ ├── layout.rb │ ├── styles.rb │ ├── view_builder.rb │ └── view_styler.rb ├── resources ├── Default-568h@2x.png └── Icon.png ├── spec ├── factories │ ├── delegates.rb │ ├── init.rb │ ├── models.rb │ ├── scaffold │ │ ├── models │ │ │ └── task.rb │ │ ├── screens │ │ │ └── tasks.rb │ │ ├── sections │ │ │ └── tasks │ │ │ │ ├── form.rb │ │ │ │ ├── index_cell.rb │ │ │ │ ├── index_table.rb │ │ │ │ └── show.rb │ │ └── styles │ │ │ └── tasks.rb │ ├── screens.rb │ └── sections.rb ├── features │ ├── scaffold │ │ └── index.rb │ └── screens │ │ └── open_screen.rb ├── helpers │ └── has_content.rb └── unit │ ├── config │ └── store_spec.rb │ ├── delegate │ └── delegate_spec.rb │ ├── elements │ └── label_spec.rb │ ├── models │ ├── association_collection_spec.rb │ ├── associations_spec.rb │ ├── bag_spec.rb │ ├── dirty_spec.rb │ ├── errors_spec.rb │ ├── finder_spec.rb │ ├── json.rb │ ├── model_spec.rb │ ├── store_extension_spec.rb │ └── store_spec.rb │ ├── prime │ ├── env.rb │ └── logger.rb │ ├── screens │ └── screen_spec.rb │ ├── sections │ └── section_spec.rb │ └── support │ ├── filter_mixin_spec.rb │ └── frame_calculator_mixin_spec.rb └── travis.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .repl_history 2 | .bundle 3 | build 4 | tags 5 | app/pixate_code.rb 6 | resources/*.nib 7 | resources/*.momd 8 | resources/*.storyboardc 9 | files/vendor/ 10 | files/.repl_history 11 | files/Gemfile.lock 12 | .DS_Store 13 | nbproject 14 | .redcar 15 | #*# 16 | *~ 17 | *.sw[po] 18 | .eprj 19 | .sass-cache 20 | .idea 21 | vendor 22 | pkg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | before_install: 3 | - (ruby --version) 4 | gemfile: 5 | - Gemfile 6 | script: ./travis.sh -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | {motion-prime}/**/*.rb -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'motion-cocoapods', '~> 1.7.0' 4 | gemspec -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | motion-prime (1.0.7) 5 | activesupport (~> 3.2.19) 6 | afmotion (~> 2.4.1) 7 | bubble-wrap (~> 1.6.0) 8 | cocoapods 9 | methadone 10 | motion-cocoapods 11 | motion-require 12 | motion-support (~> 0.2.6) 13 | rake 14 | rm-digest 15 | sugarcube (~> 1.6.0) 16 | thor 17 | 18 | GEM 19 | remote: http://rubygems.org/ 20 | specs: 21 | activesupport (3.2.21) 22 | i18n (~> 0.6, >= 0.6.4) 23 | multi_json (~> 1.0) 24 | afmotion (2.4.1) 25 | motion-cocoapods (>= 1.4.1) 26 | motion-require (>= 0.1) 27 | bubble-wrap (1.6.0) 28 | bubble-wrap-http (= 1.6.0) 29 | bubble-wrap-http (1.6.0) 30 | claide (0.7.0) 31 | cocoapods (0.35.0) 32 | activesupport (>= 3.2.15) 33 | claide (~> 0.7.0) 34 | cocoapods-core (= 0.35.0) 35 | cocoapods-downloader (~> 0.8.0) 36 | cocoapods-plugins (~> 0.3.1) 37 | cocoapods-trunk (~> 0.4.1) 38 | cocoapods-try (~> 0.4.2) 39 | colored (~> 1.2) 40 | escape (~> 0.0.4) 41 | molinillo (~> 0.1.2) 42 | nap (~> 0.8) 43 | open4 (~> 1.3) 44 | xcodeproj (~> 0.20.2) 45 | cocoapods-core (0.35.0) 46 | activesupport (>= 3.2.15) 47 | fuzzy_match (~> 2.0.4) 48 | nap (~> 0.8.0) 49 | cocoapods-downloader (0.8.1) 50 | cocoapods-plugins (0.3.2) 51 | nap 52 | cocoapods-trunk (0.4.1) 53 | nap (>= 0.8) 54 | netrc (= 0.7.8) 55 | cocoapods-try (0.4.3) 56 | colored (1.2) 57 | escape (0.0.4) 58 | fuzzy_match (2.0.4) 59 | i18n (0.7.0) 60 | methadone (1.8.0) 61 | bundler 62 | molinillo (0.1.2) 63 | motion-cocoapods (1.7.0) 64 | cocoapods (>= 0.34) 65 | motion-redgreen (1.0.0) 66 | motion-require (0.2.0) 67 | motion-stump (0.3.2) 68 | motion-support (0.2.6) 69 | motion-require (>= 0.0.6) 70 | multi_json (1.10.1) 71 | nap (0.8.0) 72 | netrc (0.7.8) 73 | open4 (1.3.4) 74 | rake (10.4.2) 75 | rm-digest (0.0.2) 76 | sugarcube (1.6.3) 77 | thor (0.19.1) 78 | xcodeproj (0.20.2) 79 | activesupport (>= 3) 80 | colored (~> 1.2) 81 | 82 | PLATFORMS 83 | ruby 84 | 85 | DEPENDENCIES 86 | motion-cocoapods (~> 1.7.0) 87 | motion-prime! 88 | motion-redgreen 89 | motion-stump 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MotionPrime [![Build Status](https://travis-ci.org/droidlabs/motion-prime.png)](https://travis-ci.org/droidlabs/motion-prime) [![Code Climate](https://codeclimate.com/github/droidlabs/motion-prime.png)](https://codeclimate.com/github/droidlabs/motion-prime) [![Roadchange](https://www.roadchange.com/droidlabs/motion-prime/badge.png)](https://roadchange.com/droidlabs/motion-prime) 2 | 3 | ![Prime](https://s3.amazonaws.com/motionprime/logo-1.png) 4 | 5 | MotionPrime is yet another framework written on RubyMotion for creating really fast iOS applications. 6 | 7 | ## Why MotionPrime? 8 | 9 | * Performance. MotionPrime designed to improve creating and scrolling performance of table views. 10 | * Simplicity. Creating first MotionPrime application is as simple as creating new RubyOnRails application. 11 | 12 | ## Getting Started 13 | 14 | #### 1. Install MotionPrime: 15 | 16 | $ gem install motion-prime 17 | 18 | #### 2a. Create [bootstrap](https://github.com/motionprime/prime_bootstrap) project: 19 | 20 | $ prime bootstrap myapp 21 | 22 | #### 2b. OR create empty project: 23 | 24 | $ prime new myapp 25 | 26 | #### 3. Run application 27 | 28 | $ rake 29 | 30 | ## Hello World (Sample) 31 | 32 | ```ruby 33 | # app/app_delegate.rb 34 | class AppDelegate < Prime::BaseAppDelegate 35 | def on_load(app, options) 36 | open_screen :main 37 | end 38 | end 39 | 40 | # app/screens/main_screen.rb 41 | class MainScreen < Prime::Screen 42 | title 'Main screen' 43 | 44 | section :my_profile 45 | end 46 | 47 | # app/sections/my_profile.rb 48 | class MyProfileSection < Prime::Section 49 | element :title, text: "Hello World" 50 | element :avatar, image: "images/avatar.png", type: :image 51 | end 52 | 53 | # app/styles/my_profile.rb 54 | Prime::Styles.define :my_profile do 55 | style :title, 56 | width: 300, height: 20, color: :black, 57 | top: 10, left: 5, background_color: :white 58 | 59 | style :avatar, 60 | width: 90, height: 90, top: 40, left: 5 61 | end 62 | ``` 63 | 64 | ## Extensions 65 | 66 | * [ECSlidingViewController 2 integration](https://github.com/motionprime/prime_sliding_menu) (Sidebar) 67 | * [RESideMenu integration](https://github.com/motionprime/prime_reside_menu) (Sidebar) 68 | * [Sliding actions support](https://github.com/motionprime/prime_sliding_action) 69 | 70 | ## Samples 71 | 72 | * [Simple to-do app](https://github.com/motionprime/prime_sample_todo) 73 | * [Send mail with attached file](https://github.com/cactis/email_attachment_example) 74 | 75 | ## Documentation 76 | 77 | * [Getting Started](http://prime.droidlabs.pro/) 78 | * [RubyDoc](http://rubydoc.info/gems/motion-prime/) 79 | 80 | ## Contributing 81 | 82 | 1. Fork it 83 | 2. Create your feature branch (`git checkout -b my-new-feature`) 84 | 3. Commit your changes (`git commit -am 'Add some feature'`) 85 | 4. Push to the branch (`git push origin my-new-feature`) 86 | 5. Create new Pull Request 87 | 88 | ## Thanks for using MotionPrime! 89 | 90 | Hope, you'll enjoy MotionPrime! 91 | 92 | Cheers, [Droid Labs](http://droidlabs.pro). 93 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | === 1.1.0 2 | * Model.fetch should always return status of main model fetch response 3 | * grid item width should be calculated based on uicollectionview with. 4 | * ability to change access token param name. 5 | * bug: if mp label do not have text and was set as hidden, it should unhide after setting text. 6 | * bug: size_to_fit works incorrect with relative width. 7 | * bug: bind_keyboard_close breaks bind_guesture. 8 | * bug: dealloc of Prime::Section will not be called for cell created in collection_data using #map. 9 | * bug: images does not render after reload table if using draw_with_layer (prerender not enabled). 10 | * bug: incorrect height (cropped) for draw label with lineSpacing in cases when there is just one line. 11 | * use one style to set rounded corners for view/draw elements (remove :rounded_corners option) 12 | * add :sides option to BaseElement border (like in draw) 13 | * add dsl for push notifications. 14 | * add some extensions/middleware system, at least for networking. 15 | * create "display_network_error" extension. 16 | * add different templates. some templates should be more like final app. 17 | * add size_to_fit support for images. 18 | * simplify border radius for common case 19 | 20 | === 1.2.0 21 | * Move api_client and model sync mixin to prime_model_sync gem. 22 | * Move models to prime_model gem. 23 | * Move bind keyboard events to forms. 24 | 25 | === 1.3.0 26 | * add cell preload for reverse scrolling table. 27 | * add computed_options.get(), this will allow to make sure that options is computed. 28 | * add testing framework. 29 | * add DSL for ViewStyles#setValue conditions. 30 | * add embed/regular has many types. embedded by default. 31 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require "bundler/gem_tasks" 3 | namespace :gem do 4 | task :release do 5 | helper = Bundler::GemHelper.new 6 | helper.release_gem(helper.send :built_gem_path) 7 | end 8 | end 9 | 10 | $:.unshift("/Library/RubyMotion/lib") 11 | require 'motion/project/template/ios' 12 | require "rubygems" 13 | require "bundler" 14 | require 'motion-cocoapods' 15 | Bundler.setup 16 | Bundler.require 17 | require 'motion-support' 18 | require 'motion-prime' 19 | require 'motion-stump' 20 | 21 | Motion::Project::App.setup do |app| 22 | app.name = 'Prime' 23 | end 24 | -------------------------------------------------------------------------------- /app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate < MotionPrime::BaseAppDelegate 2 | def application(application, didFinishLaunchingWithOptions:launchOptions) 3 | true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/prime: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require 'methadone' 5 | require_relative '../motion-prime/version' 6 | class App 7 | include Methadone::Main 8 | include Methadone::CLILogging 9 | include Methadone::SH 10 | 11 | main do |command, *opts| 12 | case command.to_sym 13 | when :new then create_base(*opts) 14 | when :bootstrap then create_bootstrap(*opts) 15 | when :generate then generate(*opts) 16 | when :g then generate(*opts) 17 | else help 18 | end 19 | 0 20 | end 21 | 22 | def self.help 23 | info "Command line tools for MotionPrime" 24 | info "Commands:" 25 | info " new " 26 | info " Creates a new MotionPrime app from a template." 27 | info " generate scaffold " 28 | info " Creates a new MotionPrime scaffold from a template." 29 | info " generate screen|model|table " 30 | info " Creates a new MotionPrime resource from a template." 31 | end 32 | 33 | def self.create_base(name) 34 | create(name, "motion-prime", "git://github.com/droidlabs/motion-prime.git") 35 | end 36 | 37 | def self.create_bootstrap(name) 38 | create(name, "prime_bootstrap", "git://github.com/motionprime/prime_bootstrap.git") 39 | end 40 | 41 | def self.create(name, template_name, repo) 42 | return puts "Usage: prime new " unless name.to_s.length > 0 43 | info "Creating new MotionPrime iOS app: #{name}" 44 | if false 45 | sh "motion create #{name} --template=#{repo}" 46 | else 47 | clone_template(template_name, repo) 48 | sh "motion create #{name} --template=#{template_name}" 49 | end 50 | info "Command: bundle install" 51 | sh "cd ./#{name}; bundle install" 52 | info "Command: pod setup" 53 | sh "cd ./#{name}; pod setup" 54 | info "Command: rake pod:install" 55 | sh "cd ./#{name}; bundle exec rake pod:install" 56 | end 57 | 58 | def self.generate(resource, name) 59 | require_relative '../generators/generator' 60 | MotionPrime::Generator.factory(resource).generate(name) 61 | end 62 | 63 | def self.home_path 64 | ENV['HOME'].split('/')[0..2].join('/') 65 | end 66 | 67 | def self.clone_template(name, repo) 68 | path = File.expand_path(File.join(home_path, 'Library/RubyMotion/template', name)) 69 | git_clone(path, repo) 70 | end 71 | 72 | def self.git_clone(path, repo) 73 | if File.exist?(path) 74 | system("git --work-tree=#{path} --git-dir=#{path}/.git pull origin master") 75 | else 76 | system("git clone #{repo} #{path}") 77 | end 78 | end 79 | 80 | description "Command line tools for MotionPrime" 81 | 82 | arg :command 83 | arg :opt, :optional 84 | 85 | version MotionPrime::VERSION 86 | 87 | go! 88 | end -------------------------------------------------------------------------------- /files/Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'motion-cocoapods', '~> 1.7.0' 4 | gem 'motion-prime', '1.0.7' 5 | 6 | # add reside menu for sidebar support 7 | # gem 'prime_reside_menu', '~> 0.1.4' 8 | 9 | # or add sliding menu for sidebar support 10 | # gem 'prime_sliding_menu', '~> 0.1.5' -------------------------------------------------------------------------------- /files/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | require 'motion/project/template/ios' 4 | require "rubygems" 5 | require 'motion-cocoapods' 6 | require 'bundler' 7 | Bundler.require 8 | require 'motion-prime' 9 | 10 | require File.expand_path 'app/environment.rb' 11 | Motion::Project::App.setup do |app| 12 | # Use `rake config' to see complete project settings. 13 | app.name = 'Prime Project' 14 | 15 | app.version = '0.0.1' 16 | app.icons = %w{Icon.png} 17 | end -------------------------------------------------------------------------------- /files/app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate < Prime::BaseAppDelegate 2 | def on_load(app, options) 3 | # open_screen :home 4 | end 5 | end -------------------------------------------------------------------------------- /files/app/config/base.rb: -------------------------------------------------------------------------------- 1 | Prime::Config.configure do |config| 2 | # Uncomments following if you don't want auto generating id for model on save 3 | # 4 | # config.model.auto_generate_id = false 5 | 6 | # After defining colors you will be able to use it via .text_color = :app_base.uicolor 7 | # config.colors do |colors| 8 | # colors.navigation_base = 0x1b75bc 9 | # colors.base = 0x1b75bc 10 | # colors.dark = 0x333333 11 | # colors.error = 0xef471f 12 | # end 13 | 14 | # After defining fonts you will be able to use it via .font = :app_base.uifont 15 | # Note: font should be copied to resources folder (e.g. resources/fonts/ubuntu.ttf) and added to Rakefile. 16 | # config.fonts do |fonts| 17 | # fonts.base = "Ubuntu" 18 | # end 19 | end 20 | -------------------------------------------------------------------------------- /files/app/environment.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | def self.root 3 | File.expand_path File.dirname(__FILE__) + '/../' 4 | end 5 | end -------------------------------------------------------------------------------- /files/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidlabs/motion-prime/9a7398f0f409b84d7ea71a7c5ca48bb0fcfe973f/files/app/models/.gitkeep -------------------------------------------------------------------------------- /files/app/screens/application.rb: -------------------------------------------------------------------------------- 1 | class ApplicationScreen < Prime::Screen 2 | 3 | end -------------------------------------------------------------------------------- /files/app/sections/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidlabs/motion-prime/9a7398f0f409b84d7ea71a7c5ca48bb0fcfe973f/files/app/sections/.gitkeep -------------------------------------------------------------------------------- /files/app/styles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidlabs/motion-prime/9a7398f0f409b84d7ea71a7c5ca48bb0fcfe973f/files/app/styles/.gitkeep -------------------------------------------------------------------------------- /files/resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidlabs/motion-prime/9a7398f0f409b84d7ea71a7c5ca48bb0fcfe973f/files/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /files/resources/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidlabs/motion-prime/9a7398f0f409b84d7ea71a7c5ca48bb0fcfe973f/files/resources/Default.png -------------------------------------------------------------------------------- /files/resources/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidlabs/motion-prime/9a7398f0f409b84d7ea71a7c5ca48bb0fcfe973f/files/resources/Default@2x.png -------------------------------------------------------------------------------- /files/resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidlabs/motion-prime/9a7398f0f409b84d7ea71a7c5ca48bb0fcfe973f/files/resources/Icon.png -------------------------------------------------------------------------------- /generators/generator.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'thor' 3 | require 'active_support/core_ext' 4 | class MotionPrime::Generator < Thor 5 | include Thor::Actions 6 | 7 | def self.source_root 8 | File.dirname(__FILE__) + '/templates' 9 | end 10 | 11 | class << self 12 | def factory(resource) 13 | case resource.to_sym 14 | when :screen 15 | require_relative './screen_generator' 16 | MotionPrime::ScreenGenerator.new 17 | when :model 18 | require_relative './model_generator' 19 | MotionPrime::ModelGenerator.new 20 | when :table 21 | require_relative './table_generator' 22 | MotionPrime::TableGenerator.new 23 | when :scaffold 24 | require_relative './scaffold_generator' 25 | MotionPrime::ScaffoldGenerator.new 26 | end 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /generators/model_generator.rb: -------------------------------------------------------------------------------- 1 | class MotionPrime::ModelGenerator < MotionPrime::Generator 2 | def generate(name) 3 | @name = name.downcase.singularize 4 | @class_name = "#{name.camelize}" 5 | template 'model.rb', "app/models/#{name}.rb" 6 | end 7 | end -------------------------------------------------------------------------------- /generators/scaffold_generator.rb: -------------------------------------------------------------------------------- 1 | class MotionPrime::ScaffoldGenerator < MotionPrime::Generator 2 | def generate(name) 3 | @s_name = name.singularize.downcase 4 | @p_name = name.pluralize.downcase 5 | @s_title = @s_name.titleize 6 | @p_title = @p_name.titleize 7 | @s_class_name = @s_name.camelize 8 | @p_class_name = @p_name.camelize 9 | template 'scaffold/screen.rb', "app/screens/#{@p_name}.rb" 10 | template 'scaffold/model.rb', "app/models/#{@s_name}.rb" 11 | template 'scaffold/table.rb', "app/sections/#{@p_name}/index_table.rb" 12 | template 'scaffold/cell.rb', "app/sections/#{@p_name}/index_cell.rb" 13 | template 'scaffold/form.rb', "app/sections/#{@p_name}/form.rb" 14 | template 'scaffold/show.rb', "app/sections/#{@p_name}/show.rb" 15 | template 'scaffold/styles.rb', "app/styles/#{@p_name}.rb" 16 | end 17 | end -------------------------------------------------------------------------------- /generators/screen_generator.rb: -------------------------------------------------------------------------------- 1 | class MotionPrime::ScreenGenerator < MotionPrime::Generator 2 | def generate(name) 3 | @name = name.downcase 4 | @class_name = "#{name.camelize}Screen" 5 | @title = name.titleize 6 | template 'screen.rb', "app/screens/#{name}.rb" 7 | end 8 | end -------------------------------------------------------------------------------- /generators/table_generator.rb: -------------------------------------------------------------------------------- 1 | class MotionPrime::TableGenerator < MotionPrime::Generator 2 | def generate(name) 3 | @name = name.downcase.singularize 4 | @model_class_name = "#{name.camelize}" 5 | @table_class_name = "#{name.pluralize.camelize}TableSection" 6 | @cell_class_name = "#{name.pluralize.camelize}CellSection" 7 | template 'table.rb', "app/sections/#{name.pluralize}/table.rb" 8 | template 'cell.rb', "app/sections/#{name.pluralize}/cell.rb" 9 | end 10 | end -------------------------------------------------------------------------------- /generators/templates/cell.rb: -------------------------------------------------------------------------------- 1 | class <%= @cell_class_name %> < Prime::Section 2 | # element :title, text: proc { model.title } 3 | end -------------------------------------------------------------------------------- /generators/templates/model.rb: -------------------------------------------------------------------------------- 1 | class <%= @class_name %> < Prime::Model 2 | timestamp_attributes 3 | # attribute :title 4 | end -------------------------------------------------------------------------------- /generators/templates/scaffold/cell.rb: -------------------------------------------------------------------------------- 1 | class <%= @p_class_name %>IndexCellSection < Prime::Section 2 | container height: 40 3 | element :title, text: proc { model.title } 4 | end -------------------------------------------------------------------------------- /generators/templates/scaffold/form.rb: -------------------------------------------------------------------------------- 1 | class <%= @p_class_name %>FormSection < Prime::FormSection 2 | field :title, 3 | label: { text: 'Title' }, 4 | input: { 5 | text: proc { model.title }, 6 | placeholder: "Enter title here" 7 | } 8 | 9 | field :delete, type: :submit, 10 | button: { 11 | title: "Delete", 12 | background_color: :red 13 | }, 14 | action: :on_delete, 15 | if: proc { model.persisted? } 16 | 17 | field :submit, type: :submit, 18 | button: { title: "Save" }, 19 | action: :on_submit 20 | 21 | def on_delete 22 | model.delete 23 | screen.close_screen(to_root: true) 24 | end 25 | 26 | def on_submit 27 | model.assign_attributes(field_values) 28 | model.save 29 | screen.close_screen 30 | end 31 | end -------------------------------------------------------------------------------- /generators/templates/scaffold/model.rb: -------------------------------------------------------------------------------- 1 | class <%= @s_class_name %> < Prime::Model 2 | timestamp_attributes 3 | attribute :title 4 | end -------------------------------------------------------------------------------- /generators/templates/scaffold/screen.rb: -------------------------------------------------------------------------------- 1 | class <%= @p_class_name %>Screen < ApplicationScreen 2 | title "<%= @p_title %>" 3 | 4 | # open_screen "<%= @p_name %>#index" 5 | def index 6 | set_title "<%= @p_title %>" 7 | set_navigation_right_button 'New' do 8 | open_screen "<%= @p_name %>#new" 9 | end 10 | set_section :<%= @p_name %>_index_table 11 | end 12 | 13 | # open_screen "<%= @p_name %>#show" 14 | def show 15 | @model = params[:model] 16 | set_title "Show <%= @s_title %>" 17 | set_navigation_back_button 'Back' 18 | set_navigation_right_button 'Edit' do 19 | open_screen "<%= @p_name %>#edit", params: { model: @model } 20 | end 21 | set_section :<%= @p_name %>_show, model: @model 22 | end 23 | 24 | # open_screen "<%= @p_name %>#edit" 25 | def edit 26 | @model = params[:model] 27 | set_title "Edit <%= @s_title %>" 28 | set_navigation_back_button 'Cancel' 29 | set_section :<%= @p_name %>_form, model: @model 30 | end 31 | 32 | # open_screen "<%= @p_name %>#new" 33 | def new 34 | @model = <%= @s_class_name %>.new 35 | set_title "New <%= @s_title %>" 36 | set_navigation_back_button 'Cancel' 37 | set_section :<%= @p_name %>_form, model: @model 38 | end 39 | 40 | def on_return 41 | if action?(:index) || action?(:show) 42 | refresh 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /generators/templates/scaffold/show.rb: -------------------------------------------------------------------------------- 1 | class <%= @p_class_name %>ShowSection < Prime::Section 2 | element :title, text: proc { model.title } 3 | end -------------------------------------------------------------------------------- /generators/templates/scaffold/styles.rb: -------------------------------------------------------------------------------- 1 | Prime::Styles.define :<%= @p_name %> do 2 | style :index do 3 | style :cell_title, 4 | text_color: :app_base, 5 | left: 20, 6 | top: 10, 7 | width: 280, 8 | font: :app_base.uifont(16), 9 | height: 20 10 | end 11 | style :show do 12 | style :title, 13 | top: 120, 14 | left: 0, 15 | right: 0, 16 | text_alignment: :center 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /generators/templates/scaffold/table.rb: -------------------------------------------------------------------------------- 1 | class <%= @p_class_name %>IndexTableSection < Prime::TableSection 2 | def collection_data 3 | <%= @s_class_name %>.all.map do |model| 4 | <%= @p_class_name %>IndexCellSection.new(model: model) 5 | end 6 | end 7 | 8 | def on_click(index) 9 | section = data[index.row] 10 | screen.open_screen '<%= @p_name %>#show', params: { model: section.model } 11 | end 12 | end -------------------------------------------------------------------------------- /generators/templates/screen.rb: -------------------------------------------------------------------------------- 1 | class <%= @class_name %> < ApplicationScreen 2 | title "<%= @title %>" 3 | 4 | def render 5 | 6 | end 7 | end -------------------------------------------------------------------------------- /generators/templates/table.rb: -------------------------------------------------------------------------------- 1 | class <%= @table_class_name %> < Prime::TableSection 2 | def collection_data 3 | # This method should return Array of sections, e.g: 4 | <%= @model_class_name %>.map do |model| 5 | <%= @cell_class_name %>.new(model: model) 6 | end 7 | end 8 | end -------------------------------------------------------------------------------- /lib/motion-prime.rb: -------------------------------------------------------------------------------- 1 | require 'motion-require' 2 | require 'motion-support' 3 | require 'motion-support/core_ext/hash' 4 | require 'sugarcube-common' 5 | 6 | require 'bubble-wrap/core' 7 | require 'bubble-wrap/reactor' 8 | 9 | require 'rm-digest' 10 | require 'afmotion' 11 | require File.expand_path('../../motion-prime/env.rb', __FILE__) 12 | require File.expand_path('../../motion-prime/prime.rb', __FILE__) 13 | 14 | Motion::Require.all(Dir.glob(File.expand_path('../../motion-prime/**/*.rb', __FILE__))) 15 | Motion::Require.all 16 | 17 | Motion::Project::App.setup do |app| 18 | app.detect_dependencies = false 19 | 20 | app.pods do 21 | pod 'NanoStore', '~> 2.7.7' 22 | pod 'SDWebImage', '~> 3.7.1' 23 | pod 'SVPullToRefresh', git: 'https://github.com/droidlabs/SVPullToRefresh.git' 24 | pod 'MBAlertView' 25 | pod 'MBProgressHUD', '~> 0.8' 26 | end 27 | end -------------------------------------------------------------------------------- /motion-prime.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../motion-prime/version', __FILE__) 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "motion-prime" 6 | spec.version = MotionPrime::VERSION 7 | spec.authors = ["Iskander Haziev", "Pavel Feklistov"] 8 | spec.email = ["gvalmon@gmail.com"] 9 | spec.description = %q{RubyMotion apps development framework} 10 | spec.summary = %q{RubyMotion apps development framework} 11 | spec.homepage = "" 12 | spec.license = "" 13 | 14 | spec.files = `git ls-files`.split($\) 15 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 16 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 17 | spec.require_paths = ["lib"] 18 | 19 | spec.add_development_dependency("motion-stump") 20 | spec.add_development_dependency("motion-redgreen") 21 | 22 | spec.add_dependency "cocoapods" 23 | spec.add_dependency "rake" 24 | spec.add_dependency "motion-cocoapods" 25 | spec.add_dependency "motion-require" 26 | spec.add_dependency "motion-support", '~> 0.2.6' 27 | spec.add_dependency 'bubble-wrap', '~> 1.6.0' 28 | spec.add_dependency 'sugarcube', '~> 1.6.0' 29 | spec.add_dependency 'afmotion', '~> 2.4.1' 30 | spec.add_dependency "methadone" 31 | spec.add_dependency "rm-digest" 32 | spec.add_dependency "thor" 33 | spec.add_dependency "activesupport", "~> 3.2.19" 34 | end 35 | -------------------------------------------------------------------------------- /motion-prime/config/base.rb: -------------------------------------------------------------------------------- 1 | motion_require './config' 2 | MotionPrime::Config.configure do |config| 3 | # MODELS 4 | if MotionPrime.env.test? 5 | config.model.store_type = :memory 6 | else 7 | config.model.store_type = :file 8 | end 9 | config.model.auto_generate_id = true 10 | 11 | config.api_client do |api| 12 | api.base = "http://example.com" 13 | api.client_id = "" 14 | api.client_secret = "" 15 | api.signature_secret = "" 16 | api.sign_request = false 17 | api.auth_path = '/oauth/token' 18 | api.api_namespace = '/api' 19 | api.allow_queue = false 20 | api.allow_cache = false 21 | api.default_methods_queue = [:post, :delete] 22 | api.default_methods_cache = [:get] 23 | end 24 | 25 | # APPEARANCE 26 | config.fonts do |fonts| 27 | fonts.base = :system 28 | end 29 | config.colors do |colors| 30 | colors.navigation_base = 0x1b75bc 31 | colors.base = 0x1b75bc 32 | colors.dark = 0x333333 33 | colors.error = 0xef471f 34 | end 35 | 36 | # SECTIONS 37 | config.prime.cell_section.mixins = [Prime::CellSectionMixin] 38 | 39 | # LOGGER 40 | config.logger.dealloc_items = ['screen'] 41 | config.logger.level = :info 42 | end -------------------------------------------------------------------------------- /motion-prime/config/config.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class Config 3 | attr_accessor :attributes 4 | 5 | def initialize(attributes = {}) 6 | @attributes = attributes || {} 7 | end 8 | 9 | def [](key) 10 | @attributes.has_key?(key.to_sym) ? fetch(key) : store(key, self.class.new) 11 | end 12 | 13 | def store(key, value) 14 | @attributes[key.to_sym] = value 15 | end 16 | alias :[]= :store 17 | 18 | def fetch(key, default = nil) 19 | @attributes[key.to_sym] || default 20 | end 21 | 22 | def nil? 23 | @attributes.empty? 24 | end 25 | alias :blank? :nil? 26 | 27 | def present? 28 | !blank? 29 | end 30 | 31 | def has_key?(key) 32 | !self[key].is_a?(self.class) 33 | end 34 | 35 | def to_hash 36 | hash = {} 37 | @attributes.each do |key, value| 38 | hash[key] = value.is_a?(MotionPrime::Config) ? value.to_hash : value 39 | end 40 | hash 41 | end 42 | 43 | class << self 44 | def method_missing(name, *args, &block) 45 | @base_config ||= self.new() 46 | @base_config.send(name.to_sym, *args, &block) 47 | end 48 | 49 | def configure(&block) 50 | @configure_blocks ||= [] 51 | @configure_blocks << block 52 | end 53 | 54 | def configure! 55 | @configure_blocks ||= [] 56 | @base_config ||= self.new() 57 | @configure_blocks.each do |block| 58 | block.call(@base_config) 59 | end 60 | setup_models 61 | setup_colors 62 | setup_fonts 63 | setup_logger 64 | end 65 | 66 | def setup_models 67 | MotionPrime::Store.connect 68 | end 69 | 70 | def setup_colors 71 | return unless @base_config 72 | colors = @base_config.colors.to_hash.inject({}) do |res, (color, value)| 73 | unless color == :prefix 74 | unless @base_config.colors.prefix.nil? 75 | res[:"#{@base_config.colors.prefix}_#{color}"] = value 76 | end 77 | res[:"app_#{color}"] = value 78 | end 79 | res 80 | end 81 | Symbol.css_colors.merge!(colors) 82 | end 83 | 84 | def setup_fonts 85 | return unless @base_config 86 | colors = @base_config.fonts.to_hash.inject({}) do |res, (font, value)| 87 | if [:system, :bold, :italic, :monospace].include?(value) 88 | value = Symbol.uifont[value] 89 | end 90 | unless font == :prefix 91 | unless @base_config.fonts.prefix.nil? 92 | res[:"#{@base_config.fonts.prefix}_#{font}"] = value 93 | end 94 | res[:"app_#{font}"] = value 95 | end 96 | res 97 | end 98 | Symbol.uifont.merge!(colors) 99 | end 100 | 101 | def setup_logger 102 | Prime::Logger.level = @base_config.logger.level 103 | Prime::Logger.dealloc_items = @base_config.logger.dealloc_items 104 | end 105 | end 106 | 107 | def method_missing(name, *args, &block) 108 | if block_given? 109 | yield self[name] 110 | else 111 | name = name.to_s 112 | if /(.+)\=$/.match(name) 113 | store($1, args[0]) 114 | elsif /(.+)\?$/.match(name) 115 | value = self[$1] 116 | value.present? && !!value 117 | else 118 | self[name] 119 | end 120 | end 121 | end 122 | end 123 | end -------------------------------------------------------------------------------- /motion-prime/core_ext/hash.rb: -------------------------------------------------------------------------------- 1 | def diff(other) 2 | dup. 3 | delete_if { |k, v| other[k] == v }. 4 | merge!(other.dup.delete_if { |k, v| has_key?(k) }) 5 | end -------------------------------------------------------------------------------- /motion-prime/core_ext/kernel.rb: -------------------------------------------------------------------------------- 1 | class Kernel 2 | def benchmark(key, &block) 3 | if Prime.env.development? 4 | t = Time.now 5 | result = block.call 6 | time = Time.now - t 7 | MotionPrime.benchmark_data[key] ||= {} 8 | MotionPrime.benchmark_data[key][:count] ||= 0 9 | MotionPrime.benchmark_data[key][:total] ||= 0 10 | MotionPrime.benchmark_data[key][:count] += 1 11 | MotionPrime.benchmark_data[key][:total] += time 12 | result 13 | else 14 | block.call 15 | end 16 | end 17 | 18 | def pp(*attrs) 19 | attrs = [*attrs] 20 | results = attrs.map.with_index do |entity, i| 21 | if entity.is_a?(Hash) 22 | "#{"\n" unless attrs[i-1].is_a?(Hash)}#{inspect_hash(entity)}\n" 23 | else 24 | entity.inspect 25 | end 26 | end 27 | NSLog(results.compact.join(' ')) 28 | attrs 29 | end 30 | 31 | def inspect_hash(hash, depth = 0) 32 | return '{}' if hash.blank? 33 | res = hash.map.with_index do |(key, value), i| 34 | k = "#{' '*depth}#{i.zero? ? '{' : ' '}#{key.inspect}=>" 35 | pair = if value.is_a?(Hash) 36 | "#{k}\n#{inspect_hash(value, depth + 1)}" 37 | else 38 | [k, value.inspect].join 39 | end 40 | if i == hash.count-1 41 | pair + '}' 42 | else 43 | pair + ",\n" 44 | end 45 | end 46 | res.join 47 | end 48 | 49 | def class_name_without_kvo 50 | self.class.name.gsub(/^NSKVONotifying_/, '') 51 | end 52 | 53 | def weak_ref 54 | WeakRef.new(self) 55 | end 56 | 57 | def strong_ref 58 | self 59 | end 60 | 61 | def allocate_strong_references(key = nil) 62 | unless self.respond_to?(:strong_references) 63 | Prime.logger.debug "User must define `strong_references` in `#{self.class.name}`" 64 | return false 65 | end 66 | 67 | refs = Array.wrap(self.strong_references).compact 68 | unless refs.present? 69 | Prime.logger.debug "`strong_references` are empty for `#{self.class.name}`" 70 | return false 71 | end 72 | 73 | @_strong_references ||= {} 74 | key ||= [@_strong_references.count, Time.now.to_i].join('_') 75 | @_strong_references[key] = refs.map(&:strong_ref) 76 | key 77 | end 78 | 79 | def release_strong_references(key = nil) 80 | unless self.respond_to?(:strong_references) 81 | Prime.logger.debug "User must define `strong_references` in `#{self.class.name}`" 82 | return false 83 | end 84 | key ||= @_strong_references.keys.last 85 | @_strong_references.try(:delete, key) 86 | key 87 | end 88 | 89 | def allocated_references_released? 90 | unless self.respond_to?(:strong_references) 91 | Prime.logger.debug "User must define `strong_references` in `#{self.class.name}`" 92 | return false 93 | end 94 | res = @_strong_references.all? { |key, refs| 95 | refs.all? { |ref| ref.retainCount - 1 <= @_strong_references.count } 96 | } 97 | Prime.logger.debug "Released `#{self.class.name}`" if res 98 | res 99 | end 100 | 101 | def clear_instance_variables(options = {}) 102 | ivars = self.instance_variables 103 | excluded_ivars = Array.wrap(options[:except]).map(&:to_s) 104 | clear_block = proc { |ivar| 105 | next if excluded_ivars.include?(ivar[1..-1]) 106 | self.instance_variable_set(ivar, nil) 107 | }.weak! 108 | ivars.each(&clear_block) 109 | end 110 | end -------------------------------------------------------------------------------- /motion-prime/core_ext/nil_class.rb: -------------------------------------------------------------------------------- 1 | class NilClass 2 | def weakref_alive? 3 | false 4 | end 5 | end -------------------------------------------------------------------------------- /motion-prime/core_ext/time.rb: -------------------------------------------------------------------------------- 1 | class Time 2 | def to_short_iso8601 3 | clone.utc.strftime("%Y%m%dT%H%M%SZ") 4 | end 5 | 6 | def self.short_iso8601(time) 7 | cached_date_formatter("yyyyMMdd'T'HHmmss'Z'"). 8 | dateFromString(time.gsub(/[\:\-]*/, '')) 9 | end 10 | end -------------------------------------------------------------------------------- /motion-prime/delegate/_base_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module DelegateBaseMixin 3 | attr_accessor :window 4 | 5 | def application(application, willFinishLaunchingWithOptions:opts) 6 | MotionPrime::Config.configure! 7 | MotionPrime::Styles.define! 8 | Prime.logger.info "Loading Prime application with env: #{Prime.env}" 9 | application.setStatusBarStyle UIStatusBarStyleLightContent 10 | application.setStatusBarHidden false 11 | end 12 | 13 | def application(application, didFinishLaunchingWithOptions:launch_options) 14 | on_load(application, launch_options) 15 | true 16 | end 17 | 18 | def application(application, didRegisterForRemoteNotificationsWithDeviceToken: token) 19 | on_apn_register_success(application, token) 20 | end 21 | 22 | def application(application, didFailToRegisterForRemoteNotificationsWithError: error) 23 | on_apn_register_fail(application, error) 24 | end 25 | 26 | def on_load(application, launch_options) 27 | end 28 | 29 | # Return the main controller. 30 | def main_controller 31 | window.rootViewController 32 | end 33 | 34 | # Return content controller (without sidebar) 35 | def content_controller 36 | main_controller.content_controller 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /motion-prime/delegate/_navigation_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module DelegateNavigationMixin 3 | def open_screen(screen, options = {}) 4 | screen = prepare_screen_for_open(screen, options) 5 | if options[:root] || !self.window 6 | open_root_screen(screen, options) 7 | else 8 | open_content_screen(screen, options) 9 | end 10 | end 11 | 12 | private 13 | def prepare_screen_for_open(screen, options = {}) 14 | Screen.create_with_options(screen, true, options) 15 | end 16 | 17 | def open_root_screen(screen, options = {}) 18 | screen.send(:on_screen_load) if screen.respond_to?(:on_screen_load) 19 | screen.wrap_in_navigation if screen.respond_to?(:wrap_in_navigation) 20 | 21 | screen = screen.main_controller.strong_ref if screen.respond_to?(:main_controller) 22 | 23 | self.window ||= UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 24 | if options[:animated] 25 | UIView.transitionWithView self.window, 26 | duration: 0.5, 27 | options: UIViewAnimationOptionTransitionFlipFromLeft, 28 | animations: proc { self.window.rootViewController = screen }, 29 | completion: nil 30 | else 31 | self.window.rootViewController = screen 32 | end 33 | self.window.makeKeyAndVisible 34 | screen 35 | end 36 | 37 | def open_content_screen(screen, options = {}) 38 | open_root_screen(screen) 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /motion-prime/delegate/app_delegate.rb: -------------------------------------------------------------------------------- 1 | motion_require '../helpers/has_authorization' 2 | motion_require './_base_mixin' 3 | motion_require './_navigation_mixin' 4 | module MotionPrime 5 | class BaseAppDelegate 6 | include HasAuthorization 7 | include DelegateBaseMixin 8 | include DelegateNavigationMixin 9 | 10 | def on_apn_register_success(application, token) 11 | end 12 | 13 | def on_apn_register_fail(application, error) 14 | end 15 | 16 | def current_user 17 | @current_user ||= if defined?(User) && User.respond_to?(:current) 18 | User.current 19 | end 20 | end 21 | 22 | def reset_current_user 23 | user_was = @current_user 24 | @current_user = nil 25 | NSNotificationCenter.defaultCenter.postNotificationName(:on_current_user_reset, object: user_was) 26 | api_client.access_token = current_user.try(:access_token) 27 | end 28 | 29 | def api_client 30 | @api_client ||= ApiClient.new(access_token: current_user.try(:access_token)) 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /motion-prime/elements/_content_padding_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module ElementContentPaddingMixin 3 | def content_padding_left 4 | computed_options[:padding_left] || 5 | computed_options[:padding] || 6 | default_padding_for(:left) || 0 7 | end 8 | 9 | def content_padding_right 10 | computed_options[:padding_right] || 11 | computed_options[:padding] || 12 | default_padding_for(:right) || 0 13 | end 14 | 15 | def content_padding_top 16 | computed_options[:padding_top] || 17 | computed_options[:padding] || 18 | default_padding_for(:top) || 0 19 | end 20 | 21 | def content_padding_bottom 22 | computed_options[:padding_bottom] || 23 | computed_options[:padding] || 24 | default_padding_for(:bottom) || 0 25 | end 26 | 27 | def content_padding_height 28 | content_padding_top + content_padding_bottom 29 | end 30 | 31 | def content_padding_width 32 | content_padding_left + content_padding_right 33 | end 34 | 35 | def content_outer_height(cached = false) 36 | height = content_padding_height + (cached ? cached_content_height : content_height) 37 | [[height, computed_options[:min_outer_height]].compact.max, computed_options[:max_outer_height]].compact.min 38 | end 39 | 40 | def cached_content_outer_height 41 | content_outer_height(true) 42 | end 43 | 44 | def content_outer_width(cached = false) 45 | width = content_padding_width + (cached ? cached_content_width : content_width) 46 | [[width, computed_options[:min_outer_width]].compact.max, computed_options[:max_outer_width]].compact.min 47 | end 48 | 49 | def cached_content_outer_width 50 | content_outer_width(true) 51 | end 52 | 53 | def default_padding_for(side) 54 | class_factory(view_class).send(:"default_padding_#{side}") 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /motion-prime/elements/_text_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module ElementTextMixin 3 | # Options 4 | # text 5 | # text_color 6 | # font 7 | # line_spacing 8 | # text_alignment 9 | # text_alignment_name 10 | # line_break_mode 11 | # underline (range) 12 | 13 | def html_string(options) 14 | styles = [] 15 | styles << "color: #{options[:text_color].hex};" if options[:text_color] 16 | styles << "line-height: #{options[:line_height] || (options[:line_spacing].to_f + options[:font].pointSize)}px;" 17 | styles << "font-family: '#{options[:font].familyName}';" 18 | styles << "font-size: #{options[:font].pointSize}px;" 19 | styles << "text-align: #{options[:text_alignment_name]};" if options[:text_alignment_name] 20 | 21 | html_options = { 22 | NSDocumentTypeDocumentAttribute => NSHTMLTextDocumentType, 23 | NSCharacterEncodingDocumentAttribute => NSNumber.numberWithInt(NSUTF8StringEncoding) 24 | } 25 | 26 | text = "#{options[:text]}" 27 | # WARNING: there is a bug - it uses WebKit and can not be used in secondary threads 28 | NSAttributedString.alloc.initWithData(text.dataUsingEncoding(NSUTF8StringEncoding), options: html_options, documentAttributes: nil, error: nil) 29 | end 30 | 31 | def attributed_string(options) 32 | attributes, paragrah_style = extract_attributed_string_options(options) 33 | 34 | prepared_text = NSMutableAttributedString.alloc.initWithString(options[:text].to_s, attributes: attributes) 35 | underline_range = options[:underline] 36 | fragment_color = options[:fragment_color] 37 | fragment_font = options[:fragment_font] 38 | if paragrah_style && (underline_range || fragment_color) && options.fetch(:number_of_lines, 1) == 1 39 | Prime.logger.debug "If attributed text has paragraph style and underline - you must set number of lines != 1" 40 | end 41 | 42 | if underline_range 43 | underline_range = [0, options[:text].length] if underline_range === true 44 | prepared_text.addAttributes({NSUnderlineStyleAttributeName => NSUnderlineStyleSingle}, range: underline_range) 45 | end 46 | if fragment_color 47 | prepared_text.addAttributes({NSForegroundColorAttributeName => fragment_color[:color].uicolor}, range: fragment_color[:range]) 48 | end 49 | if fragment_font 50 | prepared_text.addAttributes({NSFontAttributeName => fragment_font[:font].uifont}, range: fragment_font[:range]) 51 | end 52 | 53 | prepared_text 54 | end 55 | 56 | def extract_attributed_string_options(options) 57 | attributes = {} 58 | line_height = options[:line_height] 59 | line_spacing = options[:line_spacing] 60 | text_alignment = options[:text_alignment] 61 | line_break_mode = options[:line_break_mode] 62 | 63 | if line_height || line_spacing || text_alignment || line_break_mode 64 | paragrah_style = NSMutableParagraphStyle.alloc.init 65 | if line_height 66 | paragrah_style.setMinimumLineHeight(line_height) 67 | elsif line_spacing 68 | paragrah_style.setLineSpacing(line_spacing) 69 | end 70 | if text_alignment 71 | text_alignment = text_alignment.nstextalignment if text_alignment.is_a?(Symbol) 72 | paragrah_style.setAlignment(text_alignment) 73 | end 74 | if line_break_mode 75 | line_break_mode = line_break_mode.uilinebreakmode if line_break_mode.is_a?(Symbol) 76 | paragrah_style.setLineBreakMode(line_break_mode) 77 | end 78 | attributes[NSParagraphStyleAttributeName] = paragrah_style 79 | end 80 | if color = options[:text_color] || options[:title_color] 81 | attributes[NSForegroundColorAttributeName] = color.uicolor 82 | end 83 | if font = extract_font_from(options) 84 | attributes[NSFontAttributeName] = font.uifont 85 | end 86 | [attributes, paragrah_style] 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /motion-prime/elements/button.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class ButtonElement < BaseElement 3 | include MotionPrime::ElementContentPaddingMixin 4 | include MotionPrime::ElementContentTextMixin 5 | 6 | after_render :size_to_fit 7 | 8 | def size_to_fit 9 | if computed_options[:size_to_fit] 10 | if computed_options[:width] 11 | view.setHeight cached_content_outer_height 12 | end 13 | end 14 | end 15 | 16 | def view_class 17 | "MPButton" 18 | end 19 | 20 | def text_value 21 | view.try(:currentTitle) || computed_options[:title] 22 | end 23 | 24 | def font 25 | extract_font_from(computed_options[:title_label]) || :system.uifont 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /motion-prime/elements/collection_view_cell.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class TableViewCellElement < BaseElement 3 | def view_class 4 | "MPCollectionCellWithSection" 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /motion-prime/elements/draw.rb: -------------------------------------------------------------------------------- 1 | motion_require '../views/_frame_calculator_mixin' 2 | module MotionPrime 3 | class DrawElement < BaseElement 4 | # MotionPrime::DrawElement is container for drawRect method options. 5 | # Elements are located inside Sections 6 | 7 | include FrameCalculatorMixin 8 | include ElementContentPaddingMixin 9 | 10 | def draw_options 11 | options = computed_options 12 | background_color = options[:background_color].try(:uicolor) 13 | layer_options = options[:layer] || {} 14 | corner_radius = layer_options[:corner_radius].to_f 15 | 16 | layer_options.delete(:masks_to_bounds) if layer_options[:masks_to_bounds].nil? 17 | options.delete(:clips_to_bounds) if options[:clips_to_bounds].nil? 18 | masks_to_bounds = layer_options.fetch(:masks_to_bounds, options.fetch(:clips_to_bounds, corner_radius > 0)) 19 | { 20 | rect: CGRectMake(frame_left, frame_top, frame_outer_width, frame_outer_height), 21 | background_color: background_color, 22 | masks_to_bounds: masks_to_bounds, 23 | corner_radius: corner_radius, 24 | rounded_corners: layer_options[:rounded_corners], 25 | border_width: layer_options[:border_width].to_f, 26 | border_color: layer_options[:border_color].try(:uicolor) || background_color, 27 | border_sides: layer_options[:border_sides], 28 | dashes: layer_options[:dashes] 29 | } 30 | end 31 | 32 | def draw_in(rect) 33 | if @_prev_rect_size && @_prev_rect_size != rect.size 34 | reset_computed_values 35 | end 36 | @_prev_rect_size = rect.size 37 | end 38 | 39 | def render!; end 40 | 41 | def on_container_render 42 | @view = nil 43 | @computed_frame = nil 44 | end 45 | 46 | def view 47 | @view ||= section.container_view 48 | end 49 | 50 | def computed_frame 51 | @computed_frame ||= calculate_frame_for(view.try(:bounds) || section.container_bounds, computed_options) 52 | end 53 | 54 | def default_padding_for(side) 55 | 0 56 | end 57 | 58 | def bind_gesture(action, receiver = nil, target = nil) 59 | target ||= section 60 | target.bind_gesture_on_container_for(self, action, receiver.weak_ref) 61 | end 62 | 63 | def hide 64 | return if computed_options[:hidden] 65 | computed_options[:hidden] = true 66 | rerender! 67 | end 68 | 69 | def show 70 | return if !computed_options[:hidden] 71 | computed_options[:hidden] = false 72 | rerender! 73 | end 74 | 75 | def rerender!(changed_options = []) 76 | @_original_options = nil 77 | section.cached_draw_image = nil 78 | view.try(:setNeedsDisplay) 79 | end 80 | 81 | protected 82 | def frame_outer_width; computed_frame.size.width end 83 | def frame_width; frame_outer_width - content_padding_width end 84 | 85 | def frame_outer_height; computed_frame.size.height end 86 | def frame_height; frame_outer_height - content_padding_height end 87 | 88 | def frame_top; computed_frame.origin.y end 89 | def frame_inner_top; frame_top + content_padding_top end 90 | 91 | def frame_left; computed_frame.origin.x end 92 | def frame_inner_left; frame_left + content_padding_left end 93 | 94 | def frame_bottom; frame_top + frame_outer_height end 95 | def frame_inner_bottom; frame_bottom - content_padding_bottom end 96 | 97 | def frame_right; frame_left + frame_outer_width end 98 | def frame_inner_right; frame_right - content_padding_right end 99 | 100 | def reset_computed_values 101 | super 102 | @computed_frame = nil 103 | end 104 | 105 | class << self 106 | def factory(type, options = {}) 107 | return unless %w[view label image].include?(type.to_s.downcase) 108 | class_factory("#{type}_draw_element", true).new(options) 109 | end 110 | end 111 | end 112 | end -------------------------------------------------------------------------------- /motion-prime/elements/draw/view.rb: -------------------------------------------------------------------------------- 1 | motion_require '../draw.rb' 2 | module MotionPrime 3 | class ViewDrawElement < DrawElement 4 | include DrawBackgroundMixin 5 | 6 | def draw_in(rect) 7 | super 8 | draw_in_context(UIGraphicsGetCurrentContext()) 9 | end 10 | 11 | def draw_in_context(context) 12 | return if computed_options[:hidden] 13 | 14 | draw_background_in_context(context) 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /motion-prime/elements/error_message.rb: -------------------------------------------------------------------------------- 1 | motion_require './label' 2 | module MotionPrime 3 | class ErrorMessageElement < LabelElement 4 | include MotionPrime::ElementContentPaddingMixin 5 | include MotionPrime::ElementContentTextMixin 6 | 7 | def view_class 8 | "MPLabel" 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /motion-prime/elements/google_map.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class GoogleMapElement < BaseElement 3 | def view_class 4 | "GMSMapView" 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /motion-prime/elements/image.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class ImageElement < BaseElement 3 | after_render :fetch_image 4 | 5 | def view_class 6 | "UIImageView" 7 | end 8 | 9 | def fetch_image 10 | return unless computed_options[:url] 11 | unless computed_options[:default] === false 12 | raise "You must set default image for `#{name}`" unless computed_options[:default] 13 | view.setImage(computed_options[:default].uiimage) 14 | end 15 | refs = strong_references 16 | BW::Reactor.schedule do 17 | return unless refs.all?(&:weakref_alive?) 18 | manager = SDWebImageManager.sharedManager 19 | manager.downloadWithURL(computed_options[:url], 20 | options: 0, 21 | progress: lambda{ |r_size, e_size| }, 22 | completed: lambda{ |image, error, type, finished| 23 | return if !image || !refs.all?(&:weakref_alive?) 24 | 25 | if computed_options[:post_process].present? 26 | image = computed_options[:post_process][:method].to_proc.call(computed_options[:post_process][:target], image) 27 | end 28 | 29 | self.performSelectorOnMainThread :set_image, withObject: image, waitUntilDone: true 30 | } 31 | ) 32 | end 33 | end 34 | 35 | def set_image(*args) 36 | self.view.setImage(args) 37 | end 38 | 39 | def strong_references 40 | # .compact() is required here, otherwise screen will not be released 41 | refs = [section, (section.collection_section if section.respond_to?(:cell_section_name))].compact 42 | refs += section.strong_references if section 43 | refs 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /motion-prime/elements/label.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class LabelElement < BaseElement 3 | include ElementContentPaddingMixin 4 | include ElementContentTextMixin 5 | include ElementTextMixin 6 | 7 | before_render :size_to_fit_if_needed 8 | after_render :size_to_fit 9 | 10 | def view_class 11 | "MPLabel" 12 | end 13 | 14 | def size_to_fit 15 | if computed_options[:size_to_fit] 16 | if computed_options[:width] 17 | view.setHeight([cached_content_outer_height, computed_options[:height]].compact.min) 18 | else 19 | view.sizeToFit 20 | # we should re-set values, because sizeToFit do not use padding 21 | view.setWidth(view.bounds.size.width + content_padding_width) 22 | view.setHeight(computed_options[:height] || (view.bounds.size.height + content_padding_height)) 23 | end 24 | end 25 | end 26 | 27 | def size_to_fit_if_needed 28 | if computed_options[:size_to_fit] && computed_options[:width] 29 | @computed_options[:height_to_fit] = content_outer_height 30 | end 31 | end 32 | 33 | def set_text(value) 34 | options[:text] = computed_options[:text] = value 35 | styler = ViewStyler.new(view, CGRectZero, computed_options) 36 | if styler.options[:attributed_text] 37 | view.attributedText = styler.options[:attributed_text] 38 | else 39 | view.text = value 40 | end 41 | @content_height = nil 42 | size_to_fit 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /motion-prime/elements/map.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class MapElement < BaseElement 3 | after_render :remove_watermark 4 | def view_class 5 | "MKMapView" 6 | end 7 | 8 | def remove_watermark 9 | self.view.subviews.last.removeFromSuperview 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /motion-prime/elements/page_view_controller.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class PageViewControllerElement < BaseElement 3 | after_render :set_delegated 4 | def view_class 5 | "UIPageViewController" 6 | end 7 | 8 | def set_delegated 9 | if computed_options.has_key?(:delegate) && computed_options[:delegate].respond_to?(:delegated_by) && section.respond_to?(:page_controller) 10 | computed_options[:delegate].delegated_by(section.page_controller) 11 | end 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /motion-prime/elements/progress_hud.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class ProgressHudElement < BaseElement 3 | def view_class 4 | "MBProgressHUD" 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /motion-prime/elements/spinner.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class SpinnerElement < BaseElement 3 | def view_class 4 | "MPSpinner" 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /motion-prime/elements/table_header.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class TableHeaderElement < BaseElement 3 | def view_class 4 | "MPTableHeaderWithSectionView" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /motion-prime/elements/table_view.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class TableViewElement < BaseElement 3 | def view_class 4 | "MPTableView" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /motion-prime/elements/table_view_cell.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class TableViewCellElement < BaseElement 3 | def view_class 4 | "MPTableCellWithSection" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /motion-prime/elements/text_field.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class TextFieldElement < BaseElement 3 | include MotionPrime::ElementContentPaddingMixin 4 | include MotionPrime::ElementContentTextMixin 5 | 6 | def view_class 7 | "MPTextField" 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /motion-prime/elements/text_view.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class TextViewElement < BaseElement 3 | include MotionPrime::ElementContentPaddingMixin 4 | include MotionPrime::ElementContentTextMixin 5 | 6 | def view_class 7 | "MPTextView" 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /motion-prime/elements/view_with_section.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class ViewWithSectionElement < BaseElement 3 | def view_class 4 | "MPViewWithSection" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /motion-prime/elements/web_view.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class WebViewElement < BaseElement 3 | def view_class 4 | "UIWebView" 5 | end 6 | 7 | def dealloc 8 | view.try(:setDelegate, nil) 9 | super 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /motion-prime/env.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class Env 3 | def env 4 | (defined?(NSBundle) && NSBundle.mainBundle.objectForInfoDictionaryKey('PRIME_ENV')) || 5 | ENV['PRIME_ENV'] || 6 | ENV['RUBYMOTION_ENV'] || 7 | (defined?(RUBYMOTION_ENV) && RUBYMOTION_ENV) || 8 | 'development' 9 | end 10 | 11 | def to_s 12 | env 13 | end 14 | 15 | def inspect 16 | env 17 | end 18 | 19 | def ==(obj) 20 | env == obj 21 | end 22 | 23 | def method_missing(name, *args, &block) 24 | if /(.+)?$/.match(name.to_s) 25 | env == name.to_s.gsub('?', '') 26 | else 27 | false 28 | end 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /motion-prime/helpers/has_authorization.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module HasAuthorization 3 | def current_user 4 | App.delegate.current_user 5 | end 6 | def reset_current_user 7 | App.delegate.reset_current_user 8 | end 9 | def user_signed_in? 10 | current_user.present? 11 | end 12 | def api_client 13 | App.delegate.api_client 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /motion-prime/helpers/has_class_factory.rb: -------------------------------------------------------------------------------- 1 | # These things required because camelize/constantize/classify methods are very slow 2 | module MotionPrime 3 | module HasClassFactory 4 | def class_factory(name, is_mp_class = false) 5 | if is_mp_class 6 | value = Prime.class_factory_cache["motion_prime/#{name}"] 7 | return value if value 8 | class_name = camelize_factory(name) 9 | 10 | return nil unless MotionPrime.const_defined?(class_name) 11 | class_name = "MotionPrime::#{class_name}" 12 | name = "motion_prime/#{name}" 13 | else 14 | value = Prime.class_factory_cache[name] 15 | return value if value 16 | class_name = camelize_factory(name) 17 | end 18 | Prime.class_factory_cache[name] = class_name.constantize 19 | end 20 | 21 | def camelize_factory(name) 22 | value = Prime.camelize_factory_cache[name] 23 | return value if value 24 | Prime.camelize_factory_cache[name] = name.camelize 25 | end 26 | 27 | def underscore_factory(name) 28 | value = Prime.underscore_factory_cache[name] 29 | return value if value 30 | Prime.underscore_factory_cache[name] = name.underscore 31 | end 32 | 33 | def low_camelize_factory(name) 34 | value = Prime.low_camelize_factory_cache[name] 35 | return value if value 36 | Prime.low_camelize_factory_cache[name] = name.camelize(:lower) 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /motion-prime/helpers/has_normalizer.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module HasNormalizer 3 | def normalize_options(unordered_options, receiver = nil, order = nil, keys = nil) 4 | options = if order 5 | Hash[unordered_options.sort_by { |k,v| order.index(k.to_s).to_i }] 6 | else 7 | unordered_options 8 | end 9 | 10 | filtered_options = keys.nil? ? options : options.slice(*keys) 11 | filtered_options.keys.each do |key| 12 | @_key_chain = [key] if Prime.env.development? 13 | unordered_options[key] = normalize_object(filtered_options[key], receiver) 14 | end 15 | unordered_options 16 | end 17 | 18 | def normalize_object(object, receiver = nil) 19 | receiver ||= self 20 | if object.is_a?(Proc) 21 | normalize_value(object, receiver) 22 | elsif object.is_a?(Hash) 23 | object.inject({}) do |result, (key, nested_object)| 24 | if Prime.env.development? 25 | # FIXME: malloc 26 | @_key_chain ||= [] 27 | @_key_chain << key 28 | end 29 | result.merge(key => normalize_object(object[key], receiver)) 30 | end 31 | else 32 | object 33 | end 34 | end 35 | 36 | def normalize_value(object, receiver = nil) 37 | receiver ||= self 38 | if element? 39 | receiver.send(:instance_exec, section || screen, self, &object) 40 | else 41 | receiver.send(:instance_exec, self, &object) 42 | end 43 | rescue => e 44 | Prime.logger.error "Can't normalize: ", *debug_info, @_key_chain 45 | raise e 46 | end 47 | 48 | def element? 49 | self.is_a?(BaseElement) 50 | end 51 | 52 | def debug_info 53 | if element? 54 | [self.class.name, self.name, section.try(:name)] 55 | elsif self.is_a?(Section) 56 | [self.class.name, self.name, @collection_section.try(:class).try(:name)] 57 | else 58 | [self.class.name] 59 | end 60 | end 61 | end 62 | end -------------------------------------------------------------------------------- /motion-prime/helpers/has_search_bar.rb: -------------------------------------------------------------------------------- 1 | # This module adds search functionality, to Screen or TableSection 2 | module MotionPrime 3 | module HasSearchBar 4 | def add_search_bar(options = {}, &block) 5 | @_search_timeout = options.delete(:timeout) 6 | target = options.delete(:target) 7 | 8 | @_search_bar = create_search_bar(options) 9 | @_search_bar.setDelegate self 10 | 11 | if target 12 | target.addSubview @_search_bar 13 | elsif is_a?(TableSection) 14 | self.collection_view.tableHeaderView = @_search_bar 15 | end 16 | 17 | @search_callback = block 18 | @_search_bar 19 | rescue 20 | NSLog("can't add search bar to #{self.class_name_without_kvo}") 21 | end 22 | 23 | def dealloc 24 | BW::Reactor.cancel_timer(@_search_timer) if @_search_timer 25 | @_search_bar.try(:setDelegate, nil) 26 | @_search_bar = nil 27 | super 28 | end 29 | 30 | def create_search_bar(options = {}) 31 | name = is_a?(TableSection) ? name : self.class_name_without_kvo.underscore 32 | screen = is_a?(TableSection) ? self.screen : self 33 | options[:styles] ||= [] 34 | options[:styles] += [:"base_search_bar", :"base_#{name}_search_bar", :"#{name}_search_bar"] 35 | 36 | screen.search_bar(options).view 37 | end 38 | 39 | def searchBar(search_bar, textDidChange: text) 40 | BW::Reactor.cancel_timer(@_search_timer) if @_search_timer 41 | if @_search_timeout 42 | @_search_timer = BW::Reactor.add_timer(@_search_timeout.to_f/1000, proc{ @search_callback.call(text) }.weak!) 43 | else 44 | @search_callback.call(text) 45 | end 46 | end 47 | 48 | def searchBarSearchButtonClicked(search_bar) 49 | BW::Reactor.cancel_timer(@_search_timer) if @_search_timer 50 | @search_callback.call(search_bar.text) 51 | search_bar.resignFirstResponder 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /motion-prime/helpers/has_style_chain_builder.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module HasStyleChainBuilder 3 | def build_styles_chain(base_styles, suffixes) 4 | styles = [] 5 | [*base_styles].each do |base_style| 6 | [*suffixes].each do |suffix| 7 | components = [] 8 | # don't use present? here, it's slower, while this method should be very fast 9 | if base_style && base_style != '' && suffix && suffix != '' 10 | styles << [base_style.to_s, suffix.to_s].join('_').to_sym 11 | end 12 | end 13 | end 14 | styles 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /motion-prime/helpers/has_style_options.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module HasStyleOptions 3 | def extract_font_from(options, prefix = nil) 4 | options ||= {} 5 | return options[:font] if options[:font].present? 6 | 7 | name_key = [prefix, 'font_name'].compact.join('_').to_sym 8 | size_key = [prefix, 'font_size'].compact.join('_').to_sym 9 | if options.slice(size_key, name_key).any? 10 | font_name = options[name_key] || :system 11 | font_size = options[size_key] || 14 12 | font_name.uifont(font_size) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /motion-prime/helpers/has_styles.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module HasStyles 3 | def prepare_gradient(options) 4 | colors = options[:colors].map(&:uicolor).map(&:cgcolor) 5 | locations = options[:locations] if options[:locations] 6 | type = options[:type].to_s 7 | 8 | if self.is_a?(ViewStyler) 9 | gradient = CAGradientLayer.layer 10 | if type == 'horizontal' 11 | gradient.startPoint = CGPointMake(0, 0.5) 12 | gradient.endPoint = CGPointMake(1.0, 0.5) 13 | end 14 | gradient.frame = if options[:frame_width] 15 | CGRectMake(options[:frame_x].to_f, options[:frame_y].to_f, options[:frame_width].to_f, options[:frame_height].to_f) 16 | else 17 | options[:parent_frame] || CGRectZero 18 | end 19 | 20 | gradient.colors = colors 21 | gradient.locations = locations 22 | else 23 | color_space = CGColorSpaceCreateDeviceRGB() 24 | locations_pointer = Pointer.new(:float, 2) 25 | locations.each_with_index { |loc, id| locations_pointer[id] = loc } 26 | gradient = CGGradientCreateWithColors(color_space, colors, locations_pointer) 27 | # CGColorSpaceRelease(color_space) 28 | end 29 | gradient 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /motion-prime/models/_dirty_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module ModelDirtyMixin 3 | extend ::MotionSupport::Concern 4 | 5 | def self.included(base) 6 | base.class_attribute :_changed_attributes 7 | end 8 | 9 | def track_changed_attributes(&block) 10 | @_tracking_changes = true 11 | @_changed_attributes ||= {} 12 | old_attrs = self.info.clone 13 | result = block.call 14 | new_attrs = self.info.clone 15 | new_bags = self._bags.clone 16 | new_attrs.each do |key, value| 17 | if value != old_attrs[key] && !@_changed_attributes.has_key?(key.to_s) 18 | @_changed_attributes[key.to_s] = old_attrs[key] 19 | end 20 | end 21 | new_bags.each do |key, value| 22 | if value.key != old_attrs[key] && !@_changed_attributes.has_key?(key.to_s) 23 | @_changed_attributes[key.to_s] = old_attrs[key] 24 | end 25 | end 26 | @_tracking_changes = false 27 | result 28 | end 29 | 30 | def changed_attributes 31 | @_changed_attributes ||= {} 32 | end 33 | 34 | def reset_changed_attributes 35 | @_changed_attributes = {} 36 | end 37 | 38 | # Return true if model was changed 39 | # 40 | # @param key [Symbol,String] (not required) will return result only for that attribute if specified 41 | # @return result [Boolean] result 42 | def has_changed?(key = nil) 43 | if key 44 | changed_attributes.has_key?(key.to_s) 45 | else 46 | changed_attributes.present? 47 | end 48 | end 49 | 50 | def save! 51 | super 52 | reset_changed_attributes 53 | self 54 | end 55 | 56 | # Reverts model changes and returns saved version 57 | # 58 | # @return model [MotionPrime::Model] reloaded model 59 | def reload 60 | changed_attributes.each do |key, value| 61 | self.info[key] = value 62 | end 63 | reset_changed_attributes 64 | self 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /motion-prime/models/_filter_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module FilterMixin 3 | def filter_array(data, find_options = {}, sort_options = nil) 4 | data = data.select do |entity| 5 | find_options.all? do |field, value| 6 | if value.is_a?(Array) 7 | value.include?(entity.info[field].to_s) 8 | else 9 | entity.info[field] == value 10 | end 11 | end 12 | end if find_options.present? 13 | 14 | data.sort! do |a, b| 15 | left_part = [] 16 | right_part = [] 17 | 18 | sort_options[:sort].each do |(k,v)| 19 | left = a.send(k) 20 | right = b.send(k) 21 | if left.class != right.class 22 | left = left.to_s 23 | right = right.to_s 24 | end 25 | left, right = right, left if v.to_s == 'desc' 26 | left_part << left 27 | right_part << right 28 | end 29 | left_part <=> right_part 30 | end if sort_options.try(:[], :sort).present? 31 | data 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /motion-prime/models/_timestamps_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module ModelTimestampsMixin 3 | extend ::MotionSupport::Concern 4 | 5 | def self.included(base) 6 | base.class_attribute :_timestamp_attributes 7 | end 8 | 9 | def save! 10 | time = Time.now 11 | trigger_timestamp(:save, time) 12 | trigger_timestamp(:create, time) if new_record? 13 | super 14 | end 15 | 16 | def trigger_timestamp(action_name, time) 17 | field = (_timestamp_attributes || {})[action_name] 18 | return unless field 19 | self.send(:"#{field}=", time) 20 | end 21 | 22 | module ClassMethods 23 | def timestamp_attributes(actions = nil) 24 | self._timestamp_attributes ||= {} 25 | actions ||= {save: :saved_at, create: :created_at} 26 | actions.each do |action_name, field| 27 | self._timestamp_attributes[action_name.to_sym] = field 28 | self.attribute field, type: :time 29 | end 30 | end 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /motion-prime/models/errors.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class Errors 3 | attr_accessor :info 4 | attr_reader :changes 5 | 6 | def initialize(model) 7 | @info = MotionSupport::HashWithIndifferentAccess.new 8 | @changes = MotionSupport::HashWithIndifferentAccess.new 9 | @model = model 10 | model.class.attributes.map(&:to_sym).each do |key| 11 | initialize_for_key key 12 | end 13 | end 14 | 15 | def to_hash 16 | @info 17 | end 18 | 19 | def get(key) 20 | initialize_for_key(key) 21 | to_hash[key] 22 | end 23 | 24 | def set(key, errors, options = {}) 25 | initialize_for_key(key) 26 | 27 | track_changed options do 28 | to_hash[key] = Array.wrap(errors) 29 | end 30 | end 31 | 32 | def add(key, error, options = {}) 33 | initialize_for_key(key) 34 | track_changed do 35 | to_hash[key] << error 36 | end 37 | end 38 | 39 | def [](key) 40 | get(key) 41 | end 42 | 43 | def []=(key, errors) 44 | set(key, errors) 45 | end 46 | 47 | def reset_for(key, options = {}) 48 | track_changed options do 49 | to_hash[key] = [] 50 | end 51 | end 52 | 53 | def reset 54 | track_changed do 55 | to_hash.keys.each do |key| 56 | reset_for(key, silent: true) 57 | end 58 | end 59 | end 60 | 61 | def messages 62 | to_hash.values.flatten 63 | end 64 | 65 | def blank? 66 | messages.none? 67 | end 68 | 69 | def present? 70 | !blank? 71 | end 72 | 73 | def to_s 74 | messages.join(';') 75 | end 76 | 77 | def track_changed(options = {}) 78 | return yield if options[:silent] 79 | @changes = MotionSupport::HashWithIndifferentAccess.new 80 | saved_info = to_hash.clone 81 | willChangeValueForKey(:info) 82 | yield 83 | to_hash.each do |key, value| 84 | @changes[key] = [value, saved_info[key]] unless value == saved_info[key] 85 | end 86 | didChangeValueForKey(:info) 87 | end 88 | 89 | private 90 | def initialize_for_key(key) 91 | key = key.to_sym 92 | return if @info.has_key?(key) 93 | 94 | to_hash[key] ||= [] 95 | end 96 | end 97 | end -------------------------------------------------------------------------------- /motion-prime/models/exceptions.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class StoreError < StandardError; end 3 | class SyncError < StandardError; end 4 | end -------------------------------------------------------------------------------- /motion-prime/models/json.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class JsonParseError < StandardError; end 3 | 4 | class JSON 5 | PARAMETRIZE_CLASSES = [Time, Date] 6 | 7 | # Parses a string or data object and converts it in data structure. 8 | # 9 | # @param [String, NSData] str_data the string or data to convert. 10 | # @raise [JsonParseError] If the parsing of the passed string/data isn't valid. 11 | # @return [Hash, Array, NilClass] the converted data structure, nil if the incoming string isn't valid. 12 | def self.parse(str_data, &block) 13 | return nil unless str_data 14 | data = str_data.respond_to?(:to_data) ? str_data.to_data : str_data 15 | if data.respond_to?(:dataUsingEncoding) 16 | data = data.dataUsingEncoding(NSUTF8StringEncoding) 17 | end 18 | opts = NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves | NSJSONReadingAllowFragments 19 | error = Pointer.new(:id) 20 | obj = NSJSONSerialization.JSONObjectWithData(data, options: opts, error: error) 21 | raise JsonParseError, error[0].description if error[0] 22 | obj 23 | end 24 | 25 | # Generates a string from data structure. 26 | # 27 | # @param [String, Fixnum, Array, Hash, Nil] obj the object to serialize. 28 | # @param [Boolean] parametrize option to parametrize data before serialization. 29 | # @return [String] the serialized data json. 30 | def self.generate(obj, parametrize = true) 31 | if parametrize && obj.is_a?(Hash) 32 | obj.each do |key, value| 33 | obj[key] = value.to_s if PARAMETRIZE_CLASSES.include?(value.class) 34 | end 35 | end 36 | if parametrize && obj.is_a?(Array) 37 | obj.map! do |value| 38 | PARAMETRIZE_CLASSES.include?(value.class) ? value.to_s : value 39 | end 40 | end 41 | data = NSJSONSerialization.dataWithJSONObject(obj, options: 0, error: nil) 42 | data.to_str 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /motion-prime/models/model.rb: -------------------------------------------------------------------------------- 1 | motion_require '../helpers/has_authorization' 2 | motion_require './_nano_bag_mixin' 3 | motion_require './_finder_mixin' 4 | motion_require './_base_mixin' 5 | motion_require './_sync_mixin' 6 | motion_require './_association_mixin' 7 | motion_require './_dirty_mixin' 8 | motion_require './store' 9 | motion_require './store_extension' 10 | module MotionPrime 11 | class Model < NSFNanoObject 12 | include MotionPrime::HasAuthorization 13 | include MotionPrime::HasNormalizer 14 | include MotionPrime::ModelBaseMixin 15 | include MotionPrime::ModelAssociationMixin 16 | include MotionPrime::ModelSyncMixin 17 | include MotionPrime::ModelFinderMixin 18 | include MotionPrime::ModelDirtyMixin 19 | include MotionPrime::ModelTimestampsMixin 20 | 21 | attribute :bag_key # need this as we use shared store; each nested resource must belong to parent bag 22 | attribute :id 23 | 24 | def errors 25 | @errors ||= Errors.new(self.weak_ref) 26 | end 27 | 28 | def set_errors(data) 29 | errors.track_changed do 30 | data.symbolize_keys.each do |key, error_messages| 31 | errors.set(key, error_messages, silent: true) if error_messages.present? 32 | end 33 | end 34 | end 35 | 36 | def dealloc 37 | Prime.logger.dealloc_message :model, self 38 | super 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /motion-prime/models/store.rb: -------------------------------------------------------------------------------- 1 | motion_require '../config/base' 2 | module MotionPrime 3 | class Store 4 | def self.create(type = nil, path = nil) 5 | error_ptr = Pointer.new(:id) 6 | case type || MotionPrime::Config.model.store_type.to_sym 7 | when :memory 8 | store = NSFNanoStore.createAndOpenStoreWithType(NSFMemoryStoreType, path: nil, error: error_ptr) 9 | when :temporary, :temp 10 | store = NSFNanoStore.createAndOpenStoreWithType(NSFTemporaryStoreType, path: nil, error: error_ptr) 11 | when :persistent, :file 12 | path ||= begin 13 | documents_path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0] 14 | documents_path + "/nano_#{Prime.env.to_s}.db" 15 | end 16 | store = NSFNanoStore.createAndOpenStoreWithType(NSFPersistentStoreType, path: path, error: error_ptr) 17 | else 18 | raise StoreError.new("unexpected store type (#{type}), must be one of: :memory, :temporary or :persistent") 19 | end 20 | 21 | raise StoreError, error_ptr[0].description if error_ptr[0] 22 | store 23 | end 24 | 25 | def self.connect!(type = nil) 26 | self.shared_store = create(type) 27 | end 28 | 29 | def self.connect(type = nil) 30 | connect!(type) unless connected? 31 | end 32 | 33 | def self.connected? 34 | !!shared_store 35 | end 36 | 37 | def self.disconnect 38 | self.shared_store = nil 39 | end 40 | 41 | def self.shared_store 42 | @shared_store 43 | end 44 | 45 | def self.shared_store=(store) 46 | @shared_store = store 47 | end 48 | 49 | def self.debug=(debug) 50 | NSFSetIsDebugOn(debug) 51 | end 52 | end 53 | end -------------------------------------------------------------------------------- /motion-prime/prime.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | def self.class_factory_cache 3 | @class_factory_cache ||= {} 4 | end 5 | 6 | def self.camelize_factory_cache 7 | @camelize_factory_cache ||= {} 8 | end 9 | 10 | def self.low_camelize_factory_cache 11 | @low_camelize_factory_cache ||= {} 12 | end 13 | 14 | def self.underscore_factory_cache 15 | @underscore_factory_cache ||= {} 16 | end 17 | 18 | def self.benchmark_data 19 | @benchmark_data ||= {} 20 | end 21 | 22 | def self.env 23 | @env ||= MotionPrime::Env.new 24 | end 25 | 26 | def self.logger 27 | @logger ||= MotionPrime::Logger.new 28 | end 29 | 30 | def self.logger=(value) 31 | @logger = value 32 | end 33 | end 34 | ::Prime = MotionPrime unless defined?(::Prime) -------------------------------------------------------------------------------- /motion-prime/screens/_aliases_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module ScreenAliasesMixin 3 | def view_did_load; end 4 | def view_will_appear(animated) 5 | self.will_appear 6 | end 7 | def will_appear; end 8 | 9 | def view_did_appear(animated) 10 | self.on_appear 11 | end 12 | def on_appear; end 13 | 14 | def view_will_disappear(animated) 15 | self.will_disappear 16 | end 17 | def will_disappear; end 18 | 19 | def view_did_disappear(animated) 20 | self.on_disappear 21 | end 22 | def on_disappear; end 23 | 24 | def will_rotate(orientation, duration); end 25 | 26 | def should_autorotate 27 | true 28 | end 29 | 30 | def on_rotate; end; 31 | end 32 | end -------------------------------------------------------------------------------- /motion-prime/screens/_base_mixin.rb: -------------------------------------------------------------------------------- 1 | motion_require "./_aliases_mixin" 2 | motion_require "./_orientations_mixin" 3 | motion_require "./_navigation_mixin" 4 | motion_require "./_sections_mixin" 5 | module MotionPrime 6 | module ScreenBaseMixin 7 | extend ::MotionSupport::Concern 8 | 9 | include ::MotionSupport::Callbacks 10 | include MotionPrime::ScreenAliasesMixin 11 | include MotionPrime::ScreenOrientationsMixin 12 | include MotionPrime::ScreenNavigationMixin 13 | include MotionPrime::ScreenSectionsMixin 14 | 15 | attr_accessor :parent_screen, :modal, :params, :options, :tab_bar, :action 16 | class_attribute :current_screen 17 | 18 | def app_delegate 19 | UIApplication.sharedApplication.delegate 20 | end 21 | 22 | def parent_screen=(value) 23 | @parent_screen = value.try(:weak_ref) 24 | end 25 | 26 | # Setup the screen, this method will be called when you run MPViewController.new 27 | # @param options [hash] Options passed to setup 28 | # @return [MotionPrime::Screen] Ready to use screen 29 | def on_create(options = {}) 30 | unless self.is_a?(UIViewController) 31 | raise StandardError.new("ERROR: Screens must extend UIViewController.") 32 | end 33 | options[:action] ||= 'render' 34 | self.options = options 35 | self.params = options[:params] || {} 36 | options.each do |k, v| 37 | self.send("#{k}=", v) if self.respond_to?("#{k}=") 38 | end 39 | self 40 | end 41 | 42 | def action?(action) 43 | self.action == action.to_s 44 | end 45 | 46 | def modal? 47 | !!self.modal 48 | end 49 | 50 | def title 51 | title = self.class.title 52 | title = self.instance_eval(&title) if title.is_a?(Proc) 53 | title 54 | end 55 | 56 | def title=(new_title) 57 | self.class.title(new_title) 58 | self.navigationItem.title = new_title 59 | end 60 | alias_method :set_title, :title= 61 | 62 | # Return the main controller. 63 | def main_controller 64 | has_navigation? ? navigation_controller : self 65 | end 66 | 67 | # Return content controller (without sidebar) 68 | def content_controller 69 | self 70 | end 71 | 72 | # Class methods 73 | module ClassMethods 74 | def title(t = nil, &block) 75 | if block_given? 76 | @title = block 77 | else 78 | t ? @title = t : @title ||= self.to_s 79 | end 80 | end 81 | def before_render(*method_names, &block) 82 | set_callback :render, :before, *method_names, &block 83 | end 84 | def after_render(*method_names, &block) 85 | set_callback :render, :after, *method_names, &block 86 | end 87 | def create_with_options(screen, navigation = true, options = {}) 88 | screen = create_tab_bar(screen, options) if screen.is_a?(Array) 89 | if screen.is_a?(Symbol) || screen.is_a?(String) 90 | screen_name, action_name = screen.to_s.split('#') 91 | options[:action] ||= action_name || 'render' 92 | options[:navigation] = navigation unless options.has_key?(:navigation) 93 | screen = class_factory("#{screen_name}_screen").new(options) 94 | end 95 | screen 96 | end 97 | 98 | def create_tab_bar(screens, options = {}) 99 | MotionPrime::TabBarController.new(screens, options) 100 | end 101 | end 102 | end 103 | end -------------------------------------------------------------------------------- /motion-prime/screens/_orientations_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module ScreenOrientationsMixin 3 | def should_rotate(orientation) 4 | case orientation 5 | when UIInterfaceOrientationPortrait 6 | return supported_orientation?("UIInterfaceOrientationPortrait") 7 | when UIInterfaceOrientationLandscapeLeft 8 | return supported_orientation?("UIInterfaceOrientationLandscapeLeft") 9 | when UIInterfaceOrientationLandscapeRight 10 | return supported_orientation?("UIInterfaceOrientationLandscapeRight") 11 | when UIInterfaceOrientationPortraitUpsideDown 12 | return supported_orientation?("UIInterfaceOrientationPortraitUpsideDown") 13 | else 14 | false 15 | end 16 | end 17 | 18 | def supported_orientation?(orientation) 19 | NSBundle.mainBundle.infoDictionary["UISupportedInterfaceOrientations"].include?(orientation) 20 | end 21 | 22 | def supported_orientations 23 | ors = 0 24 | NSBundle.mainBundle.infoDictionary["UISupportedInterfaceOrientations"].each do |ori| 25 | case ori 26 | when "UIInterfaceOrientationPortrait" 27 | ors |= UIInterfaceOrientationMaskPortrait 28 | when "UIInterfaceOrientationLandscapeLeft" 29 | ors |= UIInterfaceOrientationMaskLandscapeLeft 30 | when "UIInterfaceOrientationLandscapeRight" 31 | ors |= UIInterfaceOrientationMaskLandscapeRight 32 | when "UIInterfaceOrientationPortraitUpsideDown" 33 | ors |= UIInterfaceOrientationMaskPortraitUpsideDown 34 | end 35 | end 36 | ors 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /motion-prime/screens/_sections_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module ScreenSectionsMixin 3 | extend ::MotionSupport::Concern 4 | 5 | include HasClassFactory 6 | include HasNormalizer 7 | 8 | def self.included(base) 9 | base.class_attribute :_section_options 10 | end 11 | 12 | attr_accessor :_action_section_options 13 | 14 | def main_section 15 | @main_section || all_sections.first 16 | end 17 | 18 | def main_section=(value) 19 | @main_section = value 20 | end 21 | 22 | def all_sections 23 | Array.wrap(@sections.try(:values)) 24 | end 25 | 26 | def all_sections_with_main 27 | (all_sections + [main_section]).compact.uniq 28 | end 29 | 30 | def set_section(name, options = {}) 31 | self._action_section_options ||= {} 32 | self._action_section_options[name.to_sym] = options 33 | end 34 | alias_method :section, :set_section 35 | 36 | def set_sections_wrapper(value) 37 | self.class.set_sections_wrapper(value) 38 | end 39 | 40 | def refresh 41 | all_sections_with_main.each { |s| s.try(:reload) } 42 | end 43 | 44 | protected 45 | def add_sections 46 | @main_section ||= nil 47 | create_sections 48 | render_sections 49 | end 50 | 51 | def create_sections 52 | section_options = self.class.section_options.merge(action_section_options) 53 | return unless section_options 54 | @sections = {} 55 | section_options.map do |name, options| 56 | if options[:instance] 57 | section = options[:instance] 58 | else 59 | section = create_section(name, options.clone) 60 | end 61 | @sections[name] = section if section 62 | end 63 | end 64 | 65 | def create_section(name, options) 66 | section_class = class_factory("#{name}_section") 67 | options = normalize_options(options).merge(screen: self) 68 | !options.has_key?(:if) || options[:if] ? section_class.new(options) : nil 69 | end 70 | 71 | def action_section_options 72 | _action_section_options || {} 73 | end 74 | 75 | def sections_wrapper 76 | self.class.sections_wrapper 77 | end 78 | 79 | def render_sections 80 | return unless @sections.present? 81 | table_wrap = sections_wrapper.nil? ? all_sections.count > 1 : sections_wrapper 82 | if table_wrap 83 | table_class = table_wrap.is_a?(TrueClass) ? MotionPrime::TableSection : table_class 84 | @main_section = table_class.new(model: all_sections, screen: self) 85 | @main_section.render 86 | else 87 | all_sections.each do |section| 88 | section.render 89 | end 90 | end 91 | end 92 | 93 | module ClassMethods 94 | def sections_wrapper 95 | @sections_wrapper 96 | end 97 | 98 | def set_sections_wrapper(value) 99 | @sections_wrapper = value 100 | end 101 | 102 | def section_options 103 | _section_options || {} 104 | end 105 | 106 | def section(name, options = {}) 107 | self._section_options ||= {} 108 | self._section_options[name.to_sym] = options 109 | 110 | define_method name do 111 | @sections[name] 112 | end 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /motion-prime/screens/controllers/navigation_controller.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class NavigationController < UINavigationController 3 | # Return the main controller. 4 | def main_controller 5 | self 6 | end 7 | # Return content controller (without sidebar) 8 | def content_controller 9 | self 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /motion-prime/screens/controllers/tab_bar_controller.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class TabBarController < UITabBarController 3 | def self.new(screens, global_options = {}) 4 | controller = alloc.init 5 | 6 | view_controllers = [] 7 | 8 | screens.each_with_index do |options, index| 9 | if options.is_a?(Hash) 10 | screen = init_screen_with_options(global_options.deep_merge(options), tag: index) 11 | else 12 | screen = options 13 | screen.tabBarItem.tag = index 14 | end 15 | 16 | screen.send(:on_screen_load) if screen.respond_to?(:on_screen_load) 17 | screen.wrap_in_navigation if screen.respond_to?(:wrap_in_navigation) 18 | screen.tab_bar = controller.weak_ref if screen.respond_to?(:tab_bar=) 19 | view_controllers << screen.main_controller.strong_ref 20 | end 21 | 22 | controller.viewControllers = view_controllers 23 | controller 24 | end 25 | 26 | def open_tab(tab) 27 | controller = viewControllers[tab] 28 | if controller 29 | self.selectedViewController = controller 30 | end 31 | controller 32 | end 33 | 34 | def dealloc 35 | Prime.logger.dealloc_message :tab_bar, self 36 | clear_instance_variables 37 | super 38 | end 39 | 40 | protected 41 | def self.init_screen_with_options(options, tag: tag) 42 | screen, title = options.delete(:screen), options.delete(:title) 43 | screen = Screen.create_with_options(screen, true, options).try(:weak_ref) 44 | title ||= screen.title 45 | image = extract_image_from_options(options, with_key: :image) 46 | screen.tabBarItem = UITabBarItem.alloc.initWithTitle title, image: image, tag: tag 47 | 48 | selected_image = extract_image_from_options(options, with_key: :selected_image) 49 | screen.tabBarItem.setSelectedImage(selected_image) if selected_image 50 | screen 51 | end 52 | 53 | def self.extract_image_from_options(options, with_key: key) 54 | image = options.delete(key) 55 | return unless image 56 | ui_image = image.uiimage 57 | if ui_image && options[:translucent] === false 58 | ui_image = ui_image.imageWithRenderingMode UIImageRenderingModeAlwaysOriginal 59 | end 60 | ui_image 61 | end 62 | end 63 | end -------------------------------------------------------------------------------- /motion-prime/screens/extensions/_indicators_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module ScreenIndicatorsMixin 3 | def show_activity_indicator(render_target = nil, options = {}) 4 | render_target ||= view 5 | @activity_indicator_view ||= {} 6 | indicator = @activity_indicator_view[render_target.object_id] ||= begin 7 | indicator = UIActivityIndicatorView.gray 8 | render_target.addSubview(indicator) 9 | indicator 10 | end 11 | 12 | center = options[:center] || {} 13 | indicator.center = CGPointMake(center.fetch(:x, render_target.center.x), center.fetch(:y, render_target.center.y)) 14 | indicator.startAnimating 15 | end 16 | 17 | def hide_activity_indicator(render_target = nil) 18 | @activity_indicator_view ||= {} 19 | render_target ||= view 20 | @activity_indicator_view[render_target.object_id].try(:stopAnimating) 21 | end 22 | 23 | def show_progress_indicator(text = nil, options = {}) 24 | @_showing_indicator = true 25 | options[:styles] ||= [] 26 | options[:styles] << :base_progress_indicator 27 | options[:styles] << :"#{self.class_name_without_kvo.underscore.gsub('_screen', '')}_indicator" 28 | options[:details_label_text] = text 29 | 30 | if @progress_indicator_view.nil? 31 | options[:add_to_view] ||= self.view 32 | @progress_indicator_view = self.progress_hud(options).view 33 | else 34 | self.update_options_for(@progress_indicator_view, options.except(:add_to_view)) 35 | @progress_indicator_view.show options.has_key?(:animated) ? options[:animatetd] : true 36 | end 37 | end 38 | 39 | def hide_progress_indicator(animated = true) 40 | @progress_indicator_view.try(:hide, animated) 41 | @_showing_indicator = false 42 | end 43 | 44 | def show_notice(message, time = 1.0, type = :notice) 45 | hud_type = case type.to_s 46 | when 'alert' then MBAlertViewHUDTypeExclamationMark 47 | else MBAlertViewHUDTypeCheckmark 48 | end 49 | 50 | unless time === false 51 | MBHUDView.hudWithBody(message, type: hud_type, hidesAfter: time, show: true) 52 | end 53 | end 54 | 55 | def show_spinner(message = nil) 56 | if message.present? 57 | spinner_message_element.set_text(message) 58 | spinner_message_element.show 59 | end 60 | spinner_element.show 61 | spinner_element.view.init_animation 62 | end 63 | 64 | def hide_spinner 65 | spinner_element.hide 66 | spinner_message_element.hide 67 | end 68 | 69 | private 70 | 71 | def spinner_element 72 | @_spinner_element ||= self.spinner({ 73 | styles: base_styles_for('spinner'), 74 | hidden: true}) 75 | end 76 | 77 | def spinner_message_element 78 | @_spinner_message_element ||= self.label({ 79 | styles: base_styles_for('spinner_message'), 80 | text: '', 81 | hidden: true}) 82 | end 83 | 84 | def base_styles_for(name) 85 | ([:base] + default_styles).map { |base| :"#{base}_#{name}" } 86 | end 87 | end 88 | end -------------------------------------------------------------------------------- /motion-prime/screens/screen.rb: -------------------------------------------------------------------------------- 1 | motion_require '../support/mp_view_controller' 2 | motion_require '../views/layout' 3 | motion_require '../screens/_base_mixin' 4 | motion_require './extensions/_indicators_mixin' 5 | motion_require './extensions/_navigation_bar_mixin' 6 | motion_require '../helpers/has_authorization' 7 | motion_require '../helpers/has_search_bar' 8 | module MotionPrime 9 | class Screen < MPViewController 10 | include Layout 11 | include ScreenBaseMixin 12 | 13 | # extensions 14 | include ScreenIndicatorsMixin 15 | include ScreenNavigationBarMixin 16 | 17 | # helpers 18 | include HasAuthorization 19 | include HasSearchBar 20 | 21 | extend HasClassFactory 22 | 23 | define_callbacks :render 24 | 25 | after_render :add_sections 26 | 27 | def render 28 | end 29 | 30 | def default_styles 31 | [:base_screen, self.class_name_without_kvo.underscore.to_sym] 32 | end 33 | 34 | def will_appear 35 | @visible = true 36 | @on_appear_happened ||= {} 37 | unless @on_appear_happened[view.object_id] 38 | set_options_for view, styles: default_styles do 39 | run_callbacks :render do 40 | send((action).to_sym) 41 | end 42 | end 43 | end 44 | @on_appear_happened[view.object_id] = true 45 | end 46 | 47 | def will_disappear 48 | @visible = false 49 | end 50 | 51 | def dealloc 52 | Prime.logger.dealloc_message :screen, self 53 | # FIXME: calling instance_eval in title method (_base_screen_mixin) instance variables need to be cleared manually 54 | clear_instance_variables(except: [:_search_bar]) 55 | super 56 | end 57 | 58 | def strong_references 59 | [self.main_controller] 60 | end 61 | 62 | def visible? 63 | !!@visible 64 | end 65 | end 66 | end -------------------------------------------------------------------------------- /motion-prime/sections/_async_form_mixin.rb: -------------------------------------------------------------------------------- 1 | module Prime 2 | module AsyncFormMixin 3 | def reload_collection_data 4 | # FIXME: duplicated cells (see cached_cell error) 5 | return super unless async_data? 6 | sections = NSMutableIndexSet.new 7 | number_of_groups.times do |section_id| 8 | sections.addIndex(section_id) 9 | end 10 | collection_view.reloadSections sections, withRowAnimation: UITableViewRowAnimationFade 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /motion-prime/sections/_cell_section_mixin.rb: -------------------------------------------------------------------------------- 1 | # This Mixin will be included only to sections, which added as cell to collection section. 2 | module MotionPrime 3 | module CellSectionMixin 4 | extend ::MotionSupport::Concern 5 | 6 | # include SectionWithContainerMixin # already included in draw_section_mixin 7 | 8 | attr_writer :collection_section 9 | attr_reader :pending_display 10 | 11 | included do 12 | class_attribute :custom_cell_section_name 13 | container_element type: :table_view_cell 14 | end 15 | 16 | def collection_section 17 | @collection_section ||= options[:collection_section].try(:weak_ref) 18 | end 19 | 20 | def table 21 | Prime.logger.info "Section#table is deprecated: #{caller[0]}" 22 | collection_section 23 | end 24 | 25 | def section_styles 26 | @section_styles ||= collection_section.try(:cell_section_styles, self) || {} 27 | end 28 | 29 | def cell_type 30 | @cell_type ||= begin 31 | self.is_a?(BaseFieldSection) ? :field : :cell 32 | end 33 | end 34 | 35 | def cell_section_name 36 | self.class.custom_cell_section_name || begin 37 | return name unless collection_section 38 | table_name = collection_section.name.gsub('_table', '') 39 | name.gsub("#{table_name}_", '') 40 | end 41 | end 42 | 43 | def container_bounds 44 | @container_bounds ||= CGRectMake(0, 0, collection_section.collection_view.bounds.size.width, container_height) 45 | end 46 | 47 | # should do nothing, because collection section will care about it. 48 | def render_container(options = {}, &block) 49 | block.call 50 | end 51 | 52 | def init_container_element(options = {}) 53 | options[:styles] ||= [] 54 | options[:styles] = [:"#{collection_section.name}_first_cell"] if collection_section.data.first == self 55 | options[:styles] = [:"#{collection_section.name}_last_cell"] if collection_section.data.last == self 56 | super(options) 57 | end 58 | 59 | def pending_display! 60 | @pending_display = true 61 | display unless collection_section.decelerating 62 | end 63 | 64 | def display 65 | @pending_display = false 66 | container_view.try(:setNeedsDisplay) 67 | end 68 | 69 | def cell 70 | container_view || begin 71 | first_element = elements.values.first 72 | return unless first_element.is_a?(BaseElement) && first_element.view 73 | first_element.view.superview.superview 74 | end 75 | end 76 | 77 | def dealloc 78 | # TODO: remove this when solve this problem: dealloc TableCells on TableView.reloadData (in case when reuseIdentifier has been used) 79 | container_view.section = nil if container_view.respond_to?(:setSection) 80 | super 81 | end 82 | 83 | module ClassMethods 84 | def set_cell_section_name(value) 85 | self.custom_cell_section_name = value 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /motion-prime/sections/_delegate_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module DelegateMixin 3 | def delegated_by(view) 4 | @delegated_views ||= [] 5 | @delegated_views << view 6 | end 7 | 8 | def clear_delegated 9 | Array.wrap(@delegated_views).each { |view| view.try(:setDelegate, nil) } 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /motion-prime/sections/_section_with_container_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module SectionWithContainerMixin 3 | extend ::MotionSupport::Concern 4 | 5 | included do 6 | class_attribute :container_element_options 7 | end 8 | 9 | def container_view 10 | container_element.try(:view) 11 | end 12 | 13 | def init_container_element(options = {}) 14 | if @_creating_container 15 | sleep 0.01 16 | return @container_element ? @container_element : init_container_element(options) 17 | end 18 | @_creating_container = true 19 | @container_element ||= begin 20 | options.merge!({ 21 | screen: screen, 22 | section: self.weak_ref, 23 | has_drawn_content: true 24 | }) 25 | container_element_options = self.class.container_element_options.clone 26 | options = (container_element_options || {}).deep_merge(options) 27 | type = options.delete(:type) 28 | MotionPrime::BaseElement.factory(type, options) 29 | end 30 | @_creating_container = false 31 | @container_element 32 | end 33 | 34 | def load_container_with_elements(options = {}) 35 | init_container_element(options[:container] || {}) 36 | # FIXME: does not work for grid sections 37 | @container_element.preload_options 38 | compute_element_options(options[:elements] || {}) 39 | 40 | if respond_to?(:prerender_elements_for_state) && prerender_enabled? 41 | prerender_elements_for_state(:normal) 42 | end 43 | end 44 | 45 | private 46 | def compute_element_options(options = {}) 47 | self.elements.values.each do |element| 48 | element.preload_options 49 | end 50 | end 51 | 52 | 53 | module ClassMethods 54 | def container_element(options) 55 | self.container_element_options = options 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /motion-prime/sections/collection/collection_delegate.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class CollectionDelegate 3 | include DelegateMixin 4 | attr_accessor :collection_section 5 | 6 | def initialize(options) 7 | self.collection_section = options[:section].try(:weak_ref) 8 | @_section_info = collection_section.to_s 9 | @section_instance = collection_section.to_s 10 | end 11 | 12 | def dealloc 13 | Prime.logger.dealloc_message :collection_delegate, @_section_info 14 | super 15 | end 16 | 17 | def numberOfSectionsInCollectionView(table) 18 | collection_section.number_of_groups 19 | end 20 | 21 | def collectionView(table, cellForItemAtIndexPath: index) 22 | cur_call_time = Time.now.to_f 23 | cur_call_offset = table.contentOffset.y 24 | if @prev_call_time 25 | time_delta = cur_call_time - @prev_call_time 26 | offset_delta = cur_call_offset - @prev_call_offset 27 | @deceleration_speed = offset_delta/time_delta 28 | end 29 | @prev_call_time = cur_call_time 30 | @prev_call_offset = cur_call_offset 31 | 32 | collection_section.cell_for_index(index) 33 | end 34 | 35 | def collectionView(table, numberOfItemsInSection: group) 36 | collection_section.number_of_cells_in_group(group) 37 | end 38 | 39 | def collectionView(table, heightForItemAtIndexPath: index) 40 | collection_section.height_for_index(index) 41 | end 42 | 43 | def collectionView(table, didSelectItemAtIndexPath:index) 44 | collection_section.on_click(index) 45 | end 46 | 47 | def scrollViewDidScroll(scroll) 48 | collection_section.scroll_view_did_scroll(scroll) 49 | collection_section.update_pull_to_refresh_after_scroll(scroll) 50 | end 51 | 52 | def scrollViewDidEndScrollingAnimation(scroll) 53 | collection_section.scroll_view_did_end_scrolling_animation(scroll) 54 | end 55 | 56 | def scrollViewWillBeginDragging(scroll) 57 | collection_section.scroll_view_will_begin_dragging(scroll) 58 | end 59 | 60 | def scrollViewWillBeginDecelerating(scroll) 61 | collection_section.scroll_view_will_begin_decelerating(scroll) 62 | end 63 | 64 | def scrollViewDidEndDecelerating(scroll) 65 | collection_section.scroll_view_did_end_decelerating(scroll) 66 | end 67 | 68 | def scrollViewDidEndDragging(scroll, willDecelerate: will_decelerate) 69 | collection_section.scroll_view_did_end_dragging(scroll, willDecelerate: will_decelerate) 70 | end 71 | end 72 | end -------------------------------------------------------------------------------- /motion-prime/sections/form/date_field_section.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class DateFieldSection < BaseFieldSection 3 | container height: 190 4 | element :label, type: :label do 5 | default_label_options 6 | end 7 | element :input, type: :date_picker do 8 | options[:input] || {} 9 | end 10 | 11 | after_element_render :input, :bind_input 12 | 13 | def bind_input 14 | picker = view(:input) 15 | picker.setDelegate form 16 | unless picker.date 17 | picker.setDate NSDate.date, animated: true 18 | end 19 | picker.on :change do 20 | form.send(options[:action]) if options[:action] 21 | end 22 | end 23 | 24 | def value 25 | view(:input).date 26 | end 27 | 28 | def input? 29 | true 30 | end 31 | 32 | def dealloc 33 | picker = view(:input) 34 | picker.setDelegate nil 35 | super 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /motion-prime/sections/form/form_delegate.rb: -------------------------------------------------------------------------------- 1 | motion_require '../table/table_delegate' 2 | module MotionPrime 3 | class FormDelegate < TableDelegate 4 | def textField(text_field, shouldChangeCharactersInRange:range, replacementString:string) 5 | limit = (table_section.class.text_field_limits || {}).find do |field_name, limit| 6 | table_section.view("#{field_name}:input") == text_field 7 | end.try(:last) 8 | return true unless limit 9 | table_section.allow_string_replacement?(text_field, limit, range, string) 10 | end 11 | 12 | def textView(text_view, shouldChangeTextInRange:range, replacementText:string) 13 | textField(text_view, shouldChangeCharactersInRange:range, replacementString:string) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /motion-prime/sections/form/form_header_section.rb: -------------------------------------------------------------------------------- 1 | motion_require '../header' 2 | module MotionPrime 3 | class FormHeaderSection < HeaderSection 4 | DEFAULT_HEADER_HEIGHT = 20 5 | 6 | element :title, text: proc { @options[:title] } 7 | element :hint, text: proc { @options[:hint] } 8 | 9 | def render_element?(name) 10 | @options[name].present? 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /motion-prime/sections/form/password_field_section.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class PasswordFieldSection < BaseFieldSection 3 | element :label, type: :label do 4 | default_label_options 5 | end 6 | element :input, type: :text_field, delegate: proc { collection_delegate } do 7 | {secure_text_entry: true}.merge(options[:input] || {}) 8 | end 9 | element :error_message, type: :error_message, text: proc { |field| field.all_errors.join("\n") if field.observing_errors? } 10 | after_element_render :input, :bind_text_input 11 | 12 | def value 13 | view(:input).text 14 | end 15 | 16 | def input? 17 | true 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /motion-prime/sections/form/select_field_section.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class SelectFieldSection < BaseFieldSection 3 | element :label, type: :label do 4 | default_label_options 5 | end 6 | element :button, type: :button do 7 | options[:button] || {} 8 | end 9 | element :arrow, type: :image do 10 | options[:arrow] || {} 11 | end 12 | element :error_message, type: :error_message, text: proc { |field| field.observing_errors? and field.all_errors.join("\n") } 13 | 14 | after_element_render :button, :bind_select_button 15 | 16 | def bind_select_button 17 | view(:button).on :touch_down do 18 | form.send(options[:action]) if options[:action] 19 | end 20 | end 21 | 22 | def value 23 | view(:button).title 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /motion-prime/sections/form/static_field_section.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class StaticFieldSection < Section 3 | include CellSectionMixin 4 | 5 | def form 6 | collection_section 7 | end 8 | 9 | def clear_observers; end 10 | 11 | protected 12 | def elements_eval_object 13 | form 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /motion-prime/sections/form/string_field_section.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class StringFieldSection < BaseFieldSection 3 | element :label, type: :label do 4 | default_label_options 5 | end 6 | 7 | element :input, type: :text_field, delegate: proc { collection_delegate } do 8 | options[:input] || {} 9 | end 10 | 11 | element :error_message, type: :error_message, text: proc { |field| field.all_errors.join("\n") if field.observing_errors? } 12 | after_element_render :input, :bind_text_input 13 | 14 | def value 15 | view(:input).text 16 | end 17 | 18 | def input? 19 | true 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /motion-prime/sections/form/submit_field_section.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class SubmitFieldSection < BaseFieldSection 3 | element :button, type: :button do 4 | {title: options[:name].to_s.titleize}.merge(options[:button] || {}) 5 | end 6 | element :error_message, type: :error_message, text: proc { |field| field.all_errors.join("\n") if field.observing_errors? } 7 | 8 | after_element_render :button, :bind_button 9 | 10 | def bind_button 11 | view(:button).on :touch do 12 | form.send(options[:action]) if options[:action] 13 | end 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /motion-prime/sections/form/switch_field_section.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class SwitchFieldSection < BaseFieldSection 3 | element :label, type: :label do 4 | default_label_options 5 | end 6 | element :input, type: :switch do 7 | options[:input] || {} 8 | end 9 | element :hint, type: :label do 10 | options[:hint] || {} 11 | end 12 | 13 | def value 14 | view(:input).isOn 15 | end 16 | 17 | def input? 18 | true 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /motion-prime/sections/form/text_field_section.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class TextFieldSection < BaseFieldSection 3 | element :label, type: :label do 4 | default_label_options 5 | end 6 | element :input, type: :text_view, delegate: proc { collection_delegate } do 7 | {editable: true}.merge(options[:input] || {}) 8 | end 9 | 10 | element :error_message, type: :error_message, text: proc { |field| field.observing_errors? and field.all_errors.join("\n") } 11 | after_element_render :input, :bind_text_input 12 | 13 | def value 14 | view(:input).text 15 | end 16 | 17 | def input? 18 | true 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /motion-prime/sections/grid.rb: -------------------------------------------------------------------------------- 1 | motion_require './collection/collection_delegate' 2 | 3 | module MotionPrime 4 | class GridSection < AbstractCollectionSection 5 | DEFAULT_GRID_SIZE = 3 6 | 7 | class_attribute :grid_size_value 8 | 9 | before_render :render_collection 10 | 11 | # Get index path for cell section 12 | # 13 | # @param section [Prime::Section] cell section. 14 | # @return index [NSIndexPath] index of cell section. 15 | def index_for_cell_section(section) 16 | return unless item = @data.try(:index, section) 17 | group = item/grid_size 18 | row = cell_sections_for_group(group).index(section) 19 | NSIndexPath.indexPathForRow(row, inSection: group) 20 | end 21 | 22 | def collection_styles_base 23 | :base_collection 24 | end 25 | 26 | def collection_delegate 27 | @collection_delegate ||= CollectionDelegate.new(section: self) 28 | end 29 | 30 | def grid_element_options 31 | collection_element_options.merge({ 32 | grid_size: grid_size 33 | }) 34 | end 35 | 36 | def render_collection 37 | self.collection_element = screen.collection_view(grid_element_options) 38 | end 39 | 40 | def render_cell(index) 41 | collection_view.registerClass(MPCollectionCellWithSection, forCellWithReuseIdentifier: cell_name(index)) 42 | view = collection_view.dequeueReusableCellWithReuseIdentifier(cell_name(index), forIndexPath: index) 43 | 44 | section = cell_section_by_index(index) 45 | element = section.container_element || section.init_container_element(container_element_options_for(index)) 46 | unless view.section 47 | element.view = view 48 | screen.set_options_for view, element.computed_options.except(:parent_view) do 49 | section.render 50 | end 51 | 52 | on_cell_render(view, index) 53 | end 54 | view 55 | end 56 | 57 | def cell_sections_for_group(section) 58 | data[section*grid_size, grid_size] 59 | end 60 | 61 | # Table View Delegate 62 | # --------------------- 63 | 64 | def grid_size 65 | self.class.grid_size || DEFAULT_GRID_SIZE 66 | end 67 | 68 | def number_of_cells_in_group(group) 69 | cell_sections_for_group(group).count.to_i 70 | end 71 | 72 | def number_of_groups 73 | (data.count.to_f / grid_size).ceil 74 | end 75 | 76 | private 77 | def container_element_options_for(index) 78 | super.merge({ 79 | type: :collection_view_cell, 80 | view_class: 'MPCollectionCellWithSection' 81 | }) 82 | end 83 | 84 | def self.grid_size(value = nil) 85 | if value 86 | self.grid_size_value = value 87 | else 88 | self.grid_size_value 89 | end 90 | end 91 | end 92 | end -------------------------------------------------------------------------------- /motion-prime/sections/header.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class HeaderSection < Section 3 | include Prime::CellSectionMixin 4 | 5 | before_initialize :prepare_header_options 6 | 7 | def prepare_header_options 8 | @cell_type = :header 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /motion-prime/sections/page_view.rb: -------------------------------------------------------------------------------- 1 | motion_require './page_view/page_view_delegate' 2 | 3 | module MotionPrime 4 | class PageViewSection < AbstractCollectionSection 5 | attr_accessor :page_controller 6 | before_render :render_collection 7 | after_render :set_first_page 8 | 9 | def collection_styles_base 10 | :base_page_view 11 | end 12 | 13 | def collection_delegate 14 | @collection_delegate ||= PageViewDelegate.new(section: self) 15 | end 16 | 17 | def page_element_options 18 | collection_element_options 19 | end 20 | 21 | def render_collection 22 | self.collection_element = screen.page_view_controller(page_element_options) 23 | end 24 | 25 | def set_first_page 26 | set_page(0) 27 | end 28 | 29 | def set_page(index, animated = false, &block) 30 | page = page_for_index(index) 31 | set_view_controllers([page], animated, &block) 32 | end 33 | 34 | def reload_collection_data 35 | set_view_controllers(page_controller.viewControllers, false) 36 | end 37 | 38 | def set_view_controllers(controllers, animated = false, &completion) 39 | completion ||= proc{|a|} 40 | index = index_for_page(controllers.last) 41 | current_index = index_for_page(page_controller.viewControllers.last).to_i 42 | direction = current_index <= index ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse 43 | page_controller.setViewControllers(controllers, direction: direction, animated: animated, completion: completion) 44 | page_did_set(index) 45 | end 46 | 47 | def page_did_set(index); end 48 | def page_will_set(index); end 49 | 50 | def add_pages(sections, follow = false) 51 | @data += Array.wrap(sections) 52 | if follow 53 | page_index = data.count - 1 54 | set_page(page_index, true) do |finished| 55 | BW::Reactor.schedule_on_main { set_page(page_index, false) } 56 | end 57 | else 58 | reload_collection_data 59 | end 60 | end 61 | 62 | def current_page_id 63 | index_for_page(page_controller.viewControllers.last) 64 | end 65 | 66 | # Delegate 67 | def page_for_index(index) 68 | return nil if !index || data.length == 0 || index < 0 || index >= data.size 69 | @view_controllers.try(:[], index) || prepare_cell_section(data[index], index) 70 | end 71 | 72 | def index_for_page(view_controller) 73 | Array.wrap(@view_controllers).index(view_controller) 74 | end 75 | 76 | private 77 | def prepare_collection_cell_sections(sections) 78 | Array.wrap(sections.flatten).each_with_index do |section, index| 79 | prepare_cell_section(section, index) 80 | end 81 | end 82 | 83 | def prepare_cell_section(section, index) 84 | @view_controllers ||= [] 85 | controller = MotionPrime::Screen.new 86 | controller.parent_screen = self.screen 87 | section.screen = controller.weak_ref 88 | controller.set_section :main, instance: section 89 | @view_controllers[index] = controller 90 | end 91 | end 92 | end -------------------------------------------------------------------------------- /motion-prime/sections/page_view/page_view_delegate.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class PageViewDelegate 3 | include DelegateMixin 4 | attr_accessor :collection_section 5 | 6 | def initialize(options) 7 | self.collection_section = options[:section].try(:weak_ref) 8 | @_section_info = collection_section.to_s 9 | @section_instance = collection_section.to_s 10 | end 11 | 12 | def dealloc 13 | Prime.logger.dealloc_message :collection_delegate, @_section_info 14 | super 15 | end 16 | 17 | def viewControllerAtIndex(index, storyboard:storyboard) 18 | collection_section.page_for_index(index) 19 | end 20 | 21 | def pageViewController(pvc, viewControllerBeforeViewController:vc) 22 | index = collection_section.index_for_page(vc) 23 | collection_section.page_for_index(index - 1) 24 | end 25 | 26 | def pageViewController(pvc, viewControllerAfterViewController:vc) 27 | index = collection_section.index_for_page(vc) 28 | collection_section.page_for_index(index + 1) 29 | end 30 | 31 | def presentationCountForPageViewController(controller) 32 | collection_section.data.size 33 | end 34 | 35 | def pageViewController(pvc, spineLocationForInterfaceOrientation:orientation) 36 | page_view_controller = collection_section.page_controller 37 | 38 | current = page_view_controller.viewControllers[0] 39 | is_portrait = UIDevice.currentDevice.orientation == UIDeviceOrientationLandscapeLeft || 40 | UIDevice.currentDevice.orientation == UIDeviceOrientationPortraitUpsideDown || 41 | UIDevice.currentDevice.orientation == UIDeviceOrientationUnknown 42 | if is_portrait 43 | collection_section.reload_collection_data 44 | page_view_controller.doubleSided = false 45 | return UIPageViewControllerSpineLocationMin 46 | else 47 | index = collection_section.index_for_page(current) 48 | if (index==0 || index%2==0) 49 | next_vc = pageViewController(page_view_controller, viewControllerAfterViewController: current) 50 | viewControllers = [current, next_vc] 51 | else 52 | prev_vc = pageViewController(page_view_controller, viewControllerBeforeViewController: current) 53 | viewControllers = [prev_vc, current] 54 | end 55 | collection_section.set_view_controllers(viewControllers, true) 56 | page_view_controller.doubleSided = true 57 | return UIPageViewControllerSpineLocationMid 58 | end 59 | end 60 | 61 | def pageViewController(pvc, didFinishAnimating: finished, previousViewControllers: previous_view_controllers, transitionCompleted: completed) 62 | if completed 63 | index = collection_section.index_for_page(collection_section.page_controller.viewControllers.last) 64 | collection_section.page_did_set(index) 65 | end 66 | end 67 | 68 | def pageViewController(pvc, willTransitionToViewControllers: pending_view_controllers) 69 | index = collection_section.index_for_page(pending_view_controllers.last) 70 | collection_section.page_will_set(index) 71 | end 72 | end 73 | end -------------------------------------------------------------------------------- /motion-prime/sections/tabbed.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class TabbedSection < Section 3 | # MotionPrime::TabbedSection is base class for building tabbed views. 4 | 5 | # == Basic Sample 6 | # class MySection < MotionPrime::TabbedSection 7 | # tab :info, default: true, page_section: :info_tab 8 | # tab :map, page_section: :map_tab 9 | # # page_section options will be converted to section class and added to section. 10 | # # e.g. in this sample: InfoTabSection.new(screen: screen, model: model).render 11 | # end 12 | # 13 | include MotionPrime::HasNormalizer 14 | 15 | class_attribute :tabs_options, :tabs_default, :tabs_indexes 16 | attr_accessor :tab_pages 17 | 18 | element :control, type: :segmented_control, 19 | styles: [:base_segmented_control], items: proc { tab_control_items } 20 | 21 | before_render :render_tab_pages 22 | after_render :render_tab_controls 23 | 24 | def tab_options 25 | @tab_options ||= normalize_options(self.class.tabs_options.clone) 26 | end 27 | 28 | def tab_control_items 29 | @tab_control_items ||= tab_options.values.map{ |o| o[:name] } 30 | end 31 | 32 | def tab_default 33 | @tab_default ||= self.class.tabs_default || 0 34 | end 35 | 36 | # Make tab button disabled by index 37 | # @param Fixnum index 38 | def disable_at(index) 39 | toggle_at(index, false) 40 | end 41 | 42 | # Make tab button enabled by index 43 | # @param Fixnum index 44 | def enable_at(index) 45 | toggle_at(index, true) 46 | end 47 | 48 | # Toggle tab button activity by index 49 | # @param [Fixnum, String] index or tab id 50 | # @param Boolean value 51 | def toggle_at(index, value) 52 | if index.is_a?(Symbol) 53 | index = self.class.tabs_indexes[index] 54 | end 55 | view(:control).setEnabled value, forSegmentAtIndex: index 56 | end 57 | 58 | # on click to segment tab 59 | # @param UISegemtedControl control 60 | def on_click(*control) 61 | show_at_index(control.selectedSegment) 62 | end 63 | 64 | def show_at_index(index) 65 | @tab_pages.each_with_index do |page, i| 66 | page.hide if index != i 67 | end 68 | view(:control).setSelectedSegmentIndex index 69 | @tab_pages[index].show 70 | end 71 | 72 | def show_by_key(key) 73 | id = self.class.tabs_indexes[key] 74 | show_at_index(id) 75 | end 76 | 77 | def tab_page(key) 78 | id = self.class.tabs_indexes[key] 79 | tab_pages[id] 80 | end 81 | 82 | def set_title(key, title) 83 | id = self.class.tabs_indexes[key] 84 | view(:control).setTitle(title, forSegmentAtIndex: id) 85 | end 86 | 87 | class << self 88 | def inherited(subclass) 89 | super 90 | subclass.tabs_options = self.tabs_options.try(:clone) 91 | subclass.tabs_default = self.tabs_default.try(:clone) 92 | subclass.tabs_indexes = self.tabs_indexes.try(:clone) 93 | end 94 | 95 | def tab(id, options = {}) 96 | options[:name] ||= id.to_s.titleize 97 | options[:id] = id 98 | 99 | self.tabs_indexes ||= {} 100 | self.tabs_indexes[id] = tabs_indexes.length 101 | self.tabs_default = tabs_indexes.length - 1 if options[:default] 102 | 103 | self.tabs_options ||= {} 104 | self.tabs_options[id] = options 105 | end 106 | end 107 | 108 | private 109 | def render_tab_pages 110 | self.tab_pages = [] 111 | index = 0 112 | tab_options.each do |key, options| 113 | page_class = class_factory("/#{options[:page_section]}_section") 114 | page = page_class.new(screen: screen, model: model) 115 | page.render 116 | page.hide if index != tab_default 117 | self.tab_pages << page 118 | index += 1 119 | end 120 | end 121 | 122 | def render_tab_controls 123 | control = element(:control).view 124 | control.addTarget( 125 | self, action: :on_click, forControlEvents: UIControlEventValueChanged 126 | ) 127 | control.setSelectedSegmentIndex(tab_default) 128 | end 129 | end 130 | end -------------------------------------------------------------------------------- /motion-prime/sections/table/refresh_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module TableSectionRefreshMixin 3 | def add_pull_to_refresh(options = {}, &block) 4 | screen.automaticallyAdjustsScrollViewInsets = false 5 | 6 | collection_view.addPullToRefreshWithActionHandler(block) # block must be a variable 7 | refresh_view = collection_view.pullToRefreshView 8 | 9 | options[:styles] ||= [] 10 | options[:styles] += [:base_pull_to_refresh] 11 | # pass yOrigin to override view top 12 | base_options = { 13 | alpha: 0, 14 | custom_offset_threshold: - collection_view.contentInset.top - refresh_view.size.height, 15 | original_top_inset: collection_view.contentInset.top 16 | } 17 | screen.set_options_for refresh_view, base_options.deep_merge(options) 18 | end 19 | 20 | def finish_pull_to_refresh 21 | reload_data 22 | collection_view.pullToRefreshView.stopAnimating 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /motion-prime/sections/table/table_delegate.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class TableDelegate 3 | include DelegateMixin 4 | attr_accessor :table_section 5 | 6 | def initialize(options) 7 | self.table_section = options[:section].try(:weak_ref) 8 | @_section_info = table_section.to_s 9 | @section_instance = table_section.to_s 10 | end 11 | 12 | def dealloc 13 | Prime.logger.dealloc_message :collection_delegate, @_section_info 14 | super 15 | end 16 | 17 | def init_pull_to_refresh 18 | return unless block = table_section.class.pull_to_refresh_block 19 | table_section.add_pull_to_refresh(table_section.class.pull_to_refresh_options || {}) do 20 | table_section.instance_eval(&block) 21 | end 22 | end 23 | 24 | def numberOfSectionsInTableView(table) 25 | table_section.number_of_groups 26 | end 27 | 28 | def tableView(table, cellForRowAtIndexPath: index) 29 | cur_call_time = Time.now.to_f 30 | cur_call_offset = table.contentOffset.y 31 | if @prev_call_time 32 | time_delta = cur_call_time - @prev_call_time 33 | offset_delta = cur_call_offset - @prev_call_offset 34 | @deceleration_speed = offset_delta/time_delta 35 | end 36 | @prev_call_time = cur_call_time 37 | @prev_call_offset = cur_call_offset 38 | 39 | table_section.cell_for_index(index) 40 | end 41 | 42 | def tableView(table, numberOfRowsInSection: group) 43 | table_section.cell_sections_for_group(group).try(:count).to_i 44 | end 45 | 46 | def tableView(table, heightForRowAtIndexPath: index) 47 | table_section.height_for_index(index) 48 | end 49 | 50 | def tableView(table, didSelectRowAtIndexPath:index) 51 | table_section.on_click(index) 52 | end 53 | 54 | def tableView(table, viewForHeaderInSection: group) 55 | table_section.header_cell_in_group(group) 56 | end 57 | 58 | def tableView(table, heightForHeaderInSection: group) 59 | table_section.height_for_header_in_group(group) 60 | end 61 | 62 | def scrollViewDidScroll(scroll) 63 | table_section.scroll_view_did_scroll(scroll) 64 | table_section.update_pull_to_refresh_after_scroll(scroll) 65 | end 66 | 67 | def scrollViewDidEndScrollingAnimation(scroll) 68 | table_section.scroll_view_did_end_scrolling_animation(scroll) 69 | end 70 | 71 | def scrollViewWillBeginDragging(scroll) 72 | table_section.scroll_view_will_begin_dragging(scroll) 73 | end 74 | 75 | def scrollViewWillBeginDecelerating(scroll) 76 | table_section.scroll_view_will_begin_decelerating(scroll) 77 | end 78 | 79 | def scrollViewDidEndDecelerating(scroll) 80 | table_section.scroll_view_did_end_decelerating(scroll) 81 | end 82 | 83 | def scrollViewDidEndDragging(scroll, willDecelerate: will_decelerate) 84 | table_section.scroll_view_did_end_dragging(scroll, willDecelerate: will_decelerate) 85 | end 86 | 87 | def textFieldShouldReturn(text_field) 88 | table_section.on_input_return(text_field) 89 | end 90 | def textFieldShouldBeginEditing(text_field) 91 | text_field.respond_to?(:readonly) ? !text_field.readonly : true 92 | end 93 | def textFieldDidBeginEditing(text_field) 94 | table_section.on_input_edit_begin(text_field) 95 | end 96 | def textFieldDidEndEditing(text_field) 97 | table_section.on_input_edit_end(text_field) 98 | end 99 | def textViewDidBeginEditing(text_view) 100 | table_section.on_input_edit_begin(text_view) 101 | end 102 | def textViewDidEndEditing(text_view) 103 | table_section.on_input_edit_end(text_view) 104 | end 105 | def textViewDidChange(text_view) 106 | unless IS_OS_8_OR_HIGHER 107 | # bug in iOS 7 - cursor is out of textView bounds 108 | line = text_view.caretRectForPosition(text_view.selectedTextRange.start) 109 | overflow = line.origin.y + line.size.height - 110 | (text_view.contentOffset.y + text_view.bounds.size.height - text_view.contentInset.bottom - text_view.contentInset.top) 111 | if overflow > 0 112 | offset = text_view.contentOffset 113 | offset.y += overflow + text_view.textContainerInset.bottom 114 | UIView.animate(duration: 0.2) do 115 | text_view.setContentOffset(offset) 116 | end 117 | end 118 | end 119 | table_section.on_input_did_change(text_view) 120 | end 121 | end 122 | end -------------------------------------------------------------------------------- /motion-prime/services/base_computed_options.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class BaseComputedOptions < ::Hash 3 | def initialize 4 | super 5 | @_keys = {} 6 | @_cached_values = {} 7 | end 8 | 9 | def slice(*keys) 10 | keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true) 11 | keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) } 12 | end 13 | 14 | def fetch(*args) 15 | key = args.shift 16 | return(args.count == 1 ? args.first : raise("`#{key}` key not found")) unless has_key?(key) 17 | self[key] 18 | end 19 | 20 | def delete(key) 21 | value = self[key] 22 | reset_key(key) 23 | super 24 | value 25 | end 26 | 27 | def [](name) 28 | @_keys[name.to_s].try(:wait) 29 | @_keys[name.to_s] ||= Dispatch::Semaphore.new(0) 30 | if @_cached_values.has_key?(name.to_s) 31 | @_keys[name.to_s].try(:signal) 32 | return @_cached_values[name.to_s] 33 | end 34 | result = @_cached_values.fetch(name.to_s, normalizer.normalize_object(super, receiver)) 35 | @_cached_values[name.to_s] = result 36 | @_keys[name.to_s].try(:signal) 37 | result 38 | end 39 | 40 | def []=(key, value) 41 | reset_key(key) 42 | super 43 | end 44 | 45 | def merge!(hash) 46 | result = super 47 | hash.keys.each { |key| reset_key(key) } 48 | result 49 | end 50 | 51 | def deep_merge!(hash) 52 | result = super 53 | hash.keys.each { |key| reset_key(key) } 54 | result 55 | end 56 | 57 | def reset_key(key) 58 | @_keys[key.to_s].try(:signal) 59 | @_keys.delete(key.to_s) 60 | @_cached_values.delete(key.to_s) 61 | end 62 | 63 | def receiver 64 | raise "Implement" 65 | end 66 | end 67 | end -------------------------------------------------------------------------------- /motion-prime/services/logger.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class Logger 3 | LOGGER_ERROR_LEVEL = 0 4 | LOGGER_INFO_LEVEL = 1 5 | LOGGER_DEBUG_LEVEL = 2 6 | LOGGER_DEALLOC_LEVEL = 3 7 | 8 | COLORS = { 9 | red: [ "\e[0;31m", "\e[0m" ], 10 | green: [ "\e[0;32m", "\e[0m" ], 11 | yellow: [ "\e[0;33m", "\e[0m" ], 12 | blue: [ "\e[0;34m", "\e[0m" ], 13 | none: [ "", ""] 14 | } 15 | 16 | class_attribute :level, :dealloc_items 17 | attr_accessor :disabled 18 | 19 | def initialize 20 | @default_level = Config.logger.level.nil? ? :info : Config.logger.level 21 | @disabled = false 22 | end 23 | 24 | # Log message, colorized if using simulator. 25 | # @param message [Array, String] Message or array of messages to log. 26 | # @param color [Symbol] Color of message: red, green, yellow, blue. 27 | # @return message [Array, String] Logged message 28 | def log(objects, label = '', color = :none) 29 | message = "#{label || 'PRIME_LOG'} : " + Array.wrap(objects).map(&:inspect).join(',') 30 | if Device.simulator? 31 | color_parts = COLORS[color] || COLORS[:none] 32 | output(color_parts.first + message + color_parts.last) 33 | else 34 | output(message) 35 | end 36 | objects 37 | end 38 | 39 | # Output message, using "puts" for simulator and NSLog for Device. 40 | # @param message [String] Message or array of messages to output. 41 | # @return message [Array, String] Message 42 | def output(message) 43 | return if disabled 44 | if Device.simulator? 45 | puts(message) 46 | else 47 | NSLog(message) 48 | end 49 | message 50 | end 51 | 52 | def error(*args) 53 | log(args, "PRIME_ERROR", :red) if LOGGER_ERROR_LEVEL <= current_level 54 | end 55 | 56 | def info(*args) 57 | log(args, "PRIME_INFO", :green) if LOGGER_INFO_LEVEL <= current_level 58 | end 59 | 60 | def debug(*args) 61 | log(args, "PRIME_DEBUG", :yellow) if LOGGER_DEBUG_LEVEL <= current_level 62 | end 63 | 64 | def dealloc_message(type, object, *args) 65 | if LOGGER_DEALLOC_LEVEL <= current_level 66 | if dealloc_items.include?(type.to_s) 67 | log([object.object_id, object.to_s] + args, "DEALLOC #{type}", :yellow) 68 | end 69 | end 70 | end 71 | 72 | private 73 | def dealloc_items 74 | self.class.dealloc_items || [] 75 | end 76 | 77 | def current_level 78 | current_level = self.class.level || @default_level 79 | case current_level.to_s 80 | when 'error' 81 | LOGGER_ERROR_LEVEL 82 | when 'info' 83 | LOGGER_INFO_LEVEL 84 | when 'debug' 85 | LOGGER_DEBUG_LEVEL 86 | when 'dealloc' 87 | LOGGER_DEALLOC_LEVEL 88 | else 89 | 2 90 | end 91 | end 92 | end 93 | end -------------------------------------------------------------------------------- /motion-prime/services/section_computed_options.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | class SectionComputedOptions < BaseComputedOptions 3 | attr_reader :section, :styles 4 | 5 | def initialize(section, options = {}) 6 | super() 7 | @section = section.weak_ref 8 | 9 | raw_options = section.class.container_options.try(:clone) || {} 10 | raw_options.deep_merge!(section.options[:container] || {}) 11 | 12 | if section_styles = section.section_styles 13 | container_options_from_styles = Styles.for(section_styles.values.flatten)[:container] 14 | if container_options_from_styles.present? 15 | raw_options = container_options_from_styles.deep_merge(raw_options) 16 | end 17 | end 18 | self.merge!(raw_options) 19 | end 20 | 21 | def normalizer 22 | section 23 | end 24 | 25 | def receiver 26 | section.send(:elements_eval_object) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /motion-prime/services/table_data_indexes.rb: -------------------------------------------------------------------------------- 1 | class TableDataIndexes 2 | def initialize(data) 3 | is_flat_data = !data.first.is_a?(Array) 4 | @count_in_sections = if is_flat_data 5 | [data.count] 6 | else 7 | data.map(&:count) 8 | end 9 | 10 | @sections_count = is_flat_data ? 1 : data.count 11 | end 12 | 13 | def max_index(*indexes) 14 | [*indexes].compact.max &method(:compare_indexes) 15 | end 16 | 17 | def compare_indexes(a, b) 18 | return 0 if a == b 19 | a.section > b.section || a.row > b.row ? 1 : -1 20 | end 21 | 22 | def greater_than(a, b) 23 | compare_indexes(a, b) > 0 24 | end 25 | 26 | def less_than(a, b) 27 | compare_indexes(a, b) < 0 28 | end 29 | 30 | def sum_index(a, rows, crop_to_edges = true) 31 | row = a.row + rows 32 | section = a.section 33 | 34 | max_row = @count_in_sections[a.section] - 1 35 | if row < 0 || row > max_row 36 | direction = row < 0 ? -1 : 1 37 | 38 | section = a.section + direction 39 | edge_row = [[0, row].max, max_row].min 40 | 41 | max_section = @sections_count - 1 42 | if section < 0 || section > max_section 43 | edge_section = [[section, 0].max, max_section].min 44 | return crop_to_edges ? NSIndexPath.indexPathForRow(edge_row, inSection: edge_section) : false 45 | end 46 | 47 | start_row = edge_row.zero? ? @count_in_sections[section] - 1 : 0 48 | rows_left = rows - (edge_row - a.row) - direction 49 | sum_index(NSIndexPath.indexPathForRow(start_row, inSection: section), rows_left) 50 | else 51 | NSIndexPath.indexPathForRow(row, inSection: section) 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /motion-prime/styles/_mixins.rb: -------------------------------------------------------------------------------- 1 | motion_require '../views/styles' 2 | MotionPrime::Styles.define :_mixin do 3 | style :label_reset, 4 | top: nil, left: nil, height: nil, right: nil, bottom: nil, top: nil, 5 | padding: nil, padding_top: nil, padding_left: nil, padding_right: nil, padding_bottom: nil, 6 | size_to_fit: false 7 | 8 | style :multiline, 9 | size_to_fit: true, 10 | number_of_lines: 0, 11 | line_break_mode: :word_wrap, 12 | line_spacing: 2, 13 | height: nil 14 | end 15 | -------------------------------------------------------------------------------- /motion-prime/styles/base.rb: -------------------------------------------------------------------------------- 1 | motion_require '../views/styles' 2 | MotionPrime::Styles.define :base do 3 | # basic screen styles 4 | style :screen, 5 | background_color: :white 6 | 7 | # basic table styles 8 | # ---------- 9 | style :table, 10 | top: 0, 11 | left: 0, 12 | right: 0, 13 | bottom: 0, 14 | separator_inset: [0,0] 15 | 16 | style :collection, 17 | top: 0, 18 | left: 0, 19 | right: 0, 20 | bottom: 0 21 | 22 | style :table_cell, 23 | background_color: :clear 24 | 25 | # basic form styles 26 | # ---------- 27 | style :form, 28 | right: 0, 29 | left: 0, 30 | top: 0, 31 | right: 0, 32 | bottom: 0, 33 | background_color: :clear, 34 | separator_color: :clear, 35 | scroll_enabled: true 36 | 37 | # available options for submit button: 38 | # @button_type: :rounded, :custom 39 | # @background_color: COLOR 40 | # @background_image: PATH_TO_FILE 41 | style :submit_button, :form_submit_field_button, 42 | background_color: :gray, 43 | title_color: :white, 44 | left: 20, 45 | right: 20, 46 | top: 10, 47 | height: 44 48 | 49 | style :segmented_control, 50 | height: 40, 51 | left: 0, 52 | right: 0, 53 | top: 0 54 | 55 | style :google_map, 56 | top: 0, 57 | left: 0, 58 | right: 0, 59 | bottom: 0 60 | 61 | style :spinner, 62 | annular: true, 63 | center: proc { screen.view.center }, 64 | width: 37, 65 | height: 37, 66 | progress_tint_color: :app_base.uicolor, 67 | background_tint_color: :black.uicolor(0.05), 68 | progress: 0.25 69 | 70 | style :spinner_message, mixins: [:multiline], 71 | top: proc { screen.view.center.y + 38 }, left: 50, width: 220, text_alignment: :center, 72 | font_name: :app_base, 73 | font_size: 18, 74 | line_spacing: 6 75 | end 76 | -------------------------------------------------------------------------------- /motion-prime/styles/form.rb: -------------------------------------------------------------------------------- 1 | motion_require '../views/styles' 2 | MotionPrime::Styles.define :base_form do 3 | style :header, container: {height: 25} 4 | style :header_label, mixins: [:multiline], 5 | left: 0, 6 | bottom: 5, 7 | top: nil, 8 | right: 0, 9 | size_to_fit: true 10 | 11 | style :header_hint, 12 | left: 0, 13 | bottom: 5, 14 | top: nil, 15 | right: 0 16 | 17 | style :field, :cell, 18 | selection_style: :none, 19 | background_color: :clear 20 | 21 | style :field_label, 22 | background_color: :clear, 23 | text_color: :gray, 24 | top: 15, 25 | height: 16, 26 | left: 20, 27 | right: 20, 28 | font_name: :app_base, 29 | font_size: 12, 30 | size_to_fit: true 31 | 32 | style :field_error_message, mixins: [:multiline], 33 | top: nil, 34 | bottom: 0, 35 | left: 20, 36 | right: 20, 37 | text_color: :app_error, 38 | font_name: :app_base, 39 | font_size: 12 40 | 41 | style :string_field_input, :password_field_input, :text_field_input, 42 | layer: { 43 | border_width: 1, 44 | border_color: :gray 45 | }, 46 | font_name: :app_base, 47 | font_size: 16, 48 | placeholder_font_name: :app_base, 49 | placeholder_font_size: 16, 50 | background_color: :white, 51 | left: 20, 52 | right: 20, 53 | top: 30, 54 | height: 30 55 | 56 | style :date_field_input, 57 | height: 150, 58 | top: 30, 59 | left: 20, 60 | right: 20 61 | 62 | style :select_field_button, 63 | background_color: :white, 64 | left: 20, 65 | right: 20, 66 | top: 30, 67 | height: 35, 68 | title_shadow_color: :white, 69 | layer: { 70 | border_color: :gray, 71 | border_width: 1 72 | }, 73 | title_color: :gray, 74 | title_label: { 75 | font_name: :app_base, 76 | font_size: 16 77 | } 78 | 79 | style :select_field_arrow, 80 | image: "images/forms/select_arrow.png", 81 | top: 40, 82 | right: 25, 83 | width: 9, 84 | height: 14 85 | 86 | style :switch_field_input, 87 | top: 10, 88 | right: 20, 89 | width: 51 90 | 91 | style :switch_field_label, 92 | top: 10, 93 | font_name: :app_base, 94 | font_size: 16 95 | 96 | style :switch_field_hint, 97 | top: 40, 98 | font_name: :app_base, 99 | font_size: 12 100 | 101 | style :field_input_with_errors, 102 | layer: { 103 | border_color: :app_error 104 | }, 105 | text_color: :app_error 106 | end 107 | -------------------------------------------------------------------------------- /motion-prime/support/_control_content_alignment.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module SupportControlContentAlignment 3 | VERTICAL_ALIGNMENT_CONSTS = { 4 | UIControlContentVerticalAlignmentCenter => :center, 5 | UIControlContentVerticalAlignmentTop => :top, 6 | UIControlContentVerticalAlignmentBottom => :bottom, 7 | UIControlContentVerticalAlignmentFill => :fill # TODO: handle this value 8 | } 9 | def setContentVerticalAlignment(value) 10 | return unless @_content_vertical_alignment = VERTICAL_ALIGNMENT_CONSTS[value] 11 | super 12 | end 13 | 14 | def padding_top 15 | padding_top = self.paddingTop || self.padding 16 | if @_content_vertical_alignment == :bottom 17 | padding_bottom = self.paddingBottom || self.padding 18 | bounds_height - padding_bottom.to_i - line_height 19 | elsif @_content_vertical_alignment == :top 20 | padding_top.to_i 21 | else # center label 22 | padding_top_offset = padding_top.to_i - (self.paddingBottom || self.padding).to_i 23 | (bounds_height - line_height)/2 + padding_top_offset 24 | end 25 | end 26 | 27 | def padding_bottom 28 | (bounds_height - (line_height + padding_top)) 29 | end 30 | 31 | def line_height 32 | @_line_height || self.font.pointSize 33 | end 34 | 35 | def bounds_height 36 | self.bounds.size.height 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /motion-prime/support/_key_value_store.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module SupportKeyValueStore 3 | # Key-Value accessors 4 | def setValue(value, forUndefinedKey: key) 5 | self.send(:"#{key}=", key) 6 | end 7 | end 8 | end -------------------------------------------------------------------------------- /motion-prime/support/_padding_attribute.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module SupportPaddingAttribute 3 | extend ::MotionSupport::Concern 4 | 5 | included do 6 | attr_accessor :paddingLeft, :paddingRight, :paddingTop, :paddingBottom, :padding 7 | end 8 | 9 | module ClassMethods 10 | def default_padding_top 11 | 0 12 | end 13 | 14 | def default_padding_left 15 | 0 16 | end 17 | 18 | def default_padding_right 19 | 0 20 | end 21 | 22 | def default_padding_bottom 23 | 0 24 | end 25 | end 26 | 27 | def padding_left 28 | self.paddingLeft || self.padding || self.class.default_padding_left 29 | end 30 | 31 | def padding_right 32 | self.paddingRight || self.padding || self.class.default_padding_right 33 | end 34 | 35 | def padding_top 36 | self.paddingTop || self.padding || self.class.default_padding_top 37 | end 38 | 39 | def padding_bottom 40 | self.paddingBottom || self.padding || self.class.default_padding_bottom 41 | end 42 | 43 | def padding_insets 44 | UIEdgeInsetsMake(padding_top, padding_left, padding_bottom, padding_right) 45 | end 46 | 47 | def apply_padding(rect) 48 | return unless apply_padding? 49 | apply_padding!(rect) 50 | end 51 | 52 | def apply_padding!(rect) 53 | raise "requires implementation" 54 | end 55 | 56 | def apply_padding? 57 | ![padding_top, padding_left, padding_right, padding_bottom].all?(&:zero?) 58 | end 59 | end 60 | end -------------------------------------------------------------------------------- /motion-prime/support/consts.rb: -------------------------------------------------------------------------------- 1 | IS_OS_8_OR_HIGHER = UIDevice.currentDevice.systemVersion.floatValue >= 8.0 2 | # this constants should be listed here to be accessible in constantize 3 | 4 | UIControlContentHorizontalAlignmentCenter 5 | UIControlContentHorizontalAlignmentLeft 6 | UIControlContentHorizontalAlignmentRight 7 | UIControlContentHorizontalAlignmentFill 8 | 9 | UIControlContentVerticalAlignmentCenter 10 | UIControlContentVerticalAlignmentTop 11 | UIControlContentVerticalAlignmentBottom 12 | UIControlContentVerticalAlignmentFill 13 | 14 | if defined?(ALAssetsLibrary) 15 | ALAssetsLibrary 16 | end 17 | 18 | if defined?(ALAuthorizationStatusNotDetermined) 19 | ALAuthorizationStatusNotDetermined 20 | end 21 | 22 | if defined?(ALAssetsGroupAll) 23 | ALAssetsGroupAll 24 | end 25 | -------------------------------------------------------------------------------- /motion-prime/support/mp_button.rb: -------------------------------------------------------------------------------- 1 | motion_require '_key_value_store' 2 | motion_require '_padding_attribute' 3 | motion_require '_control_content_alignment' 4 | class MPButton < UIButton 5 | include MotionPrime::SupportKeyValueStore 6 | include MotionPrime::SupportPaddingAttribute 7 | include MotionPrime::SupportControlContentAlignment 8 | 9 | attr_accessor :sizeToFit 10 | 11 | def setTitle(value) 12 | setTitle value, forState: UIControlStateNormal 13 | end 14 | 15 | def setImage(value) 16 | setImage value, forState: UIControlStateNormal 17 | end 18 | 19 | def setTitleEdgeInsets(value) 20 | @custom_title_inset_drawn = true 21 | super 22 | end 23 | 24 | def self.default_padding_left 25 | 5 26 | end 27 | 28 | def self.default_padding_right 29 | 5 30 | end 31 | 32 | def apply_padding!(rect) 33 | self.setTitleEdgeInsets(padding_insets) 34 | end 35 | 36 | def apply_padding? 37 | # TODO: we should run super method here, but for some reason it doesn't work in RM 2.32 38 | ![padding_top, padding_left, padding_right, padding_bottom].all?(&:zero?) && 39 | !@custom_title_inset_drawn 40 | end 41 | 42 | def drawRect(rect) 43 | apply_padding(rect) 44 | super 45 | end 46 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_collection_cell_with_section.rb: -------------------------------------------------------------------------------- 1 | class MPCollectionCellWithSection < UICollectionViewCell 2 | attr_reader :section 3 | 4 | def setSection(section) 5 | @section = section.try(:weak_ref) 6 | end 7 | 8 | def drawRect(rect) 9 | section.try(:draw_in, rect) 10 | super 11 | end 12 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_label.rb: -------------------------------------------------------------------------------- 1 | motion_require '../support/_key_value_store' 2 | motion_require '../support/_padding_attribute' 3 | class MPLabel < UILabel 4 | include MotionPrime::SupportKeyValueStore 5 | include MotionPrime::SupportPaddingAttribute 6 | 7 | def drawTextInRect(rect) 8 | rect = UIEdgeInsetsInsetRect(rect, padding_insets) 9 | super(rect) 10 | end 11 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_search_bar_custom.rb: -------------------------------------------------------------------------------- 1 | # Search bar with background and no padding 2 | class MPSearchBarCustom < UISearchBar 3 | def layoutSubviews 4 | super 5 | text_field = subviews.objectAtIndex(0).subviews.detect do |view| 6 | view.is_a?(UISearchBarTextField) 7 | end 8 | text_field.frame = CGRectMake(0, 0, 320, 44) 9 | end 10 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_spinner.rb: -------------------------------------------------------------------------------- 1 | class MPSpinner < MBRoundProgressView 2 | def init_animation 3 | return if @firstTimestamp 4 | displayLink = CADisplayLink.displayLinkWithTarget(self, selector: :"handleDisplayLink:") 5 | displayLink.addToRunLoop(NSRunLoop.currentRunLoop, forMode: NSDefaultRunLoopMode) 6 | end 7 | 8 | def handleDisplayLink(displayLink) 9 | @firstTimestamp ||= displayLink.timestamp 10 | elapsed = (displayLink.timestamp - @firstTimestamp) 11 | rotate(elapsed) 12 | end 13 | 14 | def rotate(angle) 15 | self.layer.transform = CATransform3DMakeRotation((Math::PI * 2) * angle, 0, 0, 1) 16 | end 17 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_table_cell_content_view.rb: -------------------------------------------------------------------------------- 1 | class MPTableViewCellContentView < UITableViewCellContentView 2 | attr_accessor :section 3 | 4 | def setSection(section) 5 | @section = section.try(:weak_ref) 6 | end 7 | 8 | def drawRect(rect) 9 | section.try(:draw_in, rect) 10 | super 11 | end 12 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_table_cell_with_section.rb: -------------------------------------------------------------------------------- 1 | class MPTableCellWithSection < UITableViewCell 2 | attr_reader :section 3 | attr_accessor :scroll_view, :content_view 4 | 5 | def setNeedsDisplay 6 | content_view.try(:setNeedsDisplay) 7 | super 8 | end 9 | 10 | def setSection(section) 11 | @section = section.try(:weak_ref) 12 | self.content_view.setSection(@section) 13 | end 14 | 15 | def initialize_content 16 | self.scroll_view = self.subviews.first 17 | # iOS 8 18 | if self.scroll_view.is_a?(UITableViewCellContentView) 19 | self.scroll_view.removeFromSuperview 20 | self.scroll_view = self 21 | else 22 | self.scroll_view.subviews.first.removeFromSuperview 23 | end 24 | self.content_view = MPTableViewCellContentView.alloc.initWithFrame(self.bounds) 25 | self.content_view.setBackgroundColor(:clear.uicolor) 26 | self.content_view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight 27 | self.content_view.top = 0 28 | self.content_view.left = 0 29 | 30 | self.scroll_view.addSubview(content_view) 31 | end 32 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_table_header_with_section.rb: -------------------------------------------------------------------------------- 1 | class MPTableHeaderWithSectionView < UITableViewHeaderFooterView 2 | attr_accessor :section, :selection_style, :content_view 3 | 4 | def setSection(section) 5 | @section = section.try(:weak_ref) 6 | self.content_view.setSection(@section) 7 | end 8 | 9 | def setNeedsDisplay 10 | content_view.try(:setNeedsDisplay) 11 | super 12 | end 13 | 14 | def initialize_content 15 | self.content_view = MPTableViewCellContentView.alloc.initWithFrame(self.bounds) 16 | self.content_view.setBackgroundColor(:clear.uicolor) 17 | self.content_view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight 18 | self.content_view.top = 0 19 | self.content_view.left = 0 20 | 21 | self.addSubview(content_view) 22 | end 23 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_table_view.rb: -------------------------------------------------------------------------------- 1 | class MPTableView < UITableView 2 | def dealloc 3 | Prime.logger.dealloc_message :view, self.to_s 4 | super 5 | end 6 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_text_field.rb: -------------------------------------------------------------------------------- 1 | # This class have some modifications for UITextField: 2 | # * support padding, padding_left, padding_right options 3 | # * support placeholder_color, placeholder_font options 4 | motion_require '_key_value_store' 5 | motion_require '_padding_attribute' 6 | motion_require '_control_content_alignment' 7 | class MPTextField < UITextField 8 | include MotionPrime::SupportKeyValueStore 9 | include MotionPrime::SupportPaddingAttribute 10 | include MotionPrime::SupportControlContentAlignment 11 | 12 | attr_accessor :placeholderColor, :placeholderFont, :readonly, :placeholderAlignment 13 | 14 | def self.default_padding_left 15 | 5 16 | end 17 | 18 | def self.default_padding_right 19 | 5 20 | end 21 | 22 | # placeholder position 23 | def textRectForBounds(bounds) 24 | @_line_height = font.pointSize 25 | rect = calculate_rect_for(bounds) 26 | @_line_height = nil 27 | rect 28 | end 29 | 30 | # text position 31 | def editingRectForBounds(bounds) 32 | @_line_height = self.font.pointSize 33 | rect = calculate_rect_for(bounds) 34 | @_line_height = nil 35 | rect 36 | end 37 | 38 | def drawPlaceholderInRect(rect) 39 | font_diff = self.font.pointSize - placeholder_font.pointSize 40 | rect.origin.y += font_diff/2.0 41 | color = self.placeholderColor || :gray.uicolor 42 | color.setFill 43 | 44 | truncation = :tail_truncation.uilinebreakmode 45 | alignment = (placeholderAlignment || :left.nstextalignment) 46 | self.placeholder.drawInRect(rect, withFont: placeholder_font, lineBreakMode: truncation, alignment: alignment) 47 | end 48 | 49 | private 50 | def placeholder_font 51 | self.placeholderFont || self.font || :system.uifont(16) 52 | end 53 | 54 | def calculate_rect_for(bounds) 55 | height_diff = self.bounds.size.height - (line_height + padding_top + padding_bottom) 56 | bounds = CGRectMake(bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height - height_diff) 57 | CGRectMake( 58 | bounds.origin.x + padding_left, 59 | bounds.origin.y + padding_top, 60 | bounds.size.width - (padding_left + padding_right), 61 | bounds.size.height - (padding_top + padding_bottom) 62 | ) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /motion-prime/support/mp_text_view.rb: -------------------------------------------------------------------------------- 1 | # This class have some modifications for UITextView: 2 | # * support padding, padding_left, padding_right options 3 | # * support placeholder, placeholder_color, placeholder_font options 4 | motion_require '../support/_key_value_store' 5 | motion_require '../support/_padding_attribute' 6 | class MPTextView < UITextView 7 | include MotionPrime::SupportKeyValueStore 8 | include MotionPrime::SupportPaddingAttribute 9 | 10 | attr_accessor :placeholderColor, :placeholderFont, :placeholder 11 | 12 | def self.default_padding_left 13 | 5 14 | end 15 | 16 | def self.default_padding_right 17 | 5 18 | end 19 | 20 | def drawPadding(rect) 21 | # add padding to UITextView 22 | self.textContainer.lineFragmentPadding = 0 # left/right 23 | self.textContainerInset = self.padding_insets 24 | end 25 | 26 | def drawPlaceholder(rect) 27 | padding = UIEdgeInsetsMake( 28 | padding_top, padding_left, 29 | padding_bottom, padding_right 30 | ) 31 | if self.placeholder && self.text.blank? 32 | color = self.placeholderColor || :gray.uicolor 33 | color.setFill 34 | font = self.placeholderFont || self.font || :system.uifont(16) 35 | 36 | color.setFill 37 | rect = CGRectMake( 38 | rect.origin.x + padding_left, 39 | rect.origin.y + padding_top, 40 | self.frame.size.width - padding_left, 41 | self.frame.size.height - padding_top 42 | ) 43 | placeholder.drawInRect(rect, withFont: font) 44 | end 45 | end 46 | 47 | def drawRect(rect) 48 | drawPadding(rect) 49 | drawPlaceholder(rect) 50 | super 51 | end 52 | 53 | def initPlaceholder 54 | NSNotificationCenter.defaultCenter.addObserver(self, 55 | selector: :textChanged, name: UITextViewTextDidChangeNotification, object: self 56 | ) 57 | @shouldDrawPlaceholder = placeholder && self.text.blank? 58 | end 59 | 60 | def textChanged 61 | updatePlaceholderDraw 62 | end 63 | 64 | def updatePlaceholderDraw 65 | prev = @shouldDrawPlaceholder 66 | @shouldDrawPlaceholder = placeholder && self.text.blank? 67 | if prev != @shouldDrawPlaceholder 68 | self.setNeedsDisplay 69 | end 70 | end 71 | 72 | # custom initializer 73 | def initWithCoder(aDecoder) 74 | if super 75 | initPlaceholder 76 | end 77 | self 78 | end 79 | 80 | def initWithFrame(frame) 81 | if super 82 | initPlaceholder 83 | end 84 | self 85 | end 86 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_view_controller.rb: -------------------------------------------------------------------------------- 1 | class MPViewController < UIViewController 2 | def self.new(args = {}) 3 | self.alloc.initWithNibName(nil, bundle: nil).tap do |screen| 4 | screen.on_create(args) if screen.respond_to?(:on_create) 5 | end 6 | end 7 | 8 | def viewDidLoad 9 | super 10 | self.view_did_load if self.respond_to?(:view_did_load) 11 | end 12 | 13 | def viewWillAppear(animated) 14 | super 15 | self.view_will_appear(animated) if self.respond_to?("view_will_appear:") 16 | end 17 | 18 | def viewDidAppear(animated) 19 | super 20 | self.view_did_appear(animated) if self.respond_to?("view_did_appear:") 21 | end 22 | 23 | def viewWillDisappear(animated) 24 | self.view_will_disappear(animated) if self.respond_to?("view_will_disappear:") 25 | super 26 | end 27 | 28 | def viewDidDisappear(animated) 29 | if self.respond_to?("view_did_disappear:") 30 | self.view_did_disappear(animated) 31 | end 32 | super 33 | end 34 | 35 | def shouldAutorotateToInterfaceOrientation(orientation) 36 | self.should_rotate(orientation) 37 | end 38 | 39 | def shouldAutorotate 40 | self.should_autorotate 41 | end 42 | 43 | def willRotateToInterfaceOrientation(orientation, duration:duration) 44 | self.will_rotate(orientation, duration) 45 | end 46 | 47 | def didRotateFromInterfaceOrientation(orientation) 48 | self.on_rotate 49 | end 50 | end -------------------------------------------------------------------------------- /motion-prime/support/mp_view_with_section.rb: -------------------------------------------------------------------------------- 1 | class MPViewWithSection < UIView 2 | attr_accessor :section 3 | 4 | def setSection(section) 5 | @section = section.try(:weak_ref) 6 | end 7 | 8 | def drawRect(rect) 9 | section.draw_in(rect) 10 | end 11 | end -------------------------------------------------------------------------------- /motion-prime/support/temp_fixes.rb: -------------------------------------------------------------------------------- 1 | # TODO: remove after fixing strange issue with attr reader 2 | MotionSupport::Callbacks::CallbackChain.class_eval do 3 | def config 4 | @config 5 | end 6 | end -------------------------------------------------------------------------------- /motion-prime/support/ui_view.rb: -------------------------------------------------------------------------------- 1 | class UIView 2 | attr_accessor :name 3 | 4 | def find name 5 | subviews.each do |subview| 6 | return subview if subview.name == name 7 | end 8 | nil 9 | end 10 | alias_method :subview, :find 11 | 12 | def sibling name 13 | if superview 14 | superview.find name 15 | else 16 | nil 17 | end 18 | end 19 | 20 | def closest name 21 | view = sibling name 22 | if view.nil? && superview 23 | view = superview.closest name 24 | end 25 | view 26 | end 27 | 28 | def left 29 | self.frame.origin.x 30 | end 31 | 32 | def setLeft value 33 | self.frame = [[value, self.frame.origin.y], [self.frame.size.width, self.frame.size.height]] 34 | end 35 | 36 | def top 37 | self.frame.origin.y 38 | end 39 | 40 | def setTop value 41 | self.frame = [[self.frame.origin.x, value], [self.frame.size.width, self.frame.size.height]] 42 | end 43 | 44 | def width 45 | self.frame.size.width 46 | end 47 | 48 | def setWidth value 49 | self.frame = [[self.frame.origin.x, self.frame.origin.y], [value, self.frame.size.height]] 50 | end 51 | 52 | def height 53 | self.frame.size.height 54 | end 55 | 56 | def setHeight value 57 | self.frame = [[self.frame.origin.x, self.frame.origin.y], [self.frame.size.width, value]] 58 | end 59 | end -------------------------------------------------------------------------------- /motion-prime/version.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | VERSION = "1.0.7" 3 | end -------------------------------------------------------------------------------- /motion-prime/views/_frame_calculator_mixin.rb: -------------------------------------------------------------------------------- 1 | module MotionPrime 2 | module FrameCalculatorMixin 3 | def calculate_frame_for(parent_bounds, options) 4 | width = options[:width] 5 | height = options[:height] 6 | top = options[:top] 7 | right = options[:right] 8 | bottom = options[:bottom] 9 | left = options[:left] 10 | value_type = options[:value_type].to_s # absolute/relative 11 | 12 | if options[:height_to_fit].present? && height.nil? && (top.nil? || bottom.nil?) 13 | height = options[:height_to_fit] 14 | end 15 | 16 | return parent_bounds if width.nil? && height.nil? && right.nil? && bottom.nil? 17 | frame = CGRectMake(0,0,0,0) 18 | 19 | max_width = parent_bounds.size.width 20 | max_height = parent_bounds.size.height 21 | temp_width = width || 0.0 22 | temp_height = height || 0.0 23 | 24 | if left && left > 0 && left <= 1 && value_type != 'absolute' && left.is_a?(Float) 25 | left = (max_width * left).round(2) 26 | end 27 | 28 | if right && right > 0 && right <= 1 && value_type != 'absolute' && right.is_a?(Float) 29 | right = (max_width * right).round(2) 30 | end 31 | 32 | if top && top > 0 && top <= 1 && value_type != 'absolute' && top.is_a?(Float) 33 | top = (max_height * top).round(2) 34 | end 35 | 36 | if bottom && bottom > 0 && bottom <= 1 && value_type != 'absolute' && bottom.is_a?(Float) 37 | bottom = (max_height * bottom).round(2) 38 | end 39 | 40 | # calculate left and right if width is relative, e.g 0.7 41 | if width && width > 0 && width <= 1 && value_type != 'absolute' && width.is_a?(Float) 42 | if right.nil? 43 | left ||= 0 44 | right = (max_width - max_width * width - left).round(2) 45 | else 46 | left = (max_width - max_width * width - right).round(2) 47 | end 48 | width = (max_width * width).round(2) 49 | end 50 | 51 | # calculate top and bottom if height is relative, e.g 0.7 52 | if height && height > 0 && height <= 1 && value_type != 'absolute' && height.is_a?(Float) 53 | if bottom.nil? 54 | top ||= 0 55 | bottom = (max_height - max_height * height - top).round(2) 56 | else 57 | top = (max_height - max_height * height - bottom).round(2) 58 | end 59 | height = (max_height * height).round(2) 60 | end 61 | 62 | if !left.nil? && !right.nil? 63 | frame.origin.x = left 64 | if options[:height_to_fit].nil? && width.nil? 65 | width = max_width - left - right 66 | end 67 | elsif !right.nil? 68 | frame.origin.x = max_width - temp_width - right 69 | elsif !left.nil? 70 | frame.origin.x = left 71 | else 72 | frame.origin.x = max_width / 2 - temp_width / 2 73 | end 74 | frame.size.width = width || 0.0 75 | 76 | if !top.nil? && !bottom.nil? 77 | frame.origin.y = top 78 | if options[:height_to_fit].nil? && height.nil? 79 | height = max_height - top - bottom 80 | end 81 | elsif !bottom.nil? 82 | frame.origin.y = max_height - temp_height - bottom 83 | elsif !top.nil? 84 | frame.origin.y = top 85 | else 86 | frame.origin.y = max_height / 2 - temp_height / 2 87 | end 88 | frame.size.height = height || 0.0 89 | 90 | frame 91 | rescue => e 92 | Prime.logger.error "can't calculate frame in #{self.class.name}. #{e}" 93 | CGRectMake(0,0,0,0) 94 | end 95 | end 96 | end -------------------------------------------------------------------------------- /motion-prime/views/layout.rb: -------------------------------------------------------------------------------- 1 | # TODO: make it part of Sections 2 | motion_require '../support/mp_table_cell_with_section' 3 | motion_require '../support/mp_collection_cell_with_section' 4 | motion_require '../support/mp_spinner' 5 | motion_require '../support/mp_button' 6 | motion_require '../support/mp_label' 7 | motion_require '../support/mp_text_field' 8 | motion_require '../support/mp_text_view' 9 | module MotionPrime 10 | module Layout 11 | def add_view(klass, options = {}, &block) 12 | options = options.clone 13 | render_target = options.delete(:render_target) 14 | parent_view = options.delete(:parent_view) || render_target 15 | 16 | parent_bounds = if view_stack.empty? 17 | parent_view.try(:bounds) || CGRectZero 18 | else 19 | view_stack.last.bounds 20 | end 21 | builder = ViewBuilder.new(klass, options.merge(parent_bounds: parent_bounds)) 22 | options = builder.options.merge(calculate_frame: options.fetch(:calculate_frame, true), parent_bounds: parent_bounds) 23 | view = builder.view 24 | insert_index = options[:at_index] 25 | 26 | set_options_for(view, options, &block) 27 | if superview = render_target || view_stack.last 28 | insert_index ? superview.insertSubview(view, atIndex: insert_index) : superview.addSubview(view) 29 | end 30 | view.on_added if view.respond_to?(:on_added) 31 | view 32 | end 33 | 34 | def set_options_for(view, options = {}, &block) 35 | ViewStyler.new(view, options.delete(:parent_bounds), options).apply 36 | view_stack.push(view) 37 | block.call(view) if block_given? 38 | view_stack.pop 39 | end 40 | alias_method :update_options_for, :set_options_for 41 | 42 | def setup(view, options = {}, &block) 43 | puts "DEPRECATION: screen#setup is deprecated, please use screen#set_options_for instead" 44 | set_options_for(view, options, &block) 45 | end 46 | 47 | def view_stack 48 | @view_stack ||= [] 49 | end 50 | 51 | def self.included base 52 | base.class_eval do 53 | [::UIActionSheet, ::UIActivityIndicatorView, ::MPButton, ::UIDatePicker, ::UIImageView, ::MPLabel, 54 | ::UIPageControl, ::UIPickerView, ::UIProgressView, ::UIScrollView, ::UISearchBar, ::UISegmentedControl, 55 | ::UISlider, ::UIStepper, ::UISwitch, ::UITabBar, ::UICollectionView, ::UITableView, ::UITableViewCell, 56 | ::MPTextField, ::MPTextView, ::UIToolbar, ::UIWebView, ::UINavigationBar, ::UIPageViewController, 57 | ::MPTableCellWithSection, ::MPCollectionCellWithSection, ::MBProgressHUD, ::MPSpinner].each do |klass| 58 | 59 | shorthand = "#{klass}"[2..-1].underscore.to_sym 60 | 61 | define_method(shorthand) do |options, &block| 62 | options[:screen] = self 63 | element = MotionPrime::BaseElement.factory(shorthand, options) 64 | element.render({}, &block) 65 | element 66 | end 67 | end 68 | end 69 | end 70 | end 71 | end -------------------------------------------------------------------------------- /motion-prime/views/styles.rb: -------------------------------------------------------------------------------- 1 | motion_require '../helpers/has_normalizer' 2 | module MotionPrime 3 | class Styles 4 | @@repo = {} 5 | 6 | def initialize(namespace = nil) 7 | @namespace = namespace 8 | end 9 | 10 | def style(*args, &block) 11 | names = Array.wrap(args) 12 | options = names.pop if args.last.is_a?(Hash) 13 | 14 | if options.present? 15 | parent = options.delete(:parent) 16 | if parent && (parent_namespace = options.delete(:parent_namespace) || @namespace) 17 | parent ="#{parent_namespace}_#{parent}".to_sym 18 | end 19 | mixins = Array.wrap(options.delete(:mixins)).map { |mixin_name| :"_mixin_#{mixin_name}" } 20 | 21 | names.each do |name| 22 | name = "#{@namespace}_#{name}".to_sym if @namespace 23 | @@repo[name] ||= {} 24 | @@repo[name].deep_merge!(self.class.for(parent, debug_missing: true, type: :parent, name: name)) if parent 25 | @@repo[name].deep_merge!(self.class.for(mixins, debug_missing: true, type: :mixin, name: name)) if mixins.present? 26 | @@repo[name].deep_merge! options 27 | end 28 | elsif !block_given? 29 | raise "No style rules specified for `#{names.join(', ')}`. Namespace: `#{@namespace}`" 30 | end 31 | 32 | names.each do |name| 33 | namespace = [@namespace, name].compact.join('_') 34 | self.class.new(namespace).instance_eval(&block) 35 | end if block_given? 36 | end 37 | alias_method :_, :style 38 | 39 | class << self 40 | include HasNormalizer 41 | 42 | def define(*namespaces, &block) 43 | @definition_blocks ||= [] 44 | namespaces = Array.wrap(namespaces) 45 | if namespaces.any? 46 | namespaces.each do |namespace| 47 | @definition_blocks << {namespace: namespace, block: block} 48 | end 49 | else 50 | @definition_blocks << {namespace: false, block: block} 51 | end 52 | end 53 | 54 | def define! 55 | @definition_blocks.each do |definition| 56 | block = definition[:block] 57 | self.new(definition[:namespace]).instance_eval(&block) 58 | end 59 | end 60 | 61 | def for(style_names, options = {}) 62 | style_options = {} 63 | Array.wrap(style_names).each do |name| 64 | styles = @@repo[name] 65 | Prime.logger.debug "No styles found for `#{name}` (element: `#{options[:name]}`, type: #{options.fetch(:type, 'general')})" if options[:debug_missing] && styles.blank? 66 | style_options.deep_merge!(styles || {}) 67 | end 68 | style_options 69 | end 70 | 71 | def extend_and_normalize_options(options = {}) 72 | style_options = self.for(options.delete(:styles)) 73 | normalize_options(options.deep_merge(style_options).deep_merge(options)) 74 | end 75 | end 76 | end 77 | end -------------------------------------------------------------------------------- /resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidlabs/motion-prime/9a7398f0f409b84d7ea71a7c5ca48bb0fcfe973f/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droidlabs/motion-prime/9a7398f0f409b84d7ea71a7c5ca48bb0fcfe973f/resources/Icon.png -------------------------------------------------------------------------------- /spec/factories/delegates.rb: -------------------------------------------------------------------------------- 1 | AppDelegate.send :include, MotionPrime::HasAuthorization 2 | AppDelegate.send :include, MotionPrime::DelegateBaseMixin 3 | AppDelegate.send :include, MotionPrime::DelegateNavigationMixin 4 | 5 | class BaseDelegate < MotionPrime::BaseAppDelegate 6 | def on_load(app, options) 7 | self.was_loaded = true 8 | end 9 | end -------------------------------------------------------------------------------- /spec/factories/init.rb: -------------------------------------------------------------------------------- 1 | MotionPrime::Config.configure! 2 | MotionPrime::Styles.define! -------------------------------------------------------------------------------- /spec/factories/models.rb: -------------------------------------------------------------------------------- 1 | class User < MotionPrime::Model 2 | attributes :name, :age, :birthday 3 | timestamp_attributes 4 | end 5 | class Plane < MotionPrime::Model 6 | attributes :name, :age 7 | end 8 | 9 | class Todo < MotionPrime::Model 10 | attribute :title 11 | bag :items 12 | end 13 | 14 | class TodoItem < MotionPrime::Model 15 | attribute :completed 16 | attribute :text 17 | end 18 | 19 | class Page < MotionPrime::Model 20 | attribute :text 21 | attribute :index 22 | end 23 | 24 | class Organization < MotionPrime::Model 25 | attribute :name 26 | has_many :projects 27 | end 28 | 29 | class Project < MotionPrime::Model 30 | attribute :title 31 | end 32 | 33 | class Autobot < MotionPrime::Model 34 | attribute :name 35 | attribute :uid, type: :integer 36 | attribute :release_at, type: :time 37 | attribute :strength, type: :float 38 | end 39 | 40 | module CustomModule; end 41 | class CustomModule::Car < MotionPrime::Model 42 | attribute :name 43 | attribute :created_at 44 | end 45 | Car = CustomModule::Car 46 | 47 | def stub_user(name, age, birthday, id = nil) 48 | user = User.new 49 | user.id = id || 1 50 | user.name = name 51 | user.age = age 52 | user.birthday = birthday 53 | user 54 | end 55 | 56 | def documents_path 57 | NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0] 58 | end -------------------------------------------------------------------------------- /spec/factories/scaffold/models/task.rb: -------------------------------------------------------------------------------- 1 | class Task < Prime::Model 2 | timestamp_attributes 3 | attribute :title 4 | end -------------------------------------------------------------------------------- /spec/factories/scaffold/screens/tasks.rb: -------------------------------------------------------------------------------- 1 | class TasksScreen < Prime::Screen 2 | title "Tasks" 3 | 4 | # open_screen "tasks#index" 5 | def index 6 | set_title "Tasks" 7 | set_navigation_right_button 'New' do 8 | open_screen "tasks#new" 9 | end 10 | set_section :tasks_index_table 11 | end 12 | 13 | # open_screen "tasks#show" 14 | def show 15 | @model = params[:model] 16 | set_title "Show Task" 17 | set_navigation_back_button 'Back' 18 | set_navigation_right_button 'Edit' do 19 | open_screen "tasks#edit", params: { model: @model } 20 | end 21 | set_section :tasks_show, model: @model 22 | end 23 | 24 | # open_screen "tasks#edit" 25 | def edit 26 | @model = params[:model] 27 | set_title "Edit Task" 28 | set_navigation_back_button 'Cancel' 29 | set_section :tasks_form, model: @model 30 | end 31 | 32 | # open_screen "tasks#new" 33 | def new 34 | @model = Task.new 35 | set_title "New Task" 36 | set_navigation_back_button 'Cancel' 37 | set_section :tasks_form, model: @model 38 | end 39 | 40 | def on_return 41 | if action?(:index) || action?(:show) 42 | refresh 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /spec/factories/scaffold/sections/tasks/form.rb: -------------------------------------------------------------------------------- 1 | class TasksFormSection < Prime::FormSection 2 | field :title, 3 | label: { text: 'Title' }, 4 | input: { 5 | text: proc { model.title }, 6 | placeholder: "Enter title here" 7 | } 8 | 9 | field :delete, type: :submit, 10 | button: { 11 | title: "Delete", 12 | background_color: :red 13 | }, 14 | action: :on_delete, 15 | if: proc { model.persisted? } 16 | 17 | field :submit, type: :submit, 18 | button: { title: "Save" }, 19 | action: :on_submit 20 | 21 | def on_delete 22 | model.delete 23 | screen.close_screen(to_root: true) 24 | end 25 | 26 | def on_submit 27 | model.assign_attributes(field_values) 28 | model.save 29 | screen.close_screen 30 | end 31 | end -------------------------------------------------------------------------------- /spec/factories/scaffold/sections/tasks/index_cell.rb: -------------------------------------------------------------------------------- 1 | class TasksIndexCellSection < Prime::Section 2 | container height: 40 3 | element :title, text: proc { model.title } 4 | end -------------------------------------------------------------------------------- /spec/factories/scaffold/sections/tasks/index_table.rb: -------------------------------------------------------------------------------- 1 | class TasksIndexTableSection < Prime::TableSection 2 | def collection_data 3 | Task.all.map do |model| 4 | TasksIndexCellSection.new(model: model) 5 | end 6 | end 7 | 8 | def on_click(index) 9 | section = data[index.row] 10 | screen.open_screen 'tasks#show', params: { model: section.model } 11 | end 12 | end -------------------------------------------------------------------------------- /spec/factories/scaffold/sections/tasks/show.rb: -------------------------------------------------------------------------------- 1 | class TasksShowSection < Prime::Section 2 | element :title, text: proc { model.title } 3 | end -------------------------------------------------------------------------------- /spec/factories/scaffold/styles/tasks.rb: -------------------------------------------------------------------------------- 1 | Prime::Styles.define :tasks do 2 | style :index do 3 | style :cell_title, 4 | text_color: :app_base, 5 | left: 20, 6 | top: 10, 7 | width: 280, 8 | font: :app_base.uifont(16), 9 | height: 20 10 | end 11 | style :show do 12 | style :title, 13 | top: 120, 14 | left: 0, 15 | right: 0, 16 | text_alignment: :center 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/factories/screens.rb: -------------------------------------------------------------------------------- 1 | class BaseScreen < MotionPrime::Screen 2 | title "Base" 3 | attr_accessor :was_rendered 4 | 5 | def render 6 | self.was_rendered = true 7 | set_navigation_right_button "Test", action: :test, type: UIBarButtonItemStyleDone 8 | end 9 | end 10 | 11 | class SampleScreen < MotionPrime::Screen 12 | title 'Sample' 13 | 14 | def render 15 | @main_section = SampleSection.new(screen: self) 16 | @main_section.render 17 | end 18 | end -------------------------------------------------------------------------------- /spec/factories/sections.rb: -------------------------------------------------------------------------------- 1 | class SampleViewSection < Prime::Section 2 | element :description, text: "Lorem Ipsum", as: :view 3 | end 4 | class SampleDrawSection < Prime::Section 5 | element :description, text: "Lorem Ipsum", as: :draw 6 | end 7 | class SampleSection < Prime::Section 8 | element :description, text: "Lorem Ipsum" 9 | end -------------------------------------------------------------------------------- /spec/features/scaffold/index.rb: -------------------------------------------------------------------------------- 1 | describe "scaffold index" do 2 | before do 3 | 5.times do |index| 4 | Task.create(title: "Task #{index}") 5 | end 6 | App.delegate.open_screen 'tasks#index' 7 | @controller = App.delegate.content_controller.childViewControllers.last 8 | end 9 | it "should render tasks list" do 10 | wait 0.3 do 11 | @controller.has_content?("Task 2").should.be.true 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /spec/features/screens/open_screen.rb: -------------------------------------------------------------------------------- 1 | describe "open screen" do 2 | describe "from app delegate with default options" do 3 | before do 4 | App.delegate.open_screen :sample 5 | @controller = App.delegate.content_controller 6 | end 7 | it "should open with navigation by default" do 8 | @controller.is_a?(UINavigationController).should.be.true 9 | end 10 | 11 | it "should open screen" do 12 | @controller.childViewControllers.first.is_a?(SampleScreen).should.be.true 13 | @controller.childViewControllers.first.visible?.should.be.true 14 | end 15 | end 16 | 17 | describe "from app delegate with navigation false" do 18 | before do 19 | App.delegate.open_screen :sample, navigation: false 20 | @controller = App.delegate.window.rootViewController 21 | end 22 | it "should open screen" do 23 | @controller.is_a?(SampleScreen).should.be.true 24 | @controller.visible?.should.be.true 25 | end 26 | end 27 | 28 | describe "from app delegate with action" do 29 | before do 30 | App.delegate.open_screen 'tasks#new' 31 | @controller = App.delegate.content_controller.childViewControllers.last 32 | end 33 | 34 | it "should open screen with action" do 35 | @controller.is_a?(TasksScreen).should.be.true 36 | @controller.title.should == 'New Task' 37 | end 38 | end 39 | 40 | describe "from another screen with navigation: true" do 41 | before do 42 | @parent_screen = SampleScreen.new(navigation: true) 43 | @child_screen = SampleScreen.new(navigation: true) 44 | 45 | App.delegate.open_screen @parent_screen 46 | @parent_screen.open_screen @child_screen 47 | @controller = App.delegate.content_controller 48 | 49 | # we should call it because will_appear will happen async 50 | @child_screen.will_appear 51 | @parent_screen.will_disappear 52 | end 53 | 54 | it "should open child screen navigational" do 55 | @controller.childViewControllers.last.should == @child_screen 56 | @child_screen.visible?.should.be.true 57 | end 58 | 59 | it "should make parent screen invisible" do 60 | @parent_screen.visible?.should.be.false 61 | end 62 | end 63 | end -------------------------------------------------------------------------------- /spec/helpers/has_content.rb: -------------------------------------------------------------------------------- 1 | class MotionPrime::BaseElement 2 | def has_content?(content) 3 | text = computed_options[:text] || computed_options[:title] || '' 4 | !!text.match(content) 5 | end 6 | end 7 | class MotionPrime::Section 8 | def has_content?(content) 9 | self.elements.values.any? do |element| 10 | element.has_content?(content) 11 | end 12 | end 13 | end 14 | class MotionPrime::TableSection 15 | def has_content?(content) 16 | data.any? do |section| 17 | section.has_content?(content) 18 | end 19 | end 20 | end 21 | class MotionPrime::Screen 22 | def has_content?(content) 23 | main_section.has_content?(content) 24 | end 25 | end -------------------------------------------------------------------------------- /spec/unit/config/store_spec.rb: -------------------------------------------------------------------------------- 1 | describe MotionPrime::Config do 2 | before { @config = MotionPrime::Config.new } 3 | 4 | describe "[]" do 5 | before { @config = MotionPrime::Config.new(foo: "bar") } 6 | 7 | it "returns the value if there is one" do 8 | @config[:foo].should == "bar" 9 | @config["foo"].should == "bar" 10 | end 11 | 12 | it "returns a new Configatron::Store object if there is no value" do 13 | @config[:unknown].is_a?(MotionPrime::Config).should == true 14 | @config["unknown"].is_a?(MotionPrime::Config).should == true 15 | end 16 | end 17 | 18 | describe "[]=" do 19 | it "sets the value" do 20 | @config[:foo] = "bar" 21 | @config[:foo].should == "bar" 22 | @config["foo"].should == "bar" 23 | 24 | @config[:baz] = "bazzy" 25 | @config[:baz].should == "bazzy" 26 | @config["baz"].should == "bazzy" 27 | end 28 | end 29 | 30 | describe "nil?" do 31 | it "returns true if there is no value set" do 32 | @config.foo.nil?.should == true 33 | @config.foo = "bar" 34 | @config.foo.nil?.should == false 35 | end 36 | end 37 | 38 | describe ":key_name?" do 39 | it "returns true if there is value and it's not false" do 40 | @config.foo = true 41 | @config.foo?.should == true 42 | end 43 | 44 | it "returns false if there is value and it's false" do 45 | @config.foo = false 46 | @config.foo?.should == false 47 | end 48 | 49 | it "returns false if there is no value" do 50 | @config.foo?.should == false 51 | end 52 | end 53 | 54 | describe "class methods" do 55 | it "should allow to set value for class" do 56 | MotionPrime::Config.foo.nil?.should == true 57 | MotionPrime::Config.foo = "bar" 58 | MotionPrime::Config.foo.should == "bar" 59 | end 60 | end 61 | 62 | describe "has_key?" do 63 | it "returns true if there is a key" do 64 | @config.has_key?(:foo).should == false 65 | @config.foo = "bar" 66 | @config.has_key?(:foo).should == true 67 | end 68 | 69 | it "returns false if the key is a MotionPrime::Config" do 70 | @config.has_key?(:foo).should == false 71 | @config.foo = MotionPrime::Config.new 72 | @config.has_key?(:foo).should == false 73 | end 74 | end 75 | 76 | describe "configuring with a block" do 77 | before do 78 | @config.a.b = 'B' 79 | end 80 | 81 | it "yields the store to configure" do 82 | @config.a do |a| 83 | a.c = 'C' 84 | end 85 | @config.a.b.should == 'B' 86 | @config.a.c.should == 'C' 87 | end 88 | end 89 | end -------------------------------------------------------------------------------- /spec/unit/delegate/delegate_spec.rb: -------------------------------------------------------------------------------- 1 | describe MotionPrime::BaseDelegate do 2 | 3 | before { @subject = BaseDelegate.new } 4 | 5 | it 'should call on_load on launch' do 6 | @subject.mock!(:on_load) do |app, options| 7 | app.should.be.kind_of(UIApplication) 8 | end 9 | 10 | @subject.application(UIApplication.sharedApplication, didFinishLaunchingWithOptions: {}) 11 | end 12 | end -------------------------------------------------------------------------------- /spec/unit/elements/label_spec.rb: -------------------------------------------------------------------------------- 1 | describe MotionPrime::Section do 2 | before do 3 | @screen = BaseScreen.new 4 | end 5 | 6 | describe "view label" do 7 | before do 8 | @section = SampleViewSection.new(screen: @screen) 9 | @section.create_elements 10 | end 11 | 12 | describe "size_to_fit option" do 13 | before do 14 | @section.element(:description).options.merge!({ 15 | text: '', 16 | size_to_fit: true 17 | }) 18 | end 19 | 20 | it "should set zero size" do 21 | @section.render 22 | @section.view(:description).bounds.size.width.should.be.zero 23 | @section.view(:description).bounds.size.height.should.be.zero 24 | end 25 | 26 | it "should update bounds with text" do 27 | @section.render 28 | @section.element(:description).update_with_options(text: 'test') 29 | @section.view(:description).bounds.size.width.should > 0 30 | @section.view(:description).bounds.size.height.should > 0 31 | end 32 | end 33 | 34 | # describe "`top` with zero `height`" do 35 | # before do 36 | # @section.element(:description).options.merge!({ 37 | # top: 20 38 | # }) 39 | # end 40 | 41 | # it "should set top" do 42 | # @section.render 43 | # @section.view(:description).origin.y.should == 20 44 | # end 45 | # end 46 | end 47 | 48 | describe "draw label" do 49 | before do 50 | @section = SampleDrawSection.new(screen: @screen) 51 | @section.create_elements 52 | end 53 | 54 | describe "size_to_fit option" do 55 | before do 56 | @section.element(:description).options.merge!({ 57 | text: '', 58 | size_to_fit: true 59 | }) 60 | end 61 | 62 | it "should set zero size" do 63 | @section.render 64 | @section.element(:description).size_to_fit_if_needed 65 | @section.element(:description).computed_options[:width].should.be.zero 66 | @section.element(:description).computed_options[:height].should.be.zero 67 | end 68 | 69 | it "should update bounds with text" do 70 | @section.render 71 | @section.element(:description).update_with_options(text: 'test') 72 | @section.element(:description).size_to_fit_if_needed 73 | @section.element(:description).computed_options[:width].should > 0 74 | @section.element(:description).computed_options[:height].should > 0 75 | end 76 | end 77 | 78 | describe "`top` with zero `height`" do 79 | before do 80 | @section.element(:description).options.merge!({ 81 | top: 20 82 | }) 83 | end 84 | 85 | it "should set top" do 86 | @section.render 87 | @section.container_view.bounds = UIScreen.mainScreen.bounds 88 | @section.element(:description).size_to_fit_if_needed 89 | @section.element(:description).draw_options[:top_left_corner].y.should == 20 90 | @section.element(:description).draw_options[:inner_rect].origin.y.should == 20 91 | end 92 | end 93 | end 94 | end -------------------------------------------------------------------------------- /spec/unit/models/association_collection_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Prime::AssociationCollection" do 2 | before do 3 | MotionPrime::Store.connect 4 | end 5 | 6 | after do 7 | MotionPrime::Store.shared_store.clear 8 | end 9 | 10 | describe "#new" do 11 | before do 12 | @organization = Organization.new 13 | @project = @organization.projects.new(title: 'test') 14 | end 15 | 16 | it "should instanciate model with given attributes" do 17 | @project.title.should == 'test' 18 | end 19 | 20 | it "should add model to association collection" do 21 | @organization.projects.include?(@project).should.be.true 22 | end 23 | end 24 | 25 | describe "#add" do 26 | before do 27 | @organization = Organization.new 28 | @project = Project.new(title: 'test') 29 | @organization.projects.add(@project) 30 | end 31 | 32 | it "should add model to association collection" do 33 | @organization.projects.include?(@project).should.be.true 34 | end 35 | end 36 | 37 | describe "#all" do 38 | before do 39 | @organization = Organization.new 40 | project = Project.new(title: 'test 1') 41 | @organization.projects.add(project) 42 | project = Project.new(title: 'test 2') 43 | @organization.projects.add(project) 44 | end 45 | 46 | it "should return all records" do 47 | @organization.projects.all.count.should == 2 48 | end 49 | end 50 | 51 | describe "#find" do 52 | before do 53 | @organization = Organization.new 54 | project = Project.create(title: 'test 1') 55 | @organization.projects.add(project) 56 | project = Project.create(title: 'test 2') 57 | @organization.projects.add(project) 58 | @organization.save 59 | end 60 | 61 | it "should return saved records by first hash" do 62 | @organization.projects.find(title: 'test 1').count.should == 1 63 | end 64 | end 65 | 66 | describe "#filter" do 67 | before do 68 | @organization = Organization.new 69 | project = Project.new(title: 'test 1') 70 | @organization.projects.add(project) 71 | project = Project.new(title: 'test 2') 72 | @organization.projects.add(project) 73 | end 74 | 75 | it "should return saved records by first hash" do 76 | @organization.projects.filter(title: 'test 1').count.should == 1 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/unit/models/associations_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Prime::Model Associations" do 2 | before do 3 | MotionPrime::Store.connect 4 | end 5 | 6 | after do 7 | MotionPrime::Store.shared_store.clear 8 | end 9 | 10 | describe "#bag" do 11 | it "adds a attr reader to the class" do 12 | todo = Todo.create(:title => "Today Tasks") 13 | todo.items.is_a?(MotionPrime::Bag).should == true 14 | todo.items.size.should == 0 15 | end 16 | 17 | it "adds a attr writer to the class that can take an Array" do 18 | todo = Todo.create(:title => "Today Tasks") 19 | todo.items = [TodoItem.new(:text => "Hi"), TodoItem.new(:text => "Foo"), TodoItem.new(:text => "Bar")] 20 | todo.items.is_a?(MotionPrime::Bag).should == true 21 | todo.items.size.should == 3 22 | end 23 | 24 | it "adds a writer to the class that can take a Bag" do 25 | todo = Todo.create(:title => "Today Tasks") 26 | todo.items = MotionPrime::Bag.bag 27 | todo.items.is_a?(MotionPrime::Bag).should == true 28 | todo.items.size.should == 0 29 | end 30 | 31 | it "allows to reload bag" do 32 | todo = Todo.create(:title => "Today Tasks") 33 | todo.items = [TodoItem.new(:text => "Hi")] 34 | todo.items.save 35 | todo.items << [TodoItem.new(:text => "Foo"), TodoItem.new(:text => "Bar")] 36 | todo.items.count.should == 3 37 | todo.items(true).count.should == 1 38 | end 39 | end 40 | 41 | describe "#save" do 42 | it "save a model also saves associated bags" do 43 | todo = Todo.create(:title => "Today Tasks") 44 | todo.items = [TodoItem.new(:text => "Hi"), TodoItem.new(:text => "Foo"), TodoItem.new(:text => "Bar")] 45 | todo.items.is_a?(MotionPrime::Bag).should == true 46 | todo.save 47 | 48 | todo = Todo.find(:title => "Today Tasks").first 49 | todo.should.not.be.nil 50 | todo.items.is_a?(MotionPrime::Bag).should == true 51 | todo.items.key.should == todo.items.key 52 | todo.items.size.should == 3 53 | todo.items.to_a.each do |item| 54 | item.is_a?(TodoItem).should.be.true 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/unit/models/bag_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Prime::Model Bag" do 2 | Bag = MotionPrime::Bag 3 | 4 | before do 5 | MotionPrime::Store.connect 6 | end 7 | 8 | after do 9 | MotionPrime::Store.shared_store.clear 10 | end 11 | 12 | describe "#<<" do 13 | it "should add objects to bag" do 14 | bag = Bag.bag 15 | 16 | # use << method to add object to bag 17 | bag << Page.new(:text => "Hello", :index => 1) 18 | 19 | bag.unsaved.count.should.be == 1 20 | bag.changed?.should.be.true 21 | bag.save 22 | 23 | bag.unsaved.count.should.be == 0 24 | bag.saved.count.should.be == 1 25 | bag.changed?.should.be.false 26 | end 27 | 28 | it "should raise error if item already exists" do 29 | bag = Bag.bag 30 | page = Page.new(:text => "Hello", :id => 1) 31 | bag << page 32 | lambda { 33 | bag << page 34 | }.should.raise(Prime::StoreError) 35 | lambda { 36 | bag << [page, page] 37 | }.should.raise(Prime::StoreError) 38 | end 39 | 40 | it "should not add duplicated items without an error" do 41 | bag = Bag.bag 42 | page = Page.new(:text => "Hello", :id => 1) 43 | bag << page 44 | bag.add([page, page], silent_validation: true) 45 | bag.count.should == 1 46 | end 47 | end 48 | 49 | describe "#+" do 50 | it "should add objects to bag" do 51 | bag = Bag.bag 52 | 53 | # use + method to add object to bag 54 | bag += Page.new(:text => "World", :index => 2) 55 | 56 | bag.unsaved.count.should.be == 1 57 | bag.changed?.should.be.true 58 | bag.save 59 | 60 | bag.unsaved.count.should.be == 0 61 | bag.saved.count.should.be == 1 62 | bag.changed?.should.be.false 63 | end 64 | end 65 | 66 | describe "#delete" do 67 | it "should delete object from bag" do 68 | bag = Bag.bag 69 | 70 | page = Page.new(:text => "Hello", :index => 1) 71 | bag << page 72 | bag << Page.new(:text => "World", :index => 2) 73 | bag.save 74 | bag.saved.count.should.be == 2 75 | 76 | bag.delete(page) 77 | bag.changed?.should.be.true 78 | bag.removed.count.should.be == 1 79 | bag.save 80 | bag.saved.count.should.be == 1 81 | end 82 | end 83 | 84 | describe "#store=" do 85 | it "should store bag" do 86 | store = MotionPrime::Store.create 87 | bag = Bag.bag 88 | bag.store = store 89 | bag << Page.new(:text => "1") 90 | bag.save 91 | store.bags.size.should == 1 92 | store.bags.first.to_a.first.text.should == "1" 93 | end 94 | end 95 | 96 | describe "#to_a" do 97 | it "convert a bag to array" do 98 | bag = Bag.bag 99 | bag << Page.new(:text => "1", :index => 1) 100 | bag << Page.new(:text => "2", :index => 2) 101 | 102 | bag.to_a.is_a?(Array).should.be.true 103 | bag.to_a.size.should == 2 104 | 105 | # #to_a is not ordered! 106 | ["1", "2"].include?(bag.to_a[0].text).should.be.true 107 | ["1", "2"].include?(bag.to_a[1].text).should.be.true 108 | bag.save 109 | bag.to_a.size.should == 2 110 | end 111 | end 112 | end -------------------------------------------------------------------------------- /spec/unit/models/dirty_spec.rb: -------------------------------------------------------------------------------- 1 | describe MotionPrime::Model do 2 | before do 3 | MotionPrime::Store.connect 4 | @store = MotionPrime::Store.shared_store 5 | end 6 | 7 | after do 8 | @store.clear 9 | end 10 | 11 | describe "has_changed?" do 12 | before do 13 | @user = stub_user("Bob", 10, Time.now) 14 | @user.save 15 | end 16 | 17 | it "should be false after save" do 18 | @user.has_changed?.should.be.false 19 | end 20 | 21 | it "should be true after attribute change" do 22 | @user.name = "Smith" 23 | @user.has_changed?.should.be.true 24 | end 25 | 26 | it "should be false after reload" do 27 | @user.name = "Smith" 28 | @user.reload 29 | @user.has_changed?.should.be.false 30 | @user.name.should == "Bob" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/models/errors_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Prime::Model Errors" do 2 | before do 3 | MotionPrime::Store.connect 4 | @user = stub_user("Bob", 10, Time.now) 5 | end 6 | 7 | describe "#errors" do 8 | it "should be blank on initialize" do 9 | @user.errors.blank?.should == true 10 | end 11 | 12 | it "should not be blank after adding error" do 13 | @user.errors.add(:name, 'bar') 14 | @user.errors.blank?.should == false 15 | end 16 | 17 | it "should not present after reset" do 18 | @user.errors.add(:name, 'bar') 19 | @user.errors.present?.should == true 20 | @user.errors.reset 21 | @user.errors.present?.should == false 22 | end 23 | 24 | it "should be convertable to string" do 25 | @user.errors.add(:name, 'bar') 26 | @user.errors.to_s.match(/bar/).should != nil 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /spec/unit/models/json.rb: -------------------------------------------------------------------------------- 1 | describe MotionPrime::JSON do 2 | 3 | before do 4 | @json_string = <<-EOS 5 | { 6 | "id": 1, 7 | "full_name": "Bruce Lee", 8 | "age": 23, 9 | "gender": "male" 10 | } 11 | EOS 12 | end 13 | 14 | describe "parse" do 15 | 16 | before do 17 | @json_data = Prime::JSON.parse(@json_string) 18 | end 19 | 20 | it "doesn't crash when data is nil" do 21 | Proc.new { Prime::JSON.parse(nil) }.should.not.raise Exception 22 | end 23 | 24 | it "returns a mutable object" do 25 | Proc.new { @json_data[:blah] = 123 }.should.not.raise Exception 26 | end 27 | 28 | it "should convert a top object into a Ruby hash" do 29 | obj = @json_data 30 | obj.class.should == Hash 31 | obj.keys.size.should == 4 32 | end 33 | 34 | it "should properly convert integers values" do 35 | @json_data["id"].is_a?(Integer).should == true 36 | end 37 | 38 | it "should properly convert string values" do 39 | @json_data["full_name"].is_a?(String).should == true 40 | end 41 | 42 | it "should convert an array into a Ruby array" do 43 | obj = Prime::JSON.parse("[1,2,3]") 44 | obj.class.should == Array 45 | obj.size.should == 3 46 | end 47 | end 48 | 49 | describe "generate" do 50 | before do 51 | @json_data = { 52 | foo: 'bar', 53 | 'bar' => 'baz', 54 | baz: 123, 55 | foobar: [1,2,3], 56 | foobaz: {'a' => 1, 'b' => 2} 57 | } 58 | end 59 | 60 | it "should generate from a hash" do 61 | json = Prime::JSON.generate(@json_data) 62 | json.class == String 63 | json.should == "{\"foo\":\"bar\",\"bar\":\"baz\",\"baz\":123,\"foobar\":[1,2,3],\"foobaz\":{\"a\":1,\"b\":2}}" 64 | end 65 | 66 | it "should encode and decode and object losslessly" do 67 | json = Prime::JSON.generate(@json_data) 68 | obj = Prime::JSON.parse(json) 69 | 70 | obj["foo"].should == 'bar' 71 | obj["bar"].should == 'baz' 72 | obj["baz"].should == 123 73 | obj["foobar"].should == [1,2,3] 74 | obj["foobaz"].should == {"a" => 1, "b" => 2} 75 | 76 | obj.keys.map(&:to_s).sort.should == @json_data.keys.map(&:to_s).sort 77 | end 78 | 79 | it "should parametrize date/time objects" do 80 | time = Time.new(2010, 10, 10) 81 | Prime::JSON.generate(time: time).match('2010-10-10 00:00:00').should.not.be.nil 82 | Prime::JSON.generate(time: time.to_date).match('2010-10-10').should.not.be.nil 83 | end 84 | end 85 | 86 | end 87 | 88 | -------------------------------------------------------------------------------- /spec/unit/models/store_extension_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Prime::Model Store Extension" do 2 | before do 3 | MotionPrime::Store.connect 4 | @store = MotionPrime::Store.shared_store 5 | @store.clear 6 | end 7 | 8 | after do 9 | @store.clear 10 | end 11 | 12 | it "should open and close store" do 13 | @store.open 14 | @store.closed?.should.be.false 15 | 16 | @store.close 17 | @store.closed?.should.be.true 18 | 19 | @store.open 20 | @store.closed?.should.be.false 21 | end 22 | 23 | it "should add, delete objects and count them" do 24 | obj1 = Organization.new 25 | obj1.name = "Cat" 26 | obj2 = Organization.new 27 | obj2.name = "Dog" 28 | obj3 = Organization.new 29 | obj3.name = "Cow" 30 | obj4 = Organization.new 31 | obj4.name = "Duck" 32 | 33 | @store << obj1 34 | @store << [obj2, obj3] 35 | @store += obj4 36 | 37 | @store.save 38 | Organization.count.should == 4 39 | 40 | @store.delete(obj1) 41 | Organization.count.should == 3 42 | 43 | @store.delete_keys([obj2.key]) 44 | Organization.count.should == 2 45 | 46 | @store.clear 47 | Organization.count.should == 0 48 | end 49 | 50 | it "should discard unsave changes" do 51 | @store.save_interval = 1000 # must use save_interval= to set auto save interval first 52 | @store.engine.synchronousMode = SynchronousModeFull 53 | 54 | Organization.count.should == 0 55 | obj1 = Organization.new 56 | obj1.name = "Cat" 57 | obj2 = Organization.new 58 | obj2.name = "Dog" 59 | 60 | @store << [obj1, obj2] 61 | @store.changed?.should.be.true 62 | @store.discard 63 | @store.changed?.should.be.false 64 | Organization.count.should == 0 65 | @store.save_interval = 1 66 | end 67 | 68 | it "should create a transaction and commit" do 69 | @store.transaction do |the_store| 70 | Organization.count.should == 0 71 | obj1 = Organization.new 72 | obj1.name = "Cat" 73 | obj1.save 74 | 75 | obj2 = Organization.new 76 | obj2.name = "Dog" 77 | obj2.save 78 | Organization.count.should == 2 79 | end 80 | @store.save 81 | Organization.count.should == 2 82 | end 83 | 84 | it "should create a transaction and rollback when fail" do 85 | begin 86 | @store.transaction do |the_store| 87 | Organization.count.should == 0 88 | obj1 = Organization.new 89 | obj1.name = "Cat" 90 | obj1.save 91 | 92 | obj2 = Organization.new 93 | obj2.name = "Dog" 94 | obj2.save 95 | Organization.count.should == 2 96 | raise "error" 97 | end 98 | rescue 99 | end 100 | @store.save 101 | Organization.count.should == 0 102 | end 103 | 104 | it "should save in batch" do 105 | @store.save_interval = 1000 106 | 107 | Organization.count.should == 0 108 | obj1 = Organization.new 109 | obj1.name = "Cat" 110 | @store << obj1 111 | 112 | obj2 = Organization.new 113 | obj2.name = "Dog" 114 | @store << obj2 115 | @store.save 116 | 117 | Organization.count.should == 2 118 | end 119 | 120 | end -------------------------------------------------------------------------------- /spec/unit/models/store_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Prime::Model Store" do 2 | before do 3 | MotionPrime::Store.disconnect 4 | end 5 | 6 | after do 7 | File.delete(documents_path + "/nano.db") rescue nil 8 | end 9 | 10 | it "create :memory store" do 11 | MotionPrime::Store.disconnect 12 | store = MotionPrime::Store.create :memory 13 | store.filePath.should == ":memory:" 14 | end 15 | 16 | it "create :persistent store" do 17 | path = documents_path + "/nano.db" 18 | store = MotionPrime::Store.create :persistent, path 19 | store.filePath.should == path 20 | 21 | path = documents_path + "/nano.db" 22 | store = MotionPrime::Store.create :file, path 23 | store.filePath.should == path 24 | end 25 | 26 | it "create :temp store" do 27 | store = MotionPrime::Store.create :temp 28 | store.filePath.should == "" 29 | 30 | store = MotionPrime::Store.create :temporary 31 | store.filePath.should == "" 32 | end 33 | 34 | it "should use shared_store if a model has no store defined" do 35 | Organization.store = nil 36 | MotionPrime::Store.connect 37 | Organization.store.should.not.be.nil 38 | MotionPrime::Store.shared_store.should.not.be.nil 39 | Organization.store.should == MotionPrime::Store.shared_store 40 | 41 | Organization.store = MotionPrime::Store.create :temp 42 | Organization.store.should.not == MotionPrime::Store.shared_store 43 | end 44 | 45 | it "should enable and disable debug mode" do 46 | MotionPrime::Store.debug = true 47 | @store = MotionPrime::Store.create 48 | MotionPrime::Store.debug = false 49 | @store.should.not.be.nil 50 | end 51 | end -------------------------------------------------------------------------------- /spec/unit/prime/env.rb: -------------------------------------------------------------------------------- 1 | describe "Prime.env" do 2 | 3 | before do 4 | ENV['PRIME_ENV'] = 'staging' 5 | end 6 | 7 | it 'to_s should return string' do 8 | Prime.env.to_s.is_a?(String).should.be.true 9 | end 10 | 11 | it 'should be comparable with string' do 12 | (Prime.env == 'staging').should.be.true 13 | (Prime.env == 'test').should.be.false 14 | end 15 | 16 | it 'should respond to question mark' do 17 | Prime.env.staging?.should.be.true 18 | Prime.env.test?.should.be.false 19 | end 20 | end -------------------------------------------------------------------------------- /spec/unit/prime/logger.rb: -------------------------------------------------------------------------------- 1 | describe "Prime.env" do 2 | 3 | before do 4 | @logger = Prime::Logger.new 5 | @logger.stub!(:output) do |message| 6 | message 7 | end 8 | end 9 | 10 | it 'should work' do 11 | @logger.log("Hello world").should == 'Hello world' 12 | end 13 | 14 | describe "error level" do 15 | before { Prime::Logger.level = :error } 16 | 17 | it 'should log errors' do 18 | @logger.error("message").should == ["message"] 19 | end 20 | 21 | it 'should not log info' do 22 | @logger.info("message").should == nil 23 | end 24 | end 25 | 26 | describe "info level" do 27 | before { Prime::Logger.level = :info } 28 | 29 | it 'should log info' do 30 | @logger.info("message").should == ["message"] 31 | end 32 | 33 | it 'should not log debug' do 34 | @logger.debug("message").should == nil 35 | end 36 | end 37 | 38 | describe "debug level" do 39 | before { Prime::Logger.level = :debug } 40 | 41 | it 'should log debug' do 42 | @logger.debug("message").should == ["message"] 43 | end 44 | 45 | it 'should not log dealloc' do 46 | @logger.dealloc_message("message", Prime::Section.new).should == nil 47 | end 48 | end 49 | end -------------------------------------------------------------------------------- /spec/unit/screens/screen_spec.rb: -------------------------------------------------------------------------------- 1 | describe MotionPrime::Screen do 2 | 3 | before do 4 | @screen = BaseScreen.new() 5 | @screen.will_appear 6 | end 7 | 8 | it "should render screen on appear" do 9 | @screen.was_rendered.should == true 10 | end 11 | 12 | it "should set default title" do 13 | @screen.title.should == "Base" 14 | end 15 | 16 | it "should have navigation enabled by default" do 17 | @screen.wrap_in_navigation?.should == true 18 | end 19 | 20 | it "#modal? should be false by default" do 21 | @screen.modal?.should == false 22 | end 23 | 24 | describe "UIViewController" do 25 | 26 | it "viewDidLoad" do 27 | @screen.mock!(:view_did_load) { true } 28 | @screen.viewDidLoad.should == true 29 | end 30 | 31 | it "viewWillAppear" do 32 | @screen.mock!(:view_will_appear) { |animated| animated.should == true } 33 | @screen.viewWillAppear(true) 34 | end 35 | 36 | it "viewDidAppear" do 37 | @screen.mock!(:view_did_appear) { |animated| animated.should == true } 38 | @screen.viewDidAppear(true) 39 | end 40 | 41 | it "viewWillDisappear" do 42 | @screen.mock!(:view_will_disappear) { |animated| animated.should == true } 43 | @screen.viewWillDisappear(true) 44 | end 45 | 46 | it "viewDidDisappear" do 47 | @screen.mock!(:view_did_disappear) { |animated| animated.should == true } 48 | @screen.viewDidDisappear(true) 49 | end 50 | 51 | it "shouldAutorotateToInterfaceOrientation" do 52 | @screen.mock!(:should_rotate) { |o| o.should == UIInterfaceOrientationPortrait } 53 | @screen.shouldAutorotateToInterfaceOrientation(UIInterfaceOrientationPortrait) 54 | end 55 | 56 | it "shouldAutorotate" do 57 | @screen.mock!(:should_autorotate) { true } 58 | @screen.shouldAutorotate.should == true 59 | end 60 | 61 | it "willRotateToInterfaceOrientation" do 62 | @screen.mock! :will_rotate do |orientation, duration| 63 | orientation.should == UIInterfaceOrientationPortrait 64 | duration.should == 0.5 65 | end 66 | @screen.willRotateToInterfaceOrientation(UIInterfaceOrientationPortrait, duration: 0.5) 67 | end 68 | 69 | it "didRotateFromInterfaceOrientation" do 70 | @screen.mock!(:on_rotate) { true } 71 | @screen.didRotateFromInterfaceOrientation(UIInterfaceOrientationPortrait).should == true 72 | end 73 | 74 | end 75 | 76 | describe "navigation" do 77 | it "should not have navigation by default" do 78 | @screen.has_navigation?.should == false 79 | end 80 | end 81 | end -------------------------------------------------------------------------------- /spec/unit/sections/section_spec.rb: -------------------------------------------------------------------------------- 1 | describe MotionPrime::Section do 2 | 3 | describe "general" do 4 | before do 5 | @section = SampleSection.new(screen: @screen) 6 | end 7 | 8 | describe "#name" do 9 | it "should use class name by default" do 10 | @section.name.should == 'sample' 11 | end 12 | 13 | it "should use given name" do 14 | section = SampleSection.new(name: 'my_section') 15 | section.name.should == 'my_section' 16 | end 17 | end 18 | end 19 | 20 | describe "base section" do 21 | before do 22 | @screen = BaseScreen.new 23 | @section = SampleViewSection.new(screen: @screen) 24 | @section.render 25 | end 26 | 27 | describe "#element" do 28 | it "should return element by name" do 29 | @section.element(:description).is_a?(MotionPrime::BaseElement).should.be.true 30 | end 31 | end 32 | 33 | describe "#view" do 34 | it "should return view by element name" do 35 | @section.view(:description).is_a?(MPLabel).should.be.true 36 | end 37 | end 38 | end 39 | 40 | describe "draw section" do 41 | before do 42 | @screen = BaseScreen.new 43 | @section = SampleDrawSection.new(screen: @screen) 44 | @section.render 45 | end 46 | 47 | describe "#element" do 48 | it "should return element by name" do 49 | @section.element(:description).is_a?(MotionPrime::DrawElement).should.be.true 50 | end 51 | end 52 | 53 | describe "#view" do 54 | it "should return container view by element name" do 55 | @section.view(:description).is_a?(MPViewWithSection).should.be.true 56 | end 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /spec/unit/support/filter_mixin_spec.rb: -------------------------------------------------------------------------------- 1 | describe MotionPrime::FilterMixin do 2 | before do 3 | @subject = MotionPrime::FilterMixin.new 4 | end 5 | 6 | def data 7 | model_stub = Struct.new(:info, :id) # :id is used for sorting 8 | data_array.map do |item| 9 | model_stub.new(item, item[:id]) 10 | end 11 | end 12 | 13 | def data_array 14 | [{id: 4, name: 'iPhone'}, {id: 5, name: 'MacBook'}] 15 | end 16 | 17 | it "should filter array by inclusion" do 18 | @subject.filter_array(data, {id: %w[4 5]}).count.should.equal 2 19 | end 20 | 21 | it "should find a single record (case sensitive)" do 22 | @subject.filter_array(data, {id: 4, name: 'iPhone'}).count.should.equal 1 23 | end 24 | 25 | it "order filtered records" do 26 | result = @subject.filter_array(data, {id: %w[4 5]}, sort: {id: :desc}) 27 | result.first[:id].should.equal 5 28 | end 29 | end -------------------------------------------------------------------------------- /spec/unit/support/frame_calculator_mixin_spec.rb: -------------------------------------------------------------------------------- 1 | describe MotionPrime::FrameCalculatorMixin do 2 | before do 3 | @parent_bounds = CGRectMake(0,0,0,0) 4 | @parent_bounds.size.width = 300 5 | @parent_bounds.size.height = 200 6 | @subject = MotionPrime::FrameCalculatorMixin.new 7 | end 8 | 9 | it "should set width and height" do 10 | result = @subject.calculate_frame_for(@parent_bounds, width: 200, height: 100) 11 | result.size.width.should == 200 12 | result.size.height.should == 100 13 | end 14 | 15 | it "should use parent size if size not set" do 16 | result = @subject.calculate_frame_for(@parent_bounds, {}) 17 | result.size.width.should == 300 18 | result.size.height.should == 200 19 | end 20 | 21 | it "should calculate size based on left/right" do 22 | result = @subject.calculate_frame_for(@parent_bounds, {left: 10, right: 10, top: 10, bottom: 10}) 23 | result.size.width.should == 280 24 | result.size.height.should == 180 25 | result.origin.x.should == 10 26 | result.origin.y.should == 10 27 | end 28 | 29 | it "should calculate left based on width and right" do 30 | result = @subject.calculate_frame_for(@parent_bounds, {right: 10, width: 200}) 31 | result.origin.x.should == 90 #300 - 200 - 10 32 | end 33 | 34 | it "should calculate top based on height and bottom" do 35 | result = @subject.calculate_frame_for(@parent_bounds, {bottom: 10, height: 100}) 36 | result.origin.y.should == 90 #300 - 200 - 10 37 | end 38 | 39 | it "should use width as more priority than right" do 40 | result = @subject.calculate_frame_for(@parent_bounds, {width: 100, left: 10, right: 10}) 41 | result.size.width.should == 100 42 | end 43 | 44 | it "should calculate relative width" do 45 | result = @subject.calculate_frame_for(@parent_bounds, {width: 0.5}) 46 | result.size.width.should == 150 47 | result.origin.x.should == 0 48 | end 49 | 50 | it "should calculate relative height" do 51 | result = @subject.calculate_frame_for(@parent_bounds, {height: 0.5}) 52 | result.size.height.should == 100 53 | end 54 | 55 | it "should calculate left based on relative width and absolute right" do 56 | result = @subject.calculate_frame_for(@parent_bounds, {width: 0.5, right: 70}) 57 | result.origin.x.should == 80 58 | end 59 | 60 | it "should calculate top based on relative height and absolute bottom" do 61 | result = @subject.calculate_frame_for(@parent_bounds, {height: 0.5, bottom: 70}) 62 | result.origin.y.should == 30 63 | end 64 | 65 | it "should calculate relative left and width" do 66 | result = @subject.calculate_frame_for(@parent_bounds, {left: 0.2, width: 0.4}) 67 | result.origin.x.should == 60 68 | result.size.width.should == 120 69 | end 70 | 71 | it "should calculate relative top and height" do 72 | result = @subject.calculate_frame_for(@parent_bounds, {top: 0.2, height: 0.4}) 73 | result.origin.y.should == 40 74 | result.size.height.should == 80 75 | end 76 | end -------------------------------------------------------------------------------- /travis.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | sudo chown -R travis ~/Library/RubyMotion 3 | mkdir -p ~/Library/RubyMotion/build 4 | sudo motion update 5 | bundle install 6 | pod repo update --silent 7 | bundle exec rake pod:install 8 | bundle exec rake clean 9 | bundle exec rake spec output=colorized --------------------------------------------------------------------------------