Why does SwiftUI View background extend into safe area? - swift

Here's a view that navigates to a 2nd view:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
SecondView()
} label: {
Text("Go to 2nd view")
}
}
}
}
struct SecondView: View {
var body: some View {
ZStack {
Color.red
VStack {
Text("This is a test")
.background(.green)
//Spacer() // <--If you add this, it pushes the Text to the top, but its background no longer respects the safe area--why is this?
}
}
}
}
On the 2nd screen, the green background of the Text view only extends to its border, which makes sense because the Text is a pull-in view:
Now, if you add Spacer() after the Text(), the Text view pushes to the top of the VStack, which makes sense, but why does its green background suddenly push into the safe area?
In the Apple documentation here, it says:
By default, SwiftUI sizes and positions views to avoid system defined
safe areas to ensure that system content or the edges of the device
won’t obstruct your views. If your design calls for the background to
extend to the screen edges, use the ignoresSafeArea(_:edges:) modifier
to override the default.
However, in this case, I'm not using ignoresSafeArea, so why is it acting as if I did, rather than perform the default like Apple says, which is to avoid the safe areas?

You think that you use old background modifier version.
But actually, the above code uses a new one, introduced in iOS 15 version of background modifier which by default ignores all safe area edges:
func background<S>(_ style: S, ignoresSafeAreaEdges edges: Edge.Set = .all) -> some View where S : ShapeStyle
To fix the issue just replace .background(.green) with .background(Color.green) if your app deployment target < iOS 15.0, otherwise use a new modifier: .background { Color.green }.

Related

SwiftUI ScrollView does not respond to keyboard

