In Swiftui How to automatically close modal with rotation to landscape - ios

I currently use an landscape environmentobject based on this code - https://stackoverflow.com/a/58503841/412154
Within my view I have modals that appear and disappear appropriately using #State/#Binding depending on a "Done" Button press. My app does show a different view when rotated to landscape and I would like for the modal to dismiss automatically on the rotation, but couldn't figure out how to change the #binding var based on another #ennvironmentobject
Here is a simplified sample of my Modal View
struct StepsView: View {
#Binding var isPresented:Bool
#EnvironmentObject var orientation:Orientation
var body: some View {
VStack(alignment: .center) {
Text("Step")
}
.navigationBarItems(trailing: Button(action: {
//print("Dismissing steps view...")
self.isPresented = false
}) {
Text("Done").bold()
})
}
thanks for any help!

Appreciate #davidev's answer but I wanted each Modal to act a little differently so I did it this way
struct StepsView: View {
#Binding var isPresented:Bool
#EnvironmentObject var orientation:Orientation
private var PortraitView:some View {
VStack(alignment: .center) {
Text("Modal")
}
.navigationBarItems(trailing: Button(action: {
self.isPresented = false
}) {
Text("Done").bold()
})
}
var body: some View {
buildView(isLandscape: orientation.isLandScape, isShowing: &isPresented)
}
func buildView(isLandscape:Bool, isShowing:inout Bool) -> AnyView {
if !isLandscape {
return AnyView(PortraitView)
} else {
isShowing = false
return AnyView(EmptyView())
}
}

Here is a possible approach with extending the Device class with a variable which keeps track of the opened modal View.
import Combine
final class Orientation: ObservableObject {
#Published var isLandscape: Bool = false {
willSet {
objectWillChange.send()
if (newValue)
{
self.showModal = false
}
}
}
#Published var showModal : Bool = false
}
Whenever landscape changes, and the orientation is landscape, showModal will be set to false.
Here the ContentView..
struct ContentView6: View {
#EnvironmentObject var orientation:Orientation
// 1.
#State private var showModal = false
var body: some View {
Button("Show Modal") {
// 2.
self.orientation.isLandscape.toggle()
// 3.
}.sheet(isPresented: self.$orientation.isLandscape) {
ModalView(isPresented: self.$orientation.isLandscape).environmentObject(self.orientation)
}
}
}

Related

SwiftUI - How to focus a TextField within a sheet as it is appearing?

I have a search TextField within a View that is triggered to appear within a sheet on top of the ContentView.
I'm able to automatically focus this TextField when the sheet appears using #FocusState and onAppear, however, I'm finding that the sheet needs to fully appear before the TextField is focused and the on screen keyboard appears.
This feels quite slow and I've noticed in many other apps that they are able to trigger the on screen keyboard and the sheet appearing simultaneously.
Here is my code:
struct ContentView: View {
#State var showSearch = false
var body: some View {
Button {
showSearch = true
} label: {
Text("Search")
}
.sheet(isPresented: $showSearch) {
SearchView()
}
}
}
struct SearchView: View {
#State var searchTerm = ""
#FocusState private var searchFocus: Bool
var body: some View {
TextField("Search", text: $searchTerm)
.focused($searchFocus)
.onAppear() {
searchFocus = true
}
}
}
Is there a different way to do this that will make the keyboard appear as the sheet is appearing, making the overall experience feel more seamless?
Here is an approach with a custom sheet that brings in the keyboard somewhat earlier. Not sure if its worth the effort though:
struct ContentView: View {
#State var showSearch = false
var body: some View {
ZStack {
Button {
withAnimation {
showSearch = true
}
} label: {
Text("Search")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
if showSearch {
SearchView(isPresented: $showSearch)
.transition(.move(edge: .bottom))
}
}
// .sheet(isPresented: $showSearch) {
// SearchView()
// }
}
}
struct SearchView: View {
#Binding var isPresented: Bool
#State var searchTerm = ""
#FocusState private var searchFocus: Bool
var body: some View {
Form {
TextField("Search", text: $searchTerm)
.focused($searchFocus)
Button("Close") {
searchFocus = false
withAnimation {
isPresented = false
}
}
}
.onAppear() {
searchFocus = true
}
}
}

Can't go back to InitialView with Modal transition

I'm a beginner of SwiftUI. (Sorry in advance if my English is hard to understand.)
I want to enable multiple screen transitions using Modal.
【Go back to the initial view when the view reached to the last View and pressed the button】
That's what I want to realize.
I thought my code would work perfectly but when I pressed the button of last view it stopped at the second view, not the initial view.
Can't figure out what's wrong and where to fix, any solutions?
Here's my code.
`
import SwiftUI
struct ContentView: View {
#State var isShowSecondView = false
var body: some View {
Button("To SecondView") {
isShowSecondView = true
}
.font(.largeTitle)
.fullScreenCover(isPresented: $isShowSecondView) {
SecondView(isShowSecondView: $isShowSecondView)
}
}
}
struct SecondView: View {
#Binding var isShowSecondView: Bool
#State var isShowThirdView = false
var body: some View {
Button("To ThirdView") {
isShowThirdView = true
}
.font(.largeTitle)
.fullScreenCover(isPresented: $isShowThirdView) {
ThirdView(isShowNextView: $isShowSecondView,
isShowThirdView: $isShowThirdView)
}
}
}
struct ThirdView: View {
#Binding var isShowNextView: Bool
#Binding var isShowThirdView: Bool
#State var isShowForthView = false
var body: some View {
Button("To ForthView") {
isShowForthView = true
}
.font(.largeTitle)
.fullScreenCover(isPresented: $isShowForthView) {
ForthView(isShowNextView: $isShowNextView,
isShowThirdView: $isShowThirdView, isShowForthView: $isShowForthView)
}
}
}
struct ForthView: View {
#Binding var isShowNextView: Bool
#Binding var isShowThirdView: Bool
#Binding var isShowForthView: Bool
var body: some View {
Button("Back to FirstView") {
isShowNextView = false
isShowThirdView = false
isShowForthView = false
}
.font(.largeTitle)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
`

#Environment(\.presentationMode) reloads body many times in SwiftUI

Set property wrapper to pop view "#Environment(.presentationMode) var presentationMode" but it is reloading view again and again. Why it is happening? any solution?
What i have observed except #Environment(.presentationMode) we should use bool value binding with controller, Please check code:
struct newView: View {
#State private var isActiveView: Bool = false
var body: some View {
VStack(alignment: .center) {
NavigationLink(destination: PushedView(isActiveView: $isActiveView), isActive: $isActiveView) {
Text("Create Account")
}
}
}
}
And on pushed screen check the code:
`struct PushedView: View {
#Binding var isActiveView: Bool
var body: some View {
NavigationView {
VStack {
Text(“Back”).onTapGesture {
isActiveView = false // to pop into previous view
}
}
}
}
}
`

Pass an #State variable to ContentView

Building my first SwiftUI app and I'm stuck on pass #State var to the ContentView. I have declared the #State variable in a struct, with an #Binding tag on the variable in the ContentView.
My intent is for multiple instances of NumberBlock to be called in ContentView, and be able to reset all of them to false (hide all of the images) with one button.
The new "App" struct that was added in Xcode 12 is giving an error for a missing parameter. I've tried everything I can think of to enter a parameter, but nothing seems to work. I was able to eliminate the error by using .constant(true), but that did not give me the functionality I need, which is to toggle the variable from the ContentView.
I appreciate any help eliminating the error or correcting my shallow understanding of #State and #Binding.
Here is where I create the #State reset_x var
import SwiftUI
struct NumberBlock: View {
#State var reset_x: Bool = true
#Binding var reset: Bool
var body: some View {
ZStack {
Text("test")
.onTapGesture(count: 1, perform: {
self.reset_x = false
})
Image("XMark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50, alignment: .center)
.onTapGesture(count: 1, perform: {
self.reset_x = true
print("reset_x is \(self.reset_x)")
})
.isHidden(reset_x ? true : false)
.isHidden(reset ? true : false)
}
}
}
The error occurs in this view:
import SwiftUI
#main
struct Quixx2App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Here is where I want to use the #Binding
import SwiftUI
struct ContentView: View {
#State var reset: Bool = false
#Binding var reset_x: Bool
var body: some View {
VStack {
HStack {
NumberBlock(reset: self.$reset)
NumberBlock(reset: self.$reset)
}
Button("Reset Score"){
self.scoreKeeper.redScore = 0
self.reset_x = false //this line is not doing anything
print("reset_x is \(self.reset_x)")
}
}
}
}
And the .isHidden extension
import Foundation
import SwiftUI
extension View {
#ViewBuilder func isHidden(_ hidden: Bool, remove: Bool = false) -> some View {
if hidden {
if !remove {
self.hidden()
}
} else {
self
}
}
}
So let's start with your NumberBlock view. You need one state within the View, which preserves if the image is hidden or not. By tapping the Text View, the the value is toggled and the view is rendered accordingly.
struct NumberBlock: View {
#State private var imageIsHidden: Bool
var body: some View {
VStack {
Text("Show")
.onTapGesture(count: 1, perform: {
self.imageIsHidden = false
})
Text("Image")
.onTapGesture(count: 1, perform: {
self.imageIsHidden = true
})
.isHidden(imageIsHidden ? true : false)
}
}
}
Now, you want a second feature: The ContentView should control this state. So you have to extract this state from the child view (NumberBlock) to the parent view (ContentView). By changing the property from #State to #Binding you are basically telling the child view that this data is being passed from a parent view.
My working example:
import SwiftUI
import Foundation
extension View {
#ViewBuilder func isHidden(_ hidden: Bool, remove: Bool = false) -> some View {
if hidden {
if !remove {
self.hidden()
}
} else {
self
}
}
}
struct NumberBlock: View {
#Binding var imageIsHidden: Bool
var body: some View {
VStack {
Text("Show")
.onTapGesture(count: 1, perform: {
self.imageIsHidden = false
})
Text("Image")
.onTapGesture(count: 1, perform: {
self.imageIsHidden = true
})
.isHidden(imageIsHidden ? true : false)
}
}
}
struct ContentView: View {
#State var imageOfBlock1IsHidden: Bool = true
#State var imageOfBlock2IsHidden: Bool = true
var body: some View {
VStack {
HStack {
NumberBlock(imageIsHidden: self.$imageOfBlock1IsHidden)
NumberBlock(imageIsHidden: self.$imageOfBlock2IsHidden)
}
Button("Reset Score") {
self.imageOfBlock1IsHidden = true
self.imageOfBlock2IsHidden = true
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI: detecting the NavigationView back button press

In SwiftUI I couldn't find a way to detect when the user taps on the default back button of the navigation view when I am inside DetailView1 in this code:
struct RootView: View {
#State private var showDetails: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
}
}
}
struct DetailView1: View {
#State private var showDetails: Bool = false
var body: some View {
NavigationLink(destination: DetailView2(), isActive: $showDetails) {
Text("show DetailView2")
}
.navigationBarTitle("DetailView1")
}
}
struct DetailView2: View {
var body: some View {
Text("")
.navigationBarTitle("DetailView2")
}
}
Using .onDisappear doesn't solve the problem as its closure is called when the view is popped off or a new view is pushed.
The quick solution is to create a custom back button because right now the framework have not this possibility.
struct DetailView : View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body : some View {
Text("Detail View")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
self.mode.wrappedValue.dismiss()
}){
Image(systemName: "arrow.left")
})
}
}
As soon as you press the back button, the view sets isPresented to false, so you can use an observer on that value to trigger code when the back button is pressed. Assume this view is presented inside a navigation controller:
struct MyView: View {
#Environment(\.isPresented) var isPresented
var body: some View {
Rectangle().onChange(of: isPresented) { newValue in
if !newValue {
print("detail view is dismissed")
}
}
}
}
An even nicer (SwiftUI-ier?) way of observing the published showDetails property:
struct RootView: View {
class ViewModel: ObservableObject {
#Published var showDetails = false
}
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $viewModel.showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
.onReceive(self.viewModel.$showDetails) { isShowing in
debugPrint(isShowing)
// Maybe do something here?
}
}
}
}
Following up on my comment, I would react to changes in the state of showDetails. Unfortunately didSet doesn't appear to trigger with #State variables. Instead, we can use an observable view model to hold the state, which does allow us to do intercept changes with didSet.
struct RootView: View {
class ViewModel: ObservableObject {
#Published var showDetails = false {
didSet {
debugPrint(showDetails)
// Maybe do something here?
}
}
}
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $viewModel.showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
}
}
}

Resources