107

Has anyone a one-line to find unused images in an Xcode project? (Assuming all the files are referenced by name in code or the project files - no code generated file names.)

These files tend to build up over the life of a project and it can be hard to tell if it's safe to delete any given png.

Ahmad F
  • 30,560
  • 17
  • 97
  • 143
elasticrat
  • 7,060
  • 5
  • 36
  • 36

14 Answers14

80

This is a more robust solution - it checks for any reference to the basename in any text file. Note the solutions above that didn't include storyboard files (completely understandable, they didn't exist at the time).

Ack makes this pretty fast, but there are some obvious optimizations to make if this script runs frequently. This code checks every basename twice if you have both retina/non-retina assets, for example.

#!/bin/bash

for i in `find . -name "*.png" -o -name "*.jpg"`; do 
    file=`basename -s .jpg "$i" | xargs basename -s .png | xargs basename -s @2x`
    result=`ack -i "$file"`
    if [ -z "$result" ]; then
        echo "$i"
    fi
done

# Ex: to remove from git
# for i in `./script/unused_images.sh`; do git rm "$i"; done
Ed McManus
  • 7,088
  • 2
  • 25
  • 17
  • 12
    Install [Homebrew](http://mxcl.github.com/homebrew/) and then do a `brew install ack`. – Marko Aug 07 '12 at 12:56
  • 1
    Thanks. This answer also handles files and folders with spaces in correctly. – djskinner Oct 27 '12 at 10:21
  • Excuse my incompetence but how would I execute this on my project folder? I tried both "sh FindUnusedImages.sh /Development/ProjectFolder" and "bash FindUnusedImages.sh /Development/ProjectFolder". – Johnny Jul 22 '13 at 18:05
  • 2
    @Johnny you need to make the file executable (`chmod a+x FindUnusedImages.sh`), then run it like any other program from bash `./FindUnusedImages.sh` – Mike Sprague Aug 01 '13 at 16:38
  • 2
    I've made a modification to ignore pbxproj files (thus ignoring files that are in the xcode project, but not used in code or nibs/storyboards): `result=\`ack --ignore-file=match:/.\.pbxproj/ -i "$file"\`` This requires ack 2.0 and up – Mike Sprague Aug 01 '13 at 17:16
  • @msprague Thank you! That worked like a charm. I knew it was something silly I was missing. I appreciate it. – Johnny Aug 03 '13 at 15:50
  • @msprague how about `--type=objc` instead of `--ignore blah blah` – kas-kad Sep 06 '13 at 22:30
  • 1
    That doesn't search xibs or storyboards for references. So if there's a resource that's being used in a xib, but not in the code, it won't show up. – Mike Sprague Sep 11 '13 at 00:15
  • where should I put this script so It can search in my current project? I put that file in my current project but it seems that It's searching in complete drive. – MilanPanchal Dec 23 '13 at 08:16
  • 2
    milanpanchal, you can put the script anywhere, and you just execute it from whatever directory you want to use as the root for searching for images (e.g. your project root folder). You can put it in ~/script/ for example and then go to your project root folder and run it by pointing to the script directly: ~/script/unused_images.sh – Erik van der Neut May 21 '14 at 03:11
  • Does `ack` support `.xib`, `.storyboard`, and `.swift` ? – AechoLiu Nov 28 '16 at 08:04
  • Getting error : Error: Failed to download resource "ack" Download failed: https://beyondgrep.com/ack-2.18-single-file. Their SSL certificate has expired it seems. Any other way to install ack? – atulkhatri May 20 '17 at 16:16
  • You'd probably want to add `*.imageset` (both in `find` and `xargs basename`) for asset catalog entries plus `--nojson` to ignore the `Contents.json` asset catalog files. Though obviously a more specific filter would be needed I you have references to your assets in any custom JSON files. – Patrick Pijnappel Oct 23 '18 at 01:08
61

For files which are not included in project, but just hang-around in the folder, you can press

cmd ⌘ + alt ⌥ + A

and they won't be grayed out.

For files which are not referenced neither in xib nor in code, something like this might work:

#!/bin/sh
PROJ=`find . -name '*.xib' -o -name '*.[mh]'`

find . -iname '*.png' | while read png
do
    name=`basename $png`
    if ! grep -qhs "$name" "$PROJ"; then
        echo "$png is not referenced"
    fi
done
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Roman
  • 13,100
  • 2
  • 47
  • 63
  • 6
    If you encounter error: No such file or directory, it is probably due to the spaces in the file path. The quotes needs to be added in grep line, so it goes: if ! grep -qhs "$name" "$PROJ"; – Lukasz Nov 12 '12 at 11:37
  • 9
    One scenario where this wouldn't work out is when we might load images programmatically after constructing their names. Like arm1.png, arm2.png.... arm22.png. I might construct their names in the for loop and load. E.g. Games – Rajavanya Subramaniyan Jan 30 '13 at 07:15
  • If you have images for retina display named with @2x they will list as unused. You can get rid of that by adding an extra if-statement: if [[ "$name" != *@2x* ]]; then – Sten Jul 03 '13 at 09:49
  • 3
    Cmd+Opt+a seems no longer to work on XCode 5. What does should it trigger? – powtac Apr 29 '14 at 11:37
  • cmd+opt+a doesn't seem to gray out files in Images.xcassets even though they are a part of the project :( – tettoffensive Apr 13 '16 at 16:39
  • @RajavanyaSubramaniyan, That's why I recently started adding a comment next to the loading code line with all the file names or the first one if there are too many. – Iulian Onofrei Aug 10 '17 at 15:00
  • Not working I run script in my project folder and when I checked one of listed image that is marked as not referenced in project using search then I found that image is used in code `fifthPage.iconImageView.image = [UIImage imageNamed:@"enjoy.png"];` – Varun Naharia Jul 25 '18 at 05:44
  • If your project utilizes asset catalog, then instead of finding images with *.png, you could use *.imageset and you won't have to prune "@2x", "@3x" etc as the basename of *.imageset would be enough to look for in the storyboard, xib, swift, .m files. – Kunal Shrivastava Jun 11 '19 at 20:31
29

Please have a try LSUnusedResources.

It is heavily influenced by jeffhodnett‘s Unused, but honestly Unused is very slow, and the results are not entirely correct. So I made some performance optimization, the search speed is more faster than Unused.

LessFun
  • 399
  • 3
  • 11
  • 2
    Wow that is a great tool! Much nicer than trying to run those scripts. You can visually see all of the images not used, and delete the ones you wish. One gotcha I found though is it does not pick up images referenced in the plist – RyanG Dec 17 '15 at 17:34
  • 1
    Definitely awesome and save my day! Best solution in thread. You rock. – Jakehao Apr 14 '16 at 01:50
  • 2
    Best one in thread. I wish this was higher up and I could up-vote more than once! – Yoav Schwartz Jan 02 '17 at 12:50
  • Do you know if there is something similar to this but for dead code detection? For instance, for methods no longer called (at least no longer *statically called*). – superpuccio Oct 11 '17 at 21:00
24

I tried Roman's solution, and I added a few tweaks to handle retina images. It works well, but remember that image names can be generated programmatically in code, and this script would incorrectly list these images as unreferenced. For example, you might have

NSString *imageName = [NSString stringWithFormat:@"image_%d.png", 1];

This script will incorrectly think image_1.png is unreferenced.

Here's the modified script:

#!/bin/sh
PROJ=`find . -name '*.xib' -o -name '*.[mh]' -o -name '*.storyboard' -o -name '*.mm'`

for png in `find . -name '*.png'`
do
   name=`basename -s .png $png`
   name=`basename -s @2x $name`
   if ! grep -qhs "$name" "$PROJ"; then
        echo "$png"
   fi
done
avelis
  • 1,143
  • 1
  • 9
  • 18
rob
  • 4,069
  • 3
  • 34
  • 41
  • what does the _@2x_ do in the suffix switch for basename? – ThaDon Dec 31 '11 at 13:37
  • The first call to basename will delete the .png suffix and the second call deletes the @2x suffix. The suffixes are deleted from the filenames that find returns. It then looks for the basenames in $PROJ, which contains all the project filenames. – rob Jan 01 '12 at 18:34
  • ah ok, so you actually have images named `something@2x.png` in your system? I thought perhaps `@2x` was some sort of weird regexp or directive that `basename` understood – ThaDon Jan 03 '12 at 17:03
  • Yes, imagename@2x.png images are double size for the retina display. – rob Jan 03 '12 at 19:57
  • I modified the first line such as : PROJ=`find . -name '*.xib' -o -name '*.[mh]' -o -name '*.storyboard' -o -name '*.mm'` because sometimes, you need the mm extension instead of .m – Redwarp Jul 04 '12 at 10:40
  • You should also add `-o -name '*Info.plist'` :) – KPM Sep 26 '12 at 20:56
  • This doesn't return any results regardless of how many extra images I drop in the folder. Is there something that needs to be done other than `sh findImages.sh`? – memmons Oct 01 '12 at 21:13
  • 3
    FYI, folders with spaces in the name cause issues with the script. – Steve Oct 10 '12 at 03:59
  • 3
    If you encounter error: No such file or directory, it is probably due to the spaces in the file path. The quotes needs to be added in grep line, so it goes: if ! grep -qhs "$name" "$PROJ"; – Lukasz Nov 12 '12 at 11:38
  • To further deal with spaces in file/directory names, replace the for statement with the following: find . -iname '*.png' -print0 | while read -d $'\0' png – David Hunt Jun 14 '13 at 16:32
  • 3
    This script lists all my files – jjxtra Jul 18 '13 at 19:38
  • 2
    i dunno why its not working for me its giving me all the png images – Omer Obaid Mar 06 '14 at 06:40
  • The script also fails, understandably, to spot images referenced in source code such as resourceName = self.isSelected ? @"ODSharedIcon_AvatarFrame_Mine_Selected" : @"ODSharedIcon_AvatarFrame_Mine_Unselected"; So, watch out before you start removing images! – Martin-Gilles Lavoie Nov 09 '18 at 17:01
  • Script does not work for storyboard files as the images referenced in storyboard do not have .png extension in Interface Builder – maven25 Feb 06 '21 at 17:21
