├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── Rules.md ├── UIKitExport.sketchplugin └── Contents │ └── Sketch │ ├── SteviaExport.js │ ├── UIKitExport.js │ └── manifest.json ├── demo.gif └── example.sketch /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sachadso@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 S4cha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SketchToSwift 2 | 3 | ![Demo](https://raw.githubusercontent.com/s4cha/SketchToSwift/master/demo.gif) 4 | 5 | 6 | ![Language: Javascript](https://img.shields.io/badge/language-javascript-f48041.svg?style=flat) 7 | [![License: MIT](http://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)](https://github.com/s4cha/SketchToSwift/blob/master/LICENSE) 8 | [![Release version](https://img.shields.io/badge/release-0.1-blue.svg)]() 9 | 10 | ## Why 11 | Because **integrating views** form Sketch has **no added value**, its just back and forth translating sketch values to swift code. There must be a way to automate this time costly and low added value labor. 12 | 13 | ## How 14 | By **generating the swift UIKit code** from a Sketch Artboard. We can save hours of time \o/ 15 | 16 | ## What 17 | A **Sketch plugin** with a simple shortcut that generates basic Swift code form wich you can kickstart developing your view. 18 | 19 | 20 | ## About 21 | 22 | This is a proof of concept that we can generate UIVIew swift code from Sketch Artboards. 23 | Be aware that this is very early in development. 24 | 25 | Yes this will never be perfect (huh wait for AI!), because we all have our coding styles etc. But this is not the goal anyway, the goal is to kickstart view dev and save 80% of integration time. Then you are free to change the code as you like :) 26 | 27 | 28 | ## Usage 29 | - Install (double click) plugin 30 | - Select Artboard 31 | - `cmd` + `alt` + `K` for UIKit code 32 | - `cmd` + `alt` + `L` for [Stevia](https://github.com/s4cha/Stevia) code 33 | - Paste swift Uikit UIView subclass in Xcode \o/ 34 | 35 | ## Get the best out of it 36 | 37 | Here are some rules you can follow to get the best export possible : 38 | 39 | - Name your Artboard proprely: 40 | For instance an Artboard named "Profile" will generate `class ProfileView: UIView ` 41 | - Resize Artboard for your iPhone size (ex: we usually use 375Width for iPhone 7) 42 | By resizing the artboard with the scale tool, you'll make sure the script will generate the good font sizes etc. 43 | - Name your sketch layers right in order to get sexy variable names 44 | - After the scale, you probably have float margins and font size. Making sure these are clean ext 16p instead of 16,01543 will generate cleaner code :) 45 | - The same Applies to margins and sizes in general 46 | - Flatten the view hierarchy, extract the items inside groups and bring them to the top level. 47 | - Order them (usually from the to to the bottom), the generated code will keep the view hierarchy order. 48 | 49 | 50 | ## Improvements 51 | 52 | #### Fonts 53 | - Find a way to export fonts ? 54 | 55 | #### Text 56 | - Detect Text in style caps -> apply uppercased 57 | - Detect character spacing in fonts -> AttributedString 58 | - Detech line spacing -> AttributedString 59 | - Detect Multiline (contains \n) -> Append \n and set label to mutiline 60 | - Detect text alignment all cases 61 | - Multicolor -> Generate AttributedString 62 | 63 | #### Colors 64 | - UIColors replace by custom ones? 65 | - UIColors oftenThe same refactor in one variable 66 | 67 | #### Radius 68 | - Detect UIButton corner Radius 69 | 70 | #### Borders 71 | - Button border color 72 | - Button border radius 73 | 74 | #### Sketch Groups 75 | - groups -> greate UIViewSubclasses? 76 | - UIButton set title belongs in content section 77 | 78 | #### layout 79 | - Detect fillscontainer and do not use width and height but right and bottom laike a human would do 80 | - Implement relative layout, like more visually to the right -> align on right rather tahn left , more natural 81 | 82 | #### Export 83 | - Handle single Element Export 84 | 85 | #### UITableViewCell 86 | - Artboard name Suffixed by `Cell` -> UITableViewCell subclass 87 | 88 | #### Images 89 | - Find a wa to detect images and create UIImageView instead 90 | 91 | #### Groups 92 | - Think about how to handle them proprely 93 | 94 | ## Known issues 95 | This plugin only supports Sketch v41 and higher at the moment. 96 | 97 | ## Issues 98 | Have a layout that breaks the plugin? 99 | Send it to sachadso@gmail.com for review + open Issue 100 | 101 | ## Author 102 | 103 | Sacha Durand Saint Omer, sachadso@gmail.com 104 | 105 | ## Contributing 106 | 107 | Contributions to SketchToSwift are very welcomed and encouraged! Feel free to try and tackle one of the Improvements above 108 | 109 | ## License 110 | 111 | SketchToSwift is available under the MIT license. See [LICENSE](https://github.com/s4cha/SketchToSwift/blob/master/LICENSE) for more information. 112 | -------------------------------------------------------------------------------- /Rules.md: -------------------------------------------------------------------------------- 1 | Rules 2 | 3 | - Name your Artboard this will be the name of your view 4 | - Resize Artboard for your iphhone size ex 375 for iphone 7 5 | - Name your sketch layer right so taht good variable names are created 6 | - Exact font size, its celaner 7 | - margins exact size (eg 20) 8 | 9 | Everything in root vc 10 | 11 | Button 12 | -- 13 | is a text inside a rectangle 14 | -------------------------------------------------------------------------------- /UIKitExport.sketchplugin/Contents/Sketch/SteviaExport.js: -------------------------------------------------------------------------------- 1 | function onRun(context) { 2 | 3 | var selection = context.api().selectedDocument.selectedLayers 4 | var labels = [] 5 | var buttons = [] 6 | var shapes = [] 7 | var allElements = [] 8 | var lines = [] 9 | 10 | var artboardName = "DefaultViewName" 11 | var artboardBackgroundColor = nil 12 | 13 | selection.iterate(function(item) { 14 | if (item.isArtboard) { 15 | artboardName = removeSpaces(item.name) + "View" 16 | artboardBackgroundColor = item.sketchObject.backgroundColor() 17 | item.iterate(function(element) { 18 | if (element.isText) { 19 | labels.push(element) 20 | } else if (element.isShape) { 21 | shapes.push(element) 22 | } else if (element.isGroup) { 23 | // buttons? 24 | buttons.push(element) 25 | } 26 | allElements.push(element) 27 | }) 28 | } 29 | }); 30 | 31 | allElements = allElements.reverse() 32 | labels = labels.reverse() 33 | shapes = shapes.reverse() 34 | buttons = buttons.reverse() 35 | 36 | // Header 37 | write(steviaHeader(artboardName)) 38 | 39 | // Declarations 40 | write(uikitDeclarationsFor(allElements)) 41 | 42 | // Init 43 | write(uiviewInit()) 44 | 45 | // View Hierarchy 46 | write(steviaViewHierarchy(allElements)) 47 | 48 | // Layout 49 | write(steviaLayout(allElements)) 50 | write("") 51 | 52 | // Artboard Background Color 53 | var red = artboardBackgroundColor.red() 54 | var green = artboardBackgroundColor.green() 55 | var blue = artboardBackgroundColor.blue() 56 | var alpha = artboardBackgroundColor.alpha() 57 | write(" backgroundColor = UIColor(red: " + red + ", green: " + green + ", blue: " + blue + ", alpha: " + alpha + ")") 58 | 59 | // Style 60 | write(steviaStyleLabels(labels)) 61 | write(steviaStyleShapes(shapes)) 62 | 63 | // Buttons 64 | buttons.map(function(v) { 65 | var elementName = sanitizeName(v.name) 66 | v.iterate(function(item) { 67 | if (item.isText) { 68 | write(" " + elementName + '.setTitle("' + item.text + '",for: .normal)') 69 | //Title color 70 | var color = item.sketchObject.textColor() 71 | var red = color.red() 72 | var green = color.green() 73 | var blue = color.blue() 74 | var alpha = color.alpha() 75 | write(" " + elementName + ".setTitleColor(UIColor(red: " + red + ", green: " + green + ", blue: " + blue + ", alpha: " + alpha + "), for: . normal)") 76 | 77 | // Button font 78 | var fontName = item.sketchObject.fontPostscriptName() 79 | var fontSize = item.sketchObject.fontSize() 80 | if (fontName == "SFUIText-Semibold") { 81 | write(" " + elementName + '.titleLabel?.font = .systemFont(ofSize: ' + fontSize + ", weight: UIFontWeightSemibold)") 82 | } else if (fontName == "SFUIText-Regular") { 83 | write(" " + elementName + '.titleLabel?.font = .systemFont(ofSize: ' + fontSize + ")") 84 | } else { 85 | write(" " + elementName + '.titleLabel?.font = UIFont(name: "' + fontName + '", size:' + fontSize + ")") 86 | } 87 | } else if (item.isShape) { 88 | var color = item.sketchObject.style().fills()[0].color() 89 | var red = color.red() 90 | var green = color.green() 91 | var blue = color.blue() 92 | var alpha = color.alpha() 93 | write(" " + elementName + ".backgroundColor = UIColor(red: " + red + ", green: " + green + ", blue: " + blue + ", alpha: " + alpha + ")") 94 | } 95 | }); 96 | write("") 97 | }); 98 | 99 | 100 | // Content 101 | write(uikitContentForLabels(labels)) 102 | 103 | 104 | // Print + copy 105 | var fullText = "" 106 | lines.forEach(function(line) { 107 | fullText += line + "\n" 108 | }) 109 | log(fullText) 110 | copyText(fullText) 111 | 112 | 113 | function write(text) { 114 | lines.push(text) 115 | } 116 | }; 117 | 118 | function steviaHeader(viewName) { 119 | return ` 120 | import Stevia 121 | 122 | 123 | class ${viewName} : UIView { 124 | ` 125 | } 126 | 127 | function uikitDeclarationsFor(elements) { 128 | var s = "" 129 | elements.map(function(e) { 130 | if (e.isText) { 131 | s += " let " + sanitizeName(e.name) + " = UILabel()" + "\n" 132 | } else if (e.isShape) { 133 | s += " let " + sanitizeName(e.name) + " = UIView()" + "\n" 134 | } else if (e.isGroup) { 135 | // Only if contains button 136 | s += " let " + sanitizeName(e.name) + " = UIButton()" + "\n" 137 | } 138 | }); 139 | return s 140 | } 141 | 142 | function uiviewInit() { 143 | return ` override init(frame: CGRect) { 144 | super.init(frame: frame) 145 | commonInit() 146 | } 147 | 148 | required init?(coder aDecoder: NSCoder) { 149 | super.init(coder: aDecoder) 150 | commonInit() 151 | } 152 | 153 | private func commonInit() { 154 | ` 155 | } 156 | 157 | function steviaViewHierarchy(elements) { 158 | var s = " // View Hierarchy" + "\n" 159 | s += " sv(" + "\n" 160 | var count = 0 161 | elements.reverse().map(function(e) { 162 | if (count == elements.length -1) { 163 | s += " " + sanitizeName(e.name) + "\n" 164 | } else { 165 | s += " " + sanitizeName(e.name) + "," + "\n" 166 | } 167 | count++ 168 | }); 169 | s += " )" + "\n" 170 | return s 171 | } 172 | 173 | function steviaLayout(elements) { 174 | var s = "\n" 175 | s += " // Layout" + "\n" 176 | elements.map(function(e) { 177 | var elementName = sanitizeName(e.name) 178 | s += " " + sanitizeName(e.name) + ".top(" + e.frame.y + ")" + "\n" 179 | s += " .left(" + e.frame.x + ")" + "\n" 180 | s += " .width(" + e.frame.width + ")" + "\n" 181 | s += " .height(" + e.frame.height + ")" + "\n" 182 | }); 183 | return s 184 | } 185 | 186 | function steviaStyleLabels(labels) { 187 | var s = "\n" 188 | s += " // Style" + "\n" 189 | labels.map(function(l) { 190 | var elementName = sanitizeName(l.name) 191 | var red = l.sketchObject.textColor().red() 192 | var green = l.sketchObject.textColor().green() 193 | var blue = l.sketchObject.textColor().blue() 194 | var alpha = l.sketchObject.textColor().alpha() 195 | 196 | // Special case for native fonts 197 | var fontName = l.sketchObject.fontPostscriptName() 198 | var fontSize = l.sketchObject.fontSize() 199 | 200 | if (fontName == "SFUIText-Semibold") { 201 | s += " " + elementName + '.font = .systemFont(ofSize: ' + fontSize + ", weight: UIFontWeightSemibold)" + "\n" 202 | } else if (fontName == "SFUIText-Regular") { 203 | s += " " + elementName + '.font = .systemFont(ofSize: ' + fontSize + ")" + "\n" 204 | } else if (fontName == "SFUIText-Italic") { 205 | s += " " + elementName + '.font = .italicSystemFont(ofSize: ' + fontSize + ")" + "\n" 206 | } else if (fontName == "SFUIText-Light") { 207 | s += " " + elementName + '.font = .systemFont(ofSize: ' + fontSize + ", weight: UIFontWeightLight)" + "\n" 208 | } else if (fontName == "SFUIText-Heavy") { 209 | s += " " + elementName + '.font = .systemFont(ofSize: ' + fontSize + ", weight: UIFontWeightHeavy)" + "\n" 210 | } else if (fontName == "SFUIText-Bold") { 211 | s += " " + elementName + '.font = .systemFont(ofSize: ' + fontSize + ", weight: UIFontWeightBold)" + "\n" 212 | } else if (fontName == "SFUIText-Medium") { 213 | s += " " + elementName + '.font = .systemFont(ofSize: ' + fontSize + ", weight: UIFontWeightMedium)" + "\n" 214 | } else if (fontName == "SFUIText-LightItalic") { 215 | var fontName = elementName + "Font" 216 | var fontDescriptorName = elementName + "Descriptor" 217 | s += " var " + fontName + ": UIFont = .systemFont(ofSize:" + fontSize + ", weight: UIFontWeightLight)" + "\n" 218 | s += " let " + fontDescriptorName + " = " + fontName + ".fontDescriptor.withSymbolicTraits(.traitItalic)" + "\n" 219 | s += " " + fontName + " = UIFont(descriptor: " + fontDescriptorName + "!, size: 0)" + "\n" 220 | s += " " + elementName + ".font = " + fontName + "\n" 221 | } else if (fontName == "SFUIText-MediumItalic") { 222 | var fontName = elementName + "Font" 223 | var fontDescriptorName = elementName + "Descriptor" 224 | s += " var " + fontName + ": UIFont = .systemFont(ofSize:" + fontSize + ", weight: UIFontWeightMedium)" + "\n" 225 | s += " let " + fontDescriptorName + " = " + fontName + ".fontDescriptor.withSymbolicTraits(.traitItalic)" + "\n" 226 | s += " " + fontName + " = UIFont(descriptor: " + fontDescriptorName + "!, size: 0)" + "\n" 227 | s += " " + elementName + ".font = " + fontName + "\n" 228 | } else if (fontName == "SFUIText-SemiboldItalic") { 229 | var fontName = elementName + "Font" 230 | var fontDescriptorName = elementName + "Descriptor" 231 | s += " var " + fontName + ": UIFont = .systemFont(ofSize:" + fontSize + ", weight: UIFontWeightSemibold)" + "\n" 232 | s += " let " + fontDescriptorName + " = " + fontName + ".fontDescriptor.withSymbolicTraits(.traitItalic)" + "\n" 233 | s += " " + fontName + " = UIFont(descriptor: " + fontDescriptorName + "!, size: 0)" + "\n" 234 | s += " " + elementName + ".font = " + fontName + "\n" 235 | } else if (fontName == "SFUIText-BoldItalic") { 236 | var fontName = elementName + "Font" 237 | var fontDescriptorName = elementName + "Descriptor" 238 | s += " var " + fontName + ": UIFont = .systemFont(ofSize:" + fontSize + ")" + "\n" 239 | s += " let " + fontDescriptorName + " = " + fontName + ".fontDescriptor.withSymbolicTraits([.traitItalic, .traitBold])" + "\n" 240 | s += " " + fontName + " = UIFont(descriptor: " + fontDescriptorName + "!, size: 0)" + "\n" 241 | s += " " + elementName + ".font = " + fontName + "\n" 242 | } 243 | else if (fontName == "SFUIText-HeavyItalic") { 244 | var fontName = elementName + "Font" 245 | var fontDescriptorName = elementName + "Descriptor" 246 | s += " var " + fontName + ": UIFont = .systemFont(ofSize:" + fontSize + ", weight: UIFontWeightHeavy)" + "\n" 247 | s += " let " + fontDescriptorName + " = " + fontName + ".fontDescriptor.withSymbolicTraits(.traitItalic)" + "\n" 248 | s += " " + fontName + " = UIFont(descriptor: " + fontDescriptorName + "!, size: 0)" + "\n" 249 | s += " " + elementName + ".font = "+ fontName + "\n" 250 | } else { 251 | s += " " + elementName + '.font = UIFont(name: "' + fontName + '", size:' + fontSize + ")" + "\n" 252 | } 253 | 254 | s += " " + elementName + ".textColor = UIColor(red: " + red + ", green: " + green + ", blue: " + blue + ", alpha: " + alpha + ")" + "\n" 255 | if (l.alignment == "center") { 256 | s += " " + elementName + ".textAlignment = .center" + "\n" 257 | } 258 | s += " " + elementName + ".numberOfLines = 0" + "\n" 259 | }); 260 | return s 261 | } 262 | 263 | function steviaStyleShapes(shapes) { 264 | var s = "" 265 | shapes.map(function(v) { 266 | var elementName = sanitizeName(v.name) 267 | var color = v.sketchObject.style().fills()[0].color() 268 | var red = color.red() 269 | var green = color.green() 270 | var blue = color.blue() 271 | var alpha = color.alpha() 272 | s += " " + elementName + ".backgroundColor = UIColor(red: " + red + ", green: " + green + ", blue: " + blue + ", alpha: " + alpha + ")" + "\n" 273 | }); 274 | return s 275 | } 276 | 277 | 278 | function uikitContentForLabels(labels) { 279 | var s = "\n" 280 | s += " // Content" + "\n" 281 | labels.map(function(l) { 282 | s += " " + sanitizeName(l.name) + '.text = "' + l.text + '"' + "\n" 283 | }); 284 | s += " }" + "\n" 285 | s += "}" 286 | return s 287 | } 288 | 289 | function copyText(text) { 290 | var pasteBoard = [NSPasteboard generalPasteboard]; 291 | [pasteBoard declareTypes: [NSArray arrayWithObject: NSPasteboardTypeString] owner: nil]; 292 | [pasteBoard setString: text forType: NSPasteboardTypeString]; 293 | } 294 | 295 | function sanitizeName(str) { 296 | return lowerCaseFirstLetter(removeSpaces(str)) 297 | } 298 | 299 | function lowerCaseFirstLetter(string) { 300 | return string.charAt(0).toLowerCase() + string.slice(1); 301 | } 302 | 303 | function removeSpaces(str) { 304 | return str.replace(/\s+/g, ''); 305 | } 306 | -------------------------------------------------------------------------------- /UIKitExport.sketchplugin/Contents/Sketch/UIKitExport.js: -------------------------------------------------------------------------------- 1 | function onRun(context) { // Comment to launch script in Sketch "Custom Script" 2 | 3 | var isArtboardSelected = false 4 | var selection = context.api().selectedDocument.selectedLayers 5 | 6 | selection.iterate(function(item) { 7 | if (item.isArtboard) { 8 | isArtboardSelected = true 9 | } 10 | }); 11 | 12 | if (isArtboardSelected) { 13 | parseArtboard(context) 14 | } else { 15 | parseSingleElement(context) 16 | } 17 | }; // Comment to launch script in Sketch "Custom Script" 18 | 19 | 20 | function parseSingleElement(context) { 21 | var lines = [] 22 | var selection = context.api().selectedDocument.selectedLayers 23 | selection.iterate(function(item) { 24 | if (item.isText) { 25 | write(uikitDeclarationsForSingleText(item)) 26 | write(`${sanitizeName(item.name)}.text = ${formattedTextForText(item.text)}`) 27 | write(uikitStyleForText(item)) 28 | } 29 | }); 30 | 31 | // Print + copy 32 | var fullText = "" 33 | lines.forEach(function(line) { 34 | fullText += line + "\n" 35 | }) 36 | log(fullText) 37 | copyText(fullText) 38 | 39 | 40 | function write(text) { 41 | lines.push(text) 42 | } 43 | 44 | } 45 | 46 | function formattedTextForText(t) { 47 | if (t == t.toUpperCase()) { 48 | return `"${capitalizedCase(t.toLowerCase())}".uppercased()` 49 | } else { 50 | return `"${t}"` 51 | } 52 | } 53 | 54 | function capitalizedCase(string) { 55 | return string.charAt(0).toUpperCase() + string.slice(1); 56 | } 57 | 58 | 59 | function parseArtboard(context) { 60 | 61 | var selection = context.api().selectedDocument.selectedLayers 62 | var labels = [] 63 | var buttons = [] 64 | var shapes = [] 65 | var allElements = [] 66 | var lines = [] 67 | 68 | var artboardName = "DefaultViewName" 69 | var artboardBackgroundColor = nil 70 | 71 | selection.iterate(function(item) { 72 | if (item.isArtboard) { 73 | artboardName = `${removeSpaces(item.name)}View` 74 | artboardBackgroundColor = item.sketchObject.backgroundColor() 75 | item.iterate(function(element) { 76 | if (element.isText) { 77 | labels.push(element) 78 | } else if (element.isShape) { 79 | shapes.push(element) 80 | } else if (element.isGroup) { 81 | // buttons? 82 | buttons.push(element) 83 | } 84 | allElements.push(element) 85 | }) 86 | } 87 | }); 88 | 89 | 90 | allElements = allElements.reverse() 91 | labels = labels.reverse() 92 | shapes = shapes.reverse() 93 | buttons = buttons.reverse() 94 | 95 | 96 | // Header 97 | write(uikitHeader(artboardName)) 98 | write("") 99 | 100 | // Declarations 101 | write(uikitDeclarationsFor(allElements)) 102 | 103 | // Init 104 | write(uiviewInit()) 105 | 106 | // View Hierarchy 107 | write("") 108 | write(uikitViewHierarchy(allElements)) 109 | 110 | // Layout 111 | write(uikitLayout(allElements)) 112 | write("") 113 | 114 | // Style 115 | 116 | // Artboard Background Color 117 | write(` backgroundColor = ${uiColorLineForColor(artboardBackgroundColor)}`) 118 | 119 | 120 | labels.map(function(l) { 121 | write(uikitStyleForText(l)) 122 | }); 123 | shapes.map(function(v) { 124 | var elementName = sanitizeName(v.name) 125 | var color = v.sketchObject.style().fills()[0].color() 126 | write(` ${elementName}.backgroundColor = ${uiColorLineForColor(color)}`) 127 | write("") 128 | }); 129 | 130 | 131 | // Buttons 132 | buttons.map(function(v) { 133 | var elementName = sanitizeName(v.name) 134 | v.iterate(function(item) { 135 | if (item.isText) { 136 | write(` ${elementName}.setTitle(${formattedTextForText(item.text)},for: .normal)`) 137 | //Title color 138 | var color = item.sketchObject.textColor() 139 | write(` ${elementName}.setTitleColor(${uiColorLineForColor(color)}, for: . normal)`) 140 | 141 | // Button font 142 | var fontName = item.sketchObject.fontPostscriptName() 143 | var fontSize = item.sketchObject.fontSize() 144 | if (fontName == "SFUIText-Semibold") { 145 | write(` ${elementName}.titleLabel?.font = .systemFont(ofSize: ${fontSize}, weight: UIFontWeightSemibold)`) 146 | } else if (fontName == "SFUIText-Regular") { 147 | write(` ${elementName}.titleLabel?.font = .systemFont(ofSize: ${fontSize})`) 148 | } else { 149 | write(` ${elementName}.titleLabel?.font = UIFont(name: "${fontName}", size:${fontSize})`) 150 | } 151 | } else if (item.isShape) { 152 | var color = item.sketchObject.style().fills()[0].color() 153 | write(` ${elementName}.backgroundColor = ${uiColorLineForColor(color)}`) 154 | } 155 | }); 156 | write("") 157 | }); 158 | 159 | 160 | // Content 161 | write(uikitContentForLabels(labels)) 162 | 163 | 164 | // Print + copy 165 | var fullText = "" 166 | lines.forEach(function(line) { 167 | fullText += line + "\n" 168 | }) 169 | log(fullText) 170 | copyText(fullText) 171 | 172 | 173 | function write(text) { 174 | lines.push(text) 175 | } 176 | } 177 | 178 | function uikitStyleForText(l) { 179 | var s = "" 180 | var elementName = sanitizeName(l.name) 181 | 182 | // Special case for native fonts 183 | var fontName = l.sketchObject.fontPostscriptName() 184 | var fontSize = l.sketchObject.fontSize() 185 | 186 | if (fontName == "SFUIText-Semibold") { 187 | s += `${elementName}.font = .systemFont(ofSize: ${fontSize}, weight: UIFontWeightSemibold)` 188 | } else if (fontName == "SFUIText-Regular") { 189 | s += `${elementName}.font = .systemFont(ofSize: ${fontSize})` 190 | } else if (fontName == "SFUIText-Italic") { 191 | s += `${elementName}.font = .italicSystemFont(ofSize: ${fontSize})` 192 | } else if (fontName == "SFUIText-Light") { 193 | s += `${elementName}.font = .systemFont(ofSize: ${fontSize}, weight: UIFontWeightLight)` 194 | } else if (fontName == "SFUIText-Heavy") { 195 | s += `${elementName}.font = .systemFont(ofSize: ${fontSize}, weight: UIFontWeightHeavy)` 196 | } else if (fontName == "SFUIText-Bold") { 197 | s += `${elementName}.font = .systemFont(ofSize: ${fontSize}, weight: UIFontWeightBold)` 198 | } else if (fontName == "SFUIText-Medium") { 199 | s += `${elementName}.font = .systemFont(ofSize: ${fontSize}, weight: UIFontWeightMedium)` 200 | } else if (fontName == "SFUIText-LightItalic") { 201 | var fontName = `${elementName}Font` 202 | var fontDescriptorName = `${elementName}Descriptor` 203 | s += `var ${fontName}: UIFont = .systemFont(ofSize:${fontSize}, weight: UIFontWeightLight)` 204 | s += `let ${fontDescriptorName} = ${fontName}.fontDescriptor.withSymbolicTraits(.traitItalic)` 205 | s += `${fontName} = UIFont(descriptor: ${fontDescriptorName}!, size: 0)` 206 | s += `${elementName}.font = ${fontName}` 207 | } else if (fontName == "SFUIText-MediumItalic") { 208 | var fontName = `${elementName}Font` 209 | var fontDescriptorName = `${elementName}Descriptor` 210 | s += `var ${fontName}: UIFont = .systemFont(ofSize:${fontSize}, weight: UIFontWeightMedium)` 211 | s += "\n" 212 | s += `let ${fontDescriptorName} = ${fontName}.fontDescriptor.withSymbolicTraits(.traitItalic)` 213 | s += "\n" 214 | s += `${fontName} = UIFont(descriptor: ${fontDescriptorName}!, size: 0)` 215 | s += "\n" 216 | s += `${elementName}.font = ${fontName}` 217 | } else if (fontName == "SFUIText-SemiboldItalic") { 218 | var fontName = `${elementName}Font` 219 | var fontDescriptorName = `${elementName}Descriptor` 220 | s += `var ${fontName}: UIFont = .systemFont(ofSize:${fontSize}, weight: UIFontWeightSemibold)` 221 | s += "\n" 222 | s += `let ${fontDescriptorName} = ${fontName}.fontDescriptor.withSymbolicTraits(.traitItalic)` 223 | s += "\n" 224 | s += `${fontName} = UIFont(descriptor: ${fontDescriptorName}!, size: 0)` 225 | s += "\n" 226 | s += `${elementName}.font = ${fontName}` 227 | } else if (fontName == "SFUIText-BoldItalic") { 228 | var fontName = `${elementName}Font` 229 | var fontDescriptorName = `${elementName}Descriptor` 230 | s += `var ${fontName}: UIFont = .systemFont(ofSize:${fontSize})` 231 | s += "\n" 232 | s += `let ${fontDescriptorName} = ${fontName}.fontDescriptor.withSymbolicTraits([.traitItalic, .traitBold])` 233 | s += "\n" 234 | s += `${fontName} = UIFont(descriptor: ${fontDescriptorName}!, size: 0)` 235 | s += "\n" 236 | s += `${elementName}.font = ${fontName}` 237 | } else if (fontName == "SFUIText-HeavyItalic") { 238 | var fontName = `${elementName}Font` 239 | var fontDescriptorName = `${elementName}Descriptor` 240 | s += `var ${fontName}: UIFont = .systemFont(ofSize:${fontSize}, weight: UIFontWeightHeavy)` 241 | s += "\n" 242 | s += `let ${fontDescriptorName} = ${fontName}.fontDescriptor.withSymbolicTraits(.traitItalic)` 243 | s += "\n" 244 | s += `${fontName} = UIFont(descriptor: ${fontDescriptorName}!, size: 0)` 245 | s += "\n" 246 | s += `${elementName}.font = ${fontName}` 247 | } else { 248 | s += `${elementName}.font = UIFont(name: "${fontName}", size:${fontSize})` 249 | } 250 | s += "\n" 251 | s += uicolorforText(l) 252 | s += "\n" 253 | if (l.alignment == "center") { 254 | s += `${elementName}.textAlignment = .center` 255 | s += "\n" 256 | } 257 | s += `${elementName}.numberOfLines = 0` 258 | s += "" 259 | return s 260 | } 261 | 262 | function uicolorforText(text) { 263 | return `${sanitizeName(text.name)}.textColor = ${uiColorLineForColor(text.sketchObject.textColor())}` 264 | } 265 | 266 | function uiColorLineForColor(color) { 267 | 268 | let colorMap = { 269 | "1111": "white", 270 | "0001": "black", 271 | "1001": "red", 272 | "0101": "green", 273 | "0011": "blue", 274 | "0111": "cyan", 275 | "1011": "magenta", 276 | "10.501": "orange", 277 | "0.500.51": "purple", 278 | "0.60.40.21": "brown", 279 | "1101": "yellow", 280 | "0.50.50.51": "gray", 281 | "0.670.670.671": "lightGray", 282 | "0.330.330.331": "darkGray", 283 | "0000": "clear" 284 | } 285 | 286 | var red = Math.round(color.red() * 100) / 100 287 | var green = Math.round(color.green() * 100) / 100 288 | var blue = Math.round(color.blue() * 100) / 100 289 | var alpha = Math.round(color.alpha() * 100) / 100 290 | let colorKey = `${red}${green}${blue}${alpha}` 291 | var color = colorMap[colorKey] 292 | if (color != undefined) { 293 | return `.${color}` 294 | } 295 | 296 | return `UIColor(red: ${red}, green: ${green}, blue: ${blue}, alpha: ${alpha})` 297 | } 298 | 299 | 300 | function uikitHeader(viewName) { 301 | return `import UIKit 302 | 303 | 304 | class ${viewName} : UIView {` 305 | } 306 | 307 | function uiviewInit() { 308 | return ` override init(frame: CGRect) { 309 | super.init(frame: frame) 310 | commonInit() 311 | } 312 | 313 | required init?(coder aDecoder: NSCoder) { 314 | super.init(coder: aDecoder) 315 | commonInit() 316 | } 317 | 318 | private func commonInit() { 319 | ` 320 | } 321 | 322 | function uikitDeclarationsFor(elements) { 323 | var s = "" 324 | elements.map(function(e) { 325 | s += uikitDeclarationsForSingle(e) 326 | s += "\n" 327 | }); 328 | return s 329 | } 330 | 331 | function uikitDeclarationsForSingleText(e) { 332 | return uikitDeclarationsForSingle(e) 333 | } 334 | 335 | function uikitDeclarationsForSingle(e) { 336 | var s = "" 337 | if (e.isText) { 338 | s += uikitDeclarationsForText(e) 339 | } else if (e.isShape) { 340 | s += ` let ${sanitizeName(e.name)} = UIView()\n` 341 | } else if (e.isGroup) { 342 | // Only if contains button 343 | s += ` let ${sanitizeName(e.name)} = UIButton()\n` 344 | } 345 | return s 346 | } 347 | 348 | 349 | function uikitDeclarationsForText(e) { 350 | return `let ${sanitizeName(e.name)} = UILabel()` 351 | } 352 | 353 | 354 | function uikitViewHierarchy(elements) { 355 | var s = "" 356 | elements.map(function(e) { 357 | s += ` ${sanitizeName(e.name)}.translatesAutoresizingMaskIntoConstraints = false` + "\n" 358 | }); 359 | s += "\n" 360 | elements.reverse().map(function(e) { 361 | s += ` addSubview(${sanitizeName(e.name)})` + "\n" 362 | }); 363 | return s 364 | } 365 | 366 | function uikitLayout(elements) { 367 | var s = "\n" 368 | elements.map(function(v) { 369 | var elementName = sanitizeName(v.name) 370 | // Top 371 | s += ` addConstraint( 372 | NSLayoutConstraint(item: ${elementName}, 373 | attribute: .top, 374 | relatedBy: .equal, 375 | toItem: self, 376 | attribute: .top, 377 | multiplier: 1, 378 | constant: ${v.frame.y}) 379 | ) 380 | 381 | ` 382 | 383 | // Left 384 | s += ` addConstraint( 385 | NSLayoutConstraint(item: ${elementName}, 386 | attribute: .left, 387 | relatedBy: .equal, 388 | toItem: self, 389 | attribute: .left, 390 | multiplier: 1, 391 | constant: ${v.frame.x}) 392 | ) 393 | 394 | ` 395 | 396 | // Width 397 | s += ` addConstraint( 398 | NSLayoutConstraint(item: ${elementName}, 399 | attribute: .width, 400 | relatedBy: .equal, 401 | toItem: nil, 402 | attribute: .notAnAttribute, 403 | multiplier: 1, 404 | constant: ${v.frame.width}) 405 | ) 406 | 407 | ` 408 | 409 | // Height 410 | s += ` addConstraint( 411 | NSLayoutConstraint(item: ${elementName}, 412 | attribute: .height, 413 | relatedBy: .equal, 414 | toItem: nil, 415 | attribute: .notAnAttribute, 416 | multiplier: 1, 417 | constant: ${v.frame.height}) 418 | ) 419 | 420 | ` 421 | }); 422 | return s 423 | } 424 | 425 | function uikitContentForLabels(labels) { 426 | var contentStr = labels.map( 427 | l => ` ${sanitizeName(l.name)}.text = ${formattedTextForText(l.text)}` 428 | ).join('\n') 429 | 430 | var s = " // Content \n" 431 | s += `${contentStr} 432 | } 433 | }` 434 | return s 435 | } 436 | 437 | function copyText(text) { 438 | var pasteBoard = [NSPasteboard generalPasteboard]; 439 | [pasteBoard declareTypes: [NSArray arrayWithObject: NSPasteboardTypeString] owner: nil]; 440 | [pasteBoard setString: text forType: NSPasteboardTypeString]; 441 | } 442 | 443 | function sanitizeName(str) { 444 | return lowerCaseFirstLetter(removeSpaces(str)) 445 | } 446 | 447 | function lowerCaseFirstLetter(string) { 448 | return string.charAt(0).toLowerCase() + string.slice(1); 449 | } 450 | 451 | function removeSpaces(str) { 452 | return str.replace(/\s+/g, ''); 453 | } 454 | -------------------------------------------------------------------------------- /UIKitExport.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Export for UIKit", 3 | "identifier" : "com.sachadso.UIKit", 4 | "version" : "0.0.5", 5 | "description" : "Export UIKit code directly for Sketch!", 6 | "authorEmail" : "sachadso@gmail.com", 7 | "author" : "Sacha Durand Saint Omer", 8 | "commands" : [ 9 | { 10 | "script" : "UIKitExport.js", 11 | "handler" : "onRun", 12 | "shortcut" : "command alt k", 13 | "name" : "Copy UIKit Code", 14 | "identifier" : "UIKitExport" 15 | }, 16 | { 17 | "script" : "SteviaExport.js", 18 | "handler" : "onRun", 19 | "shortcut" : "command alt l", 20 | "name" : "Copy Stevia Code", 21 | "identifier" : "SteviaExport" 22 | } 23 | ], 24 | "menu" : 25 | { 26 | "title" : " 📲 Sketch to Swift", 27 | "items" : 28 | [ 29 | "UIKitExport", 30 | "SteviaExport" 31 | ] 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshOS/SketchToSwift/60105a0a07cb470aa97ed7710e3fc689a41f0197/demo.gif -------------------------------------------------------------------------------- /example.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshOS/SketchToSwift/60105a0a07cb470aa97ed7710e3fc689a41f0197/example.sketch --------------------------------------------------------------------------------