37
Rapid App Development with RubyMotion Stefán Hafliðason http://stefan.haflidason.com @styrmis Fwfo!npsf External libraries that make development even easier

(Even more) Rapid App Development with RubyMotion

Embed Size (px)

DESCRIPTION

When creating iOS apps with RubyMotion, a good working knowledge of Objective-C and the iOS/OSX APIs is still required in order to build production-ready apps. In this presentation we take a look at some external libraries however that aim to bridge the gap, making app development more accessible to Ruby developers and much faster/easier for everyone, irrespective of past development experience.

Citation preview

Page 1: (Even more) Rapid App Development with RubyMotion

Rapid App Development with RubyMotion

Stefán Hafliðason ! http://stefan.haflidason.com

" @styrmis

External libraries that make development even easier

Page 2: (Even more) Rapid App Development with RubyMotion

• Promises increased developer productivity

• Brings the flexibility of Ruby to iOS and OSX development

• Bridges directly to Obj-C libraries: no intermediate glue code

• A REPL for working with your app live!

• Make tweaks quickly

• Build whole views programmatically on the fly

Why RubyMotion?

Page 3: (Even more) Rapid App Development with RubyMotion

RubyMotion + 3rd Party Libs

• Stock RubyMotion makes life (generally) easier

• Like Rails, there’s a healthy (and growing) ecosystem of libraries

• These libraries can help speed up development even further

• If the cost of experimenting is reduced, we’re more likely to try out new ideas, and that’s my goal.

Page 4: (Even more) Rapid App Development with RubyMotion

What are we looking for?• Non-Polluting!

• Zero/Minimal pollution of our current namespace.

• Minimal Magic!

• Because when magic breaks, we’ll need to fix it.

• Allows fallback to ‘plain’ RubyMotion

• There when you need it, unobtrusive when you don’t.

Page 5: (Even more) Rapid App Development with RubyMotion

The Libraries• View management: Ruby Motion Query (RMQ),

MotionKit

• More Ruby-like view creation: ProMotion!

• Core Data: ruby-xcdm + Core Data Query (CDQ)

• Various helpers: BubbleWrap, SugarCube, MotionAwesome

Page 6: (Even more) Rapid App Development with RubyMotion

Ruby Motion Query

• “It’s like jQuery for RubyMotion. Stylesheets, templates, events, and more”

• Supercharges the REPL/console

• Easy to add, access and modify views

• Also: event binding, styling, animation and more.

Page 7: (Even more) Rapid App Development with RubyMotion

Layout ExperimentationQuickly laying out 8 table views on the screen (iPad). Started in the console, then moved code into the app itself.

 1  class  RootViewController  <  UIViewController    2      def  viewDidLoad    3          #  Start  by  laying  out  with  RMQ    4          @week  =  UITableView.alloc.init    5          rmq.append(@week).layout({  t:0,  l:0,  w:217,  h:704})    6      7          @monday  =  UITableView.alloc.init    8          rmq.append(@monday).layout({  t:0,  l:221,  w:200,  h:350})    9    10          @tuesday  =  UITableView.alloc.init  11          rmq.append(@tuesday).layout({  t:0,  l:422,  w:200,  h:350})  12    13          #  ...  14    15          @saturday  =  UITableView.alloc.init  16          rmq.append(@saturday).layout({  t:354,  l:422,  w:200,  h:350})  17    18          @sunday  =  UITableView.alloc.init  19          rmq.append(@sunday).layout({  t:354,  l:623,  w:200,  h:350})  20    21          #  ...  22      end  23  end

Page 8: (Even more) Rapid App Development with RubyMotion

Wiring it upLet’s give those tableviews a data source

1  class  TableViewDataSource  2      #  Implement  a  simple  data  source  delegate  3  end  4    5  rmq(UITableView).each  do  |tv|  6      tv.dataSource  =  TableViewDataSource.new  7  end

Page 9: (Even more) Rapid App Development with RubyMotion

Live Experimentation

Trying out an inverted colour scheme:

1  (main)>  rmq(UITableView).each  do  |tv|  2  (main)>      tv.backgroundColor  =  rmq.color.black  3  (main)>      rmq(tv).find(UITableViewCell).each  do  |cell|  4  (main)>          cell.backgroundColor  =  rmq.color.from_hex("#333")  5  (main)>          cell.textColor  =  rmq.color.from_hex("#EEE")  6  (main)>      end  7  (main)>  end

