├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── Images ├── Main.png ├── MySwiftUIView.png ├── Preview.png └── test.png ├── Package.swift ├── README.md ├── Sources └── ConsoleUI │ ├── MySwiftUIView.swift │ └── main.swift └── Tests ├── ConsoleUITests ├── ConsoleUITests.swift └── XCTestManifests.swift └── LinuxMain.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: eneko 4 | patreon: eneko 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Run tests 15 | run: swift test 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | .swiftpm 6 | /*.png 7 | 8 | -------------------------------------------------------------------------------- /Images/Main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eneko/ConsoleUI/195521581e233922191d3a4ca6166e1a7828107a/Images/Main.png -------------------------------------------------------------------------------- /Images/MySwiftUIView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eneko/ConsoleUI/195521581e233922191d3a4ca6166e1a7828107a/Images/MySwiftUIView.png -------------------------------------------------------------------------------- /Images/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eneko/ConsoleUI/195521581e233922191d3a4ca6166e1a7828107a/Images/Preview.png -------------------------------------------------------------------------------- /Images/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eneko/ConsoleUI/195521581e233922191d3a4ca6166e1a7828107a/Images/test.png -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ConsoleUI", 7 | platforms: [ 8 | .macOS(.v10_15) 9 | ], 10 | products: [ 11 | .executable(name: "consoleui", targets: ["ConsoleUI"]) 12 | ], 13 | targets: [ 14 | .target(name: "ConsoleUI", dependencies: []), 15 | .testTarget(name: "ConsoleUITests", dependencies: ["ConsoleUI"]) 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ConsoleUI 4 | 5 | Demo repository to showcase how to **programmatically generate images from a command-line tool with [SwiftUI](https://developer.apple.com/xcode/swiftui/)**. 6 | 7 | ![](Images/test.png) 8 | 9 | ## The Basics: Hello, world! 10 | 11 | ### Create a new command-line executable 12 | 13 | ``` 14 | $ mkdir mytool && cd mytool 15 | $ swift package init --type executable 16 | $ swift run 17 | Hello, world! 18 | ``` 19 | 20 | ### Generate Xcode project (optional) 21 | 22 | Xcode does not support live previews on Swift packages yet (FB8979344). To allow for realtime previews follow these steps: 23 | 24 | 1. Generate an Xcode project 25 | 2. Add a macOS target to the project 26 | 3. Add your view (see next section) to the macOS target 27 | 28 | ``` 29 | $ swift package generate-xcodeproj 30 | $ open mytool.xcodeproj 31 | ``` 32 | 33 | ### Add SwiftUI view 34 | 35 | Create a view with a red label over white background 36 | 37 | ```swift 38 | import SwiftUI 39 | 40 | struct MySwiftUIView : View { 41 | var body: some View { 42 | ZStack { 43 | Color.blue.edgesIgnoringSafeArea(.all) 44 | Text("Hello, world!") 45 | .foregroundColor(.white) 46 | .font(.largeTitle) 47 | } 48 | } 49 | } 50 | 51 | #if DEBUG 52 | struct MySwiftUIView_Previews : PreviewProvider { 53 | static var previews: some View { 54 | MySwiftUIView() 55 | } 56 | } 57 | #endif 58 | ``` 59 | 60 | ![SwiftUI](Images/MySwiftUIView.png) 61 | 62 | ### Rasterize view and save to file 63 | 64 | We will use this method to rasterize views to images: 65 | 66 | ```swift 67 | func rasterize(view: NSView, format: NSBitmapImageRep.FileType) -> Data? { 68 | guard let bitmapRepresentation = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { 69 | return nil 70 | } 71 | bitmapRepresentation.size = view.bounds.size 72 | view.cacheDisplay(in: view.bounds, to: bitmapRepresentation) 73 | return bitmapRepresentation.representation(using: format, properties: [:]) 74 | } 75 | ``` 76 | 77 | The following code instantiates the SwiftUI view, wrapped inside a `NSHostingView` which is then 78 | rasterized and saved to disk 79 | 80 | ```swift 81 | let wrapper = NSHostingView(rootView: MySwiftUIView()) 82 | wrapper.frame = CGRect(x: 0, y: 0, width: 800, height: 450) 83 | 84 | let png = rasterize(view: wrapper, format: .png) 85 | try png?.write(to: URL(fileURLWithPath: "test.png")) 86 | ``` 87 | 88 | ![main.swift](Images/Main.png) 89 | 90 | ### Run the command and open in Preview 91 | 92 | ``` 93 | $ swift run && open test.png 94 | ``` 95 | 96 | ![Preview](Images/Preview.png) 97 | -------------------------------------------------------------------------------- /Sources/ConsoleUI/MySwiftUIView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MySwiftUIView.swift 3 | // ConsoleUI 4 | // 5 | // Created by Eneko Alonso on 6/20/19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MySwiftUIView : View { 11 | var body: some View { 12 | ZStack { 13 | Color.blue.edgesIgnoringSafeArea(.all) 14 | Text("Hello, world!") 15 | .foregroundColor(.white) 16 | .font(.largeTitle) 17 | } 18 | } 19 | } 20 | 21 | #if DEBUG 22 | struct MySwiftUIView_Previews : PreviewProvider { 23 | static var previews: some View { 24 | MySwiftUIView() 25 | } 26 | } 27 | #endif 28 | -------------------------------------------------------------------------------- /Sources/ConsoleUI/main.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | func rasterize(view: NSView, format: NSBitmapImageRep.FileType) -> Data? { 5 | guard let bitmapRepresentation = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { 6 | return nil 7 | } 8 | bitmapRepresentation.size = view.bounds.size 9 | view.cacheDisplay(in: view.bounds, to: bitmapRepresentation) 10 | return bitmapRepresentation.representation(using: format, properties: [:]) 11 | } 12 | 13 | 14 | let wrapper = NSHostingView(rootView: MySwiftUIView()) 15 | wrapper.frame = CGRect(x: 0, y: 0, width: 800, height: 450) 16 | 17 | let png = rasterize(view: wrapper, format: .png) 18 | try png?.write(to: URL(fileURLWithPath: "test.png")) 19 | -------------------------------------------------------------------------------- /Tests/ConsoleUITests/ConsoleUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.Bundle 3 | 4 | final class ConsoleUITests: XCTestCase { 5 | func testExample() throws { 6 | guard #available(macOS 10.13, *) else { 7 | return 8 | } 9 | 10 | let fooBinary = productsDirectory.appendingPathComponent("ConsoleUI") 11 | 12 | let process = Process() 13 | process.executableURL = fooBinary 14 | 15 | let pipe = Pipe() 16 | process.standardOutput = pipe 17 | 18 | try? FileManager.default.removeItem(atPath: "test.png") 19 | 20 | try process.run() 21 | process.waitUntilExit() 22 | 23 | XCTAssertTrue(FileManager.default.fileExists(atPath: "test.png")) 24 | } 25 | 26 | /// Returns path to the built products directory. 27 | var productsDirectory: URL { 28 | #if os(macOS) 29 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 30 | return bundle.bundleURL.deletingLastPathComponent() 31 | } 32 | fatalError("couldn't find the products directory") 33 | #else 34 | return Bundle.main.bundleURL 35 | #endif 36 | } 37 | 38 | static var allTests = [ 39 | ("testExample", testExample), 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /Tests/ConsoleUITests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ConsoleUITests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ConsoleUITests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ConsoleUITests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------