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

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

Related

Why does SwiftUI View background extend into safe area?

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

Arrange custom views in SwiftUI without space or overlap

I'm trying to build a UI (SwiftUI / iOS) out of a number of custom views.
All those custom views have a defined aspect ratio or ratio for their frame.
Here's a simplified version of such a custom view:
struct TestView: View {
var body: some View {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(height: geometry.size.width / 3)
}
}
}
My ContentView currently looks like that:
struct TestContentView: View {
var body: some View {
GeometryReader {geomerty in
VStack {
TestView()
TestView()
}
}
}
}
I would like to have the two rectangles to be positioned right below each other (at the top of the screen). So without any space between them. So a bit like an old-fashioned UITableView with only to rows.
But whatever I try, I only get one of two results:
They are equally spread out over the screen (vertically)
They overlap (= the view on the top only gets a vertical size of 20
The only solution I've found so far is to define the frame size of the sub-views also in the TestContentView(). But that seems to be quite un-SwiftUI.
Thanks!
Remove the GeometryReader from your content view, since it isn't doing anything
You said that your TestView has a defined aspect ratio, but, in fact, it doesn't -- it just has a defined width. If you do define an aspect ratio, it starts working as expected:
struct TestView: View {
var body: some View {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.aspectRatio(3, contentMode: .fit)
}
}
struct TestContentView: View {
var body: some View {
VStack(spacing: 0) {
TestView()
TestView()
Spacer()
}
}
}

What is the proper Swift-UI way for sizing UI elements in extracted subviews?

Supposing I have the main view of my application and I need to create many different images, texts, buttons, textfields, ... with proper sizes to get a neat layout - device independently.
My current approach was to work with GeometryReader to get the screen-sizes (width, height) of the device and based on this I compute their sizes.
struct MyView: View {
var body: some View {
GeometryReader { g in
...
VStack {
Subview1(g, ...)
}
...
HStack {
Subview2(g, ...)
...
Subview7(g, ...)
}
...
}
}
}
One of the many subviews as example:
struct Subview5: View {
var g: GeometryProxy
...
var body: some View {
Image(...)
.frame(width: g.size.width * 0.5, height: g.height * 0.3)
}
}
So with passing the geometry of the main view to the extracted subviews the code gets more and more difficult to read.
Is there another more elegant or a typical Swift-UI way doing this?
Thank you for taking your time in answering my question!

why geometry reader doesn't center its child?

red border is geometry Area and black border is text area
currently using Xcode12 Beta 3
struct Testing_Geometry_: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.border(Color.black)
}
.border(Color.red)
}
}
I wanted to position text in center with this code
struct Testing_Geometry_: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.position(x:geo.frame(in:.global).midX,y:geo.frame(in:.global).midY)
.border(Color.black)
}
.border(Color.red)
}
}
but I got this result which means Text is taking the whole geometry size and I think it's not correct!
cause texts has to fit in their space
three roles suggested by #twostraws for layout systems are
1- parent offers its size
2-child chooses its size
3-parent positions its child
but I think this isn't right!
text is taking the whole geometry space
If someone is looking for basic solution, you can put it in one of Stack and make it use whole size of geometry size with alignment center. That will make all elements underneath to use correct size and aligned in center
GeometryReader { geometry in
ZStack {
// ... some of your views
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
}
The problem is that modifier order matters, because modifiers actually create parent views. I've used backgrounds instead of borders because I think they're easier to see. Consider this code that's the same as yours but just using a background:
struct TestingGeometryView: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.position(x:geo.frame(in:.global).midX,y:geo.frame(in:.global).midY)
.background(Color.gray)
}
.background(Color.red)
}
}
This gives the following:
From this you are thinking "Text is taking the whole geometry size and I think it's not correct!" because the gray background is taking the whole screen instead of just around the Text. Again, the problem is modifier order- the background (or border in your example) is a parent view, but you are making it the parent of the "position" view instead of the Text view. In order for position to do what it does, it takes the entire parent space available (in this case the whole screen minus safe area). So putting background or border as parent of position means they will take the entire screen.
Let's switch the order to this, so that background view is only for the Text view and we can see size of Text view:
struct TestingGeometryView: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.background(Color.gray)
.position(x:geo.frame(in:.global).midX,y:geo.frame(in:.global).midY)
}
.background(Color.red)
}
}
This gives the result I think you were expecting with the Text view only taking up the minimum size required, and following those rules that #twostraws explained so nicely.
This is why modifier order is so important. It's clear that GeometryReader view is taking up the entire screen, and Text view is only taking up the space it requires. In your example, Text view was still only taking up the required space but your border was around the position view, not the Text view. Hope it's clear :-)

How to use GeometryReader within a LazyVGrid

I'm building a grid with cards which have an image view at the top and some text at the bottom. Here is the swift UI code for the component:
struct Main: View {
var body: some View {
ScrollView {
LazyVGrid(columns: .init(repeating: .init(.flexible()), count: 2)) {
ForEach(0..<6) { _ in
ZStack {
Rectangle()
.foregroundColor(Color(UIColor.random))
VStack {
Rectangle()
.frame(minHeight: 72)
Text(ipsum)
.fixedSize(horizontal: false, vertical: true)
.padding()
}
}.clipShape(RoundedRectangle(cornerRadius: 10))
}
}.padding()
}.frame(width: 400, height: 600)
}
}
This component outputs the following layout:
This Looks great, but I want to add a Geometry reader into the Card component in order to scale the top image view according to the width of the enclosing grid column. As far as I know, that code should look like the following:
struct Main: View {
var body: some View {
ScrollView {
LazyVGrid(columns: .init(repeating: .init(.flexible()), count: 2)) {
ForEach(0..<6) { _ in
ZStack {
Rectangle()
.foregroundColor(Color(UIColor.random))
VStack {
GeometryReader { geometry in
Rectangle()
.frame(minHeight: 72)
Text(ipsum)
.fixedSize(horizontal: false, vertical: true)
.padding()
}
}
}.clipShape(RoundedRectangle(cornerRadius: 10))
}
}.padding()
}.frame(width: 400, height: 600)
}
}
The trouble is that this renders as the following:
As you can see, I'm not even trying to use the GeometryReader, I've just added it. If I add the geometry reader at the top level, It will render the grid correctly, however this is not of great use to me because I plan to abstract the components into other View conforming structs. Additionally, GeometryReader seems to be contextually useful, and it wouldn't make sense to do a bunch of math to cut the width value in half and then make my calculations from there considering the geometry would be from the top level (full width).
Am I using geometry reader incorrectly? My understanding is that it can be used anywhere in the component tree, not just at the top level.
Thanks for taking a look!
I had the same problem as you, but I've worked it out. Here's some key point.
If you set GeometryReader inside LazyVGrid and Foreach, according to SwiftUI layout rule, GeometryReader will get the suggested size (may be just 10 point). More importantly, No matter what subview inside GeometryReader, it wouldn't affect the size of GeometryReader and GeometryReader's parent view.
For this reason, your view appears as a long strip of black. You can control height by setting GeometryReader { subView }.frame(some size),
Generally, we need two GeometryReader to implement this. The first one can get size and do some Computing operations, then pass to second one.
(Since my original code contains Chinese, it may be hard for you to read, so I can only give a simple structure for you.)
GeometryReader { firstGeo in
LazyVGrid(columns: rows) {
ForEach(dataList) { data in
GeometryReader { secondGeo in
// subview
}
.frame(width: widthYouWantSubViewGet)
}
}
}
I just started to learn swift for a week. There may be some mistakes in my understanding. You are welcome to help correct it.