Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/building-apps/build-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ Only applicable to iOS and tvOS projects.

## ImageAsset

An item group that contains image assets.
An item group that contains image assets, including files inside asset catalogs
(\*.xcassets) and Icon Composer directories (\*.icon).

## InterfaceDefinition

Expand Down
9 changes: 8 additions & 1 deletion dotnet/DefaultCompilationIncludes.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ All \*.pdf, \*.jpg, \*.png and \*.json files inside asset catalogs
(\*.xcassets) in the project directory or any subdirectory are included by
default (as `ImageAsset` items).

## Icon Composer files

All files inside Icon Composer directories (\*.icon) in the project directory
or any subdirectory are included by default (as `ImageAsset` items). Icon
Composer files are created by Xcode's Icon Composer tool (Xcode 26+) and
contain layered app icons with `icon.json` metadata.

## Atlas Textures

All \*.png files inside \*.atlas directories in the project directory or any
Expand All @@ -52,7 +59,7 @@ included by default (as `Metal` items).

All files in the Resources/ subdirectory, except any items in the `Compile` or
`EmbeddedResource` item groups, and except the ones mentioned above
(\*.scnassets, \*.storyboard, \*.xib, \*.xcassets, \*.atlas, \*.mlmodel,
(\*.scnassets, \*.storyboard, \*.xib, \*.xcassets, \*.icon, \*.atlas, \*.mlmodel,
\*.metal) are included by default (as `BundleResource` items).

[1]: https://docs.microsoft.com/en-us/dotnet/core/tools/csproj#default-compilation-includes-in-net-core-projects
Expand Down
3 changes: 2 additions & 1 deletion dotnet/targets/Microsoft.Sdk.DefaultItems.template.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</None>

<!-- Default Asset Catalog file inclusion -->
<ImageAsset Include="**\*.xcassets\**\*.*" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder);**\*.xcassets\**\*.DS_Store">
<ImageAsset Include="**\*.xcassets\**\*.*;**\*.icon\**\*.*" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder);**\*.xcassets\**\*.DS_Store;**\*.icon\**\*.DS_Store">
<Link>$([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)'))</Link>
<Visible>false</Visible>
<IsDefaultItem>true</IsDefaultItem>
Expand Down Expand Up @@ -65,6 +65,7 @@
$(DefaultItemExcludes);
$(DefaultExcludesInProjectFolder);
$(_ResourcePrefix)\**\*.xcassets\**\*.*;
$(_ResourcePrefix)\**\*.icon\**\*.*;
$(_ResourcePrefix)\**\*.storyboard;**\*.xib;
$(_ResourcePrefix)\**\*.atlas\*.png;
$(_ResourcePrefix)\**\*.mlmodel;
Expand Down
46 changes: 26 additions & 20 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/ACTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ void FindXCAssetsDirectory (string main, string secondary, out string mainResult
mainResult = main;
secondaryResult = secondary;

while (!string.IsNullOrEmpty (mainResult) && !mainResult.EndsWith (".xcassets", StringComparison.OrdinalIgnoreCase)) {
while (!string.IsNullOrEmpty (mainResult) && !mainResult.EndsWith (".xcassets", StringComparison.OrdinalIgnoreCase) && !mainResult.EndsWith (".icon", StringComparison.OrdinalIgnoreCase)) {
mainResult = Path.GetDirectoryName (mainResult)!;
if (!string.IsNullOrEmpty (secondaryResult))
secondaryResult = Path.GetDirectoryName (secondaryResult)!;
Expand Down Expand Up @@ -292,13 +292,13 @@ public override bool Execute ()
var vpath = BundleResource.GetVirtualProjectPath (this, imageAsset);
var catalogFullPath = imageAsset.GetMetadata ("FullPath");

// get the parent (which will typically be .appiconset, .launchimage, .imageset, .iconset, etc)
// get the parent (which will typically be .appiconset, .launchimage, .imageset, .iconset, .icon, etc)
var catalog = Path.GetDirectoryName (vpath)!;
catalogFullPath = Path.GetDirectoryName (catalogFullPath)!;

var assetType = Path.GetExtension (catalog).TrimStart ('.');

// keep walking up the directory structure until we get to the .xcassets directory
// keep walking up the directory structure until we get to the .xcassets or .icon directory
FindXCAssetsDirectory (catalog, catalogFullPath, out var catalog2, out var catalogFullPath2);
catalog = catalog2;
catalogFullPath = catalogFullPath2;
Expand All @@ -325,11 +325,11 @@ public override bool Execute ()
continue;
}

// filter out everything except paths containing a Contents.json file since our main processing loop only cares about these
if (Path.GetFileName (vpath) != "Contents.json")
continue;

items.Add (asset);
// Handle both Contents.json (for .xcassets) and icon.json (for .icon folders)
var fileName = Path.GetFileName (vpath);
if (fileName == "Contents.json" || fileName == "icon.json") {
items.Add (asset);
}
}

// clone any *.xcassets dirs that need cloning
Expand Down Expand Up @@ -370,8 +370,9 @@ public override bool Execute ()

File.Copy (src, dest, true);

// filter out everything except paths containing a Contents.json file since our main processing loop only cares about these
if (Path.GetFileName (vpath) != "Contents.json")
// Handle both Contents.json (for .xcassets) and icon.json (for .icon folders)
var fileName = Path.GetFileName (vpath);
if (fileName != "Contents.json" && fileName != "icon.json")
continue;

item = new TaskItem (dest);
Expand All @@ -380,33 +381,37 @@ public override bool Execute ()
FindXCAssetsDirectory (Path.GetFullPath (dest), "", out var catalogFullPath, out var _);
items.Add (new AssetInfo (item, vpath, asset.Catalog, catalogFullPath, asset.AssetType));
} else {
// filter out everything except paths containing a Contents.json file since our main processing loop only cares about these
if (Path.GetFileName (vpath) != "Contents.json")
// Handle both Contents.json (for .xcassets) and icon.json (for .icon folders)
var fileName = Path.GetFileName (vpath);
if (fileName != "Contents.json" && fileName != "icon.json")
continue;

items.Add (asset);
}
}
}

// Note: `items` contains only the Contents.json files at this point
// Note: `items` contains only the Contents.json and icon.json files at this point
for (int i = 0; i < items.Count; i++) {
var asset = items [i];
var assetItem = asset.Item;
var vpath = asset.VirtualProjectPath;
var catalog = asset.Catalog;
var path = assetItem.GetMetadata ("FullPath");
var assetType = asset.AssetType;
var vpathDirNameWithoutExtension = Path.GetFileNameWithoutExtension (Path.GetDirectoryName (vpath)!);

if (Platform == ApplePlatform.TVOS) {
if (assetType.Equals ("imagestack", StringComparison.OrdinalIgnoreCase)) {
imageStacksInAssets.Add (Path.GetFileNameWithoutExtension (Path.GetDirectoryName (vpath)!));
} else if (assetType.Equals ("brandassets", StringComparison.OrdinalIgnoreCase)) {
brandAssetsInAssets.Add (Path.GetFileNameWithoutExtension (Path.GetDirectoryName (vpath)!));
if (assetType.Equals ("imagestack", StringComparison.OrdinalIgnoreCase) || assetType.Equals ("icon", StringComparison.OrdinalIgnoreCase)) {
imageStacksInAssets.Add (vpathDirNameWithoutExtension);
}
if (assetType.Equals ("brandassets", StringComparison.OrdinalIgnoreCase) || assetType.Equals ("icon", StringComparison.OrdinalIgnoreCase)) {
brandAssetsInAssets.Add (vpathDirNameWithoutExtension);
}
} else {
if (assetType.Equals ("appiconset", StringComparison.OrdinalIgnoreCase))
appIconsInAssets.Add (Path.GetFileNameWithoutExtension (Path.GetDirectoryName (vpath)!));
if (assetType.Equals ("appiconset", StringComparison.OrdinalIgnoreCase) || assetType.Equals ("icon", StringComparison.OrdinalIgnoreCase)) {
appIconsInAssets.Add (vpathDirNameWithoutExtension);
}
}

if (unique.Add (catalog)) {
Expand All @@ -416,7 +421,8 @@ public override bool Execute ()
catalogs.Add (item);
}

if (SdkPlatform != "WatchSimulator") {
// Only process Contents.json files for on-demand resources (not icon.json files)
if (SdkPlatform != "WatchSimulator" && Path.GetFileName (vpath) == "Contents.json") {
var text = File.ReadAllText (assetItem.ItemSpec);

if (string.IsNullOrEmpty (text))
Expand Down
35 changes: 35 additions & 0 deletions tests/dotnet/AppWithComposerIcon/AppDelegate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using Foundation;

#if !__MACOS__
using UIKit;
#endif

#nullable enable

namespace AppWithComposerIcon {
#if !(__MACCATALYST__ || __MACOS__)
public class AppDelegate : UIApplicationDelegate {
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
return true;
}
}
#endif

public class Program {
static int Main (string [] args)
{
#if __MACCATALYST__ || __MACOS__
GC.KeepAlive (typeof (NSObject)); // prevent linking away the platform assembly

Console.WriteLine (Environment.GetEnvironmentVariable ("MAGIC_WORD"));

return args.Length;
#else
UIApplication.Main (args, null, typeof (AppDelegate));
return 0;
#endif
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
1 change: 1 addition & 0 deletions tests/dotnet/AppWithComposerIcon/MacCatalyst/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../shared.mk
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"groups" : [
{
"layers" : [
{
"image-name" : "back.png",
"name" : "back"
},
{
"image-name" : "front.png",
"name" : "front"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-ios</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
1 change: 1 addition & 0 deletions tests/dotnet/AppWithComposerIcon/iOS/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../shared.mk
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"groups" : [
{
"layers" : [
{
"image-name" : "back.png",
"name" : "back"
},
{
"image-name" : "front.png",
"name" : "front"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-macos</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
1 change: 1 addition & 0 deletions tests/dotnet/AppWithComposerIcon/macOS/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../shared.mk
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"groups" : [
{
"layers" : [
{
"image-name" : "back.png",
"name" : "back"
},
{
"image-name" : "front.png",
"name" : "front"
}
]
}
]
}
23 changes: 23 additions & 0 deletions tests/dotnet/AppWithComposerIcon/shared.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<OutputType>Exe</OutputType>

<ApplicationTitle>AppWithComposerIcon</ApplicationTitle>
<ApplicationId>com.xamarin.appwithcomposericon</ApplicationId>

<UseInterpreter>true</UseInterpreter> <!-- this is only to speed up the build -->
</PropertyGroup>

<Import Project="../../common/shared-dotnet.csproj" />

<PropertyGroup>
<AppIcon>AppIcon</AppIcon>
</PropertyGroup>

<ItemGroup>
<Compile Include="../*.cs" />

<!-- ImageAssets: included by default (each platform has a Resources/AppIcon.icon directory) -->
</ItemGroup>
</Project>
3 changes: 3 additions & 0 deletions tests/dotnet/AppWithComposerIcon/shared.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
TOP=../../../..
TESTNAME=AppWithComposerIcon
include $(TOP)/tests/common/shared-dotnet.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-tvos</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
1 change: 1 addition & 0 deletions tests/dotnet/AppWithComposerIcon/tvOS/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../shared.mk
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"groups" : [
{
"layers" : [
{
"image-name" : "back.png",
"name" : "back"
},
{
"image-name" : "front.png",
"name" : "front"
}
]
}
]
}
30 changes: 30 additions & 0 deletions tests/dotnet/UnitTests/AppIconTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -672,5 +672,35 @@ void TestXCAssetsImpl (ApplePlatform platform, string runtimeIdentifiers, Dictio
throw;
}
}

[TestCase (ApplePlatform.iOS, "iossimulator-x64")]
[TestCase (ApplePlatform.TVOS, "tvossimulator-x64")]
[TestCase (ApplePlatform.MacCatalyst, "maccatalyst-x64")]
[TestCase (ApplePlatform.MacOSX, "osx-x64")]
public void ComposerIcon (ApplePlatform platform, string runtimeIdentifiers)
{
Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers);
Configuration.IgnoreIfIgnoredPlatform (platform);

var project = "AppWithComposerIcon";
var projectPath = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath);
Clean (projectPath);

var properties = GetDefaultProperties (runtimeIdentifiers);
DotNet.Execute ("build", projectPath, properties);

var resourcesDirectory = GetResourcesDirectory (platform, appPath);

// Verify that the raw .icon files are not in the app bundle as BundleResources
var iconJsonInBundle = Path.Combine (resourcesDirectory, "AppIcon.icon", "icon.json");
Assert.That (iconJsonInBundle, Does.Not.Exist, "icon.json should not be in the app bundle as a raw BundleResource");

var frontPngInBundle = Path.Combine (resourcesDirectory, "AppIcon.icon", "Assets", "front.png");
Assert.That (frontPngInBundle, Does.Not.Exist, "front.png should not be in the app bundle as a raw BundleResource");

var backPngInBundle = Path.Combine (resourcesDirectory, "AppIcon.icon", "Assets", "back.png");
Assert.That (backPngInBundle, Does.Not.Exist, "back.png should not be in the app bundle as a raw BundleResource");
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new test verifies that the raw .icon inputs aren't copied as BundleResources, but it doesn't assert that asset compilation actually produced an Assets.car in the app bundle. Without that assertion, the test could pass even if the icon catalog was ignored (resulting in missing app icons). Consider asserting Assets.car exists (as TestXCAssetsImpl does) and/or validating that the compiled assets contain at least the expected icon entry.

Suggested change
Assert.That (backPngInBundle, Does.Not.Exist, "back.png should not be in the app bundle as a raw BundleResource");
Assert.That (backPngInBundle, Does.Not.Exist, "back.png should not be in the app bundle as a raw BundleResource");
// Verify that the compiled asset catalog exists in the app bundle
string? assetsCarPath = null;
foreach (var file in Directory.GetFiles (resourcesDirectory, "Assets.car", SearchOption.AllDirectories)) {
assetsCarPath = file;
break;
}
Assert.That (assetsCarPath, Is.Not.Null, "Compiled asset catalog (Assets.car) should exist in the app bundle");

Copilot uses AI. Check for mistakes.
}
}
}

Loading
Loading