Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Xamarin

Generate Icon Sets for Android and iOS in Native Mobile App Development

5.00/5 (3 votes)
13 Nov 2023CPOL4 min read 11.6K  
Use PowerShell scripts to generate icons of native mobile apps
Rather than using some online Web services, use PowerShell scripts and Inkscape to generate icons of native mobile apps. And Continuous Integration could be smoother.

Introduction

This article is for developers who already have some experience in developing Android apps and iOS apps using Xamarin. While there are quite a few icon libraries around, free or commercial, sometimes, you may want to generate icon sets from an existing image file, ideally, a SVG file.

This article is to share my experience in generating icons of various sizes from SVG files when developing native mobile apps. While the primary developing tool that I had used was Xamarin, the principles and the tools mentioned here should be applicable even if you have been using other tools like XCode or Android Studio.

XCode 14+ supports single-size app icons. And Android Studio includes Image Asset Studio that generates app icons from material icons and custom images. However, if you prefer not to use such interactive way for the sake of Continuous Integration, or you are using Xamarin, please read on.

Background

A few years ago, I developed Visual Acuity Charts which tests distant visual acuity for checking early sign of myopia. There are some freely available SVG icons and I used some online transformation tools to produce icon sets needed. And I had been using "Material icons generator" created by Nika Nikabadze for Visual Sutido, and https://appicon.co/.

Android Icon Resources in an Android library project of Microsoft Visual Studio

Image 1

iOS Icon Assets

Image 2

Image 3

However, the process of using these VS extension or online tools is rather troublesome:

  • With the VS extension, too many user interactions troubled me.
  • With the online tools: Upload SVG file -> Configuration -> Generate -> Wait -> Download zip -> Unzip to the project folders. And overall, such process is not friendly to Continuous Integration, thus I had written a series of PowerShell scripts to generate icon sets.

Overall, those two tools are fairly decent, however, if you feel the troubles that I had felt, you may try the approach introduced in this article.

Prerequisites

SVG Icon Libraries

There are many more out there, free or commercial.

Remarks

  • Make sure the usage of the free icons conform to the license such as SIL Open Font License (OFL) that Google Material Icons library use.
  • Google Material Icons site has provided downloads of image sets for Android and iOS in ZIPs.

Inkscape

"Inkscape is a Free and open source vector graphics editor for GNU/Linux, Windows and macOS."

As a software developer, I do casual and simple graphic design occasionally and I don't intend to buy an all-in-one, powerful and complex graphic design tool typically used by professional graphic designer. Inkscape satisfies what I need.

Composing Simple Icons

For example, rather than drawing everything from scratch, I used existing SVG files to compose the app launch icon.

The App Launcher Icon

Image 4

The Visibility icon from Google Material Icons

Image 5

Nevertheless, Inscape provides rich features for you to draw complex icons or images from scratch.

Adjust Existing Icons to Conform to Design Guidelines from Google and Apple

From time to time, you may find some candidates not so conforming to specific design guidelines and want to adjust the size and the margin.

References

Using the Code

The following PowerShell scripts utilize the command line ability of Inkscape, and the drafting of the scripts had referred to the icon design guidelines above.

Android

The following script generates icons for buttons and the app icon. Please read the in-source comment.

svgForAndroid.ps1

PowerShell
param([string]$svgFile, [int]$width, [int]$height, [boolean]$forAppIcon, 
      [string]$appIconName)
# Create icons for drawable and mipmap
# Examples:
# ./svgForAndroid.ps1 -svgFile "my.svg" -width 36
# ./svgForAndroid.ps1 "my.svg" 36
# For App Launcher Icons, 
# ref: https://developer.android.com/training/multiscreen/screendensities
# ./svgForAndroid.ps1 -svgFile "my.svg" -width 48 -forAppIcon $true 
# and this is excluding 36x36 ldpi
# The script file and the svg file should be in the same folder, 
# and the generated files will be in a sub-folder.
# For convenience of developing using Xamarin, follow such convension:
# Rename the svg file to something like send_36.svg if you want 36pt.
# Remarks: Android requires all resource file names are in lower case.

cd $PSScriptRoot

$baseFileName=[System.IO.Path]::GetFileNameWithoutExtension($svgFile)