Thanks for taking your time to help others :)
Problem description:
I want to bring up the ScrollView content as keyboard shows up. And I can't.
Despite, if ScrollView is turned upside down... it works!!! But I can't do that because I have to implement .contextMenu(...) and this produces an even worse bug (detailed in this post).
App must support iOS 14.
Simple code demo to show what happens.
import SwiftUI
struct ContentView: View {
#State var text: String = ""
var body: some View {
VStack {
ScrollView() {
LazyVStack {
ForEach(1..<201, id: \.self) { num in
Text("Message \(num)")
}
// .upsideDown() // With these modifiers, will prompt up the scrollView content
}
}
// .upsideDown() // With these modifiers, will prompt up the scrollView content
TextField("Your text here", text: $text)
.textFieldStyle(.roundedBorder)
.padding()
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Example app")
}
}
extension View {
func upsideDown() -> some View {
self.rotationEffect(.degrees(180))
}
}
GIF resources to see behaviour:
Good result (when upside down):
Bad result (on normal scrollView):
What we have checked?
Already tried this solution, but does not help.
Tried setting an offset to ScrollView of keyboards height when it does come up but... nothing happens.
Questions
Why does this happen when it is upside down? (and not at normal scrollview)
Why on both cases it brings up the TextField but does NOT bring up the content of Scrollview if is not upside down???

ZStack explicit animation not working when conditionally hiding/showing a view

I want to create some sort of banner view that either moves in or out with animations when some state changes in SwiftUI. It feels like a basic task to do but the transition when showing the banner is not being animated. Hiding works fine.
So for demonstration purposes, here is a basic view modifier that creates a ZStack with the content and a conditional view that acts as the banner overlay and has a transition:
struct BannerViewModifier: ViewModifier {
let showBanner: Bool
func body(content: Content) -> some View {
ZStack {
content
if showBanner {
VStack {
Text("banner")
Spacer()
}.transition(.move(edge: .top).combined(with: .opacity))
}
}
}
}
If I would then go ahead and tack that view modifier onto a button that toggles a boolean like this (with explicit animations)...
#State var showBanner = false
var body: some View {
Button(showBanner ? "Hide banner" : "Show banner") {
withAnimation { showBanner.toggle() }
}.modifier(BannerViewModifier(showBanner: showBanner))
}
... it results in not animating the show transition as I already mentioned.
Solutions I've tried already:
When adding an implicit animation to the VStack with the transition modifier like this:
.animation(.default).transition(.move(edge: .top).combined(with: .opacity)) the show animation works.
However this feels like bad practice since the user of the view modifier cannot control the animation properties since they are baked into the view modifier already. Furthermore the animation modifier .animation(.default) was deprecated in iOS 15 and its replacement .animation(.default, value: showBanner) also does not animate the show transition either.

ProgressView vs View inside ZStack behave differently

I have 2 views inside a ZStack. One is ProgressView() and another one is Circle().
In this ZStack, i have a green background color with onTapGesture event.
I notice that whenever i click on the ProgressView() the event of background color do not trigger but when i click on the Circle() it trigger the event of background color.
So why is that if both of these are on top of background color? i think it should not trigger the event for both views.
ZStack() {
Color.green.ignoresSafeArea(.all)
.onTapGesture {
print("checked")
}
VStack() {
ProgressView()
Circle().frame(width: 30, height: 30, alignment: .leading)
}
}
They are actually not. Let's consider view hierarchy below (from view debug mode)
Shape is a native SwiftUI view and it is just rendered into the same backend as background and, actually by default not hit-testable.
ProgressView in contrary has UIKit backend, because it just representable of UIView, and all UIViews are added above native SwiftUI view (note, even if they are put into background). And by default this opaque UIView does not pass touch events through to SwiftUI.
That's it.
*changed to yellow color for better visibility
As #Asperi wrote your VStack is in front of the Color View.
But one can use the modifier allowsHitTesting.
With this gesture are not processed by this view.
#State var text: String = "Hello World"
var body: some View {
ZStack() {
Color.green.ignoresSafeArea(.all)
.onTapGesture {
text = "Green tapped"
}
VStack() {
ProgressView()
Circle().frame(width: 30, height: 30, alignment: .leading)
Text(text)
}
.allowsHitTesting(false)
}
}
This seems like a normal behavior.
However, if you want to get rid of this problem, you can always wrap your stack with a simple blank onTapGesture{}.
VStack {
}.onTapGesture{}
onTapGesture{} will get your stack rid of this problem, plus it will not affect your sub view and buttons inside the stack. Your Button inside the stack will behave normally.

How to disable vertical scroll in TabView with SwiftUI?

I have set up a TabView in my application, so that I can swipe horizontally between multiple pages, but I also have an unwanted vertical scroll that may appear, with a bounce effect so. How can I disable this vertical scroll?
My code:
struct ContentView: View {
#State private var currentTabIndex: Double = 0
var body: some View {
VStack {
TabView(selection: $currentTabIndex) {
Text("Text n°1")
.tag(0)
Text("Text n°2")
.tag(1)
}
.border(Color.black)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
}
I had this same problem. It's not an exact solution, but you can turn off bouncing on scrollviews (which is used within a TabView). And as long as the items within the TabView are not larger than the TabView frame, it should act as if you disabled vertical scrolling.
I would call it either .onAppear or in your init function:
.onAppear(perform: {
UIScrollView.appearance().bounces = false
})
Note: this disables the bouncing on ALL scrollviews across your app... So you may want to re-enable it .onDisappear.
Still an issue with Xcode 12.4.
I managed to workaround that by wrapping the TabView within a ScrollView and using the alwaysBounceVertical property set to false, as follow:
ScrollView(.horizontal) {
TabView {
///your content goes here
}
.tabViewStyle(PageTabViewStyle())
}
.onAppear(perform: {
UIScrollView.appearance().alwaysBounceVertical = false
})
.onDisappear(perform: {
UIScrollView.appearance().alwaysBounceVertical = true
})
I actually came across this because I saw this effect in a tutorial but couldn’t replicate it on iOS 15.2. However, I managed to replicate it on iOS 14.4 on another simulator side by side. So I guess this behaviour is disabled or fundamentally changed in the newer iOS.
Demonstration

How can I get a SwiftUI View to completely fill its superview?

The following is supposed to create a Text whose bounds occupy the entire screen, but it seems to do nothing.
struct ContentView: View {
var body: some View {
Text("foo")
.relativeSize(width: 1.0, height: 1.0)
.background(Color.red)
}
}
The following hack:
extension View {
/// Causes the view to fill into its superview.
public func _fill(alignment: Alignment = .center) -> some View {
GeometryReader { geometry in
return self.frame(
width: geometry.size.width,
height: geometry.size.height,
alignment: alignment
)
}
}
}
struct ContentView2: View {
var body: some View {
Text("foo")
._fill()
.background(Color.red)
}
}
seems to work however.
Is this a SwiftUI bug with relativeSize, or am I missing something?
You need to watch WWDC 2019 Session 237: Building Custom Views with SwiftUI, because Dave Abrahams discusses this topic, and uses Text in his examples.
To restate briefly what Dave explains in detail:
The parent (in this case, a root view created by the system and filling the screen) proposes a size to its child.
The child chooses its own size, consuming as much or as little of the proposed size as it wants.
The parent positions the child in the parent’s coordinate space based on various parameters including the size chosen by the child.
Thus you cannot force a small Text to fill the screen, because in step 2, the Text will refuse to consume more space than needed to fit its content.
Color.red is different: in step 2, it just returns the proposed size as its own size. We can call views like this “expandable”: they expand to fill whatever space they're offered.
ZStack is also different: in step 2, it asks its children for their sizes and picks its own size based on its children's sizes. We can call views like this “wrapping”: they wrap their children tightly.
So if you promote Color.red to be the “main” view returned by body, and put the Text in an overlay, your ContentView will behave like Color.red and be expandable:
struct ContentView: View {
var body: some View {
Color.red
.overlay(Text("foo"))
}
}
If you use a ZStack containing both Color.red and Text, the ZStack will wrap the Color.red, and thus take on its expandability:
struct ContentView: View {
var body: some View {
ZStack {
Color.red
Text("hello")
}
}
}