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

时间:2019-06-11 18:54:52

标签: swift swiftui

我对SwiftUI框架还很陌生,我还没有完全掌握它,所以请多多包涵。

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

enter image description here

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

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

ContentView

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

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

LikeButton

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中如何工作。

任何对此的帮助将不胜感激。预先感谢!

5 个答案:

答案 0 :(得分:8)

在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)

            }

        }

    }

}

身体的解释:

  • GeometryReader为我们提供了首选的Superview大小,从而为我们的Toast提供了理想的大小。
  • ZStack将视图彼此堆叠。
  • 逻辑微不足道:如果不希望看到烤面包(isShowing == false),则我们渲染presenting视图。如果必须呈现烤面包(isShowing == true),则我们将presenting视图渲染得有些模糊-因为我们可以-然后我们接下来创建烤面包。
  • 吐司只是一个VStack和一个Text,具有自定义框架大小,一些设计的钟声和口哨声(颜色和拐角半径)以及默认的slide过渡。
  • li>

我在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过渡。


外观

enter image description here

借助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秒后隐藏面包(根据要求)

将此代码附加在.transition(.slide)的烤面包VStack中。

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

答案 1 :(得分:7)

我在上面修改了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

View + 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

使用此功能,您可以烘烤文本或图像(或同时显示两者,如下所示)或任何其他视图。

enter image description here

答案 2 :(得分:1)

应用范围内的视图

如果您希望它在整个应用程序范围内,请放在整个应用程序范围内!例如,您可以将其添加到MyProjectApp.swift文件中(或在UIKit / AppDelegate项目的sceneDelegate文件中),如下所示:

请注意按钮和状态只是为了提供更多说明,您可以考虑以自己喜欢的方式更改它们

@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,例如ObservableEnvironment并繁荣发展! ?你做到了!

答案 3 :(得分:0)

点击按钮时,使用.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()选项的信息,请参见here和Apple的SwiftUI文档中该页面上的“另请参阅”部分。

编辑:使用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)
}

答案 4 :(得分:0)

我正在使用以下开源: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)
        }
    }
}