jamonholmgren / ProMotion

ProMotion is a RubyMotion gem that makes iPhone development less like Objective-C and more like Ruby.
MIT License
1.26k stars 147 forks source link

Element styling #46

Closed jamonholmgren closed 11 years ago

jamonholmgren commented 11 years ago

I think the element styling needs more work and potentially a new paradigm.

I'd like some ideas. Potential discussion points:

  1. Subclass UIKit elements to add additional functionality to them, much as we've done to the UIController set?
  2. Provide a "stylesheets" folder and do something with that?
  3. Pull in an external styling system I'm not aware of, or integrate better with Teacup?

Of course, we should avoid forcing this styling paradigm on people and still work well with Teacup, Pixate, and other styling DSLs. But I think a built-in or included styling system would be a good idea.

Comments needed!

macfanatic commented 11 years ago

I've been an iOS dev since 4.0, so I might be a little stuck in my ways, but personally I'm fine with the way things are working currently. The only thing that would make me even happier would be if the add() method would also be defined on UIView so that I could do things like:

class WeatherView < UIView
  attr_accessor :conditions

  def self.new(conditions=nil)
    wv = alloc.initWithFrame([[0,0], [320,480]])
    wv.conditions = conditions
    wv
  end

  def initWithFrame(f)
    super
    add UILabel, font: :bold.uifont(12), backgroundColor: :clear.uicolor, placeholder: "Conditions"
    self
  end
end

This would also allow me to use add() more robustly in my controller, since I could call it on self and add to the self.view, but then I could also add items to existing views easily too:

class HomeScreen < PM::Screen
  def view_did_load
    @box = add UIView, frame: [[10,10],[40,40]], backgroundColor: :red.uicolor
    @box.add UILabel, font: :bold.uifont(12), backgroundColor: :clear.uicolor, placeholder: "Conditions"
  end
end

My rule of thumb is if I add more than 3-4 views in my controller, I need to create a view subclass & do my setup there. But it's so much easier to use the hash attributes syntax that add() provides :)

adelevie commented 11 years ago

Pixate looks awesome and seems to be best equipped for out-of-the-box good design (e.g. with their Bootstrap port).

jamonholmgren commented 11 years ago

Let's move add into its own module and make it work with UIView subclasses, then. Work for you?

jamonholmgren commented 11 years ago

I agree, Alan. Pixate is definitely cool -- I was one of the Kickstarter supporters and have a T-shirt even. Haha...

Jasonbit commented 11 years ago

Yah, Pixate is really cool. But I'm sure a lot of people don't want to pay for the license.

When I first checked out ProMotion this week (after working on many ios apps from first release of the sdk), I thought, yah, I like this approach of making things less obj-c (and I'm prob one of the few people who doesn't mind obj-c). Right now at my office we've fallen into the Storyboard trap - namely issues with merges all the time and not even sure you touched something which shows up as a regression in the build.

I've been thinking a lot about this problem for a few months and while we won't be switching our project over to Ruby Motion anytime soon, I still have a good sense of what I would like (in an ideal world). Maybe something between css and teacup..? (below doesn't really share much with css except in how properties could be considered for inclusion)


Stylescreen.new(:home) do

  # this could live in a base style class
  view do
    backgroundColor: "#edebe6".uicolor
  end

  # as could this
  button do
    titleFont: fontWithName('Signika-Regular', size:20),
    backgroundColor: "#edebe6".uicolor,
    borderWidth: 1.0,
    borderColor: "#e0ded9".uicolor,
    cornerRadius: 5.0
  end

  submit_button :extends => :button, :instance => true do
    left: 10,
    right: 10,
    bottom: constrain_above(:cancel_button)
  end

  cancel_button :extends => :button do
    left: 10,
    right: 10,
    bottom: constrain_pin(:bottom)
  end

end

class HomeScreen < PM::Screen
  # Not even sure this would be needed.. or maybe HomeScreen < PM::StyledScreen
  style :home

  def on_load

    # So we could hook up the events later
    @cancel_button = from_style(:cancel_button)
    @submit_button = from_style(:submit_button)

    add [@cancel_button, @submit_button]

    # or maybe by setting the :instance => true in the style you get a free @submit_button and 
    # you can avoid the above add to view code..? Just a thought. There's definitely reasons 
    # one might not want to do this, but this is ruby and i want to type less and get more :)

  end

end

Obviously this is closely related to teacup, but to me, this feels much more of what I would want with a Ruby library. It also gives me the added power of defining what I need in the context of ProMotion but not having to try to hack in teacup. I would love to ditch the CGRectMake calls in my implementation classes and have it work in the style definitions with autolayout. (Autolayout support is a must have in my mind.)

It's probably a decent amount of work with constraints, the implicit alloc/inits, and needing to decide what styles can be applied to the views/controls and layers. Probably less work than creating something like pixate tho..

Anyway, I just thought I'd chime in and it could be something I could set aside some time to contribute.

jamonholmgren commented 11 years ago

Thanks for the input, Jason! Good stuff.

One thing I want is to have a common handler for conversion of camel case to underscores. We can do that with a monkeypatch of String like the ActiveSupport's Inflector class:

class String
  def to_snakecase
    self.gsub(/::/, '/').
      gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
      gsub(/([a-z\d])([A-Z])/,'\1_\2').
      tr("-", "_").
      downcase
  end
  def to_camelcase(uppercase_first_letter=true)
    string = self.dup
    if uppercase_first_letter
      string = string.sub(/^[a-z\d]*/) { inflections.acronyms[$&] || $&.capitalize }
    else
      string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase }
    end
    string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }.gsub('/', '::')
  end
