Size of View in SwiftUI - swift

Here I am reading my View Size in a background process, everything works fine except than sending the un-wished value!
In this down code, I used (CGSize) -> Content, and It send the exist value of CGSize, which means CGSize() or (0.0, 0.0). Which it make sense because it is first finable value, but as you see in the codes I am calculating the needed value and I want send that value.
my Goal: how can I send my (CGSize) -> Content which CGSize is calculated.
PS: I am thinking using completionhandler on CGSize.
Something like this but I am not sure:
var content: (#escaping () -> CGSize) -> Content
or even this:
var content: (#escaping (Content) -> CGSize) -> Content
In this way that it waits until CGSize get calculated then sent it with Content together, as you can see in (CGSize) -> Content it capture first possible CGSize!
Console:
(94.66666666666666, 20.333333333333332)
read size onAppear is: (0.0, 0.0)
read size onChange is: (94.66666666666666, 20.333333333333332)
import SwiftUI
struct ContentView: View {
var body: some View {
CustomView { size in
Text("Hello, world!")
.background(Color.yellow)
.onAppear() {
print("read size onAppear is:", size.debugDescription)
}
.onChange(of: size) { newValue in
print("read size onChange is:", newValue.debugDescription)
}
}
}
}
struct CustomView<Content: View>: View {
#State private var sizeOfText: CGSize = CGSize()
var content: (CGSize) -> Content
var body: some View {
return content(sizeOfText)
.background(
GeometryReader { geometry in
Color.clear
.onAppear() { sizeOfText = geometry.size; print(sizeOfText) }
})
}
}

I could be able to find the magical answer:
extension View {
func sizeReader(size: #escaping (CGSize) -> Void) -> some View {
return self
.background(
GeometryReader { geometry in
Color.clear
.preference(key: ContentSizeReaderPreferenceKey.self, value: geometry.size)
.onPreferenceChange(ContentSizeReaderPreferenceKey.self) { newValue in size(newValue) }
}
.hidden()
)
}
}
struct ContentSizeReaderPreferenceKey: PreferenceKey {
static var defaultValue: CGSize { get { return CGSize() } }
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() }
}
Use case:
You can use .sizeReader on any View you like to read or save the Size of View. It is reverse GeometryReader, and I can say it make a significant difference in your app or project when you need to read or work with the size of a View! Look how much would be cleaner and easier using .sizeReader in your project.
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.sizeReader { size in print(size) }
}
}

Related

ForEach inside HStack causes size to be zero

I was playing with a SwiftUI animation that required the view size. In my case, it is some list implemented using HStack. Since it consists of many items, I used the ForEach struct.
It shocked me when I saw its size (0.0, 0.0). After digging, I found out that changing ForEach to an explicit items declaration made the size correct.
And now, there's a question – why is that? How do you suggest overcoming this issue?
For me, it looks like a SwiftUI bug. Since ForEach conforms to the View protocol, it should behave like any other view IMO :D
Below you can find snippets of the code related to the issue. Both ForEach implementations and size modifier-related.
struct ContentView: View {
#State private var contentSize: CGSize = .zero
var body: some View {
HStack {
HStack {
// Version 1: - With ForEach inside HStack's size is (0.0, 0.0)
ForEach([0, 1], id: \.self) { item in
Text("Item \(item)")
}
// Version 2: - Without ForEach inside HStack's size is (101.66666666666666, 20.333333333333332)
Text("Item 0")
Text("Item 1")
}
.fixedSize()
.size($contentSize)
}
}
}
extension View {
public func size(_ size: Binding<CGSize>) -> some View {
modifier(SizeBindingViewModifier(size: size))
}
}
struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: Value = .zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue()
}
}
struct SizeBindingViewModifier: ViewModifier {
#Binding private var size: CGSize
init(size: Binding<CGSize>) {
_size = size
}
func body(content: Content) -> some View {
content
.background(
GeometryReader { proxy in
Color
.clear
.preference(
key: SizePreferenceKey.self,
value: proxy.size
)
}
)
.onPreferenceChange(SizePreferenceKey.self) { size in
print("size: \(size)")
self.size = size
}
}
}
Interestingly, if you check value and nextValue() in reduce, value is initially set correctly.
In fact, in the example above, reduce is only called (with nextValue() = .zero) when the ForEach is used. Presumably this is due size not being calculable initially due to the "dynamic" nature of the ForEach.
Assuming your views will never have zero size, a simple workaround could be:
static func reduce(value: inout Value, nextValue: () -> Value) {
guard nextValue() != .zero else { return }
value = nextValue()
}

