This is a grab-bag collection of UIView subclasses, UIViewController modules, and random tools.
Ruby
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
app
lib
resources
spec
.gitignore
.travis.yml
Gemfile
Gemfile.lock
LICENSE
README.md
Rakefile
graymatter.gemspec

README.md

GrayMatter

A collection of useful tools, by Colin T.A. Gray. Depends on SugarCube. Tests require Teacup.

module namespace: GM

GestureRecognizers

GM::HorizontalPanGestureRecognizer

GM::VerticalPanGestureRecognizer

These recognize a pan gesture in only one direction. The default threshold is HorizontalPanGestureRecognizer::DefaultThreshold (4), but can be changed with the threshold attribute.

UIViews

GM::SetupView (module)

It infuriates me that there are two ways to setup a view: initWithFrame and awakeFromNib. There needs to be one place to put code for custom views. SetupView provides that one place.

class MyView < UIView
  include GM::SetupView

  def setup
    # this code will only be run once
  end
end

GM::ForegroundColorView

Sometimes you need a background color that is part of your view hierarchy. I can't remember why I needed to, but this view does the trick. Assign a color attribute and it will fill a rect with that color. Also supports a path attribute, which is a UIBezierPath that clips the view.

Basically, you can draw a swath of color this way.

GM::FabTabView

This is a very simple tab view. It controls a list of controllers, which should implement a fab_tab_button attribute (if you want to explicitly declare that your controller is a FabTabController, you can include GM::FabTabController). The fab_tab_button should be a subcalss of UIControl.

When you use a FabTabView, you must assign a root_controller, and its child controllers must . An example says it all I think:

def viewDidLoad
  ctlr_a = CustomControllerA.new
  self.addChildViewController(ctlr_a)
  ctlr_b = CustomControllerB.new
  self.addChildViewController(ctlr_b)

  # the defaults that we take advantage of using this method:
  #   - assign the root_controller, obviously
  #   - the tab view will be sized to cover the entire view bounds of the root
  #     view controller
  self.view << FabTabView.alloc.initInRootController(self)
  # self.view << FabTabView.new(self) does the same thing
end

Why use a custom tab controller? Because the built-in one does not support custom buttons, that's the only reason. This one is much less feature-rich, but gets the job done!

GM::GradientView

This used to be a separate gem, but I've removed that. It lives here now.

It's great as a background view!

TODO: implement the radial gradient. I just haven't needed it.

GM::TypewriterView

A UICollectionView can do everything that TypewriterView does, but with lots more delegate methods to implement. ;-)

Add a bunch of subviews to TypewriterView and it will display them left-to-right, top-to-bottom. You can assign scroll_view and background_view objects, too, and the scroll_view will get assigned the appropriate contentSize, and the background_view will be ignored when it lays out the subviews, and it will be sized to cover the entire view.

GM::InsetTextField

I'm sure we've all implemented a subclass of UITextField that implements the methods placeholderRectForBounds, textRectForBounds, editingRectForBounds

GM::MaskedImageView

Masks a UIImageView using a UIBezierPath. Assign an image to image, and a bezier path to path, and that's it.

GM::RoundedRectView

You can assign a different radius for each side. Radius is attached to a side (not per corner), so that means that there will be some symmetry.

GM::FakeTableView

Adding views using addSubview will actually add them to what looks like a grouped table view.

GM::SensibleFlowLayout

This UICollectionViewFlowLayout subclass makes it easier to add animations to added/removed items from a UICollectionView. Just subclass GM::SensibleFlowLayout and define these two methods, where you add your attributes:

  • def appearing_attributes(attrs, for_path: index_path)
  • def disappearing_attributes(attrs, for_path: index_path)

UIViewController modules

These modules are all meant to enhance your custom UIViewController classes.

GM::KeyboardHandler

This one is so handy! I've tried to get it to be both simple and thorough. Ideally, you can pass it your scroll view, and it will take care of setting the contentInset when the keyboard is shown. You must call keyboard_handler_start and keyboard_handler_stop - these methods register (and unregister) keyboard events. You pass the scroll view into the prepare_keyboard_handler method before the view is visible.

class MyController
  include GM::KeyboardHandler

  def viewDidLoad
    prepare_keyboard_handler(@scroll_view)
  end

  def viewWillAppear(animated)
    super
    keyboard_handler_start
  end

  def viewDidDisappear(animated)
    super
    keyboard_handler_stop
  end
end

GM::HideShowModal

When you prepare the modal (usually in viewDidLoad) a modal view is added to the bottom of the window (making it the frontmost view) and immediately hidden. When you call show_modal, the modal fades in with a spinner. hide_modal does the obvious. You can also use show_modal_in(time_interval) to have the modal appear after a second or two. Best used with TheEntireUI.disable, but that's up to you.

class MyController < UIViewController

  def viewDidLoad
    prepare_hide_show_modal  # accepts a `target` - the view where the modal should be added
  end

  def submit_button_pressed
    show_modal
    submit_form {
      hide_modal
      UIAlertView.alert "Success!"
    }
  end

  def refresh
    show_modal_in(1.second)
    fetch_data {
      hide_modal
    }
  end
end

GM::Parallax

Given a scrollview and a hash of views and rules, you can easily create really neat parallax effects. The two simplest rules - true and false - will either fix the view's location relative to its initial origin (false rule, e.g. "Should I move?" => false) or it will scroll with the scroll view ("Should I move?" => true).

