自界说弹窗
在UIKit
中 自界说弹窗办法很多,比方: 模态ViewController
、view掩盖
、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
swiftUI
中 View
的 modifie
入参是view,并且回来值也是view,咱们运用这一特性来做弹窗。自界说ViewModifier
,需求完成ViewModifier
协议,而在ViewModifier
协议办法中咱们能够拿到当时的content,对当时的content在做一层ZStack包装。在自界说的ViewModifier
中,仍是用了@ViewBuilder
来润饰传参,这样能够在ViewBuilder
中传入多个View,最终在经过ZStack
包裹。下面的处理中其实还对customView
进行了一层包装,在完成动画的时分能够做到顶部和底部弹入、弹出的过度。
import SwiftUI
struct 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)
}
}
}
咱们看下弹窗作用:
关于内部弹窗的完成,能够自界说各种样式,接下来是一个自界说的Page,结合ObservableObject
和ViewModifier
,
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往往是比较杂乱 最外层是 NavigationView
或TabView
,这时分弹窗并不会把导航栏进行掩盖,弹窗的层级比导航栏还要低(最外层的View层级是最低的)。点击回来按钮会直接回来到上一层,弹窗会在dissmiss的时分消失掉。而不是按照咱们的主意 先把弹窗消失掉,再次点击回来到上一个界面。
这时分弹窗有其他布景色会明显和导航栏别离,并且由于A界面在push到B界面的时分,导航栏是从A界面传过来的,B界面的弹窗并不能把导航栏包裹在内,就会形成视图UI层级上的别离。
ZStack的弹窗办法适用范围:没有导航栏或导航栏躲藏,没有布景色的弹窗,或许是大局的体系弹窗 能够在App的最外层最一次包装
2.运用体系的模态弹窗跳转完成
-
sheet
,fullscreenCover
: 模态常用,根本不会用到弹窗中,功用有局限性。 -
alert
,alertSheet
:体系自带,不支持自界说。在项目中用到的不多,满足不了自界说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"))
}
}
作用如下
上面的办法适合一些自界说弹窗,只适用底部弹出
3.获取主操控器 自界说模态跳转
咱们能够运用在UIKit
中的办法,获取当时的操控器,运用present的办法弹出自界说视图。咱们能够运用swiftUI中的环境变量,每次取值的时分都获取当时ViewController
,运用ViewController
去present出咱们自界说的弹窗。
1.自界说environmentKey
和value
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
}
}
}
}
上面例子履行作用如下
以上就是三种弹窗的完成和作用。
总结
- 运用
ZStack
会有层级关系,当App结构杂乱时 掩盖不了NavigationView
和TabbarView
,适用于toast和一些无布景的弹窗 - 体系弹窗:
alert
或许sheet
做一些体系级的弹窗比较适宜,自界说不推荐 - 自界说模态跳转:适合在各个界面做弹窗处理,需求在各个界面引进环境变量,present和dismiss弹窗,并且present履行有动画,各个弹窗的机遇联接有点长。
以上就是弹窗相关的内容,如有不对之处 欢迎指正。如果有其他办法 欢迎沟通。