├── README.md ├── ViewController.coffee └── ViewController.sketchplugin └── Contents └── Sketch ├── createBackBtn.cocoascript ├── createLink.cocoascript └── manifest.json /README.md: -------------------------------------------------------------------------------- 1 | ## ViewController-for-Framer 2 | The ViewController module for Framer.js helps you create multi step user flows with pre-made transitions like "fade in", "zoom in" and "slide in". It consists of a Framer module and an optional [Sketch plugin](#sketch). Check out the intro article on [Medium](https://uxdesign.cc/create-ui-flows-using-sketch-and-framer-36b6552306b5#.4j5idvu0r). 3 | 4 | ![framerdemo](http://cl.ly/0a1y073v3A0L/2016-04-30%2009_59_07.gif) 5 | 6 | [Try the demo protoype](http://share.framerjs.com/un1di17cqs6u/) 7 | 8 | ### Getting started 9 | 10 | The ViewController module makes it easy to create larger UI flows inside Framer. To get started, [download](https://github.com/awt2542/ViewController-for-Framer/archive/master.zip) the ViewController.coffee file and put inside your project's modules folder. Then follow these steps: 11 | 12 | **Step 1** Create a new ViewController 13 | 14 | ```coffeescript 15 | ViewController = require 'ViewController' 16 | Views = new ViewController 17 | initialView: sketch.home 18 | ``` 19 | 20 | **Step 2** Call one of the [supported transitions](#transitions) to switch view or use the [Sketch plugin](#sketch) to generate links. 21 | 22 | ```coffeescript 23 | sketch.home.onClick -> Views.slideInLeft(sketch.menu) 24 | ``` 25 | 26 | ### Available transitions 27 | 28 | Transitions are trigged by using one of the transition methods. Eg. `Views.fadeIn(anotherLayer)`. Each transition accepts an animationOption object as the second argument. Eg. `Views.fadeIn(anotherLayer, time: 2)` 29 | 30 | | Transition | Demo 31 | | ------------- |-------------| 32 | | .switchInstant() |![fadeIn](http://cl.ly/2f0S4026411g/switchInstant.gif)| 33 | | .slideInUp() |![fadeIn](http://cl.ly/0d350p25132M/slideInUp.gif)| 34 | | .slideInRight() |![fadeIn](http://cl.ly/3p3c2d122n1c/slideInRight.gif)| 35 | | .slideInDown() |![fadeIn](http://cl.ly/0u3o2I463428/slideInDown.gif)| 36 | | .slideInLeft() |![fadeIn](http://cl.ly/3O0o0e0X1R3H/slideInLeft.gif)| 37 | | .slideOutUp() |![fadeIn](http://cl.ly/3S2u3P09262T/slideOutUp.gif)| 38 | | .slideOutRight() |![fadeIn](http://cl.ly/1W031x3k0025/slideOutRight.gif)| 39 | | .slideOutDown() |![fadeIn](http://cl.ly/2t2m2c1w2W0t/slideOutDown.gif)| 40 | | .slideOutLeft() |![fadeIn](http://cl.ly/1L0u2u0J2P1o/slideOutLeft.gif)| 41 | | .moveInRight() |![fadeIn](http://cl.ly/3W1H3n400E0m/moveInRight.gif)| 42 | | .moveInLeft() |![fadeIn](http://cl.ly/0K0B2A0e1A1U/moveInLeft.gif)| 43 | | .moveInUp() || 44 | | .moveInDown() || 45 | | .pushInRight() |![fadeIn](http://cl.ly/181l1T08372m/pushInRight.gif)| 46 | | .pushInLeft() |![fadeIn](http://cl.ly/0a003K0e0v1t/pushInLeft.gif)| 47 | | .pushOutRight() |![fadeIn](http://cl.ly/0Z3R1W2s3o1A/pushOutRight.gif)| 48 | | .pushOutLeft() |![fadeIn](http://cl.ly/0n3M0C113B3p/pushOutLeft.gif)| 49 | | .fadeIn() |![fadeIn](http://cl.ly/3w3X2c080X3q/fadeIn.gif)| 50 | | .zoomIn() |![fadeIn](http://cl.ly/191u2B3U0X13/zoomIn.gif)| 51 | | .zoomOut() |![fadeIn](http://cl.ly/2w3d3O0F121g/zoomOut.gif)| 52 | 53 | 54 | ### Properties and methods 55 | 56 | 57 | #### .initialView 58 | 59 | Set the initial view 60 | 61 | ```coffeescript 62 | Views = new ViewController 63 | initialView: sketch.home 64 | ``` 65 | 66 | #### .initialViewName 67 | 68 | Set the initial view based on a layer name. In the following example, the layer named "initialView" will automatically be set as the initialView. 69 | 70 | ```coffeescript 71 | Views = new ViewController 72 | initialViewName: "initialView" # default value 73 | ``` 74 | 75 | #### .currentView 76 | 77 | Returns the current view 78 | 79 | ```coffeescript 80 | Views = new ViewController 81 | initialView: sketch.home 82 | Views.slideIn(sketch.menu) 83 | print Views.currentView # returns sketch.menu 84 | ``` 85 | 86 | #### .previousView 87 | 88 | Returns the previous view 89 | 90 | ```coffeescript 91 | Views = new ViewController 92 | initialView: sketch.home 93 | Views.slideIn(sketch.menu) 94 | print Views.previousView # returns sketch.home 95 | ``` 96 | 97 | #### .history 98 | 99 | Returns the full history of the ViewController in an array 100 | 101 | ```coffeescript 102 | Views = new ViewController 103 | initialView: sketch.home 104 | Views.slideIn(sketch.menu) 105 | Views.slideIn(sketch.profile) 106 | print Views.history 107 | ``` 108 | 109 | #### .back() 110 | 111 | Go back one step in history and reverse the animation. 112 | 113 | ```coffeescript 114 | Views = new ViewController 115 | initialView: sketch.home 116 | Views.slideIn(sketch.menu) 117 | sketch.btn.onClick -> Views.back() # animates back to sketch.home 118 | ``` 119 | 120 | #### .animationOptions 121 | 122 | Default animation options for all transitions inside the ViewController. 123 | 124 | ```coffeescript 125 | Views = new ViewController 126 | animationOptions: 127 | time: .8 128 | curve: "ease-in-out" 129 | ``` 130 | 131 | #### .autoLink 132 | 133 | automatically create onClick-links based on layer names according to the format: transitionName\_viewName. For example, renaming the "home" layer inside Sketch to slideInRight\_menu would be equivalent to the following code: 134 | 135 | ```coffeescript 136 | sketch.home.onClick -> Views.slideInRight(menu) 137 | ``` 138 | 139 | To get started, just create a new ViewController and import a Sketch file with properly named layers. autoLink is "true" by default. 140 | 141 | See [available transitions](#transitions) and the separate [sketch plugin](#sketch) that helps you with renaming your layers. 142 | 143 | Example project: [http://share.framerjs.com/owauo3t6i7al/](http://share.framerjs.com/owauo3t6i7al/) 144 | 145 | #### .backButtonName 146 | 147 | Layers matching this name will automatically call .back() on click. Defaults to "backButton" 148 | 149 | #### .scroll (experimental) 150 | 151 | Automatically adds scroll components to each view. If a view is larger than the ViewController, it will automatically enable scrollHorizontal and/or scrollVertical. Defaults to "false". 152 | 153 | ### Events 154 | 155 | 156 | #### change:currentView 157 | 158 | Triggered when the currentView changes 159 | 160 | ```coffeescript 161 | Views.onChange "currentView", (current) -> 162 | print "new view is: "+current.name 163 | ``` 164 | 165 | #### change:previousView 166 | 167 | Triggered when the previousView changes 168 | 169 | ```coffeescript 170 | Views.onChange "previousView", (previous) -> 171 | print "previous view is: "+previous.name 172 | ``` 173 | 174 | #### ViewWillSwitch 175 | 176 | Triggered before a transition starts 177 | 178 | ```coffeescript 179 | Views.onViewWillSwitch (oldView, newView) -> 180 | print oldView,newView 181 | ``` 182 | 183 | #### ViewDidSwitch 184 | 185 | Triggered after a transition has finished 186 | 187 | ```coffeescript 188 | Views.onViewDidSwitch (oldView, newView) -> 189 | print oldView,newView 190 | ``` 191 | 192 | ### Sketch plugin 193 | 194 | ![sketchPlugin](http://cl.ly/0y0s0M451Q2K/ScreenFlowDemo.gif) 195 | 196 | If you have [autoLink](#autolink) enabled in your ViewController (enabled by default) you can create links by renaming your layers according to the format: transitionName_viewName. This plugin makes renaming layers slightly more convenient. 197 | 198 | 1. Select two layers, one button and one view (eg. an artboard) 199 | 2. Run the plugin and choose one of the available transitions 200 | 3. Import the changes to Framer 201 | 4. Set up a ViewController in your project according to the [Getting Started guide](#gettingstarted) 202 | 203 | Get the plugin here: [https://github.com/awt2542/ViewController-for-Framer/archive/master.zip](https://github.com/awt2542/ViewController-for-Framer/archive/master.zip) 204 | 205 | ### Example prototypes 206 | 207 | - [Basic example](http://share.framerjs.com/un1di17cqs6u/) 208 | - [autoLink example](http://share.framerjs.com/owauo3t6i7al/) 209 | 210 | 211 | Thanks to Chris for the [original inspiration](https://github.com/chriscamargo/framer-viewNavigationController) for this module and to Stephen, Jordan, Jason, Koen, Fran & Marc for early feedback. Also thanks to Invision for the excellent UI kit used in the examples: [Do UI kit](https://www.invisionapp.com/do) 212 | -------------------------------------------------------------------------------- /ViewController.coffee: -------------------------------------------------------------------------------- 1 | class module.exports extends Layer 2 | 3 | constructor: (options={}) -> 4 | options.width ?= Screen.width 5 | options.height ?= Screen.height 6 | options.clip ?= true 7 | options.initialViewName ?= 'initialView' 8 | options.backButtonName ?= 'backButton' 9 | options.animationOptions ?= { curve: "cubic-bezier(0.19, 1, 0.22, 1)", time: .7 } 10 | options.backgroundColor ?= "black" 11 | options.scroll ?= false 12 | options.autoLink ?= true 13 | 14 | super options 15 | @history = [] 16 | 17 | @onChange "subLayers", (changeList) => 18 | view = changeList.added[0] 19 | if view? 20 | # default behaviors for views 21 | view.clip = true 22 | view.on Events.Click, -> return # prevent click-through/bubbling 23 | # add scrollcomponent 24 | if @scroll 25 | children = view.children 26 | scrollComponent = new ScrollComponent 27 | name: "scrollComponent" 28 | width: @width 29 | height: @height 30 | parent: view 31 | scrollComponent.content.backgroundColor = "" 32 | if view.width <= @width 33 | scrollComponent.scrollHorizontal = false 34 | if view.height <= @height 35 | scrollComponent.scrollVertical = false 36 | for c in children 37 | c.parent = scrollComponent.content 38 | view.scrollComponent = scrollComponent # make it accessible as a property 39 | # reset size since content moved to scrollComponent. prevents scroll bug when dragging outside. 40 | view.size = {width: @width, height: @height} 41 | 42 | transitions = 43 | switchInstant: {} 44 | fadeIn: 45 | newView: 46 | from: {opacity: 0} 47 | to: {opacity: 1} 48 | zoomIn: 49 | newView: 50 | from: {scale: 0.8, opacity: 0} 51 | to: {scale: 1, opacity: 1} 52 | zoomOut: 53 | oldView: 54 | to: {scale: 0.8, opacity: 0} 55 | newView: 56 | to: {} 57 | slideInUp: 58 | newView: 59 | from: {y: @height} 60 | to: {y: 0} 61 | slideInRight: 62 | newView: 63 | from: {x: @width} 64 | to: {x: 0} 65 | slideInDown: 66 | newView: 67 | from: {maxY: 0} 68 | to: {y: 0} 69 | slideInLeft: 70 | newView: 71 | from: {maxX: 0} 72 | to: {maxX: @width} 73 | moveInUp: 74 | oldView: 75 | to: {y: -@height} 76 | newView: 77 | from: {y: @height} 78 | to: {y: 0} 79 | moveInRight: 80 | oldView: 81 | to: {maxX: 0} 82 | newView: 83 | from: {x: @width} 84 | to: {x: 0} 85 | moveInDown: 86 | oldView: 87 | to: {y: @height} 88 | newView: 89 | from: {y: -@height} 90 | to: {y: 0} 91 | moveInLeft: 92 | oldView: 93 | to: {x: @width} 94 | newView: 95 | from: {maxX: 0} 96 | to: {x: 0} 97 | pushInRight: 98 | oldView: 99 | to: {x: -(@width/5), brightness: 70} 100 | newView: 101 | from: {x: @width} 102 | to: {x: 0} 103 | pushInLeft: 104 | oldView: 105 | to: {x: @width/5, brightness: 70} 106 | newView: 107 | from: {x: -@width} 108 | to: {x: 0} 109 | pushOutRight: 110 | oldView: 111 | to: {x: @width} 112 | newView: 113 | from: {x: -(@width/5), brightness: 70} 114 | to: {x: 0, brightness: 100} 115 | pushOutLeft: 116 | oldView: 117 | to: {maxX: 0} 118 | newView: 119 | from: {x: @width/5, brightness: 70} 120 | to: {x: 0, brightness: 100} 121 | slideOutUp: 122 | oldView: 123 | to: {maxY: 0} 124 | newView: 125 | to: {} 126 | slideOutRight: 127 | oldView: 128 | to: {x: @width} 129 | newView: 130 | to: {} 131 | slideOutDown: 132 | oldView: 133 | to: {y: @height} 134 | newView: 135 | to: {} 136 | slideOutLeft: 137 | oldView: 138 | to: {maxX: 0} 139 | newView: 140 | to: {} 141 | 142 | # shortcuts 143 | transitions.slideIn = transitions.slideInRight 144 | transitions.slideOut = transitions.slideOutRight 145 | transitions.pushIn = transitions.pushInRight 146 | transitions.pushOut = transitions.pushOutRight 147 | 148 | # events 149 | Events.ViewWillSwitch = "viewWillSwitch" 150 | Events.ViewDidSwitch = "viewDidSwitch" 151 | Layer::onViewWillSwitch = (cb) -> @on(Events.ViewWillSwitch, cb) 152 | Layer::onViewDidSwitch = (cb) -> @on(Events.ViewDidSwitch, cb) 153 | 154 | _.each transitions, (animProps, name) => 155 | 156 | if options.autoLink 157 | layers = Framer.CurrentContext._layers 158 | for btn in layers 159 | if _.includes btn.name, name 160 | viewController = @ 161 | btn.onClick -> 162 | anim = @name.split('_')[0] 163 | linkName = @name.replace(anim+'_','') 164 | linkName = linkName.replace(/\d+/g, '') # remove numbers 165 | viewController[anim] _.find(layers, (l) -> l.name is linkName) 166 | 167 | @[name] = (newView, animationOptions = @animationOptions) => 168 | 169 | return if newView is @currentView 170 | 171 | 172 | 173 | # make sure the new layer is inside the viewcontroller 174 | newView.parent = @ 175 | newView.sendToBack() 176 | 177 | # reset props in case they were changed by a prev animation 178 | newView.point = {x:0, y: 0} 179 | newView.opacity = 1 180 | newView.scale = 1 181 | newView.brightness = 100 182 | 183 | # oldView 184 | @currentView?.point = {x: 0, y: 0} # fixes offset issue when moving too fast between screens 185 | @currentView?.props = animProps.oldView?.from 186 | animObj = _.extend {properties: animProps.oldView?.to}, animationOptions 187 | _.defaults(animObj, { properties: {} }) 188 | outgoing = @currentView?.animate animObj 189 | 190 | # newView 191 | newView.props = animProps.newView?.from 192 | incoming = newView.animate _.extend {properties: animProps.newView?.to}, animationOptions 193 | 194 | # layer order 195 | if _.includes name, 'Out' 196 | newView.placeBehind(@currentView) 197 | outgoing.on Events.AnimationEnd, => @currentView.bringToFront() 198 | else 199 | newView.placeBefore(@currentView) 200 | 201 | @emit(Events.ViewWillSwitch, @currentView, newView) 202 | 203 | # change CurrentView before animation has finished so one could go back in history 204 | # without having to wait for the transition to finish 205 | @saveCurrentViewToHistory name, outgoing, incoming 206 | @currentView = newView 207 | @emit("change:previousView", @previousView) 208 | @emit("change:currentView", @currentView) 209 | 210 | if incoming.isAnimating 211 | hook = incoming 212 | else 213 | hook = outgoing 214 | hook?.on Events.AnimationEnd, => 215 | @emit(Events.ViewDidSwitch, @previousView, @currentView) 216 | 217 | 218 | if options.initialViewName? 219 | autoInitial = _.find Framer.CurrentContext._layers, (l) -> l.name is options.initialViewName 220 | if autoInitial? then @switchInstant autoInitial 221 | 222 | if options.initialView? 223 | @switchInstant options.initialView 224 | 225 | if options.backButtonName? 226 | backButtons = _.filter Framer.CurrentContext._layers, (l) -> _.includes l.name, options.backButtonName 227 | for btn in backButtons 228 | btn.onClick => @back() 229 | 230 | @define "previousView", 231 | get: -> @history[0].view 232 | 233 | saveCurrentViewToHistory: (name,outgoingAnimation,incomingAnimation) -> 234 | @history.unshift 235 | view: @currentView 236 | animationName: name 237 | incomingAnimation: incomingAnimation 238 | outgoingAnimation: outgoingAnimation 239 | 240 | back: -> 241 | previous = @history[0] 242 | if previous.view? 243 | 244 | if _.includes previous.animationName, 'Out' 245 | previous.view.bringToFront() 246 | 247 | backIn = previous.outgoingAnimation.reverse() 248 | moveOut = previous.incomingAnimation.reverse() 249 | 250 | backIn.start() 251 | moveOut.start() 252 | 253 | @currentView = previous.view 254 | @history.shift() 255 | moveOut.on Events.AnimationEnd, => @currentView.bringToFront() 256 | -------------------------------------------------------------------------------- /ViewController.sketchplugin/Contents/Sketch/createBackBtn.cocoascript: -------------------------------------------------------------------------------- 1 | var onRun = function(context) { 2 | for (var i=0; i < context.selection.count(); i++){ 3 | var item = context.selection[i] 4 | item.setName("backButton"); 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /ViewController.sketchplugin/Contents/Sketch/createLink.cocoascript: -------------------------------------------------------------------------------- 1 | var onRun = function(context) { 2 | 3 | function createSelect(first,second,selectIndex){ 4 | var options = ['Switch instant ⚡️','Fade in 👻','Zoom in 👀','Zoom out 👀','Slide in up ⬆️','Slide in right ⬅️','Slide in down ⬇️','Slide in left ➡️','Slide out up ⬆️','Slide out right ➡️','Slide out down ⬇️','Slide out left ⬅️','Push in right ⏪','Push in left ⏩','Push out right ⏩','Push out left ⏪','Move in up ⏫','Move in right ⏪','Move in down ⏬','Move in left ⏩'] 5 | var keywords = ['switchInstant','fadeIn','zoomIn','zoomOut','slideInUp','slideInRight','slideInDown','slideInLeft','slideOutUp','slideOutRight','slideOutDown','slideOutLeft','pushInRight','pushInLeft','pushOutRight','pushOutLeft','moveInUp','moveInRight','moveInDown','moveInLeft'] 6 | var accessory = NSComboBox.alloc().initWithFrame(NSMakeRect(0,0,200,25)) 7 | accessory.addItemsWithObjectValues(options) 8 | accessory.selectItemAtIndex(selectIndex) // pre-select first item 9 | 10 | var button = first 11 | var view = second 12 | 13 | var alert = NSAlert.alloc().init() 14 | alert.setMessageText('Choose a transition') 15 | alert.setInformativeText('"'+button.name()+'" to "'+view.name()+'"') 16 | 17 | alert.addButtonWithTitle('Create link') // 1000 18 | alert.addButtonWithTitle('Cancel') // 1001 19 | alert.addButtonWithTitle('Reverse ↔') // 1002 20 | alert.setAccessoryView(accessory) 21 | 22 | var responseCode = alert.runModal() 23 | var sel = accessory.indexOfSelectedItem() 24 | 25 | if (responseCode == 1000){ 26 | context.document.showMessage("ViewController: Renamed 1 layer. Open Framer to import the changes") 27 | button.setName(keywords[sel]+"_"+view.name()); 28 | } else if (responseCode == 1002) { 29 | createSelect(view,button,sel) 30 | } 31 | 32 | } 33 | 34 | function selectArtboard(first){ 35 | var artboards = context.document.currentPage().artboards() 36 | var artboardNames = [] 37 | if (artboards.count() == 0){ 38 | context.document.showMessage("ViewController: Add an artboard to your document before trying to create a link") 39 | } 40 | for(var i=0; i < artboards.count(); i++){ 41 | artboardNames.push(artboards[i].name()) 42 | } 43 | 44 | var accessory = NSComboBox.alloc().initWithFrame(NSMakeRect(0,0,200,25)) 45 | accessory.addItemsWithObjectValues(artboardNames) 46 | accessory.selectItemAtIndex(0) // pre-select first item 47 | 48 | var button = first 49 | 50 | var alert = NSAlert.alloc().init() 51 | 52 | alert.setMessageText('Choose an artboard') 53 | alert.setInformativeText('Where should '+button.name()+' link to?') 54 | 55 | alert.addButtonWithTitle('Select Artboard') // 1000 56 | alert.addButtonWithTitle('Cancel') // 1001 57 | alert.setAccessoryView(accessory) 58 | 59 | var responseCode = alert.runModal() 60 | var sel = accessory.indexOfSelectedItem() 61 | 62 | if (responseCode == 1000){ 63 | createSelect(button,artboards[sel],0) 64 | } 65 | 66 | } 67 | 68 | if (context.selection.count() == 2){ 69 | var one = context.selection[0] 70 | var second = context.selection[1] 71 | if (second.class() == "MSArtboardGroup"){ 72 | createSelect(one,second,0) 73 | } else { 74 | createSelect(second,one,0) 75 | } 76 | } else if (context.selection.count() == 1) { 77 | var button = context.selection[0]; 78 | selectArtboard(button); 79 | } else { 80 | context.document.showMessage("ViewController: You need to select at least one layer") 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /ViewController.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author" : "Andreas", 3 | "commands" : [ 4 | { 5 | "script" : "createLink.cocoascript", 6 | "handler" : "onRun", 7 | "shortcut" : "", 8 | "name" : "Create a link", 9 | "identifier" : "createLink" 10 | }, 11 | { 12 | "script" : "createBackBtn.cocoascript", 13 | "handler" : "onRun", 14 | "shortcut" : "", 15 | "name" : "Create back button", 16 | "identifier" : "createBackBtn" 17 | } 18 | ], 19 | "menu" : { 20 | "items" : [ 21 | "createLink", 22 | "createBackBtn" 23 | ], 24 | "title" : "ViewController" 25 | }, 26 | "identifier" : "com.example.sketch.156fcaaf-4d3d-4534-9c0d-e89f33efc0a5", 27 | "version" : "1.0", 28 | "description" : "", 29 | "authorEmail" : "", 30 | "name" : "ViewController" 31 | } --------------------------------------------------------------------------------