PreferenceKey not working when embedded in HStack - SwiftUI

The Dimensions view takes any combination of views and shows the width & height of all the views combined using PreferenceKeys.

As seen in Example 1: When constructing all the views inside Dimensions, it will work and show the sizes.

While in Examples 2 & 3: If the views are constructed outside of Dimensions, it will not work.

QUESTION: How would it work when the views are constructed outside? Thanks.

Running on Xcode Version 12.5.1 (12E507) target iOS 14.5

The code:

import SwiftUI



struct ContentView: View {

    

    var body: some View {

        VStack {

            

            // Example 1

            Dimensions {

                HStack {

                    

                    Text("Hello, world!")

                        .padding()

                    

                    // HelloWorldVariable   // <===== THIS WILL BREAK IT

                    // HelloWorldView()     // <===== THIS WILL BREAK IT

                }

            }.background(Color.blue)

            

            // Example 2

            Dimensions {

                HStack {

                    HelloWorldVariable // <===== THIS DOES NOT WORK in the HStack

                }

            }.background(Color.green)

            

            // Example 3

            Dimensions {

                HStack {

                    HelloWorldView() // <===== THIS DOES NOT WORK in the HStack

                }

            }.background(Color.yellow)

        }

    }

    

    private var HelloWorldVariable: some View {

        Text("Hello, world!")

            .padding()

    }

}



// MARK:- HelloWorldView

struct HelloWorldView: View {

    var body: some View {

        Text("Hello, world!")

            .padding()

    }

}



// MARK:- Dimensions

struct Dimensions<Content: View>: View {

    

    private var content: () -> Content

    @State private var contentSize: CGSize = .zero

    

    init(@ViewBuilder content: @escaping () -> Content) {

        self.content = content

    }

    

    var body: some View {

        VStack {

            content()

                .background(ObserveViewDimensions())

            

            VStack {

                Text("contentWidth \(contentSize.width)")

                Text("contentHeight \(contentSize.height)")

            }

        }

        .onPreferenceChange(DimensionsKey.self, perform: { value in

            self.contentSize = value

        })

    }

}



// MARK:- ObserveViewDimensions

struct ObserveViewDimensions: View {

    var body: some View {

        GeometryReader { geometry in

            Color.clear.preference(key: DimensionsKey.self, value: geometry.size)

        }

    }

}



// MARK:- DimensionsKey

struct DimensionsKey: PreferenceKey {

    static var defaultValue: CGSize = .zero

    

    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {

        value = nextValue()

    }

    

    typealias Value = CGSize

}



// MARK:- Preview

struct ContentView_Previews: PreviewProvider {

    static var previews: some View {

        ContentView()

    }

}
Answered by Noob1 in 687711022

I found a workaround by changing the PreferenceKey to hold an array; it solved the problem.

Inspired by: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

The new code:

import SwiftUI



struct ContentView: View {

    

    var body: some View {

        VStack {

            

            // Example 1

            Dimensions {

                HStack {

                    

                    Text("Hello, world!")

                        .padding()

                    

                     HelloWorldVariable

                    

                     HelloWorldView()

                }

            }

        }

    }

    

    private var HelloWorldVariable: some View {

        Text("Hello, world!")

            .padding()

    }

}



// MARK:- HelloWorldView

struct HelloWorldView: View {

    var body: some View {

        Text("Hello, world!")

            .padding()

    }

}



// MARK:- Dimensions

struct Dimensions<Content: View>: View {

    

    private var content: () -> Content

    @State private var contentSize: CGSize = .zero

    

    init(@ViewBuilder content: @escaping () -> Content) {

        self.content = content

    }

    

    var body: some View {

        VStack {

            content()

                .background(ObserveViewDimensions())

            

            VStack {

                Text("contentWidth \(contentSize.width)")

                Text("contentHeight \(contentSize.height)")

            }

        }

        .onPreferenceChange(DimensionsKey.self, perform: { value in

            // 4. Add this

            DispatchQueue.main.async {

                self.contentSize = value.first?.size ?? .zero

            }

        })

    }

}



