Friday, October 10, 2025
HomeProgrammingSwiftUI Structure — Cracking the Dimension Code |fatbobman

SwiftUI Structure — Cracking the Dimension Code |fatbobman


Photograph by Markus Winkler on Unsplash

Within the “SwiftUI Structure — The Thriller of Dimension,” we defined quite a few sizing ideas concerned within the SwiftUI structure course of. On this article, we’ll additional deepen our understanding of the SwiftUI structure mechanism by imitating the view modifiers body and fixedSize, and show some points to pay attention to throughout structure via a number of examples.

In SwiftUI, we are able to make the most of completely different structure containers to generate almost similar rendering outcomes. For instance, ZStack, overlay, background, VStack, and HStack can all obtain related structure results.

Right here’s an instance utilizing ZStack, overlay, and background:

struct HeartView: View {
var physique: some View {
Circle()
.fill(.yellow)
.body(width: 30, peak: 30)
.overlay(Picture(systemName: "coronary heart").foregroundColor(.pink))
}
}

struct ButtonView: View {
var physique: some View {
RoundedRectangle(cornerRadius: 12)
.fill(Coloration.blue.gradient)
.body(width: 150, peak: 50)
}
}

// ZStack
struct IconDemo1: View {
var physique: some View {
ZStack(alignment: .topTrailing) {
ButtonView()
HeartView()
.alignmentGuide(.high, computeValue: { $0.peak / 2 })
.alignmentGuide(.trailing, computeValue: { $0.width / 2 })
}
}
}

// overlay
struct IconDemo2: View {
var physique: some View {
ButtonView()
.overlay(alignment: .topTrailing) {
HeartView()
.alignmentGuide(.high, computeValue: { $0.peak / 2 })
.alignmentGuide(.trailing, computeValue: { $0.width / 2 })
}
}
}

// background
struct IconDemo3: View {
var physique: some View {
HeartView()
.background(alignment:.heart){
ButtonView()
.alignmentGuide(HorizontalAlignment.heart, computeValue: {$0[.trailing]})
.alignmentGuide(VerticalAlignment.heart, computeValue: {$0[.top]})
}
}
}

Though IconDemo1, IconDemo2, and IconDemo3 look the identical within the remoted preview, inserting them inside different structure containers reveals distinct variations of their structure end result contained in the container. The composition and dimension of the required dimension are completely different (see the required dimension of every marked by the pink field within the determine under).

It is because completely different structure containers have completely different methods in planning their very own required dimension, which results in the above phenomenon.

Containers like ZStack, VStack, and HStack, their required dimension consists of the whole dimension obtained after inserting all their subviews in line with the required structure guidelines. Whereas the required dimension of the overlay and background relies upon solely on their essential view (on this instance, the required dimension of the overlay is set by ButtonView, and the required dimension of the background is set by HeartView).

Suppose the present design requirement is to put out ButtonView and HeartView as an entire, then ZStack is an effective selection.

Every container has its relevant eventualities. For instance, within the following requirement, to create a subview just like the “like” operate in a video app (solely take into account the place and dimension of gesture icon throughout structure), the overlay container that relies upon solely on the required dimension of the primary view could be very appropriate:

struct FavoriteDemo: View {
var physique: some View {
ZStack(alignment: .bottomTrailing) {
Rectangle()
.fill(Coloration.cyan.gradient.opacity(0.5))
Favourite()
.alignmentGuide(.backside, computeValue: { $0[.bottom] + 200 })
.alignmentGuide(.trailing, computeValue: { $0[.trailing] + 100 })
}
.ignoresSafeArea()
}
}

