Skip to content

Instantly share code, notes, and snippets.

@dyerw
Last active February 28, 2024 05:09
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dyerw/6af307d4703361daed24 to your computer and use it in GitHub Desktop.
Save dyerw/6af307d4703361daed24 to your computer and use it in GitHub Desktop.
AssetCleaner - Removes all unused assets from an XCode project
USAGE = \
"""
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:
continue
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 = f.read()
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)))
sys.stdout.write("\n")
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()),
images)
# 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'),
os.listdir(image_locations[image]))))
.issubset(images)]
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):
shutil.rmtree(asset)
@ndkhoa96
Copy link

Hi author
Can you share how to run this script?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment