【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()
    }
}

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

相关推荐
青花瓷13 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
2401_8658548815 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
HackerTom1 天前
iOS用rime且导入自制输入方案
ios·iphone·rime
良技漫谈1 天前
Rust移动开发:Rust在iOS端集成使用介绍
后端·程序人生·ios·rust·objective-c·swift
2401_852403551 天前
高效管理iPhone存储:苹果手机怎么删除相似照片
ios·智能手机·iphone
星际码仔2 天前
【动画图解】是怎样的方法,能被称作是 Flutter Widget 系统的核心?
android·flutter·ios
emperinter2 天前
WordCloudStudio:AI生成模版为您的文字云创意赋能 !
图像处理·人工智能·macos·ios·信息可视化·iphone
关键帧Keyframe2 天前
音视频面试题集锦第 8 期
ios·音视频开发·客户端
pb82 天前
引入最新fluwx2.5.4的时候报错
flutter·ios
Rverdoser2 天前
xcode更新完最新版本无法运行调试
xcode