SwiftUIs layout primitives generally dont provide relative sizing options, e.g. make this view 50?% of the width of its container. Lets build our own! Use case: chat bubbles Consider this chat conversation view as an example of what I...
SwiftUIs layout primitives generally dont provide relative sizing options, e.g. make this view 50?% of the width of its container. Lets build our own!
Use case: chat bubbles
Consider this chat conversation view as an example of what I want to build. The chat bubbles always remain 80?% as wide as their container as the view is resized:
Building a proportional sizing modifier
1. The Layout
We can build our own relative sizing modifier on top of the Layout protocol. The layout multiplies its own proposed size (which it receives from its parent view) with the given factors for width and height. It then proposes this modified size to its only subview. Heres the implementation (the full code, including the demo app, is on GitHub):
Notes:
I made the type private because I want to control how it can be used. This is important for maintaining the assumption that the layout only ever has a single subview (which makes the math much simpler).
Proposed sizes in SwiftUI can be nil or infinity in either dimension. Our layout passes these special values through unchanged (infinity times a percentage is still infinity). Ill discuss below what implications this has for users of the layout.
2. The View extension
Next, well add an extension on View that uses the layout we just wrote. This becomes our public API:
Notes:
I decided to go with a verbose name, relativeProposed(width:height:), to make the semantics clear: were changing the proposed size for the subview, which wont always result in a different actual size. More on this below.
Were wrapping the subview (self in the code above) in a VStack. This might seem redundant, but its necessary to make sure the layout only receives a single element in its subviews collection. See Chris Eidhofs SwiftUI Views are Lists for an explanation.
Usage
The layout code for a single chat bubble in the demo video above looks like this:
The outermost flexible frame with maxWidth: .infinity is responsible for positioning the chat bubble with leading or trailing alignment, depending on whos speaking.
You can even add another frame that limits the width to a maximum, say 400 points:
Here, our relative sizing modifier only has an effect as the bubbles become narrower than 400 points. In a wider window the width-limiting frame takes precedence. I like how composable this is!
80?% wont always result in 80?%
If you watch the debugging guides Im drawing in the video above, youll notice that the relative sizing modifier never reports a width greater than 400, even if the window is wide enough:
This is because our layout only adjusts the proposed size for its subview but then accepts the subviews actual size as its own. Since SwiftUI views always choose their own size (which the parent cant override), the subview is free to ignore our proposal. In this example, the layouts subview is the frame(maxWidth: 400) view, which sets its own width to the proposed width or 400, whichever is smaller.
Understanding the modifiers behavior
Proposed size ? actual size
Its important to internalize that the modifier works on the basis of proposed sizes. This means it depends on the cooperation of its subview to achieve its goal: views that ignore their proposed size will be unaffected by our modifier. I dont find this particularly problematic because SwiftUIs entire layout system works like this. Ultimately, SwiftUI views always determine their own size, so you cant write a modifier that does the right thing (whatever that is) for an arbitrary subview hierarchy.
nil and infinity
I already mentioned another thing to be aware of: if the parent of the relative sizing modifier proposes nil or .infinity, the modifier will pass the proposal through unchanged. Again, I dont think this is particularly bad, but its something to be aware of.
Proposing nil is SwiftUIs way of telling a view to become its ideal size (fixedSize does this). Would you ever want to tell a view to become, say, 50?% of its ideal width? Im not sure. Maybe itd make sense for resizable images and similar views.
By the way, you could modify the layout to do something like this:
If the proposal is nil or infinity, forward it to the subview unchanged. Take the reported size of the subview as the new basis and apply the scaling factors to that size (this still breaks down if the child returns infinity). Now propose the scaled size to the subview. The subview might respond with a different actual size. Return this latest reported size as your own size.This process of sending multiple proposals to child views is called probing. Lots of built-in containers views do this too, e.g. VStack and HStack.
Nesting in other container views
The relative sizing modifier interacts in an interesting way with stack views and other containers that distribute the available space among their children. I thought this was such an interesting topic that I wrote a separate article about it: How the relative size modifier interacts with stack views.
The code
The complete code is available in a Gist on GitHub.
Digression: Proportional sizing in early SwiftUI betas
The very first SwiftUI betas in 2019 did include proportional sizing modifiers, but they were taken out before the final release. Chris Eidhof preserved a copy of SwiftUIs header file from that time that shows their API, including quite lengthy documentation.
I dont know why these modifiers didnt survive the beta phase. The release notes from 2019 dont give a reason:
The relativeWidth(_:), relativeHeight(_:), and relativeSize(width:height:) modifiers are deprecated. Use other modifiers like frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) instead. (51494692)
I also dont remember how these modifiers worked. They probably had somewhat similar semantics to my solution, but I cant be sure. The doc comments linked above sound straightforward (Sets the width of this view to the specified proportion of its parents width.), but they dont mention the intricacies of the layout algorithm (proposals and responses) at all.