我正在尝试实现一个可以显示在导航栏顶部的视图。我们将此视图称为NotificationView
。从任何其他SwiftUI视图中,触发该视图应该很容易。例如在登录视图中,如果用户名和密码错误。
下面您将找到我到目前为止的代码。仅当您在Username
和Password
字段中输入一个值时,此代码的问题才变得可见。通知视图正确显示,但同时Username
和Password
字段丢失了它们的值。
之所以发生这种情况,是因为再次渲染了整个视图树,并通过LoginView
用空的LoginModel
创建了一个全新的NavigationLink
实例。
您如何在SwiftUI中正确执行操作,以免丢失值?
import SwiftUI
final class NotificationModel: ObservableObject {
func show(text: String) {
self.text = text
isHidden = false
}
@Published
var isHidden = true
var text = ""
}
struct NotificationView: View {
@EnvironmentObject
var model: NotificationModel
var body: some View {
GeometryReader { geometry in
if !self.model.isHidden {
Group {
HStack {
Image(systemName: "exclamationmark.triangle")
.font(Font.largeTitle.weight(.light))
Text(self.model.text)
.font(Font.body.weight(.medium))
.padding()
Spacer()
}
.frame(maxWidth: .infinity, minHeight: geometry.safeAreaInsets.top + 44)
.padding(EdgeInsets(top: geometry.safeAreaInsets.top, leading: 20, bottom: 0, trailing: 20))
}
.background(Color.red)
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
self.model.isHidden = true
}
}
}
}.edgesIgnoringSafeArea(.all)
}
}
final class LoginModel: ObservableObject {
@Published
var email = ""
@Published
var password = ""
let notificationModel: NotificationModel
init(notificationModel: NotificationModel) {
self.notificationModel = notificationModel
}
func submit() {
notificationModel.show(text: "Username/password wrong")
}
}
struct LoginView: View {
@ObservedObject
var model: LoginModel
var body: some View {
VStack {
TextField("Username", text: $model.email)
Divider()
TextField("Password", text: $model.password)
Divider()
Button(action: {
self.model.submit()
}) {
Text("Submit")
}
}
}
}
struct ContentView: View {
@EnvironmentObject
var notificationModel: NotificationModel
var body: some View {
ZStack {
NavigationView {
VStack {
NavigationLink(destination: LoginView(model: LoginModel(notificationModel: notificationModel))) {
Text("Login")
}
Spacer()
}
.navigationBarTitle("Start")
}
NotificationView()
}
}
}
答案 0 :(得分:0)
您可能需要在contentView中翻转LoginModel和notificationModel。
final class NotificationModel: ObservableObject {
func show(text: String) {
self.text = text
self.isHidden = false
}
@Published
var isHidden = true
var text = ""
}
struct NotificationView: View {
@EnvironmentObject
var model: NotificationModel
var body: some View {
GeometryReader { geometry in
if !self.model.isHidden {
Group {
HStack {
Image(systemName: "exclamationmark.triangle")
.font(Font.largeTitle.weight(.light))
Text(self.model.text)
.font(Font.body.weight(.medium))
.padding()
Spacer()
}
.frame(maxWidth: .infinity, minHeight: geometry.safeAreaInsets.top + 44)
.padding(EdgeInsets(top: geometry.safeAreaInsets.top, leading: 20, bottom: 0, trailing: 20))
}
.background(Color.red)
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
self.model.isHidden = true
}
}
}
}.edgesIgnoringSafeArea(.all)
}
}
final class LoginModel: ObservableObject {
@Published
var email = ""
@Published
var password = ""
let notificationModel: NotificationModel
init(notificationModel: NotificationModel) {
self.notificationModel = notificationModel
}
func submit() {
notificationModel.show(text: "Username/password wrong")
}
}
struct LoginView: View {
@ObservedObject
var model: LoginModel
var body: some View {
VStack {
TextField("Username", text: $model.email)
Divider()
TextField("Password", text: $model.password)
Divider()
Button(action: {
self.model.submit()
}) {
Text("Submit")
}
}
}
}
struct ContentView: View {
@EnvironmentObject var loginModel : LoginModel
var body: some View {
return ZStack {
NavigationView {
VStack {
NavigationLink(destination: LoginView(model: loginModel)) {
Text("Login")
}
Spacer()
}
.navigationBarTitle("Start")
}
NotificationView().environmentObject(loginModel.notificationModel)
}
}
}
答案 1 :(得分:0)
全局 Toast
示例:
代码包含 Toast
源代码,修改自 PopupView
:
Github Project
只需复制添加运行⬇️
import SwiftUI
// Example
@main
struct EProgressProApp: App {
@Environment(\.scenePhase) var scenePhase
@State var show: Bool = false
init() {
}
var body: some Scene {
WindowGroup {
NavigationView {
TestPage()
}
// Global toast
// If View B is presented by sheet, add ⬇️ in View B again.
// Toast unable to cross A to presented B(If anyone knows, please leave a comment to let me know...
.em_popup(isPresented: $show,
position: .top(padding: .em_view_space_2),
duration: nil,
animation: .spring(),
ignoreEdges: nil,
closeOnTap: true,
closeOnTapOutside: true,
popupContent: {
Color.red.frame(width: 200, height: 100)
}, onDismiss: {
print("dismissed")
})
.environment(\.em_env_toast, $show)
}
}
}
// EnviromentKey to control toast present
struct EMToastKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var em_env_toast: Binding<Bool> {
get {
self[EMToastKey.self]
}
set {
self[EMToastKey.self] = newValue
}
}
}
struct TestPage: View {
@Environment(\.em_env_toast) private var showToast
var body: some View {
VStack {
Text("Tap")
.onTapGesture {
showToast.wrappedValue.toggle()
}
Spacer()
}
}
}
extension View {
func em_env_toast(_ isPresented: Binding<Bool>) -> some View {
environment(\.em_env_toast, isPresented)
}
}
// Modified from PopupView(fixed layout bug in presented view)
// Toast Source Code
extension View {
public func em_popup<EMPopupContent: View>(isPresented: Binding<Bool>,
position: EMPopupPosition,
duration: Double? = nil,
animation: Animation? = .spring(),
ignoreEdges: Edge.Set?,
closeOnTap: Bool = true,
closeOnTapOutside: Bool = true,
popupContent: @escaping () -> EMPopupContent,
onDismiss: @escaping() -> ()) -> some View {
self.modifier(
EMPopupView(isPresented: isPresented,
position: position,
duration: duration,
animation: animation,
ignoreEdges: ignoreEdges,
closeOnTap: closeOnTap,
closeOnTapOutside: closeOnTapOutside,
popupContent: popupContent,
onDismiss: onDismiss)
)
}
@ViewBuilder
func em_apply_if<T: View>(_ condition: Bool, apply: (Self) -> T) -> some View {
if condition {
apply(self)
} else {
self
}
}
}
public enum EMPopupPosition: Equatable {
case top(padding: CGFloat = 0)
case bottom(padding: CGFloat = 0)
var padding: CGFloat {
switch self {
case let .top(padding):
return padding
case let .bottom(padding):
return padding
}
}
static public func == (lhs: EMPopupPosition, rhs: EMPopupPosition) -> Bool {
switch lhs {
case .top:
switch rhs {
case .top:
return true
case .bottom:
return false
}
case .bottom:
switch rhs {
case .top:
return false
case .bottom:
return true
}
}
}
}
public struct EMPopupView<EMPopupContent: View>: ViewModifier {
@Binding var isPresented: Bool
var position: EMPopupPosition
var duration: Double?
var animation: Animation?
var ignoreEdges: Edge.Set?
var closeOnTap: Bool
var closeOnTapOutside: Bool
var popupContent: () -> EMPopupContent
var onDismiss: () -> ()
@State private var presenterContentRect: CGRect = .zero
@State private var popupContentRect: CGRect = .zero
@State private var topPadding: CGFloat = 0
private var dispatchWorkHolder: DispatchWorkHolder = DispatchWorkHolder()
// Handle dispatch capture self
private var isPresentedRef: ClassReference<Binding<Bool>>?
private var hideOffset: CGFloat {
if case EMPopupPosition.top(_) = position {
return -popupContentRect.height
} else {
return presenterContentRect.height
}
}
init(isPresented: Binding<Bool>,
position: EMPopupPosition,
duration: Double?,
animation: Animation?,
ignoreEdges: Edge.Set?,
closeOnTap: Bool,
closeOnTapOutside: Bool,
popupContent: @escaping () -> EMPopupContent,
onDismiss: @escaping() -> ()) {
self._isPresented = isPresented
self.position = position
self.duration = duration
self.animation = animation
self.ignoreEdges = ignoreEdges
self.closeOnTap = closeOnTap
self.closeOnTapOutside = closeOnTapOutside
self.popupContent = popupContent
self.onDismiss = onDismiss
self.isPresentedRef = ClassReference(self.$isPresented)
}
public func body(content: Content) -> some View {
content
.background(
// get presenter' rect
GeometryReader { proxy -> AnyView in
let rect = proxy.frame(in: .global)
if rect.integral != self.presenterContentRect.integral {
DispatchQueue.main.async {
self.presenterContentRect = rect
}
}
return AnyView(EmptyView())
}
)
.overlay(makePopupContent())
}
private func makePopupContent() -> some View {
if duration != nil {
dispatchWorkHolder.work?.cancel()
dispatchWorkHolder.work = DispatchWorkItem(block: { [weak isPresentedRef] in
isPresentedRef?.value.wrappedValue = false
onDismiss()
})
if isPresented && dispatchWorkHolder.work != nil {
DispatchQueue.main.asyncAfter(deadline: .now() + duration!, execute: dispatchWorkHolder.work!)
}
}
let popup = ZStack {
// background
Color.clear
.onTapGesture {
if closeOnTapOutside {
self.dispatchWorkHolder.work?.cancel()
isPresented = false
self.onDismiss()
}
}
// pop up content
VStack(spacing: 0) {
if position == EMPopupPosition.bottom(padding: 0) { Spacer() }
HStack(spacing: 0) {
popupContent()
.padding(.top, position == EMPopupPosition.top(padding: 0) ? position.padding : 0)
.padding(.bottom, position == EMPopupPosition.bottom(padding: 0) ? position.padding : 0)
.background(
GeometryReader { proxy -> AnyView in
let rect = proxy.frame(in: .global)
if rect.integral != self.popupContentRect.integral {
DispatchQueue.main.async {
self.popupContentRect = rect
}
}
return AnyView(EmptyView().frame(width: popupContentRect.width).animation(.none))
}
)
.onTapGesture {
if closeOnTap {
self.dispatchWorkHolder.work?.cancel()
isPresented = false
self.onDismiss()
}
}
}
if position == EMPopupPosition.top(padding: 0) { Spacer() }
}
}
.em_apply_if(ignoreEdges != nil, apply: { view in
view.edgesIgnoringSafeArea(ignoreEdges!)
})
.opacity(isPresented ? 1 : 0)
.offset(y: isPresented ? 0 : hideOffset)
.animation(animation)
return popup.zIndex(.infinity)
}
}
// Handle dispatch capture self
fileprivate class DispatchWorkHolder {
var work: DispatchWorkItem?
}
// Handle dispatch capture self
fileprivate final class ClassReference<T> {
var value: T
init(_ value: T) {
self.value = value
}
}