end

This would allow us to handle ruby-style snake case and Obj-C's headless camel case for these attributes.

jamonholmgren commented 11 years ago

If you haven't caught on by now, nothing's really sacred with me as far as monkeypatching the crap out of RubyMotion. As long as it doesn't cause bugs or make a lot of gems incompatible I take a pretty pragmatic approach to this.

jamonholmgren commented 11 years ago

Just thinking out loud (figuratively) here...

ProMotion::Styles.define do
  style login_button: {
      title_font: fontWithName('Signika-Regular', size:20),
      background_color: "#edebe6".uicolor,
      border_width: 1.0,
      border_color: "#e0ded9".uicolor,
      corner_radius: 5.0
  }

  style profile_pic_view: {
    custom_style: something,
    layer: {
      something_inside_layer: 5
    }
  }
end

class HomeScreen < ProMotion::Screen
  def will_appear
    add UIButton, style: :login_button, on_touch: :log_in_action
    add CustomProfileView, style: :profile_pic_view, on_touch: :maximize_profile_action
  end

  # ...
end

Make add have all of the proper initializers mapped to as many UIKit objects as we can think of so you can just pass in the class and get a blank instance out of it.

Maybe the on_touch is going too far.

It might work better to make the style hashes into lambda blocks and not run them until called. I wouldn't want all that stuff sitting in memory.

Jasonbit commented 11 years ago

Jamon,

I like that! Seems like a good direction to start with. I think you're right regarding the lambda blocks too. My one question would be what would make the most sense for breaking up the styles and then including them as needed. Not a hard problem to solve, but it would probable make sense from a maintenance perspective.

Swatto commented 11 years ago

Jamon,

Your last proposition is very nice and I like the approch and the on_touch possibility. It could be great to have the possibilty to define the style inside the class of the view or the controller to avoid multiple class definition in addition to this (and be more readable).

And you with that kind of code, you could style an element and have a another element in an other view or controller with the same "style class" and be different.

class HomeScreen < ProMotion::Screen
  def will_appear
    add UIButton, style: :login_button, on_touch: :log_in_action
  end

  def styling
    style login_button: {
      title_font: fontWithName('Signika-Regular', size:20),
      background_color: "#edebe6".uicolor,
      border_width: 1.0,
      border_color: "#e0ded9".uicolor,
      corner_radius: 5.0
    }
  end
end
jamonholmgren commented 11 years ago

Good point, @Swatto ! That would actually work really well. Although, is that really much different than defining a method that returns the styles?

class HomeScreen < ProMotion::Screen
  def will_appear
    add UIButton, style: login_button_style, on_touch: :log_in_action
  end

  def login_button_style
    {
      title_font: fontWithName('Signika-Regular', size:20),
      background_color: "#edebe6".uicolor,
      border_width: 1.0,
      border_color: "#e0ded9".uicolor,
      corner_radius: 5.0
    }
  end
end
Swatto commented 11 years ago

I think this approch is less powerfull than the styling method :

