[ad_1]
SwiftUI’s format primitives typically 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
Think about this chat dialog view for instance of what I need to construct. The chat bubbles at all times stay 80 % as extensive as their container because the view is resized:
Constructing a proportional sizing modifier
1. The Format
We will construct our personal relative sizing modifier on high of the Format protocol. The format multiplies its personal proposed measurement (which it receives from its mum or dad view) with the given elements for width and top. It then proposes this modified measurement to its solely subview. Right here’s the implementation (the total code, together with the demo app, is on GitHub):
/// A customized format that proposes a share of its
/// acquired proposed measurement to its subview.
///
/// - Precondition: should comprise 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 },
top: proposal.top.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 },
top: proposal.top.map { $0 * relativeHeight }
)
subviews[0].place(
at: CGPoint(x: bounds.midX, y: bounds.midY),
anchor: .heart,
proposal: resizedProposal
)
}
}
Notes:
-
I made the sort personal as a result of I need to management how it may be used. That is essential for sustaining the belief that the format solely ever has a single subview (which makes the mathematics a lot less complicated).
-
Proposed sizes in SwiftUI could be
nilor infinity in both dimension. Our format passes these particular values via unchanged (infinity instances a share remains to be infinity). I’ll focus on under what implications this has for customers of the format.
2. The View extension
Subsequent, we’ll add an extension on View that makes use of the format we simply wrote. This turns into our public API:
extension View {
/// Proposes a share of its acquired proposed measurement to `self`.
public func relativeProposed(width: Double = 1, top: Double = 1) -> some View {
RelativeSizeLayout(relativeWidth: width, relativeHeight: top) {
// Wrap content material view in a container to verify the format 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 identify,
relativeProposed(width:top:), to make the semantics clear: we’re altering the proposed measurement for the subview, which gained’t at all times lead to a special precise measurement. Extra on this under. -
We’re wrapping the subview (
selfwithin the code above) in aVStack. This might sound redundant, however it’s obligatory to verify the format solely receives a single factor in its subviews assortment. See Chris Eidhof’s SwiftUI Views are Lists for a proof.
Utilization
The format 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 possibly can 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 develop into narrower than 400 factors. In a wider window the width-limiting body takes priority. I like how composable that is!
80 % gained’t at all times lead to 80 %
When you 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 extensive sufficient:

It is because our format solely adjusts the proposed measurement for its subview however then accepts the subview’s precise measurement as its personal. Since SwiftUI views at all times select their very own measurement (which the mum or dad can’t override), the subview is free to disregard our proposal. On this instance, the format’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 measurement ≠ precise measurement
It’s essential to internalize that the modifier works on the premise of proposed sizes. This implies it is dependent upon the cooperation of its subview to realize its objective: views that ignore their proposed measurement will probably be unaffected by our modifier. I don’t discover this significantly problematic as a result of SwiftUI’s complete format system works like this. In the end, SwiftUI views at all times decide their very own measurement, so you may’t write a modifier that “does the precise factor” (no matter that’s) for an arbitrary subview hierarchy.
nil and infinity
I already talked about one other factor to concentrate on: if the mum or dad of the relative sizing modifier proposes nil or .infinity, the modifier will move the proposal via unchanged. Once more, I don’t assume that is significantly dangerous, however it’s one thing to concentrate on.
Proposing nil is SwiftUI’s method of telling a view to develop into its excellent measurement (fixedSize does this). Would you ever need to inform a view to develop into, say, 50 % of its excellent width? I’m unsure. Possibly it’d make sense for resizable photos and comparable views.
By the way in which, you possibly can modify the format to do one thing like this:
- If the proposal is
nilor infinity, ahead it to the subview unchanged. - Take the reported measurement of the subview as the brand new foundation and apply the scaling elements to that measurement (this nonetheless breaks down if the kid returns infinity).
- Now suggest the scaled measurement to the subview. The subview may reply with a special precise measurement.
- Return this newest reported measurement as your individual measurement.
This strategy of sending a number of proposals to youngster views known as probing. Plenty 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 attention-grabbing method with stack views and different containers that distribute the out there area amongst their youngsters. I believed this was such an attention-grabbing matter that I wrote a separate article about it: How the relative measurement modifier interacts with stack views.
The code
The whole code is out there 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 had been taken out earlier than the ultimate launch. Chris Eidhof preserved a duplicate of SwiftUI’s “header file” from that point that reveals their API, together with fairly prolonged documentation.
I don’t know why these modifiers didn’t survive the beta section. The discharge notes from 2019 don’t give a cause:
The
relativeWidth(_:),relativeHeight(_:), andrelativeSize(width:top:)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 comparable semantics to my resolution, however I can’t make sure. The doc feedback linked above sound easy (“Units the width of this view to the desired proportion of its mum or dad’s width.”), however they don’t point out the intricacies of the format algorithm (proposals and responses) in any respect.
containerRelativeFrame
Replace Might 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.
Notice that containerRelativeFrame behaves otherwise than my relativeProposed modifier because it computes the dimensions relative to the closest container view, whereas my modifier makes use of its proposed measurement because the reference. The SwiftUI documentation considerably vaguely lists the views that depend as a container for containerRelativeFrame. Notably, stack views don’t depend!
Try Jordan Morgan’s article Modifier Monday: .containerRelativeFrame(_ axes:) (2022-06-26) to study extra about containerRelativeFrame.
[ad_2]
