You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by dp...@apache.org on 2024/08/21 07:22:28 UTC

(cordova-ios) branch master updated: feat(icons)!: Support a single 1024⨉1024 icon, but also more complex customizations (#1465)

This is an automated email from the ASF dual-hosted git repository.

dpogue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-ios.git


The following commit(s) were added to refs/heads/master by this push:
     new 3ce3a7e0 feat(icons)!: Support a single 1024⨉1024 icon, but also more complex customizations (#1465)
3ce3a7e0 is described below

commit 3ce3a7e081442b9906030bec7e8800a598d470e8
Author: Darryl Pogue <da...@dpogue.ca>
AuthorDate: Wed Aug 21 00:22:23 2024 -0700

    feat(icons)!: Support a single 1024⨉1024 icon, but also more complex customizations (#1465)
    
    * feat(icons)!: Use a single 1024⨉1024 icon
    
    Closes GH-1462.
    
    * feat!(icons): Allow a lot more control over icon assignment
    
    Ironically, this also allows most people to drastically simplify their
    icons by only providing a single 1024⨉1024 image with no special
    attributes.
    
    Closes GH-592.
    Closes GH-623.
    Closes GH-657.
    Closes GH-658.
    Closes GH-1019.
    Closes GH-1233.
    Closes GH-1387.
    
    * fix: Only support tinted/dark icons if using Xcode 16+
    
    Xcode 15 doesn't understand the appearance variants, and if variants are
    specified then it thinks there are 3 images assigned to the same slot
    and ends up picking one at random and we don't want that.
    
    ---------
    
    Co-authored-by: jcesarmobile <jc...@gmail.com>
---
 lib/prepare.js                                     | 281 ++++++++++++----
 .../AppIcon.appiconset/Contents.json               | 237 +-------------
 .../AppIcon.appiconset/icon-1024.png               | Bin 35783 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon-20.png | Bin 476 -> 0 bytes
 .../AppIcon.appiconset/icon-20@2x.png              | Bin 934 -> 0 bytes
 .../AppIcon.appiconset/icon-20@3x.png              | Bin 1417 -> 0 bytes
 .../AppIcon.appiconset/icon-24@2x.png              | Bin 1136 -> 0 bytes
 .../AppIcon.appiconset/icon-27.5@2x.png            | Bin 1325 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon-29.png | Bin 723 -> 0 bytes
 .../AppIcon.appiconset/icon-29@2x.png              | Bin 1367 -> 0 bytes
 .../AppIcon.appiconset/icon-29@3x.png              | Bin 2065 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon-40.png | Bin 934 -> 0 bytes
 .../AppIcon.appiconset/icon-40@2x.png              | Bin 1892 -> 0 bytes
 .../AppIcon.appiconset/icon-44@2x.png              | Bin 2069 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon-50.png | Bin 1173 -> 0 bytes
 .../AppIcon.appiconset/icon-50@2x.png              | Bin 2270 -> 0 bytes
 .../AppIcon.appiconset/icon-60@2x.png              | Bin 2712 -> 0 bytes
 .../AppIcon.appiconset/icon-60@3x.png              | Bin 4304 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon-72.png | Bin 1681 -> 0 bytes
 .../AppIcon.appiconset/icon-72@2x.png              | Bin 3315 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon-76.png | Bin 1764 -> 0 bytes
 .../AppIcon.appiconset/icon-76@2x.png              | Bin 3518 -> 0 bytes
 .../AppIcon.appiconset/icon-83.5@2x.png            | Bin 3842 -> 0 bytes
 .../AppIcon.appiconset/icon-86@2x.png              | Bin 4022 -> 0 bytes
 .../AppIcon.appiconset/icon-98@2x.png              | Bin 4744 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon.png    | Bin 1376 -> 31816 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon@2x.png | Bin 2568 -> 0 bytes
 .../unit/fixtures/icon-support/configs/multi.xml   |  42 +++
 .../unit/fixtures/icon-support/configs/none.xml    |  24 ++
 .../fixtures/icon-support/configs/single-only.xml  |  27 ++
 .../icon-support/configs/single-variants.xml       |  27 ++
 .../fixtures/icon-support/contents-json/multi.js   | 145 ++++++++
 .../fixtures/icon-support/contents-json/none.js    |  39 +++
 .../icon-support/contents-json/single-only.js      |  39 +++
 .../icon-support/contents-json/single-variants.js  |  63 ++++
 .../icon-support/res/ios/AppIcon-1024x1024@1x.png  | Bin 0 -> 11062 bytes
 .../icon-support/res/ios/AppIcon-20x20@2x.png      | Bin 0 -> 730 bytes
 .../icon-support/res/ios/AppIcon-20x20@3x.png      | Bin 0 -> 947 bytes
 .../icon-support/res/ios/AppIcon-29x29@2x.png      | Bin 0 -> 959 bytes
 .../icon-support/res/ios/AppIcon-29x29@3x.png      | Bin 0 -> 1219 bytes
 .../icon-support/res/ios/AppIcon-38x38@2x.png      | Bin 0 -> 1182 bytes
 .../icon-support/res/ios/AppIcon-38x38@3x.png      | Bin 0 -> 1542 bytes
 .../icon-support/res/ios/AppIcon-40x40@2x.png      | Bin 0 -> 1284 bytes
 .../icon-support/res/ios/AppIcon-40x40@3x.png      | Bin 0 -> 1797 bytes
 .../icon-support/res/ios/AppIcon-60x60@2x.png      | Bin 0 -> 1598 bytes
 .../icon-support/res/ios/AppIcon-60x60@3x.png      | Bin 0 -> 2192 bytes
 .../icon-support/res/ios/AppIcon-64x64@2x.png      | Bin 0 -> 6809 bytes
 .../icon-support/res/ios/AppIcon-64x64@3x.png      | Bin 0 -> 6812 bytes
 .../icon-support/res/ios/AppIcon-68x68@2x.png      | Bin 0 -> 6537 bytes
 .../icon-support/res/ios/AppIcon-76x76@2x.png      | Bin 0 -> 1930 bytes
 .../icon-support/res/ios/AppIcon-83.5x83.5@2x.png  | Bin 0 -> 2067 bytes
 .../fixtures/icon-support/res/ios/appicon-dark.png | Bin 0 -> 12297 bytes
 .../fixtures/icon-support/res/ios/appicon-tint.png | Bin 0 -> 11244 bytes
 .../unit/fixtures/icon-support/res/ios/appicon.png | Bin 0 -> 11668 bytes
 .../AppIcon.appiconset/Contents.json               | 178 +---------
 .../Assets.xcassets/AppIcon.appiconset/icon-40.png | Bin 4082 -> 0 bytes
 .../AppIcon.appiconset/icon-40@2x.png              | Bin 7411 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon-50.png | Bin 4874 -> 0 bytes
 .../AppIcon.appiconset/icon-50@2x.png              | Bin 9611 -> 0 bytes
 .../AppIcon.appiconset/icon-60@2x.png              | Bin 11924 -> 0 bytes
 .../AppIcon.appiconset/icon-60@3x.png              | Bin 30083 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon-72.png | Bin 4944 -> 0 bytes
 .../AppIcon.appiconset/icon-72@2x.png              | Bin 11706 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon-76.png | Bin 7025 -> 0 bytes
 .../AppIcon.appiconset/icon-76@2x.png              | Bin 15262 -> 0 bytes
 .../AppIcon.appiconset/icon-83.5@2x.png            | Bin 20563 -> 0 bytes
 .../AppIcon.appiconset/icon-small.png              | Bin 3294 -> 0 bytes
 .../AppIcon.appiconset/icon-small@2x.png           | Bin 5608 -> 0 bytes
 .../AppIcon.appiconset/icon-small@3x.png           | Bin 7040 -> 0 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon.png    | Bin 3902 -> 31816 bytes
 .../Assets.xcassets/AppIcon.appiconset/icon@2x.png | Bin 7869 -> 0 bytes
 tests/spec/unit/prepare.spec.js                    | 364 +++++++++++++++++++++
 72 files changed, 999 insertions(+), 467 deletions(-)

diff --git a/lib/prepare.js b/lib/prepare.js
index 722fc641..73f90044 100644
--- a/lib/prepare.js
+++ b/lib/prepare.js
@@ -33,11 +33,21 @@ const projectFile = require('./projectFile');
 const Podfile = require('./Podfile').Podfile;
 const check_reqs = require('./check_reqs');
 const PlatformConfigParser = require('./PlatformConfigParser');
+const versions = require('./versions');
 
 // launch storyboard and related constants
 const IMAGESET_COMPACT_SIZE_CLASS = 'compact';
 const CDV_ANY_SIZE_CLASS = 'any';
 
+const ASSUMED_XCODE_VERSION = '15.0.0';
+function checkOrAssumeXcodeVersion () {
+    if (process.platform === 'darwin') {
+        return versions.get_apple_xcode_version();
+    } else {
+        return Promise.resolve(ASSUMED_XCODE_VERSION);
+    }
+}
+
 module.exports.prepare = function (cordovaProject, options) {
     const platformJson = PlatformJson.load(this.locations.root, 'ios');
     const munger = new PlatformMunger('ios', this.locations.root, platformJson, new PluginInfoProvider());
@@ -55,15 +65,11 @@ module.exports.prepare = function (cordovaProject, options) {
     return updateWww(cordovaProject, this.locations)
         // update project according to config.xml changes.
         .then(() => updateProject(this._config, this.locations))
-        .then(() => {
-            updateIcons(cordovaProject, this.locations);
-            updateLaunchStoryboardImages(cordovaProject, this.locations);
-            updateBackgroundColor(cordovaProject, this.locations);
-            updateFileResources(cordovaProject, this.locations);
-        })
-        .then(() => {
-            alertDeprecatedPreference(this._config);
-        })
+        .then(() => updateIcons(cordovaProject, this.locations))
+        .then(() => updateLaunchStoryboardImages(cordovaProject, this.locations))
+        .then(() => updateBackgroundColor(cordovaProject, this.locations))
+        .then(() => updateFileResources(cordovaProject, this.locations))
+        .then(() => alertDeprecatedPreference(this._config))
         .then(() => {
             events.emit('verbose', 'Prepared iOS project successfully');
         });
@@ -83,13 +89,12 @@ module.exports.clean = function (options) {
 
     const projectConfig = new ConfigParser(this.locations.configXml);
 
-    return Promise.resolve().then(() => {
-        cleanWww(projectRoot, this.locations);
-        cleanIcons(projectRoot, projectConfig, this.locations);
-        cleanLaunchStoryboardImages(projectRoot, projectConfig, this.locations);
-        cleanBackgroundColor(projectRoot, projectConfig, this.locations);
-        cleanFileResources(projectRoot, projectConfig, this.locations);
-    });
+    return Promise.resolve()
+        .then(() => cleanWww(projectRoot, this.locations))
+        .then(() => cleanIcons(projectRoot, projectConfig, this.locations))
+        .then(() => cleanLaunchStoryboardImages(projectRoot, projectConfig, this.locations))
+        .then(() => cleanBackgroundColor(projectRoot, projectConfig, this.locations))
+        .then(() => cleanFileResources(projectRoot, projectConfig, this.locations));
 };
 
 /**
@@ -362,59 +367,173 @@ function handleBuildSettings (platformConfig, locations, infoPlist) {
     return Promise.resolve();
 }
 
-function mapIconResources (icons, iconsDir) {
-    // See https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html
-    // for launch images sizes reference.
+function mapIconResources (icons, iconsDir, xcodeversion) {
+    // Ref: https://developer.apple.com/design/human-interface-guidelines/app-icons
+    // These are ordered according to how Xcode puts them in the Contents.json file
     const platformIcons = [
-        { dest: 'icon-20.png', width: 20, height: 20 },
+        // iOS fallback icon sizes
         { dest: 'icon-20@2x.png', width: 40, height: 40 },
         { dest: 'icon-20@3x.png', width: 60, height: 60 },
-        { dest: 'icon-40.png', width: 40, height: 40 },
-        { dest: 'icon-40@2x.png', width: 80, height: 80 },
-        { dest: 'icon-50.png', width: 50, height: 50 },
-        { dest: 'icon-50@2x.png', width: 100, height: 100 },
+        { dest: 'icon-29@2x.png', width: 58, height: 58 },
+        { dest: 'icon-29@3x.png', width: 87, height: 87 },
+        { dest: 'icon-38@2x.png', width: 76, height: 76 },
+        { dest: 'icon-38@3x.png', width: 114, height: 114 },
+        { dest: 'icon-40@2x.png', width: 80, height: 80, target: 'spotlight' },
+        { dest: 'icon-40@3x.png', width: 120, height: 120, target: 'spotlight' },
         { dest: 'icon-60@2x.png', width: 120, height: 120 },
         { dest: 'icon-60@3x.png', width: 180, height: 180 },
-        { dest: 'icon-72.png', width: 72, height: 72 },
-        { dest: 'icon-72@2x.png', width: 144, height: 144 },
-        { dest: 'icon-76.png', width: 76, height: 76 },
+        { dest: 'icon-64@2x.png', width: 128, height: 128 },
+        { dest: 'icon-64@3x.png', width: 192, height: 192 },
+        { dest: 'icon-68@2x.png', width: 136, height: 136 },
         { dest: 'icon-76@2x.png', width: 152, height: 152 },
         { dest: 'icon-83.5@2x.png', width: 167, height: 167 },
-        { dest: 'icon-1024.png', width: 1024, height: 1024 },
-        { dest: 'icon-29.png', width: 29, height: 29 },
-        { dest: 'icon-29@2x.png', width: 58, height: 58 },
-        { dest: 'icon-29@3x.png', width: 87, height: 87 },
-        { dest: 'icon.png', width: 57, height: 57 },
-        { dest: 'icon@2x.png', width: 114, height: 114 },
-        { dest: 'icon-24@2x.png', width: 48, height: 48 },
-        { dest: 'icon-27.5@2x.png', width: 55, height: 55 },
-        { dest: 'icon-44@2x.png', width: 88, height: 88 },
-        { dest: 'icon-86@2x.png', width: 172, height: 172 },
-        { dest: 'icon-98@2x.png', width: 196, height: 196 }
+
+        // Default iOS icon
+        { dest: 'icon.png', width: 1024, height: 1024, useDefault: true },
+
+        // macOS icon sizes
+        { dest: 'mac-16.png', width: 16, height: 16, target: 'mac' },
+        { dest: 'mac-16@2x.png', width: 32, height: 32, target: 'mac' },
+        { dest: 'mac-32.png', width: 32, height: 32, target: 'mac' },
+        { dest: 'mac-32@2x.png', width: 64, height: 64, target: 'mac' },
+        { dest: 'mac-128.png', width: 128, height: 128, target: 'mac' },
+        { dest: 'mac-128@2x.png', width: 256, height: 256, target: 'mac' },
+        { dest: 'mac-256.png', width: 256, height: 256, target: 'mac' },
+        { dest: 'mac-256@2x.png', width: 512, height: 512, target: 'mac' },
+        { dest: 'mac-512.png', width: 512, height: 512, target: 'mac' },
+        { dest: 'mac-512@2x.png', width: 1024, height: 1024, target: 'mac' },
+
+        // WatchOS fallback icon sizes
+        { dest: 'watchos-22@2x.png', width: 44, height: 44, target: 'watchos' },
+        { dest: 'watchos-24@2x.png', width: 48, height: 48, target: 'watchos' },
+        { dest: 'watchos-27.5@2x.png', width: 55, height: 55, target: 'watchos' },
+        { dest: 'watchos-29@2x.png', width: 58, height: 58, target: 'watchos' },
+        { dest: 'watchos-30@2x.png', width: 60, height: 60, target: 'watchos' },
+        { dest: 'watchos-32@2x.png', width: 64, height: 64, target: 'watchos' },
+        { dest: 'watchos-33@2x.png', width: 66, height: 66, target: 'watchos' },
+        { dest: 'watchos-40@2x.png', width: 80, height: 80, target: 'watchos' },
+        { dest: 'watchos-43.5@2x.png', width: 87, height: 87, target: 'watchos' },
+        { dest: 'watchos-44@2x.png', width: 88, height: 88, target: 'watchos' },
+        { dest: 'watchos-46@2x.png', width: 92, height: 92, target: 'watchos' },
+        { dest: 'watchos-50@2x.png', width: 100, height: 100, target: 'watchos' },
+        { dest: 'watchos-51@2x.png', width: 102, height: 102, target: 'watchos' },
+        { dest: 'watchos-54@2x.png', width: 108, height: 108, target: 'watchos' },
+        { dest: 'watchos-86@2x.png', width: 172, height: 172, target: 'watchos' },
+        { dest: 'watchos-98@2x.png', width: 196, height: 196, target: 'watchos' },
+        { dest: 'watchos-108@2x.png', width: 216, height: 216, target: 'watchos' },
+        { dest: 'watchos-117@2x.png', width: 234, height: 234, target: 'watchos' },
+        { dest: 'watchos-129@2x.png', width: 258, height: 258, target: 'watchos' },
+
+        // Allow customizing the watchOS icon with target="watchos"
+        // This falls back to using the iOS app icon by default
+        { dest: 'watchos.png', width: 1024, height: 1024, target: 'watchos', useDefault: true }
     ];
 
     const pathMap = {};
+
+    // We can only support dark mode and tinted icons with Xcode 16
+    const isAtLeastXcode16 = versions.compareVersions(xcodeversion, '16.0.0') >= 0;
+
+    function getDefaultIconForTarget (target) {
+        const def = icons.filter(res => !res.width && !res.height && !res.target).pop();
+
+        if (target) {
+            return icons
+                .filter(res => res.target === target)
+                .filter(res => !res.width && !res.height)
+                .pop() || def;
+        }
+
+        return def;
+    }
+
+    function getIconBySizeAndTarget (width, height, target) {
+        return icons
+            .filter(res => res.target === target)
+            .find(res =>
+                (res.width || res.height) &&
+                (!res.width || (width === res.width)) &&
+                (!res.height || (height === res.height))
+            ) || null;
+    }
+
     platformIcons.forEach(item => {
-        const icon = icons.getBySize(item.width, item.height) || icons.getDefault();
+        const dest = path.join(iconsDir, item.dest);
+        let icon = getIconBySizeAndTarget(item.width, item.height, item.target);
+
+        if (!icon && item.target === 'spotlight') {
+            // Fall back to a non-targeted icon
+            icon = getIconBySizeAndTarget(item.width, item.height);
+        }
+
+        if (!icon && item.useDefault) {
+            if (item.target) {
+                icon = getIconBySizeAndTarget(item.width, item.height);
+            }
+
+            const defaultIcon = getDefaultIconForTarget(item.target);
+            if (!icon && defaultIcon) {
+                icon = defaultIcon;
+            }
+        }
+
         if (icon) {
-            const target = path.join(iconsDir, item.dest);
-            pathMap[target] = icon.src;
+            if (icon.src) {
+                pathMap[dest] = icon.src;
+            }
+
+            // Only iOS has dark/tinted icon variants
+            if (isAtLeastXcode16 && (!item.target || item.target === 'spotlight')) {
+                if (icon.foreground) {
+                    pathMap[dest.replace('.png', '-dark.png')] = icon.foreground;
+                }
+
+                if (icon.monochrome) {
+                    pathMap[dest.replace('.png', '-tinted.png')] = icon.monochrome;
+                }
+            }
         }
     });
+
     return pathMap;
 }
 
-function getIconsDir (projectRoot, platformProjDir) {
-    let iconsDir;
-    const xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Assets.xcassets'));
+function generateAppIconContentsJson (resourceMap) {
+    const contentsJSON = {
+        images: [],
+        info: {
+            author: 'xcode',
+            version: 1
+        }
+    };
 
-    if (xcassetsExists) {
-        iconsDir = path.join(platformProjDir, 'Assets.xcassets', 'AppIcon.appiconset');
-    } else {
-        iconsDir = path.join(platformProjDir, 'Resources', 'icons');
-    }
+    Object.keys(resourceMap).forEach(res => {
+        const [filename, platform, size, scale, variant] = path.basename(res).match(/([A-Za-z]+)(?:-([0-9.]+)(?:@([0-9.]x))?)?(?:-([a-z]+))?\.png/);
 
-    return iconsDir;
+        const entry = {
+            filename,
+            idiom: 'universal',
+            platform: (platform === 'icon') ? 'ios' : platform,
+            size: `${size ?? 1024}x${size ?? 1024}`
+        };
+
+        if (scale) {
+            entry.scale = scale;
+        }
+
+        if (variant) {
+            entry.appearances = [
+                {
+                    appearance: 'luminosity',
+                    value: variant
+                }
+            ];
+        }
+
+        contentsJSON.images.push(entry);
+    });
+
+    return contentsJSON;
 }
 
 function updateIcons (cordovaProject, locations) {
@@ -422,32 +541,58 @@ function updateIcons (cordovaProject, locations) {
 
     if (icons.length === 0) {
         events.emit('verbose', 'This app does not have icons defined');
-        return;
+        return Promise.resolve();
     }
 
     const platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
-    const iconsDir = getIconsDir(cordovaProject.root, platformProjDir);
-    const resourceMap = mapIconResources(icons, iconsDir);
-    events.emit('verbose', `Updating icons at ${iconsDir}`);
-    FileUpdater.updatePaths(
-        resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+    const iconsDir = path.join(platformProjDir, 'Assets.xcassets', 'AppIcon.appiconset');
+
+    return checkOrAssumeXcodeVersion()
+        .then((xcodeversion) => {
+            const resourceMap = mapIconResources(icons, iconsDir, xcodeversion);
+            events.emit('verbose', `Updating icons at ${iconsDir}`);
+            FileUpdater.updatePaths(
+                resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+
+            // Now we need to update the AppIcon.appiconset/Contents.json file
+            const contentsJSON = generateAppIconContentsJson(resourceMap);
+
+            events.emit('verbose', 'Updating App Icon image set contents.json');
+            fs.writeFileSync(path.join(cordovaProject.root, iconsDir, 'Contents.json'), JSON.stringify(contentsJSON, null, 2));
+        });
 }
 
 function cleanIcons (projectRoot, projectConfig, locations) {
     const icons = projectConfig.getIcons('ios');
-    if (icons.length > 0) {
-        const platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
-        const iconsDir = getIconsDir(projectRoot, platformProjDir);
-        const resourceMap = mapIconResources(icons, iconsDir);
-        Object.keys(resourceMap).forEach(targetIconPath => {
-            resourceMap[targetIconPath] = null;
-        });
-        events.emit('verbose', `Cleaning icons at ${iconsDir}`);
-
-        // Source paths are removed from the map, so updatePaths() will delete the target files.
-        FileUpdater.updatePaths(
-            resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
+    if (icons.length === 0) {
+        return Promise.resolve();
     }
+
+    const platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
+    const iconsDir = path.join(platformProjDir, 'Assets.xcassets', 'AppIcon.appiconset');
+
+    return checkOrAssumeXcodeVersion()
+        .then((xcodeversion) => {
+            const resourceMap = mapIconResources(icons, iconsDir, xcodeversion);
+            Object.keys(resourceMap).forEach(targetIconPath => {
+                resourceMap[targetIconPath] = null;
+            });
+            events.emit('verbose', `Cleaning icons at ${iconsDir}`);
+
+            // Source paths are removed from the map, so updatePaths() will delete the target files.
+            FileUpdater.updatePaths(
+                resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
+
+            const contentsJSON = generateAppIconContentsJson(resourceMap);
+
+            // delete filename from contents.json
+            contentsJSON.images.forEach(image => {
+                image.filename = undefined;
+            });
+
+            events.emit('verbose', 'Updating App Icon image set contents.json');
+            fs.writeFileSync(path.join(projectRoot, iconsDir, 'Contents.json'), JSON.stringify(contentsJSON, null, 2));
+        });
 }
 
 /**
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/Contents.json b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/Contents.json
index 2ce78c1b..2c8c083a 100644
--- a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,241 +1,20 @@
 {
   "images" : [
     {
-      "size" : "20x20",
-      "idiom" : "iphone",
-      "filename" : "icon-20@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "20x20",
-      "idiom" : "iphone",
-      "filename" : "icon-20@3x.png",
-      "scale" : "3x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "iphone",
-      "filename" : "icon-29.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "iphone",
-      "filename" : "icon-29@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "iphone",
-      "filename" : "icon-29@3x.png",
-      "scale" : "3x"
-    },
-    {
-      "size" : "40x40",
-      "idiom" : "iphone",
-      "filename" : "icon-40@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "40x40",
-      "idiom" : "iphone",
-      "filename" : "icon-60@2x.png",
-      "scale" : "3x"
-    },
-    {
-      "size" : "57x57",
-      "idiom" : "iphone",
       "filename" : "icon.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "57x57",
-      "idiom" : "iphone",
-      "filename" : "icon@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "60x60",
-      "idiom" : "iphone",
-      "filename" : "icon-60@2x.png",
-      "scale" : "2x"
+      "idiom" : "universal",
+      "platform": "ios",
+      "size" : "1024x1024"
     },
     {
-      "size" : "60x60",
-      "idiom" : "iphone",
-      "filename" : "icon-60@3x.png",
-      "scale" : "3x"
-    },
-    {
-      "size" : "20x20",
-      "idiom" : "ipad",
-      "filename" : "icon-20.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "20x20",
-      "idiom" : "ipad",
-      "filename" : "icon-20@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "ipad",
-      "filename" : "icon-29.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "ipad",
-      "filename" : "icon-29@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "40x40",
-      "idiom" : "ipad",
-      "filename" : "icon-40.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "40x40",
-      "idiom" : "ipad",
-      "filename" : "icon-40@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "50x50",
-      "idiom" : "ipad",
-      "filename" : "icon-50.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "50x50",
-      "idiom" : "ipad",
-      "filename" : "icon-50@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "72x72",
-      "idiom" : "ipad",
-      "filename" : "icon-72.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "72x72",
-      "idiom" : "ipad",
-      "filename" : "icon-72@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "76x76",
-      "idiom" : "ipad",
-      "filename" : "icon-76.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "76x76",
-      "idiom" : "ipad",
-      "filename" : "icon-76@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "83.5x83.5",
-      "idiom" : "ipad",
-      "filename" : "icon-83.5@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "1024x1024",
-      "idiom" : "ios-marketing",
-      "filename" : "icon-1024.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "24x24",
-      "idiom" : "watch",
-      "filename" : "icon-24@2x.png",
-      "scale" : "2x",
-      "role" : "notificationCenter",
-      "subtype" : "38mm"
-    },
-    {
-      "size" : "27.5x27.5",
-      "idiom" : "watch",
-      "filename" : "icon-27.5@2x.png",
-      "scale" : "2x",
-      "role" : "notificationCenter",
-      "subtype" : "42mm"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "watch",
-      "filename" : "icon-29@2x.png",
-      "role" : "companionSettings",
-      "scale" : "2x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "watch",
-      "filename" : "icon-29@3x.png",
-      "role" : "companionSettings",
-      "scale" : "3x"
-    },
-    {
-      "size" : "40x40",
-      "idiom" : "watch",
-      "filename" : "icon-40@2x.png",
-      "scale" : "2x",
-      "role" : "appLauncher",
-      "subtype" : "38mm"
-    },
-    {
-      "size" : "44x44",
-      "idiom" : "watch",
-      "filename" : "icon-44@2x.png",
-      "scale" : "2x",
-      "role" : "appLauncher",
-      "subtype" : "40mm"
-    },
-    {
-      "size" : "50x50",
-      "idiom" : "watch",
-      "filename" : "icon-50@2x.png",
-      "scale" : "2x",
-      "role" : "appLauncher",
-      "subtype" : "44mm"
-    },
-    {
-      "size" : "86x86",
-      "idiom" : "watch",
-      "filename" : "icon-86@2x.png",
-      "scale" : "2x",
-      "role" : "quickLook",
-      "subtype" : "38mm"
-    },
-    {
-      "size" : "98x98",
-      "idiom" : "watch",
-      "filename" : "icon-98@2x.png",
-      "scale" : "2x",
-      "role" : "quickLook",
-      "subtype" : "42mm"
-    },
-    {
-      "size" : "108x108",
-      "idiom" : "watch",
-      "scale" : "2x",
-      "role" : "quickLook",
-      "subtype" : "44mm"
-    },
-    {
-      "size" : "1024x1024",
-      "idiom" : "watch-marketing",
-      "filename" : "icon-1024.png",
-      "scale" : "1x"
+      "filename" : "icon.png",
+      "idiom" : "universal",
+      "platform": "watchos",
+      "size" : "1024x1024"
     }
   ],
   "info" : {
     "version" : 1,
     "author" : "xcode"
   }
-}
\ No newline at end of file
+}
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-1024.png
deleted file mode 100644
index 1b9cbb91..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-1024.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-20.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-20.png
deleted file mode 100644
index 5788eed9..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-20.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png
deleted file mode 100644
index 4feefdf8..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png
deleted file mode 100644
index 69ce592c..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-24@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-24@2x.png
deleted file mode 100644
index f0babf9d..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-24@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-27.5@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-27.5@2x.png
deleted file mode 100644
index 78f21bb3..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-27.5@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-29.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-29.png
deleted file mode 100644
index 5dff98e9..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-29.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png
deleted file mode 100644
index 34b39085..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png
deleted file mode 100644
index c63fd776..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-40.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-40.png
deleted file mode 100644
index 4feefdf8..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-40.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png
deleted file mode 100644
index 32897363..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-44@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-44@2x.png
deleted file mode 100644
index 01c5af72..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-44@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-50.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-50.png
deleted file mode 100644
index 81f93f16..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-50.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png
deleted file mode 100644
index a980efe1..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png
deleted file mode 100644
index 1e794a48..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png
deleted file mode 100644
index db10c553..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-72.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-72.png
deleted file mode 100644
index 097f5b40..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-72.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png
deleted file mode 100644
index e3d49a2f..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-76.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-76.png
deleted file mode 100644
index aa6810c3..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-76.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png
deleted file mode 100644
index da3a050f..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png
deleted file mode 100644
index 039a02ae..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-86@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-86@2x.png
deleted file mode 100644
index f32a034a..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-86@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-98@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-98@2x.png
deleted file mode 100644
index 85ec9e36..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon-98@2x.png and /dev/null differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon.png
index bbdba9ee..b9250b12 100644
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon.png and b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon.png differ
diff --git a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon@2x.png b/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon@2x.png
deleted file mode 100644
index 41e6be4a..00000000
Binary files a/templates/project/__PROJECT_NAME__/Assets.xcassets/AppIcon.appiconset/icon@2x.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/icon-support/configs/multi.xml b/tests/spec/unit/fixtures/icon-support/configs/multi.xml
new file mode 100644
index 00000000..504ca297
--- /dev/null
+++ b/tests/spec/unit/fixtures/icon-support/configs/multi.xml
@@ -0,0 +1,42 @@
+<?xml version='1.0' encoding='utf-8'?>
+<widget android-packageName="io.cordova.hellocordova.android" id="io.cordova.hellocordova" ios-CFBundleIdentifier="io.cordova.hellocordova.ios" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+    <name>Hello Cordova</name>
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+    <author email="dev@cordova.apache.org" href="http://cordova.io">
+        Apache Cordova Team
+    </author>
+    <content src="index.html" />
+
+    <platform name="ios">
+        <preference name="orientation" value="all" />
+        <preference name="target-device" value="handset" />
+        <preference name="deployment-target" value="13.0" />
+
+        <icon src="res/ios/AppIcon-20x20@2x.png" height="40" width="40" />
+        <icon src="res/ios/AppIcon-20x20@3x.png" height="60" width="60" />
+        <icon src="res/ios/AppIcon-29x29@2x.png" height="58" width="58" />
+        <icon src="res/ios/AppIcon-29x29@3x.png" height="87" width="87" />
+        <icon src="res/ios/AppIcon-38x38@2x.png" height="76" width="76" />
+        <icon src="res/ios/AppIcon-38x38@3x.png" height="114" width="114" />
+        <icon src="res/ios/AppIcon-40x40@2x.png" height="80" width="80" target="spotlight" />
+        <icon src="res/ios/AppIcon-40x40@3x.png" height="120" width="120" target="spotlight" />
+        <icon src="res/ios/AppIcon-60x60@2x.png" height="120" width="120" />
+        <icon src="res/ios/AppIcon-60x60@3x.png" height="180" width="180" />
+        <icon src="res/ios/AppIcon-64x64@2x.png" height="128" width="128" />
+        <icon src="res/ios/AppIcon-64x64@3x.png" height="192" width="192" />
+        <icon src="res/ios/AppIcon-68x68@2x.png" height="136" width="136" />
+        <icon src="res/ios/AppIcon-76x76@2x.png" height="152" width="152" />
+        <icon src="res/ios/AppIcon-83.5x83.5@2x.png" height="167" width="167" />
+        <icon src="res/ios/AppIcon-1024x1024@1x.png" height="1024" width="1024" />
+    </platform>
+
+    <access origin="http://*.apache.org" />
+    <access origin="https://*.apache.org" />
+
+    <allow-navigation href="http://*.apache.org" />
+    <allow-navigation href="https://*.apache.org" />
+     
+</widget>
+
diff --git a/tests/spec/unit/fixtures/icon-support/configs/none.xml b/tests/spec/unit/fixtures/icon-support/configs/none.xml
new file mode 100644
index 00000000..f1b8751c
--- /dev/null
+++ b/tests/spec/unit/fixtures/icon-support/configs/none.xml
@@ -0,0 +1,24 @@
+<?xml version='1.0' encoding='utf-8'?>
+<widget android-packageName="io.cordova.hellocordova.android" id="io.cordova.hellocordova" ios-CFBundleIdentifier="io.cordova.hellocordova.ios" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+    <name>Hello Cordova</name>
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+    <author email="dev@cordova.apache.org" href="http://cordova.io">
+        Apache Cordova Team
+    </author>
+    <content src="index.html" />
+
+    <platform name="ios">
+        <preference name="orientation" value="all" />
+        <preference name="target-device" value="handset" />
+        <preference name="deployment-target" value="13.0" />
+    </platform>
+
+    <access origin="http://*.apache.org" />
+    <access origin="https://*.apache.org" />
+
+    <allow-navigation href="http://*.apache.org" />
+    <allow-navigation href="https://*.apache.org" />
+     
+</widget>
diff --git a/tests/spec/unit/fixtures/icon-support/configs/single-only.xml b/tests/spec/unit/fixtures/icon-support/configs/single-only.xml
new file mode 100644
index 00000000..975364f7
--- /dev/null
+++ b/tests/spec/unit/fixtures/icon-support/configs/single-only.xml
@@ -0,0 +1,27 @@
+<?xml version='1.0' encoding='utf-8'?>
+<widget android-packageName="io.cordova.hellocordova.android" id="io.cordova.hellocordova" ios-CFBundleIdentifier="io.cordova.hellocordova.ios" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+    <name>Hello Cordova</name>
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+    <author email="dev@cordova.apache.org" href="http://cordova.io">
+        Apache Cordova Team
+    </author>
+    <content src="index.html" />
+
+    <platform name="ios">
+        <preference name="orientation" value="all" />
+        <preference name="target-device" value="handset" />
+        <preference name="deployment-target" value="13.0" />
+
+        <icon src="res/ios/appicon.png" />
+    </platform>
+
+    <access origin="http://*.apache.org" />
+    <access origin="https://*.apache.org" />
+
+    <allow-navigation href="http://*.apache.org" />
+    <allow-navigation href="https://*.apache.org" />
+     
+</widget>
+
diff --git a/tests/spec/unit/fixtures/icon-support/configs/single-variants.xml b/tests/spec/unit/fixtures/icon-support/configs/single-variants.xml
new file mode 100644
index 00000000..52cd0d5d
--- /dev/null
+++ b/tests/spec/unit/fixtures/icon-support/configs/single-variants.xml
@@ -0,0 +1,27 @@
+<?xml version='1.0' encoding='utf-8'?>
+<widget android-packageName="io.cordova.hellocordova.android" id="io.cordova.hellocordova" ios-CFBundleIdentifier="io.cordova.hellocordova.ios" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+    <name>Hello Cordova</name>
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+    <author email="dev@cordova.apache.org" href="http://cordova.io">
+        Apache Cordova Team
+    </author>
+    <content src="index.html" />
+
+    <platform name="ios">
+        <preference name="orientation" value="all" />
+        <preference name="target-device" value="handset" />
+        <preference name="deployment-target" value="13.0" />
+
+        <icon src="res/ios/appicon.png" foreground="res/ios/appicon-dark.png" monochrome="res/ios/appicon-tint.png" />
+    </platform>
+
+    <access origin="http://*.apache.org" />
+    <access origin="https://*.apache.org" />
+
+    <allow-navigation href="http://*.apache.org" />
+    <allow-navigation href="https://*.apache.org" />
+     
+</widget>
+
diff --git a/tests/spec/unit/fixtures/icon-support/contents-json/multi.js b/tests/spec/unit/fixtures/icon-support/contents-json/multi.js
new file mode 100644
index 00000000..2b5a317a
--- /dev/null
+++ b/tests/spec/unit/fixtures/icon-support/contents-json/multi.js
@@ -0,0 +1,145 @@
+/**
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+
+module.exports = {
+  "images" : [
+    {
+      "filename": "icon-20@2x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "2x",
+      "size" : "20x20"
+    },
+    {
+      "filename": "icon-20@3x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "3x",
+      "size" : "20x20"
+    },
+    {
+      "filename": "icon-29@2x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "2x",
+      "size" : "29x29"
+    },
+    {
+      "filename": "icon-29@3x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "3x",
+      "size" : "29x29"
+    },
+    {
+      "filename": "icon-38@2x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "2x",
+      "size" : "38x38"
+    },
+    {
+      "filename": "icon-38@3x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "3x",
+      "size" : "38x38"
+    },
+    {
+      "filename": "icon-40@2x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "2x",
+      "size" : "40x40"
+    },
+    {
+      "filename": "icon-40@3x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "3x",
+      "size" : "40x40"
+    },
+    {
+      "filename": "icon-60@2x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "2x",
+      "size" : "60x60"
+    },
+    {
+      "filename": "icon-60@3x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "3x",
+      "size" : "60x60"
+    },
+    {
+      "filename": "icon-64@2x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "2x",
+      "size" : "64x64"
+    },
+    {
+      "filename": "icon-64@3x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "3x",
+      "size" : "64x64"
+    },
+    {
+      "filename": "icon-68@2x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "2x",
+      "size" : "68x68"
+    },
+    {
+      "filename": "icon-76@2x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "2x",
+      "size" : "76x76"
+    },
+    {
+      "filename": "icon-83.5@2x.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "scale" : "2x",
+      "size" : "83.5x83.5"
+    },
+    {
+      "filename" : "icon.png",
+      "idiom" : "universal",
+      "platform": "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "filename" : "watchos.png",
+      "idiom" : "universal",
+      "platform": "watchos",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+};
+
diff --git a/tests/spec/unit/fixtures/icon-support/contents-json/none.js b/tests/spec/unit/fixtures/icon-support/contents-json/none.js
new file mode 100644
index 00000000..e08f8af5
--- /dev/null
+++ b/tests/spec/unit/fixtures/icon-support/contents-json/none.js
@@ -0,0 +1,39 @@
+/**
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+
+module.exports = {
+  "images" : [
+    {
+      "filename" : "icon.png",
+      "idiom" : "universal",
+      "platform": "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "filename" : "icon.png",
+      "idiom" : "universal",
+      "platform": "watchos",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+};
diff --git a/tests/spec/unit/fixtures/icon-support/contents-json/single-only.js b/tests/spec/unit/fixtures/icon-support/contents-json/single-only.js
new file mode 100644
index 00000000..600c3f12
--- /dev/null
+++ b/tests/spec/unit/fixtures/icon-support/contents-json/single-only.js
@@ -0,0 +1,39 @@
+/**
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+
+module.exports = {
+  "images" : [
+    {
+      "filename" : "icon.png",
+      "idiom" : "universal",
+      "platform": "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "filename" : "watchos.png",
+      "idiom" : "universal",
+      "platform": "watchos",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+};
diff --git a/tests/spec/unit/fixtures/icon-support/contents-json/single-variants.js b/tests/spec/unit/fixtures/icon-support/contents-json/single-variants.js
new file mode 100644
index 00000000..1b3dc339
--- /dev/null
+++ b/tests/spec/unit/fixtures/icon-support/contents-json/single-variants.js
@@ -0,0 +1,63 @@
+/**
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+
+module.exports = {
+  "images" : [
+    {
+      "filename" : "icon.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "filename" : "icon-dark.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "tinted"
+        }
+      ],
+      "filename" : "icon-tinted.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "filename" : "watchos.png",
+      "idiom" : "universal",
+      "platform" : "watchos",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+};
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-1024x1024@1x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-1024x1024@1x.png
new file mode 100644
index 00000000..8cfcb974
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-1024x1024@1x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-20x20@2x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-20x20@2x.png
new file mode 100644
index 00000000..dac81388
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-20x20@2x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-20x20@3x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-20x20@3x.png
new file mode 100644
index 00000000..79e2e927
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-20x20@3x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-29x29@2x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-29x29@2x.png
new file mode 100644
index 00000000..662beedc
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-29x29@2x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-29x29@3x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-29x29@3x.png
new file mode 100644
index 00000000..9f8bfb09
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-29x29@3x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-38x38@2x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-38x38@2x.png
new file mode 100644
index 00000000..85a74483
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-38x38@2x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-38x38@3x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-38x38@3x.png
new file mode 100644
index 00000000..0e0c498d
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-38x38@3x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-40x40@2x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-40x40@2x.png
new file mode 100644
index 00000000..601336d4
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-40x40@2x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-40x40@3x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-40x40@3x.png
new file mode 100644
index 00000000..c2e5c98d
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-40x40@3x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-60x60@2x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-60x60@2x.png
new file mode 100644
index 00000000..aef9bf51
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-60x60@2x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-60x60@3x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-60x60@3x.png
new file mode 100644
index 00000000..d9f5559b
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-60x60@3x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-64x64@2x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-64x64@2x.png
new file mode 100644
index 00000000..a436d035
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-64x64@2x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-64x64@3x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-64x64@3x.png
new file mode 100644
index 00000000..56482a3b
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-64x64@3x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-68x68@2x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-68x68@2x.png
new file mode 100644
index 00000000..f756ba8c
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-68x68@2x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-76x76@2x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-76x76@2x.png
new file mode 100644
index 00000000..30380807
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-76x76@2x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-83.5x83.5@2x.png b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-83.5x83.5@2x.png
new file mode 100644
index 00000000..efed7886
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/AppIcon-83.5x83.5@2x.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/appicon-dark.png b/tests/spec/unit/fixtures/icon-support/res/ios/appicon-dark.png
new file mode 100644
index 00000000..884c4231
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/appicon-dark.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/appicon-tint.png b/tests/spec/unit/fixtures/icon-support/res/ios/appicon-tint.png
new file mode 100644
index 00000000..b37e8115
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/appicon-tint.png differ
diff --git a/tests/spec/unit/fixtures/icon-support/res/ios/appicon.png b/tests/spec/unit/fixtures/icon-support/res/ios/appicon.png
new file mode 100644
index 00000000..07afba0e
Binary files /dev/null and b/tests/spec/unit/fixtures/icon-support/res/ios/appicon.png differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json
index d19e65fd..2c8c083a 100644
--- a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,182 +1,20 @@
 {
   "images" : [
     {
-      "idiom" : "iphone",
-      "size" : "29x29",
-      "filename" : "icon-small.png",
-      "scale" : "1x"
-    },
-    {
-      "idiom" : "iphone",
-      "size" : "29x29",
-      "filename" : "icon-small@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "iphone",
-      "size" : "29x29",
-      "filename" : "icon-small@3x.png",
-      "scale" : "3x"
-    },
-    {
-      "idiom" : "iphone",
-      "size" : "40x40",
-      "filename" : "icon-40@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "iphone",
-      "size" : "40x40",
-      "filename" : "icon-60@2x.png",
-      "scale" : "3x"
-    },
-    {
-      "idiom" : "iphone",
-      "size" : "57x57",
       "filename" : "icon.png",
-      "scale" : "1x"
-    },
-    {
-      "idiom" : "iphone",
-      "size" : "57x57",
-      "filename" : "icon@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "iphone",
-      "size" : "60x60",
-      "filename" : "icon-60@2x.png",
-      "scale" : "2x"
+      "idiom" : "universal",
+      "platform": "ios",
+      "size" : "1024x1024"
     },
     {
-      "idiom" : "iphone",
-      "size" : "60x60",
-      "filename" : "icon-60@3x.png",
-      "scale" : "3x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "29x29",
-      "filename" : "icon-small.png",
-      "scale" : "1x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "29x29",
-      "filename" : "icon-small@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "40x40",
-      "filename" : "icon-40.png",
-      "scale" : "1x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "40x40",
-      "filename" : "icon-40@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "50x50",
-      "filename" : "icon-50.png",
-      "scale" : "1x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "50x50",
-      "filename" : "icon-50@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "72x72",
-      "filename" : "icon-72.png",
-      "scale" : "1x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "72x72",
-      "filename" : "icon-72@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "76x76",
-      "filename" : "icon-76.png",
-      "scale" : "1x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "76x76",
-      "filename" : "icon-76@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "ipad",
-      "size" : "83.5x83.5",
-      "filename" : "icon-83.5@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "24x24",
-      "idiom" : "watch",
-      "scale" : "2x",
-      "role" : "notificationCenter",
-      "subtype" : "38mm"
-    },
-    {
-      "size" : "27.5x27.5",
-      "idiom" : "watch",
-      "scale" : "2x",
-      "role" : "notificationCenter",
-      "subtype" : "42mm"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "watch",
-      "role" : "companionSettings",
-      "scale" : "2x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "watch",
-      "role" : "companionSettings",
-      "scale" : "3x"
-    },
-    {
-      "size" : "40x40",
-      "idiom" : "watch",
-      "scale" : "2x",
-      "role" : "appLauncher",
-      "subtype" : "38mm"
-    },
-    {
-      "size" : "44x44",
-      "idiom" : "watch",
-      "scale" : "2x",
-      "role" : "longLook",
-      "subtype" : "42mm"
-    },
-    {
-      "size" : "86x86",
-      "idiom" : "watch",
-      "scale" : "2x",
-      "role" : "quickLook",
-      "subtype" : "38mm"
-    },
-    {
-      "size" : "98x98",
-      "idiom" : "watch",
-      "scale" : "2x",
-      "role" : "quickLook",
-      "subtype" : "42mm"
+      "filename" : "icon.png",
+      "idiom" : "universal",
+      "platform": "watchos",
+      "size" : "1024x1024"
     }
   ],
   "info" : {
     "version" : 1,
     "author" : "xcode"
   }
-}
\ No newline at end of file
+}
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-40.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-40.png
deleted file mode 100644
index e865adbc..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-40.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png
deleted file mode 100644
index 6d07dce5..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-50.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-50.png
deleted file mode 100644
index 98a9d96d..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-50.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png
deleted file mode 100644
index bac693f7..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png
deleted file mode 100644
index 955af362..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png
deleted file mode 100644
index e1268916..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-72.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-72.png
deleted file mode 100644
index 8c6e5df3..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-72.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png
deleted file mode 100644
index dd819da6..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-76.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-76.png
deleted file mode 100644
index 63afe7f1..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-76.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png
deleted file mode 100644
index 4cff29a2..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png
deleted file mode 100644
index 3c1a0115..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-small.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-small.png
deleted file mode 100644
index 0ea1c42f..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-small.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png
deleted file mode 100644
index 2c72038e..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png
deleted file mode 100644
index 5c37dfc2..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png and /dev/null differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon.png
index b2571a71..b9250b12 100644
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon.png and b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon.png differ
diff --git a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon@2x.png b/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon@2x.png
deleted file mode 100644
index d75098f5..00000000
Binary files a/tests/spec/unit/fixtures/ios-config-xml/SampleApp/Assets.xcassets/AppIcon.appiconset/icon@2x.png and /dev/null differ
diff --git a/tests/spec/unit/prepare.spec.js b/tests/spec/unit/prepare.spec.js
index 769a3afa..a84a601c 100644
--- a/tests/spec/unit/prepare.spec.js
+++ b/tests/spec/unit/prepare.spec.js
@@ -29,6 +29,7 @@ const rewire = require('rewire');
 const prepare = rewire('../../../lib/prepare');
 const projectFile = require('../../../lib/projectFile');
 const FileUpdater = require('cordova-common').FileUpdater;
+const versions = require('../../../lib/versions');
 
 const tmpDir = path.join(__dirname, '../../../tmp');
 const FIXTURES = path.join(__dirname, 'fixtures');
@@ -400,6 +401,369 @@ describe('prepare', () => {
         });
     });
 
+    describe('App Icon handling', () => {
+        const xcver = '16.0.0';
+        const mapIconResources = prepare.__get__('mapIconResources');
+
+        describe('#mapIconResources', () => {
+            it('should handle a default icon', () => {
+                const icons = [
+                    { src: 'dummy.png' }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual(jasmine.objectContaining({
+                    'icon.png': 'dummy.png',
+                    'watchos.png': 'dummy.png'
+                }));
+            });
+
+            it('should handle a default icon for a watchos target', () => {
+                const icons = [
+                    { src: 'dummy.png' },
+                    { src: 'dummy-watch.png', target: 'watchos' }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual(jasmine.objectContaining({
+                    'icon.png': 'dummy.png',
+                    'watchos.png': 'dummy-watch.png'
+                }));
+            });
+
+            it('should handle default icon variants on Xcode 16+', () => {
+                const icons = [
+                    { src: 'dummy.png', monochrome: 'dummy-tint.png', foreground: 'dummy-dark.png' }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual(jasmine.objectContaining({
+                    'icon.png': 'dummy.png',
+                    'icon-dark.png': 'dummy-dark.png',
+                    'icon-tinted.png': 'dummy-tint.png',
+                    'watchos.png': 'dummy.png'
+                }));
+            });
+
+            it('should ignore default icon variants on Xcode 15', () => {
+                const icons = [
+                    { src: 'dummy.png', monochrome: 'dummy-tint.png', foreground: 'dummy-dark.png' }
+                ];
+
+                const resMap = mapIconResources(icons, '', '15.0.0');
+
+                expect(resMap).toEqual(jasmine.objectContaining({
+                    'icon.png': 'dummy.png',
+                    'watchos.png': 'dummy.png'
+                }));
+            });
+
+            it('should handle a single sized icon', () => {
+                const icons = [
+                    { src: 'dummy.png', height: 1024, width: 1024 }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual(jasmine.objectContaining({
+                    'icon.png': 'dummy.png',
+                    'watchos.png': 'dummy.png'
+                }));
+            });
+
+            it('should handle a single sized icon for watchos target', () => {
+                const icons = [
+                    { src: 'dummy.png', height: 1024, width: 1024, target: 'watchos' }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual(jasmine.objectContaining({
+                    'watchos.png': 'dummy.png'
+                }));
+            });
+
+            it('should handle a sized icon', () => {
+                const icons = [
+                    { src: 'dummy.png', height: 120, width: 120 }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual(jasmine.objectContaining({
+                    'icon-40@3x.png': 'dummy.png',
+                    'icon-60@2x.png': 'dummy.png'
+                }));
+            });
+
+            it('should handle a sized spotlight icon', () => {
+                const icons = [
+                    { src: 'dummy.png', height: 120, width: 120 },
+                    { src: 'dummy-spot.png', height: 120, width: 120, target: 'spotlight' }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual(jasmine.objectContaining({
+                    'icon-40@3x.png': 'dummy-spot.png',
+                    'icon-60@2x.png': 'dummy.png'
+                }));
+            });
+
+            it('should handle sized icon variants', () => {
+                const icons = [
+                    { src: 'dummy.png', height: 76, width: 76, monochrome: 'dummy-tint.png', foreground: 'dummy-dark.png' }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual(jasmine.objectContaining({
+                    'icon-38@2x.png': 'dummy.png',
+                    'icon-38@2x-dark.png': 'dummy-dark.png',
+                    'icon-38@2x-tinted.png': 'dummy-tint.png'
+                }));
+            });
+
+            it('should ignore sized watchos icons without a target', () => {
+                const icons = [
+                    { src: 'dummy.png', height: 216, width: 216 }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual({});
+            });
+
+            it('should handle a sized macOS icon', () => {
+                const icons = [
+                    { src: 'dummy.png', height: 256, width: 256, target: 'mac' }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual(jasmine.objectContaining({
+                    'mac-128@2x.png': 'dummy.png',
+                    'mac-256.png': 'dummy.png'
+                }));
+            });
+
+            it('should ignore tinted icons for non-iOS targets', () => {
+                const icons = [
+                    { monochrome: 'dummy-tint.png', height: 256, width: 256, target: 'mac' },
+                    { foreground: 'dummy-dark.png', height: 216, width: 216, target: 'watchos' }
+                ];
+
+                const resMap = mapIconResources(icons, '', xcver);
+
+                expect(resMap).toEqual({});
+            });
+        });
+
+        describe('#updateIcons', () => {
+            const updateIcons = prepare.__get__('updateIcons');
+            const logFileOp = prepare.__get__('logFileOp');
+            let iconsDir = '';
+
+            beforeEach(() => {
+                prepare.__set__('ASSUMED_XCODE_VERSION', '15.0.0');
+
+                const platformProjDir = path.relative(iosProject, p.locations.xcodeCordovaProj);
+                iconsDir = path.join(platformProjDir, 'Assets.xcassets', 'AppIcon.appiconset');
+            });
+
+            function updateIconsWithConfig (configFile) {
+                // create a suitable mock project for our method
+                const project = {
+                    root: iosProject,
+                    locations: p.locations,
+                    projectConfig: new ConfigParser(path.join(FIXTURES, 'icon-support', 'configs', configFile))
+                };
+
+                // copy the icon fixtures to the iOS project
+                fs.cpSync(path.join(FIXTURES, 'icon-support', 'res'), path.join(iosProject, 'res'), { recursive: true });
+
+                // copy icons and update Contents.json
+                return updateIcons(project, p.locations);
+            }
+
+            it('should not update paths if no icons are specified', () => {
+                const updatePaths = spyOn(FileUpdater, 'updatePaths');
+
+                return updateIconsWithConfig('none.xml')
+                    .then(() => {
+                        expect(updatePaths).not.toHaveBeenCalled();
+
+                        // verify that that Contents.json is as we expect
+                        const result = JSON.parse(fs.readFileSync(path.join(iosProject, iconsDir, 'Contents.json')));
+                        expect(result).toEqual(require('./fixtures/icon-support/contents-json/none'));
+                    });
+            });
+
+            it('should update paths if a single icon is specified', () => {
+                const updatePaths = spyOn(FileUpdater, 'updatePaths');
+
+                return updateIconsWithConfig('single-only.xml')
+                    .then(() => {
+                        expect(updatePaths).toHaveBeenCalledWith({
+                            [path.join(iconsDir, 'icon.png')]: 'res/ios/appicon.png',
+                            [path.join(iconsDir, 'watchos.png')]: 'res/ios/appicon.png'
+                        }, { rootDir: iosProject }, logFileOp);
+
+                        // verify that that Contents.json is as we expect
+                        const result = JSON.parse(fs.readFileSync(path.join(iosProject, iconsDir, 'Contents.json')));
+                        expect(result).toEqual(require('./fixtures/icon-support/contents-json/single-only'));
+                    });
+            });
+
+            it('should update only some paths if a single icon with variants is specified with Xcode 15', () => {
+                const updatePaths = spyOn(FileUpdater, 'updatePaths');
+                spyOn(versions, 'get_apple_xcode_version').and.returnValue(Promise.resolve('15.0.0'));
+
+                return updateIconsWithConfig('single-variants.xml')
+                    .then(() => {
+                        expect(updatePaths).toHaveBeenCalledWith({
+                            [path.join(iconsDir, 'icon.png')]: 'res/ios/appicon.png',
+                            [path.join(iconsDir, 'watchos.png')]: 'res/ios/appicon.png'
+                        }, { rootDir: iosProject }, logFileOp);
+
+                        // verify that that Contents.json is as we expect
+                        const result = JSON.parse(fs.readFileSync(path.join(iosProject, iconsDir, 'Contents.json')));
+                        expect(result).toEqual(require('./fixtures/icon-support/contents-json/single-only'));
+                    });
+            });
+
+            it('should update paths if a single icon with variants is specified with Xcode 16', () => {
+                prepare.__set__('ASSUMED_XCODE_VERSION', '16.0.0');
+                const updatePaths = spyOn(FileUpdater, 'updatePaths');
+                spyOn(versions, 'get_apple_xcode_version').and.returnValue(Promise.resolve('16.0.0'));
+
+                return updateIconsWithConfig('single-variants.xml')
+                    .then(() => {
+                        expect(updatePaths).toHaveBeenCalledWith({
+                            [path.join(iconsDir, 'icon.png')]: 'res/ios/appicon.png',
+                            [path.join(iconsDir, 'icon-dark.png')]: 'res/ios/appicon-dark.png',
+                            [path.join(iconsDir, 'icon-tinted.png')]: 'res/ios/appicon-tint.png',
+                            [path.join(iconsDir, 'watchos.png')]: 'res/ios/appicon.png'
+                        }, { rootDir: iosProject }, logFileOp);
+
+                        // verify that that Contents.json is as we expect
+                        const result = JSON.parse(fs.readFileSync(path.join(iosProject, iconsDir, 'Contents.json')));
+                        expect(result).toEqual(require('./fixtures/icon-support/contents-json/single-variants'));
+                    });
+            });
+
+            it('should update paths if multiple icon sizes are specified', () => {
+                const updatePaths = spyOn(FileUpdater, 'updatePaths');
+
+                return updateIconsWithConfig('multi.xml')
+                    .then(() => {
+                        expect(updatePaths).toHaveBeenCalledWith({
+                            [path.join(iconsDir, 'icon.png')]: 'res/ios/AppIcon-1024x1024@1x.png',
+                            [path.join(iconsDir, 'watchos.png')]: 'res/ios/AppIcon-1024x1024@1x.png',
+                            [path.join(iconsDir, 'icon-20@2x.png')]: 'res/ios/AppIcon-20x20@2x.png',
+                            [path.join(iconsDir, 'icon-20@3x.png')]: 'res/ios/AppIcon-20x20@3x.png',
+                            [path.join(iconsDir, 'icon-29@2x.png')]: 'res/ios/AppIcon-29x29@2x.png',
+                            [path.join(iconsDir, 'icon-29@3x.png')]: 'res/ios/AppIcon-29x29@3x.png',
+                            [path.join(iconsDir, 'icon-38@2x.png')]: 'res/ios/AppIcon-38x38@2x.png',
+                            [path.join(iconsDir, 'icon-38@3x.png')]: 'res/ios/AppIcon-38x38@3x.png',
+                            [path.join(iconsDir, 'icon-40@2x.png')]: 'res/ios/AppIcon-40x40@2x.png',
+                            [path.join(iconsDir, 'icon-40@3x.png')]: 'res/ios/AppIcon-40x40@3x.png',
+                            [path.join(iconsDir, 'icon-60@2x.png')]: 'res/ios/AppIcon-60x60@2x.png',
+                            [path.join(iconsDir, 'icon-60@3x.png')]: 'res/ios/AppIcon-60x60@3x.png',
+                            [path.join(iconsDir, 'icon-64@2x.png')]: 'res/ios/AppIcon-64x64@2x.png',
+                            [path.join(iconsDir, 'icon-64@3x.png')]: 'res/ios/AppIcon-64x64@3x.png',
+                            [path.join(iconsDir, 'icon-68@2x.png')]: 'res/ios/AppIcon-68x68@2x.png',
+                            [path.join(iconsDir, 'icon-76@2x.png')]: 'res/ios/AppIcon-76x76@2x.png',
+                            [path.join(iconsDir, 'icon-83.5@2x.png')]: 'res/ios/AppIcon-83.5x83.5@2x.png'
+                        }, { rootDir: iosProject }, logFileOp);
+
+                        // verify that that Contents.json is as we expect
+                        const result = JSON.parse(fs.readFileSync(path.join(iosProject, iconsDir, 'Contents.json')));
+                        expect(result).toEqual(require('./fixtures/icon-support/contents-json/multi'));
+                    });
+            });
+        });
+
+        describe('#cleanIcons', () => {
+            const updateIcons = prepare.__get__('updateIcons');
+            const cleanIcons = prepare.__get__('cleanIcons');
+            const logFileOp = prepare.__get__('logFileOp');
+            let iconsDir = '';
+
+            beforeEach(() => {
+                const platformProjDir = path.relative(iosProject, p.locations.xcodeCordovaProj);
+                iconsDir = path.join(platformProjDir, 'Assets.xcassets', 'AppIcon.appiconset');
+            });
+
+            it('should remove icon images', () => {
+                // create a suitable mock project for our method
+                const project = {
+                    root: iosProject,
+                    locations: p.locations,
+                    projectConfig: new ConfigParser(path.join(FIXTURES, 'icon-support', 'configs', 'multi.xml'))
+                };
+
+                // copy the icon fixtures to the iOS project
+                fs.cpSync(path.join(FIXTURES, 'icon-support', 'res'), path.join(iosProject, 'res'), { recursive: true });
+
+                // copy icons and update Contents.json
+                updateIcons(project, p.locations);
+
+                // now, clean the images
+                const updatePaths = spyOn(FileUpdater, 'updatePaths');
+
+                return cleanIcons(iosProject, project.projectConfig, p.locations)
+                    .then(() => {
+                        expect(updatePaths).toHaveBeenCalledWith({
+                            [path.join(iconsDir, 'icon.png')]: null,
+                            [path.join(iconsDir, 'watchos.png')]: null,
+                            [path.join(iconsDir, 'icon-20@2x.png')]: null,
+                            [path.join(iconsDir, 'icon-20@3x.png')]: null,
+                            [path.join(iconsDir, 'icon-29@2x.png')]: null,
+                            [path.join(iconsDir, 'icon-29@3x.png')]: null,
+                            [path.join(iconsDir, 'icon-38@2x.png')]: null,
+                            [path.join(iconsDir, 'icon-38@3x.png')]: null,
+                            [path.join(iconsDir, 'icon-40@2x.png')]: null,
+                            [path.join(iconsDir, 'icon-40@3x.png')]: null,
+                            [path.join(iconsDir, 'icon-60@2x.png')]: null,
+                            [path.join(iconsDir, 'icon-60@3x.png')]: null,
+                            [path.join(iconsDir, 'icon-64@2x.png')]: null,
+                            [path.join(iconsDir, 'icon-64@3x.png')]: null,
+                            [path.join(iconsDir, 'icon-68@2x.png')]: null,
+                            [path.join(iconsDir, 'icon-76@2x.png')]: null,
+                            [path.join(iconsDir, 'icon-83.5@2x.png')]: null
+                        }, { rootDir: iosProject, all: true }, logFileOp);
+                    });
+            });
+
+            it('should have no effect if no icons are specified', () => {
+                // create a suitable mock project for our method
+                const project = {
+                    root: iosProject,
+                    locations: p.locations,
+                    projectConfig: new ConfigParser(path.join(FIXTURES, 'icon-support', 'configs', 'none.xml'))
+                };
+
+                // copy the icon fixtures to the iOS project
+                fs.cpSync(path.join(FIXTURES, 'icon-support', 'res'), path.join(iosProject, 'res'), { recursive: true });
+
+                // copy icons and update Contents.json
+                updateIcons(project, p.locations);
+
+                // now, clean the images
+                const updatePaths = spyOn(FileUpdater, 'updatePaths');
+
+                return cleanIcons(iosProject, project.projectConfig, p.locations)
+                    .then(() => {
+                        expect(updatePaths).not.toHaveBeenCalled();
+                    });
+            });
+        });
+    });
+
     describe('colorPreferenceToComponents', () => {
         const colorPreferenceToComponents = prepare.__get__('colorPreferenceToComponents');
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org
For additional commands, e-mail: commits-help@cordova.apache.org