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

rea*_*lph 13 swift swiftui

我对SwiftUI框架还很陌生,还没有完全理解,所以请耐心等待。

当绑定更改时,是否可以从“另一个视图”内部触发“叠加视图”?参见下图:

在此处输入图片说明

我认为此“重叠视图”将包裹我的所有视图。我不确定如何执行此操作-也许可以使用ZIndex。我还猜想,当绑定更改时,我需要某种回调,但是我也不知道该怎么做。

到目前为止,这是我得到的:

内容查看

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

    var body: some View {
        VStack {
            LikeButton(liked: $liked)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Like按钮

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Mat*_*ini 29

在SwiftUI中创建“烤面包”非常容易,而且很有趣。

我们开始做吧!

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)

            }

        }

    }

}
Run Code Online (Sandbox Code Playgroud)

正文说明:

  • GeometryReader为我们提供了首选的超级视图尺寸,从而为我们提供了完美的尺寸Toast
  • ZStack 将视图彼此堆叠。
  • 逻辑很简单:如果不希望看到烤面包(isShowing == false),则可以渲染presenting视图。如果必须显示(isShowing == true),则我们将presenting视图渲染得有些模糊-因为我们可以-然后我们接下来创建烤面包。
  • The toast is just a VStack with a Text, with custom frame sizing, some design bells and whistles (colors and corner radius), and a default slide transition.

I added this method on View to make the Toast creation easier:

extension View {

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

}
Run Code Online (Sandbox Code Playgroud)

And a little demo on how to use it:

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!"))
    }

}
Run Code Online (Sandbox Code Playgroud)

I used a NavigationView to make sure the view fills the entire screen, so the Toast is sized and positioned correctly.

The withAnimation block ensures the Toast transition is applied.


How it looks:

在此处输入图片说明

It's easy to extend the Toast with the power of SwiftUI DSL.

The Text property can easily become a @ViewBuilder closure to accomodate the most extravagant of the layouts.


To add it to your content view:

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!"))
    }
}
Run Code Online (Sandbox Code Playgroud)

How to hide the toast afte 2 seconds (as requested):

Append this code after .transition(.slide) in the toast VStack.

.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
      withAnimation {
        self.isShowing = false
      }
    }
}
Run Code Online (Sandbox Code Playgroud)

Tested on Xcode 11.1

  • 由于 .onAppear() 在第一次出现时仅被调用一次,因此您需要将其替换为: `var body: some View { if self.isShowing { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.isShowing = false } } 返回 GeometryReader {.........` (8认同)
  • 当我“无权访问”填满整个屏幕的视图时,如何使其发挥作用?正如问题所假设的那样,我想从*任何视图*(即仅填充屏幕的一小部分的子视图)执行此操作。这怎么可能?对于给定的解决方案,您必须有权访问基本视图才能使其工作。 (3认同)
  • @realph 添加为答案的最后一部分 - 您可以使用“DispatchQueue.asyncAfter”简单地将“isShowing”设置为 false (2认同)
  • 你是个明星!我明天将尝试打开包装,但很高兴现在可以正常使用。再次感谢。 (2认同)
  • @realph禁用淡入/淡出动画,请使用.transition(.identity) (2认同)

pro*_*asm 9

我修改了上面 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
Run Code Online (Sandbox Code Playgroud)

查看+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
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

有了这个,你可以敬酒文本,或图像(或两者,如下所示),或任何其他视图。

在此处输入图片说明


Moj*_*ini 5

应用程序范围视图

如果您希望它在应用程序范围内使用,请将其放在应用程序范围内的某个位置!例如,您可以将其添加到MyProjectApp.swift(或sceneDelegateUIKit/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
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

请注意,这是一个演示,您应该使用environment变量或 smt 以便能够从该视图主体之外更改它

  • 有什么方法可以让它在工作表或全屏前面工作吗? (11认同)