A NativePHP Mobile plugin for app icons that goes beyond a single icon.png: per-platform primary icons, iOS 18 light/dark/tinted appearances, Android adaptive & themed icons, and named alternate icons you can switch on a schedule (Christmas, Black Friday, …) — all driven by one config file.
⚠️ Read this first — app icons are not splash screens. The home-screen icon is owned by the OS and baked into the app at build time. Unlike a splash animation, you cannot download a new icon and apply it at runtime. Runtime switching only works among variants you bundled at build time; adding a new one needs a rebuild + app release. See Platform Constraints.
- PHP 8.2+ with
ext-gd(image generation) - NativePHP Mobile 3.0+
- Minimum platforms: Android 8 (API 26), iOS 18.0
composer require s2br/nativephp-mobile-icon
php artisan vendor:publish --tag=mobile-icon-config
php artisan native:plugin:register s2br/nativephp-mobile-icon(native:plugin:register wires it into your NativeServiceProvider; publish the provider first with php artisan vendor:publish --tag=nativephp-plugins-provider if you haven't.)
| Capability | iOS | Android |
|---|---|---|
| Per-platform primary icon | ✅ | ✅ |
| Light / dark / tinted appearances (iOS 18) | ✅ | — |
| Adaptive icon (foreground/background) | — | ✅ |
| Themed/monochrome icon (Android 13+) | — | ✅ |
| Named alternate icons (bundled) | ✅ | ✅ |
| Scheduled switching (date/season) | ✅* | ✅* |
| Remote/downloadable icons (no rebuild) | ❌ | ❌ |
* among build-time-bundled variants only — see constraints.
Save your icon files in your app's resources/icons/ directory, and reference them with a path relative to the project root (resolved via base_path() at build time — the same convention the splashscreen plugin uses for resources/animations/):
your-app/
└── resources/
└── icons/ ← create this folder
├── app-ios.png
├── app-android.png
└── christmas-ios.png …
MOBILE_ICON_IOS="resources/icons/app-ios.png"
MOBILE_ICON_ANDROID="resources/icons/app-android.png"Image requirements:
| Use | Size | Transparency |
|---|---|---|
iOS primary (default.ios) |
1024×1024 PNG | Opaque — Apple rejects transparent app icons |
| iOS dark / tinted appearance | 1024×1024 PNG | Transparency expected |
Android adaptive foreground |
1024×1024 PNG | Transparency expected (keep art in the centre safe zone) |
| Alternate variants | 1024×1024 PNG | Match the platform's primary rule |
Any location under the project root works, but resources/icons/ is the recommended spot. A missing file surfaces as a build-time warning from the plugin's validate().
Everything lives in config/mobile-icon.php.
'default' => [
'ios' => 'resources/icons/app-ios.png', // 1024×1024 PNG, no transparency
'android' => 'resources/icons/app-android.png',
],'ios_appearance' => [
'light' => 'resources/icons/app-light.png',
'dark' => 'resources/icons/app-dark.png',
'tinted' => 'resources/icons/app-tinted.png',
],'android_adaptive' => [
'foreground' => 'resources/icons/fg.png',
'background' => '#079F3D', // hex color OR a PNG path
'monochrome' => 'resources/icons/mono.png', // optional, powers themed icons
],'variants' => [
'christmas' => [
'ios' => 'resources/icons/christmas-ios.png',
'android' => 'resources/icons/christmas-android.png',
],
'black_friday' => [
'ios' => 'resources/icons/bf-ios.png',
'android' => 'resources/icons/bf-android.png',
],
],'schedule' => [
['variant' => 'christmas', 'from' => '12-24', 'to' => '12-26'], // recurs yearly
['variant' => 'black_friday', 'from' => '2026-11-27', 'to' => '2026-11-28'], // that year only
],Dates use MM-DD (recurs every year) or YYYY-MM-DD (specific year); a full-date entry wins over a recurring one — the same rule as the splashscreen plugin.
use S2BR\MobileIcon\Facades\MobileIcon;
MobileIcon::set('christmas'); // switch to a bundled variant
MobileIcon::reset(); // back to the primary icon
MobileIcon::supported(); // is runtime switching available?
MobileIcon::current(); // active variantAfter applying, native dispatches S2BR\MobileIcon\Events\AppIconChanged (variant, success).
use Livewire\Attributes\On;
#[On('native:'.\S2BR\MobileIcon\Events\AppIconChanged::class)]
public function onIconChanged(string $variant, bool $success): void {}JS:
import { MobileIcon } from './vendor/.../mobileIcon.js';
MobileIcon.set('christmas');
const off = MobileIcon.onChanged(({ variant, success }) => console.log(variant, success));Show a notification count on the app icon (the red number).
use S2BR\MobileIcon\Facades\MobileIcon;
MobileIcon::setBadge(3); // show "3"
MobileIcon::setBadge(3, 'S2BR', '3 new messages'); // custom Android notification text
MobileIcon::setBadge(0); // clear
MobileIcon::clearBadge(); // clearMobileIcon.setBadge(3);
MobileIcon.clearBadge();Platform reality — read this before relying on it:
- iOS sets the exact number via
UNUserNotificationCenter.setBadgeCount. It requires badge/notification permission (NativePHP's push enrollment requests it). iOS never decrements the badge on its own — callsetBadge()/clearBadge()as the user reads notifications to keep it in sync. - Android has no API to put an arbitrary number on the launcher icon. The badge is driven by a single quiet local notification this plugin posts, so:
- Supporting launchers (Samsung, Xiaomi, …) show the count; stock/Pixel shows a dot.
- It needs the
POST_NOTIFICATIONSruntime permission on Android 13+ (declared by the plugin; you still request it at runtime). - The badge implies a real, silent notification sits in the shade — pass
title/bodyto make it meaningful.setBadge(0)/clearBadge()cancels it.
So: iOS = exact number; Android = a dot, or a count on launchers that support it.
These are OS limitations, not plugin shortcomings — know them before designing around dynamic icons:
- Build-time only. Every variant must be compiled into the app. There is no way to download and apply a brand-new icon at runtime on either platform.
- iOS shows a system alert (“You have changed the icon for …”) on every
setAlternateIconNamecall. There is no App-Store-safe way to suppress it. Plan your UX around one deliberate switch, not frequent flips. - Android switches by enabling an
<activity-alias>, which typically restarts the app and can behave differently per launcher/OEM; pinned shortcuts may not follow.
If you need a splash that updates remotely without an app release, that's the splashscreen plugin — icons can't work that way.
copy_assetsvalidates your source PNGs.pre_compilegenerates per-platform icons with GD, writes the iOS asset catalog / Android adaptive resources, registers alternate icons (iOSCFBundleAlternateIcons, Android<activity-alias>), and embeds the schedule for on-launch resolution.- Runtime bridge functions apply a variant via
setAlternateIconName(iOS) /PackageManageralias toggling (Android).
This is an early, structurally complete plugin. Build-time orchestration, the runtime bridge, config, and the GD pipeline are in place; some native write-back seams (Info.plist merge, AndroidManifest alias injection, exact build paths) are marked with TODO(on-device) and need verification against a real NativePHP build.
- MVP: per-platform icons + iOS appearances + Android adaptive/themed (no runtime jank).
- Phase 2: scheduled switching among bundled variants, with the constraints above clearly surfaced.
Contributions and ideas welcome.
composer install
composer testMIT.