What is difference between GeometryReader and GeometryProxy in SwiftUI? - swiftui

As per Apple,
GeometryReader
A container view that defines its content as a function of its own size and coordinate space.
GeometryProxy:
A proxy for access to the size and coordinate space (for anchor resolution) of the container view.
I am trying to understand when to use GeometryReader and when to use GeometryProxy? I did google but didn't see any post coming up in results. So I am asking here so that new developers like me can use it for reference.

GeometryReader
SwiftUI’s GeometryReader allows us to determine the size and
coordinates of views as a function of its own size and coordinates.
You can use a GeometryReader like this:
GeometryReader { geometry in
SomeView()
.offset(x: geometry.size.width / 2)
}
GeometryProxy
The closure variable (geometry) in the code above is of type GeometryProxy. This struct provides us with the following information:
public var size: CGSize { get }
public var safeAreaInsets: EdgeInsets { get }
public func frame(in coordinateSpace: CoordinateSpace) -> CGRect
public subscript<T>(anchor: Anchor<T>) -> T where T : Equatable { get }
Basically a GeometryReader reads the view (its size, coordinates etc.) and returns a GeometryProxy struct from which you can access all the information.
Useful links:
Understanding frames and coordinates inside GeometryReader
GeometryReader to the Rescue
Anchor preferences in SwiftUI

Related

Can you use the current font size as a metric for other properties?

Pretty simple question. I have a custom geometry-based shape that I want to utilize the current (i.e. default) font size to determine calculations for its rendering. For instance, if the font size is 12, I want the corner radius to be 1/4 that side, or three. But I'm not sure how to get the current font's metrics. Is it possible?
The reasoning is my control has a text component. If someone applies a font to it and changes the size as a result, I want to update my geometry to match that new size. So is it possible?
You can't get the typography of the font but you can get the View size or use ScaledMetric
import SwiftUI
struct FontSizeView: View {
//Use the View's size
#State var textSize: CGSize = .zero
//Apple's solution to adjusting views using the font size as a reference
#ScaledMetric var subViewSize: CGFloat = 48
var body: some View {
VStack{
Text("Hello, World!")
.modifier(ViewSizeViewModifier(size: $textSize))
Circle()
.frame(width: subViewSize, height: subViewSize)
}
}
}
struct ViewSizeViewModifier: ViewModifier{
#Binding var size: CGSize
func body(content: Content) -> some View {
content
.background(
GeometryReader{ proxy in
Color.clear
.onAppear(){
//initial value
size = proxy.size
}.onChange(of: proxy.size) { newValue in
//Updates with changes to orientation and such.
size = newValue
}
}
)
}
}

SwiftUI - How to size a view relative to its parent when the parent is inside a scrollview?

