Skip to content

Commit 4ec3f39

Browse files
authored
Merge pull request #6 from strvcom/feat/dc/fixes-and-improvements
feat: VersionIcon fixes and improvements
2 parents e34eb32 + b58662d commit 4ec3f39

18 files changed

Lines changed: 1260 additions & 492 deletions

Bin/VersionIcon

-555 KB
Binary file not shown.

Package.resolved

Lines changed: 7 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,14 @@ let package = Package(
1111
dependencies: [
1212
.package(url: "https://github.com/JohnSundell/Files.git", from: "4.1.1"),
1313
.package(url: "https://github.com/DanielCech/Moderator.git", from: "0.5.1"),
14-
.package(url: "https://github.com/DanielCech/ScriptToolkit.git", .branch("master")),
1514
.package(url: "https://github.com/kareman/SwiftShell.git", from: "5.0.1"),
16-
.package(url: "https://github.com/kareman/FileSmith.git", from: "0.3.0"),
1715
],
1816
targets: [
1917
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2018
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
2119
.target(
2220
name: "VersionIcon",
23-
dependencies: ["Files", "FileSmith", "SwiftShell", "ScriptToolkit", "Moderator"],
21+
dependencies: ["Files", "SwiftShell", "Moderator"],
2422
swiftSettings:
2523
[
2624
// Macro definition - uncomment only when debugging

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ $ pod install
4343
## Usage
4444

4545
* Make a duplicate of your app icon resource in asset catalog - let's have for example _AppIcon_ and _AppIconOriginal_. The copy is used as a backup. Production builds typically have no icon overlays. (if your project contains icon resource with other than this default name, you need to specify it using `--appIcon` and/or `--appIconOriginal` parameter.
46+
* VersionIcon discovers matching icon entries directly from both `Contents.json` files, so it works with modern asset catalogs instead of assuming a fixed list of legacy iOS sizes.
4647
* Create a new Run Script Phase in Build Settings > Build Phases in your app
4748
* Use this shell script:
4849
```shell
4950
if [ "${CONFIGURATION}" = "Release" ]; then
5051
"Pods/VersionIcon/Bin/VersionIcon" --resources "Pods/VersionIcon/Bin" --original
5152
else
52-
"Pods/VersionIcon/Bin/VersionIcon" --ribbon Blue-TopRight.png --title Devel-TopRight.png --resources "Pods/VersionIcon/Bin" --strokeWidth 0.07
53+
"Pods/VersionIcon/Bin/VersionIcon" --ribbon Blue-TopRight.png --title Devel-TopRight.png --resources "Pods/VersionIcon/Bin" --strokeWidth 0.07 --on-error warn
5354
fi
5455
```
5556
* If your projects contains different configuration names, you'll need to adjust the script.
@@ -85,16 +86,22 @@ fi
8586

8687
* `--verticalTitlePosition <Version Title Size Ratio>`
8788
* Version title position related to icon width. Default = '0.2'.
89+
90+
* `--titleRotation <Version Title Rotation>`
91+
* Version title rotation in degrees. Allowed range is `-180...180`. Default = `0`.
8892

8993
* `--titleAlignment <Version Title Text Alignment>`
9094
* Possible values are left, center, right. Default = 'center'.
9195

9296
* `--versionStyle <The format of version label>`
93-
* Possible values are _dash_, _parenthesis_, _versionOnly_, _buildOnly_. Default = 'dash'.
97+
* Possible values are _dash_, _parenthesis_, _parenthesisTwoLines, _twoLines_, _versionOnly_, _buildOnly_ and _empty_. Default = 'dash'.
9498

9599
#### Script Setup
96100
* `--resources <VersionIcon resources path>`
97101
* Default path where Ribbons and Titles folders are located. It is not necessary to set when script is executed as a build phase in Xcode
102+
103+
* `--on-error <fail|warn>`
104+
* Controls whether VersionIcon should fail the build (`fail`, default) or print the error and continue (`warn`).
98105

99106
* `--original`
100107
* If you need to use just original icon without any modifications, use this parameter. The production app typically has no icon overlay.
@@ -122,4 +129,3 @@ Issues and pull requests are welcome!
122129
## License
123130

124131
VersionIcon is released under the MIT license. See [LICENSE](https://github.com/DanielCech/DeallocTests/blob/master/LICENSE) for details.
125-
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import AppKit
2+
import Files
3+
import Foundation
4+
import SwiftShell
5+
6+
func validateImageResource(fileName: String?, kind: String) throws {
7+
guard let path = fileName else {
8+
return
9+
}
10+
11+
guard FileManager.default.fileExists(atPath: path) else {
12+
throw ScriptError.fileNotFound(message: "\(kind) image: \(path)")
13+
}
14+
15+
guard NSImage(contentsOfFile: path) != nil else {
16+
throw ScriptError.generalError(message: "Unable to load \(kind) image: \(path)")
17+
}
18+
}
19+
20+
func getAppSetup(scriptSetup: ScriptSetup) throws -> AppSetup {
21+
#if DEBUGGING
22+
// Enter testing values from your project
23+
let sourceRootPath = ""
24+
let projectDir = ""
25+
let infoPlistFile = ""
26+
#else
27+
guard
28+
let sourceRootPath = main.env["SRCROOT"],
29+
let projectDir = main.env["PROJECT_DIR"],
30+
let infoPlistFile = main.env["INFOPLIST_FILE"]
31+
else {
32+
print("Missing environment variables")
33+
throw ScriptError.moreInfoNeeded(message: "Missing required environment variables: SRCROOT, PROJECT_DIR, INFOPLIST_FILE. Please run script from Xcode script build phase.")
34+
}
35+
#endif
36+
37+
print(" sourceRootPath: \(sourceRootPath)")
38+
print(" projectDir: \(projectDir)")
39+
print(" infoPlistFile: \(infoPlistFile)")
40+
41+
let sourceFolder = try Folder(path: sourceRootPath)
42+
let resolvedInfoPlistFile = try resolveInfoPlistPath(
43+
infoPlistFile: infoPlistFile,
44+
projectDir: projectDir,
45+
sourceRootPath: sourceRootPath
46+
)
47+
48+
guard let appIconFolder = try locateAppIconFolder(
49+
named: scriptSetup.appIcon,
50+
projectDir: projectDir,
51+
sourceRootFolder: sourceFolder
52+
) else {
53+
throw ScriptError.folderNotFound(message: "\(scriptSetup.appIcon).appiconset - icon asset folder")
54+
}
55+
56+
guard let originalAppIconFolder = try locateAppIconFolder(
57+
named: scriptSetup.appIconOriginal,
58+
projectDir: projectDir,
59+
sourceRootFolder: sourceFolder
60+
) else {
61+
throw ScriptError.folderNotFound(message: "\(scriptSetup.appIconOriginal).appiconset - source icon asset for modifications")
62+
}
63+
64+
return try AppSetup(
65+
sourceRootPath: sourceRootPath,
66+
projectDir: projectDir,
67+
infoPlistFile: resolvedInfoPlistFile,
68+
appIconFolder: appIconFolder,
69+
appIconContents: iconMetadata(iconFolder: appIconFolder),
70+
originalAppIconFolder: originalAppIconFolder,
71+
originalAppIconContents: iconMetadata(iconFolder: originalAppIconFolder)
72+
)
73+
}
74+
75+
func iconMetadata(iconFolder: Folder) throws -> IconMetadata {
76+
let contentsFile = try iconFolder.file(named: "Contents.json")
77+
let jsonData = try contentsFile.read()
78+
do {
79+
return try JSONDecoder().decode(IconMetadata.self, from: jsonData)
80+
} catch {
81+
throw ScriptError.generalError(message: String(describing: error))
82+
}
83+
}
84+
85+
func getVersionText(appSetup: AppSetup, designStyle: DesignStyle) throws -> String {
86+
#if DEBUGGING
87+
return "1.0 - 20"
88+
#endif
89+
90+
let versionNumberResult = run("/usr/libexec/PlistBuddy", "-c", "Print CFBundleShortVersionString", appSetup.infoPlistFile)
91+
let buildNumberResult = run("/usr/libexec/PlistBuddy", "-c", "Print CFBundleVersion", appSetup.infoPlistFile)
92+
93+
guard versionNumberResult.succeeded else {
94+
throw ScriptError.generalError(message: "Unable to read CFBundleShortVersionString from \(appSetup.infoPlistFile): \(versionNumberResult.stderror)")
95+
}
96+
97+
guard buildNumberResult.succeeded else {
98+
throw ScriptError.generalError(message: "Unable to read CFBundleVersion from \(appSetup.infoPlistFile): \(buildNumberResult.stderror)")
99+
}
100+
101+
var versionNumber = versionNumberResult.stdout
102+
if versionNumber == "$(MARKETING_VERSION)" {
103+
versionNumber = main.env["MARKETING_VERSION"] ?? ""
104+
}
105+
106+
var buildNumber = buildNumberResult.stdout
107+
if buildNumber == "$(CURRENT_PROJECT_VERSION)" {
108+
buildNumber = main.env["CURRENT_PROJECT_VERSION"] ?? ""
109+
}
110+
111+
switch designStyle.versionStyle {
112+
case .dash:
113+
return "\(versionNumber) - \(buildNumber)"
114+
case .parenthesis:
115+
return "\(versionNumber)(\(buildNumber))"
116+
case .parenthesisTwoLines:
117+
return "\(versionNumber)\n(\(buildNumber))"
118+
case .twoLines:
119+
return "\(versionNumber)\n\(buildNumber)"
120+
case .versionOnly:
121+
return "\(versionNumber)"
122+
case .buildOnly:
123+
return "\(buildNumber)"
124+
case .empty:
125+
return ""
126+
}
127+
}
128+
129+
private func resolveInfoPlistPath(
130+
infoPlistFile: String,
131+
projectDir: String,
132+
sourceRootPath: String
133+
) throws -> String {
134+
if infoPlistFile.hasPrefix("/") {
135+
return infoPlistFile
136+
}
137+
138+
let projectRelativePath = projectDir.appendingPathComponent(path: infoPlistFile)
139+
if FileManager.default.fileExists(atPath: projectRelativePath) {
140+
return projectRelativePath
141+
}
142+
143+
let sourceRelativePath = sourceRootPath.appendingPathComponent(path: infoPlistFile)
144+
if FileManager.default.fileExists(atPath: sourceRelativePath) {
145+
return sourceRelativePath
146+
}
147+
148+
throw ScriptError.fileNotFound(message: "Info.plist: \(infoPlistFile)")
149+
}
150+
151+
private func locateAppIconFolder(
152+
named appIcon: String,
153+
projectDir: String,
154+
sourceRootFolder: Folder
155+
) throws -> Folder? {
156+
let iconFolderName = "\(appIcon).appiconset"
157+
158+
if let projectFolder = try? Folder(path: projectDir),
159+
let iconFolder = projectFolder.findFirstFolder(name: iconFolderName) {
160+
return iconFolder
161+
}
162+
163+
return sourceRootFolder.findFirstFolder(name: iconFolderName)
164+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import AppKit
2+
import Foundation
3+
4+
extension NSColor {
5+
convenience init?(hexString: String) {
6+
let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
7+
let scanner = Scanner(string: hexString)
8+
9+
if hexString.hasPrefix("#") {
10+
_ = scanner.scanString("#")
11+
}
12+
13+
var color: UInt64 = 0
14+
guard scanner.scanHexInt64(&color) else {
15+
return nil
16+
}
17+
18+
let mask = 0x000000FF
19+
let r = Int(color >> 16) & mask
20+
let g = Int(color >> 8) & mask
21+
let b = Int(color) & mask
22+
23+
self.init(
24+
red: CGFloat(r) / 255.0,
25+
green: CGFloat(g) / 255.0,
26+
blue: CGFloat(b) / 255.0,
27+
alpha: 1
28+
)
29+
}
30+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Foundation
2+
import Moderator
3+
4+
protocol PrintableError {
5+
var errorDescription: String { get }
6+
}
7+
8+
enum ScriptError: Error {
9+
case generalError(message: String)
10+
case fileExists(message: String)
11+
case fileNotFound(message: String)
12+
case folderExists(message: String)
13+
case folderNotFound(message: String)
14+
case argumentError(message: String)
15+
case moreInfoNeeded(message: String)
16+
case renameFailed(message: String)
17+
}
18+
19+
extension ScriptError: PrintableError {
20+
var errorDescription: String {
21+
let prefix = "💥 error: "
22+
23+
switch self {
24+
case let .generalError(message):
25+
return prefix + message
26+
case let .fileExists(message):
27+
return prefix + "file exists: \(message)"
28+
case let .fileNotFound(message):
29+
return prefix + "file not found: \(message)"
30+
case let .folderExists(message):
31+
return prefix + "folder exists: \(message)"
32+
case let .folderNotFound(message):
33+
return prefix + "folder not found: \(message)"
34+
case let .argumentError(message):
35+
return prefix + "invalid argument: \(message)"
36+
case let .moreInfoNeeded(message):
37+
return prefix + "more info needed: \(message)"
38+
case let .renameFailed(message):
39+
return prefix + "rename failed: \(message)"
40+
}
41+
}
42+
}
43+
44+
extension ArgumentError: PrintableError {
45+
var errorDescription: String {
46+
"💥 error: \(errormessage)"
47+
}
48+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Files
2+
3+
extension Folder {
4+
func findFirstFile(name: String) -> File? {
5+
for file in files.recursive where file.name == name {
6+
return file
7+
}
8+
9+
return nil
10+
}
11+
12+
func findFirstFolder(name: String) -> Folder? {
13+
if self.name == name {
14+
return self
15+
}
16+
17+
for folder in subfolders.recursive where folder.name == name {
18+
return folder
19+
}
20+
21+
return nil
22+
}
23+
}

0 commit comments

Comments
 (0)