UIHostingController size too big

I'm trying to embed a SwiftUI View within a UIKit UIView, within a View again. It will look something like this:
View
↓
UIView
↓
View
Current code:
struct ContentView: View {
var body: some View {
Representable {
Text("Hello world!")
}
}
}
struct Representable<Content: View>: UIViewRepresentable {
private let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIView(context: Context) -> UIView {
let host = UIHostingController(rootView: content())
let hostView = host.view!
return hostView
}
func updateUIView(_ uiView: UIView, context: Context) {
uiView.backgroundColor = .systemRed
}
}
I want the Representable to only set the backgroundColor of the Text. It shouldn't be any bigger. Also, this is just an example, so this isn't just a Text and setting the background color.
Now
Aim
There is also a problem if the text is really long - it doesn't get constrained by the size of the screen / parent (using hugging priority in this case):
How can I make sure that Representable is only as big as the content itself, Text in this case? It should also work if the text wraps over a line for example when constrained to a certain width.
The simplest way is to use SwiftUI-Introspect and just grab the UIView from it. This is all the code needed:
Text("This is some really long text that will have to wrap to multiple lines")
.introspect(selector: TargetViewSelector.siblingOfType) { target in
target.backgroundColor = .systemRed
}
If the view is a bit more complex and there isn't a UIView specifically for it, you can embed it in a ScrollView so the content will now be a UIView:
ScrollView {
Text("Complex content here")
}
.introspectScrollView { scrollView in
scrollView.isScrollEnabled = false
scrollView.clipsToBounds = false
scrollView.subviews.first!.backgroundColor = .systemRed
}
If you don't want to use Introspect (which I would highly recommend), there is a second solution below. The second solution works in most situations, but not all.
See solution above first.
I've created a working answer. It looks quite complicated, but it works.
It basically works by using the inside GeometryReader to measure the size of the content to be wrapped and the outside GeometryReader to measure the size of the whole container. This means that Text will now wrap lines because it's constrained by the outside container's size.
Code:
struct ContentView: View {
var body: some View {
Wrapper {
Text("This is some really long text that will have to wrap to multiple lines")
}
}
}
struct Wrapper<Content: View>: View {
#State private var size: CGSize?
#State private var outsideSize: CGSize?
private let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
GeometryReader { outside in
Color.clear.preference(
key: SizePreferenceKey.self,
value: outside.size
)
}
.onPreferenceChange(SizePreferenceKey.self) { newSize in
outsideSize = newSize
}
.frame(width: size?.width, height: size?.height)
.overlay(
outsideSize != nil ?
Representable {
content()
.background(
GeometryReader { inside in
Color.clear.preference(
key: SizePreferenceKey.self,
value: inside.size
)
}
.onPreferenceChange(SizePreferenceKey.self) { newSize in
size = newSize
}
)
.frame(width: outsideSize!.width, height: outsideSize!.height)
.fixedSize()
.frame(width: size?.width ?? 0, height: size?.height ?? 0)
}
.frame(width: size?.width ?? 0, height: size?.height ?? 0)
: nil
)
}
}
struct SizePreferenceKey: PreferenceKey {
static let defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct Representable<Content: View>: UIViewRepresentable {
private let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIView(context: Context) -> UIView {
let host = UIHostingController(rootView: content())
let hostView = host.view!
return hostView
}
func updateUIView(_ uiView: UIView, context: Context) {
uiView.backgroundColor = .systemRed
}
}
Result:
Another example to show that it does make the wrapper the exact size as the SwiftUI view:
struct ContentView: View {
var body: some View {
VStack {
Wrapper {
Text("This is some really long text that will have to wrap to multiple lines")
}
.border(Color.green, width: 3)
Wrapper {
Text("This is some really long text that will have to wrap to multiple lines. However, this bottom text is a bit longer and may wrap more lines - but this isn't a problem here")
}
.border(Color.blue, width: 3)
}
}
}

How can I make a Custom ScrollView without using SwiftUI ScrollView?

I'm currently working on Custom ScrollView without ScrollView.
I don't want to add too many features, but I want to be able to scroll by dragging.
I've searched the Internet, but all I can find are examples of wrapped ScrollView.
The code I'm working on is as follows(WIP code):
import SwiftUI
struct ContentView: View {
#State private var yOffset: CGFloat = 0
#State private var contentSize: CGSize = .zero
var body: some View {
CustomScrollView {
ForEach(0..<100) { i in
Text(
"\(i)"
)
.frame(
maxWidth: .infinity
)
.background(
Color.green
)
}
.offset(y: 0)
.size(size: $contentSize)
}
.offset(y: 0)
}
}
struct CustomScrollView<Content: View>: View {
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
self.content
}
}
//==================================================
// For extension
//==================================================
struct ChildSizeReader<Content: View>: View {
#Binding var size: CGSize
let content: () -> Content
var body: some View {
// Remove ZStack from the existing answer.
content().background(
GeometryReader { proxy in
Color.clear.preference(
key: SizePreferenceKey.self,
value: proxy.size
)
}
)
.onPreferenceChange(SizePreferenceKey.self) { preferences in
self.size = preferences
}
}
}
struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: Value = .zero
static func reduce(value _: inout Value, nextValue: () -> Value) {
_ = nextValue()
}
}
extension View {
func size(size: Binding<CGSize>) -> some View {
ChildSizeReader(size: size) {
self
}
}
}
As you can see in the attached image, the top of the content is missing.
I would like to adjust it so that the top is visible, but I don't know how to do that.
Note:
The dragging and other processes will be implemented after the top is visible first.
Reference:
https://www.youtube.com/watch?v=vuFUX1Qwrmo
SwiftUI - Get size of child?

