SwiftUI之自定义toastView

612 阅读4分钟

自定义全局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("哈哈")
        }
    }
}

效果如下图

swiftUI_toast_view.gif