How to find unused images in an Xcode project?

XcodeAssets

Xcode Problem Overview


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.

Xcode Solutions


Solution 1 - Xcode

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

Solution 2 - Xcode

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

Solution 3 - Xcode

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.

Solution 4 - Xcode

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

Solution 5 - Xcode

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

Solution 6 - Xcode

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

Solution 7 - Xcode

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.

Solution 8 - Xcode

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

Solution 9 - Xcode

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!

Solution 10 - Xcode

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.

Solution 11 - Xcode

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.

Solution 12 - Xcode

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

Solution 13 - Xcode

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 = "[email protected]"
"""

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!")

Solution 14 - Xcode

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionelasticratView Question on Stackoverflow
Solution 1 - XcodeEd McManusView Answer on Stackoverflow
Solution 2 - XcodeRomanView Answer on Stackoverflow
Solution 3 - XcodeLessFunView Answer on Stackoverflow
Solution 4 - XcoderobView Answer on Stackoverflow
Solution 5 - XcodeArunView Answer on Stackoverflow
Solution 6 - XcodeBalazs NemethView Answer on Stackoverflow
Solution 7 - XcodeStakenborgView Answer on Stackoverflow
Solution 8 - XcodeGabriel MadrugaView Answer on Stackoverflow
Solution 9 - XcodeelpView Answer on Stackoverflow
Solution 10 - XcodeSamView Answer on Stackoverflow
Solution 11 - XcodeKunal ShahView Answer on Stackoverflow
Solution 12 - XcodeSwasidhantView Answer on Stackoverflow
Solution 13 - XcodeDevarshiView Answer on Stackoverflow
Solution 14 - XcodePraveen MatanamView Answer on Stackoverflow