自定义全局toast
上篇文章中我们介绍了SwiftUI中自定义弹窗的实现方式,自定义弹窗,接下来我们自定义实现全局toast。 toast提示在常用的几种样式中涉及的有:
- 1.纯文字
- 2.文字加图片
- 3.loading动画
toast可以设置dismiss时间,自定义message ,设置灰色背景等功能。
下面我们用自定义弹窗其中一种方式来实现MBProgressHUD类型的效果,自定义toast弹窗,多用在错误提示或说明。我们使用下面三件套来实现。
- 1.自定义
ObservableObject
- 2.自定义
ViewModifier
- 3.自定义
toastView
- 4.自定义
Transition
一般在toast
中我们使用.opacity
. 这个就可以忽略 - 5.在最外层设置
EnvironmentObject
,全局可用
1.自定义ObservableObject
ObserverObject
中只设置一个值来监听toast展示或隐藏,其他的参数都是对图片、message、loading做的设置。
import Combine
class ToastViewObserver: ObservableObject {
/// 是否会自动消失
var isLoading = false
/// toast message
var message: String?
/// 展示图片
var imageName: String?
/// 是否展示灰底背景,当背景色为透明的时候 该参数意义不大
var isShowBgColor = false
/// 是否展示旋转的菊花
var isShowActivity = false
/// 消失的时间
var duration: TimeInterval = 2
/// 动画时间
private var animationDuration = 0.2
/// 是否要展示toast
var isShowToast = false {
didSet(newValue){
withAnimation(.easeInOut(duration: animationDuration)) {
self.objectWillChange.send()
}
}
}
func showLoading() {
self.isLoading = true
self.message = nil
self.isShowBgColor = true
self.isShowActivity = true
self.isShowToast = true
}
func dismissLoading() {
self.isLoading = false
self.message = nil
self.imageName = nil
self.duration = 2
self.isShowBgColor = false
self.isShowActivity = false
self.isShowToast = false
}
func showToast(_ message:String? = nil,
imageName: String? = nil,
activity: Bool = false,
duration:TimeInterval = 2,
showBGColor: Bool = false
) {
self.isShowActivity = activity
self.duration = duration
self.message = message
self.imageName = imageName
self.isShowBgColor = showBGColor
self.isShowToast = true
}
}
2.自定义ViewModifier
viewModifier
是我们实现toast的一种方式,也可以在最外层通过ZStack对主App进行一层包装,通过上面的ToastObserverObject
来控制 toast 是否展示。当然还是有区别的
- 1.
ViewModifier
实现的方式 弹出toast并不会覆盖NavigationView
,toast的层级在NavigationView
之下 - 2.最外层ZStack包装下,toast的层级在最上面,这时候点击返回按钮是没有响应的
我们使用第一种方式来实现
struct ToastViewModifier<T : View>: ViewModifier {
@EnvironmentObject var toastObserver: ToastViewObserver
/// 定义属性的时候不能设置为 some View 只能用一个泛型先替换
private var customView: T
/// 展示的时候执行的动画
var transition: AnyTransition
/// 弹窗的背景色
var bgColor: Color
///使用ViewBuilder 可以传入多个View 进行动态设置
init(bgColor: Color = .black.opacity(0.3),
transition:AnyTransition = .opacity,
@ViewBuilder content:() -> T){
self.bgColor = bgColor
self.transition = transition
self.customView = content()
}
func body(content: Content) -> some View {
ZStack{
content.zIndex(0)
//添加背景
if toastObserver.isShowToast {
if toastObserver.isShowBgColor{
bgColor
.edgesIgnoringSafeArea(.all)
.transition(.opacity).transition(.opacity).zIndex(1)
}
Rectangle()
.fill(.clear)
.overlay(content: {
self.customView
})
.frame(width: kScreenWidth,height: kScreenHeight)
.ignoresSafeArea()
.transition(transition)
.zIndex(2)
}
}.ignoresSafeArea(.keyboard)
}
}
初始化参数中 我们用@ViewBuilder
对参数进行修饰,在content中可以最多放入十个控件,另外在开头的位置上我们引入了环境变量@EnvironmentObject var toastObserver: ToastViewObserver
,在app启动的时候再最外层已经设置好了,只要修改了其中的值,所有依赖该变量的View 都会进行重绘。
3.自定义toastView
toastView
中要承载message、image、activity等UI组件,添加定时器在ObserverableObject
中设置的指定时间移除toastView。
struct ToastView: View {
@EnvironmentObject private var toastObserver: ToastViewObserver
@State private var duration: TimeInterval = 0
/// 设置定时器
private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
if toastObserver.isLoading {
toastContentView()
}else{
toastContentView()
.frame(maxWidth: kScreenWidth - 100)
.onReceive(timer) { _ in
timeChange()
}
.onAppear{
self.duration = toastObserver.duration
}
.onDisappear {
self.timer.upstream.connect().cancel()
}
}
}
func toastContentView() -> some View {
Button {} label: {
if let message = toastObserver.message,let imageName = toastObserver.imageName {
VStack{
Image(imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60,height: 60)
.cornerRadius(10)
.padding(.top,15)
Text(message)
.foregroundColor(.white)
.font(.system(size: 14))
.padding(.top,10)
.lineLimit(nil)
}.padding(20)
}else{
if let message = toastObserver.message {
//只有文字
Text(message)
.foregroundColor(.white)
.font(.system(size: 14))
.padding(10)
.lineLimit(nil)
}else if let imageName = toastObserver.imageName{
//只有图片
Image(imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60,height: 60)
.cornerRadius(10)
.padding(15)
}
else if toastObserver.isShowActivity {
ActivityIndicator(.large).padding(30)
}
}
}
.background(.black)
.cornerRadius(10)
}
/// 设置定时器 超过时间 就把toast dismiss
func timeChange() {
duration = duration - 1;
if duration <= 0{
toastObserver.dismissLoading()
self.timer.upstream.connect().cancel()
}
}
}
4.关于Transition,一般我们用.opacity
已经足够,
5.设置全局的环境变量
@main
struct TestToastApp: App {
var body: some Scene {
WindowGroup {
//设置环境变量
ContentView().environmentObject(ToastViewObserver())
}
}
}
接下来 为了使用便捷,我们给View添加了一个拓展方法。在任何需要进行展示的地方都可以调用toastModifier
方法,而且他还会在界面消失的时候自动移除toast
import Combine
extension View {
func toastModifier(_ toastObserver: ToastViewObserver) -> some View {
self.modifier(ToastViewModifier(content: {
ToastView()
}))
.onDisappear {
//page消失的时候 toast自动移除
toastObserver.dismissLoading()
}
}
}
最后
我们在随便的一个界面中集成toastView进行测试
import SwiftUI
import Combine
struct ToastViewTest: View {
let gradientList: [Gradient] = [.red,.orange,.yellow,.orange,.blue,.indigo,.purple]
@EnvironmentObject var toastObserver: ToastViewObserver
var body: some View {
VStack{
VStack{
Spacer()
ForEach(0..<7){ index in
Button("展示弹窗(index + 1)"){
toastViewAction(index+1)
}
.frame(width: 200,height: 40)
.foregroundColor(.white)
.background(LinearGradient(gradient: gradientList[index], startPoint: .top, endPoint: .bottom))
.cornerRadius(10)
.padding(.top,5)
}
Spacer()
}
}
.toastModifier(toastObserver)
}
func toastViewAction(_ index: Int) {
switch index {
case 1:
toastObserver.showToast("我索拉卡福建省")
case 2:
toastObserver.showToast("宿舍楼多快好省大黄蜂大沙发花洒电极法手机打考拉",activity: true)
case 3:
toastObserver.showLoading()
case 4:
toastObserver.showToast("卡看看你我的时间是最长的哈哈哈哈",duration: 5)
case 5:
toastObserver.showToast("哈哈哈沙发发",imageName: "user_header_1",showBGColor: true)
case 6:
toastObserver.showToast(imageName: "user_header_1",showBGColor: true)
case 7:
toastObserver.showToast("哈哈哈胜多负少雷锋精神李开复佳发顺丰九里峰景流口水分离",imageName: "user_header_1")
default:
print("哈哈")
}
}
}
效果如下图