├── .gitignore ├── LICENSE ├── README.md ├── ScreenshotCreator.playground ├── Contents.swift ├── Resources │ ├── Devices │ │ ├── Apple iPad Pro Silver.png │ │ └── Apple iPhone 7 Plus Silver.png │ └── Screenshots │ │ └── .keep ├── Sources │ └── ScreenshotCreator.swift └── contents.xcplayground └── phone1.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | 67 | .DS_Store 68 | *~ 69 | .*.sw* 70 | *.xctimeline 71 | ScreenshotCreator.playground/Resources/Screenshots/*.png 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 tnantoka 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScreenshotCreator 2 | 3 | Create app screenshots programmatically on Playground with Swift and Xcode. 4 | 5 | ## Example 6 | 7 | ``` 8 | var config = ScreenshotConfig() 9 | config.backgroundColor = UIColor.brown 10 | config.titles = [ 11 | "phone1" : "Hello, world!" 12 | ] 13 | 14 | let creator = ScreenshotCreator(config: config) 15 | creator.preview() 16 | 17 | creator.save() 18 | ``` 19 | 20 | ### Output 21 | 22 | ![](/phone1.png) 23 | 24 | 25 | ### Console 26 | 27 | ``` 28 | $ open /path/to/Documents/ScreenshotCreator 29 | ``` 30 | 31 | ## Acknowledgements 32 | 33 | - http://facebook.design/devices 34 | 35 | ## License 36 | 37 | My code is licensed under the MIT license. 38 | **Each asset has its own license!** 39 | 40 | ## See Also 41 | 42 | [tnantoka/IconCreator](https://github.com/tnantoka/IconCreator) 43 | -------------------------------------------------------------------------------- /ScreenshotCreator.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import UIKit 4 | 5 | var config = ScreenshotConfig() 6 | config.backgroundColor = UIColor.brown 7 | config.titles = [ 8 | "phone1" : "Hello, world!" 9 | ] 10 | 11 | let creator = ScreenshotCreator(config: config) 12 | creator.preview() 13 | 14 | creator.save() 15 | -------------------------------------------------------------------------------- /ScreenshotCreator.playground/Resources/Devices/Apple iPad Pro Silver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnantoka/ScreenshotCreator/be5050bb6e001502e9c0774d18bd1aa1a6d7667c/ScreenshotCreator.playground/Resources/Devices/Apple iPad Pro Silver.png -------------------------------------------------------------------------------- /ScreenshotCreator.playground/Resources/Devices/Apple iPhone 7 Plus Silver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnantoka/ScreenshotCreator/be5050bb6e001502e9c0774d18bd1aa1a6d7667c/ScreenshotCreator.playground/Resources/Devices/Apple iPhone 7 Plus Silver.png -------------------------------------------------------------------------------- /ScreenshotCreator.playground/Resources/Screenshots/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnantoka/ScreenshotCreator/be5050bb6e001502e9c0774d18bd1aa1a6d7667c/ScreenshotCreator.playground/Resources/Screenshots/.keep -------------------------------------------------------------------------------- /ScreenshotCreator.playground/Sources/ScreenshotCreator.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | let bundle = Bundle.main 4 | 5 | struct Device { 6 | static func load(_ name: String) -> UIImage { 7 | let path = bundle.path(forResource: name, ofType: "png", inDirectory: "Devices") 8 | return UIImage(contentsOfFile: path!)! 9 | } 10 | 11 | static let phone = Device.load("Apple iPhone 7 Plus Silver") 12 | static let pad = Device.load("Apple iPad Pro Silver") 13 | } 14 | 15 | struct Size { 16 | static let phone = CGSize(width: 1242.0, height: 2208.0) 17 | static let pad = CGSize(width: 2048.0, height: 2732.0) 18 | } 19 | 20 | struct Screenshot { 21 | let screen: UIImage 22 | let filename: String 23 | 24 | var isPhone: Bool { 25 | return length <= Size.phone.height 26 | } 27 | var isPortrait: Bool { 28 | return screen.size.height > screen.size.width 29 | } 30 | 31 | var size: CGSize { 32 | let size = isPhone ? Size.phone : Size.pad 33 | return isPortrait ? size : CGSize(width: size.height, height: size.width) 34 | } 35 | var rect: CGRect { 36 | return CGRect(origin: CGPoint.zero, size: size) 37 | } 38 | 39 | var length: CGFloat { 40 | return max(screen.size.width, screen.size.height) 41 | } 42 | var device: UIImage { 43 | return isPhone ? Device.phone : Device.pad 44 | } 45 | 46 | let titleRectScale: CGFloat = 0.1 47 | var titleRect: CGRect { 48 | var rect = self.rect 49 | rect.size.height = length * titleRectScale 50 | return rect 51 | } 52 | 53 | init?(path: String) { 54 | guard let screen = UIImage(contentsOfFile: path) else { return nil } 55 | 56 | self.screen = screen 57 | filename = ((path as NSString).lastPathComponent as NSString).deletingPathExtension 58 | } 59 | 60 | static var all: [Screenshot] = { 61 | return bundle.paths(forResourcesOfType: "png", inDirectory: "Screenshots").flatMap { path in 62 | Screenshot(path: path) 63 | } 64 | }() 65 | 66 | func framed(config: ScreenshotConfig) -> UIImage { 67 | let title = config.title(filename) 68 | 69 | let opaque = true 70 | let scale: CGFloat = 1.0 71 | UIGraphicsBeginImageContextWithOptions(size, opaque, scale) 72 | 73 | let context = UIGraphicsGetCurrentContext()! 74 | 75 | config.backgroundColor.setFill() 76 | context.fill(rect) 77 | 78 | drawDevice(in: context) 79 | drawTitle(title, in: context, attributed: textAttributes(config)) 80 | 81 | let image = UIGraphicsGetImageFromCurrentImageContext() 82 | 83 | UIGraphicsEndImageContext() 84 | 85 | return image! 86 | } 87 | 88 | func drawDevice(in context: CGContext) { 89 | let scale: CGFloat = 0.7 90 | let transform = CGAffineTransform(scaleX: scale, y: scale) 91 | 92 | let deviceSize = device.size.applying(transform) 93 | 94 | if isPortrait { 95 | let deviceOrigin = CGPoint( 96 | x: rect.midX - deviceSize.width / 2.0, 97 | y: rect.midY - deviceSize.height / 2.0 98 | ) 99 | device.draw(in: CGRect(origin: deviceOrigin, size: deviceSize).offsetBy(dx: 0.0, dy: titleRect.height)) 100 | } else { 101 | let deviceOrigin = CGPoint( 102 | x: rect.midY - deviceSize.width / 2.0, 103 | y: rect.midX - deviceSize.height / 2.0 104 | ) 105 | context.saveGState() 106 | context.scaleBy(x: 1.0, y: -1.0) 107 | context.rotate(by: -90.0 * CGFloat(M_PI) / 180.0) 108 | device.draw(in: CGRect(origin: deviceOrigin, size: deviceSize).offsetBy(dx: titleRect.height, dy: 0.0)) 109 | context.restoreGState() 110 | } 111 | 112 | let screenSize = screen.size.applying(transform) 113 | let screenOrigin = CGPoint( 114 | x: rect.midX - screenSize.width / 2.0, 115 | y: rect.midY - screenSize.height / 2.0 116 | ) 117 | screen.draw(in: CGRect(origin: screenOrigin, size: screenSize).offsetBy(dx: 0.0, dy: titleRect.height)) 118 | } 119 | 120 | func textAttributes(_ config: ScreenshotConfig) -> [String : Any] { 121 | let fontSize = length * config.fontSizeScaleY 122 | 123 | let defaultStyle = NSParagraphStyle.default 124 | let style = defaultStyle.mutableCopy() as! NSMutableParagraphStyle 125 | style.alignment = .center 126 | 127 | let attributes: [String : Any] = [ 128 | NSFontAttributeName: UIFont(name: config.fontName, size: fontSize)!, 129 | NSForegroundColorAttributeName: config.textColor, 130 | NSParagraphStyleAttributeName: style, 131 | ] 132 | 133 | return attributes 134 | } 135 | 136 | func drawTitle(_ title: String, in context: CGContext, attributed attributes: [String : Any]) { 137 | // let frame = title.boundingRect( 138 | // with: size, 139 | // options: [.usesLineFragmentOrigin, .usesFontLeading,], 140 | // attributes: attributes, 141 | // context: nil 142 | // ) 143 | 144 | title.draw( 145 | in: titleRect.offsetBy( 146 | dx: 0.0, 147 | dy: titleRect.midY 148 | // dy: titleRect.midY - frame.midY 149 | ), 150 | withAttributes: attributes 151 | ) 152 | } 153 | } 154 | 155 | public struct ScreenshotConfig { 156 | public var backgroundColor = UIColor.gray 157 | public var textColor = UIColor.white 158 | 159 | public var fontSizeScaleY: CGFloat = 0.04 160 | public var fontName = ".SFUIDisplay-Light" 161 | 162 | public var titles: [String: String] = [:] 163 | 164 | public func title(_ filename: String) -> String { 165 | return titles[filename] ?? "Untitled" 166 | } 167 | 168 | public init() { 169 | } 170 | } 171 | 172 | public struct ScreenshotCreator { 173 | let fileManager = FileManager.default 174 | let screenshots = Screenshot.all 175 | 176 | let config: ScreenshotConfig 177 | 178 | public init(config: ScreenshotConfig) { 179 | self.config = config 180 | } 181 | 182 | public func preview() -> [UIImage] { 183 | return screenshots.map { $0.framed(config: config) } 184 | } 185 | 186 | var docURL: URL { 187 | return fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! 188 | } 189 | var dirURL: URL { 190 | return docURL.appendingPathComponent("ScreenshotCreator", isDirectory: true) 191 | } 192 | public func save() { 193 | let _ = try? fileManager.createDirectory(at: dirURL, withIntermediateDirectories: false, attributes: nil) 194 | 195 | for screenshot in screenshots { 196 | let data = UIImagePNGRepresentation(screenshot.framed(config: config))! 197 | try? data.write(to: dirURL.appendingPathComponent("\(screenshot.filename).png")) 198 | } 199 | 200 | print("$ open \(dirURL.relativePath)") 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /ScreenshotCreator.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /phone1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnantoka/ScreenshotCreator/be5050bb6e001502e9c0774d18bd1aa1a6d7667c/phone1.png --------------------------------------------------------------------------------