SwiftUI, if let, GeometryReader, aspectRatio, BOOM?!

I am encountering the following strange behaviour in SwiftUI. Assume these definitions:

import SwiftUI

let cardAspectRatio : CGSize? = CGSize(width: 63, height: 88)

extension View {

    @ViewBuilder
    func fitCardAspectRatio1() -> some View {
        let ratio = cardAspectRatio!
        self.aspectRatio(ratio, contentMode: .fit)
    }

    @ViewBuilder
    func fitCardAspectRatio2() -> some View {
        if let ratio = cardAspectRatio {
            self.aspectRatio(ratio, contentMode: .fit)
        } else {
            fatalError()
        }
    }

    func fitCardAspectRatio3() -> some View {
        let ratio = cardAspectRatio!
        return AnyView(self.aspectRatio(ratio, contentMode: .fit))
    }
}

Should not all three methods behave equivalently? I know that internally they return different view types, but that shouldn't make a difference for the user, shouldn't it? In particular, consider the following situation:

struct YellowCard : View {
    @ViewBuilder var body : some View {
 	    Color.yellow.border(Color.blue).fitCardAspectRatio1()
    }
}

YellowCard uses fitCardAspectRatioX. The YellowCard is used in a YellowBoard:

struct YellowBoard : View {

    private func cardView(size: CGSize) -> some View {
        return YellowCard().frame(width: size.width, height: size.height)
    }

    private func computeDimensions(_ size : CGSize) -> (card : CGSize, size : CGSize) {
        let ratio = aspectRatio()
        let factor = min(size.width / ratio.width, size.height / ratio.height)
        let size = CGSize(width: factor * ratio.width, height: factor * ratio.height)
        let cardWidth = size.width / 3
        let cardHeight = size.height / 2
        let card = CGSize(width: cardWidth, height: cardHeight)
        return (card: card, size: size)
    }

    var body : some View {
        GeometryReader { geom in
            let dims = computeDimensions(geom.size)
            VStack(spacing: 0) {
                HStack(spacing: 0) {
                    ForEach(0 ..< 3) { _ in cardView(size: dims.card) }
                }
                HStack(spacing: 0) {
                    ForEach(3 ..< 5) { _ in cardView(size: dims.card) }
                }
            }
        }.aspectRatio(aspectRatio(), contentMode: .fit)
    }

    func aspectRatio() -> CGSize {
        let card = cardAspectRatio!
        let width = 3 * card.width
        let height = 2 * card.height
        return CGSize(width: width, height: height)
    }
}

This behaves as expected when YellowCard is defined via fitCardAspectRatio1. When for example running this on MacCatalyst and resizing the window with just a YellowBoard in it, the board adapts to the sizes. But when using fitCardAspectRatio2 or fitCardAspectRatio3 instead in the definition of YellowCard, the board will not adapt its size but just keep its initial size.

That surely is a bug? Or is this behaviour somehow to be expected?

Just tried out your code as an iOS app and all three extensions deliver similar results. Applying a frame modifier and manually adjusting the width and height will produce cards in the aspect ratio as coded to different frame sizes.

struct ContentView: View {
    var body: some View {
        ZStack {
            YellowBoard()
                .frame(width: 600, height: 600, alignment: .center)
            Text("Hello, world!")
                .padding()
        }
    }
}

Applying .fitCardAspectRatio1(), .fitCardAspectRatio2(), or .fitCardAspectRatio3() makes no intelligible difference in Xcode iOS simulator.

You will see the bug only when you vary the size of the window, for example in MacCatalyst. Or try this:

public struct XMeasureBehavior<Content: View>: View {
    
    @State public var width: CGFloat = 300
    @State public var height: CGFloat = 300
    public var maxWidth : CGFloat = 700
    public var maxHeight : CGFloat = 700
    
    public var content: Content
    
    public var body: some View {
        VStack {
            content
                .border(Color.blue)
                .frame(width: width, height: height)
                .border(Color.red)
            Slider(value: $width, in: 0...maxWidth)
            Slider(value: $height, in: 0...maxHeight)
        }
    }
    
}

struct ContentView: View {
    var body: some View {
            XMeasureBehavior(content: YellowBoard())
        }
    }
}

The code above also checks out fine in iOS which suggests that the issues you are observing are peculiar to Mac and possibly confirms the existence of a bug - IMHO.

You are right. I just tested it on my iPad Pro and it works in all cases there the same. So I guess it is indeed a bug, on MacCatalyst only.

I've tested this also now in a pure macOS app (instead of MacCatalyst), and the same problem there. So I guess this is a problem with the SwiftUI version that ships with Catalina.

Terms of Service

Privacy Policy

Cookie Policy