Page 10: (Even more) Rapid App Development with RubyMotion

Exploring View Hierarchies

 1  (main)>  rmq.log  :tree    2      3  ───  UIView    250313120    {l:  0,  t:  64,  w:  1024,  h:  704}    4          ├───  UITableView    172116992    {l:  0,  t:  0,  w:  217,  h:  704}    5          │        ├───  UITableViewWrapperView    250322496    {l:  0,  t:  0,  w:  217,  h:  704}    6          │        │        ├───  UITableViewCell    250561184    {l:  0,  t:  88,  w:  217,  h:  44}    7          │        │        │        ├───  UITableViewCellScrollV    250561584    {l:  0,  t:  0,  w:  217,  h:  44}    8          │        │        │        │        ├───  UITableViewCellContent    250562720    {l:  0,  t:  0,  w:  217,  h:  43}    9          │        │        │        │        │        ├───  UILabel    250563392    {l:  15,  t:  0,  w:  187,  h:  43}  10          │        │        │        │        ├───  _UITableViewCellSepara    250564720    {l:  15,  t:  43,  w:  202,  h:  1}  11          │        │        ├───  UITableViewCell    250552688    {l:  0,  t:  44,  w:  217,  h:  44}  12          │        │        │        ├───  UITableViewCellScrollV    250553088    {l:  0,  t:  0,  w:  217,  h:  44}  13          │        │        │        │        ├───  UITableViewCellContent    250554640    {l:  0,  t:  0,  w:  217,  h:  43}  14          │        │        │        │        │        ├───  UILabel    250555312    {l:  15,  t:  0,  w:  187,  h:  43}  15          │        │        │        │        ├───  _UITableViewCellSepara    250556592    {l:  15,  t:  43,  w:  202,  h:  1}  16          │        │        ├───  UITableViewCell    250531888    {l:  0,  t:  0,  w:  217,  h:  44}  17          │        │        │        ├───  UITableViewCellScrollV    250533056    {l:  0,  t:  0,  w:  217,  h:  44}  18          │        │        │        │        ├───  UITableViewCellContent    250533840    {l:  0,  t:  0,  w:  217,  h:  43}  19          │        │        │        │        │        ├───  UILabel    250538544    {l:  15,  t:  0,  w:  187,  h:  43}  20          │        │        │        │        ├───  _UITableViewCellSepara    250543888    {l:  15,  t:  43,  w:  202,  h:  1}

Page 11: (Even more) Rapid App Development with RubyMotion

Useful Helpers

1 # App! 2 ! 3 rmq.app.window! 4 rmq.app.delegate! 5 rmq.app.environment! 6 rmq.app.production?! 7 rmq.app.test?! 8 rmq.app.development?! 9 rmq.app.version!10 rmq.app.name!11 rmq.app.identifier!12 rmq.app.resource_path!13 rmq.app.document_path!14 !15

1 # Device! 2 ! 3 rmq.device.screen! 4 rmq.device.width # screen width! 5 rmq.device.height # screen height! 6 rmq.device.ipad?! 7 rmq.device.iphone?! 8 rmq.device.four_inch?! 9 rmq.device.retina?!10 !11 # return values are :unknown, :portrait,!12 # :portrait_upside_down, :landscape_left,!13 # :landscape_right, :face_up, :face_down!14 rmq.device.orientation!15 rmq.device.landscape?!16 rmq.device.portrait?

Why these are not easy to get at in the iOS SDK is beyond me…

Page 12: (Even more) Rapid App Development with RubyMotion

1 class LoginLayout < MotionKit::Layout 2 include LoginStyles 3 4 def layout 5 add UIImageView, :logo do 6 frame [[0, 0], ['100%', :scale]] 7 end 8 9 add UIView, :button_container do10 frame from_bottom(height: 50, width: '100%')11 add UIButton, :login_button do12 background_color superview.backgroundColor13 frame [[ 10, 5 ], [ 50, parent.height - 10 ]]14 end15 end16 17 add UIView, :inputs do18 frame x: 0, y: 0, width: '100%', height: '100% - 50'19 autoresizing_mask :pin_to_top, :flexible_height, :flexible_width20 add UITextField, :username_input do21 frame [[10, 10], ['100% - 10', :auto]]22 end23 add UITextField, :password_input do24 frame below(:username_input, margin: 8)25 end26 end27 end28 end