struct Favourite: View {
@State var hearts = [(String, CGFloat, CGFloat)]()
var physique: some View {
Picture(systemName: "hand.thumbsup")
.symbolVariant(.fill)
.foregroundColor(.blue)
.font(.title)
.overlay(alignment: .backside) {
ZStack {
Coloration.clear
ForEach(hearts, id: .0) { coronary heart in
Textual content("+1")
.font(.title)
.foregroundColor(.white)
.daring()
.transition(.uneven(insertion: .transfer(edge: .backside).mixed(with: .opacity), elimination: .transfer(edge: .high).mixed(with: .opacity)))
.offset(x: coronary heart.1, y: coronary heart.2)
.activity {
attempt? await Process.sleep(nanoseconds: 500000000)
if let index = hearts.firstIndex(the place: { $0.0 == coronary heart.0 }) {
let _ = withAnimation(.easeIn) {
hearts.take away(at: index)
}
}
}
}
}
.body(width: 50, peak: 100)
.allowsHitTesting(false)
}
.onTapGesture {
withAnimation(.easeOut) {
hearts.append((UUID().uuidString, .random(in: -10...10), .random(in: -10...10)))
}
}
}
}

Views of the identical look could have completely different implications. When utilizing structure containers to create mixed views, the influence on the dad or mum container’s structure of the composed view should be thought of, and an acceptable container ought to be chosen for various necessities.

Just like UIKit and AppKit, SwiftUI’s structure operations are carried out on the view degree (essence), whereas all operations concentrating on the related backing layer are nonetheless accomplished via Core Animation. Due to this fact, changes made on to the CALayer (look) are undetectable by SwiftUI’s structure system.

Such operations that alter content material after structure however earlier than rendering is prevalent in SwiftUI, e.g., offset, scaleEffect, rotationEffect, shadow, background, cornerRadius, and many others., are carried out at this stage.

Right here’s an instance:

struct OffsetDemo1:View{
var physique: some View{
HStack{
Rectangle()
.fill(.orange.gradient)
.body(maxWidth:.infinity)
Rectangle()
.fill(.inexperienced.gradient)
.body(maxWidth:.infinity)
Rectangle()
.fill(.cyan.gradient)
.body(maxWidth:.infinity)
}
.border(.pink)
}
}

We alter the place of the center rectangle with offset, which doesn’t have an effect on the dimensions of HStack. On this case, the looks and essence are decoupled:

Rectangle()
.fill(.inexperienced.gradient)
.body(width: 100, peak: 50)
.border(.blue)
.offset(x: 30, y: 30)
.border(.inexperienced)

In SwiftUI, the offset modifier corresponds to the CGAffineTransform operation in Core Animation. .offset(x: 30, y: 30) is equal to .transformEffect(.init(translationX: 30, y: 30)). Such modifications made immediately on the CALayer degree don’t have an effect on structure.

The above would be the impact you need, however in order for you the displaced view to have an effect on the structure of its dad or mum view (container), chances are you’ll want one other method — use structure containers as a substitute of Core Animation operations:

// Utilizing padding
Rectangle()
.fill(.inexperienced.gradient)
.body(width: 100, peak: 50)
.border(.blue)
.padding(EdgeInsets(high: 30, main: 30, backside: 0, trailing: 0))
.border(.inexperienced)

Or it might appear like this:

// Utilizing body
Rectangle()
.fill(.inexperienced.gradient)
.body(width: 100, peak: 50)
.border(.blue)
.body(width: 130, peak: 80, alignment: .bottomTrailing)
.border(.inexperienced)

// Utilizing place
Rectangle()
.fill(.inexperienced.gradient)
.body(width: 100, peak: 50)
.border(.blue)
.place(x: 80, y: 55)
.body(width: 130, peak: 80)
.border(.inexperienced)

In comparison with the offset view modifier, since there isn’t any prepared alternative, it’s a bit tedious to make the outcomes of rotationEffect, in flip, have an effect on the structure:

struct RotationDemo: View {
var physique: some View {
HStack(alignment: .heart) {
Textual content("HI")
.border(.pink)
Textual content("Hiya world")
.fixedSize()
.border(.yellow)
.rotationEffect(.levels(-40))
.border(.pink)
}
.border(.blue)
}
}
extension View {
func rotationEffectWithFrame(_ angle: Angle) -> some View {
modifier(RotationEffectWithFrameModifier(angle: angle))
}
}

