Swift更换应用图标进阶:屏蔽系统弹窗
Swift更换应用图标进阶:屏蔽系统弹窗

Swift更换应用图标进阶:屏蔽系统弹窗

在前文《SwiftUI更换应用图标》中,谈到使用UIApplication.shared.setAlternateIconName方法更换图标。

但是在更换图标的过程中,会存在一个待用户确认的提示框,这个提示框由UIApplication.shared.setAlternateIconName方法调用并弹出。

屏蔽系统弹窗代码

在Swift中无法屏蔽这一弹窗,但是可以借助UIKit代码屏蔽这一弹窗。

我们需要使用UIKit和Swift代码:

class TransparentViewController: UIViewController {
    override func viewDidLoad() {
        print("进入到 TransparentViewController 的 viewDidLoad方法")
        super.viewDidLoad()
        let backgroundView = UIView()
        backgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.0) // 完全透明
        backgroundView.frame = view.bounds
        view.addSubview(backgroundView)
    }
    
    override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        if viewControllerToPresent is UIAlertController {
            print("拦截了系统弹窗")
             dismiss(animated: false)
            completion?()
        } else {
            super.present(viewControllerToPresent, animated: flag,completion: completion)
        }
    }
}

class IconChanger {
    static func changeIconSilently(to name: String?,selected: Binding<String>) {
        guard let windowScene = UIApplication.shared.connectedScenes
            .compactMap({ $0 as? UIWindowScene })
            .first(where: { $0.activationState == .foregroundActive }),
              let rootVC = windowScene.windows.first?.rootViewController else {
            // 安全地拿到当前活跃窗口的根控制器
            print("无法找到 rootVC,退出方法")
            return
        }
        
        var topVC = rootVC
        while let presented = topVC.presentedViewController {
            topVC = presented
        }
        
        let transparentVC = TransparentViewController()
        transparentVC.modalPresentationStyle = .overFullScreen
        
        if !(topVC is TransparentViewController) && !(topVC is UIAlertController) {
            topVC.present(transparentVC, animated: false)
            if UIApplication.shared.supportsAlternateIcons {
                print("支持功能图标的功能")
                UIApplication.shared.setAlternateIconName(name)
                DispatchQueue.main.async {
                    // 修改存储的图标名称
                    AppStorageManager.shared.appIcon = name ?? "AppIcon 2"
                    selected.wrappedValue = AppStorageManager.shared.appIcon
                }
            } else {
                print("不支持更换图标功能")
            }
        } else {
            // 已有其他控制器,无法静默处理
            print("topVC 判断出错,设置 App Icon 出错")
        }
    }
}

在SwiftUI中使用IconChanger的changeIconSilently方法更换图标:

struct AppIconView: View {
    @State private var selectedIconName: String = UIApplication.shared.alternateIconName ?? "AppIcon 2"
    
    var body: some View {
        // 图标按钮
        Button(action: {
            IconChanger.changeIconSilently(to: "AppIcon \(index)",selected: $selectedIconName)
        }, label: { 
            // 图标
        }
    }
}

在点击图标按钮后,不再弹出系统弹窗。

解析屏蔽系统弹窗代码

可以通过修改上面的代码来实现屏蔽系统弹窗,下面是代码是如何实现屏蔽系统弹窗的。

在SwiftUI中无法实现这个功能,原因在于SwiftUI是自动管理状态和界面。而UIKit可以管理控制器堆栈,因此当涉及系统弹窗时,UIKit能够检测到系统弹窗并将其拦截,然后返回到原来的页面中。

1、SwiftUI视图代码

首先来看SwiftUI视图代码,这里的selectedIconName是获取图标的名称,通过UIApplication.shared.alternateIconName可以获取到当前应用的图标。

在SwiftUI视图中,当点击对应的图标时,会调用Button中的方法。

struct AppIconView: View {
    @State private var selectedIconName: String = UIApplication.shared.alternateIconName ?? "AppIcon 2"
    
