I have a LottieAnimationView along with some other components inside ScrollView, animation is only supposed to play once.
#State var messageBannerVisisbility: Bool = false
var body: some View {
VStack(alignment: .center) {
TrackableScrollView {
VStack(alignment: .center) {
headerView(components: header)
contentView(components: body)
}
} onScrollingStarted: {
hideMessageBanner()
} onScrollingFinished: {
showMessageBanner()
}
.animation(nil)
footerView(footer: content.footer)
}
.onAppear {
showMessageBanner()
}
}
#ViewBuilder private func footerView(footer: SignupPageV2Footer) -> some View {
VStack(alignment: .leading, spacing: 0) {
if let message = footer.message, messageBannerVisisbility {
footerMessageView(from: message)
}
HStack(alignment: .center, spacing: 8) {
Label(footer.signupAction.info.stripHTMLTags)
.textColor(.secondary)
.frame(width: 115, alignment: .leading)
LozengeButton(title: footer.signupAction.label, isLoading: $viewModel.isPaymentInProgress) {
viewModel.startPayment()
}
.accessibility(identifier: "subscribe_button")
}
.padding([.horizontal, .top], 16)
.padding(.bottom, 8)
.background(Color.white)
}
.background(Color.white.edgesIgnoringSafeArea(.bottom).elevation(.LightBackground.small))
}
#ViewBuilder private func footerMessageView(from message: SignupPageV2FooterMessage) -> some View {
message.build { deeplink in
viewModel.handleDeeplink(deeplink)
} processEvent: { event in
viewModel.handleEvent(event)
}
.transition(.move(edge: .bottom).combined(with: .opacity))
}
private func showMessageBanner() {
if messageBannerVisisbility == true { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(.easeIn(duration: 0.3)) {
messageBannerVisisbility = true
}
}
}
private func hideMessageBanner() {
if messageBannerVisisbility == false { return }
withAnimation(.easeIn(duration: 0.3)) {
messageBannerVisisbility = false
}
}
TrackableScrollView is my custom implementation to have scroll Start and End callbacks to show/hide footerMessageView, which is inside footerView, while scrolling.
The issue I am facing is, whenever I scroll LottieAnimationView seems to be resetting and therefore it plays the animation again everytime I scroll.
How do I just update footerView so that animation is only played once after the screen loads?
I have seen similar problems and I have been able to solve them by using an id on the view that is supposed to animate only once.
The best way is to add the id as something that is unique to that instance of the view, normally the id would be based on the data you give the view.
.id(item.id)
This will prevent the view from being re-drawn when there are changes in other components.
Try this:
(I cannot check this code will work or not)
struct SomeView1: View {
#State var messageBannerVisisbility: Bool = false
var body: some View {
VStack(alignment: .center) {
ScrollViewTest(messageBannerVisisbility: messageBannerVisisbility)
footerView(footer: content.footer)
}
.onAppear {
showMessageBanner()
}
}
}
struct ScrollViewTest: View {
#Binding var messageBannerVisisbility: Bool
var body: some View {
TrackableScrollView {
VStack(alignment: .center) {
headerView(components: header)
contentView(components: body)
}
} onScrollingStarted: {
withAnimation(.easeIn(duration: 0.3)) {
messageBannerVisisbility = true
}
} onScrollingFinished: {
withAnimation(.easeIn(duration: 0.3)) {
messageBannerVisisbility = false
}
}
.animation(nil)
}
}
#ViewBuilder private func footerView(footer: SignupPageV2Footer) -> some View {
VStack(alignment: .leading, spacing: 0) {
if let message = footer.message, messageBannerVisisbility {
footerMessageView(from: message)
}
HStack(alignment: .center, spacing: 8) {
Label(footer.signupAction.info.stripHTMLTags)
.textColor(.secondary)
.frame(width: 115, alignment: .leading)
LozengeButton(title: footer.signupAction.label, isLoading: $viewModel.isPaymentInProgress) {
viewModel.startPayment()
}
.accessibility(identifier: "subscribe_button")
}
.padding([.horizontal, .top], 16)
.padding(.bottom, 8)
.background(Color.white)
}
.background(Color.white.edgesIgnoringSafeArea(.bottom).elevation(.LightBackground.small))
}
#ViewBuilder
private func footerMessageView(from message: SignupPageV2FooterMessage) -> some View {
message.build { deeplink in
viewModel.handleDeeplink(deeplink)
} processEvent: { event in
viewModel.handleEvent(event)
}
.transition(.move(edge: .bottom).combined(with: .opacity))
}
Related
I have sub-navigation inside Listview and trying to achieve leftSlide and rightSlide animation but it always shows default slide animation. I even tried to add .transition(.move(edge: . leading)) & .transition(.move(edge: . trailing))
import SwiftUI
import Combine
struct GameTabView: View {
#State var selectedTab: Int = 0
init() {
UITableView.appearance().sectionHeaderTopPadding = 0
}
var body: some View {
listView
.ignoresSafeArea()
}
var listView: some View {
List {
Group {
Color.gray.frame(height: 400)
sectionView
}
.listRowInsets(EdgeInsets())
}
.listStyle(.plain)
}
var sectionView: some View {
Section {
tabContentView
.transition(.move(edge: . leading)) // NOT WORKING
.background(Color.blue)
} header: {
headerView
}
}
private var headerView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
Button {
withAnimation {
selectedTab = 0
}
} label: {
Text("AAAA")
.padding()
}
Button {
withAnimation {
selectedTab = 1
}
} label: {
Text("BBBB")
.padding()
}
Button {
withAnimation {
selectedTab = 2
}
} label: {
Text("BBBB")
.padding()
}
}
}
}
.background(Color.green)
}
#ViewBuilder private var tabContentView: some View {
switch selectedTab {
case 0:
DummyScreen(title: "FIRST", color: .red)
case 1:
DummyScreen(title: "SECOND", color: .green)
case 2:
DummyScreen(title: "THIRD", color: .blue)
default:
EmptyView()
}
}
}
struct DummyScreen: View {
let title: String
let color: Color
var body: some View {
VStack {
ForEach(0..<15, id: \.self) { index in
HStack {
Text("#\(index): title \(title)")
.foregroundColor(Color.black)
.font(.system(size: 30))
.padding(.vertical, 20)
Spacer()
}
.background(Color.yellow)
}
}
.background(color)
}
}
Just like Asperi said, you can change to another type of View to make the transition works.
I tried your code, changed List to VStack with ScrollView inside to wrap around the tabContentView, and the result of the UI showed the same except with a proper animation now, and you don't have to manually adjust the height of your contents since HStack height is dynamic based on your Text() growth.
Edited: header fixed, animation fixed
import SwiftUI
import SwiftUITrackableScrollView //Added
import Combine
struct GameTabView: View {
#State private var scrollViewContentOffset = CGFloat(0) //Added
#State var selectedTab: Int = 0
init() {
UITableView.appearance().sectionHeaderTopPadding = 0
}
var body: some View {
listView
.ignoresSafeArea()
}
var listView: some View {
ZStack { //Added
TrackableScrollView(.vertical, showIndicators: true, contentOffset: $scrollViewContentOffset) {
VStack {
Color.gray.frame(height: 400)
sectionView
}
}
if(scrollViewContentOffset > 400) {
VStack {
headerView
Spacer()
}
}
}
}
var sectionView: some View {
Section {
tabContentView
.transition(.scale) // FIXED
.background(Color.blue)
} header: {
headerView
}
}
private var headerView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
Button {
withAnimation {
selectedTab = 0
}
} label: {
Text("AAAA")
.padding()
}
Button {
withAnimation {
selectedTab = 1
}
} label: {
Text("BBBB")
.padding()
}
Button {
withAnimation {
selectedTab = 2
}
} label: {
Text("BBBB")
.padding()
}
}
}
}
.background(Color.green)
}
#ViewBuilder private var tabContentView: some View {
switch selectedTab {
case 0:
DummyScreen(title: "FIRST", color: .red)
case 1:
DummyScreen(title: "SECOND", color: .green)
case 2:
DummyScreen(title: "THIRD", color: .blue)
default:
EmptyView()
}
}
}
struct DummyScreen: View {
let title: String
let color: Color
var body: some View {
VStack {
ForEach(0..<15, id: \.self) { index in
HStack {
Text("#\(index): title \(title)")
.foregroundColor(Color.black)
.font(.system(size: 30))
.padding(.vertical, 20)
Spacer()
}
.background(Color.yellow)
}
}
.background(color)
}
}
Thanks to #tail and #Asperi
I finally got the solution via updating ScrollView with ScrollviewProxy:
import SwiftUI
import Combine
struct GameTabView: View {
#State var selectedTab: Int = 0
#State var proxy: ScrollViewProxy?
init() {
UITableView.appearance().sectionHeaderTopPadding = 0
}
var body: some View {
listView
.ignoresSafeArea()
}
var listView: some View {
List {
Group {
Color.gray.frame(height: 400)
sectionView
}
.listRowInsets(EdgeInsets())
}
.listStyle(.plain)
}
var sectionView: some View {
Section {
tabContentView
} header: {
headerView
}
}
private var headerView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
Button {
withAnimation {
selectedTab = 0
self.proxy?.scrollTo(0, anchor: .center)
}
} label: {
Text("AAAA")
.padding()
}
Button {
withAnimation {
selectedTab = 1
self.proxy?.scrollTo(1, anchor: .center)
}
} label: {
Text("BBBB")
.padding()
}
Button {
withAnimation {
selectedTab = 2
self.proxy?.scrollTo(2, anchor: .center)
}
} label: {
Text("BBBB")
.padding()
}
}
}
}
.background(Color.green)
}
#ViewBuilder private var tabContentView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(0..<3, id: \.self) { i in
DummyScreen(title: "Name: \(i)", color: .blue)
.frame(idealWidth: UIScreen.main.bounds.width)
.id(i)
}
}
}
.onAppear {
self.proxy = proxy
}
}
}
}
firstly I am really new to iOS development and Swift (2 weeks coming here from PHP :))
I am trying to build a iOS application that has a side menu. And my intention is when I click on a item in the menu the new view will appear on screen like the 'HomeViewController' and for each consequent item like example1, 2 etc (In place of the menu button, Note I will be adding the top nav bar soon to open the menu)
I am wondering how I can accomplish this feature?
Thanks
ContentView.swift
import SwiftUI
struct MenuItem: Identifiable {
var id = UUID()
let text: String
}
func controllView(clickedview:String) {
print(clickedview)
}
struct MenuContent: View{
let items: [MenuItem] = [
MenuItem(text: "Home"),
MenuItem(text: "Example1"),
MenuItem(text: "Example2"),
MenuItem(text: "Example3")
]
var body: some View {
ZStack {
Color(UIColor(red: 33/255.0, green: 33/255.0, blue: 33/255.0, alpha: 1))
VStack(alignment: .leading, spacing: 0) {
ForEach(items) {items in
HStack {
Text(items.text)
.bold()
.font(.system(size: 20))
.multilineTextAlignment(/*#START_MENU_TOKEN#*/.leading/*#END_MENU_TOKEN#*/)
.foregroundColor(Color.white)
Spacer()
}
.onTapGesture {
controllView(clickedview: items.text)
}
.padding()
Divider()
}
Spacer()
}
.padding(.top, 40)
}
}
}
struct SideMenu: View {
let width: CGFloat
let menuOpen: Bool
let toggleMenu: () -> Void
var body: some View {
ZStack {
//Dimmed backgroud
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.15))
.opacity(self.menuOpen ? 1 : 0)
.animation(Animation.easeIn.delay(0.25))
.onTapGesture {
self.toggleMenu()
}
//Menucontent
HStack {
MenuContent()
.frame(width: width)
.offset(x: menuOpen ? 0 : -width)
.animation(.default)
Spacer()
}
}
}
}
struct ContentView: View {
#State var menuOpen = false
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
if menuOpen {
withAnimation {
print("Left")
menuOpen.toggle()
}
}
}
if $0.translation.width > -100 {
if !menuOpen {
withAnimation {
print("Right")
menuOpen.toggle()
}
}
}
}
ZStack {
if !menuOpen {
Button(action: {
self.menuOpen.toggle()
}, label: {
Text("Open Menu")
.bold()
.foregroundColor(Color.white)
.frame(width: 200, height: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.background(Color(.systemBlue))
})
}
SideMenu(width: UIScreen.main.bounds.width/1.6, menuOpen: menuOpen, toggleMenu: toggleMenu)
}
.edgesIgnoringSafeArea(.all)
.gesture(drag)
}
func toggleMenu(){
menuOpen.toggle()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The on tap command from the above code:
.onTapGesture {
controllView(clickedview: items.text)
}
HomeViewController.swift
import UIKit
import WebKit
class HomeViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://developer.apple.com")!
webView.load(URLRequest(url: url))
}
}
You'll need a couple of ingredients:
A way to store the state of the currently active view
A way to communicate the state between your menu and main content view
For the first one, I made an enum that listed the different types of views (ViewType) and added it to your MenuItem model.
For the second, you can pass state via a #Binding from parent to child views and back up the chain.
struct MenuItem: Identifiable {
var id = UUID()
let text: String
let viewType : ViewType
}
enum ViewType {
case home, example1, example2, example3
}
struct MenuContent: View{
#Binding var activeView : ViewType
let items: [MenuItem] = [
MenuItem(text: "Home", viewType: .home),
MenuItem(text: "Example1", viewType: .example1),
MenuItem(text: "Example2", viewType: .example2),
MenuItem(text: "Example3", viewType: .example3)
]
var body: some View {
ZStack {
Color(UIColor(red: 33/255.0, green: 33/255.0, blue: 33/255.0, alpha: 1))
VStack(alignment: .leading, spacing: 0) {
ForEach(items) { item in
HStack {
Text(item.text)
.bold()
.font(.system(size: 20))
.multilineTextAlignment(.leading)
.foregroundColor(Color.white)
Spacer()
}
.onTapGesture {
activeView = item.viewType
}
.padding()
Divider()
}
Spacer()
}
.padding(.top, 40)
}
}
}
struct SideMenu: View {
let width: CGFloat
let menuOpen: Bool
let toggleMenu: () -> Void
#Binding var activeView : ViewType
var body: some View {
ZStack {
//Dimmed backgroud
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.15))
.opacity(self.menuOpen ? 1 : 0)
.animation(Animation.easeIn.delay(0.25))
.onTapGesture {
self.toggleMenu()
}
//Menucontent
HStack {
MenuContent(activeView: $activeView)
.frame(width: width)
.offset(x: menuOpen ? 0 : -width)
.animation(.default)
Spacer()
}
}
}
}
struct ContentView: View {
#State private var menuOpen = false
#State private var activeView : ViewType = .home
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
if menuOpen {
withAnimation {
print("Left")
menuOpen.toggle()
}
}
}
if $0.translation.width > -100 {
if !menuOpen {
withAnimation {
print("Right")
menuOpen.toggle()
}
}
}
}
ZStack {
VStack {
if !menuOpen {
Button(action: {
self.menuOpen.toggle()
}, label: {
Text("Open Menu")
.bold()
.foregroundColor(Color.white)
.frame(width: 200, height: 50, alignment: .center)
.background(Color(.systemBlue))
})
}
switch activeView {
case .home:
HomeViewControllerRepresented()
case .example1:
Text("Example1")
case .example2:
Text("Example2")
case .example3:
Text("Example3")
}
}
SideMenu(width: UIScreen.main.bounds.width/1.6,
menuOpen: menuOpen,
toggleMenu: toggleMenu,
activeView: $activeView)
.edgesIgnoringSafeArea(.all)
}
.gesture(drag)
}
func toggleMenu(){
menuOpen.toggle()
}
}
struct HomeViewControllerRepresented : UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> HomeViewController {
HomeViewController()
}
func updateUIViewController(_ uiViewController: HomeViewController, context: Context) {
}
}
I created a banner modifier that displays a banner from the top. This animates well. However, when I tap to dismiss it, it does not animate at all, just hides even though the tap gesture action has withAnimation wrapping it.
struct BannerModifier: ViewModifier {
#Binding var model: BannerData?
func body(content: Content) -> some View {
content.overlay(
Group {
if model != nil {
VStack {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "exclamationmark.triangle.fill")
VStack(alignment: .leading) {
Text(model?.title ?? "")
.font(.headline)
if let message = model?.message {
Text(message)
.font(.footnote)
}
}
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(.red)
.cornerRadius(10)
.shadow(radius: 10)
Spacer()
}
.padding()
.animation(.easeInOut)
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
model = nil
}
}
.gesture(
DragGesture()
.onChanged { _ in
withAnimation {
model = nil
}
}
)
}
}
)
}
}
struct BannerData: Identifiable {
let id = UUID()
let title: String
let message: String?
}
In the tap gesture, I wipe out the model but it does not animate. It only hides immediately. How can I animate it so it slide up which is opposite of how it slide down to display? It would be really nice if I can also make the drag gesture interactive so I can slide it out like the native notifications.
Removing view from hierarchy is always animated by container, so to fix your modifier it is needed to apply .animation on some helper container (note: Group is not actually a real container).
Here is corrected variant
struct BannerModifier: ViewModifier {
#Binding var model: BannerData?
func body(content: Content) -> some View {
content.overlay(
VStack { // << holder container !!
if model != nil {
VStack {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "exclamationmark.triangle.fill")
VStack(alignment: .leading) {
Text(model?.title ?? "")
.font(.headline)
if let message = model?.message {
Text(message)
.font(.footnote)
}
}
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.red)
.cornerRadius(10)
.shadow(radius: 10)
Spacer()
}
.padding()
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
model = nil
}
}
.gesture(
DragGesture()
.onChanged { _ in
withAnimation {
model = nil
}
}
)
}
}
.animation(.easeInOut) // << here !!
)
}
}
Tested with Xcode 12.1 / iOS 14.1 and test view:
struct TestBannerModifier: View {
#State var model: BannerData?
var body: some View {
VStack {
Button("Test") { model = BannerData(title: "Error", message: "Fix It!")}
Button("Reset") { model = nil }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.modifier(BannerModifier(model: $model))
}
}
This is a slightly improoved version of the banner posted by Asperi
Hope it helps someone.
import SwiftUI
public class BannerData {
public enum BannerType {
case warning, error, success
var textColor: Color {
switch self {
case .warning:
return .black
case .error:
return .white
case .success:
return .white
}
}
var backgroundColor: Color {
switch self {
case .warning:
return .yellow
case .error:
return .red
case .success:
return .green
}
}
var icon: String {
switch self {
case .warning:
return "exclamationmark.triangle.fill"
case .error:
return "exclamationmark.circle.fill"
case .success:
return "checkmark.circle.fill"
}
}
}
var type: BannerType = .success
let title: String
let message: String?
public init(title: String, message: String? = nil, type: BannerType) {
self.title = title
self.message = message
self.type = type
}
}
public struct BannerModifier: ViewModifier {
#Binding var model: BannerData?
public init(model: Binding<BannerData?>) {
_model = model
}
public func body(content: Content) -> some View {
content.overlay(
VStack {
if model != nil {
VStack {
HStack(alignment: .firstTextBaseline) {
Image(systemName: model?.type.icon ?? BannerData.BannerType.success.icon)
VStack(alignment: .leading) {
Text(model?.title ?? "")
.font(.headline)
if let message = model?.message {
Text(message)
.font(.footnote)
}
}
Spacer()
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(model?.type.backgroundColor ?? .clear)
.cornerRadius(10)
.shadow(radius: 10)
Spacer()
}
.padding()
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
model = nil
}
}
.gesture(
DragGesture()
.onChanged { _ in
withAnimation {
model = nil
}
}
)
}
}
.animation(.spring())
)
}
}
I have created a TabBar which is a view at the root of the app.
I'd like to hide the TabBar component when navigating from the closet view to a subview.
The Closet view is a NavigationView containing multiple NavigationLink
Here is the root view of the app:
struct Home: View {
#State var selected = 0
#ObservedObject var viewModel: HomeViewModel
init(viewModel: HomeViewModel) {
self.viewModel = viewModel
}
var body: some View {
ZStack {
if self.selected == 0 {
// viewModel.feedView
Stylist()
}
else if self.selected == 1 {
OutfitView()
}
else if self.selected == 2 {
viewModel.closetView
} else {
Calendar()
}
VStack {
Spacer()
TabBar(selected: self.$selected)
.frame(width: UIScreen.main.bounds.width - 20, alignment: .center)
}
}
}
Here is the TabBar component:
var body : some View{
HStack{
Spacer(minLength: 0)
HStack{
Button(action: {
self.selected = 0
}) {
Image(systemName: "bolt.fill").foregroundColor(self.selected == 0 ? Color("GradientMiddle") : .gray).padding(.horizontal).font(.system(size: 20))
}
Spacer(minLength: 15)
Button(action: {
self.selected = 1
}) {
Image(systemName: "sun.min.fill").foregroundColor(self.selected == 1 ? Color("GradientMiddle") : .gray).padding(.horizontal).font(.system(size: 20))
}
Spacer(minLength: 15)
Button(action: {
self.selected = 2
}) {
Image(systemName: "cube.box.fill").foregroundColor(self.selected == 2 ? Color("GradientMiddle") : .gray).padding(.horizontal).font(.system(size: 20))
}
Spacer(minLength: 15)
Button(action: {
self.selected = 4
}) {
Image(systemName: "calendar").foregroundColor(self.selected == 4 ? Color("GradientMiddle") : .gray).padding(.horizontal).font(.system(size: 20))
}
}.padding(.vertical, 20)
.padding(.horizontal)
.background(Color(UIColor.systemGray5))
.clipShape(Capsule())
.padding(42)
.animation(.interactiveSpring(response: 0.6, dampingFraction: 0.6, blendDuration: 0.6))
}
}
Is there an way to that properly ?
Update:
When I try #Asperi solution I have an error:
Value of type 'some View' has no member 'observingNavigate'
Maybe that could be caused by how I create closetView ?
So here is how I create ClosetView:
HomeViewModel.swift:
class HomeViewModel: ObservableObject {
}
extension HomeViewModel {
var closetView: some View {
return HomeBuilder.makeClosetView()
}
}
HomeBuilder.swift:
enum HomeBuilder {
static func makeClosetView() -> some View {
let viewModel = ClosetViewModel()
return Closet(viewModel: viewModel)
}
}
You need a callback from your Closet view so root view can perform some action. Here is a demo of possible solution.
Let's assume we have the following demo ClosetView
struct ClosetView: View {
var didNavigate: ((Bool) -> Void)?
var body: some View {
NavigationView {
NavigationLink("Link", destination:
Text("Demo")
.onAppear { didNavigate?(true) }
.onDisappear { didNavigate?(false) }
)
}
}
func observingNavigate(callback: #escaping ((Bool) -> Void)) -> some View {
var view = self
view.didNavigate = callback
return view
}
}
then changes in Home will be the following
#State private var showTabbar = true // << state !!
var body: some View {
ZStack {
if self.selected == 0 {
// viewModel.feedView
Stylist()
}
else if self.selected == 1 {
OutfitView()
}
else if self.selected == 2 {
viewModel.closetView
.observingNavigate { self.showTabbar = !$0 } // << here !!
} else {
Calendar()
}
VStack {
Spacer()
if showTabbar { // << here !!
TabBar(selected: self.$selected)
.frame(width: UIScreen.main.bounds.width - 20, alignment: .center)
}
}
}
}
Note: I don't see how you create viewModel.closetView so callback is injected as property, however it can also be injected via constructor arguments, but this does not change idea much.
I have the following:
struct SettingsView: View {
#State private var showSuccessSave: Bool = false
var body: some View {
NavigationView {
ScrollView {
VStack {
SuccessNotificationView()
.offset(y: self.showSuccessSave ? -UIScreen.main.bounds.height/9 : -UIScreen.main.bounds.height)
.animation(
.interactiveSpring(
response: 0.8,
dampingFraction: 0.8,
blendDuration: 1
)
)
.onTapGesture {
self.showSuccessSave = false
}
Button(action: {
self.showSuccessSave.toggle()
}) {
Text("Save")
}
}
}
.navigationBarTitle("Settings")
}
}
}
And the function I call:
struct SuccessNotificationView: View {
var body: some View {
Text("Success")
.padding()
.foregroundColor(Color.white)
.frame(width: UIScreen.main.bounds.width, height: 100)
.background(Color.green)
.cornerRadius(20)
}
}
When it pops up, look at the settings text:
How do I make that notification be on the foreground covering the Settings text? Right now it displays everything in that view on top of it.
Here is possible approach. Tested with Xcode 11.4 / iOS 13.4
struct SettingsView: View {
#State private var showSuccessSave: Bool = false
var body: some View {
ZStack(alignment: .top) {
NavigationView {
ScrollView {
VStack {
Button(action: {
self.showSuccessSave.toggle()
}) {
Text("Save")
}
}
}
.navigationBarTitle("Settings")
}
SuccessNotificationView()
.offset(y: self.showSuccessSave ? 0 : -UIScreen.main.bounds.height)
.animation(
.interactiveSpring(
response: 0.8,
dampingFraction: 0.8,
blendDuration: 1
)
)
.onTapGesture {
self.showSuccessSave = false
}
}
}
}