struct RotationEffectWithFrameModifier: ViewModifier {
let angle: Angle
@State non-public var dimension: CGSize = .zero
var bounds: CGRect {
CGRect(origin: .zero, dimension: dimension)
.offsetBy(dx: -size.width / 2, dy: -size.peak / 2)
.making use of(.init(rotationAngle: CGFloat(angle.radians)))
}
func physique(content material: Content material) -> some View {
content material
.rotationEffect(angle)
.background(
GeometryReader { proxy in
Coloration.clear
.activity(id: proxy.body(in: .native)) {
dimension = proxy.dimension
}
}
)
.body(width: bounds.width, peak: bounds.peak)
}
}

struct RotationDemo: View {
var physique: some View {
HStack(alignment: .heart) {
Textual content("HI")
.border(.pink)
Textual content("Hiya world")
.fixedSize()
.border(.yellow)
.rotationEffectWithFrame(.levels(-40))
.border(.pink)
}
.border(.blue)
}
}

scaleEffect may also be carried out in the same approach to have an effect on the unique structure.

In SwiftUI, builders should be clear whether or not an operation targets the essence (based mostly on structure mechanism) or look (at CALayer degree). They’ll additionally see if it needs to have an effect on the essence by modifying the looks. This manner, the ultimate rendered impact might be in step with the anticipated structure.

Please learn “Structure in SwiftUI Manner” to learn to use completely different structure logics in SwiftUI to realize the identical visible design necessities.

On this chapter, we’ll deepen the understanding of various dimension ideas within the structure course of by imitating body and fixedSize utilizing the Structure protocol.

The structure logic of body and fixedSize has been described within the earlier part; this part solely explains the important thing code. The imitation code might be obtained right here.

There are two variations of body in SwiftUI. This part imitates body(width: CGFloat? = nil, peak: CGFloat? = nil, alignment: Alignment = .heart).

Primarily the body view modifier is a wrapper across the _FrameLayout structure container. On this instance, we title the customized structure container MyFrameLayout and the view modifier myFrame.

In SwiftUI, structure containers normally have to be wrapped earlier than utilizing them. For instance, _VStackLayout is wrapped as VStack, _FrameLayout is wrapped because the body view modifier.

The impact of this wrapping conduct is (taking MyFrameLayout for instance):

Enhance the a number of parentheses situation brought on by Structure protocol’s callAsFunction

In “Alignment in SwiftUI: Every part You Want To Know, I’ve launched that “alignment” occurs between subviews inside a container. Due to this fact, for _FrameLayout, which solely takes one subview from the developer however nonetheless wants alignment. We should add a Coloration.clear view within the modifier to resolve the dearth of alignment objects.

non-public struct MyFrameLayout: Structure, ViewModifier {
let width: CGFloat?
let peak: CGFloat?
let alignment: Alignment

func physique(content material: Content material) -> some View {
MyFrameLayout(width: width, peak: peak, alignment: alignment)() { // Because of the a number of parentheses brought on by callAsFunction
Coloration.clear // Add views for alignment help.
content material
}
}
}

public extension View {
func myFrame(width: CGFloat? = nil, peak: CGFloat? = nil, alignment: Alignment = .heart) -> some View {
self
.modifier(MyFrameLayout(width: width, peak: peak, alignment: alignment))
}
@obtainable(*, deprecated, message: "Please move a number of parameters.")
func myFrame() -> some View {
modifier(MyFrameLayout(width: nil, peak: nil, alignment: .heart))
}
}

This model of the body has the next features:

  • When each dimensions have particular values set, use these two values because the required dimension of the _FrameLayout container and the structure dimension of the subview.
  • When just one dimension has a selected worth A set, use this worth A because the required dimension of the _FrameLayout container in that dimension. For the opposite dimension, use the required dimension of the subview because the required dimension (use A and the proposed dimension obtained by _FrameLayout because the proposed dimension of the subview).
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
guard subviews.rely == 2, let content material = subviews.final else { fatalError("Cannot use MyFrameLayout immediately") }
var end result: CGSize = .zero

