SwiftUI Edges not ignored on simulator - ios

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.

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.

How to stop SwiftUI change the fixed padding when we switch button content between Text and Image?

So I have two buttons on the bottom of my screen, button A and button B, somewhere along the line I need to replace the image in the button with text, so we do so by changing the Bool in the top.
Although we apply the same modifiers, the padding of button B changes, and the UI moves around, it seems as if the text claims more space. Desired situation: button A and B, should not move around when changing the button Image to Text.
import SwiftUI
private var showImage: Bool = true
struct SwiftUIView: View {
var body: some View {
VStack {
Spacer()
Button(action: {
print("CLICK")
}) {
Image(systemName: "a.circle")
.modifier(TestButtonModifier())
}
.padding(10)
Button(action: {
print("CLICK")
}) {
if showImage {
Image(systemName: "b.circle")
.modifier(TestButtonModifier())
} else {
Text("B")
.modifier(TestButtonModifier())
}
}
.padding(10)
} //: VSTACK
}
}
struct TestButtonModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.system(size: 52, weight: .regular))
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 100)
.background(Color.black)
.padding(2)
.foregroundColor(Color.white)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
It is due to VStack spacing, which by default differs between different pairs of subviews, so specify some explicit (or remove at all, ie set to zero)
struct SwiftUIView: View {
var body: some View {
VStack(spacing: 0) { // << here !!
// .. other code

SwiftUI: Custom button does not recognize touch with clear background and buttonStyle

I stumbled upon a weird behaviour for Buttons in SwiftUI in combination with a custom ButtonStyle.
My target was to create a custom ButtonStyle with some kind of 'push-back animation'. I used the following setup for this:
struct CustomButton<Content: View>: View {
private let content: () -> Content
init(content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack {
Button(action: { ... }) {
content()
}
.buttonStyle(PushBackButtonStyle(pushBackScale: 0.9))
}
}
}
private struct PushBackButtonStyle: ButtonStyle {
let pushBackScale: CGFloat
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
}
}
// Preview
struct Playground_Previews: PreviewProvider {
static var previews: some View {
CustomButton {
VStack(spacing: 10) {
HStack {
Text("Button Text").background(Color.orange)
}
Divider()
HStack {
Text("Detail Text").background(Color.orange)
}
}
}
.background(Color.red)
}
}
When I now try to touch on this button outside of the Text view, nothing will happen. No animation will be visible and the action block will not be called.
What I found out so far:
when you remove the .buttonStyle(...) it does work as expected (no custom animation of course)
or when you set a .background(Color.red)) on the VStack in the CustomButton it does also work as expected in combination with the .buttonStyle(...)
The question now is if anybody have a better idea of how to properly work around this issue or how to fix it?
Just add hit testing content shape in your custom button style, like below
Tested with Xcode 11.4 / iOS 13.4
private struct PushBackButtonStyle: ButtonStyle {
let pushBackScale: CGFloat
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.contentShape(Rectangle()) // << fix !!
.scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
}
}
Simply use a .frame and it should work.
To make it easily testable I have rewritten it like this:
struct CustomButton: View {
var body: some View {
Button(action: { }) {
VStack(spacing: 10) {
HStack {
Text("Button Text").background(Color.orange)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.orange)
}
Divider()
HStack {
Text("Detail Text").background(Color.orange)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.orange)
}
}
}
.buttonStyle(PushBackButtonStyle(pushBackScale: 0.9))
}
}
private struct PushBackButtonStyle: ButtonStyle {
let pushBackScale: CGFloat
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
}
}
I hope I could help. :-)
#Edit With video.

Change text based on toggle values in SwiftUI

