1

Yesterday I came across this post: What is GeometryReader in SwiftUI?, and I was curious about the behavior of GeometryReader, so I went on some experimentation, and the following is what I did after finished reading the post:

import SwiftUI
import PlaygroundSupport

/* ------- Approach 1 ------- */

struct ViewGettingSize: View {
    
    @Binding var size: CGSize
    
    func makeView(with geometry: GeometryProxy) -> some View {
        
        // ⭐️ Try to update @Binding var `size`,
        //    but SwiftUI ignores this assignment, why?
        //    @Binding var `size` is NOT updated.
        self.size = geometry.size
        
        print(geometry.size)       // (158.5, 45.5)
        print(self.size)           // (50, 50)
        
        return Color.pink
    }
    
    var body: some View {
        GeometryReader { geo in
            self.makeView(with: geo)  // Color.pink
        }
    }
}

/* ------- Approach 2 ------- */

struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

struct ViewSettingSizePreference: View {
    func makeView(with geometry: GeometryProxy) -> some View {
        print(geometry.size)       // (158.5, 45.5)
        return Color.orange
            .preference(key: SizePreferenceKey.self, value: geometry.size)
    }
    var body: some View {
        GeometryReader { geo in
            self.makeView(with: geo)   // Color.orange
        }
    }
}

/* ------- Test These Approaches ------- */

let text = Text("some text").font(.largeTitle)

// live view
struct ContentView: View {
    
    @State private var size = CGSize(50, 50)
    @State private var size2 = CGSize(50, 50)
    
    var body: some View {
        VStack {
            
            Group {
                
                /* ------- Approach 1 ------- */
                text
                    // ⭐️ this one doesn't work.
                    .background(ViewGettingSize(size: $size))
                Color.blue
                    // ⭐️ `size` is still (50,50)
                    .frame(width: self.size.width, height: self.size.height)
                
                /* ------- Approach 2 ------- */
                text
                    // ⭐️ this one works.
                    .background(ViewSettingSizePreference())
                    .onPreferenceChange(SizePreferenceKey.self) { (size) in
                        print(size)        // (158.5, 45.5)
                        self.size2 = size  // ⭐️ `size2` updated successfully.
                        print(self.size2)  // (158.5, 45.5)
                }
                Color.purple
                    .frame(width: self.size2.width, height: self.size2.height)
                
            }// Group
                .border(Color.black)
            
        }// VStack (container)
            .padding()
            .background(Color.gray)
    }
}

PlaygroundPage.current.setLiveView(ContentView())

Result:

content view

From above, I used two approaches and tried to update the ContentView through updating its @State variables, although the second approach was successful, but the first one failed, does anyone know why it failed? Thanks.

lochiwei
  • 1,240
  • 9
  • 16

1 Answers1

2
   //    but SwiftUI ignores this assignment, why?

Because it would produce rendering cycle (changing state initiates refresh, which change state, and so on), and SwiftUI rendering engine is clever enough to drop such updates.

You need to delay such update for next event loop, like

func makeView(with geometry: GeometryProxy) -> some View {
    DispatchQueue.main.async {        
       self.size = geometry.size
    }
    
    print(geometry.size)       // (158.5, 45.5)
    print(self.size)           // (50, 50)
    
    return Color.pink
}

See this approach correct usage for eg. in https://stackoverflow.com/a/60214735/12299030

Asperi
  • 228,894
  • 20
  • 464
  • 690
  • but @Peter-Warbo has [reported](https://stackoverflow.com/q/60214625/5409815) that this approach would crash sometimes, wouldn't it? and IMO, this approach looks quite "unnatural" for SwiftUI, is it a hack? – lochiwei Oct 12 '20 at 04:42