How can I make my CustomView returns View plus some more extra data in SwiftUI?

I want build a CustomView that it works almost the same as like GeometryReader in functionality, I do not want re build the existed GeometryReader, I want use it to show case of my goal, for example I created this CustomView which reads the Size of content, I want my CustomView could be able send back that read Value of size in form of closure as we seen often in Swift or SwiftUI,
My Goal: I am trying to receive Size of View, which has been read in CustomView and saved in sizeOfText in my parent/ContentView View as form of closure.
Ps: I am not interested to Binding or using ObservableObject for this issue, the question try find the answer in way of sending back data as Closure form.
import SwiftUI
struct ContentView: View {
var body: some View {
CustomView { size in // <<: Here
Text("Hello, world!")
.background(Color.yellow)
.onAppear() {
print("read size is:", size.debugDescription)
}
.onChange(of: size) { newValue in
print("read size is:", newValue.debugDescription)
}
}
}
}
struct CustomView<Content: View>: View {
#State private var sizeOfText: CGSize = CGSize()
var content: () -> Content
var body: some View {
return content()
.background(
GeometryReader { geometry in
Color.clear.onAppear() { sizeOfText = geometry.size }
})
}
}
Specifiy the type of content as CGSize and then pass sizeOfText to content.
If you wish to learn more about closure, visit swift Doc.
https://docs.swift.org/swift-book/LanguageGuide/Closures.html
import SwiftUI
struct CustomView<Content: View>: View {
#State private var sizeOfText: CGSize = CGSize()
var content: (CGSize) -> Content
var body: some View {
return content(sizeOfText)
.background(
GeometryReader { geometry in
Color.clear.onAppear() { sizeOfText = geometry.size }
})
}
}
struct ContentView: View {
var body: some View {
CustomView { size in
Text("Hello, world!")
.background(Color.yellow)
.onAppear() {
print("read size is:", size.debugDescription)
}
}
}
}
You can specify the type in the content closure like this: var content: (_ size: CGFloat) -> Content
And then you can call the closure with your desired value. The value can also be #State in CustomView.
struct ContentView1: View {
var body: some View {
CustomView { size in // <-- Here
Text("Hello, world!")
.background(Color.yellow)
.onAppear() {
// print("read size is:", size.debugDescription)
}
}
}
}
struct CustomView<Content: View>: View {
#State private var sizeOfText: CGSize = CGSize()
var content: (_ size: CGFloat) -> Content // <-- Here
var body: some View {
return content(10)
.background(
GeometryReader { geometry in
Color.clear.onAppear() { sizeOfText = geometry.size }
})
}
}

