SwiftUI view over all the views including sheet view - ios

I need to show a view above all views based upon certain conditions, no matter what the top view is. I am trying the following code:
struct TestView<Presenting>: View where Presenting: View {
/// The binding that decides the appropriate drawing in the body.
#Binding var isShowing: Bool
/// The view that will be "presenting" this notification
let presenting: () -> Presenting
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .top) {
self.presenting()
HStack(alignment: .center) {
Text("Test")
}
.frame(width: geometry.size.width - 44,
height: 58)
.background(Color.gray.opacity(0.7))
.foregroundColor(Color.white)
.cornerRadius(20)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
extension View {
func showTopView(isShowing: Binding<Bool>) -> some View {
TestView(isShowing: isShowing,
presenting: { self })
}
}
struct ContentView: View {
#State var showTopView = false
NavigationView {
ZStack {
content
}
}
.showTopView(isShowing: $showTopView)
}
Now this is working fine in case of the views being pushed. But I am not able to show this TopView above the presented view.
Any help is appreciated!

Here is a way for your goal, you do not need Binding, just use let value.
struct ContentView: View {
#State private var isShowing: Bool = Bool()
var body: some View {
CustomView(isShowing: isShowing, content: { yourContent }, isShowingContent: { isShowingContent })
}
var yourContent: some View {
NavigationView {
VStack(spacing: 20.0) {
Text("Hello, World!")
Button("Show isShowing Content") { isShowing = true }
}
.navigationTitle("My App")
}
}
var isShowingContent: some View {
ZStack {
Color.black.opacity(0.5).ignoresSafeArea()
VStack {
Spacer()
Button("Close isShowing Content") { isShowing = false }
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue.cornerRadius(10.0))
.padding()
}
}
}
}
struct CustomView<Content: View, IsShowingContent: View>: View {
let isShowing: Bool
#ViewBuilder let content: () -> Content
#ViewBuilder let isShowingContent: () -> IsShowingContent
var body: some View {
Group {
if isShowing { ZStack { content().blur(radius: isShowing ? 5.0 : 0.0); isShowingContent() } }
else { content() }
}
.animation(.default, value: isShowing)
}
}

Related

Updating tapGesture area(frame) after device is rotated SwiftUI

I have an issue with updating the area(frame) of .onTapGesture after a device is rotated. Basically, even after changing #State var orientation the area where .onTapGesture works remain the same as on the previous orientation.
Would appreciate having any advice on how to reset that tap gesture to the new area after rotation.
Thanks in advance!
struct ContentView: View {
var viewModel = SettingsSideMenuViewModel()
var body: some View {
VStack {
SideMenu(viewModel: viewModel)
Button("Present menu") {
viewModel.isShown.toggle()
}
Spacer()
}
.padding()
}
}
final class SettingsSideMenuViewModel: ObservableObject {
#Published var isShown = false
func dismissHostingController() {
guard !isShown else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
debugPrint("viewShoudBeDismissedHere")
}
}
}
struct SideMenu: View {
#ObservedObject var viewModel: SettingsSideMenuViewModel
#State private var orientation = UIDeviceOrientation.unknown
var sideBarWidth = UIScreen.main.bounds.size.width * 0.7
var body: some View {
GeometryReader { proxy in
ZStack {
GeometryReader { _ in
EmptyView()
}
.background(Color.black.opacity(0.6))
.opacity(viewModel.isShown ? 1 : 0)
.animation(.easeInOut.delay(0.2), value: viewModel.isShown)
.onTapGesture {
viewModel.isShown.toggle()
viewModel.dismissHostingController()
}
content
}
.edgesIgnoringSafeArea(.all)
.frame(width: proxy.size.width,
height: proxy.size.height)
.onRotate { newOrientation in
orientation = newOrientation
}
}
}
var content: some View {
HStack(alignment: .top) {
ZStack(alignment: .top) {
Color.white
Text("SOME VIEW HERE")
VStack(alignment: .leading, spacing: 20) {
Text("SOME VIEW HERE")
Divider()
Text("SOME VIEW HERE")
Divider()
Text("SOME VIEW HERE")
}
.padding(.top, 80)
.padding(.horizontal, 40)
}
.frame(width: sideBarWidth)
.offset(x: viewModel.isShown ? 0 : -sideBarWidth)
.animation(.default, value: viewModel.isShown)
Spacer()
}
}
}
struct DeviceRotationViewModifier: ViewModifier {
let action: (UIDeviceOrientation) -> Void
func body(content: Content) -> some View {
content
.onAppear()
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
action(UIDevice.current.orientation)
}
}
}
extension View {
func onRotate(perform action: #escaping (UIDeviceOrientation) -> Void) -> some View {
self.modifier(DeviceRotationViewModifier(action: action))
}
}
struct SideMenu_Previews: PreviewProvider {
static var viewModel = SettingsSideMenuViewModel()
static var previews: some View {
SideMenu(viewModel: viewModel)
}
}
In this example is just slideoutMenu with a blurred area. By opening that menu in portrait and taping on the blurred area this menu should close. The issue is when the menu is opened in portrait and then rotated to landscape - the tapGesture area stays the same as it was in portrait, hence if tapped in the landscape - nothing happens. This works in the same direction too. Thus the question is how to reset the tapGesture area on rotation?
This view is presented in UIHostingController. slideOutView?.modalPresentationStyle = .custom the issue is there. But if slideOutView?.modalPresentationStyle = .fullScreen (or whatever) - everything works okay.

