SwiftUI:可以从任何视图触发的全局覆盖

SwiftUI: Global Overlay That Can Be Triggered From Any View

我是 SwiftUI 框架的新手,我还没有完全理解它,所以请多多包涵。

有没有办法在绑定更改时从 "another view" 内部触发 "overlay view"?见下图:

我想这个 "overlay view" 会包含我所有的观点。我还不确定该怎么做——也许使用 ZIndex。我也想当绑定更改时我需要某种回调,但我也不确定该怎么做。

这是我目前得到的:

ContentView

struct ContentView : View {
    @State private var liked: Bool = false

    var body: some View {
        VStack {
            LikeButton(liked: $liked)
        }
    }
}

点赞按钮

struct LikeButton : View {
    @Binding var liked: Bool

    var body: some View {
        Button(action: { self.toggleLiked() }) {
            Image(systemName: liked ? "heart" : "heart.fill")
        }
    }

    private func toggleLiked() {
        self.liked = !self.liked
        // NEED SOME SORT OF TOAST CALLBACK HERE
    }
}

我觉得我的 LikeButton 中需要某种回调,但我不确定这一切在 Swift 中是如何工作的。

如有任何帮助,我们将不胜感激。提前致谢!

使用 .presentation() 在点击按钮时显示提醒。

LikeButton中:

@Binding var liked: Bool

var body: some View {
    Button(action: {self.liked = !self.liked}, label: {
        Image(systemName: liked ? "heart.fill" : "heart")
    }).presentation($liked) { () -> Alert in
        Alert.init(title: Text("Thanks for liking!"))
    }
}

您还可以使用 .presentation() 来呈现其他模态视图,例如 PopoverActionSheet。有关不同 .presentation() 选项的信息,请参阅 Apple 的 SwiftUI 文档中该页面的 here 和 "See Also" 部分。

编辑:使用自定义视图的示例 Popover:

@State var liked = false
let popover = Popover(content: Text("Thanks for liking!").frame(width: 200, height: 100).background(Color.white), dismissHandler: {})
var body: some View {
    Button(action: {self.liked = !self.liked}, label: {
        Image(systemName: liked ? "heart.fill" : "heart")
    }).presentation(liked ? popover : nil)
}

在 SwiftUI 中构建 "toast" 非常简单且有趣!

开始吧!

struct Toast<Presenting>: View where Presenting: View {

    /// The binding that decides the appropriate drawing in the body.
    @Binding var isShowing: Bool
    /// The view that will be "presenting" this toast
    let presenting: () -> Presenting
    /// The text to show
    let text: Text

    var body: some View {

        GeometryReader { geometry in

            ZStack(alignment: .center) {

                self.presenting()
                    .blur(radius: self.isShowing ? 1 : 0)

                VStack {
                    self.text
                }
                .frame(width: geometry.size.width / 2,
                       height: geometry.size.height / 5)
                .background(Color.secondary.colorInvert())
                .foregroundColor(Color.primary)
                .cornerRadius(20)
                .transition(.slide)
                .opacity(self.isShowing ? 1 : 0)

            }

        }

    }

}

正文说明:

  • GeometryReader 为我们提供了 superview 的首选大小,从而为我们的 Toast.
  • 提供了完美的大小
  • ZStack 将视图相互堆叠。
  • 逻辑很简单:如果不应该看到吐司 (isShowing == false),那么我们渲染 presenting 视图。如果必须呈现吐司 (isShowing == true),那么我们将 presenting 视图渲染得有点模糊 - 因为我们可以 - 接下来我们创建吐司。
  • Toast 只是一个带有 TextVStack,具有自定义框架大小、一些设计花哨的功能(颜色和圆角半径)以及默认的 slide 过渡。

我在 View 上添加了这个方法,使 Toast 创建更容易:

extension View {

    func toast(isShowing: Binding<Bool>, text: Text) -> some View {
        Toast(isShowing: isShowing,
              presenting: { self },
              text: text)
    }

}

以及有关如何使用它的小演示:

struct ContentView: View {

    @State var showToast: Bool = false

    var body: some View {
        NavigationView {
            List(0..<100) { item in
                Text("\(item)")
            }
            .navigationBarTitle(Text("A List"), displayMode: .large)
            .navigationBarItems(trailing: Button(action: {
                withAnimation {
                    self.showToast.toggle()
                }
            }){
                Text("Toggle toast")
            })
        }
        .toast(isShowing: $showToast, text: Text("Hello toast!"))
    }

}

我使用 NavigationView 来确保视图填满整个屏幕,因此 Toast 的大小和位置都正确。

withAnimation 块确保应用 Toast 转换。


外观:

