Potential SwiftUI ZStack Transition bug when pressed rapidly - swift

I want to make a modal that works cross platform on iOS and Mac.
The issue is if I toggle the modal rapidly—there's a strange behavior. Attached GIF with the weird behavior below. Is this SwiftUI Bug?
If not, What did I do wrong?
EDIT: more info.
Without animation and transition, the interaction works as expected.
Here's the code on Playground
import SwiftUI
import PlaygroundSupport
struct CView: View {
#State var isShown = false
var body: some View {
ZStack {
Color.white
ZStack {
Color.green
VStack {
HStack {
Spacer()
Text("Open").onTapGesture {
self.isShown = true
}
}
Spacer()
}
}
.edgesIgnoringSafeArea(.all)
.zIndex(0)
if self.isShown {
ZStack {
Color.red
VStack {
HStack {
Spacer()
Text("Close").onTapGesture {
self.isShown = false
}
}
Spacer()
}
}
.transition(AnyTransition.move(edge: .bottom))
.animation(.easeInOut)
.edgesIgnoringSafeArea(.all)
.zIndex(1)
}
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.setLiveView(CView())
``
[1]: https://i.stack.imgur.com/21z7z.gif

Here is a solution. Tested with Xcode 11.5b
struct CView: View {
#State private var isShown = false
#State private var showing = false
var body: some View {
ZStack {
Color.white // needed ??
ZStack {
Color.green
VStack {
HStack {
Spacer()
Text("Open").onTapGesture {
self.isShown = true
}
}
Spacer()
}
}.disabled(showing) // << inactive on in-progress
// .edgesIgnoringSafeArea(.all) // ?? bad visibility
.zIndex(1) // by default it is always 0, so make above white
if self.isShown {
ZStack {
Color.red
VStack {
HStack {
Spacer()
Text("Close").onTapGesture {
self.isShown = false
}
}
Spacer()
}
}
.transition(AnyTransition.move(edge: .bottom))
.animation(.easeInOut)
// .edgesIgnoringSafeArea(.all) // ??
.zIndex(2) // top-most
.onAppear {
self.showing = true
}
.onDisappear {
self.showing = false
}
}
}
}
}

Related

Implementing Button in side menu

can someone Help me with fixing this. I want this code to work such as when I click the Home button on the side menu, it should take me to the Main View("This is the Main View"). I have tried using presenting sheets, however, presenting sheet doesn't look realistic. When the Home button is tapped, everything should disappear and only the Home Screen should come up with the side menu. I have tried writing up this code, however, I couldn't make the home button work. The codes are as below:
import SwiftUI
import Foundation
import Combine
struct Home: View {
#State var showMenu = false
#EnvironmentObject var userSettings: UserSettings
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.showMenu = false
}
}
}
return NavigationView {
GeometryReader {
geometry in
ZStack(alignment: .leading) {
MainView(showMenu: self.$showMenu)
.frame(width: geometry.size.width, height: geometry.size.height)
.offset(x: self.showMenu ? geometry.size.width/2 : 0)
.disabled(self.showMenu ? true : false)
if self.showMenu {
MenuView()
.frame(width: geometry.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
.navigationBarTitle("Pay Data", displayMode: .inline)
.navigationBarItems(leading: (Button(action: {
withAnimation {
self.showMenu.toggle()
}
}){
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}
))
}
}
}
struct MainView: View {
#Binding var showMenu: Bool
#EnvironmentObject var userSettings: UserSettings
var body: some View {
Text("This is Main View")
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
.environmentObject(UserSettings())
}
}
//This is the Menu View. The Home Button is located in this view.
import SwiftUI
import Combine
import Foundation
struct MenuView: View {
#EnvironmentObject var userSettings: UserSettings
#State var showMenu = false
#State var Homevariable = false
var body: some View {
VStack(alignment: .leading) {
Button(action: {
UserDefaults.standard.set(false, forKey: "status")
}) {
(Text(Image(systemName: "rectangle.righthalf.inset.fill.arrow.right")) + (Text("Home")))
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
.edgesIgnoringSafeArea(.all)
}
}
struct MenuView_Previews: PreviewProvider {
static var previews: some View {
MenuView()
.environmentObject(UserSettings())
}
}
//This is the another view. I want the side Menu to appear on this as well, so when I press the Home button it takes me to the Main View("This is the Main View")
import SwiftUI
struct Calculation: View {
var body: some View {
Text("Hello, World!")
}
}
struct Calculation_Previews: PreviewProvider {
static var previews: some View {
Calculation()
}
}
Here you go. You are basically rebuilding a navigation logic, so in MainView you have to switch between the screens and put the side menu over it:
(PS: you can do without GeometryReader)
struct ContentView: View {
#State private var showMenu = false
#State private var selected: SelectedScreen = .home
var body: some View {
NavigationView {
ZStack {
// show selected screen
switch selected {
case .home:
MainView()
.disabled(self.showMenu ? true : false)
case .screen1:
OtherView(screen: 1)
case .screen2:
OtherView(screen: 2)
}
// put menu over it
if self.showMenu {
MenuView(showMenu: $showMenu, selected: $selected)
.transition(.move(edge: .leading))
}
}
.navigationBarTitle("Pay Data", displayMode: .inline)
// .navigationBarItems is deprecated, use .toolbar
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
withAnimation {
self.showMenu.toggle()
}
} label: {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}
}
}
}
}
}
enum SelectedScreen {
case home
case screen1
case screen2
}
struct MenuView: View {
#Binding var showMenu: Bool
#Binding var selected: SelectedScreen
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 24) {
Button {
selected = .home
showMenu = false
} label: {
Label("Home", systemImage: "rectangle.righthalf.inset.fill.arrow.right")
}
Button {
selected = .screen1
showMenu = false
} label: {
Label("Screen 1", systemImage: "1.circle")
}
Button {
selected = .screen2
showMenu = false
} label: {
Label("Screen 2", systemImage: "2.circle")
}
}
.padding()
.frame(maxHeight: .infinity)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
Spacer()
}
}
}
struct MainView: View {
var body: some View {
Text("This is Main View")
.font(.largeTitle)
}
}
struct OtherView: View {
let screen: Int
var body: some View {
Text("Other View: Screen \(screen)")
.font(.largeTitle)
}
}

