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

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

Related

.ignoresSafeArea(.keyboard) does not work on SwiftUI view

I have the following view which is presented as a sheet:
var body: some View {
NavigationView {
GeometryReader { reader in
VStack {
TransactionAmount(amountString: $amountString)
.padding(.top, Constants.amountPadding)
Spacer()
SelectedCategory(selectedCategory: $selectedCategory)
.padding()
.onTapGesture {
isCategoryPickerPresented = true
UIImpactFeedbackGenerator.feedback(style: .medium)
}
AddTransactionToolbar(noteText: $noteText)
.padding(.horizontal)
NumberPad(numberString: $amountString, selectedCategory: $selectedCategory)
}
.sheet(isPresented: $isCategoryPickerPresented) {
CategoryPicker(selection: $selectedCategory)
.presentationDetents([.medium])
}
.onAppear {
selectedCategory = storageManager.categories.first
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.ignoresSafeArea(.keyboard)
.navigationTitle("Add Transaction")
.navigationBarTitleDisplayMode(.inline)
}
}
}
For some reason ignoresSafeArea(.keyboard) is not working and my view is being pushed up when the keyboard is presented. Wrapping the view in GeometryReader hasn't made any difference either. Not really sure where to go from here. How do I fix this?

Combining 2 ScrollViews in Swift

I'm trying to connect 2 views. One is a ScrollView and the other is a List. Does anyone know how to do that? Putting both into a singular ScrollView doesn't work, and calling APIResponseView().environmentObject(Model()) inside of ContentView() also doesn't work.
struct MyApp: App {
var body: some Scene {
WindowGroup {
VStack{
ContentView() // this is a ScrollView
APIResponseView().environmentObject(Model()) // this is a List
}
}
}
}
As you can see, the views aren't connected:
Image found here
If you want to have the ScrollView move out of view when scrolling the list, you have to put the ScrollView INSIDE the list:
struct ContentView: View {
#State private var test = false
var body: some View {
// List
List {
// ScrollView
ScrollView(.horizontal) {
HStack {
ForEach(0..<6) { i in
Text("ScrollView \(i)")
.frame(width: 150, height: 200)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.blue)
)
}
}
}
ForEach(0..<10) { item in
HStack {
VStack {
Text("Overheadline")
.font(.caption)
.foregroundColor(.secondary)
Text("List Item \(item)")
}
Spacer()
RoundedRectangle(cornerRadius: 10)
.fill(.gray)
.frame(width: 50, height: 50)
}
}
}
.listStyle(.plain)
}
}

How to apply a different type of transition to a view in SwiftUI, depending on a conditional

