I'd like to calculate the line width of a shape inside a view based on the view's size. Looking through various posts here on StackOverflow, I think the solution is to use a GeometryReader like this:
struct MyView: View {
var body: some View {
GeometryReader { geometry in
// Here goes your view content,
// and you can use the geometry variable
// which contains geometry.size of the parent
// You also have function to get the bounds
// of the parent: geometry.frame(in: .global)
}
}
}
My question is, how can I define variables inside the GeometryReader construct to be used for the view? I've tried to put a var statement directly after the line "GeometryReader { geometry in", but this gives a compiler error.
This seems to be a function builder related bug (as of Beta 3), and I recommend filing feedback on it.
The workaround I've been using is to use GeometryProxy in a separate method with an explicit return.
var body: some View {
GeometryReader { proxy in
self.useProxy(proxy)
}
}
func useProxy(_ proxy: GeometryProxy) -> some View {
var width = proxy.size.width
return VStack {
// use width in here
}
}
Related
In a chat-like app, the newest messages are shown at the bottom of the list. As you scroll up, the older messages are loaded and displayed, similar to the endless scrolling we’re accustomed to.
There are several options available to match this use case:
The first option is to use a List and invert it 180 degrees.
Another option is to use a ScrollView with a LazyVStack and invert them, similar to the List approach.
Another approach would be to cheat and fall back to the well-tested UIKit solutions.
Since SwiftUI is the future, I decided against the UIKit options (unless essential). and went for the ScrollView/LazyVStack option.
The problem is that when the items are prepended to the list, the ScrollView start-position (offset) is always the first item of the prepended list.
I cannot think of a non-hacky solution to force the ScrollView to stick with its initial offset (help is appreciated).
attaching an example code of both my reversed ScrollView and main ChatView screen.
struct ReversedScrollView<Content: View>: View {
var content: Content
init(#ViewBuilder builder: () -> Content) {
self.axis = axis
self.content = builder()
}
var body: some View {
GeometryReader { proxy in
ScrollView(axis) {
VStack {
Spacer()
content
}
.frame(
minWidth: minWidth(in: proxy, for: axis),
minHeight: minHeight(in: proxy, for: axis)
)
}
}
}
}
public struct ChatView: View {
public var body: some View {
GeometryReader { geometry in
messagesView(in: geometry)
}
}
#ViewBuilder private func messagesView(in geometry: GeometryProxy) -> some View {
ReversedScrollView {
ScrollViewReader { proxy in
LazyVStack {
ForEach(messages) { message in
ChatMessageCellContainer(
message: message,
size: geometry.size,
)
.id(message.id)
}
}
}
}
}
}
I was wondering how GeometryReader works under cover, and I am interested to build a custom GeometryReader for learning purpose!
Frankly I think every single view that we use in body is kind of GeometryReaderView with this difference that they do not use a closure for sending the proxy for us and it would annoying that every single view call back it's proxy! Therefore apple decided to give Geometry reading function to GeometryReader! So it is just my thoughts!
So I am looking a possible and also more likely SwiftUI-isch approach to reading proxy of view, or in other words see my codes in down:
struct ContentView: View {
var body: some View {
CustomGeometryReaderView { proxy in
Color.red
.onAppear() {
print(proxy)
}
}
}
}
struct CustomGeometryReaderView<Content: View>: View {
#ViewBuilder let content: (CGSize) -> Content
var body: some View {
// Here I most find a way to reading the available size for CustomGeometryReaderView and reporting it back!
return Color.clear.overlay(content(CGSize(width: 100.0, height: 100.0)), alignment: .topLeading)
}
}
Also I know that reading and reporting proxy of a view is not just the size of view, also it is about CoordinateSpace, frame ... But for now for making things easier to solve I am just working on size! So size matter!
As I said I am not interested to working with UIKit or UIViewRepresentable for reading the size! May apple using something like that under cover or may not!
My goal was trying solve the issue with pure SwiftUI or may some of you have some good link about source code of GeometryReader for reading and learning of it.
Ok, there are several instruments in SwiftUI providing access to view size (except GeometryReader of course).
The problem of course is to transfer that size value into view build phase, because only GeometryReader allows to do it in same build cycle.
Here is a demo of possible approach using Shape - a shape by design has no own size and consumes everything available, so covers all area, and has been provided that area rect as input.
Tested with Xcode 13 / iOS 15
struct CustomGeometryReaderView<Content: View>: View {
#ViewBuilder let content: (CGSize) -> Content
private struct AreaReader: Shape {
#Binding var size: CGSize
func path(in rect: CGRect) -> Path {
DispatchQueue.main.async {
size = rect.size
}
return Rectangle().path(in: rect)
}
}
#State private var size = CGSize.zero
var body: some View {
// by default shape is black so we need to clear it explicitly
AreaReader(size: $size).foregroundColor(.clear)
.overlay(Group {
if size != .zero {
content(size)
}
})
}
}
Alternate: same, but using callback-based pattern
struct CustomGeometryReaderView<Content: View>: View {
#ViewBuilder let content: (CGSize) -> Content
private struct AreaReader: Shape {
var callback: (CGSize) -> Void
func path(in rect: CGRect) -> Path {
callback(rect.size)
return Rectangle().path(in: rect)
}
}
#State private var size = CGSize.zero
var body: some View {
AreaReader { size in
if size != self.size {
DispatchQueue.main.async {
self.size = size
}
}
}
.foregroundColor(.clear)
.overlay(Group {
if size != .zero {
content(size)
}
})
}
}
Whenever I need to know the frame of a view, GeometryReader is just unnecessarily complicated and the 'Bound preference tried to update multiple times per frame' warnings are horrible. So I decided to create CustomGeometryReader
I am using SwiftUI and I am trying to pass up the height from a subview up to its parent view. It’s my understanding to use something like PreferenceKey along with .anchorPreference and then act on the change using .onPreferenceChange.
However, due to the lack of documentation on Apple’s end, I am not sure if I am using this correctly or if this is a bug with the framework perhaps.
Essentially, I want a view that can grow or shrink based on its content, however, I want to cap its size, so it doesn’t grow past, say 300 pts vertically. After that, any clipped content will be accessible via its ScrollView.
The issue is that the value is always zero for height, but I get correct values for the width.
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
VStack {
GeometryReader { geometry in
VStack(alignment: .leading) {
content()
}
.padding(.top, 10)
.padding([.leading, .bottom, .trailing], 20)
.anchorPreference(key: SizePreferenceKey.self, value: .bounds, transform: { geometry[$0].size })
}
}
.onPreferenceChange(SizePreferenceKey.self) { self.contentHeight = $0.height }
When you want to get size of content then you need to read it from inside content instead of outside parent available space... in your case you could do this (as content itself is unknown) from content's background, like
VStack(alignment: .leading) {
content()
}
.padding(.top, 10)
.padding([.leading, .bottom, .trailing], 20)
.background(GeometryReader { geometry in
Color.clear
.anchorPreference(key: SizePreferenceKey.self, value: .bounds, transform: { geometry[$0].size })
})
.onPreferenceChange(SizePreferenceKey.self) { self.contentHeight = $0.height }
Note: content() should have determined size from itself, otherwise you'll get chicken-egg problem in ScrollView
Unfortunately, there seems to be no easy solution for this. I came up with this:
Anchors are partial complete values and require a GeometryProxy to return a value. That is, you create an anchor value - say a bounds property - for any child view (whose value is incomplete at this time). Then you can get the actual bounds value relative to a given geometry proxy only when you have that proxy.
With onPreferenceChange you don't get a geometry proxy, though. You need to use backgroundPreferenceValue or overlayPreferenceValue.
The idea would be now, to use backgroundPreferenceValue, create a geometry proxy and use this proxy to relate your "bounds" anchors that have been created for each view in your scroll view content and which have been collected with an appropriate preference key, storing anchor bounds values in an array. When you have your proxy and the anchors (view bounds) you can calculate the actual bounds for each view relative to your geometry proxy - and this proxy relates to your ScrollView.
Then with backgroundPreferenceValue we could set the frame of the background view of the ScrollView. However, there's a catch:
The problem with a ScrollView is, that you cannot set the background and expect the scroll view sets its frame accordingly. That won't work.
The solution to this is using a #State variable containing the height of the content, respectively the max height. It must be set somehow when the bounds are available. This is in backgroundPreferenceValue, however, we cannot set this state property directly, since we are in the view "update phase". We can workaround this problem by just using onAppear where we can set a state property.
The state property "height" can then be used to set the frame of the ScrollView directly using the frame modifier.
See code below:
Xcode Version 13.0 beta 4:
import SwiftUI
struct ContentView: View {
let labels = (0...1).map { "- \($0) -" }
//let labels = (0...9).map { "- \($0) -" }
#State var height: CGFloat = 0
var body: some View {
HStack {
ScrollView {
ForEach(labels, id: \.self) {
Text($0)
.anchorPreference(
key: ContentFramesStorePreferenceKey.self,
value: .bounds,
transform: { [$0] })
}
}
}
.frame(height: height)
.backgroundPreferenceValue(ContentFramesStorePreferenceKey.self) { anchors in
GeometryReader { proxy in
let boundss: [CGRect] = anchors.map { proxy[$0] }
let bounds = boundss.reduce(CGRect.zero) { partialResult, rect in
partialResult.union(rect)
}
let maxHeight = min(bounds.height, 100)
Color.red.frame(width: proxy.size.width, height: maxHeight)
.onAppear {
self.height = maxHeight
}
}
}
}
}
fileprivate struct ContentFramesStorePreferenceKey: PreferenceKey {
typealias Value = [Anchor<CGRect>]
static var defaultValue: Value = []
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(
NavigationView {
ContentView()
}
.navigationViewStyle(.stack)
)
Supposing I have the main view of my application and I need to create many different images, texts, buttons, textfields, ... with proper sizes to get a neat layout - device independently.
My current approach was to work with GeometryReader to get the screen-sizes (width, height) of the device and based on this I compute their sizes.
struct MyView: View {
var body: some View {
GeometryReader { g in
...
VStack {
Subview1(g, ...)
}
...
HStack {
Subview2(g, ...)
...
Subview7(g, ...)
}
...
}
}
}
One of the many subviews as example:
struct Subview5: View {
var g: GeometryProxy
...
var body: some View {
Image(...)
.frame(width: g.size.width * 0.5, height: g.height * 0.3)
}
}
So with passing the geometry of the main view to the extracted subviews the code gets more and more difficult to read.
Is there another more elegant or a typical Swift-UI way doing this?
Thank you for taking your time in answering my question!
Assume I have a View with an Image that has a shadow property:
struct ContentView: View {
var body: some View {
Image("turtlerock").shadow(radius: 10)
}
}
Now imagine I want to access the value of the shadow radius. I assumed I could do this:
struct ContentView: View {
var body: some View {
let myImage = Image("turtlerock").shadow(radius: 10)
print(myImage.modifier.radius)
}
}
However, this returns an error:
Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
Is there a way to accomplish this somehow?
When modifying and building views, you can do this without a return statement and a building block one above the other without commas. This is called a multi-statement closure. When you try to create a variable inside a multi-statement closure, the compiler is going to complain because there is a mismatch in types (you can only combine views one after another, nothing more). See this answer for more details: https://stackoverflow.com/a/56435128/7715250
A way to fix this is to explicitly return the views you are combining, so you don't make use of the multi-closure statements:
struct MyView: View {
var body: some View {
let image = Image("Some image").shadow(radius: 10)
let myRadius = image.modifier.radius
// Do something with myRadius
return image // No multi closure statements.
}
}
If your view you want to reference is inside a stack, you should declare it outside the stack like this:
var body: some View {
let myImage = Image("image").shadow(radius: 10)
let stack = HStack {
myImage
Image("image2")
}
return stack
}
You can define the image outside body:
let myImage = Image("turtlerock").shadow(radius: 10)
var body: some View {
myImage
}
To print the radius you can do like so:
var body: some View {
myImage
.tapAction {
print(self.myImage.modifier.radius) // 10.0
}
}
When it happens to me in a testing environment I just nest everything in the body inside a
return ZStack{ ...}
A bit quick and dirty, but it works for my purposes.
I'm using Group {}:
func makeContentView() -> some View {
Group {
if some_condition_here {
Text("Hello World")
.foregroundColor(.red)
.font(.system(size: 13, weight: .bold, design: .monospaced))
} else {
Rectangle()
.fill(Color.gray20)
}
}
}
}