Hug subviews in SwiftUI - swift

Dave Abrahams explained some of the mechanics of SwiftUI layouts in his WWDC19 talk about Custom Views, but he left out some bits and I have trouble getting my views properly sized.
Is there a way for a View to tell its container that it is not making any space demands, but it will use all the space it is given? Another way to say it is that the container should hug its subviews.
Concrete example, I want something like c:
If you have some Texts inside a VStack like in a), the VStack will adopt it's width to the widest subview.
If you add a Rectangle though as in b), it will expand as much as it can, until the VStack fills its container.
This indicates that Texts and Rectangles are in different categories when it comes to layout, Text has a fixed size and a Rectangle is greedy. But how can I communicate this to my container if I'm making my own View?
The result I actually want to achieve is c). VStack should ignore Rectangle (or my custom view) when it determines its size, and then once it has done that, then it should tell Rectangle, or my custom view, how much space it can have.
Given that SwiftUI seems to layout bottom-up, maybe this is impossible, but it seems that there should be some way to achieve this.

There is no modifier (AFAIK) to accomplish this, so here's my approach. If this is something you are going to use too often, it could be worth creating your own modifier.
Also note that here I am using standard preferences, but anchor preferences are even better. It is a heavy topic to explain here. I've written an article that you can check here: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
You can use the code below to accomplish what you are looking for.
import SwiftUI
struct MyRectPreference: PreferenceKey {
typealias Value = [CGRect]
static var defaultValue: [CGRect] = []
static func reduce(value: inout [CGRect], nextValue: () -> [CGRect]) {
value.append(contentsOf: nextValue())
}
}
struct ContentView : View {
#State private var widestText: CGFloat = 0
var body: some View {
VStack {
Text("Hello").background(RectGetter())
Text("Wonderful World!").background(RectGetter())
Rectangle().fill(Color.blue).frame(width: widestText, height: 30)
}.onPreferenceChange(MyRectPreference.self, perform: { prefs in
for p in prefs {
self.widestText = max(self.widestText, p.size.width)
}
})
}
}
struct RectGetter: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: MyRectPreference.self, value: [geometry.frame(in: .global)])
}
}
}

So I actually found a way to do this. First I tried putting Spacers around the views in various configurations, to try to push it together, but that didn't work. Then I realised I could perhaps use the .background modifier, and that actually did work. It seems to let the owning view calculate its size first, and then just takes that as its frame, which is exactly what I want.
This is just an example with some hacks to get the right height, but that is a small detail, and in my particular use case it is not needed. Probably not here either if you're clever enough.
var body: some View {
VStack(spacing: 10) {
Text("Short").background(Color.green)
Text("A longer text").background(Color.green)
Text("Dummy").opacity(0)
}
.background(backgroundView)
.background(Color.red)
.padding()
.background(Color.blue)
}
var backgroundView: some View {
VStack(spacing: 10) {
Spacer()
Spacer()
Rectangle().fill(Color.yellow)
}
}
The blue view and all the color backgrounds are of course just to make it easier to see.
This code produces this:

Related

Best method to return multiple views in SwiftUI

var myView: some View {
Group {
Text("Hello")
Text("Bye")
}
}
#ViewBuilder var myView: some View {
Text("Hello")
Text("Bye")
}
Should I use a Group or #ViewBuilder, are there any advantages in using one over the other in terms of performance and customizability? If not, is there a convention in place to use one rather than the other?
Personally I think that you shouldn't do both, if you need two vertically aligned Text views then just wrap them in VStack (or whatever you need) and make a component for flexibility. It will help with debug and will make your entire codebase more readable. Just write something like that and properly name it.
struct TwoTextViews: some View {
#Binding var firstLabelText: String
#Binding var secondLabelText: String
var body: some View {
VStack {
Text(firstLabelText)
Text(secondLabelText)
}
}
}
Don't really get the situation when you should return a list of separate views instead of them wrapped into some container.

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

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