if let width, let peak { // Each dimensions are set.
end result = .init(width: width, peak: peak)
}
if let width, peak == nil { // Solely width is ready
let contentHeight = content material.sizeThatFits(.init(width: width, peak: proposal.peak)).peak // Required dimension of the subview on this dimension
end result = .init(width: width, peak: contentHeight)
}
if let peak, width == nil {
let contentWidth = content material.sizeThatFits(.init(width: proposal.width, peak: peak)).width
end result = .init(width: contentWidth, peak: peak)
}
if peak == nil, width == nil {
end result = content material.sizeThatFits(proposal)
}
return end result
}

In placeSubviews, we’ll make the most of the auxiliary view added within the modifier to align and place the subview.

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard subviews.rely == 2, let background = subviews.first, let content material = subviews.final else {
fatalError("Cannot use MyFrameLayout immediately")
}
background.place(at: .zero, anchor: .topLeading, proposal: .init(width: bounds.width, peak: bounds.peak))
// Get the place of the Coloration.clear's alignment information
let backgroundDimensions = background.dimensions(in: .init(width: bounds.width, peak: bounds.peak))
let offsetX = backgroundDimensions[alignment.horizontal]
let offsetY = backgroundDimensions[alignment.vertical]
// Get the place of the subview's alignment information
let contentDimensions = content material.dimensions(in: .init(width: bounds.width, peak: bounds.peak))
// Calculate the topLeading offset of content material
let main = offsetX - contentDimensions[alignment.horizontal] + bounds.minX
let high = offsetY - contentDimensions[alignment.vertical] + bounds.minY
content material.place(at: .init(x: main, y: high), anchor: .topLeading, proposal: .init(width: bounds.width, peak: bounds.peak))
}

Now, we are able to use myFrame to exchange the body in views and obtain the identical impact.

fixedSize gives an unspecified mode (nil) proposed dimension for a selected dimension of the subview. It does this to return the best dimension as its required dimension in that dimension and use that dimension as its personal required dimension returned to the dad or mum view.

non-public struct MyFixedSizeLayout: Structure, ViewModifier {
let horizontal: Bool
let vertical: Bool

func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
guard subviews.rely == 1, let content material = subviews.first else {
fatalError("Cannot use MyFixedSizeLayout immediately")
}
// Put together the proposed dimension for submission to the subview
let width = horizontal ? nil : proposal.width // If horizontal is true then submit the proposal dimensions for the unspecified mode, in any other case present the prompt dimensions for the dad or mum view on this dimension
let peak = vertical ? nil : proposal.peak // If vertical is true then submit the proposal dimensions for the unspecified mode, in any other case present the prompt dimensions for the dad or mum view on this dimension
let dimension = content material.sizeThatFits(.init(width: width, peak: peak)) // Submits the proposal dimensions decided above to the subview and will get the subview's required dimensions
return dimension // Take the required dimension of the kid view because the required dimension of the MyFixedSizeLayout container
}

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard subviews.rely == 1, let content material = subviews.first else {
fatalError("Cannot use MyFixedSizeLayout immediately")
}
content material.place(at: .init(x: bounds.minX, y: bounds.minY), anchor: .topLeading, proposal: .init(width: bounds.width, peak: bounds.peak))
}

func physique(content material: Content material) -> some View {
MyFixedSizeLayout(horizontal: horizontal, vertical: vertical)() {
content material
}
}
}

public extension View {
func myFixedSize(horizontal: Bool, vertical: Bool) -> some View {
modifier(MyFixedSizeLayout(horizontal: horizontal, vertical: vertical))
}
func myFixedSize() -> some View {
myFixedSize(horizontal: true, vertical: true)
}
}

Given the massive variations between the 2 body variations, each functionally and implementation-wise, they correspond to completely different structure containers in SwiftUI. body(minWidth:, idealWidth: , maxWidth: , minHeight: , idealHeight:, maxHeight: , alignment:) is a wrapper across the _FlexFrameLayout structure container.

