【ios】在 SwiftUI 中实现可随时调用的加载框

在 SwiftUI 项目中实现一个自定义的加载框(loading)功能,可以在任意位置调用,以便显示加载动画或者进度条。下面的教程将详细讲解如何创建一个可复用的 Loading 组件,并通过通知机制控制其显示和隐藏。

先上效果:

swift ui加载框效果演示

创建 Loading.swift 文件

在项目中创建一个名为 Loading.swift 的新文件,并粘贴以下代码:

swift 复制代码
import SwiftUI

struct Loading: ViewModifier {
    // 定义一些通知名称,用于在应用中控制弹窗的显示、隐藏和进度更新
    static let showNotification = Notification.Name("Loading.showNotification")
    static let hiddenNotification = Notification.Name("Loading.hiddenNotification")
    static let updateProgressNotification = Notification.Name("Loading.updateProgressNotification")
    
    // 全局的单例实例,确保在整个应用中只有一个 Loading 对象
    static let shared = Loading()
    
    // 一些状态变量,用于跟踪当前视图的显示状态和加载提示的相关信息
    @State private var isContentShowing = false  // 是否显示内容的标志
    @State private var isPresented = false       // 是否显示加载提示的标志
    @State private var progress: Double = 0.0    // 当前的进度值
    @State private var mode: LoadingMode = .standard // 当前的加载模式(标准或进度)
    @State private var labelText: String? = nil  // 可选的提示文本
    
    // 静态标识符,用于跟踪弹窗是否已经显示,以防止重复显示
    private static var isShowing = false
    
    // 定义一个枚举,表示加载提示的模式,可以是标准模式或带进度的模式
    enum LoadingMode {
        case standard
        case progress
    }
    
    // body 方法用于构建自定义视图的内容
    func body(content: Content) -> some View {
        ZStack {
            // 显示原始内容
            content
            if isPresented {
                // 黑透
                Color.black.opacity(0.5)
                    .ignoresSafeArea()
                // 使用 GeometryReader 获取屏幕尺寸,用于动态计算弹窗的位置和大小
                GeometryReader { geometry in
                    VStack {
                        if mode == .progress {
                            ZStack {
                                // 浅色带
                                Circle()
                                    .trim(from: 0.0, to: 1.0)
                                    .stroke(Color.gray.opacity(0.3), lineWidth: 3)
                                    .frame(width: 60, height: 60)
                                
                                // 环形进度条
                                Circle()
                                    .trim(from: 0.0, to: progress) // 进度值
                                    .stroke(Color.white, lineWidth: 3) // 进度条颜色和宽度
                                    .rotationEffect(.degrees(-90)) // 旋转90度,起点从顶部开始
                                    .frame(width: 60, height: 60) // 环形进度条的大小
                                    .animation(.easeInOut(duration: 0.5), value: progress) // 平滑过度动画
                                
                                Text("\(Int(progress * 100))%")
                                    .foregroundColor(.white)
                                    .font(.headline)
                            }
                            
                            if let labelText = labelText {
                                Text(labelText)
                                    .foregroundColor(.white)
                            }
                        } else {
                            // 标准模式下显示一个普通的加载指示器
                            ProgressView()
                                .tint(Color.white)
                                .padding(.top, 10)
                            // 如果有提示文本,就显示在加载指示器下面
                            if let labelText = labelText {
                                Text(labelText)
                                    .foregroundColor(.white)
                                    .padding(.top, 10)
                            }
                        }
                    }
                    .padding(.vertical, 10)
                    .padding(.horizontal, 20)
                    .background(Color.black.opacity(0.7)) // 添加一个半透明的黑色背景
                    .cornerRadius(8) // 设置圆角
                    .position(x: geometry.size.width / 2, y: geometry.size.height / 2) // 居中显示
                }
            }
        }
        // 当视图出现时,设置 isContentShowing 为 true
        .onAppear {
            isContentShowing = true
        }
        // 当视图消失时,设置 isContentShowing 为 false
        .onDisappear {
            isContentShowing = false
        }
        // 监听显示通知,当接收到显示通知时,显示加载提示
        .onReceive(NotificationCenter.default.publisher(for: Loading.showNotification)) { notification in
            // 如果内容没有显示,或者弹窗已经显示了,就不做任何操作
            guard isContentShowing, !Self.isShowing else { return }
            Self.isShowing = true // 标记弹窗为已显示
            
            // 解析通知中的用户信息,确定加载模式和提示文本
            if let userInfo = notification.userInfo,
               let mode = userInfo["mode"] as? LoadingMode {
                self.mode = mode
                if mode == .progress {
                    self.progress = 0.0 // 如果是进度模式,重置进度为 0
                }
            } else {
                self.mode = .standard // 默认模式为标准模式
            }
            self.labelText = notification.userInfo?["label"] as? String
            isPresented = true // 显示加载提示
        }
        // 监听隐藏通知,当接收到隐藏通知时,隐藏加载提示
        .onReceive(NotificationCenter.default.publisher(for: Loading.hiddenNotification)) { _ in
            guard isContentShowing else { return }
            isPresented = false
            Self.isShowing = false // 标记弹窗为已隐藏
        }
        // 监听进度更新通知,当接收到更新通知时,更新进度值
        .onReceive(NotificationCenter.default.publisher(for: Loading.updateProgressNotification)) { notification in
            guard isContentShowing, mode == .progress, let progressValue = notification.object as? Double else { return }
            self.progress = progressValue
        }
    }
    