The def styling with all of the definition in it, have a major feature : we can generate style with event (a return name from a server, from an input of the user, etc). With the name of the styling in a method definition name, we can't generate style by his name.

What do you think about it ?

jamonholmgren commented 11 years ago

You could do:

  method_name_here = :login_button_style
  add UIButton, style: send(method_name_here), on_touch: :log_in_action

send calls a method by a symbol or string name.

I do think this needs careful thought. We don't want to overcomplicate it.

The thing that I'm considering right now is lazy-loading the attributes hash. We don't want to hold all the attribute hashes in memory if they're not in use. A lambda or block would work but would be a bit awkward in all the scenarios I've imagined. Any thoughts?

Jasonbit commented 11 years ago

I agree with Swatto, but not only for reasons listed, but also because of code separation and sharing.

Putting styles in a method of the (for instance) HomeScreen takes away all the power of this concept. Like css, a big part of the power of styles (for me anyway) is reuse and extensibility. I want one base button, for example, and then extend off that for multiple screens. And having this adhere to a file/naming convention, outside of implementation, would be really cool.

jamonholmgren commented 11 years ago

Agree, Jason. I'll do some thinking on this next week. Ideas welcome.

jamonholmgren commented 11 years ago

The thing that has me stuck on this one still is the memory usage. I don't want to store all the styles (and their associated objects) in memory in the chance that an element might use it. Even Teacup ends up instantiating a lot of styles without using them if you re-use stylesheets across ViewControllers.

It likely makes the most sense to create procs that only run when that style is called.

ProMotion::Styles.define do
  style :login_button do {
    background_color: UIColor.blackColor,
    font: fontWithName('Signika-Regular', size:20)
  } end

  style :logout_button, inherit: [ :login_button, :base_styles ] do {
    background_color: UIColor.redColor,
    font: fontWithName('Signika-Bold', size:20)
  } end

  # or maybe...

  style :logout_button do {
    include: [ :login_button, :base_styles ],
    background_color: UIColor.redColor,
    font: fontWithName('Signika-Bold', size:20)
  } end

end

The syntax looks slightly wonky, I realize, with the embedded hash, but this would be the most performant way to do this I believe -- a block (or proc) with a hash inside.

The inherit thing seems like it would work pretty well. You just merge the hashes. This would be fairly straightforward to implement.

markrickert commented 11 years ago

Check out: https://github.com/tombenner/nui

And see an implementation i did here: https://github.com/Skookum/RubyMotionTalk/tree/master/RedditRss

I don't think element styling should be a part of ProMotion when there are so many other libraries that do it so well.

jamonholmgren commented 11 years ago

That looks really good. On your RedditRss app, I didn't see your stylesheet -- did I miss it somewhere?

jamonholmgren commented 11 years ago

I think I'll write an entire Styling section in the README and talk about Teacup, NUI, Pixate, and of course the built-in ProMotion system. I'm not sure how far I'll take the built-in system (it's fairly good already for in-place styling) -- the above is an idea and really depends on whether devs would like something like that or if they already have a favorite styling system.

I think a mixture of systems would work fine too -- we don't have to only support one. Teacup 2.0 (https://github.com/colinta/teacup/tree/2.0) looks like it will be pretty good and include support for Pixate and NUI. So it probably makes sense to just incrementally improve the internal system and recommend some of the other systems for more complex needs.

markrickert commented 11 years ago

The stylesheet i'm using is the NUI default, therefore no need to declare it :)

One neat thing about NUI is that you can call setAutoUpdatePath with an absolute path for testing and as you save the stylesheet file, the interface in the simulator updates automatically! https://github.com/Skookum/RubyMotionTalk/blob/master/RedditRss/app/app_delegate.rb#L3

markrickert commented 11 years ago

My only beef with NUI is that it's iOS 6 only... but when iOS 7 comes, out, I'll be dropping support for iOS 5.

iOS 5.1.1 is currently about 3% of my user base (roughly 10k people). Everyone else is on some versions of 6.

jasonlux commented 11 years ago

Yah, the NUI autoupdate is pretty cool, forgot abou that. :)

I have to admit tho, I do like Jamon's direction. Not that I couldn't do it in NUI or teacup, but this


  style :logout_button do {
    include: [ :login_button, :base_styles ],
    background_color: UIColor.redColor,
    font: fontWithName('Signika-Bold', size:20)
  } end

just looks very nice and clean to me. And that's the reason I gravitated towards ProMotion - I like the abstraction and the way things are represented. I dunno, the syntax just speaks to me more than the others.

@jamon, you're probably right about the instantiation - would be interesting to see what the memory usage might be for curiosity's sake.

jamonholmgren commented 11 years ago

I think if it doesn't take too much to implement (which I don't think it will) we might do this. Just gives users another option. But I'll make it clear that ProMotion can be used with your favorite styling system.

It'll be in a later release. Perhaps 1.0.

markrickert commented 11 years ago

Damn. Promotion is going to eclipse bubblewrap in terms of functionality soon :wink:

jamonholmgren commented 11 years ago

@markrickert Touché ...

I'm certainly willing to be talked out of this. I don't want to fall into the "not built here" syndrome. I agree with Jason that this syntax would make a lot of sense to me, though.

jamon commented 11 years ago

it seems as though you guys have accidentally added my account to this thread (jamon@github.com).

On Wed, May 8, 2013 at 11:29 AM, Jamon Holmgren notifications@github.comwrote:

@markrickert https://github.com/markrickert Touché ...

I'm certainly willing to be talked out of this. I don't want to fall into the "not built here" syndrome. I agree with Jason that this syntax would make a lot of sense to me, though.

— Reply to this email directly or view it on GitHubhttps://github.com/clearsightstudio/ProMotion/issues/46#issuecomment-17617388 .

jamonholmgren commented 11 years ago

With a name as cool as that, @jamon, you should be involved. :) Sorry about that. Feel free to unwatch, of course.

jasonlux commented 11 years ago

Oh, whoops, sorry. I should have paid more attention.

@jamonholmgren I guess it's up to you, and sure, we don't want to fall into the not built here thing, but like I was saying earlier, the approach your taking feels best to me. So I guess that's my two cents.

jfi commented 11 years ago

I really like this, to throw my 2c in. Also a fan of the on_touch event.. :)

jamonholmgren commented 11 years ago

I'm currently working on a way to integrate the parts of Teacup that can help us here.

jamonholmgren commented 11 years ago

Looking at Teacup in more detail, I think it will work fine as-is. Since Screens are UIViewControllers, you can use Teacup normally.

I would recommend you use the instance method syntax (rather than the class method syntax):

class StyleScreen < PM::Screen
  title "Styles"
  stylesheet :standard

  def will_appear
    @view_setup ||= begin
      layout(self.view, :root) do
        # these subviews will be added to `self.view`
        subview(UIToolbar, :toolbar)
        subview(UIButton, :hi_button)
      end
      true
    end
  end

end

I'll write up a styling with Teacup guide at some point and include it in the repo.

Jasonbit commented 11 years ago

So does this mean PM will only use Teacup for styling or will it have its own styling as well?

jamonholmgren commented 11 years ago

@Jasonbit: ProMotion will continue to have its own lightweight styling system with set_attributes and add. I'll continue to develop that as well and may even incorporate some of the ideas we had here. But after looking into what they've done with Teacup I think it's a fine choice for more demanding styling requirements.

jamonholmgren commented 11 years ago

ProMotion now supports Teacup styling in a very simple way. Just add stylesheet to your screen and stylename: to your add or set_attributes calls.

class HomeScreen < PM::Screen
  stylesheet :home

  def will_appear
    add UILabel.new, stylename: :teacup_style
  end
end
jamonholmgren commented 11 years ago

I decided to write a guide to styling views. It uses the simplest solution; one that doesn't eat memory nor require any modification to the current ProMotion styling code.

https://github.com/clearsightstudio/ProMotion/wiki/Guide%3A-Styling-Your-Views

Essentially, you define a module that gets mixed into your screen. To extend, it's relatively easy using parentstyle.merge({ new_styles: :whatever}). Just ruby, no magic. Then, you use that method in your add element call.

module MyStyles
  def button_style
    {
      background_color: UIColor.whiteColor
    }
  end
end

class MyScreen < PM::Screen
  include MyStyles

  def on_load
    add some_button, button_style
    add some_other_button, button_style.merge({
      some_override: :value
    })
  end
end
bartzon commented 11 years ago

Has the add UILabel.new, stylename: :foo for Teacup option disappeared? :)

It doesn't work for me, and I can't locate it in the source files anywhere.

jamonholmgren commented 11 years ago

Addressed your question in the PR.