Updating tapGesture area(frame) after device is rotated SwiftUI - ios

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.

Related

SwiftUI bottom safe area does not shrink after keyboard was shown

If after activating TextField I press Open link button, NavigationLink will be opened. After that if I return back to previous screen, VStack with TextField will stay in the middle of the screen, because bottom SafeArea will be expanded by keyboard. This happening if first view in ZStack is ScrollView. It should go back to bottom after keyboard is disabled. How can I fix that?
struct ContentView: View {
#State private var text = ""
var body: some View {
NavigationStack {
ZStack(alignment: .bottom) {
ScrollView {
Color.green.opacity(0.2)
.frame(height: 1000)
}
.ignoresSafeArea(.keyboard)
VStack {
TextField("", text: $text, prompt: Text("Input"))
.textFieldStyle(.roundedBorder)
.padding()
NavigationLink("Open link") {
Text("Details view")
}
}
.background { Color.red }
.background(ignoresSafeAreaEdges: .bottom)
}
}
}
}
You can try using the #FocusState property wrapper. Add 3 following command lines:
//1
#FocusState private var nameIsFocused: Bool
//2
.focused($nameIsFocused)
//3
.simultaneousGesture(TapGesture().onEnded({ _ in
nameIsFocused = false
}))
The code you wrote looks like this:
struct ContentView: View {
#State private var text = ""
//1
#FocusState private var nameIsFocused: Bool
var body: some View {
NavigationStack {
ZStack(alignment: .bottom) {
ScrollView {
Color.green.opacity(0.2)
.frame(height: 1000)
}
.ignoresSafeArea(.keyboard)
VStack {
TextField("", text: $text, prompt: Text("Input"))
//2
.focused($nameIsFocused)
.textFieldStyle(.roundedBorder)
.padding()
NavigationLink("Open link") {
Text("Details view")
}
//3
.simultaneousGesture(TapGesture().onEnded({ _ in
nameIsFocused = false
}))
}
.background { Color.red }
.background(ignoresSafeAreaEdges: .bottom)
}
}
}
}
Result:
Hope it is useful for you!

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

SwiftUI Edges not ignored on simulator

I'm a SwiftUI trainee. On this particular view below there is an issue like in the image.
While .ignoreSafeArea(.bottom) or .edgesIgnoreSafeArea(.bottom) works on preview.
It does not work on the simulator. I would like to learn is it a bug or am I missing something. Thanks for your help ahead!
Issue screen shot
Updated Solution
The problem was caused by root view logic. When you use navigationLink to navigate another screen on root view causes this problem. I didnt want to use standard NavigationLink to navigate because it was freezing animations(Lottie) I'm playing on screen when you go to some screen via navigationLink and come back.
Below is the view code. Hopefully not that messy.
import SwiftUI
struct ChatView: View {
// MARK: properties
let userName : String
let userImageUrl : String
#ObservedObject var viewModel : ChatViewModel = ChatViewModel()
#ObservedObject var appState : NavigationController = NavigationController.shared
// MARK: body
var body: some View {
ZStack(alignment: .bottom) {
VStack {
buildNavigationBar()
buildMessages()
} // end of Vstack
.ignoresSafeArea(edges:.bottom)
buildInputRow()
} // end of Zstack
.ignoresSafeArea(edges:.bottom)
}
fileprivate func buildInputRow() -> some View {
return
HStack(alignment: .center){
DynamicHorizontalSpacer(size: 30)
Button {
} label: {
Image(systemName: "photo.circle.fill")
.font(.system(size: 35))
}
DynamicHorizontalSpacer(size: 25)
UnobscuredTextFieldView(textBinding: .constant("Hello"), promptText: "Type!", width: 180, color: .white)
DynamicHorizontalSpacer(size: 25)
Button {} label: {
Image(systemName: "paperplane.fill")
.font(.system(size: 30))
.foregroundColor(.accentColor)
}
Spacer()
} // end of HStack
.frame(width: .infinity, height: 100, alignment: .center)
.background(Color.gray.opacity(0.4).ignoresSafeArea(edges:.bottom))
}
fileprivate func buildMessages() -> some View {
return ScrollView(showsIndicators: false) {
ForEach(0...50, id : \.self) { index in
ChatTileView(index: index)
}
.padding(.horizontal,5)
} // end of scrollview
.ignoresSafeArea(edges:.bottom)
}
fileprivate func buildNavigationBar() -> ChatViewNavigationBar {
return ChatViewNavigationBar(userImageUrl: self.userImageUrl, userName: self.userName) {
appState.appState = .Home
}
}
}
fileprivate func buildMessageBox() -> some View {
return HStack(alignment: .center) {
Text(
"""
Fake message
"""
)
.font(.system(size:11))
.foregroundColor(.white)
}
}
}
fileprivate extension View {
func messageBoxModifier(index : Int) -> some View {
self
.multilineTextAlignment(index.isMultiple(of: 2) ?.trailing : .leading)
.frame(minHeight: 30)
.padding(.vertical,7)
.padding(.horizontal,10)
.background(index.isMultiple(of: 2) ? Color.green : Color.mint)
.cornerRadius(12)
.shadow(color: .black.opacity(0.3), radius: 5, y: 5)
}
}
some components used in eg. DynamicHorizantalSpacer
DynamicHorizontalSpacer && Vertical as well they share same logic
struct DynamicVerticalSpacer: View {
let size : CGFloat?
var body: some View {
Spacer()
.frame(width: 0, height: size ?? 20, alignment: .center)
}
}
TextField that I'm using.
struct UnobscuredTextFieldView: View {
#Binding var textBinding : String
let promptText: String
let width : CGFloat
let color : Color
var body: some View {
TextField(text: $textBinding, prompt: Text(promptText)) {
Text("Email")
}
.textFieldModifier()
.modifier(RoundedTextFieldModifier(color:color ,width: width))
}
}
fileprivate extension TextField {
func textFieldModifier() -> some View {
self
.textCase(.lowercase)
.textSelection(.disabled)
.disableAutocorrection(true)
.textInputAutocapitalization(.never)
.textContentType(.emailAddress)
}
}
The problem was caused by root view logic. When you use navigationLink to navigate another screen on root view causes this problem. I didnt want to use standard NavigationLink to navigate because it was freezing animations I'm playing on screen when you go to some screen via navigationLink and come back.

