diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 25393f2ad8bb479ab75803fce63d3c6a175335fd..c8da51f167ea62914d323733ed006a92fbc5bac2 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -12998,6 +12998,7 @@ DD1190F9BEC2AD42AE2AF188 /* [CP] Copy Pods Resources */, 85C65C7820EE5A5A00C468B2 /* Embed Frameworks */, 5BC1D38E20D3C12B002A44B3 /* CopyFiles */, + B70A6A75216382EA0053BAB2 /* Check image-Assets state Script */, ); buildRules = ( ); @@ -13366,6 +13367,20 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-NynjaUnitTests/Pods-NynjaUnitTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + B70A6A75216382EA0053BAB2 /* Check image-Assets state Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check image-Assets state Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "start_time=`date +%s`\n./scripts/clean_assets.swift\nend_time=`date +%s`\necho execution time was `expr $end_time - $start_time` s."; + }; C00EEF59269812B03AB09233 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/scripts/clean_assets.swift b/scripts/clean_assets.swift new file mode 100755 index 0000000000000000000000000000000000000000..b069048eb9913b97005a7495c13ecc35dae22ea7 --- /dev/null +++ b/scripts/clean_assets.swift @@ -0,0 +1,180 @@ +#!/usr/bin/xcrun --sdk macosx swift + +import Foundation + +// MARK: - Helpers + +extension Array { + func dropLast(items amount: UInt) -> ArraySlice { + guard self.count > amount else { return [] } + let offsetIndex = self.count - Int(amount) + return self[0.. [String] { + var elements = [String]() + while let object = enumerator?.nextObject() as? String { + elements.append(object) + } + return elements +} + +// MARK: - List Assets +func listAssets() -> [String] { + let extensionName = "imageset" + let enumerator = FileManager.default.enumerator(atPath: assetCatalogPath) + return elementsInEnumerator(enumerator) + .filter { $0.hasSuffix(extensionName) } // Is Asset + .map { $0.replacingOccurrences(of: ".\(extensionName)", with: "") } // Remove extension + .map { $0.components(separatedBy: "/").last ?? $0 } // Remove folder path +} + +func cleanListAssets() -> [String] { + let extensionName = "imageset" + let enumerator = FileManager.default.enumerator(atPath: assetCatalogPath) + return elementsInEnumerator(enumerator) + .filter { $0.components(separatedBy: ".").count != 1 } + .filter { !$0.hasSuffix(".\(extensionName)") } + .filter { !$0.hasSuffix(".json") } + .filter { !$0.hasSuffix(".DS_Store") } + .filter { !$0.contains(".appiconset") } + .map { $0.replacingOccurrences(of: ".\(extensionName)", with: "") } +} + +func parseString(_ path: String) -> ImageAsset? { + let pathComponents: [String] = path.components(separatedBy: ".") + + guard let fileFormat = pathComponents.last else { print("error: No fileFormat found at path: \(path)"); return nil } + + let clearPath = path.replacingOccurrences(of: ".\(fileFormat)", with: "") + + let clearPathComponents = clearPath.components(separatedBy: "/") + guard let fileName = clearPathComponents.last else { print("error: No fileName found at path: \(path)"); return nil } + guard let assetName = clearPathComponents.dropLast().last else { print("error: No assetName found at path: \(path)"); return nil } + let nestedFilePath = clearPathComponents.dropLast(items: 2).filter{ !$0.isEmpty }.joined(separator: "/") + + let asset = ImageAsset.init(assetName: assetName, fileName: fileName, fileExtension: fileFormat, nestedInFolderPath: nestedFilePath) + + return asset +} + +func getAssetsList(for paths: [String]) -> [ImageAsset] { + return paths.compactMap{ parseString($0) } +} + + +// MARK: - List Used Assets in the codebase +func localizedStrings(inStringFile: String) -> [String] { + var localizedStrings = [String]() + let namePattern = "([\\w-\\s*]+)" + let patterns = [ + "#imageLiteral\\(resourceName: \"\(namePattern)\"\\)", // Image Literal + "ImageAsset\\(name:\\s*\"\(namePattern)\"\\)",//NYNJA codeGenerated Assets + "UIImage\\(named:\\s*\"\(namePattern)\"\\)", // Default UIImage call (Swift) + "UIImage imageNamed:\\s*\\@\"\(namePattern)\"", // Default UIImage call + "\\ [String] { + let enumerator = FileManager.default.enumerator(atPath:sourcePath) + + return elementsInEnumerator(enumerator) + .filter { $0.hasSuffix(".m") || $0.hasSuffix(".swift") || $0.hasSuffix(".xib") || $0.hasSuffix(".storyboard") } // Only Swift and Obj-C files + .filter { !$0.contains("Pods") && !$0.contains("Frameworks") } + .map { "\(sourcePath)/\($0)" } // Build file paths + .map { try? String(contentsOfFile: $0, encoding: .utf8)} // Get file contents + .compactMap{$0} + .compactMap{$0} // Remove nil entries + .map(localizedStrings) // Find localizedStrings ocurrences + .flatMap{$0} // Flatten +} + + +// MARK: - Begining of script +let assets = Set(listAssets()) +let used = Set(listUsedAssetLiterals() + ignoredUnusedNames) + +print("warning: SourcePath \(sourcePath)") + +print("Used Assets: \(used.count)") +//print(used) + +// Generate Warnings for Unused Assets +let unused = assets.subtracting(used) +unused.forEach { print("error: [Asset Unused] \($0)") } + +// Generate Error for broken Assets +let broken = used.subtracting(assets) +broken.forEach { print("error: [Asset Missing] \($0)") } + +let paths = cleanListAssets() +let imgAssets = getAssetsList(for: paths) + +// Filter out files e.g. @2x, @3x from single asset catalog to print out only main assetName and not allow sized files to be in the list +let filteredAssets = Dictionary(grouping: imgAssets, by: { $0.nestedInFolderPath + $0.assetName }).filter{ $0.value.count > 0 }.compactMap{ $0.value.first } + +let notPdf = filteredAssets.filter{ $0.fileExtension.lowercased() != "pdf" }.map{ $0.assetName } +print("Used Not PDF: \(notPdf.count)") +notPdf.forEach { print("warning: [Not PDF] \($0)") } + +let duplicates = Dictionary(grouping: filteredAssets, by: { $0.assetName }).filter {$0.value.count > 1} + +if broken.count > 0 { + exit(1) +}