I have created a View with Text, Image and Navigation Button in SwiftUI.When navigation button is pressed it will navigate to another view which contains Toggle.When I change the Toggle Value , I also want to change the Text value in the previous View.
Value is getting updated when changing the toggle but didn't reflected when accessed in previous View.
//BluetoothView.swift
struct BluetoothView: View {
#ObjectBinding var bluetooth = Settings()
var body: some View {
return NavigationButton(destination: ToggleBluetoothView()) {
HStack() {
Image("default")
.resizable()
.cornerRadius(12)
.frame(width: 25, height: 25)
.clipped()
.aspectRatio(contentMode: .fit)
Text("Bluetooth")
.color(.blue)
.font(.system(size: 18))
Text(bluetooth.isBluetoothOn ? "On" : "Off")
.color(.gray)
.font(.subheadline)
.frame(width: 50, height: 40, alignment: .trailing)
}
}
}
}
//ToggleBluetoothView.swift
struct ToggleBluetoothView: View {
#ObjectBinding var bluetooth = Settings()
var body: some View {
Form {
Section(header: Text("ENABLE TO CONNECT WITH NEARBY DEVICES")) {
Toggle(isOn: $bluetooth.isBluetoothOn) {
Text("Bluetooth")
}
}
}
}
}
//Settings.swift
class Settings: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var isBluetoothOn = false { didSet { update() } }
func update() {
didChange.send(())
}
}
You are instantiating Settings separately in each view. Both views need to be seeing the same Settings object:
Change the following:
NavigationButton(destination: ToggleBluetoothView(bluetooth: bluetooth)) { ... }
and remove the initial value in ToggleBluetoothView:
struct ToggleBluetoothView: View {
#ObjectBinding var bluetooth: Settings
var body: some View {
Form {
Section(header: Text("ENABLE TO CONNECT WITH NEARBY DEVICES")) {
Toggle(isOn: $bluetooth.isBluetoothOn) {
Text("Bluetooth")
}
}
}
}
}

Tap Action not working when Color is clear SwiftUI

my tapAction is not recognizing a tap when my foregroundColor is clear. When i remove the color it works fine.
That's my code:
ZStack {
RoundedRectangle(cornerRadius: 0)
.foregroundColor(Color.clear)
.frame(width: showMenu ? UIScreen.main.bounds.width : 0)
.tapAction {
self.showMenu.toggle()
}
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.green)
.shadow(radius: 5, y: 2)
.padding(.trailing, 50)
.frame(width: showMenu ? UIScreen.main.bounds.width : 0)
}
.edgesIgnoringSafeArea(.top)
The accurate way is to use .contentShape(Rectangle()) on the view.
Described in this tutorial:
control-the-tappable-area-of-a-view by Paul Hudson #twostraws
VStack {
Image("Some Image").resizable().frame(width: 50, height: 50)
Spacer().frame(height: 50)
Text("Some Text")
}
.contentShape(Rectangle())
.onTapGesture {
print("Do Something")
}
how-to-control-the-tappable-area-of-a-view-using-contentshape stackoverflow
I have also discovered that a shape filled with Color.clear does not generate a tappable area.
Here are two workarounds:
Use Color.black.opacity(0.0001) (even on 10-bits-per-channel displays). This generates a color that is so transparent that it should have no effect on your appearance, and generates a tappable area that fills its frame. I don't know if SwiftUI is smart enough to skip rendering the color, so I don't know if it has any performance impact.
Use a GeometryReader to get the frame size, and then use the contentShape to generate the tappable area:
GeometryReader { proxy in
Color.clear.contentShape(Path(CGRect(origin: .zero, size: proxy.size)))
}
Here is the component
struct InvisibleButton: View {
let action: (() -> Void)?
var body: some View {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
action?()
}
}
}
usage: Put your view and InbisibleButton in ZStack
ZStack {
**yourView()**
InvisibleButton {
print("Invisible button tapped")
}
}
you also can make a modifier to simplify usage:
struct InvisibleButtonModifier: ViewModifier {
let action: (() -> Void)?
func body(content: Content) -> some View {
ZStack {
content
InvisibleButton(action: action)
}
}
}
**yourView()**
.modifier(InvisibleButtonModifier {
print("Invisible button tapped")
})
However, if your SwiftUI View has a UIKit view as a subview under, you will have to set Color.gray.opacity(0.0001) in order to UIView's touches be ignored
In my case a View that didn't trigger onTapGesture:
struct MainView: View {
var action: () -> Void
var body: some View {
NotTappableView()
.contentShape(Rectangle())
.onTapGesture(
action()
)
}
}
I solved this way:
struct MainView: View {
var action: () -> Void
var body: some View {
NotTappableView()
.overlay(
Color.clear
.contentShape(Rectangle())
.onTapGesture {
action()
}
)
}
}
This made whole untappable view now tappable.

Resources