13

May be you can try slender, does a decent job.

update: With emcmanus idea, I went ahead and create a small util with no ack just to avoid additional setup in a machine.

https://github.com/arun80/xcodeutils

James McMahon
  • 48,506
  • 64
  • 207
  • 283
Arun
  • 336
  • 5
  • 14
  • 1
    Slender is paid app. several false positives and not good for commercial products. script provided by emcmanus is really great. – Arun Nov 15 '12 at 16:06
6

Only this script is working for me which is even handling the space in the filenames:

Edit

Updated to support swift files and cocoapod. By default it's excluding the Pods dir and check only the project files. To run to check the Pods folder as well, run with --pod attrbiute :

/.finunusedimages.sh --pod

Here is the actual script:

#!/bin/sh

#varables
baseCmd="find ." 
attrs="-name '*.xib' -o -name '*.[mh]' -o -name '*.storyboard' -o -name '*.mm' -o -name '*.swift'"
excudePodFiles="-not \( -path  */Pods/* -prune \)"
imgPathes="find . -iname '*.png' -print0"


#finalize commands
if [ "$1" != "--pod" ]; then
    echo "Pod files excluded"
    attrs="$excudePodFiles $attrs"
    imgPathes="find . $excudePodFiles -iname '*.png' -print0"
fi

