Skip to content

Instantly share code, notes, and snippets.

Last active October 26, 2021 13:27
What would you like to do?
AssetCleaner - Removes all unused assets from an XCode project
Searches an XCode project to determine which image
assets aren't being used then removes them.
WARNING: This will delete things permanently if your
project is not under source control.
import os
import sys
import shutil
import argparse
# Handle command line stuff
parser = argparse.ArgumentParser(description='Remove unused assets from XCode.', epilog=USAGE)
parser.add_argument('dir', help='location of XCode project')
parser.add_argument('-d', '--dry', help='don\'t remove assets, just display what would be removed', action='store_true')
parser.add_argument('-f', '--filter', help='only remove assets beginning with a string, non case sensitive', nargs=1)
args = parser.parse_args()
dir_to_clean = args.dir
dry_run = args.dry
prefix_filter = args.filter[0] if args.filter else None
# Extensions in which we're looking for used assets
EXTENSIONS = ['.h', '.m', '.xib', '.plist', '.storyboard', '.swift', '.html']
def clean_png_filename(filename):
Takes a png filename and strips the string to how it would be
found in the code.
e.g. "text_field_grey@2x.png" -> "text_field_grey"
return filename.split("@")[0].split(".")[0]
if dry_run:
print "~~ Dry Run, nothing will be removed ~~"
# Gets a list of all files with any of the given extensions
# also gets a list of all image files
files_to_search = []
images = []
image_locations = {} # We use this to figure out what directories to delete later
for root, dirs, files in os.walk(dir_to_clean):
# AppIcon isn't directly referenced in the source, but we don't want to delete it
if "AppIcon" in root:
files_to_search += [root + "/" + valid_file
for valid_file in files
if any([valid_file.lower().endswith(extension)
for extension in EXTENSIONS])]
new_images = set([clean_png_filename(image_file)
for image_file in files
if image_file.lower().endswith('.png')])
for image in new_images:
image_locations[image] = root
images += new_images
files_left = len(files_to_search)
sys.stdout.write("%d files to search | %d images remaining" % (files_left, len(images)))
# Search each file for each string
for search_file in files_to_search:
with open(search_file, 'r') as f:
file_text =
for image in images:
if image in file_text:
images = filter(lambda imagename: imagename != image, images)
files_left -= 1
sys.stdout.write("\r%d files to search | %d images remaining" % (files_left, len(images)))
print "Found %d unused images" % len(images)
# Filter images based on supplied prefix
if prefix_filter:
images = filter(lambda imagename: imagename.lower().startswith(prefix_filter.lower()),
# We're getting the folder locations of each image
# but only if that path contains ".xcassets"
# and if all the '.png' files in that folder are
# in the list of images to be deleted i.e. no images
# that aren't in the list are in the directory
assets_to_remove = [image_locations[image]
for image in images
if ".xcassets" in image_locations[image] \
and set(map(clean_png_filename,
filter(lambda filename: filename.lower().endswith('.png'),
print "Deleting %d assets" % len(assets_to_remove)
print "Removing: \n%s\n" % "\n".join(assets_to_remove)
if not dry_run:
for asset in assets_to_remove:
if os.path.exists(asset):
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment