├── .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 |
--------------------------------------------------------------------------------