ProMotion is a RubyMotion gem that makes iOS development more like Ruby and less like Objective-C.
Table of Contents generated with DocToc
http://www.clearsightstudio.com/insights/ruby-motion-promotion-tutorial
http://www.clearsightstudio.com/insights/tutorial-make-youtube-video-app-rubymotion-promotion/
This is pretty bare-bones, but we'll be building it out as we go along.
https://github.com/jamonholmgren/promotion-demo
Check out the free BigDay! Reminder app on the App Store to see what's possible. ClearSight Studio built the app for Kijome Software, a small app investment company.
TipCounter was built by Matt Brewer for bartenders and servers to easily track their tips. Used ProMotion and the development was a lot of fun!
Have an interest in crime statistics and locations? Live in Winston-Salem, NC? This hyper-local and open source RubyMotion app uses a mixture custom UIViewControllers and ProMotion for ease of attribute setting and adding views. Check it out on the App Store or fork it and contribute!
ProMotion is designed to be as intuitive and Ruby-like as possible. For example, here is a typical app folder structure:
app/
screens/
events/
list_events_screen.rb
show_event_screen.rb
edit_event_screen.rb
home_screen.rb
settings_screen.rb
models/
event.rb
views/
buttons/
save_event_button.rb
app_delegate.rb
Create a new RubyMotion project.
motion create myapp
Open it in your favorite editor, then go into your Rakefile and modify the top to look like the following:
# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project/template/ios'
require 'bundler'
Bundler.require
Create a Gemfile and add the following lines:
source 'https://rubygems.org'
gem "ProMotion", "~> 0.6.2"
Run bundle install
in Terminal to install ProMotion.
Go into your app/app_delegate.rb file and replace everything with the following:
class AppDelegate < ProMotion::Delegate
def on_load(app, options)
open HomeScreen.new(nav_bar: true)
end
end
Make sure you remove the didFinishLoadingWithOptions
method or call super
in it. Otherwise
ProMotion won't get set up and on_load
won't be called.
Create a folder in /app
named screens
. Create a file in that folder named home_screen.rb
.
Now drop in this code:
class HomeScreen < ProMotion::Screen
title "Home"
def will_appear
set_attributes self.view, {
backgroundColor: UIColor.whiteColor
}
end
end
Run rake
. You should now see the simulator open with your home screen and a navigation bar like the image below. Congrats!
- Will auto-detect if you've loaded motion-xray and enable it.
- Added
open_split_screen
for iPad-supported apps (thanks @rheoli for your contributions to this) - Added
refreshable
to TableScreens (thanks to @markrickert) for pull-to-refresh support. ProMotion::AppDelegateParent
renamed toProMotion::Delegate
(AppDelegateParent
is an alias)set_attributes
andadd
now apply nested attributes recursivelyset_attributes
andadd
now accept snake_case instead of camelCase methods (e.g., background_color)- Added
add_to
method for adding views to any parent view.remove
works with this normally. - Deprecated Console.log and replaced with PM::Logger
- Many improvements to how screens and navigation controllers are loaded, tests
class HomeScreen < ProMotion::Screen
title "Home"
def on_load
# Load data
end
def will_appear
# Set up the elements in your view with add
@label ||= add UILabel.alloc.initWithFrame(CGRectMake(5, 5, 20, 20))
end
def on_appear
# Everything's loaded and visible
end
end
# In app/app_delegate.rb
class AppDelegate < ProMotion::Delegate
def on_load(app, options)
open MyHomeScreen.new(nav_bar: true)
end
end
# In app/app_delegate.rb
class AppDelegate < ProMotion::Delegate
def on_load(app, options)
open_split_screen MenuScreen, DetailScreen
end
end
Creating a tabbed bar with multiple screens. This will set the tab bar as the root view controller for your app, so keep that in mind. It can be done from the AppDelegate#on_load or from a screen (that screen will go away, though).
def on_load(app, options)
@home = MyHomeScreen.new(nav_bar: true)
@settings = SettingsScreen.new
@contact = ContactScreen.new(nav_bar: true)
open_tab_bar @home, @settings, @contact
end
For each screen that belongs to the tab bar, you need to set the tab name and icon in the files. In this example, we would need add the following to the three files (my_home_screen.rb, settings_screen.rb, contact_screen.rb):
def on_load
set_tab_bar_item title: "Tab Name Goes Here", icon: "icons/tab_icon.png" # in resources/icons folder
# or...
set_tab_bar_item system_icon: UITabBarSystemItemContacts
end
To programmatically switch to a different tab, use open_tab
.
def some_action
open_tab "Contacts"
end
These two methods add the buttons to the top navigation bar of a screen. The action:
lets you specify a method to
call when that button is tapped, and you can pass in a UIBarButton style using type:
.
set_nav_bar_right_button "Save", action: :save_something, type: UIBarButtonItemStyleDone
set_nav_bar_left_button "Cancel", action: :return_to_some_other_screen, type: UIBarButtonItemStylePlain
If you pass an instance of a UIImage
, the UIBarButton
will automatically display with that image instead of text. Don't forget retina and landscape versions of your image!
If you pass :system
for the title, then you can get a system item. E.g.:
set_nav_bar_right_button nil, action: :add_something, system_icon: UIBarButtonSystemItemAdd
Additionally, if you pass an instance of a UIBarButtonItem
, the UIBarButton
will automatically display that particular button item.
set_nav_bar_left_button self.editButtonItem
If the user taps something and you want to open a new screen, it's easy. Just use open
and pass in the screen class
or an instance of that screen.
def settings_button_tapped
# ...with a class...
open SettingsScreen
# ...or with an instance...
@settings_screen = SettingsScreen.new
open @settings_screen
end
You can also open a screen as a modal.
open SettingsScreen.new, modal: true
You can pass in arguments to other screens if they have accessors:
class HomeScreen < ProMotion::Screen
# ...
def settings_button_tapped
open ProfileScreen.new(user: some_user)
end
end
class ProfileScreen < ProMotion::Screen
attr_accessor :user
def on_load
self.user # => some_user instance
end
end
Closing a screen is as easy as can be.
# User taps a button, indicating they want to close this screen.
def close_screen_tapped
close
end
You can close a screen (modal or in a nav controller) and pass back arguments to the previous screen's "on_return" method:
class ItemScreen < ProMotion::Screen
# ...
def save_and_close
if @model.save
close(model_saved: true)
end
end
end
class MainScreen < ProMotion::Screen
# ...
def on_return(args = {})
if args[:model_saved]
self.reload_something
end
end
end
It's common to want to open a screen in the same navigation controller if on iPhone but in a separate detail view when on iPad. Here's a good way to do that.
class MenuScreen < ProMotion::TableScreen
# ...
def some_action
open SomeScreen.new, in_detail: true
end
end
The in_detail
option tells ProMotion to look for a split screen and open in the detail screen
if it's available. If not, open normally. This also works for in_master:
.
Any view item (UIView, UIButton, custom UIView subclasses, etc) can be added to the current view with add
.
add
accepts a second argument which is a hash of attributes that get applied to the element before it is
dropped into the view.
add(view, attr={})
@label = add UILabel.new, {
text: "This is awesome!",
font: UIFont.systemFontOfSize(18),
resize: [ :left, :right, :top, :bottom, :width, :height ], # autoresizingMask
left: 5, # These four attributes are used with CGRectMake
top: 5,
width: 20,
height: 20
}
@element = add UIView.alloc.initWithFrame(CGRectMake(0, 0, 20, 20)), {
backgroundColor: UIColor.whiteColor
}
The set_attributes
method is identical to add except that it does not add it to the current view.
If you use snake_case and there isn't an existing method, it'll try camelCase. This allows you to
use snake_case for Objective-C methods.
set_attributes(view, attr={})
@element = set_attributes UIView.alloc.initWithFrame(CGRectMake(0, 0, 20, 20)), {
# `background_color` is translated to `backgroundColor` automatically.
background_color: UIColor.whiteColor
}
You can use add_to
to add a view to any other view, not just the main view.
add_to(parent_view, new_view, attr={})
add_to @some_parent_view, UIView.alloc.initWithFrame(CGRectMake(0, 0, 20, 20)), {
backgroundColor: UIColor.whiteColor
}
You can create sectioned table screens easily with TableScreen, SectionedTableScreen, and GroupedTableScreen.
class SettingsScreen < ProMotion::GroupedTableScreen
title "Settings"
def on_load
add_right_nav_button(label: "Save", action: :save)
set_tab_bar_item(title: "Settings", icon: "settings.png")
end
# table_data is automatically called. Use this format in the return value.
# It's an array of cell groups, each cell group consisting of a title and an array of cells.
def table_data
[{
title: "Your Account",
cells: [
{ title: "Edit Profile", action: :edit_profile, arguments: { id: 3 } },
{ title: "Log Out", action: :log_out },
{ title: "Notification Settings", action: :notification_settings }
]
}, {
title: "App Stuff",
cells: [
{ title: "About", action: :show_about },
{ title: "Feedback", action: :show_feedback }
]
}]
end
# This method allows you to create a "jumplist", the index on the right side of the table
def table_data_index
# Ruby magic to make an alphabetical array of letters.
# Try this in Objective-C and tell me you want to go back.
return ("A".."Z").to_a
end
# Your table cells, when tapped, will execute the corresponding actions
# and pass in the specified arguments.
def edit_profile(args={})
puts args[:id] # => 3
end
end
You can provide remotely downloaded images for cells by including the CocoaPod "SDWebImage" in your Rakefile and doing this:
cells: [
{
title: "Cell with image",
remote_image: { url: "http://placekitten.com/200/300", placeholder: "some-local-image" }
}
]
Sometimes you want to inherit from a different UIViewController other than that provided by ProMotion,
such as when using Formotion. RubyMotion doesn't currently
allow us to override built-in methods when including them as a module. And we really need to override
viewDidLoad
and others.
Fortunately, there's a workaround for that.
class EventsScreen < Formotion::FormController # Can also be < UIViewController
include ProMotion::ScreenModule # Not TableScreenModule since we're using Formotion for that
# Required functions for ProMotion to work properly
def viewDidLoad
super
self.view_did_load if self.respond_to?(:view_did_load)
end
def viewWillAppear(animated)
super
self.view_will_appear(animated) if self.respond_to?("view_will_appear:")
end
def viewDidAppear(animated)
super
self.view_did_appear(animated) if self.respond_to?("view_did_appear:")
end
def viewWillDisappear(animated)
self.view_will_disappear(animated) if self.respond_to?("view_will_disappear:")
super
end
def viewDidDisappear(animated)
self.view_did_disappear(animated) if self.respond_to?("view_did_disappear:")
super
end
def shouldAutorotateToInterfaceOrientation(orientation)
self.should_rotate(orientation)
end
def shouldAutorotate
self.should_autorotate
end
def willRotateToInterfaceOrientation(orientation, duration:duration)
self.will_rotate(orientation, duration)
end
def didRotateFromInterfaceOrientation(orientation)
self.on_rotate
end
end
Has all the methods of Screen
Method | Description |
---|---|
searchable(placeholder: "placeholder text") | Class method to make the current table searchable. |
|
Class method to make the current table refreshable.
All parameters are optional. If you do not specify a a callback, it will assume you've implemented an
And after you're done with your asyncronous process, call |
Method that is called to get the table's cell data and build the table. Example format using nearly all available options. Note... if you're getting crazy deep into styling your table cells, you really should be subclassing them and specifying that new class in :cell_class
and then providing :cell_class_attributes to customize it.Performance note... It's best to build this array in a different method and store it in something like @table_data . Then your table_data
method just returns that.
| |
update_table_data |
Causes the table data to be refreshed, such as when a remote data source has
been downloaded and processed. |
Accessible from ProMotion.logger or PM.logger ... you can also set a new logger by setting PM.logger = MyLogger.new
Method | Description |
---|---|
log(label, message_text, color) |
Output a colored console message. Example: PM.logger.log("TESTING", "This is red!", :red)
|
error(message) |
Output a red colored console error. Example: PM.logger.error("This is an error")
|
deprecated(message) |
Output a yellow colored console deprecated. Example: PM.logger.deprecated("This is a deprecation warning.")
|
warn(message) |
Output a yellow colored console warning. Example: PM.logger.warn("This is a warning")
|
debug(message) |
Output a purple colored console debug message. Example: PM.logger.debug(@some_var)
|
info(message) |
Output a green colored console info message. Example: PM.logger.info("This is an info message")
|
Method | Description |
---|---|
log(log, with_color:color) [DEPRECATED] -- use Logger |
Class method to output a colored console message. Example: ProMotion::Console.log("This is red!", with_color: ProMotion::Console::RED_COLOR)
|
If you need help, feel free to ping me on twitter @jamonholmgren or open a ticket on GitHub. Opening a ticket is usually the best and we respond to those pretty quickly.
I'm very open to ideas. Tweet me with your ideas or open a ticket (I don't mind!) and let's discuss.
- Clone the repos into
Your-Project/Vendor/ProMotion
- Update your
Gemfile
to reference the project asgem 'ProMotion', :path => "vendor/ProMotion/"
- Run
bundle
- Run
rake clean
and thenrake
- Contribute!
- Fork the project
- Create a feature branch
- Code
- Update or create new specs
- Make sure tests are passing by running
rake spec
- Submit pull request
- Fame, adoration, kudos everywhere
- Jamon Holmgren: @jamonholmgren
- Silas Matson: @silasjmatson
- Matt Brewer: @macfanatic