Share
Embed
(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
Transcript
37 slides
-
Rapid App Development with RubyMotion Stefán Hafliðason http://stefan.haflidason.com @styrmis 'XGPOQTG
External libraries that make development even easier -
• 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? -
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. -
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. -
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 -
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. -
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 -
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 -
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 -
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} -
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… -
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 -
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 -
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! -
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… -
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. -
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 -
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? -
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 -
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? -
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 -
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 -
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… -
.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> -
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 -
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! -
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>} -
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 -
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 -
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) -
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 -
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 -
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 -
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. -
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. -
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 -
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