SwiftUI’s structure primitives usually don’t present relative sizing choices, e.g. “make this view 50 % of the width of its container”. Let’s construct our personal!
Use case: chat bubbles
Contemplate this chat dialog view for example of what I need to construct. The chat bubbles all the time stay 80 % as vast as their container because the view is resized:
Constructing a proportional sizing modifier
1. The Format
We are able to construct our personal relative sizing modifier on prime of the Format
protocol. The structure multiplies its personal proposed dimension (which it receives from its guardian view) with the given elements for width and peak. It then proposes this modified dimension to its solely subview. Right here’s the implementation (the total code, together with the demo app, is on GitHub):
/// A customized structure that proposes a proportion of its
/// acquired proposed dimension to its subview.
///
/// - Precondition: should include precisely one subview.
fileprivate struct RelativeSizeLayout: Format {
var relativeWidth: Double
var relativeHeight: Double
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
assert(subviews.depend == 1, "expects a single subview")
let resizedProposal = ProposedViewSize(
width: proposal.width.map { $0 * relativeWidth },
peak: proposal.peak.map { $0 * relativeHeight }
)
return subviews[0].sizeThatFits(resizedProposal)
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
assert(subviews.depend == 1, "expects a single subview")
let resizedProposal = ProposedViewSize(
width: proposal.width.map { $0 * relativeWidth },
peak: proposal.peak.map { $0 * relativeHeight }
)
subviews[0].place(
at: CGPoint(x: bounds.midX, y: bounds.midY),
anchor: .middle,
proposal: resizedProposal
)
}
}
Notes:
-
I made the kind personal as a result of I need to management how it may be used. That is essential for sustaining the belief that the structure solely ever has a single subview (which makes the mathematics a lot less complicated).
-
Proposed sizes in SwiftUI may be
nil
or infinity in both dimension. Our structure passes these particular values by means of unchanged (infinity instances a proportion remains to be infinity). I’ll talk about under what implications this has for customers of the structure.
2. The View extension
Subsequent, we’ll add an extension on View
that makes use of the structure we simply wrote. This turns into our public API:
extension View {
/// Proposes a proportion of its acquired proposed dimension to `self`.
public func relativeProposed(width: Double = 1, peak: Double = 1) -> some View {
RelativeSizeLayout(relativeWidth: width, relativeHeight: peak) {
// Wrap content material view in a container to ensure the structure solely
// receives a single subview. As a result of views are lists!
VStack { // alternatively: `_UnaryViewAdaptor(self)`
self
}
}
}
}
Notes:
-
I made a decision to go together with a verbose title,
relativeProposed(width:peak:)
, to make the semantics clear: we’re altering the proposed dimension for the subview, which received’t all the time lead to a special precise dimension. Extra on this under. -
We’re wrapping the subview (
self
within the code above) in aVStack
. This may appear redundant, nevertheless it’s essential to ensure the structure solely receives a single factor in its subviews assortment. See Chris Eidhof’s SwiftUI Views are Lists for a proof.
Utilization
The structure code for a single chat bubble within the demo video above appears to be like like this:
let alignment: Alignment = message.sender == .me ? .trailing : .main
chatBubble
.relativeProposed(width: 0.8)
.body(maxWidth: .infinity, alignment: alignment)
The outermost versatile body with maxWidth: .infinity
is accountable for positioning the chat bubble with main or trailing alignment, relying on who’s talking.
You’ll be able to even add one other body that limits the width to a most, say 400 factors:
let alignment: Alignment = message.sender == .me ? .trailing : .main
chatBubble
.body(maxWidth: 400)
.relativeProposed(width: 0.8)
.body(maxWidth: .infinity, alignment: alignment)
Right here, our relative sizing modifier solely has an impact because the bubbles change into narrower than 400 factors. In a wider window the width-limiting body takes priority. I like how composable that is!
80 % received’t all the time lead to 80 %
For those who watch the debugging guides I’m drawing within the video above, you’ll discover that the relative sizing modifier by no means reviews a width better than 400, even when the window is vast sufficient:
It’s because our structure solely adjusts the proposed dimension for its subview however then accepts the subview’s precise dimension as its personal. Since SwiftUI views all the time select their very own dimension (which the guardian can’t override), the subview is free to disregard our proposal. On this instance, the structure’s subview is the body(maxWidth: 400)
view, which units its personal width to the proposed width or 400, whichever is smaller.
Understanding the modifier’s habits
Proposed dimension ≠ precise dimension
It’s essential to internalize that the modifier works on the idea of proposed sizes. This implies it will depend on the cooperation of its subview to attain its objective: views that ignore their proposed dimension will likely be unaffected by our modifier. I don’t discover this notably problematic as a result of SwiftUI’s complete structure system works like this. Finally, SwiftUI views all the time decide their very own dimension, so you possibly can’t write a modifier that “does the correct factor” (no matter that’s) for an arbitrary subview hierarchy.
nil
and infinity
I already talked about one other factor to pay attention to: if the guardian of the relative sizing modifier proposes nil
or .infinity
, the modifier will cross the proposal by means of unchanged. Once more, I don’t suppose that is notably dangerous, nevertheless it’s one thing to pay attention to.
Proposing nil
is SwiftUI’s method of telling a view to change into its supreme dimension (fixedSize
does this). Would you ever need to inform a view to change into, say, 50 % of its supreme width? I’m unsure. Perhaps it’d make sense for resizable pictures and related views.
By the way in which, you may modify the structure to do one thing like this:
- If the proposal is
nil
or infinity, ahead it to the subview unchanged. - Take the reported dimension of the subview as the brand new foundation and apply the scaling elements to that dimension (this nonetheless breaks down if the kid returns infinity).
- Now suggest the scaled dimension to the subview. The subview would possibly reply with a special precise dimension.
- Return this newest reported dimension as your personal dimension.
This means of sending a number of proposals to baby views is named probing. A number of built-in containers views do that too, e.g. VStack
and HStack
.
Nesting in different container views
The relative sizing modifier interacts in an fascinating method with stack views and different containers that distribute the out there house amongst their kids. I assumed this was such an fascinating matter that I wrote a separate article about it: How the relative dimension modifier interacts with stack views.
The code
The entire code is on the market in a Gist on GitHub.
Digression: Proportional sizing in early SwiftUI betas
The very first SwiftUI betas in 2019 did embody proportional sizing modifiers, however they have been taken out earlier than the ultimate launch. Chris Eidhof preserved a replica of SwiftUI’s “header file” from that point that exhibits their API, together with fairly prolonged documentation.
I don’t know why these modifiers didn’t survive the beta part. The discharge notes from 2019 don’t give a motive:
The
relativeWidth(_:)
,relativeHeight(_:)
, andrelativeSize(width:peak:)
modifiers are deprecated. Use different modifiers likebody(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)
as an alternative. (51494692)
I additionally don’t keep in mind how these modifiers labored. They most likely had considerably related semantics to my answer, however I can’t make certain. The doc feedback linked above sound simple (“Units the width of this view to the required proportion of its guardian’s width.”), however they don’t point out the intricacies of the structure algorithm (proposals and responses) in any respect.
containerRelativeFrame
Replace Could 1, 2024: Apple launched the containerRelativeFrame
modifier for its 2023 OSes (iOS 17/macOS 14). In case your deployment goal permits it, this could be a good, built-in various.
Word that containerRelativeFrame
behaves in another way than my relativeProposed
modifier because it computes the scale relative to the closest container view, whereas my modifier makes use of its proposed dimension because the reference. The SwiftUI documentation considerably vaguely lists the views that depend as a container for containerRelativeFrame
. Notably, stack views don’t depend!
Take a look at Jordan Morgan’s article Modifier Monday: .containerRelativeFrame(_ axes:) (2022-06-26) to study extra about containerRelativeFrame
.