In my current project I have transitions applied to my authentication views. However, on sign in, I want a different transition to appear. (Different than if the user simply clicked the "back" button)
Here is some code for a basic mock up I made:
Auth View:
struct AuthView: View {
#EnvironmentObject var testModel: TestModel //Not used yet, used later
#State private var showSignIn: Bool = false
#State private var showSignUp: Bool = false
var body: some View {
if (!showSignIn && !showSignUp) {
WelcomeView(showSignIn: $showSignIn, showSignUp: $showSignUp)
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: .leading), removal: AnyTransition.move(edge: .leading)))
} else if showSignIn {
SignInView(showSignIn: $showSignIn)
.transition(AnyTransition.move(edge: .trailing))
} else {
SignUpView(showSignUp: $showSignUp)
.transition(AnyTransition.move(edge: .trailing))
}
}
}
Welcome View code:
struct WelcomeView: View {
#Binding var showSignIn: Bool
#Binding var showSignUp: Bool
var body: some View {
VStack {
Text("Welcome!")
Button(action: { withAnimation { showSignIn.toggle() } }) {
Text("Sign In")
.underline()
.foregroundColor(.white)
}
Button(action: { withAnimation { showSignUp.toggle() } }) {
Text("Sign Up")
.underline()
.foregroundColor(.white)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue.ignoresSafeArea())
}
}
Sign in view:
struct SignInView: View {
#EnvironmentObject var testModel: TestModel
#Binding var showSignIn: Bool
var body: some View {
VStack {
Text("Sign In")
Button(action: { withAnimation { showSignIn.toggle() } }) {
Text("Go back")
.underline()
.foregroundColor(.white)
}
Button(action: { withAnimation { testModel.signedIn.toggle() } }) {
Text("Complete Sign In")
.underline()
.foregroundColor(.white)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green.ignoresSafeArea())
}
}
ContentView:
struct ContentView: View {
#StateObject var testModel = TestModel()
var body: some View {
if testModel.signedIn {
Text("Home page")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
.transition(AnyTransition.opacity)
.onTapGesture {
testModel.signedIn.toggle()
}
} else {
AuthView()
.environmentObject(testModel)
}
}
}
What it makes:
What I want:
I'm trying to apply a different transition effect when I "finish" the sign in process. In my mock up, I simulated this through clicking "Complete sign in"
I tried changing my SignInView code (in my AuthView) to this, applying a ternary operator to the transitions:
SignInView(showSignIn: $showSignIn)
.transition(AnyTransition.move(edge: (testModel.signedIn ? .bottom : .trailing)))
But it had no effect.
Thanks for any help in advance!
I solved my own question!
I made a new #State variable, with a type of AnyTransition, using that variable in replacement of my ternary operator. I then used logic to change the type of transition (the #State variable) when I run my log-in function.

iOS 14 SwiftUI Keyboard lifts view automatically

I am using TextField in my view and when it becomes the first responder, it lefts the view as shown in the below GIF.
Is there any way I can get rid of this behavior?
Here is my code
NavigationView(content: {
ZStack{
MyTabView(selectedIndex: self.$index)
.view(item: self.item1) {
NewView(title: "Hello1").navigationBarTitle("")
.navigationBarHidden(true)
}
.view(item: self.item2) {
NewView(title: "Hello2").navigationBarTitle("")
.navigationBarHidden(true)
}
.view(item: self.item3) {
NewView(title: "Hello3").navigationBarTitle("")
.navigationBarHidden(true)
}
}.navigationBarHidden(true)
.navigationBarTitle("")
}).ignoresSafeArea(.keyboard, edges: .bottom)
// New View
struct NewView:View {
#State var text:String = ""
var title:String
var body: some View {
VStack {
Spacer()
Text("Hello")
TextField(title, text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
.onAppear {
debugPrint("OnApper \(self.title)")
}
}
}
For .ignoresSafeArea to work you need to fill all the available area (eg. by using a Spacer).
The following will not work (no Spacers, just a TextField):
struct ContentView: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("asd", text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
However, it will work when you add Spacers (fill all the available space):
struct ContentView: View {
#State var text: String = ""
var body: some View {
VStack {
Spacer()
TextField("asd", text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
If you don't want to use Spacers you can also use a GeometryReader:
struct ContentView: View {
#State var text: String = ""
var body: some View {
GeometryReader { _ in
...
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
You should apply the modifier on the ZStack, NOT the NavigationView
NavigationView(content: {
ZStack{
,,,
}.navigationBarHidden(true)
.navigationBarTitle("")
.ignoresSafeArea(.keyboard, edges: .bottom) // <- This line moved up
})
Full working example:
struct ContentView: View {
#State var text = ""
var body: some View {
VStack{
Spacer()
Text("Hello, World")
TextField("Tap to test keyboard ignoring", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding()
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
What eventually worked for me, combining answers posted here and considering also this question, is the following (Xcode 12.4, iOS 14.4):
GeometryReader { _ in
VStack {
Spacer()
TextField("Type something...", text: $value)
Spacer()
}.ignoresSafeArea(.keyboard, edges: .bottom)
}
Both spacers are there to center vertically the textfield.
Using only the GeometryReader or the ignoresSafeArea modifier didn't do the trick, but after putting them all together as shown above stopped eventually the view from moving up upon keyboard appearance.
That's what I figured out:
GeometryReader { _ in
ZStack {
//PUT CONTENT HERE
}.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
}
It seems to work for me. In this case you do not need to check iOS 14 availability.

Potential SwiftUI ZStack Transition bug when pressed rapidly

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
}
}
}
}
}