During development I prefer to keep separate dev/qa/production versions of my application installed on device. This can be simply achieved by using different bundle ids. But they all will look the same on device springboard. Earlier I stored icon name in configs to use different icons but it still require to manually create and add this icons. And it have no valuable information apart of differentiating between types of builds.

In this article I will show a script to overlay icon with information from application Info.plist, xcconfigs or git output.

Available information

To extract information from application Info.plist use:

version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}"`

For terminal output (like git):

branch=`${git} rev-parse --abbrev-ref HEAD`

For variables from xcconfig:

scheme_Name="${APP_SCHEME_NAME}"

Tools

Tools required for image proccesing are: ImageMagick and ghostscript. You can use brew to quickly install them:

brew install imagemagick
brew install ghostscript

Overlaying script

Add script to new build phase: ProjectName > TargetName > Build Phases > New Run Script Phase.

Script can be just dropped as a text here, but I prefer to store and it separately, so I am adding:

sh Scripts/iconVersioning.sh

Adding Build Phase

To prevent overlaying icon on release builds it will exit on “Release” configuration:

if [ $CONFIGURATION = "Release" ]; then
  cp "${base_path}" "$target_path"
  return 0;
fi

And here is whole shell script:

#!/bin/sh
export PATH=/opt/local/bin/:/opt/local/sbin:$PATH:/usr/local/bin:

convertPath=`which convert`
echo ${convertPath}
if [[ ! -f ${convertPath} || -z ${convertPath} ]]; then
    echo "WARNING: Skipping Icon versioning, you need to install ImageMagick, you can use brew to simplify process:
    brew install imagemagick"
exit 0;
fi

gsPath=`which gs`
echo ${gsPath}
if [[ ! -f ${gsPath} || -z ${gsPath} ]]; then
    echo "WARNING: Skipping Icon versioning, you need to install ghostscript (fonts) first, you can use brew to simplify process:
    brew install ghostscript"
exit 0;
fi

git=`sh /etc/profile; which git`
branch=`${git} rev-parse --abbrev-ref HEAD`
commits_count=`${git} rev-list ${branch} | wc -l  | tr -d ' '`
commit=`${git} rev-parse --short HEAD`
version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}"`

DIR="${BASH_SOURCE%/*}"
if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi

branch="${branch}->${APP_SCHEME_NAME}"

shopt -s extglob
shopt -u extglob
caption="${version} ($commits_count)\n${branch}"
echo $caption

function abspath() { pushd . > /dev/null; if [ -d "$1" ]; then cd "$1"; dirs -l +0; else cd "`dirname \"$1\"`"; cur_dir=`dirs -l +0`; if [ "$cur_dir" == "/" ]; then echo "$cur_dir`basename \"$1\"`"; else echo "$cur_dir/`basename \"$1\"`"; fi; fi; popd > /dev/null; }

function processIcon() {
    base_file=$1
    
    cd "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
    base_path=`find . -name ${base_file}`
    
    real_path=$( abspath "${base_path}" )
    echo "base path ${real_path}"
    
    if [[ ! -f ${base_path} || -z ${base_path} ]]; then
      return;
    fi
    
    # TODO: if they are the same we need to fix it by introducing temp
    target_file=`basename $base_path`
    target_path="${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/${target_file}"
    
    base_tmp_normalizedFileName="${base_file%.*}-normalized.${base_file##*.}"
    base_tmp_path=`dirname $base_path`
    base_tmp_normalizedFilePath="${base_tmp_path}/${base_tmp_normalizedFileName}"
    
    stored_original_file="${base_tmp_normalizedFilePath}-tmp"
    if [[ -f ${stored_original_file} ]]; then
      echo "found previous file at path ${stored_original_file}, using it as base"
      mv "${stored_original_file}" "${base_path}"
    fi
    
    if [ $CONFIGURATION = "Release" ]; then
      cp "${base_path}" "$target_path"
      return 0;
    fi
    
    echo "Reverting optimized PNG to normal"
    # Normalize
    echo "xcrun -sdk iphoneos pngcrush -revert-iphone-optimizations -q ${base_path} ${base_tmp_normalizedFilePath}"
    xcrun -sdk iphoneos pngcrush -revert-iphone-optimizations -q "${base_path}" "${base_tmp_normalizedFilePath}"
    
    # move original pngcrush png to tmp file
    echo "moving pngcrushed png file at ${base_path} to ${stored_original_file}"
    #rm "$base_path"
    mv "$base_path" "${stored_original_file}"
    
    # Rename normalized png's filename to original one
    echo "Moving normalized png file to original one ${base_tmp_normalizedFilePath} to ${base_path}"
    mv "${base_tmp_normalizedFilePath}" "${base_path}"
    
    width=`identify -format %w ${base_path}`
    height=`identify -format %h ${base_path}`
    band_height=$((($height * 30) / 100))
    band_position=$(($height - $band_height))
    text_position=$(($band_position - 3))
    point_size=$(((13 * $width) / 100))
    
    echo "Image dimensions ($width x $height) - band height $band_height @ $band_position - point size $point_size"
    
    #
    # blur band and text
    #
    convert ${base_path} -blur 10x8 /tmp/blurred.png
    convert /tmp/blurred.png -gamma 0 -fill white -draw "rectangle 0,$band_position,$width,$height" /tmp/mask.png
    convert -size ${width}x${band_height} xc:none -fill 'rgba(0,0,0,0.2)' -draw "rectangle 0,0,$width,$band_height" /tmp/labels-base.png
    convert -background none -size ${width}x${band_height} -pointsize $point_size -fill white -gravity center -gravity South caption:"$caption" /tmp/labels.png
    
    convert ${base_path} /tmp/blurred.png /tmp/mask.png -composite /tmp/temp.png
    
    rm /tmp/blurred.png
    rm /tmp/mask.png
    
    #
    # compose final image
    #
    filename=New${base_file}
    convert /tmp/temp.png /tmp/labels-base.png -geometry +0+$band_position -composite /tmp/labels.png -geometry +0+$text_position -geometry +${w}-${h} -composite "${target_path}"
    
    # clean up
    rm /tmp/temp.png
    rm /tmp/labels-base.png
    rm /tmp/labels.png
    
    echo "Overlayed ${target_path}"
}

icon_count=`/usr/libexec/PlistBuddy -c "Print CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles" "${CONFIGURATION_BUILD_DIR}/${INFOPLIST_PATH}" | wc -l`
last_icon_index=$((${icon_count} - 2))

i=0
while [  $i -lt $last_icon_index ]; do
  icon=`/usr/libexec/PlistBuddy -c "Print CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles:$i" "${CONFIGURATION_BUILD_DIR}/${INFOPLIST_PATH}"`

  if [[ $icon == *.png ]] || [[ $icon == *.PNG ]]
  then
    processIcon $icon
  else
    processIcon "${icon}.png"
    processIcon "${icon}@2x.png"
    processIcon "${icon}@3x.png"
  fi
  let i=i+1
done

# Workaround to fix issue#16 to use wildcard * to actually find the file
# Only 72x72 and 76x76 that we need for ipad app icons
processIcon "AppIcon72x72~ipad*"
processIcon "AppIcon72x72@2x~ipad*"
processIcon "AppIcon76x76~ipad*"
processIcon "AppIcon76x76@2x~ipad*"

Result

Overlayed icons

Acknowledgments

This is mostly extraction and adaptation of technique from KZBootstrap by Krzysztof Zabłocki.