Conditional component in SwiftUI

I'm giving my first steps with SwiftUI and I'm having problems with a component shown depending on a condition.
I'm trying to show a fullscreen popup (full screen with semi transparent black background and the popup in the middle with white background). To achieve this I've made this component:
struct CustomUiPopup: View {
var body: some View {
ZStack {
}
.overlay(CustomUiPopupOverlay, alignment: .top)
.zIndex(1)
}
private var CustomUiPopupOverlay: some View {
ZStack {
Spacer()
ZStack {
Text("POPUP")
.padding()
}
.zIndex(1)
.frame(width: UIScreen.main.bounds.size.width - 66)
.background(Color.white)
.cornerRadius(8)
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
.background(Color.black.opacity(0.6))
}
}
If I set this in my main view, the popup is shown correctly over the button:
struct MainView: View {
var body: some View {
CustomUiPopup()
Button("Click to show popup") {
print("click on button")
}
}
}
If I set this, my popup is not shown (correct because hasToShowPopup is false), but if I click on the button it fails, the popup is not shown and the button can not be clicked again (?!), it seems like the view was freezed.
struct MainView: View {
#State private var hasToShowPopup = false
var body: some View {
if hasToShowPopup {
CustomUiPopup()
}
Button("Click to show popup") {
hasToShowPopup = true
}
}
}
I've even tried to initializate hasToShowPopup to true but the popup keeps failing, it's not shown in the first place:
struct MainView: View {
#State private var hasToShowPopup = true
var body: some View {
if hasToShowPopup {
CustomUiPopup()
}
Button("Click to show popup") {
hasToShowPopup = true
}
}
}
So my conclusion is that, I don't know why, but if I put my CustomUiPopup inside an "if" something is not rendered correctly.
What is wrong with my code?
Anyway, if this is not the correct approach to show a popup, I'll be glad to have any advice.
Following Ptit Xav suggestion I've tried this with the same results (my CustomUiPopup doesn't show):
struct MainView: View {
#State private var hasToShowPopup = false
var body: some View {
VStack {
if hasToShowPopup {
CustomUiPopup()
}
Button("Click to show popup") {
hasToShowPopup = true
}
}
}
}
This works fine with me:
struct CustomUiPopup: View {
var body: some View {
ZStack {
Spacer()
Text("POPUP")
.padding()
.zIndex(1)
.frame(width: UIScreen.main.bounds.size.width - 66)
.background(Color.white)
.cornerRadius(8)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
Color.black.opacity(0.6)
.ignoresSafeArea()
)
}
}
struct ContentView: View {
#State private var hasToShowPopup = false
var body: some View {
ZStack {
Button("Click to show popup") {
hasToShowPopup = true
}
if hasToShowPopup {
CustomUiPopup()
}
}
}
}

How can I hide TabBar Swift UI?