function export([string]$subDir, [decimal]$ratio){
    $dir= [System.IO.Path]::Combine($baseFileName,$subDir)
    $exportedFileName=If ($forAppIcon) {If ($appIconName) {$appIconName+".png"} 
    Else {"5367533/ic_launcher.png"}} Else {$baseFileName+"_"+ $width + ".png"}
    $exported= [System.IO.Path]::Combine($dir, $exportedFileName)
    $dwidth=[int]($width * $ratio)
    $dheight=0
    if ($height -gt 0){$dheight =  $height * $ratio} Else {$dheight = $dwidth}
    $arguments="$svgFile --export-filename=$exported -w $dwidth -h $dheight -d 96"
    New-Item -ItemType Directory -Force -Path $dir

    $procArgs = @{
        FilePath         = "C:\Program Files\Inkscape\bin\inkscape.exe"
        ArgumentList     = $arguments
        PassThru         = $true
    }
    $processTsc = Start-Process @procArgs
}

If ($forAppIcon){
    export "mipmap-mdpi" 1
    export "mipmap-hdpi" 1.5
    export "mipmap-xhdpi" 2
    export "mipmap-xxhdpi" 3
    export "mipmap-xxxhdpi" 4
} Else {
    export "drawable-mdpi" 1
    export "drawable-hdpi" 1.5
    export "drawable-xhdpi" 2
    export "drawable-xxhdpi" 3
    export "drawable-xxxhdpi" 4
}

iOS

Image Set

The following scripts generate three png files and Contents.json.

svgForIOSImageset.ps1

PowerShell
param([string]$svgFile, [Int32]$width, [Int32]$height, [boolean]$original)
# Create icons for imageset.
# Examples:
# .\svgForAndroid.ps1 -svgFile "my.svg" -width 36 -original $true
# .\svgForIOSImageset.ps1 -svgFile "my.svg" -width 36
# If $original is false, template-rendering-intent in Contents.json 
# will become template  for visual effects such as replacing colors.
# The script file and the svg file should be in the same folder, 
# and the generated files will be in a sub-folder.

cd $PSScriptRoot

$baseFileName=[System.IO.Path]::GetFileNameWithoutExtension($svgFile)
$dir= $baseFileName + "_" + $width + ".imageset"
New-Item -ItemType Directory -Force -Path $dir

function export([decimal]$ratio){
    $exportedFileName=$baseFileName + "_" + $width +"pt_" + $ratio + "x.png"
    $exported= [System.IO.Path]::Combine($dir, $exportedFileName)
    $dwidth=[int]($width * $ratio)
    $dheight=0
    if ($height -gt 0){$dheight = $height * $ratio} Else {$dheight = $dwidth}
    $arguments="$svgFile --export-filename=$exported -w $dwidth -h $dheight -d 96"
    Write-Host $arguments
    $procArgs = @{
        FilePath         = "C:\Program Files\Inkscape\bin\inkscape.exe"
        ArgumentList     = $arguments
        PassThru         = $true
    }
    $processTsc = Start-Process @procArgs
    return $exportedFileName
}

$f1=export 1
$f2=export 2
$f3=export 3
$intent=If ($original) {"original"} Else {"template"}

$contentsTemplate=
@"
{
    "images": [
        {
            "filename": "$f1",
            "idiom": "universal",
            "scale": "1x"
        },
        {
            "filename": "$f2",
            "idiom": "universal",
            "scale": "2x"
        },
        {
            "filename": "$f3",
            "idiom": "universal",
            "scale": "3x"
        }
    ],
    "info": {
        "author": "whocare",
        "template-rendering-intent": "$intent",
        "version": 1
    }
}
"@

$jsonFile=[System.IO.Path]::Combine($dir, "Contents.json")
Set-Content $jsonFile $contentsTemplate

After running the scripts, you get these files in a folder like myicon.imageset:

Image 6

App Icon

After running the scripts, you get 26 files including Contents.json in a folder like myAppIcon.appiconset:

svgForIOSAppIconSet.ps1

PowerShell
param([string]$svgFile)
# Create AppIcons.appiconset of Assets.xcassets
# Examples:
# ./svgForIOSAppIconSet.ps1 -svgFile "my.svg"
# The script file and the svg file should be in the same folder, 
# and the generated files will be in a sub-folder like "my.appiconset".
cd $PSScriptRoot

$baseFileName=[System.IO.Path]::GetFileNameWithoutExtension($svgFile)
$dir= $baseFileName + ".appiconset"
New-Item -ItemType Directory -Force -Path $dir

function export([decimal]$size){
    $exportedFileName=$size.ToString() + ".png"
    $exported= [System.IO.Path]::Combine($dir, $exportedFileName)
    $arguments="$svgFile --export-filename $exported -w $size -h $size"

    $procArgs = @{
        FilePath         = "C:\Program Files\Inkscape\bin\inkscape.exe"
        ArgumentList     = $arguments
        PassThru         = $true
    }
    $processTsc = Start-Process @procArgs
    return $exportedFileName
}