I'm trying to dynamically size some views which end up being placed inside of a scrollview. Here is the simplest sample code I can think of:
struct RootView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
// More views above
HStack(spacing: 16) {
MyView()
MyView()
}
.padding([.leading, .trailing], 16)
// More views below
}
}
}
}
struct MyView: View {
var body: some View {
VStack(alignment: .leading, spacing: 24) {
Image("myImage")
.resizable()
.scaledToFill()
VStack(alignment: .leading, spacing: 0) {
Text("Text")
OtherView()
}
}
}
}
EDIT: I think really the main issue I'm having is regarding how to dynamically size each MyView inside of the HStack.
If I wanted the Image in MyView to be sized to fill its width and grow vertically to maintain its aspect ratio, and then also size each MyView in RootView to be 40% of RootView's width, what is the best way to accomplish this? I've tried using GeometryReader but when it's nested inside the ScrollView, it causes the view its used in to collapse in on itself. If I use it outside of the ScrollView, I'm effectively always going to be getting the screen width (in this application) which isn't always what I need. On top of that, imagine that MyView is nested deeper in the view hierarchy and not called directly from RootView, but rather one of its child views. Or better yet, imagine that we don't know that RootView doesn't know its rendering a MyView if the view is determined at runtime.
To give a little context to anyone who is interested in some backstory, the app I'm trying to build is very modular in nature. The idea is that we really only have one "container view" struct that determines which views to render at runtime. We basically have a ScrollView in this container view and then any number of subviews. I'm really struggling with why it seems so difficult to set a view's content dimensions relative to its parent, any assistance would be hugely appreciated.
The best way I can think of is using a GeometryReader view. Here is an example.
GeometryReader { geometry in
RoundedRect(cornerRadius: 5).frame(width: geometry.size.width * 0.8, height: geometry.size.height * 0.8)
}
Typically I use the GeometryReader as a "Root" view and scale everything off of it, however you can place it inside of another view or even as an overlay to get the parent view size. For example.
VStack {
GeometryReader { geometry in
//Do something with geometry here.
}
}
Check it out here.
If I understood your goal correctly it is just needed to make images resizable (that makes them fill available space taking into account aspect ratio), like
VStack(alignment: .leading, spacing: 24) {
Image("myImage")
.resizable() // << here !!
.aspectRatio(contentMode: .fill)

How to calculate remaining available vertical space

I have a few VStacks in my SwiftUI ContentView and I'm wondering how calculate remaning vertical space at the bottom of screen. Reason is to draw chart there and I need to know y-axis scale.
Thanks.
Here is a demo of possible approach - use GeometryReader that consumes all available space and gives metrics
struct DemoView: View {
#State private var height = CGFloat.zero
var body: some View {
VStack {
View1()
View2()
// GeometryReader consumes all remaining space here
GeometryReader { gp in
// use gp.size here
}
}
}
}

SwiftUI Screen safe area

Im trying to calculate the screen safe area size in a SwiftUI app launch so I can derive component sizes from the safe area rectangle for iOS devices of different screen sizes.
UIScreen.main.bounds - I can use this at the start but it gives me the total screen and not the safe area
GeometryReader - using this I can get the CGSize of the safe area but I cant find a way to send this anywhere - tried using Notifications and simple functions both of which caused errors
Finally I tried using the .onPreferenceSet event in the initial view then within that closure set a CGSize variable in a reference file, but doing that, for some reason makes the first view initialise twice. Does anyone know a good way to get the edge insets or the safe area size at app startup?
More simpler solution :
UIApplication.shared.windows.first { $0.isKeyWindow }?.safeAreaInsets.bottom
or shorter:
UIApplication.shared.windows.first?.safeAreaInsets.top
Have you tried this?
You can use EnvironmentObject to send the safe area insets anywhere in your code after initializing it in your initial View.
This works for me.
class GlobalModel: ObservableObject {
//Safe Area size
#Published var safeArea: (top: CGFloat, bottom: CGFloat)
init() {
self.safeArea = (0, 0)
}
}
Inside SceneDelegate.
let globalModel = GlobalModel()
let contentView = ContentView().environmentObject(globalModel)
Inside your initial view.
struct ContentView: View {
#EnvironmentObject var globalModel: GlobalModel
var body: some View {
ZStack {
GeometryReader { geo in
Color.clear
.edgesIgnoringSafeArea(.all)
.onAppear {
self.globalModel.safeArea = (geo.safeAreaInsets.top, geo.safeAreaInsets.bottom)
}
}
SomeView()
}
}
}

Why isn't onPreferenceChange being called if it's inside a ScrollView in SwiftUI?

I've been seeing some strange behavior for preference keys with ScrollView. If I put the onPreferenceChange inside the ScrollView it won't be called, but if I put it outside it does!
I've setup a width preference key as follows:
struct WidthPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
The following simple view does not print:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
.onPreferenceChange(WidthPreferenceKey.self) {
print($0) // Not being called, we're in a scroll view.
}
}
}
}
But this works:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
}
.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
}
I know that I can use the latter approach to fix this, but sometimes I'm inside a child view that does not have access to its parent scroll view but I still want to record a preference key.
Any ideas on how to get onPreferenceChange to get called inside a ScrollView?
Note: I get Bound preference WidthPreferenceKey tried to update multiple times per frame. when I put the function inside the scroll view, which might explain what is going on but I can't figure it out.
Thanks!
I had been trying to figure out this issue for a long time and have found how to deal with it, although the way I used was just one of the workarounds.
Use onAppear to ScrollView with a flag to make its children show up.
...
#State var isShowingContent = false
...
ScrollView {
if isShowingContent {
ContentView()
}
}
.onAppear {
self.isShowingContent = true
}
Or,
Use List instead of it.
It has the scroll feature, and you can customize it with its own functionality and UITableView appearance in terms of UI. the most important is that it works as we expected.
[If you have time to read more]
Let me say my thought about that issue.
I have confirmed that onPreferenceChange isn't called at the bootstrap time of a view put inside a ScrollView. I'm not sure if it is the right behavior or not. But, I assume that it's wrong because ScrollView has to be capable of containing any views even if some of those use PreferenceKey to pass any data among views inside it. If it's the right behavior, it would be quite easy for us to get in trouble when creating our custom views.
Let's get into more detail.
I suppose that ScrollView would work slightly different from the other container views such as List, (H/V)Stack when it comes to set up its child view at the bootstrap time. In other words, ScrollView would try to draw(or lay out) children in its own way. Unfortunately, that way would affect the children's layout mechanism working incorrectly as what we've been seeing. We could guess what happened with the following message on debug view.
TestHPreferenceKey tried to update multiple times per frame.
It might be a piece of evidence to tell us that the update of children has occurred while ScrollView is doing something for its setup. At that moment, it could be guessed that the update to PreferenceKey has been ignored.
That's why I tried to put the placing child views off to onAppear.
I hope that will be useful for someone who's struggling with various issues on SwiftUI.
I think onPreferenceChange in your example is not called because it’s function is profoundly different from preference(key…)
preference(key:..) sets a preference value for the view it is used on.
whereas onPreferenceChange is a function called on a parent view – a view on a higher position in the view tree hierarchy. Its function is to go through all its children and sub-children and collect their preference(key:) values. When it found one it will use the reduce function from the PreferenceKey on this new value and all the already collected values. Once it has all the values collected and reduced them it will execute the onPreference closure on the result.
In your first example this closure is never called because the Text(“Hello”) view has no children which set the preference key value (in fact the view has no children at all). In your second example the Scroll view has a child which sets its preference value (the Text view).
All this does not explain the multiple times per frame error – which is most likely unrelated.
Recent update (24.4.2020):
In a similar case I could induce the call of onPreferenceChange by changing the Equatable condition for the PreferenceData. PreferenceData needs to be Equatable (probably to detect a change in them). However, the Anchor type by itself is not equatable any longer. To extract the values enclosed in an Anchor type a GeometryProxy is required. You get a GeometryProxy via a GeometryReader. For not disturbing the design of views by enclosing some of them into a GeometryReader I generated one in the equatable function of the PreferenceData struct:
struct ParagraphSizeData: Equatable {
let paragraphRect: Anchor<CGRect>?
static func == (value1: ParagraphSizeData, value2: ParagraphSizeData) -> Bool {
var theResult : Bool = false
let _ = GeometryReader { geometry in
generateView(geometry:geometry, equality:&theResult)
}
func generateView(geometry: GeometryProxy, equality: inout Bool) -> Rectangle {
let paragraphSize1, paragraphSize2: NSSize
if let anAnchor = value1.paragraphRect { paragraphSize1 = geometry[anAnchor].size }
else {paragraphSize1 = NSZeroSize }
if let anAnchor = value2.paragraphRect { paragraphSize2 = geometry[anAnchor].size }
else {paragraphSize2 = NSZeroSize }
equality = (paragraphSize1 == paragraphSize2)
return Rectangle()
}
return theResult
}
}
With kind regards
It seems like the issue is not necessarily with ScrollView, but with your usage of PreferenceKey. For instance, here is a sample struct in which a PreferenceKey is set according to the width of a Rectangle, and then printed using .onPreferenceChange(), all inside of a ScrollView. As you drag the Slider to change the width, the key is updated and the print closure is executed.
struct ContentView: View {
#State private var width: CGFloat = 100
var body: some View {
VStack {
Slider(value: $width, in: 100...200)
ScrollView(.vertical) {
Rectangle()
.background(WidthPreferenceKeyReader())
.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
.frame(width: self.width)
}
}
}
struct WidthPreferenceKeyReader: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
As you noted, the first time the key tries to set, the console prints "Bound preference WidthPreferenceKey tried to update multiple times per frame," but a real value is immediately set afterward, and it continues to update dynamically.
What value are you actually trying to set, and what are you trying to do in .onPreferenceChange()?
I think this is because you implemented reduce() incorrectly.
You can find the details here:
https://stackoverflow.com/a/73300115/4366470
TL;DR: Replace value = nextValue() in reduce() with value += nextValue().
You may only read it in superView, but you can change it with transformPreference after you set it .
struct ContentView: View {
var body: some View {
ScrollView {
VStack{
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
}.transformPreference(WidthPreferenceKey.self, {
$0 = 30})
}.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
}
The last value is 30 now. Hope it is what you want.
You can read from other layer:
ScrollView {
Text("Hello").preference(key: WidthPreferenceKey.self, value: CGFloat(40.0))
.backgroundPreferenceValue(WidthPreferenceKey.self) { x -> Color in
print(x)
return Color.clear
}
}
The problem here is actually not in ScrollView but in usage - this mechanism allow to transfer data up in viewTree:
A view with multiple children automatically combines its values for a
given preference into a single value visible to its ancestors.
source
The keywords here - with multiple children. This mean that u can pass it in viewTree from child to parent.
Let's review u'r code:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
.onPreferenceChange(WidthPreferenceKey.self) {
print($0) // Not being called, we're in a scroll view.
}
}
}
}
As u can see now - child pass value to itself, and not to parent - so this don't want to work, as per design.
And working case:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
}
.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
}
Here, ScrollView is parent and Text is child, and child talk to parent - everything works as expected.
So, as I sad in the beginning the problem here not in ScrollView but in usage and in Apple documentation (u need to read it few times as always).
And regarding this:
Bound preference WidthPreferenceKey tried to update multiple times per
frame.
This is because u may change multiply values in same time and View can't be rendered, try to .receive(on:) or DispatchQueue.main.async as workaround (I guess this may be a bug)