SwiftUI view over all the views including sheet view

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

Swift UI and ObservableObject Object Data Cleared

I'm a bit beginner in SWIFT and right now I'm facing a problem whit UI. Let me try to explain my problem.
my homeview screen data coming from web service using Observable object and it loads the data first time. But when I tried to open my left side slide menus than homeView webservice/obervable object data is just cleared when open the left slide menu view. Why my observable object data is empty. Let me share my code:
1.------ This is a my main/parentView
struct ContentView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.viewRouter.showSlideOutMenu = false
self.viewRouter.showDepartmentsMenu = false
self.viewRouter.showAccountMenu = false
}
}
}
return GeometryReader { g in
ZStack(alignment: .leading) {
RouteChanger(viewRouter: self._viewRouter)
if self.viewRouter.showSlideOutMenu {
MainMenuView(viewRouter: self._viewRouter)
.frame(width: g.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
}
}
2.----- This is my RouteChanger view for navigate to different pages of my views.
struct RouteChanger: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
GeometryReader { g in
VStack {
if self.viewRouter.currentPage == "Home" {
HomeView()
//.modifier(PageSwitchModifier())
}
}
}
}
}
3.... This is my homeView where I am using Observeable Object
struct HomeView: View {
#ObservedObject var homeController = HomeController()
var body: some View {
GeometryReader { g in
ZStack {
Color(UIColor.midTown.blue)
.edgesIgnoringSafeArea(.top)
VStack { //whole body
if self.homeController.homePageData.CODE == "0" {
ImageViewWidget(imageUrl: (self.homeController.homePageData.DATA?.headerList[0].img_url)!)
.frame(minWidth: g.size.width, maxWidth: g.size.width, minHeight: (g.size.width * UIImage(named: "header")!.size.height) / UIImage(named: "header")!.size.width, maxHeight: (g.size.width * UIImage(named: "header")!.size.height) / UIImage(named: "header")!.size.width)
}
else {
Text("Loading...")
.foregroundColor(Color.blue)
.padding()
.frame(width: g.size.width)
}
}
}
}
}
}
The EnvironmentObject is injected for all subviews automatically, so related part of your ContentView should look like below
ZStack(alignment: .leading) {
RouteChanger() // << here
if self.viewRouter.showSlideOutMenu {
MainMenuView() // << here
.frame(width: g.size.width/2)
.transition(.move(edge: .leading))
}

Resources