借助 SwiftUI DSL 的强大功能,可以轻松扩展 Toast

Text 属性 可以很容易地成为一个 @ViewBuilder 闭包以适应最奢侈的布局。


将其添加到您的内容视图:

struct ContentView : View {
    @State private var liked: Bool = false

    var body: some View {
        VStack {
            LikeButton(liked: $liked)
        }
        // make it bigger by using "frame" or wrapping it in "NavigationView"
        .toast(isShowing: $liked, text: Text("Hello toast!"))
    }
}

如何在 2 秒后隐藏吐司(按要求):

在 toast VStack 中的 .transition(.slide) 之后附加此代码 VStack

.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
      withAnimation {
        self.isShowing = false
      }
    }
}

测试于 Xcode 11.1

我修改了上面 Matteo Pacini 的出色回答,合并了评论以使 Toast 在延迟后淡入和淡出。我还修改了 View 扩展,使其更通用一些,并接受类似于 .sheet 工作方式的尾随闭包。

ContentView.swift:

struct ContentView: View {
    @State private var lightsOn: Bool = false
    @State private var showToast: Bool = false

    var body: some View {
        VStack {
            Button(action: {
                if (!self.showToast) {
                    self.lightsOn.toggle()

                    withAnimation {
                        self.showToast = true
                    }
                }
            }){
                Text("switch")
            } //Button
            .padding(.top)

            Image(systemName: self.lightsOn ? "lightbulb" : "lightbulb.fill")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .padding(.all)
                .toast(isPresented: self.$showToast) {
                    HStack {
                        Text("Lights: \(self.lightsOn ? "ON" : "OFF")")
                        Image(systemName: self.lightsOn ? "lightbulb" : "lightbulb.fill")
                    } //HStack
                } //toast
        } //VStack
    } //body
} //ContentView

查看+Toast.swift:

extension View {
    func toast<Content>(isPresented: Binding<Bool>, content: @escaping () -> Content) -> some View where Content: View {
        Toast(
            isPresented: isPresented,
            presenter: { self },
            content: content
        )
    }
}

Toast.swift:

struct Toast<Presenting, Content>: View where Presenting: View, Content: View {
    @Binding var isPresented: Bool
    let presenter: () -> Presenting
    let content: () -> Content
    let delay: TimeInterval = 2

    var body: some View {
        if self.isPresented {
            DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
                withAnimation {
                    self.isPresented = false
                }
            }
        }

        return GeometryReader { geometry in
            ZStack(alignment: .bottom) {
                self.presenter()

                ZStack {
                    Capsule()
                        .fill(Color.gray)

                    self.content()
                } //ZStack (inner)
                .frame(width: geometry.size.width / 1.25, height: geometry.size.height / 10)
                .opacity(self.isPresented ? 1 : 0)
            } //ZStack (outer)
            .padding(.bottom)
        } //GeometryReader
    } //body
} //Toast

有了它,您可以吐司文本或图像(或两者,如下所示)或任何其他视图。

我正在使用这个开源:https://github.com/huynguyencong/ToastSwiftUI。使用起来非常简单。

struct ContentView: View {
    @State private var isShowingToast = false
    
    var body: some View {
        VStack(spacing: 20) {
            Button("Show toast") {
                self.isShowingToast = true
            }
            
            Spacer()
        }
        .padding()

        // Just add a modifier to show a toast, with binding variable to control
        .toast(isPresenting: $isShowingToast, dismissType: .after(3)) {
            ToastView(message: "Hello world!", icon: .info)
        }
    }
}

App-wide 查看

如果您希望它是 app-wide,请在某处输入 app-wide!例如,您可以像这样将它添加到 MyProjectApp.swift(或 sceneDelegate 中 UIKit/AppDelegate 项目)文件中:

请注意按钮和状态只是为了更多的解释,你可以考虑按照你喜欢的方式改变它们

@main
struct SwiftUIAppPlaygroundApp: App {  // <- Note that where we are!
    @State var showToast = false

    var body: some Scene {
        WindowGroup {
            Button("App-Wide Button") { showToast.toggle() }

            ZStack {
                ContentView() // <- The app flow

                if showToast {
                    MyCustomToastView().ignoresSafeArea(.all, edges: .all) // <- App-wide overlays
                }
            }
        }
    }
}

看到了吗?现在您可以在屏幕的任何位置添加任何类型的视图,不会阻止动画。只需将 @State 转换为某种 AppState,如 Observables 或 Environments 即可!你做到了!

这里是如何覆盖所有视图,包括 NavigationView!

创建一个 class 模型来存储您的观点!

class ParentView:ObservableObject {
    
        @Published var view:AnyView = AnyView(EmptyView())
        
    }