    // 在主线程中执行
    static func postNotificationOnMainThread(name: Notification.Name, object: Any? = nil, userInfo: [AnyHashable: Any]? = nil) {
        if Thread.isMainThread {
            NotificationCenter.default.post(name: name, object: object, userInfo: userInfo)
        } else {
            DispatchQueue.main.async {
                NotificationCenter.default.post(name: name, object: object, userInfo: userInfo)
            }
        }
    }

    static func show(mode: LoadingMode = .standard, label: String? = nil) {
        postNotificationOnMainThread(name: Loading.showNotification, userInfo: ["mode": mode, "label": label as Any])
    }
    
    // 显示标准模式的加载提示
    static func showByStandard(label: String? = nil) {
        show(mode: .standard, label: label)
    }
    
    // 显示进度模式的加载提示
    static func showByProgress(label: String? = nil) {
        show(mode: .progress, label: label)
    }

    // 隐藏
    static func hidden() {
        postNotificationOnMainThread(name: Loading.hiddenNotification)
    }

    // 更新进度值
    static func updateProgress(_ progress: Double) {
        postNotificationOnMainThread(name: Loading.updateProgressNotification, object: progress)
    }
}

// 给View扩展loadingable方法
extension View {
    func loadingable() -> some View {
        return self.modifier(Loading.shared)
    }
}

调用示例

在顶层视图中调用 loadingable()

为了使 Loading 能够在应用的任意位置调用,我们需要在主视图中添加 .loadingable() 修饰符。例如,在 ContentView 中:

loadingable只需要在顶层视图调用即可,往后不管嵌套多少层,只要是在这个视图下,都可以调用显示!!!以下是一个示例,具体怎么用看你自己了。

swift 复制代码
import SwiftUI

struct ContentView: View {
    var body: some View {
        ZStack {
            VStack {
                Spacer()
                Button("展示普通加载") {
                    Loading.showByStandard(label: "加载中")
                    simulateProgressUpdate()
                }
                .padding()
                
                Spacer()
                
                Button("展示进度加载") {
                    Loading.showByProgress(label: "加载中")
                    simulateProgressUpdate()
                }
                .padding()
                
                Spacer()
            }
        }
        .loadingable()
    }
    
    private func simulateProgressUpdate() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            Loading.updateProgress(0.2)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            Loading.updateProgress(0.4)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
            Loading.updateProgress(0.6)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            Loading.updateProgress(0.8)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
            Loading.updateProgress(1.0)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            Loading.hidden()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

为了实现这个效果,忙活了大半天,给点个赞呗~

相关推荐
幸福回头40 分钟前
ms-swift 代码推理数据集
llm·swift
若水无华11 小时前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
不二狗11 小时前
每日算法 -【Swift 算法】Two Sum 问题:从暴力解法到最优解法的演进
开发语言·算法·swift
Aress"12 小时前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy1 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克1 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨1 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆1 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂2 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T2 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa