自界说弹窗

UIKit中 自界说弹窗办法很多,比方: 模态ViewControllerview掩盖keyWindow/RootWindow等。接下来咱们看下SwiftUI中的弹窗办法。

1.自界说(运用ZStack)

SwiftUI中完成弹窗很简单,运用ZStack 进行组装,操控好对应的变量来显现躲藏 控件。

struct TestStack: View {
  
  @State var isShow = false
  
  var body: some View {
    ZStack{
      Text("咱们来了 ")
      if isShow {
                Text("你好,哈哈哈").transition(.scale).zIndex(1)
      }
    }
  }
}

如上面的代码,是一个很简单的示例,经过变量来操控Text是否显现。关于动画履行时间和动画需求自界说。这样来说关于一个界面是好处理的,但是在SwiftUI中涉及到杂乱界面和交互的时分问题很多。

接下来 咱们运用这一特性来做一个杂乱点的业务。咱们知道在一个界面中或许涉及到很多弹窗,身份的承认,权限承认等等,鄙人面的页面中咱们来做一个界面中 对多个弹窗的处理。

完成过程

  • 1.经过ZStack来进行包装
  • 2.构建一个统一的黑底通明的布景
  • 3.自界说ObservableObject来操控弹窗展现和躲藏,ObservableObject作为一个EnvironmentObject
  • 4.自界说ViewModifier 来加载弹窗
  • 5.ObservableObject监听键盘弹出和收起,在键盘弹出时 先收起键盘 再次点击 收起弹窗
1.统一弹窗布景 色彩能够自界说
struct CustomBackgroundView: View {
  /// 环境变量 统一操控
  @EnvironmentObject var popObserve: CustomPopObserver
  
  var bgColor: Color = .black.opacity(0.3)
  /// 记录键盘状况
  @State var isShowKeyBoard = false
  
  var body: some View {
    bgColor
      .edgesIgnoringSafeArea(.all)
      .transition(.opacity)
          //对View extension 下面有具体完成
      .keyBoardObserver({ noti, status in
        self.isShowKeyBoard = status
      })
      .onTapGesture {
        if self.isShowKeyBoard {
          self.hideKeyBoard()
        }else {
          //自己消失
          popObserve.backgroundTapPublisher.send()
        }
      }
  }
}
2.自界说ObservableObject 记录当时弹窗的状况
import Combine
​
/// 界说多个弹窗类型,
enum CustomPopType: Int {
  case popView1 = 1
  case popView2
  case popView3
  case popView4
  case popView5
  case popView6
  case popView7
}
​
class CustomPopObserver: ObservableObject {
  
  /// 点击布景是否需求躲藏
  private var isHiddenWhenTapBackground = true
  /// 设置布景色彩
  var backgroundColor = Color.black.opacity(0.3)
  
  /// 展现第一个视图
  var popType: CustomPopType = .popView1 {
    didSet {
      self.isShowShowPopView = true
    }
  }
  
  ///躲藏展现弹窗操控标识,手动来机型send操作,增加变化的animation
  var isShowShowPopView = false {
    didSet{
      withAnimation(.spring()) {
        self.objectWillChange.send()
      }
    }
  }
  
  /// 点击后面布景触发事件
  let backgroundTapPublisher = ObservableObjectPublisher()
  
  private var cancellabelSet = Set<AnyCancellable>()
  
  init() {
    //订阅backgroundTapPublisher,当有事件触发的时分会进行调用
    backgroundTapPublisher.sink { _ in
      if self.isHiddenWhenTapBackground {
        /// 设置所有状况值为false,躲藏弹窗
        self.isShowShowPopView = false          
      }
    }
    .store(in: &cancellabelSet)
  }
​
  func clear() {
    cancellabelSet.forEach { cancellable in
      cancellable.cancel()
    }
    cancellabelSet.removeAll()
  }
  
  deinit {
    clear()
  }
}
​
3.设置ViewModifier

swiftUIViewmodifie 入参是view,并且回来值也是view,咱们运用这一特性来做弹窗。自界说ViewModifier,需求完成ViewModifier协议,而在ViewModifier协议办法中咱们能够拿到当时的content,对当时的content在做一层ZStack包装。在自界说的ViewModifier中,仍是用了@ViewBuilder来润饰传参,这样能够在ViewBuilder中传入多个View,最终在经过ZStack包裹。下面的处理中其实还对customView进行了一层包装,在完成动画的时分能够做到顶部和底部弹入、弹出的过度。

import SwiftUIstruct CustomPopViewModifier<T : View>: ViewModifier {
  /// 环境变量,只需有变化就会重绘依赖的view
  @EnvironmentObject var showData: CustomPopObserver
  /// 界说属性的时分不能设置为 some View 只能用一个泛型先替换
  private var customView: T
  /// 展现的时分履行的动画
  var transition: AnyTransition
  /// 弹窗的布景色
  var bgColor: Color
​
  ///运用ViewBuilder 能够传入多个View 进行动态设置
  init(bgColor: Color = .black.opacity(0.3),
     transition:AnyTransition = .bottomStyle,
     @ViewBuilder content:() -> T){
    self.bgColor = bgColor
    self.transition = transition
    self.customView = content()
  }
  
  func body(content: Content) -> some View {
    ZStack{
        //上层的View,在包装的时放到最基层
      content.zIndex(0)
      //增加布景
      if showData.isShowShowPopView {
        //增加布景图片
        CustomBackgroundView(bgColor: bgColor).transition(.opacity).zIndex(1)
        /// 包裹一层的原因: 在履行动画的时分 从下到现在 在消失是从.bottom 到 .bottom ,不设置为全屏幕的巨细不会从边缘进行动画,包裹的这一层是全屏幕巨细
        Rectangle()
          .fill(.clear)
          .overlay(content: {
            self.customView
          })
          .frame(width: kScreenWidth,height: kScreenHeight)
          .ignoresSafeArea()
          .transition(transition)
          .zIndex(2)
      }
    }.ignoresSafeArea(.keyboard)
  }
}
​

接下来 咱们做一下其他的准备工作,设置动画办法。经过对AnyTransition增加一些默许完成。

extension AnyTransition {
  
  static let popupBackgroundStyle = AnyTransition.opacity
  
  static let bottomStyle = AnyTransition.slideToEdge(insertion: .bottom, removal: .bottom)
  
  static let topStyle = AnyTransition.slideToEdge(insertion: .top, removal: .top)
  
  static let leadingStyle = AnyTransition.slideToEdge(insertion: .leading,removal: .leading)
  
  static let trailingStyle = AnyTransition.slideToEdge(insertion: .trailing,removal: .trailing)
  
  static let leadingAndTrailingStyle = AnyTransition.slideToEdge(insertion: .leading,removal:.trailing)
  
  static let trailingAndLeadingStyle = AnyTransition.slideToEdge(insertion: .trailing,removal:.leading)
  
  static let topAndBottomStyle = AnyTransition.slideToEdge(insertion: .top,removal: .bottom)
  
  static let bottomAndTopStyle = AnyTransition.slideToEdge(insertion: .bottom,removal: .top)
  
  private enum MoveDirection {
    case leading
    case trailing
    case top
    case bottom
  }
  
  private static func slideToEdge(
    insertion: MoveDirection? = .leading,
    removal: MoveDirection? = .trailing
  ) -> AnyTransition {
    return AnyTransition.asymmetric(
      insertion: createMoveTransition(insertion!),
      removal: createMoveTransition(removal!)
    )
  }
​
  private static func createMoveTransition(
    _ direction: MoveDirection
  ) -> AnyTransition {
    switch direction {
      case .leading: return AnyTransition.move(edge: .leading)
      case .trailing: return AnyTransition.move(edge: .trailing)
      case .top:   return AnyTransition.move(edge: .top)
      case .bottom:  return AnyTransition.move(edge: .bottom)
    }
  }
}
​
extension AnyTransition {
  static var fly: AnyTransition {
    AnyTransition.modifier(active: FlyModifier(pct: 0), identity: FlyModifier(pct: 1))
  }
}
​
struct FlyModifier: GeometryEffect {
  var pct: Double
  
  var animatableData: Double {
    get {
      pct
    }
    set {
      pct = newValue
    }
  }
  
  func effectValue(size: CGSize) -> ProjectionTransform {
    let a = CGFloat(Angle(degrees: 90 * (1 - pct)).radians)
    
    var transform3d = CATransform3DIdentity
    transform3d.m34 = -1 / max(size.width, size.height)
    
    transform3d = CATransform3DRotate(transform3d, a, 1, 0, 0)
    transform3d = CATransform3DTranslate(transform3d, -size.width / 2.0, -size.width / 2.0, 0)
    
    let afffineTransform1 = ProjectionTransform(CGAffineTransform(translationX: size.width / 2.0, y: size.width / 2.0))
    let afffineTransform2 = ProjectionTransform(CGAffineTransform(scaleX: CGFloat(pct * 2), y: CGFloat(pct * 2)))
    
    if pct <= 0.5 {
      return ProjectionTransform(transform3d).concatenating(afffineTransform2).concatenating(afffineTransform1)
    } else {
      return ProjectionTransform(transform3d).concatenating(afffineTransform1)
    }
  }
}

咱们看下弹窗作用:

SwiftUI中弹窗实现

关于内部弹窗的完成,能够自界说各种样式,接下来是一个自界说的Page,结合ObservableObjectViewModifier

import SwiftUI
​
struct CustomPopPage: View {
  
  @ObservedObject private var popObserve = CustomPopObserver()
 
  let gradientList: [Gradient] = [.red,.orange,.yellow,.orange,.blue,.indigo,.purple]
​
  var body: some View {
    VStack{
      Spacer()
      ForEach(0..<7){ index in
        Button("展现弹窗(index + 1)"){
          popViewAction(index+1)
        }
        .frame(width: 200,height: 40)
        .foregroundColor(.white)
        .background(LinearGradient(gradient: gradientList[index], startPoint: .top, endPoint: .bottom))
        .cornerRadius(10)
        .padding(.top,5)
      }
      Spacer()
    }
    .modifier(CustomPopViewModifier(transition: getTransitionWithIndex(popObserve.popType),content: {
      switch popObserve.popType {
      case .popView1:
        CustomPopView1()
      case .popView2:
        CustomPopView2()
      case .popView3:
        CustomPopView3()
      case .popView4:
        CustomPopView4()
      case .popView5:
        CustomPopView1()
      case .popView6:
        CustomPopView2()
      case .popView7:
        CustomPopView1()
      }
    }))
    .environmentObject(popObserve)
  }
  
  func popViewAction(_ idx: Int) {
    guard let type = CustomPopType(rawValue: idx) else {
      return
    }
    popObserve.popType = type
  }
  
  func getTransitionWithIndex(_ index: CustomPopType) -> AnyTransition {
    switch index.rawValue {
    case 1:
      return .topStyle
    case 2:
      return .bottomStyle
    case 3:
      return .topAndBottomStyle
    case 4:
      return .bottomAndTopStyle
    case 5:
      return .leadingStyle
    case 6:
      return .leadingAndTrailingStyle
    case 7:
      return .trailingStyle
    default:
      return .bottomStyle
    }
  }
}

这样操作下来 咱们就能完成上图中的弹窗操作,但是还有一些问题存在。

问题

咱们App在设计的时分 UI往往是比较杂乱 最外层是 NavigationViewTabView,这时分弹窗并不会把导航栏进行掩盖,弹窗的层级比导航栏还要低(最外层的View层级是最低的)。点击回来按钮会直接回来到上一层,弹窗会在dissmiss的时分消失掉。而不是按照咱们的主意 先把弹窗消失掉,再次点击回来到上一个界面。

这时分弹窗有其他布景色会明显和导航栏别离,并且由于A界面在push到B界面的时分,导航栏是从A界面传过来的,B界面的弹窗并不能把导航栏包裹在内,就会形成视图UI层级上的别离。

ZStack的弹窗办法适用范围:没有导航栏或导航栏躲藏,没有布景色的弹窗,或许是大局的体系弹窗 能够在App的最外层最一次包装

2.运用体系的模态弹窗跳转完成

  • sheetfullscreenCover : 模态常用,根本不会用到弹窗中,功用有局限性。
  • alertalertSheet:体系自带,不支持自界说。在项目中用到的不多,满足不了自界说UI的需求。

该办法可自界说功用不多,都是从底部弹出,后面view缩小的一个动画,并且动画不可修正,局限性太高。简略测试代码如下,

struct TestView: View {
  @State var state = false
  @State var isSheet = false
  
  var body: some View {
    NavigationView {
      Color.white.edgesIgnoringSafeArea(.all)
        .overlay(content: {
          VStack{
            Button("全屏弹窗") {
              state = true
            }
            .foregroundColor(.white)
            .frame(width: 100,height: 35)
            .background(.purple)
            .cornerRadius(10)
            .padding(.bottom,10)
            
            Button("半屏弹窗") {
              isSheet = true
            }
            .foregroundColor(.white)
            .frame(width: 100,height: 35)
            .background(.purple)
            .cornerRadius(10)
            
          }
        })
        .fullScreenCover(isPresented: $state) {
          ZStack{
            Color.blue.opacity(0.3).contentShape(Rectangle())
              .onTapGesture {
                state = false
              }
            
            Text("这是全屏弹窗").foregroundColor(.red).font(.system(size: 18))
          }.ignoresSafeArea()
      }
      .sheet(isPresented: $isSheet) {
        ZStack{
          Color.blue.opacity(0.3).contentShape(Rectangle())
            .onTapGesture {
              isSheet = false
            }
          
          Text("这是半屏弹窗").foregroundColor(.red).font(.system(size: 18))
        }.ignoresSafeArea()
      }
    }.navigationTitle(Text("swiftUI"))
  }
}
​

作用如下

SwiftUI中弹窗实现

上面的办法适合一些自界说弹窗,只适用底部弹出

3.获取主操控器 自界说模态跳转

咱们能够运用在UIKit中的办法,获取当时的操控器,运用present的办法弹出自界说视图。咱们能够运用swiftUI中的环境变量,每次取值的时分都获取当时ViewController ,运用ViewController去present出咱们自界说的弹窗。

1.自界说environmentKeyvalue
struct ViewControllerHolder {
  weak var value: UIViewController?
}
​
struct ViewControllerKey: EnvironmentKey {
  
  static var defaultValue: ViewControllerHolder {
    ViewControllerHolder(value: UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController)
  }
}
​
extension EnvironmentValues {
  var viewController: UIViewController? {
    get{ self[ViewControllerKey.self].value }
    set{ self[ViewControllerKey.self].value = newValue }
  }
}
2.设置UIViewController拓宽办法
extension UIViewController {
  func present<Content: View>(@ViewBuilder content:() -> Content) {
    let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
    toPresent.modalPresentationStyle = .overCurrentContext
    toPresent.modalTransitionStyle = .crossDissolve
    toPresent.view.backgroundColor = .clear
    toPresent.rootView = AnyView(content().environment(.viewController, toPresent))
    
    present(toPresent, animated: false)
  }
}
3.在需求弹窗的地方引进环境变量
///自界说弹窗
struct PresentViewTest: View {
  /// 引进环境变量
  @Environment(.viewController) private var vcHolder
  
  var body: some View {
    NavigationView {
      Color.blue.opacity(0.3)
        .edgesIgnoringSafeArea(.all)
        .overlay {
          Button("弹窗展现") {
            vcHolder?.present(content: {
              PresentView1()
            })
          }
          .frame(width: 200,height: 35)
          .background(.purple)
          .foregroundColor(.white)
          .cornerRadius(10)
        }
      
    }.navigationTitle("presentTest")
  }
}
​
struct PresentView1: View {
  /// 引进环境变量
  @Environment(.viewController) private var vcHolder
  
  @State var isShow = false
  
  var body: some View {
    ZStack{
      Color.black.opacity(0.3).edgesIgnoringSafeArea(.all).onTapGesture {
        withAnimation {
          isShow = false
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2){
          vcHolder?.dismiss(animated: true)
        }
      }
      
      Text("我是present界面的弹窗")
        .font(.system(size: 14))
        .padding(15)
        .frame(width: 300,height: 200 )
        .foregroundColor(.black)
        .background(.yellow)
        .cornerRadius(10)
        .scaleEffect(isShow ? 1 : 0)
        .rotationEffect(.degrees(isShow ? 360 : 0))
        .opacity(isShow ? 1: 0)
    }
    .onAppear {
      withAnimation(.easeIn(duration: 0.2)) {
        isShow = true
      }
    }
  }
}
​

上面例子履行作用如下

SwiftUI中弹窗实现

以上就是三种弹窗的完成和作用。

总结

  • 运用ZStack 会有层级关系,当App结构杂乱时 掩盖不了NavigationViewTabbarView,适用于toast和一些无布景的弹窗
  • 体系弹窗:alert或许sheet 做一些体系级的弹窗比较适宜,自界说不推荐
  • 自界说模态跳转:适合在各个界面做弹窗处理,需求在各个界面引进环境变量,present和dismiss弹窗,并且present履行有动画,各个弹窗的机遇联接有点长。

以上就是弹窗相关的内容,如有不对之处 欢迎指正。如果有其他办法 欢迎沟通。