function exportForWatch(){
    $exportedFileName="watch.png"
    $exported= [System.IO.Path]::Combine($dir, $exportedFileName)
    $arguments="$svgFile --export-filename $exported -w 55 -h 55"

    $procArgs = @{
        FilePath         = "C:\Program Files\Inkscape\bin\inkscape.exe"
        ArgumentList     = $arguments
        PassThru         = $true
    }
    $processTsc = Start-Process @procArgs
    return $exportedFileName
}

export 20
export 29
export 32
export 40
export 50
export 57
export 58
export 60
export 64
export 72
export 76
export 80
export 87
export 100
export 114
export 120
export 128
export 144
export 152
export 167
export 180
export 256
export 512
export 1024

exportForWatch 

$contentsTemplate=
@"
{
    "images": [
        {
            "size": "60x60",
            "expected-size": "180",
            "filename": "180.png",

            "idiom": "iphone",
            "scale": "3x"
        },
        {
            "size": "40x40",
            "expected-size": "80",
            "filename": "80.png",

            "idiom": "iphone",
            "scale": "2x"
        },
        {
            "size": "40x40",
            "expected-size": "120",
            "filename": "120.png",

            "idiom": "iphone",
            "scale": "3x"
        },
        {
            "size": "60x60",
            "expected-size": "120",
            "filename": "120.png",

            "idiom": "iphone",
            "scale": "2x"
        },
        {
            "size": "57x57",
            "expected-size": "57",
            "filename": "57.png",

            "idiom": "iphone",
            "scale": "1x"
        },
        {
            "size": "29x29",
            "expected-size": "58",
            "filename": "58.png",

            "idiom": "iphone",
            "scale": "2x"
        },
        {
            "size": "29x29",
            "expected-size": "29",
            "filename": "29.png",

            "idiom": "iphone",
            "scale": "1x"
        },
        {
            "size": "29x29",
            "expected-size": "87",
            "filename": "87.png",

            "idiom": "iphone",
            "scale": "3x"
        },
        {
            "size": "57x57",
            "expected-size": "114",
            "filename": "114.png",

            "idiom": "iphone",
            "scale": "2x"
        },
        {
            "size": "20x20",
            "expected-size": "40",
            "filename": "40.png",

            "idiom": "iphone",
            "scale": "2x"
        },
        {
            "size": "20x20",
            "expected-size": "60",
            "filename": "60.png",

            "idiom": "iphone",
            "scale": "3x"
        },
        {
            "size": "1024x1024",
            "filename": "1024.png",
            "expected-size": "1024",
            "idiom": "ios-marketing",

            "scale": "1x"
        },
        {
            "size": "40x40",
            "expected-size": "80",
            "filename": "80.png",

            "idiom": "ipad",
            "scale": "2x"
        },
        {
            "size": "72x72",
            "expected-size": "72",
            "filename": "72.png",

            "idiom": "ipad",
            "scale": "1x"
        },
        {
            "size": "76x76",
            "expected-size": "152",
            "filename": "152.png",

            "idiom": "ipad",
            "scale": "2x"
        },
        {
            "size": "50x50",
            "expected-size": "100",
            "filename": "100.png",

            "idiom": "ipad",
            "scale": "2x"
        },
        {
            "size": "29x29",
            "expected-size": "58",
            "filename": "58.png",

            "idiom": "ipad",
            "scale": "2x"
        },
        {
            "size": "76x76",
            "expected-size": "76",
            "filename": "76.png",

            "idiom": "ipad",
            "scale": "1x"
        },
        {
            "size": "29x29",
            "expected-size": "29",
            "filename": "29.png",

            "idiom": "ipad",
            "scale": "1x"
        },
        {
            "size": "50x50",
            "expected-size": "50",
            "filename": "50.png",

            "idiom": "ipad",
            "scale": "1x"
        },
        {
            "size": "72x72",
            "expected-size": "144",
            "filename": "144.png",

            "idiom": "ipad",
            "scale": "2x"
        },
        {
            "size": "40x40",
            "expected-size": "40",
            "filename": "40.png",

            "idiom": "ipad",
            "scale": "1x"
        },
        {
            "size": "83.5x83.5",
            "expected-size": "167",
            "filename": "167.png",

            "idiom": "ipad",
            "scale": "2x"
        },
        {
            "size": "20x20",
            "expected-size": "20",
            "filename": "20.png",

            "idiom": "ipad",
            "scale": "1x"
        },
        {
            "size": "20x20",
            "expected-size": "40",
            "filename": "40.png",

            "idiom": "ipad",
            "scale": "2x"
        },
        {
            "idiom": "watch",
            "filename": "watch.png",

            "subtype": "38mm",
            "scale": "2x",
            "size": "86x86",
            "expected-size": "172",
            "role": "quickLook"
        },
        {
            "idiom": "watch",
            "filename": "watch.png",

            "subtype": "38mm",
            "scale": "2x",
            "size": "40x40",
            "expected-size": "80",
            "role": "appLauncher"
        },
        {
            "idiom": "watch",
            "filename": "watch.png",

            "subtype": "42mm",
            "scale": "2x",
            "size": "98x98",
            "expected-size": "196",
            "role": "quickLook"
        },
        {
            "idiom": "watch",
            "filename": "watch.png",

            "subtype": "38mm",
            "scale": "2x",
            "size": "24x24",
            "expected-size": "48",
            "role": "notificationCenter"
        },
        {
            "idiom": "watch",
            "filename": "watch.png",

            "subtype": "42mm",
            "scale": "2x",
            "size": "27.5x27.5",
            "expected-size": "55",
            "role": "notificationCenter"
        },
        {
            "size": "29x29",
            "expected-size": "87",
            "filename": "87.png",

            "idiom": "watch",
            "role": "companionSettings",
            "scale": "3x"
        },
        {
            "idiom": "watch",
            "filename": "watch.png",

            "subtype": "42mm",
            "scale": "2x",
            "size": "44x44",
            "expected-size": "88",
            "role": "longLook"
        },
        {
            "size": "29x29",
            "expected-size": "58",
            "filename": "58.png",

            "idiom": "watch",
            "role": "companionSettings",
            "scale": "2x"
        },
        {
            "size": "1024x1024",
            "expected-size": "1024",
            "filename": "1024.png",

            "idiom": "watch-marketing",
            "scale": "1x"
        },
        {
            "size": "128x128",
            "expected-size": "128",
            "filename": "128.png",

            "idiom": "mac",
            "scale": "1x"
        },
        {
            "size": "256x256",
            "expected-size": "256",
            "filename": "256.png",

            "idiom": "mac",
            "scale": "1x"
        },
        {
            "size": "128x128",
            "expected-size": "256",
            "filename": "256.png",

            "idiom": "mac",
            "scale": "2x"
        },
        {
            "size": "256x256",
            "expected-size": "512",
            "filename": "512.png",

            "idiom": "mac",
            "scale": "2x"
        },
        {
            "size": "32x32",
            "expected-size": "32",
            "filename": "32.png",

            "idiom": "mac",
            "scale": "1x"
        },
        {
            "size": "512x512",
            "expected-size": "512",
            "filename": "512.png",

            "idiom": "mac",
            "scale": "1x"
        },
        {
            "size": "16x16",
            "expected-size": "32",
            "filename": "32.png",

            "idiom": "mac",
            "scale": "2x"
        },
        {
            "size": "32x32",
            "expected-size": "64",
            "filename": "64.png",

            "idiom": "mac",
            "scale": "2x"
        },
        {
            "size": "512x512",
            "expected-size": "1024",
            "filename": "1024.png",

            "idiom": "mac",
            "scale": "2x"
        }
    ]
}
"@