在父视图中创建模型并在视图层次结构中调用它 将此 class 传递给父视图的环境对象

struct Example: View {
    @StateObject var parentView = ParentView()

    var body: some View {
        ZStack{
            NavigationView{
                ChildView()
                    .environmentObject(parentView)
                    .navigationTitle("dynamic parent view")
            }
            parentView.view
        }
    }
}

从现在开始,您可以通过

在子视图中调用父视图
@EnvironmentObject var parentView:ParentView

然后,例如在您的点击手势中,您可以更改父视图并显示一个涵盖所有内容的弹出窗口,包括您的导航视图

@StateObject var parentView = ParentView()

这是完整的解决方案副本,您可以在预览中使用它!

import SwiftUI

class ParentView:ObservableObject {
        @Published var view:AnyView = AnyView(EmptyView())
    }


struct example: View {
    @StateObject var parentView = ParentView()

    var body: some View {
        ZStack{
            NavigationView{
                ChildView()
                    .environmentObject(parentView)
                    .navigationTitle("dynamic parent view")
            }
            parentView.view
        }
    }
}
struct ChildView: View {
    @EnvironmentObject var parentView:ParentView

    var body: some View {
        ZStack{
            Text("hello")
                .onTapGesture {
                    parentView.view = AnyView(Color.red.opacity(0.4).ignoresSafeArea())
                }
        }
    }
}

struct example_Previews: PreviewProvider {
    static var previews: some View {
        example()
    }
}

您也可以像这样显着改善它...!

struct ParentViewModifire:ViewModifier {
    @EnvironmentObject var parentView:ParentView

    @Binding var presented:Bool
    let anyView:AnyView
    func body(content: Content) -> some View {
        content
            .onChange(of: presented, perform: { value in
                if value {
                    parentView.view = anyView
                }
            })
    }
}
extension View {
     func overlayAll<Overlay>(_ overlay: Overlay, presented: Binding<Bool>) -> some View where Overlay : View {
        self
        .modifier(ParentViewModifire(presented: presented, anyView: AnyView(overlay)))
    }
}

现在在您的子视图中,您可以在您的视图中调用此修饰符

struct ChildView: View {
    @State var newItemPopUp:Bool = false
    var body: some View {
        ZStack{
            Text("hello")
               .overlayAll(newCardPopup, presented: $newItemPopUp)
        }
    }
}

Apple 当前不提供任何允许您制作类似于他们自己的警报弹出窗口的全局视图的 API。

事实上,这些视图实际上仍在使用 UIKit。

如果您想要自己的全局弹出窗口,您可以自行破解(请注意,这尚未经过测试,但非常相似的东西应该适用于全局的 toasts):

import SwiftUI
import Foundation

/// Global class that will manage toasts
class ToastPresenter: ObservableObject {
    // This static property probably isn't even needed as you can inject via @EnvironmentObject
    static let shared: ToastPresenter = ToastPresenter()
    
    private init() {}
    
    @Published private(set) var isPresented: Bool = false
    private(set) var text: String?
    private var timer: Timer?
    
    /// Call this function to present toasts
    func presentToast(text: String, duration: TimeInterval = 5) {
        // reset the toast if one is currently being presented.
        isPresented = false
        self.text = nil
        timer?.invalidate()
        
        self.text = text
        isPresented = true
        timer = Timer(timeInterval: duration, repeats: false) { [weak self] _ in
            self?.isPresented = false
        }
    }
}


/// The UI for a toast
struct Toast: View {
    var text: String
    
    var body: some View {
        Text(text)
            .padding()
            .background(Capsule().fill(Color.gray))
            .shadow(radius: 6)
            .transition(AnyTransition.opacity.animation(.default))
    }
}

extension View {
    /// ViewModifier that will present a toast when its binding changes
    @ViewBuilder func toast(presented: Binding<Bool>, text: String) -> some View {
        ZStack {
            self
        
            if presented.wrappedValue {
                Toast(text: text)
            }
        }
        .ignoresSafeArea(.all, edges: .all)
    }
}

/// The first view in your app's view hierarchy
struct RootView: View {
    @StateObject var toastPresenter = ToastPresenter.shared
    
    var body: some View {
        MyAppMainView()
            .toast(presented: $toastPresenter.isPresented, text: toastPresenter.text)
            // Inject the toast presenter into the view hierarchy
            .environmentObject(toastPresenter)
    }
}

/// Some view later on in the app
struct SomeViewDeepInTheHierarchy: View {
    @EnvironmentObject var toastPresenter: ToastPresenter
    
    var body: some View {
        Button {
            toastPresenter.presentToast(text: "Hello World")
        } label: {
            Text("Show Toast")
        }
    }
}