I have a TabBarView in the root view. In one of the parent views that's nested within the root view, I'd like the tab bar to hide when navigating from that parent view to the child view. Is there any func or command to handle that?
Something like this:
ContentView (with TabBarView) - > ExploreView (Called in TabBarView ) -> MessagesView (Child of ExploreVIew - Hide Tab bar)
My code can be seen below.
TabView{
NavigationView{
GeometryReader { geometry in
ExploreView()
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading: Button(action: {
}, label: {
HStack{
Image("cityOption")
Text("BER")
Image("arrowCities")
}.foregroundColor(Color("blackAndWhite"))
.font(.system(size: 12, weight: .semibold))
}),trailing:
HStack{
Image("closttop")
.renderingMode(.template)
.padding(.trailing, 125)
NavigationLink(destination: MessagesView()
.navigationBarTitle(Text("Messages"), displayMode: .inline)
.navigationBarItems(trailing: Image("writemessage"))
.foregroundColor(Color("blackAndWhite"))
){
Image("messages")
}
}.foregroundColor(Color("blackAndWhite"))
)
}
}
.tabItem{
HStack{
Image("clostNav").renderingMode(.template)
Text("Explore")
.font(.system(size: 16, weight: .semibold))
}.foregroundColor(Color("blackAndWhite"))
}
SearchView().tabItem{
Image("search").renderingMode(.template)
Text("Search")
}
PostView().tabItem{
HStack{
Image("post").renderingMode(.template)
.resizable().frame(width: 35, height: 35)
}
}
OrdersView().tabItem{
Image("orders").renderingMode(.template)
Text("Orders")
}
ProfileView().tabItem{
Image("profile").renderingMode(.template)
Text("Profile")
}
}
Appreciate any help! Thanks!
Create CustumPresentViewController.swift -
import UIKit
import SwiftUI
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder { return
ViewControllerHolder(value:
UIApplication.shared.windows.first?.rootViewController ) }
}
extension EnvironmentValues {
var viewController: ViewControllerHolder {
get { return self[ViewControllerKey.self] }
set { self[ViewControllerKey.self] = newValue }
}
}
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle =
.automatic, #ViewBuilder builder: () -> Content) {
// Must instantiate HostingController with some sort of view...
let toPresent = UIHostingController(rootView:
AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
// ... but then we can reset rootView to include the environment
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, ViewControllerHolder(value:
toPresent))
)
self.present(toPresent, animated: true, completion: nil)
}
}
Use this in required View -
#Environment(\.viewController) private var viewControllerHolder:
ViewControllerHolder
private var viewController: UIViewController? {
self.viewControllerHolder.value
}
var body: some View {
NavigationView {
ZStack {
Text("Navigate")
}.onTapGesture {
self.viewController?.present(style: .fullScreen) {
EditUserView()
}
}
}
}
A bit late here, on iOS 16 you could use ContentView().toolbar(
.hidden, for: .tabBar)
So in your case, it would be like as below,
struct ExploreView: View {
var body: some View {
some_view {
}
.toolbar(.hidden, for: .tabBar)
}
}
in a normal iphone environment the tabbar vanishes from itself if you are navigating....or are u running in another environment?
check this:
struct ContentView: View {
#State var hideTabbar = false
#State var navigate = false
var body: some View {
NavigationView {
TabView {
VStack {
NavigationLink(destination: Text("without tab")){
Text("aha")
}
.tabItem {
Text("b")
}
}.tag(0)
Text("Second View")
.tabItem {
GeometryReader { geometry in
Image(systemName: "2.circle")
// Text("Second")
Text("\(geometry.size.height) - \(geometry.size.width)")
}
}.tag(1)
}
}
}//.isHidden(self.hideTabbar)
}

SwiftUI - NavigationBar in View from NavigationLink quickly showing then disappearing