SwiftUI - Sheet Dismmis button not working

I want to add a button on the top of my view that will be the "X" to close the sheet. I am unable to use presentationmode option. For some reason it isn't working. Can someone please let me know how I can add a button that shows an "X" on the top that if I click on will close this view. Your help is very much appreciated.
import SwiftUI
struct TopicsExperienceCards: View {
// #Environment(\.presentationMode) var presentationMode
let etype: EItype
var body: some View {
NavigationView{
/* Alernatively Page Layout View */
ScrollView (.vertical, showsIndicators: false) {
Rectangle()
.fill(Color(etype.accentcolor))
.frame(width: 300, height: 5)
.padding()
GroupBox {
TabView {
ForEach(etype.content1,id: \.self) {item in
VStack (alignment:.center, spacing:0){
Text(item)
.padding()
.frame(width:300, height:300, alignment:.center)
Divider()
Spacer()
Text("Room for an image")
Spacer()
Spacer()
}
} //foreach
} //: TABVIEW
.tabViewStyle(PageTabViewStyle())
.onAppear {
setupAppearance() }
} //end of GroupBox
// .padding()
.frame(width:350, height:650)
.clipShape(RoundedRectangle(cornerRadius: 25.0, style: .circular))
.shadow(radius: 5)
} //end of ScrollView
.edgesIgnoringSafeArea(.all)
} //end of Navigation view
}
}
/* Function for the black dots in pagination */
func setupAppearance() {
UIPageControl.appearance().currentPageIndicatorTintColor = .black
UIPageControl.appearance().pageIndicatorTintColor = UIColor.black.withAlphaComponent(0.2)
}
struct TopicsExperienceCards_Previews: PreviewProvider {
static let etypes: [EItype] = Bundle.main.decode("eibasestructure.json")
static var previews: some View {
TopicsExperienceCards(etype:etypes[1])
}
}
You can just pass in the Binding that you use to present the sheet to TopicsExperienceCards as well.
struct ContentView: View {
#State var isPresented = false
var body: some View {
Button("Present") {
isPresented = true /// set Binding to true to present
}
.sheet(isPresented: $isPresented) {
TopicsExperienceCards(isPresented: $isPresented) /// pass Binding here
}
}
}
struct TopicsExperienceCards: View {
#Binding var isPresented: Bool
var body: some View {
VStack {
HStack {
Spacer()
Button(action: {
isPresented = false /// set Binding back to false
}) {
Image(systemName: "xmark")
.padding()
}
}
Spacer()
}
}
}
Result:

How to animate navigationBarHidden in SwiftUI?

struct ContentView: View {
#State var hideNavigationBar: Bool = false
var body: some View {
NavigationView {
ScrollView {
VStack {
Rectangle().fill(Color.red).frame(height: 50)
.onTapGesture(count: 1, perform: {
withAnimation {
self.hideNavigationBar.toggle()
}
})
VStack {
ForEach(1..<50) { index in
HStack {
Text("Sample Text")
Spacer()
}
}
}
}
}
.navigationBarTitle("Browse")
.navigationBarHidden(hideNavigationBar)
}
}
}
When you tap the red rectangle it snaps the navigation bar away. I thought withAnimation{} would fix this, but it doesn't. In UIKit you would do something like this navigationController?.setNavigationBarHidden(true, animated: true).
Tested in xCode 12 beta 6 and xCode 11.7
You could try using
.navigationBarHidden(hideNavigationBar).animation(.linear(duration: 0.5)) instead of .navigationBarHidden(hideNavigationBar)
and also move self.hideNavigationBar.toggle() out of the animation block. That is not required if you use the above approach for hiding of navigation bar with animation.
I think, the only solution is to use a position function in SwiftUI 2
var body: some View {
GeometryReader { geometry in
NavigationView {
ZStack {
Color("background")
.ignoresSafeArea()
// ContentView
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: logo, trailing: barButtonItems)
.toolbar {
ToolbarItem(placement: .principal) {
SearchBarButton(placeholder: LocalizedStringKey("home_vc.search_bar.placeholder"))
.opacity(isNavigationBarHidden ? 0 : 1)
.animation(.easeInOut(duration: data.duration))
}
}
}
.frame(height: geometry.size.height + (isNavigationBarHidden ? 70 : 0))
// This is the key ⬇
.position(x: geometry.size.width/2, y: geometry.size.height/2 - (isNavigationBarHidden ? 35 : 0))
.animation(.easeInOut(duration: 0.38))
.onTapGesture {
isNavigationBarHidden.toggle()
}
}
}
I'm still learning animation in SwiftUI but at this stage, I understand that you must animate the parent view.
So your code would become...
struct ContentView: View {
#State var hideNavigationBar: Bool = false
var body: some View {
NavigationView {
ScrollView {
VStack {
Rectangle().fill(Color.red).frame(height: 50)
.onTapGesture(count: 1) {
self.hideNavigationBar.toggle()
}
VStack {
ForEach(1..<50) { index in
HStack {
Text("Sample Text")
Spacer()
}
}
}
}
}
.navigationBarTitle("Browse")
.navigationBarHidden(hideNavigationBar)
.animation(.spring()) // for example
}
}
}
Note that the last argument in any function call can be placed into a single closure.
So...
.onTapGesture(count: 1, perform: {
self.hideNavigationBar.toggle()
})
can become...
.onTapGesture(count: 1) {
self.hideNavigationBar.toggle()
}
Simpler syntax in my humble opinion.

SwiftUI: Can't get the transition of a DetailView to a ZStack in the MainView to work

