tauri-plugin-app-icon 0.1.0

A Tauri plugin to programmatically change the app icon.
Documentation
import SwiftRs
import Tauri
import Foundation
import UIKit
import os.log

private let logger = OSLog(subsystem: "com.danielerolli.beaver-notes", category: "AppIcon")

class ChangeArgs: Decodable {
    let name: String
    let disable: [String]?
    let suppressNotification: Bool?
}

class ResetArgs: Decodable {
    let disable: [String]?
    let suppressNotification: Bool?
}

class AppIconPlugin: Plugin {

    @objc public func isSupported(_ invoke: Invoke) throws {
        DispatchQueue.main.sync {
            let supported = UIApplication.shared.supportsAlternateIcons
            os_log("isSupported: %@", log: logger, type: .info, supported ? "YES" : "NO")
            invoke.resolve(["value": supported])
        }
    }

    @objc public func getName(_ invoke: Invoke) throws {
        DispatchQueue.main.sync {
            let name = UIApplication.shared.alternateIconName
            os_log("getName: %@", log: logger, type: .info, name ?? "nil (default)")
            invoke.resolve(["value": name as Any? ?? NSNull()])
        }
    }

    @objc public func change(_ invoke: Invoke) throws {
        let args = try invoke.parseArgs(ChangeArgs.self)
        os_log("change requested: name=%@ suppress=%@", log: logger, type: .info, args.name, (args.suppressNotification ?? false) ? "YES" : "NO")
        setIcon(iconName: args.name, suppressNotification: args.suppressNotification ?? false, invoke)
    }

    @objc public func reset(_ invoke: Invoke) throws {
        let args = try invoke.parseArgs(ResetArgs.self)
        os_log("reset requested", log: logger, type: .info)
        setIcon(iconName: nil, suppressNotification: args.suppressNotification ?? false, invoke)
    }

    private func setIcon(iconName: String?, suppressNotification: Bool, _ invoke: Invoke) {
        DispatchQueue.main.async {
            guard UIApplication.shared.supportsAlternateIcons else {
                os_log("setIcon rejected: alternate icons not supported on this device", log: logger, type: .error)
                invoke.reject("Alternate icons not supported. This feature requires a real device (not simulator) and alternate icons configured in Xcode Assets.")
                return
            }

            if suppressNotification {
                if UIApplication.shared.responds(to: #selector(getter: UIApplication.supportsAlternateIcons)) {
                    typealias SetAlternateIconName = @convention(c) (NSObject, Selector, NSString?, @escaping (NSError) -> Void) -> Void
                    let selectorString = "_setAlternateIconName:completionHandler:"
                    let selector = NSSelectorFromString(selectorString)
                    let imp = UIApplication.shared.method(for: selector)
                    let method = unsafeBitCast(imp, to: SetAlternateIconName.self)
                    method(UIApplication.shared, selector, iconName as NSString?, { _ in
                        os_log("setIcon succeeded (suppressed): %@", log: logger, type: .info, iconName ?? "default")
                    })
                    invoke.resolve()
                }
            } else {
                UIApplication.shared.setAlternateIconName(iconName) { error in
                    if let error = error {
                        os_log("setIcon failed: %@", log: logger, type: .error, error.localizedDescription)
                        invoke.reject(error.localizedDescription)
                    } else {
                        os_log("setIcon succeeded: %@", log: logger, type: .info, iconName ?? "default")
                        invoke.resolve()
                    }
                }
            }
        }
    }
}

@_cdecl("init_plugin_app_icon")
func initPlugin() -> Plugin {
    os_log("AppIconPlugin initialized", log: logger, type: .info)
    return AppIconPlugin()
}