$jsonFile=[System.IO.Path]::Combine($dir, "Contents.json")
Set-Content $jsonFile $contentsTemplate 

Points of Interest

You may adjust the PowerShell scripts provided in this article according to your SDLC / CI process.

My experience in developing native mobile apps is limited to smartphone and tablet. Therefore, if you are developing native apps for smartwatches or smart TV, you may need to add a few more lines according to respective icon design specifications.

MAUI

For developing a native app for both Android and iOS, you need to create at least two platform specific app projects, one for Android and the other for iOS, while shared library codes are contained in Xamarin.Forms projects and .NET standard projects.

With MAUI, you just need one app project for Android, iOS, Windows and Mac, etc. And the icon sources can be vector images (SVG files). Apparently, MAUI will read the SVG files and generate PNG files according to the requirements of respective platforms, just as the script files in the article are doing. This sounds natural, promising and productive.

A few years ago, I had rewritten "Tour of Heroes" (the official tutorial app of Angular) for Xamarin, to demonstrate how to use WebApiClientGen in a real world project. In October 2023, I had migrated the app to MAUI. The deployable for Android built by Xamarin is around 22MB, however, the one by MAUI is around 33MB, while both are release build.

Such inflation in size is clearly not desirable. I will be interested in checking if Microsoft could fix this problem after .NET Conf 2023 and before the scheduled retirement of Xamarin in April 2024.

History

  • 31st August, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)