The other thing it can do which is great is keep two scroll views in sync, so if you've got a speadsheet header and you need it to keep up with scrolling inside the cells-view, that is pretty easy.

class MyController < UIViewController

  layout do
    @scroll_view = subview(UIScrollView, :scroll_view) do
      @bg_image = subview(UIImage, :bg_image)
    end
  end

  def viewDidLoad
    prepare_parallax(@scroll_view,
      @bg_image => [-2, 2],  # scrolls horizontally at double rate, and
      @diagonal => ->(offset) { CGPoint.new(offset.y * 1.5, 0) },
      @moving_thing => ->(offset) { (120..400) === offset.y ? CGPoint.new(offset.y - 120, 0) : (offset.y < 120 ? CGPoint.new(0, 0) : CGPoint.new(280, 0)) },
      @another_scroller => [0, 1],  # contentOffset.y will be the same when scroll_view is changed
      )
    prepare_parallax(@another_scroller,
      @scroll_view => [0, 1],  # you do need to mirror the two scroll view rules
      )
  end
end

UIView modules

GM::Triggerable

This is one of my favorites because I tend to make a lot of custom UIView subclasses. If you have lots of buttons or controls in there, it's messy to create attributes for those and then "reach into" the view to assign touch/change events to those controls.

Instead, include GM::Triggerable in that subclass and trigger custom events from those controls. It looks like this:

class BamBoomView < UIView
  include GM::Triggerable

  def initWithFrame(frame)
    super.tap do
      bam_button = UIButton.rounded
      bam_button.setTitle('Bam', forState: :normal.uicontrolstate)
      bam_button.sizeToFit
      bam_button.on :touch {
        self.trigger :bam
      }
      self << bam_button

      boom_button = UIButton.rounded
      boom_button.setTitle('BOOM', forState: :normal.uicontrolstate)
      boom_button.sizeToFit
      boom_button.on :touch {
        self.trigger :boom
      }
      self << boom_button
    end
  end
end

cell = BamBoomView.new
cell.on :bam do
  puts "BAM!"
end
cell.on :boom do
  puts "BOOM!"
end

Tools

GM::PeoplePicker

Easy to show the address book people picker.

GM::PeoplePicker.show { |person|
  # an ABAddressBook person will be available here, or nil if the operation was
  # canceled.
}

GM::ExposeController

This is a very simple 'slied-to-expose' controller (it's not a subclass of UIViewController, I'm using the term controller loosely here), like facebook's and google's slide-menu. It's very low tech, but effective! You need to supply it with a target - the view that will control the slide, and a slide_view - the view that is moved to expose whatever is beneath it.

If you want to squeeze some performance out of it, you can assign a delegate and respond to will_open_slide_menu and did_close_slide_menu, and you can add/remove the background view at that time, which should save some CPU cycles.

GM::SelectOneController

This one is really handy for table-based forms. Assign items and style them with a cell_handler block, and an optional include_other boolean will include a UITextField. An on_done block is called with one of the objects in items when it is selected.

GM::KeyboardState

Tracks the keyboard throughout the lifetime of the app - you can always find out whether the keyboard is visible or not. It is SILLY that this is not something easy to determine! (well, now it is, I guess)

if KeyboardState.visible?
  KeyboardState.last_notification
end

FuncTools

A bunch of useful functions for asynchronous programming

after = GM::FuncTools.after(2) { puts "hi!" }
after.call  # =>
after.call  # => 'hi!'
after.call  # => 'hi!'

keep_it_up = GM::FuncTools.until(3) { puts 'boo!' }
keep_it_up.call  # => 'boo!'
keep_it_up.call  # => 'boo!'
keep_it_up.call  # => 'boo!'
keep_it_up.call
keep_it_up.call

once = GM::FuncTools.once { puts "i'm outta here" }
once.call  # => "i'm outta here"
once.call  # =>
once.call  # =>

TheEntireUI

This class is for easily accessing, obviously, the entire UI. For now that just means disable-ing and enable-ing the UI:

GM::TheEntireUI.disable  # a view is added to the UIWindow that intercepts UI events
GM::TheEntireUI.enable  # the view is removed

You can call these using notifications, too, if that just fits your app better (I can't imagine a situation where it would... but I ported this thing from code that was using notifications, so there it is).

GM::DisableUI.post_notification  # => GM::TheEntireUI.disable
GM::EnableUI.post_notification  # => GM::TheEntireUI.enable

D, Drawing, Drawler

D is graymatter's drawing library. They are drawing primitives, built with CoreGraphics. Drawing is a UIView subclass that accepts an array of GM::D objects and draws them. Drawler is a UIView subclass that you can create as a one-off.

D
def drawRect(rect)
  GM::D::circle(rect.frame.center, 10, :white).draw
  GM::D::circle(rect.frame.center, 8, :red).draw
end
Drawing
circles = Drawing.new
circles << GM::D::circle(rect.frame.center, 10, :white)
circles << GM::D::circle(rect.frame.center, 8, :red)
self.view << circles

# you can use teacup to create these
style :dot,
  draw: [
    GM::D::circle([20, 20], 10, :white),
    GM::D::circle([20, 20], 8, :red),
  ]
Drawler
self.view << Drawler do |context|
  CGContextAddEllipseInRect(context, frame)
  CGContextDrawPath(context, KCGPathFillStroke)
end