Get width of a view using in SwiftUI

I need to get width of a rendered view in SwiftUI, which is apparently not that easy.
The way I see it is that I need a function that returns a view's dimensions, simple as that.
var body: some View {
VStack(alignment: .leading) {
Text(timer.name)
.font(.largeTitle)
.fontWeight(.heavy)
Text(timer.time)
.font(.largeTitle)
.fontWeight(.heavy)
.opacity(0.5)
}
}
The only way to get the dimensions of a View is by using a GeometryReader. The reader returns the dimensions of the container.
What is a geometry reader? the documentation says:
A container view that defines its content as a function of its own size and coordinate space. Apple Doc
So you could get the dimensions by doing this:
struct ContentView: View {
#State var frame: CGSize = .zero
var body: some View {
HStack {
GeometryReader { (geometry) in
self.makeView(geometry)
}
}
}
func makeView(_ geometry: GeometryProxy) -> some View {
print(geometry.size.width, geometry.size.height)
DispatchQueue.main.async { self.frame = geometry.size }
return Text("Test")
.frame(width: geometry.size.width)
}
}
The printed size is the dimension of the HStack that is the container of inner view.
You could potentially using another GeometryReader to get the inner dimension.
But remember, SwiftUI is a declarative framework. So you should avoid calculating dimensions for the view:
read this to more example:
Make a VStack fill the width of the screen in SwiftUI
How to make view the size of another view in SwiftUI
Getting the dimensions of a child view is the first part of the task. Bubbling the value of dimensions up is the second part. GeometryReader gets the dims of the parent view which is probably not what you want. To get the dims of the child view in question we might call a modifier on its child view which has actual size such as .background() or .overlay()
struct GeometryGetterMod: ViewModifier {
#Binding var rect: CGRect
func body(content: Content) -> some View {
print(content)
return GeometryReader { (g) -> Color in // (g) -> Content in - is what it could be, but it doesn't work
DispatchQueue.main.async { // to avoid warning
self.rect = g.frame(in: .global)
}
return Color.clear // return content - doesn't work
}
}
}
struct ContentView: View {
#State private var rect1 = CGRect()
var body: some View {
let t = HStack {
// make two texts equal width, for example
// this is not a good way to achieve this, just for demo
Text("Long text").overlay(Color.clear.modifier(GeometryGetterMod(rect: $rect1)))
// You can then use rect in other places of your view:
Text("text").frame(width: rect1.width, height: rect1.height).background(Color.green)
Text("text").background(Color.yellow)
}
print(rect1)
return t
}
}
Here is another convenient way to get and do something with the size of current view: readSize function.
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
Usage:
struct ContentView: View {
#State private var commonSize = CGSize()
var body: some View {
VStack {
Text("Hello, world!")
.padding()
.border(.yellow, width: 1)
.readSize { textSize in
commonSize = textSize
}
Rectangle()
.foregroundColor(.yellow)
.frame(width: commonSize.width, height: commonSize.height)
}
}
}
There's a much simpler way to get the width of a view using GeometryReader. You need to create a state variable to store the width, then surround the desired view with a GeometryReader, and set the width value to the geometry inside that width. For instace:
struct ContentView: View {
#State var width: CGFloat = 0.00 // this variable stores the width we want to get
var body: some View {
VStack(alignment: .leading) {
GeometryReader { geometry in
Text(timer.name)
.font(.largeTitle)
.fontWeight(.heavy)
.onAppear {
self.width = geometry.size.width
print("text width: \(width)") // test
}
} // in this case, we are reading the width of text
Text(timer.time)
.font(.largeTitle)
.fontWeight(.heavy)
.opacity(0.5)
}
}
}
Note that the width will change if the target's view also changes. If you want to store it, I would suggest using a let constant somewhere else. Hope that helps!