// MARK:- ObserveViewDimensions 3. // <== update this with the new data type

struct ObserveViewDimensions: View {

    var body: some View {

        GeometryReader { geometry in

            Color.clear.preference(key: DimensionsKey.self, value: [ViewSizeData(size: geometry.size)])

        }

    }

}



// MARK:- DimensionsKey 2. // <== update this with the new data type

struct DimensionsKey: PreferenceKey {

    static var defaultValue: [ViewSizeData] = []

    

    static func reduce(value: inout [ViewSizeData], nextValue: () -> [ViewSizeData]) {

        value.append(contentsOf: nextValue())

    }

    

    typealias Value = [ViewSizeData] 

}



// MARK:- ViewSizeData 1 <==== Add This

struct ViewSizeData: Identifiable, Equatable, Hashable {

    let id: UUID = UUID()

    let size: CGSize

    

    static func == (lhs: Self, rhs: Self) -> Bool {

        return lhs.id == rhs.id

    }

    

    func hash(into hasher: inout Hasher) {

        hasher.combine(id)

    }

}



// MARK:- Preview

struct ContentView_Previews: PreviewProvider {

    static var previews: some View {

        ContentView()

    }

}
Accepted Answer

I found a workaround by changing the PreferenceKey to hold an array; it solved the problem.

Inspired by: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

The new code:

import SwiftUI



struct ContentView: View {

    

    var body: some View {

        VStack {

            

            // Example 1

            Dimensions {

                HStack {

                    

                    Text("Hello, world!")

                        .padding()

                    

                     HelloWorldVariable

                    

                     HelloWorldView()

                }

            }

        }

    }

    

    private var HelloWorldVariable: some View {

        Text("Hello, world!")

            .padding()

    }

}



// MARK:- HelloWorldView

struct HelloWorldView: View {

    var body: some View {

        Text("Hello, world!")

            .padding()

    }

}



// MARK:- Dimensions

struct Dimensions<Content: View>: View {

    

    private var content: () -> Content

    @State private var contentSize: CGSize = .zero

    

    init(@ViewBuilder content: @escaping () -> Content) {

        self.content = content

    }

    

    var body: some View {

        VStack {

            content()

                .background(ObserveViewDimensions())

            

            VStack {

                Text("contentWidth \(contentSize.width)")

                Text("contentHeight \(contentSize.height)")

            }

        }

        .onPreferenceChange(DimensionsKey.self, perform: { value in

            // 4. Add this

            DispatchQueue.main.async {

                self.contentSize = value.first?.size ?? .zero

            }

        })

    }

}



// MARK:- ObserveViewDimensions 3. // <== update this with the new data type

struct ObserveViewDimensions: View {

    var body: some View {

        GeometryReader { geometry in

            Color.clear.preference(key: DimensionsKey.self, value: [ViewSizeData(size: geometry.size)])

        }

    }

}



// MARK:- DimensionsKey 2. // <== update this with the new data type

struct DimensionsKey: PreferenceKey {

    static var defaultValue: [ViewSizeData] = []

    

    static func reduce(value: inout [ViewSizeData], nextValue: () -> [ViewSizeData]) {

        value.append(contentsOf: nextValue())

    }

    

    typealias Value = [ViewSizeData] 

}



// MARK:- ViewSizeData 1 <==== Add This

struct ViewSizeData: Identifiable, Equatable, Hashable {

    let id: UUID = UUID()

    let size: CGSize

    

    static func == (lhs: Self, rhs: Self) -> Bool {

        return lhs.id == rhs.id

    }

    

    func hash(into hasher: inout Hasher) {

        hasher.combine(id)

    }

}



// MARK:- Preview

struct ContentView_Previews: PreviewProvider {

    static var previews: some View {

        ContentView()

    }

}

After further investigation, it turns out the Canvas (SwiftUI Previews) has that bug in Xcode Version 12.5.1 (12E507).

The original code works on an iPhone X running iOS 14.8

The above workaround works on the Canvas though.

PreferenceKey not working when embedded in HStack - SwiftUI
 
 
Q