I have a ContentView containing a NavigationView that leads to a DestinationView. I want to hide the navigation bar in the ContentView, but show it in the DestinationView. To hide it in the ContentView I set navigationBarHidden to true and give navigationBarTitle an empty string.
In the DestinationView I set navigationBarHidden to false and give it the title "DestinationView".
If I run the project and tap on the NavigationLink, the DestinationView shows the NavigationBar but quickly hides it after the view appeared. Can anybody help me with this?
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.red.frame(maxWidth: .infinity, maxHeight: .infinity)
NavigationLink(destination: DestinationView()) {
ZStack {
Color.green.frame(width: 200, height: 200)
Text("Tap me")
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
struct DestinationView: View {
var body: some View {
List {
Text("1")
Text("2")
}
.navigationBarTitle("DestinationView")
.navigationBarHidden(false)
}
}
You need to use variable to achieve this and bind it with your destination
struct ContentView: View {
#State var isNavigationBarHidden: Bool = true
var body: some View {
NavigationView {
ZStack {
Color.red.frame(maxWidth: .infinity, maxHeight: .infinity)
NavigationLink(destination: DestinationView(isNavigationBarHidden: self.$isNavigationBarHidden)) {
ZStack {
Color.green.frame(width: 200, height: 200)
Text("Tap me")
}
}
}
.navigationBarHidden(self.isNavigationBarHidden)
.navigationBarTitle("")
.onAppear {
self.isNavigationBarHidden = true
}
}
}
}
struct DestinationView: View {
#Binding var isNavigationBarHidden: Bool
var body: some View {
List {
Text("1")
Text("2")
}
.navigationBarTitle("DestinationView")
.onAppear {
self.isNavigationBarHidden = false
}
}
}
There is an issue with the safe area layout guide
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.red.frame(maxWidth: .infinity, maxHeight: .infinity)
VStack {
NavigationLink(destination: DestinationView()) {
ZStack {
Color.green.frame(width: 200, height: 200)
Text("Tap me")
}
}
}
}.edgesIgnoringSafeArea(.all)
.navigationBarHidden(true)
}
}
}
struct DestinationView: View {
var body: some View {
VStack {
List {
Text("1")
Text("2")
}
}.navigationBarTitle("DestinationView")
.navigationBarHidden(false)
}
}
Happy Coding...
Edit: use the accepted answer as it's a much cleaner solution.
I encountered this bug and ended up using UIViewControllerRepresentable to wrap a controller which sets the navigation bar hidden state in its viewDidAppear method:
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.red.frame(maxWidth: .infinity, maxHeight: .infinity)
NavigationLink(destination: DestinationView()) {
ZStack {
Color.green.frame(width: 200, height: 200)
Text("Tap me")
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
struct DestinationView: View {
var body: some View {
List {
Text("1")
Text("2")
}
.navigationBarTitle("DestinationView")
.navigationBarHidden(false)
.background(HorribleHack())
}
}
struct HorribleHack: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> HorribleHackViewController {
HorribleHackViewController()
}
func updateUIViewController(_ uiViewController: HorribleHackViewController, context: Context) {
}
}
class HorribleHackViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
DispatchQueue.main.async {
self.navigationController?.setNavigationBarHidden(false, animated: false)
}
}
}
For me passing a binding around through the view hierarchy wasn't optimal, adding the state to an environment var was preferable.
class SceneState: ObservableObject {
#Published var isNavigationBarHidden = true
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
...
var sceneState = SceneState()
...
let contentView = ContentView().environmentObject(sceneState)
...
}
struct ContentView: View {
var body: some View {
NavigationView {
View1()
}
}
}
struct View1: View {
#EnvironmentObject var sceneState: SceneState
#State private var showView2: Bool = false
var body: some View {
VStack {
Text("NO nav bar.")
Button("Go to View2") {
self.showView2 = true
}
NavigationLink(destination: View2(), isActive: $showView2, label: {EmptyView()})
}
.navigationBarHidden(self.sceneState.isNavigationBarHidden)
.navigationBarTitle("")
.navigationBarBackButtonHidden(self.sceneState.isNavigationBarHidden)
}
}
struct View2: View {
#EnvironmentObject var sceneState: SceneState
var body: some View {
VStack {
Text("WITH nav bar.")
}
.navigationBarHidden(self.sceneState.isNavigationBarHidden)
.navigationBarTitle("WWDC")
.navigationBarBackButtonHidden(self.sceneState.isNavigationBarHidden)
.onAppear {
self.sceneState.isNavigationBarHidden = false
}
}
}
There ist actually a really simple solution to this problem. After many tries I figured it, that you have to add the .navigationBarHidden(false) directly to the destination view inside the NavigationLink like this:
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.red.frame(maxWidth: .infinity, maxHeight: .infinity)
NavigationLink(destination: DestinationView()
.navigationBarHidden(false)) {
ZStack {
Color.green.frame(width: 200, height: 200)
Text("Tap me")
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
struct DestinationView: View {
var body: some View {
List {
Text("1")
Text("2")
}
.navigationBarTitle("DestinationView")
.navigationBarHidden(false)
}
}
It will work like desired, and won't disappear after showing up.
I run this code on an iOS 14 Simulator and the navigation bar did not hide, so I assume this might be an issue with iOS 13. I had a similar problem and my code which resulted in nav bar disappearing on iOS 13.5 Simulator worked fine on iOS 14.4 Simulator.

Present a new view in SwiftUI

I want to click a button and then present a new view like present modally in UIKit
I have already seen "How to present a new view using sheets", but I don't want to attach it to the main view as a modal sheet.
And I don't want to use NavigationLink, because I don't want a new view and old view have a navigation relationship.
Thanks for your help...
To show a modal (iOS 13 style)
You just need a simple sheet with the ability to dismiss itself:
struct ModalView: View {
#Binding var presentedAsModal: Bool
var body: some View {
Button("dismiss") { self.presentedAsModal = false }
}
}
And present it like:
struct ContentView: View {
#State var presentingModal = false
var body: some View {
Button("Present") { self.presentingModal = true }
.sheet(isPresented: $presentingModal) { ModalView(presentedAsModal: self.$presentingModal) }
}
}
Note that I passed the presentingModal to the modal so you can dismiss it from the modal itself, but you can get rid of it.
To make it REALLY present fullscreen (Not just visually)
You need to access to the ViewController. So you need some helper containers and environment stuff:
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController)
}
}
extension EnvironmentValues {
var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}
Then you should use implement this extension:
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, #ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, toPresent)
)
NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: "dismissModal"), object: nil, queue: nil) { [weak toPresent] _ in
toPresent?.dismiss(animated: true, completion: nil)
}
self.present(toPresent, animated: true, completion: nil)
}
}
Finally
you can make it fullscreen like:
struct ContentView: View {
#Environment(\.viewController) private var viewControllerHolder: UIViewController?
var body: some View {
Button("Login") {
self.viewControllerHolder?.present(style: .fullScreen) {
Text("Main") // Or any other view you like
// uncomment and add the below button for dismissing the modal
// Button("Cancel") {
// NotificationCenter.default.post(name: Notification.Name(rawValue: "dismissModal"), object: nil)
// }
}
}
}
}
For iOS 14 and Xcode 12:
struct ContentView: View {
#State private var isPresented = false
var body: some View {
Button("Show Modal with full screen") {
self.isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}
}
struct FullScreenModalView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("This is a modal view")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
See also: How to present a full screen modal view using fullScreenCover()
Disclaimer: Below is not really like a "native modal", neither behave nor look&feel, but if anyone would need a custom transition of one view over other, making active only top one, the following approach might be helpful.
So, if you expect something like the following
Here is a simple code for demo the approach (of corse animation & transition parameters can be changed by wish)
struct ModalView : View {
#Binding var activeModal: Bool
var body : some View {
VStack {
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
self.activeModal = false
}
}) {
Text("Hide modal")
}
Text("Modal View")
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.green)
}
}
struct MainView : View {
#Binding var activeModal: Bool
var body : some View {
VStack {
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
self.activeModal = true
}
}) {
Text("Show modal")
}
Text("Main View")
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.yellow)
}
}
struct ModalContainer: View {
#State var showingModal = false
var body: some View {
ZStack {
MainView(activeModal: $showingModal)
.allowsHitTesting(!showingModal)
.disabled(showingModal)
if showingModal {
ModalView(activeModal: $showingModal)
.transition(.move(edge: .bottom))
.zIndex(1)
}
}
}
}
Here is a simple one way - forward views. It's very straight forward.
struct ChildView: View{
private let colors: [Color] = [.red, .yellow,.green,.white]
#Binding var index : Int
var body: some View {
let next = (self.index+1) % MyContainer.totalChildren
return ZStack{
colors[self.index % colors.count]
Button("myNextView \(next) ", action: {
withAnimation{
self.index = next
}
}
)}.transition(.asymmetric(insertion: .move(edge: .trailing) , removal: .move(edge: .leading) ))
}
}
struct MyContainer: View {
static var totalChildren = 10
#State private var value: Int = 0
var body: some View {
HStack{
ForEach(0..<(Self.totalChildren) ) { index in
Group{
if index == self.value {
ChildView(index: self.$value)
}}
}
}
}
}
then presents it from ContentView when a button is tapped:
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
Button("Press to dismiss") {
dismiss()
}
.font(.title)
.padding()
.background(Color.black)
}
}
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
Button("Show Sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
}
}

Resources