├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.md ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── ibcolortool │ ├── Color+Format.swift │ ├── FileHandle+TextOutputStream.swift │ ├── InterfaceBuilderFile+Colors.swift │ └── main.swift └── Tests ├── LinuxMain.swift └── ibcolortoolTests ├── Fixtures.swift ├── Helpers.swift └── ibcolortoolTests.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | macos: 11 | runs-on: macOS-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v1 16 | - name: Build and Test 17 | run: swift test 18 | env: 19 | DEVELOPER_DIR: /Applications/Xcode_11.4.app/Contents/Developer 20 | 21 | linux: 22 | runs-on: ubuntu-latest 23 | 24 | container: 25 | image: swift:5.2 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v1 30 | - name: Build and Test 31 | run: swift test --enable-test-discovery 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Read Evaluate Press, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is 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 14 | OR 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 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | prefix ?= /usr/local 4 | bindir ?= $(prefix)/bin 5 | libdir ?= $(prefix)/lib 6 | srcdir = Sources 7 | 8 | REPODIR = $(shell pwd) 9 | BUILDDIR = $(REPODIR)/.build 10 | SOURCES = $(wildcard $(srcdir)/**/*.swift) 11 | 12 | .DEFAULT_GOAL = all 13 | 14 | .PHONY: all 15 | all: ibcolortool 16 | 17 | ibcolortool: $(SOURCES) 18 | @swift build \ 19 | -c release \ 20 | --disable-sandbox \ 21 | --build-path "$(BUILDDIR)" 22 | 23 | .PHONY: install 24 | install: ibcolortool 25 | @install -d "$(bindir)" "$(libdir)" 26 | @install "$(BUILDDIR)/release/ibcolortool" "$(bindir)" 27 | 28 | .PHONY: uninstall 29 | uninstall: 30 | @rm -rf "$(bindir)/ibcolortool" 31 | 32 | .PHONY: clean 33 | distclean: 34 | @rm -f $(BUILDDIR)/release 35 | 36 | .PHONY: clean 37 | clean: distclean 38 | @rm -rf $(BUILDDIR) 39 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "IBDecodable", 6 | "repositoryURL": "https://github.com/IBDecodable/IBDecodable.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "804c5f3dcf290c019b25e0ce2e1044d3a02c185a", 10 | "version": "0.4.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-argument-parser", 15 | "repositoryURL": "https://github.com/apple/swift-argument-parser.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "223d62adc52d51669ae2ee19bdb8b7d9fd6fcd9c", 19 | "version": "0.0.6" 20 | } 21 | }, 22 | { 23 | "package": "SWXMLHash", 24 | "repositoryURL": "https://github.com/drmohundro/SWXMLHash.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "a4931e5c3bafbedeb1601d3bb76bbe835c6d475a", 28 | "version": "5.0.1" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ibcolortool", 8 | platforms: [ 9 | .macOS("10.13") 10 | ], 11 | dependencies: [ 12 | // Dependencies declare other packages that this package depends on. 13 | .package(url: "https://github.com/IBDecodable/IBDecodable.git", from: "0.4.0"), 14 | .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.0.6")), 15 | ], 16 | targets: [ 17 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 18 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 19 | .target( 20 | name: "ibcolortool", 21 | dependencies: ["IBDecodable", "ArgumentParser"] 22 | ), 23 | .testTarget( 24 | name: "ibcolortoolTests", 25 | dependencies: ["ibcolortool"] 26 | ), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ibcolortool 2 | 3 | Lists all of the colors used in Storyboards and XIB files. 4 | 5 | ## Usage 6 | 7 | ```terminal 8 | $ ibcolortool path/to/View.xib path/to/Scene.storyboard 9 | UIColor(red: 1.0, green: 0.5763723, blue: 0.0, alpha: 1.0) 10 | UIColor(white: 0.0, alpha: 1.0) 11 | UIColor(named: "Living Coral") 12 | UIColor.groupTableViewBackgroundColor 13 | ``` 14 | 15 | ## Requirements 16 | 17 | - Xcode 11+ 18 | - Swift 5.1+ 19 | 20 | ## Installation 21 | 22 | ### Homebrew 23 | 24 | Run the following command to install using [homebrew](https://brew.sh/): 25 | 26 | ```terminal 27 | $ brew install nshipster/formulae/ibcolortool 28 | ``` 29 | 30 | ### Manually 31 | 32 | Run the following commands to build and install manually: 33 | 34 | ```terminal 35 | $ git clone https://github.com/NSHipster/ibcolortool.git 36 | $ cd ibcolortool 37 | $ make install 38 | ``` 39 | 40 | ## License 41 | 42 | MIT 43 | 44 | ## Contact 45 | 46 | Mattt ([@mattt](https://twitter.com/mattt)) 47 | -------------------------------------------------------------------------------- /Sources/ibcolortool/Color+Format.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | import IBDecodable 4 | 5 | extension Color { 6 | enum Format: String, ExpressibleByArgument { 7 | case rgbHexadecimal = "hex" 8 | case uicolorDeclaration = "uicolor" 9 | 10 | func representation(of color: Color) -> String? { 11 | switch self { 12 | case .rgbHexadecimal: 13 | switch color { 14 | case .sRGB(let components): 15 | return String(format: "#%02X%02X%02X", UInt8(components.red * 255), UInt8(components.green * 255), UInt8(components.blue * 255)) 16 | case .calibratedRGB(let components): 17 | return String(format: "#%02X%02X%02X", UInt8(components.red * 255), UInt8(components.green * 255), UInt8(components.blue * 255)) 18 | case .calibratedWhite(let components): 19 | return String(format: "#%02X%02X%02X", arguments: Array(repeating: UInt8(components.white * 255), count: 3)) 20 | default: 21 | return nil 22 | } 23 | case .uicolorDeclaration: 24 | switch color { 25 | case .sRGB(let components): 26 | return "UIColor(red: \(components.red), green: \(components.green), blue: \(components.blue), alpha: \(components.alpha))" 27 | case .calibratedRGB(let components): 28 | return "UIColor(red: \(components.red), green: \(components.green), blue: \(components.blue), alpha: \(components.alpha))" 29 | case .calibratedWhite(let components): 30 | return "UIColor(white: \(components.white), alpha: \(components.alpha))" 31 | case .name(let components): 32 | return "UIColor(named: \(components.name))" 33 | case .systemColor(let components): 34 | return "UIColor.\(components.name)" 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/ibcolortool/FileHandle+TextOutputStream.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension FileHandle: TextOutputStream { 4 | public func write(_ string: String) { 5 | guard let data = string.data(using: .utf8) else { return } 6 | write(data) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/ibcolortool/InterfaceBuilderFile+Colors.swift: -------------------------------------------------------------------------------- 1 | import IBDecodable 2 | 3 | typealias Entry = (element: IBIdentifiable?, color: Color) 4 | 5 | fileprivate final class InterfaceBuilderElementVisitor { 6 | var entries: [Entry] = [] 7 | 8 | func visit(element: IBElement) -> Bool { 9 | for child in Mirror(reflecting: element).children { 10 | if let color = child.value as? Color { 11 | entries.append((element as? IBIdentifiable, color)) 12 | } 13 | } 14 | 15 | return true 16 | } 17 | } 18 | 19 | extension StoryboardFile { 20 | var entries: [Entry] { 21 | let visitor = InterfaceBuilderElementVisitor() 22 | _ = document.browse(visitor.visit(element:)) 23 | return visitor.entries 24 | } 25 | } 26 | 27 | extension XibFile { 28 | var entries: [Entry] { 29 | let visitor = InterfaceBuilderElementVisitor() 30 | _ = document.browse(visitor.visit(element:)) 31 | return visitor.entries 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ibcolortool/main.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | import IBDecodable 4 | 5 | let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst()) 6 | 7 | let fileManager = FileManager.default 8 | 9 | var standardOutput = FileHandle.standardOutput 10 | var standardError = FileHandle.standardError 11 | 12 | struct IBColorTool: ParsableCommand { 13 | static var configuration = CommandConfiguration( 14 | commandName: "ibcolortool", 15 | abstract: "Lists the colors used in Storyboards and XIB files.", 16 | version: "0.0.1" 17 | ) 18 | 19 | @Argument(help: "One or more paths to XIB or Storyboard files", 20 | transform: { URL(fileURLWithPath: $0) }) 21 | var inputs: [URL] 22 | 23 | @Option(default: .uicolorDeclaration, 24 | help: "Representation format for colors.") 25 | var format: Color.Format 26 | 27 | @Flag(help: "Include the color's corresponding Object ID") 28 | var includeObjectID: Bool 29 | 30 | func run() throws { 31 | var entries: [Entry] = [] 32 | 33 | guard !inputs.isEmpty else { 34 | print(IBColorTool.helpMessage()) 35 | return 36 | } 37 | 38 | for fileURL in inputs { 39 | do { 40 | switch fileURL.pathExtension.lowercased() { 41 | case "storyboard": 42 | let file = try StoryboardFile(url: fileURL) 43 | entries.append(contentsOf: file.entries) 44 | case "xib": 45 | let file = try XibFile(url: fileURL) 46 | entries.append(contentsOf: file.entries) 47 | default: 48 | print("Unknown file extension: ", fileURL.pathExtension, to: &standardError) 49 | } 50 | } catch { 51 | print(fileURL, error, to: &standardError) 52 | continue 53 | } 54 | } 55 | 56 | let lines: [String] = entries.compactMap { entry in 57 | let id = entry.element?.id 58 | let representation = format.representation(of: entry.color) 59 | 60 | guard id != nil || representation != nil else { return nil } 61 | 62 | if includeObjectID { 63 | return "\(id ?? "")\t\(representation ?? "")" 64 | } else { 65 | return representation ?? "" 66 | } 67 | } 68 | 69 | print(lines.joined(separator: "\n"), to: &standardOutput) 70 | } 71 | } 72 | 73 | IBColorTool.main() 74 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | fatalError("Run with `swift test --enable-test-discovery`") 2 | -------------------------------------------------------------------------------- /Tests/ibcolortoolTests/Fixtures.swift: -------------------------------------------------------------------------------- 1 | let xib = #""" 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | """# 25 | 26 | let storyboard = #""" 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | """# 55 | -------------------------------------------------------------------------------- /Tests/ibcolortoolTests/Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func runIBColorTool(with arguments: [String]) throws -> String { 4 | let process = Process() 5 | process.executableURL = productsDirectory.appendingPathComponent("ibcolortool") 6 | 7 | let pipe = Pipe() 8 | process.standardOutput = pipe 9 | 10 | process.arguments = arguments 11 | 12 | try process.run() 13 | process.waitUntilExit() 14 | 15 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 16 | return String(data: data, encoding: .utf8)! 17 | } 18 | 19 | var productsDirectory: URL { 20 | #if os(macOS) 21 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 22 | return bundle.bundleURL.deletingLastPathComponent() 23 | } 24 | fatalError("couldn't find the products directory") 25 | #else 26 | return Bundle.main.bundleURL 27 | #endif 28 | } 29 | 30 | func temporaryFile(contents: String, extension: String) throws -> URL { 31 | let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) 32 | let temporaryFilename = UUID().uuidString 33 | 34 | let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFilename).appendingPathExtension(`extension`) 35 | try contents.data(using: .utf8)?.write(to: temporaryFileURL, options: .atomic) 36 | 37 | return temporaryFileURL 38 | } 39 | -------------------------------------------------------------------------------- /Tests/ibcolortoolTests/ibcolortoolTests.swift: -------------------------------------------------------------------------------- 1 | import class Foundation.Bundle 2 | import XCTest 3 | 4 | final class IBColorToolTests: XCTestCase { 5 | func testUIColorDeclarationFormatXIB() throws { 6 | let xibFilePath = (try temporaryFile(contents: xib, extension: "xib")).path 7 | let actual = try runIBColorTool(with: [xibFilePath]) 8 | let expected = "UIColor(red: 1.0, green: 0.5763723, blue: 0.0, alpha: 1.0)\n" 9 | 10 | XCTAssertEqual(actual, expected) 11 | } 12 | 13 | func testRGBHexFormatXIB() throws { 14 | let xibFilePath = (try temporaryFile(contents: xib, extension: "xib")).path 15 | let actual = try runIBColorTool(with: ["--format", "hex", xibFilePath]) 16 | let expected = "#FF9200\n" 17 | 18 | XCTAssertEqual(actual, expected) 19 | } 20 | 21 | func testUIColorDeclarationFormatStoryboard() throws { 22 | let storyboardFilePath = (try temporaryFile(contents: storyboard, extension: "storyboard")).path 23 | let actual = try runIBColorTool(with: [storyboardFilePath]) 24 | let expected = "UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)\n" 25 | 26 | XCTAssertEqual(actual, expected) 27 | } 28 | 29 | func testRGBHexFormatStoryboard() throws { 30 | let storyboardFilePath = (try temporaryFile(contents: storyboard, extension: "storyboard")).path 31 | let actual = try runIBColorTool(with: ["--format", "hex", storyboardFilePath]) 32 | let expected = "#00FF00\n" 33 | 34 | XCTAssertEqual(actual, expected) 35 | } 36 | 37 | func testStoryboardAndXIBWithObjectID() throws { 38 | let storyboardFilePath = (try temporaryFile(contents: storyboard, extension: "storyboard")).path 39 | let xibFilePath = (try temporaryFile(contents: xib, extension: "xib")).path 40 | let actual = try runIBColorTool(with: [ 41 | "--format", "hex", 42 | "--include-object-id", 43 | storyboardFilePath, 44 | xibFilePath 45 | ]) 46 | 47 | let expected = """ 48 | 3Dd-xk-CM4\t#00FF00 49 | iN0-l3-epB\t#FF9200 50 | 51 | """ 52 | 53 | XCTAssertEqual(actual, expected) 54 | } 55 | } 56 | --------------------------------------------------------------------------------