• Flexible DSL for view layouts

• Simpler handling of device rotation

• Support for constraints / Auto Layout

• Build your own DSL on top

Page 13: (Even more) Rapid App Development with RubyMotion

ProMotion 1 class HelpScreen < PM::TableScreen 2 title "Table Screen" 3 4 def table_data 5 [{ 6 title: "Help", 7 cells: [ 8 { title: "About this app", action: :tapped_about }, 9 { title: "Log out", action: :log_out }10 ]11 }]12 end13 14 def tapped_about(args={})15 open AboutScreen16 end17 18 def log_out19 # Log out!20 end21 end

• Aims to remove as much boilerplate code as possible

• More intuitive, Ruby-style view controller building

• Built-in classes for common view types

Page 14: (Even more) Rapid App Development with RubyMotion

BubbleWrap• The first major extension library, contains a wide

array of helpers:

• Camera, JSON handling, notifications, key-value persistence, location API, message API, SMS, Timers…

• An extremely easy to use HTTP library for working with remote APIs

• And more!

Page 15: (Even more) Rapid App Development with RubyMotion

SugarCube: Sugar coating for verbose APIs1  (main)>  tree  2      0:  .  UIWindow(#d282f80,  [[0.0,  0.0],  [768.0,  1024.0]])  3      1:  `-­‐-­‐  UILayoutContainerView(#9d23f70,  [[0.0,  0.0],  [768.0,  1024.0]])  4      2:          +-­‐-­‐  UINavigationTransitionView(#a28bf30,  [[0.0,  0.0],  [1024.0,  768.0]])  5      3:          |      `-­‐-­‐  UIViewControllerWrapperView(#a2c2b20,  [[0.0,  0.0],  [1024.0,  768.0]])  6      4:          |              `-­‐-­‐  UIView(#a2b23f0,  [[0.0,  64.0],  [1024.0,  704.0]])  7      5:          |                      +-­‐-­‐  UITableView(#aa7f200,  [[0.0,  0.0],  [217.0,  704.0]])

1 (main)> a 4 # alias for 'adjust'!2 => UIView(#a2b23f0, [[0.0, 64.0], [1024.0, 704.0]]), child of UIViewControllerWrapperView(#a2c2b20)!3 (main)> d 100 # alias for 'down'!4 [[0.0, 164.0], [1024.0, 704.0]]!5 => UIView(#a2b23f0, [[0.0, 164.0], [1024.0, 704.0]]), child of UIViewControllerWrapperView(#a2c2b20)!6 (main)> thinner 50!7 [[0.0, 164.0], [974.0, 704.0]]

Quick aliases for adjusting any view quickly, e.g. up, down, left, right, thinner, wider, taller, shorter…

Page 16: (Even more) Rapid App Development with RubyMotion

SugarCube: Sugar coating for verbose APIs

1 (main)> tree root! 2 0: . #<UINavigationController:0x9d243c0>! 3 1: -- #<RootViewController:0x9d24650>! 4 ! 5 => #<UINavigationController:0x9d243c0>! 6 (main)> a 1! 7 => #<RootViewController:0x9d24650>! 8 (main)> $sugarcube_view! 9 => #<RootViewController:0x9d24650>!10 (main)> $sugarcube_view.any_public_method

Works for view controllers too:

Now when testing a particular method you have the option to simply invoke it directly.

Page 17: (Even more) Rapid App Development with RubyMotion

MotionAwesome# $ % & ' ( ) * + , - . / 0

1  label(:check_square_o,  size:  18,  text:  @items[indexPath.row])  do  |label|  2      view  =  UIView.alloc.initWithFrame(cell.contentView.frame)  3      rmq(label).layout({  l:10,  t:10,  w:200,  h:25  })  4      view.addSubview(label)  5      cell.contentView.addSubview(view)  6  end

Page 18: (Even more) Rapid App Development with RubyMotion

Core Data and RubyMotion

• No equivalent of Xcode’s visual data modeller

• How do I define my data model?!

• What about versioning?!

• How will I handle migrations?

Page 19: (Even more) Rapid App Development with RubyMotion

What we need• Our data model (NSEntityDescriptions +

NSRelationshipDescriptions forming our NSManagedObject)

• A Core Data Stack (NSManagedObjectModel + NSPersistentStoreCoordinator + NSManagedObjectContext)

• A workflow for versioning and migrating between versions

Page 20: (Even more) Rapid App Development with RubyMotion

Defining Our Data Model• We would normally do this in Xcode

• Visual Editor for .xcdatamodel bundles

• Integrated handling of versioning and custom migration code

• Automatic lightweight (schema) migrations

• How do we achieve this with RubyMotion?

Page 21: (Even more) Rapid App Development with RubyMotion

Options for RubyMotion

• Handle everything programmatically (low level) 1

• Use Xcode to work with .xcdatamodel files, copy in each time 1

• Use a Ruby library for creating .xcdatamodel files 2

Page 22: (Even more) Rapid App Development with RubyMotion

Handling Everything Programmatically

entity = NSEntityDescription.alloc.initentity.name = 'Task'entity.managedObjectClassName = 'Task'entity.properties = [ 'task_description', NSStringAttributeType, 'completed', NSBooleanAttributeType ].each_slice(2).map do |name, type| property = NSAttributeDescription.alloc.init property.name = name property.attributeType = type property.optional = false property end

Page 23: (Even more) Rapid App Development with RubyMotion

Handling Everything Programmatically

entity = NSEntityDescription.alloc.initentity.name = 'Task'entity.managedObjectClassName = 'Task'entity.properties = [ 'task_description', NSStringAttributeType, 'completed', NSBooleanAttributeType ].each_slice(2).map do |name, type| property = NSAttributeDescription.alloc.init property.name = name property.attributeType = type property.optional = false property end

Not all that bad, but we want to use .xcdatamodel files so that we can benefit from versioning, automatic schema

migrations…

Page 24: (Even more) Rapid App Development with RubyMotion

.xcdatamodel files are just XML

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>!<model name="" userDefinedModelVersionIdentifier="001" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="2061" systemVersion="12D78" minimumToolsVersion="Xcode 4.3" macOSVersion="Automatic" iOSVersion="Automatic">! <entity name="Article" syncable="YES">! <attribute name="title" optional="YES" attributeType="String" syncable="YES"/>! <relationship name="author" optional="YES" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="Author" inverseName="articles" inverseEntity="Article" syncable="YES"/>! </entity>! <entity name="Author" syncable="YES">! <attribute name="name" optional="YES" attributeType="String" syncable="YES"/>! <relationship name="articles" optional="YES" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="Article" inverseName="author" inverseEntity="Author" syncable="YES"/>! </entity>!</model>

Page 25: (Even more) Rapid App Development with RubyMotion

Using a library to generate .xcdatamodel files (ruby-xcdm)

1 schema "001" do! 2 entity "Article" do! 3 string :body, optional: false! 4 integer32 :length! 5 boolean :published, default: false! 6 datetime :publishedAt, default: false! 7 string :title, optional: false! 8 ! 9 belongs_to :author!10 end!11 !12 entity "Author" do!13 float :fee!14 string :name, optional: false!15 has_many :articles!16 end!17 end

Page 26: (Even more) Rapid App Development with RubyMotion

Workflow• Create schema file in schemas directory, e.g. schemas/001_initial.rb

• Build the schema

• Add a new schema version, e.g. 002_add_new_fields.rb

• Rebuild the schema

• That’s it!

Page 27: (Even more) Rapid App Development with RubyMotion

Workflow$ echo "gem 'ruby-xcdm', '0.0.5'" >> Gemfile$ bundle install$ rake schema:buildGenerating Data Model learn-xcdm Loading schemas/001_initial.rb Writing resources/learn-xcdm.xcdatamodeld/1.xcdatamodel/contents$ rake # The default rake task is to run the app in the simulator(main)> mom = NSManagedObjectModel.mergedModelFromBundles(nil)=> #<NSManagedObjectModel:0x8fa7690>(main)> mom.entities.count=> 2(main)> mom.entities.first.name=> "Article"(main)> mom.entities.first.propertiesByName=> {"body"=>#<NSAttributeDescription:0x8e5db30>, "title"=>#<NSAttributeDescription:0x8ea4770>}

Page 28: (Even more) Rapid App Development with RubyMotion

Advantages of using ruby-xcdm

• No magic: generates XML from a schema

• Schema versions are fully text-based and readable, making them well-suited to version control

• Can compile our versions into .xcdatamodeld bundles, completely removing dependence on Xcode

Page 29: (Even more) Rapid App Development with RubyMotion

Basic Core Data Stack 1 model = NSManagedObjectModel.mergedModelFromBundles(nil) 2 3 store = NSPersistentStoreCoordinator.alloc.initWithManagedObjectModel(model) 4 store_path = File.join(NSHomeDirectory(), 'Documents', 'LearnXcdm.sqlite') 5 store_url = NSURL.fileURLWithPath(store_path) 6 7 options = { NSMigratePersistentStoresAutomaticallyOption => true, 8 NSInferMappingModelAutomaticallyOption => true } 9 10 error_ptr = Pointer.new(:object)11 12 unless store.addPersistentStoreWithType(NSSQLiteStoreType,13 configuration: nil,14 URL: store_url,15 options: options,16 error: error_ptr)17 raise "[ERROR] Failed to create persistent store: #{error_ptr[0].description}"18 end19 20 @context = NSManagedObjectContext.alloc.init21 @context.persistentStoreCoordinator = store

Page 30: (Even more) Rapid App Development with RubyMotion

Core Data Query

• From the developers of ruby-xcdm and RubyMotionQuery (RMQ)

• Abstracts away much of the complexity of Core Data

• All you need is your .xcdatamodeld bundle (that we just created using ruby-xcdm)

Page 31: (Even more) Rapid App Development with RubyMotion

Core Data Query in Action# app/models/task.rbclass Task < CDQManagedObjectend!# app/app_delegate.rbclass AppDelegate include CDQ! def application(application, didFinishLaunchingWithOptions:launchOptions) cdq.setup true endend

Page 32: (Even more) Rapid App Development with RubyMotion

Core Data Query in Action(main)> Task.count=> 0(main)> t1 = Task.create(task_description: "Complete presentation")(main)> t2 = Task.create(task_description: "File tax return")(main)> cdq.save=> true(main)> exit$ rake...(main)> Task.count=> 2(main)> t1, t2 = Task.all.array(main)> t1.task_description=> "Complete chapter"(main)> t2.task_description=> "File tax return"(main)> t2.destroy=> #<NSManagedObjectContext:0x914cbe0>(main)> cdq.save=> true(main)> Task.count=> 1

Page 33: (Even more) Rapid App Development with RubyMotion

Author.where(:name).eq("Emily")Author.where(:name).not_equal("Emily")Author.limit(1)Author.offset(10)Author.where(:name).contains("A").offset(10).first!# ConjuctionsAuthor.where(:name).contains("Emily").and.contains("Dickinson")Author.where(:name).starts_with("E").or(:pub_count).eq(1)!# Nested ConjuctionsAuthor.where(:name).contains("Emily").and(cdq(:pub_count).gt(100).or.lt(10))!# RelationshipsAuthor.first.publications.offset(2).limit(1)cdq(emily_dickinson).publications.where(:type).eq('poetry')!class Author < CDQManagedObject scope :prolific, where(:pub_count).gt(50)end

Core Data Query in Action

Page 34: (Even more) Rapid App Development with RubyMotion

Curious about Core Data and RubyMotion?

A book on this is available on Leanpub, covers how to use the Core Data stack with RubyMotion

and how to use libraries like CoreDataQuery to make

developing Core Data-driven apps easier.

Page 35: (Even more) Rapid App Development with RubyMotion

RubyMotion vs

Swift

• Entirely complementary: there’s a place for both

• I will be coding in Swift instead of Obj-C

• Not instead of Ruby(Motion)…

• Same base (LLVM), can use best tool for the job in each case.

Page 36: (Even more) Rapid App Development with RubyMotion

Which library?• Live development: RubyMotionQuery + SugarCube

• View layout and styling: MotionKit

• Project structure: ProMotion

• Core Data: ruby-xcdm + Core Data Query (CDQ)!

• Testing, mocking: motion-stump!

• Anything else: try Sugarcube and BubbleWrap

Page 37: (Even more) Rapid App Development with RubyMotion

Next Steps• In the coming weeks I’ll be researching and writing about:

• Libraries that I didn’t cover in depth today such as MotionKit, ProMotion, BubbleWrap and motion-stump.

• How to best handle heavyweight/data migrations in RubyMotion (Core Data)

• Deconstructing the one bit of ‘magic’ in Core Data Query

• How to write a ruby gem and contribute to the RubyMotion ecosystem.

Stefán Hafliðason ! http://stefan.haflidason.com

" @styrmis