Upgrade to Pro — share decks privately, control downloads, hide ads and more …

(Even more) Rapid App Development with RubyMotion

(Even more) Rapid App Development with RubyMotion

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.

Stefan Haflidason

June 12, 2014
Tweet

More Decks by Stefan Haflidason

Other Decks in Programming

Transcript

  1. • 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?
  2. 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.
  3. 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.
  4. 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
  5. 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.
  6. Layout Experimentation Quickly 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
  7. Wiring it up Let’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
  8. 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
  9. 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}
  10. 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…
  11. 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 do 10 frame from_bottom(height: 50, width: '100%') 11 add UIButton, :login_button do 12 background_color superview.backgroundColor 13 frame [[ 10, 5 ], [ 50, parent.height - 10 ]] 14 end 15 end 16 17 add UIView, :inputs do 18 frame x: 0, y: 0, width: '100%', height: '100% - 50' 19 autoresizing_mask :pin_to_top, :flexible_height, :flexible_width 20 add UITextField, :username_input do 21 frame [[10, 10], ['100% - 10', :auto]] 22 end 23 add UITextField, :password_input do 24 frame below(:username_input, margin: 8) 25 end 26 end 27 end 28 end • Flexible DSL for view layouts • Simpler handling of device rotation • Support for constraints / Auto Layout • Build your own DSL on top
  12. 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 end 13 14 def tapped_about(args={}) 15 open AboutScreen 16 end 17 18 def log_out 19 # Log out! 20 end 21 end • Aims to remove as much boilerplate code as possible • More intuitive, Ruby-style view controller building • Built-in classes for common view types
  13. 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!
  14. SugarCube: Sugar coating for verbose APIs 1  (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…
  15. 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.
  16. MotionAwesome 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
  17. 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?
  18. 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
  19. 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?
  20. Options for RubyMotion • Handle everything programmatically (low level) •

    Use Xcode to work with .xcdatamodel files, copy in each time • Use a Ruby library for creating .xcdatamodel files
  21. Handling Everything Programmatically entity = NSEntityDescription.alloc.init entity.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
  22. Handling Everything Programmatically entity = NSEntityDescription.alloc.init entity.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…
  23. .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>
  24. 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
  25. 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!
  26. Workflow $ echo "gem 'ruby-xcdm', '0.0.5'" >> Gemfile $ bundle

    install $ rake schema:build Generating 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>}
  27. 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
  28. 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 end 19 20 @context = NSManagedObjectContext.alloc.init 21 @context.persistentStoreCoordinator = store
  29. 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)
  30. Core Data Query in Action # app/models/task.rb class Task <

    CDQManagedObject end ! # app/app_delegate.rb class AppDelegate include CDQ ! def application(application, didFinishLaunchingWithOptions:launchOptions) cdq.setup true end end
  31. 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
  32. 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 ! # Conjuctions Author.where(:name).contains("Emily").and.contains("Dickinson") Author.where(:name).starts_with("E").or(:pub_count).eq(1)

    ! # Nested Conjuctions Author.where(:name).contains("Emily").and(cdq(:pub_count).gt(100).or.lt(10) ) ! # Relationships Author.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
  33. 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.
  34. 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.
  35. 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
  36. 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