I can't find the answer to this anywhere, hopefully one of you can help me out.
I have a MainView with some content. And with the press of a button I want to open a DetailView. I am using a ZStack to layer the DetailView on the top, filling the screen.
But with the following code I can't get it to work. The DetailView does not have a transition when it inserts and it stops at removal. I have tried with and without setting the zIndex manually, and a custom assymetricalTransition. Couldn't get that to work. Any solutions?
//MainView
#State var showDetail: Bool = false
var body: some View {
ZStack {
VStack {
Text("Hello MainWorld")
Button(action: {
withAnimation(.spring()) {
self.showDetail.toggle()
}
}) {
Text("Show detail")
}
}
if showDetail {
ContentDetail(showDetail: $showDetail)
.transition(.move(edge: .bottom))
}
}
.edgesIgnoringSafeArea(.all)
}
And here is the DetailView:
//DetailView
#Binding var showDetail: Bool
var body: some View {
VStack (spacing: 25) {
Text("Hello, DetailWorld!")
Button(action: { withAnimation(.spring()) {
self.showDetail.toggle()
}
}) {
Text("Close")
}
.padding(.bottom, 50)
}
.frame(width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
.background(Color.yellow)
.edgesIgnoringSafeArea(.all)
}
The result of this code is this:
I'm running Xcode 11.4.1 so implicit animations doesn't seem to work either. Really stuck here, hope one of you can help me out! Thanks :)
Here is a solution. Tested with Xcode 11.4 / iOS 13.4.
struct MainView: View {
#State var showDetail: Bool = false
var body: some View {
ZStack {
Color.clear // extend ZStack to all area
VStack {
Text("Hello MainWorld")
Button(action: {
self.showDetail.toggle()
}) {
Text("Show detail")
}
}
if showDetail {
DetailView(showDetail: $showDetail)
.transition(AnyTransition.move(edge: .bottom))
}
}
.animation(Animation.spring()) // one animation to transitions
.edgesIgnoringSafeArea(.all)
}
}
struct DetailView: View {
#Binding var showDetail: Bool
var body: some View {
VStack (spacing: 25) {
Text("Hello, DetailWorld!")
Button(action: {
self.showDetail.toggle()
}) {
Text("Close")
}
.padding(.bottom, 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity) // fill in container
.background(Color.yellow)
}
}

Tabbar middle button utility function in SwiftUI

I'm trying to reproduce a "Instagram" like tabBar which has a "Utility" button in the middle which doesn't necessarily belong to the tabBar eco system.
I have attached this gif to show the behaviour I am after. To describe the issue. The tab bar in the middle (Black plus) is click a ActionSheet is presented INSTEAD of switching the view.
How I would do this in UIKit is simply use the
override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
print("Selected item")
}
Function from the UITabBarDelegate. But obviously we can't do this in SwiftUI so was looking to see if there was any ideas people have tried. My last thought would be to simply wrap it in a UIView and use it with SwiftUI but would like to avoid this and keep it native.
I have seen a write up in a custom TabBar but would like to use the TabBar provided by Apple to avoid any future discrepancies.
Thanks!
Edit: Make the question clearer.
Thanks to Aleskey for the great answer (Marked as correct). I evolved it a little bit in addition to a medium article that was written around a Modal. I found it to be a little different
Here's the jist.
A MainTabBarData which is an Observable Object
final class MainTabBarData: ObservableObject {
/// This is the index of the item that fires a custom action
let customActiontemindex: Int
let objectWillChange = PassthroughSubject<MainTabBarData, Never>()
var previousItem: Int
var itemSelected: Int {
didSet {
if itemSelected == customActiontemindex {
previousItem = oldValue
itemSelected = oldValue
isCustomItemSelected = true
}
objectWillChange.send(self)
}
}
func reset() {
itemSelected = previousItem
objectWillChange.send(self)
}
/// This is true when the user has selected the Item with the custom action
var isCustomItemSelected: Bool = false
init(initialIndex: Int = 1, customItemIndex: Int) {
self.customActiontemindex = customItemIndex
self.itemSelected = initialIndex
self.previousItem = initialIndex
}
}
And this is the TabbedView
struct TabbedView: View {
#ObservedObject private var tabData = MainTabBarData(initialIndex: 1, customItemIndex: 2)
var body: some View {
TabView(selection: $tabData.itemSelected) {
Text("First Screen")
.tabItem {
VStack {
Image(systemName: "globe")
.font(.system(size: 22))
Text("Profile")
}
}.tag(1)
Text("Second Screen")
.tabItem {
VStack {
Image(systemName: "plus.circle")
.font(.system(size: 22))
Text("Profile")
}
}.tag(2)
Text("Third Screen")
.tabItem {
VStack {
Image(systemName: "number")
.font(.system(size: 22))
Text("Profile")
}
}.tag(3)
}.actionSheet(isPresented: $tabData.isCustomItemSelected) {
ActionSheet(title: Text("SwiftUI ActionSheet"), message: Text("Action Sheet Example"),
buttons: [
.default(Text("Option 1"), action: option1),
.default(Text("Option 2"), action: option2),
.cancel(cancel)
]
)
}
}
func option1() {
tabData.reset()
// ...
}
func option2() {
tabData.reset()
// ...
}
func cancel() {
tabData.reset()
}
}
struct TabbedView_Previews: PreviewProvider {
static var previews: some View {
TabbedView()
}
}
Similar concept, just uses the power of SwiftUI and Combine.
You could introduce new #State property for storing old tag of presented tab. And perform the next method for each of your tabs .onAppear { self.oldSelectedItem = self.selectedItem } except the middle tab. The middle tab will be responsible for showing the action sheet and its method will look the following:
.onAppear {
self.shouldShowActionSheet.toggle()
self.selectedItem = self.oldSelectedItem
}
Working example:
import SwiftUI
struct ContentView: View {
#State private var selectedItem = 1
#State private var shouldShowActionSheet = false
#State private var oldSelectedItem = 1
var body: some View {
TabView (selection: $selectedItem) {
Text("Home")
.tabItem { Image(systemName: "house") }
.tag(1)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Search")
.tabItem { Image(systemName: "magnifyingglass") }
.tag(2)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Add")
.tabItem { Image(systemName: "plus.circle") }
.tag(3)
.onAppear {
self.shouldShowActionSheet.toggle()
self.selectedItem = self.oldSelectedItem
}
Text("Heart")
.tabItem { Image(systemName: "heart") }
.tag(4)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Profile")
.tabItem { Image(systemName: "person.crop.circle") }
.tag(5)
.onAppear { self.oldSelectedItem = self.selectedItem }
}
.actionSheet(isPresented: $shouldShowActionSheet) { ActionSheet(title: Text("Title"), message: Text("Message"), buttons: [.default(Text("Option 1"), action: option1), .default(Text("Option 2"), action: option2) , .cancel()]) }
}
func option1() {
// do logic 1
}
func option2() {
// do logic 2
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Previous answers did not help me so I'm pasting my complete solution.
import SwiftUI
import UIKit
enum Tab {
case map
case recorded
}
#main
struct MyApp: App {
#State private var selectedTab: Tab = .map
#Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
VStack {
switch selectedTab {
case .map:
NavigationView {
FirstView()
}
case .recorded:
NavigationView {
SecondView()
}
}
CustomTabView(selectedTab: $selectedTab)
.frame(height: 50)
}
}
}
}
struct FirstView: View {
var body: some View {
Color(.systemGray6)
.ignoresSafeArea()
.navigationTitle("First view")
}
}
struct SecondView: View {
var body: some View {
Color(.systemGray6)
.ignoresSafeArea()
.navigationTitle("second view")
}
}
struct CustomTabView: View {
#Binding var selectedTab: Tab
var body: some View {
HStack {
Spacer()
Button {
selectedTab = .map
} label: {
VStack {
Image(systemName: "map")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
Text("Map")
.font(.caption2)
}
.foregroundColor(selectedTab == .map ? .blue : .primary)
}
.frame(width: 60, height: 50)
Spacer()
Button {
} label: {
ZStack {
Circle()
.foregroundColor(.secondary)
.frame(width: 80, height: 80)
.shadow(radius: 2)
Image(systemName: "plus.circle.fill")
.resizable()
.foregroundColor(.primary)
.frame(width: 72, height: 72)
}
.offset(y: -2)
}
Spacer()
Button {
selectedTab = .recorded
} label: {
VStack {
Image(systemName: "chart.bar")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
Text("Recorded")
.font(.caption2)
}
.foregroundColor(selectedTab == .recorded ? .blue : .primary)
}
.frame(width: 60, height: 50)
Spacer()
}
}
}