Observe window resize event with SwiftUI

3.6k Views Asked by At

I currently have a LazyVGrid setup as such:

struct NetworkGrid: View {
    var networks: [Network]
    let columns = [
            GridItem(.flexible()),
            GridItem(.flexible()),
            GridItem(.flexible()),
            GridItem(.flexible())
        ]
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns) {
                ForEach(networks) { network in
                    NetworkCard(network: network)
                }
            }
        }
    }
}

I would like to set the number of grid columns based on the current window size, i.e.

func windowDidResize(_ notification: Notification) {
    itemWidth = CGFloat(300)
    if window.width <= itemWidth {
        GridItem(.flexible()), GridItem(.flexible())
    } else if window.width <= itemWidth * 2 {
        GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())
    } else if window.width <= itemWidth * 3 {
        GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())
    }
    ...
}

How would I go about implementing such an observer with SwiftUI?

3

There are 3 best solutions below

5
jnpdx On BEST ANSWER

The SwiftUI equivalent of listening for window size would probably be using a GeometryReader. In your example, you can read the size and dynamically decide the columns based on its width reading:

struct NetworkGrid: View {
    var networks: [Network]
    
    func columnsForWidth(width: CGFloat) -> [GridItem] {
        print("Columns for width: \(width)")
        return Array(repeating: GridItem(.flexible()), count: Int(width) / 100)
    }
    
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                LazyVGrid(columns: columnsForWidth(width: geometry.size.width)) {
                    ForEach(networks) { network in
                        NetworkCard(network: network)
                    }
                }
            }
        }
    }
}
0
Madiyar On

If you want to update @State or @ObservedObject variables when the view is resized, you can use onAppear(perform:) to update the variables with the initial view size and onChange(of:perform:) to detect view size changes:

struct MyView: View {
  @State private var size: CGSize = .zero

  var body: some View {
    GeometryReader { geometry in
      ZStack {
        Text("Hello World")
      }.onAppear {
        size = geometry.size
      }.onChange(of: geometry.size) { newSize in
        size = newSize
      }
    }
  }
}
0
Graystripe On

It's possible to achieve this without GeometryReader:

let networkCardSize = 200

ScrollView(.vertical) {
    LazyVGrid(
        columns: [
            // Move an item across rows iff it has enough space
            // to contain *just* the item (+ padding).
            GridItem(.adaptive(minimum: networkCardSize, maximum: networkCardSize)
        ],
        alignment: .leading,
        spacing: 0
    ) {
        ForEach(networks) { network in
            NetworkCard(network: network)
                .padding(10)
        }
    }
}

spacing didn't work as expected for me, but probably has a good solution. I'm also not sure how dynamically sized elements fare here. Static sizes are great tho!

gif with items in grid that move to other rows when window size can't show entire row