    var body: some View {
        // 图标按钮
        Button(action: {
            IconChanger.changeIconSilently(to: "AppIcon \(index)",selected: $selectedIconName)
        }, label: { 
            // 图标
        }
    }
}

2、changeIconSilently方法

IconChanger类中有一个changeIconSilently静态方法:

class IconChanger {
    static func changeIconSilently(to name: String?,selected: Binding<String>) {
        guard let windowScene = UIApplication.shared.connectedScenes
            .compactMap({ $0 as? UIWindowScene })
            .first(where: { $0.activationState == .foregroundActive }),
              let rootVC = windowScene.windows.first?.rootViewController else {
            // 安全地拿到当前活跃窗口的根控制器
            print("无法找到 rootVC,退出方法")
            return
        }
        
        var topVC = rootVC
        while let presented = topVC.presentedViewController {
            topVC = presented
        }
        
        let transparentVC = TransparentViewController()
        transparentVC.modalPresentationStyle = .overFullScreen
        
        if !(topVC is TransparentViewController) && !(topVC is UIAlertController) {
            topVC.present(transparentVC, animated: false)
            if UIApplication.shared.supportsAlternateIcons {
                print("支持功能图标的功能")
                UIApplication.shared.setAlternateIconName(name)
                DispatchQueue.main.async {
                    // 修改存储的图标名称
                    AppStorageManager.shared.appIcon = name ?? "AppIcon 2"
                    selected.wrappedValue = AppStorageManager.shared.appIcon
                }
            } else {
                print("不支持更换图标功能")
            }
        } else {
            // 已有其他控制器,无法静默处理
            print("topVC 判断出错,设置 App Icon 出错")
        }
    }
}

这个静态方法接收两个参数,分别是name和selected。

static func changeIconSilently(to name: String?,selected: Binding<String>) { }

name为修改的图标名称,用于UIApplication.shared.setAlternateIconName更换图标的方法。selected是SwiftUI中当前图标的名称。

当更换图标成功后,可以将selected修改为新的图标名称,然后可以通过这一变量配置图标选中等效果,这里不过多累赘。

进入到changeIconSilently方法后,我们获取到了rootVC根控制器。

guard let windowScene = UIApplication.shared.connectedScenes
    .compactMap({ $0 as? UIWindowScene })
    .first(where: { $0.activationState == .foregroundActive }),
      let rootVC = windowScene.windows.first?.rootViewController else {
    // 安全地拿到当前活跃窗口的根控制器
    print("无法找到 rootVC,退出方法")
    return
}

这段代码表示从UIApplication(UIKit框架单例模式)中获取当前App正在前台活动(foregroundActive)的UI界面(UIWindowScene)的窗口数组。

通过windowScene获取第一个窗口,也就是主窗口rootVC。这部分内容可以参考《iOS窗口容器UIWindow》的“与SwiftUI的关系”部分。

为什么需要获取rootVC根控制器?获取它做什么呢?

因为rootVC(rootViewController简称)根控制器是整个界面的起点,我们通过获取rootVC根控制器操作界面。

例如,以“存钱猪猪”应用为例,这个应用窗口就是UIWindow,rootViewController根控制器就是底部的主视图。弹出的“统计”Sheet视图就是其他视图。

层级关系为rootViewController根控制器在底部,上面是Sheet视图。

当我们获取到rootViewController根控制器后,就可以控制底部的主视图,比如嵌套SwiftUI视图、弹出提示框等。也可以获取到rootViewController根控制器的上层控制器Sheet,操作Sheet视图。

下一步就是获取到最顶层的控制器,也就是视图。

var topVC = rootVC
while let presented = topVC.presentedViewController {
    topVC = presented
}

原因在于,我们需要在最顶层的控制器中弹出视图,而不是在底部rootViewController根控制器或者中间的控制器中,弹出视图。

如果不在最顶层弹出视图,那么提示框可能被覆盖或者不显示,因此提示框应该在所有视图的最顶部。

然后创建一个自定义的TransparentViewController控制器,并设置为全屏显示。

let transparentVC = TransparentViewController()
transparentVC.modalPresentationStyle = .overFullScreen

按照代码的执行逻辑,进入到TransparentViewController控制器。

3、TransparentViewController控制器

TransparentViewController控制器是一个UIViewController,可以理解为视图,但是比SwiftUI更复杂,它可以加载并管理视图,控制视图的显示、消失和加载等等行为。

class TransparentViewController: UIViewController { 
    override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        if viewControllerToPresent is UIAlertController {
            print("拦截了系统弹窗")
             dismiss(animated: false)
            completion?()
        } else {
            super.present(viewControllerToPresent, animated: flag,completion: completion)
        }
    }
}

在TransparentViewController类中,使用override重写了present(弹窗)方法。

这就表示,如果在TransparentViewController控制器中,系统想要显示弹窗,就会调用TransparentViewController控制器的present方法。该方法会判断视图控制器是否为提示框。

如果弹出的控制器类型是提示框,因为present方法被我重写了,提示框就不会继承父类的行为,这也意味着不会弹出。而是执行我重写的代码:

if viewControllerToPresent is UIAlertController {
    print("拦截了系统弹窗")
     dismiss(animated: false)
    completion?()
}

在重写的代码中,我没有执行其他的操作,而是调用dismiss方法,这个方法会将TransparentViewController控制器关闭,返回上一个控制器。随后调用了闭包,当然这个闭包貌似没有什么用,但我为了遵循方法还是调用一下。

否则就使用super正常调用父类的行为,弹出不是提示框的窗口。

4、返回到changeIconSilently方法

下面是判断最顶层的控制器是否是自定义的TransparentViewController控制器或者是系统提示框。

if !(topVC is TransparentViewController) && !(topVC is UIAlertController) {
    topVC.present(transparentVC, animated: false)
    if UIApplication.shared.supportsAlternateIcons {
        print("支持功能图标的功能")
        UIApplication.shared.setAlternateIconName(name)
        DispatchQueue.main.async {
            // 修改存储的图标名称
            AppStorageManager.shared.appIcon = name ?? "AppIcon 2"
            selected.wrappedValue = AppStorageManager.shared.appIcon
        }
    } else {
        print("不支持更换图标功能")
    }
} else {
    // 已有其他控制器,无法静默处理
    print("topVC 判断出错,设置 App Icon 出错")
}

如果判断成功的话,表示现在已经有自定义控制器或提示框显示,就不再显示自定义的控制。

if !(topVC is TransparentViewController) && !(topVC is UIAlertController) { ... }

如果最顶层的控制器既不是自定义的TransparentViewController控制器,也不是系统提示框,那么就调用最顶层控制器的present方法。

topVC.present(transparentVC, animated: false)

present是UIViewController的方法,表示弹出一个新的控制器。也可以理解为SwiftUI中的ZStack新增一个最上层的视图。这样,新的控制器就会在旧的控制器之上显示。

然后判断iOS系统是否支持更换图标的功能,如果支持,则调用更换图标的方法,并将传入的name参数传递进去。

这个UIApplication.shared.setAlternateIconName是如何实现的呢?可以理解为系统会在最顶层的控制器中,调用一个系统弹窗方法,提示用户确认弹窗,确认后修改图标。

现在最顶层的控制器就是我们自定义的一个控制器TransparentViewController,我们还重写了present方法,所以系统就会调用最顶层控制器TransparentViewController的present方法来调用系统弹窗,我们重写的present方法是监测弹窗的类型,如果是提示框,就关闭TransparentViewController控制器,其他类型则正常弹出。

所以,当UIApplication.shared.setAlternateIconName实际调用系统弹窗时,被我们重写的present方法忽略掉系统弹窗,因此就会实现没有系统弹窗的功能。

这就是本文的核心内容,然后通过DispatchQueue.main.async更新主线程的图标代码,这里就是根据项目而定了。

总结

本文的核心内容是通过UIWindow的根控制器来管理其他的控制器。还涉及到UIViewController的present弹出控制器方法。

在文章中提及的“控制器”可以理解为视图或者页面,在UIKit中常用控制器来表示视图或者页面。

参考文章

1、惊人开发技巧:轻松更换 App 图标,无需系统弹窗!:https://mp.weixin.qq.com/s/-wGkKPRTz7aYvMxsN3PcFg

2、SwiftUI更换应用图标:https://fangjunyu.com/2025/01/28/swiftui%e6%9b%b4%e6%8d%a2%e5%ba%94%e7%94%a8%e5%9b%be%e6%a0%87/

3、Swift知识扩展:Static静态方法的实际运用:https://fangjunyu.com/2024/10/17/swift%e7%9f%a5%e8%af%86%e6%89%a9%e5%b1%95%ef%bc%9astatic%e9%9d%99%e6%80%81%e6%96%b9%e6%b3%95%e7%9a%84%e5%ae%9e%e9%99%85%e8%bf%90%e7%94%a8/

4、iOS窗口容器UIWindow:https://fangjunyu.com/2025/05/20/ios%e7%aa%97%e5%8f%a3%e5%ae%b9%e5%99%a8uiwindow/

5、SwiftUI和iOS核心类UIViewController:https://fangjunyu.com/2025/05/19/swiftui%e5%92%8cios%e6%a0%b8%e5%bf%83%e7%b1%bbuiviewcontroller/

6、Swift重写父类的override:https://fangjunyu.com/2025/05/18/swift%e9%87%8d%e5%86%99%e7%88%b6%e7%b1%bb%e7%9a%84override/

7、Swift引用父类的super:https://fangjunyu.com/2025/05/19/swift%e5%bc%95%e7%94%a8%e7%88%b6%e7%b1%bb%e7%9a%84super/

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注