#select project files to check
projFiles=`eval "$baseCmd $attrs"`
echo "Looking for in files: $projFiles"

#check images
eval "$imgPathes" | while read -d $'\0' png
do
   name=`basename -s .png "$png"`
   name=`basename -s @2x $name`
   name=`basename -s @3x $name`

   if grep -qhs "$name" $projFiles; then
        echo "(used - $png)"
   else
        echo "!!!UNUSED - $png"
   fi
done
Community
  • 1
  • 1
Balazs Nemeth
  • 2,333
  • 19
  • 29
3

I made a very slight modification to the excellent answer provided by @EdMcManus to handle projects utilizing asset catalogs.

#!/bin/bash

for i in `find . -name "*.imageset"`; do
    file=`basename -s .imageset "$i"`
    result=`ack -i "$file" --ignore-dir="*.xcassets"`
    if [ -z "$result" ]; then
        echo "$i"
    fi
done

I don't really write bash scripts, so if there are improvements to be made here (likely) let me know in the comments and I'll update it.

Stakenborg
  • 2,890
  • 2
  • 24
  • 30
  • I have an issue with spaces in files name. I've found out that is useful to set ` IFS=$'\n' `, just before the code (this one sets the internal field separator to new line) - won't work if again files have new lines in name. – Laura Calinoiu Jan 10 '17 at 13:10
3

Using the other answers, this one is a good example of how to ignore images on two directories and do not search occurrences of the images on the pbxproj or xcassets files (Be careful with the app icon and splash screens). Change the * in the --ignore-dir=*.xcassets to match your directory:

#!/bin/bash

for i in `find . -not \( -path ./Frameworks -prune \) -not \( -path ./Carthage -prune \) -not \( -path ./Pods -prune \) -name "*.png" -o -name "*.jpg"`; do 
    file=`basename -s .jpg "$i" | xargs basename -s .png | xargs basename -s @2x | xargs basename -s @3x`
    result=`ack -i --ignore-file=ext:pbxproj --ignore-dir=*.xcassets "$file"`
    if [ -z "$result" ]; then
        echo "$i"
    fi
done
Gabriel Madruga
  • 201
  • 2
  • 5
2

I wrote a lua script, I'm not sure I can share it because I did it at work, but it works well. Basically it does this:

Step one- static image references (the easy bit, covered by the other answers)

  • recursively looks through image dirs and pulls out image names
  • strips the image names of .png and @2x (not required/used in imageNamed:)
  • textually searches for each image name in the source files (must be inside string literal)

Step two- dynamic image references (the fun bit)

  • pulls out a list of all string literals in source containing format specifiers (eg, %@)
  • replaces format specifiers in these strings with regular expressions (eg, "foo%dbar" becomes "foo[0-9]*bar"
  • textually searches through the image names using these regex strings

Then deletes whatever it didn't find in either search.

The edge case is that image names that come from a server aren't handled. To handle this we include the server code in this search.

Sam
  • 3,659
  • 3
  • 36
  • 49
  • Neat. Out of curiosity is there some utility for transforming format specifiers to wildcard regexes? Just thinking there's a lot of complexity you'd have to handle to accurately accomodate all specifiers and platforms. [(Format specifier docs)](https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html) – Ed McManus Jan 19 '13 at 08:09
2

You can try FauxPas App for Xcode. It is really good in findings the missing images and a lot of other issues/ violations related to Xcode project.

Kunal Shah
  • 51
  • 4
2

I used this framework:-

http://jeffhodnett.github.io/Unused/

Works damn well! Only 2 places I saw issues are when image names are from server and when the image asset name is different from the name of the image inside the asset folder...

Swasidhant
  • 1,231
  • 1
  • 12
  • 13
  • This doesn't look for assets, only for image files that aren't directly referenced. If you're using Assets like you should, this tool will unfortunately not work for you. – Robin Daugherty Mar 01 '20 at 21:22
2

You can make a shell script that grep your source code and compare the founded images with your project folder.

Here the man(s) for GREP and LS

Easily you can loop all of your source file, save images in array or something equals and use

cat file.m | grep [-V] myImage.png

With this trick, you can search all images in your project source code!!

hope this helps!

elp
  • 8,021
  • 7
  • 61
  • 120
1

I have created a python script to identify the unused images: 'unused_assets.py' @ gist. It can be used like this:

python3 unused_assets.py '/Users/DevK/MyProject' '/Users/DevK/MyProject/MyProject/Assets/Assets.xcassets'

Here are few rules to use the script:

  • It is important to pass project folder path as first argument, assets folder path as second argument
  • It is assumed that all the images are maintained within Assets.xcassets folder and are used either within swift files or within storyboards

Limitations in first version:

  • Doesn't work for objective c files

I will try to improve it over the time, based on feedback, however the first version should be good for most.

Please find below the code. The code should be self explanatory as I have added appropriate comments to each important step.

# Usage e.g.: python3 unused_assets.py '/Users/DevK/MyProject' '/Users/DevK/MyProject/MyProject/Assets/Assets.xcassets'
# It is important to pass project folder path as first argument, assets folder path as second argument
# It is assumed that all the images are maintained within Assets.xcassets folder and are used either within swift files or within storyboards

"""
@author = "Devarshi Kulshreshtha"
@copyright = "Copyright 2020, Devarshi Kulshreshtha"
@license = "GPL"
@version = "1.0.1"
@contact = "kulshreshtha.devarshi@gmail.com"
"""

import sys
import glob
from pathlib import Path
import mmap
import os
import time

# obtain start time
start = time.time()

arguments = sys.argv

# pass project folder path as argument 1
projectFolderPath = arguments[1].replace("\\", "") # replacing backslash with space
# pass assets folder path as argument 2
assetsPath = arguments[2].replace("\\", "") # replacing backslash with space

print(f"assetsPath: {assetsPath}")
print(f"projectFolderPath: {projectFolderPath}")

# obtain all assets / images 
# obtain paths for all assets

assetsSearchablePath = assetsPath + '/**/*.imageset'  #alternate way to append: fr"{assetsPath}/**/*.imageset"
print(f"assetsSearchablePath: {assetsSearchablePath}")

imagesNameCountDict = {} # empty dict to store image name as key and occurrence count
for imagesetPath in glob.glob(assetsSearchablePath, recursive=True):
    # storing the image name as encoded so that we save some time later during string search in file 
    encodedImageName = str.encode(Path(imagesetPath).stem)
    # initializing occurrence count as 0
    imagesNameCountDict[encodedImageName] = 0

print("Names of all assets obtained")

# search images in swift files
# obtain paths for all swift files

swiftFilesSearchablePath = projectFolderPath + '/**/*.swift' #alternate way to append: fr"{projectFolderPath}/**/*.swift"
print(f"swiftFilesSearchablePath: {swiftFilesSearchablePath}")

for swiftFilePath in glob.glob(swiftFilesSearchablePath, recursive=True):
    with open(swiftFilePath, 'rb', 0) as file, \
        mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) as s:
        # search all the assests within the swift file
        for encodedImageName in imagesNameCountDict:
            # file search
            if s.find(encodedImageName) != -1:
                # updating occurrence count, if found 
                imagesNameCountDict[encodedImageName] += 1