_FlexFrameLayout is basically a mix of two functionalities:

  • When the best worth is ready, and the dad or mum view gives an unspecified mode proposed dimension in that dimension, return the best worth because the required dimension and use it because the structure dimension of the subview.
  • When min or (and) max has a worth, return the required dimension in that dimension in line with the next guidelines (diagram from SwiftUI-Lab):
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
guard subviews.rely == 2, let content material = subviews.final else { fatalError("Cannot use MyFlexFrameLayout immediately") }

var resultWidth: CGFloat = 0
var resultHeight: CGFloat = 0
let contentWidth = content material.sizeThatFits(proposal).width // Get the required dimension of the kid view when it comes to width utilizing the proposal dimension of the dad or mum view because the proposal dimension
// idealWidth has a worth and the dad or mum view has an unspecified mode for the proposal dimension when it comes to width, the required width is idealWidth
if let idealWidth, proposal.width == nil {
resultWidth = idealWidth
} else if minWidth == nil, maxWidth == nil { // min and max are each unspecified, returning the required dimensions of the kid view when it comes to width.
resultWidth = contentWidth
} else if let minWidth, let maxWidth { // If each min and max have values
resultWidth = clamp(min: minWidth, max: maxWidth, supply: proposal.width ?? contentWidth)
} else if let minWidth { // min If there's a worth, guarantee that the requirement dimension will not be smaller than the minimal worth.
resultWidth = clamp(min: minWidth, max: maxWidth, supply: contentWidth)
} else if let maxWidth { // When max has a worth, guarantee that the required dimension will not be bigger than the utmost worth.
resultWidth = clamp(min: minWidth, max: maxWidth, supply: proposal.width ?? contentWidth)
}
// Use the required width decided above because the proposal width to get the required peak of the kid view
let contentHeight = content material.sizeThatFits(.init(width: proposal.width == nil ? nil : resultWidth, peak: proposal.peak)).peak
if let idealHeight, proposal.peak == nil {
resultHeight = idealHeight
} else if minHeight == nil, maxHeight == nil {
resultHeight = contentHeight
} else if let minHeight, let maxHeight {
resultHeight = clamp(min: minHeight, max: maxHeight, supply: proposal.peak ?? contentHeight)
} else if let minHeight {
resultHeight = clamp(min: minHeight, max: maxHeight, supply: contentHeight)
} else if let maxHeight {
resultHeight = clamp(min: minHeight, max: maxHeight, supply: proposal.peak ?? contentHeight)
}
let dimension = CGSize(width: resultWidth, peak: resultHeight)
return dimension
}

// Restrict values to between minimal and most
func clamp(min: CGFloat?, max: CGFloat?, supply: CGFloat) -> CGFloat {
var end result: CGFloat = supply
if let min {
end result = Swift.max(supply, min)
}
if let max {
end result = Swift.min(supply, max)
}
return end result
}

Within the View extension, you may verify if min, superb, and max values are in ascending order:

public extension View {
func myFrame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .heart) -> some View {
// min < superb < max
func areInNondecreasingOrder(
_ min: CGFloat?, _ superb: CGFloat?, _ max: CGFloat?
) -> Bool {
let min = min ?? -.infinity
let superb = superb ?? min
let max = max ?? superb
return min <= superb && superb <= max
}

// The official SwiftUI implementation will nonetheless execute in case of a numerical error, however will show an error message within the console.
if !areInNondecreasingOrder(minWidth, idealWidth, maxWidth)
|| !areInNondecreasingOrder(minHeight, idealHeight, maxHeight) {
fatalError("Contradictory body constraints specified.")
}
return modifier(MyFlexFrameLayout(minWidth: minWidth, idealWidth: idealWidth, maxWidth: maxWidth, minHeight: minHeight, idealHeight: idealHeight, maxHeight: maxHeight, alignment: alignment))
}
}

The Structure protocol gives a superb window to realize a deep understanding of the SwiftUI structure mechanism. Whether or not you’ll want to create customized structure containers utilizing the Structure protocol in your future work or not, mastering it would convey nice advantages.

I hope this text will enable you to.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments