├── 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 | 
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() ||
33 | | .slideInUp() ||
34 | | .slideInRight() ||
35 | | .slideInDown() ||
36 | | .slideInLeft() ||
37 | | .slideOutUp() ||
38 | | .slideOutRight() ||
39 | | .slideOutDown() ||
40 | | .slideOutLeft() ||
41 | | .moveInRight() ||
42 | | .moveInLeft() ||
43 | | .moveInUp() ||
44 | | .moveInDown() ||
45 | | .pushInRight() ||
46 | | .pushInLeft() ||
47 | | .pushOutRight() ||
48 | | .pushOutLeft() ||
49 | | .fadeIn() ||
50 | | .zoomIn() ||
51 | | .zoomOut() ||
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 | 
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 | }
--------------------------------------------------------------------------------