print("Images searched in all swift files!")

# search images in storyboards
# obtain path for all storyboards

storyboardsSearchablePath = projectFolderPath + '/**/*.storyboard' #alternate way to append: fr"{projectFolderPath}/**/*.storyboard"
print(f"storyboardsSearchablePath: {storyboardsSearchablePath}")
for storyboardPath in glob.glob(storyboardsSearchablePath, recursive=True):
    with open(storyboardPath, 'rb', 0) as file, \
        mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) as s:
        # search all the assests within the storyboard file
        for encodedImageName in imagesNameCountDict:
            # file search
            if s.find(encodedImageName) != -1:
                # updating occurrence count, if found
                imagesNameCountDict[encodedImageName] += 1

print("Images searched in all storyboard files!")
print("Here is the list of unused assets:")

# printing all image names, for which occurrence count is 0
print('\n'.join({encodedImageName.decode("utf-8", "strict") for encodedImageName, occurrenceCount in imagesNameCountDict.items() if occurrenceCount == 0}))

print(f"Done in {time.time() - start} seconds!")
Devarshi
  • 16,440
  • 13
  • 72
  • 125
0

Use http://jeffhodnett.github.io/Unused/ to find the unused images.

Praveen Matanam
  • 2,773
  • 1
  • 20
  • 24
  • It seems to me that neither this app handles well the space in the folder names. And it quite slow for one of my larger project. – Balazs Nemeth Jun 01 '15 at 23:37