在 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()
}
}
为了实现这个效果,忙活了大半天,给点个赞呗~