Cannot convert value type '()' to expected argument type '(() -> Void)?' error SwiftUI - swift

I am trying to get the height of an item im creating by setting a geometry reader, then adding an .opAppear on an empty ZStack which pulls the height of the geo reader.
struct MyView: View {
#State private var height: CGFloat = 0
var body: some View {
GeometryReader { geometry in
ZStack {/*empty ZStack*/}.onAppear {perform: self.height = geometry.size.height
}
//More code...
}
}
}
My first question is why do I get an error which says "cannot convert value type '()' to expected argument type '(() -> Void)?'" on the .onAppear
My second question would be is if there is a better item than a ZStack to use for this?

Here are a few modifications which should help with the readability of your code. I'm not clear on your goal, but this will now compile.
import SwiftUI
struct MyView: View {
#State private var height: CGSize = .zero
var body: some View {
GeometryReader { geometry in
ZStack {
}
.onAppear {
self.height = geometry.size
}
}
}
}

Related

Setting an #ObservedObject var that also relies on a struct with generics (Reference to generic type '' requires arguments in <...>)

I'm learning about SwiftUI generics and I have having some issues when defining a #ObservedObject variable; itself an extension of a struct that has generic arguments. I'm getting the following error when setting:
#ObservedObject var vm = GenericView.GenericViewModel
Error:
Reference to generic type '_' requires arguments in <...>
Xcode is asking me to explicitly define my generics, but everything that I have tried so far has failed. What exactly is it that Xcode wants me to pass? Here is some sample code:
GenericView
import SwiftUI
struct GenericView<T: Shape, U: Shape, V: View>: View {
#StateObject var vm = GenericView.GenericViewModel()
let shape1: T
let shape2: U
let color1: Color
let color2: Color
let color3: Color
let content: () -> V
var body: some View {
ZStack {
HStack {
shape1
.foregroundColor(color1)
shape2
.foregroundColor(color2)
}
.overlay(
VStack {
content()
Text(vm.someData)
}
.foregroundColor(color3)
)
}
.frame(width: 200,
height: 100)
}
}
GenericViewModel
import Foundation
extension GenericView {
class GenericViewModel: ObservableObject {
#Published var someData = ""
init() { self.someData = someData }
}
}
ContentView
import SwiftUI
struct ContentView: View {
#ObservedObject var vm = GenericView.GenericViewModel
var body: some View {
VStack {
GenericView(vm: vm,
shape1: Circle(),
shape2: Rectangle(),
color1: .black,
color2: .gray,
color3: .white) {
Text("Hello, world.")
}
Button {
vm.someData = "Hello back."
} label: {
Rectangle()
.overlay(
Text("Respond")
.foregroundColor(.black)
.bold()
)
.frame(width: 100,
height: 50)
}
.padding()
}
}
}
The modifications to make it correct :
Definition of GenericView
struct GenericView<T: Shape, U: Shape, V: View>: View {
// This observed object : it does not belong to Generci view
#ObservedObject var vm: GenericViewModel
...
Declaration of model :
struct ContentView: View {
// The model belongs to content view
// the model var must declare the type of the generic view
#StateObject var vm = GenericView<Circle, Rectangle, Text>.GenericViewModel()
...

Initializer 'xxx' requires that 'Int' conform to 'BinaryFloatingPoint'

I am making a Slider based on a view model, but I am facing this error message Initializer 'init(value:in:step:label:minimumValueLabel:maximumValueLabel:onEditingChanged:)' requires that 'Int.Stride' (aka 'Int') conform to 'BinaryFloatingPoint'
It is strange because converting the integer from view model into Double doesn't quite do the trick.
I found very similar question and read the SO answer (How can I make Int conform to BinaryFloatingPoint or Double/CGFloat conform to BinaryInteger?), but it doesn't seem like I can implementation the solution for my case, probably because I am using ObservedObject for the view model.
If I remove $ in front of setInformationVM.elapsedRestTime, I would see another error message saying Cannot convert value of type 'Int' to expected argument type 'Binding<Int>'
They said "Binding are generally used when there is a need for 2-way communication" - would that mean the Slider needs a way to communicate/update back to the View Model? Why is it that the Slider was accepting #State private var xx: Double for the value in general , but not a simple integer from my view model?
import Foundation
import SwiftUI
import Combine
struct SetRestDetailView: View {
#EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
#State var showingLog = false
var body: some View {
GeometryReader { geometry in
ZStack() {
(view content removed for readability)
}
.sheet(isPresented: $showingLog) {
let setInformatationVM = self.watchDayProgramVM.exerciseVMList[0].sets[2]
setLoggingView(setInformationVM: setInformatationVM, restfullness: 3, stepValue: 10)
}
}
}
setLoggingView
struct setLoggingView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var setInformationVM: SetInformationTestClass
#State var restfullness: Int
var stepValue: Int
var body: some View {
GeometryReader { geometry in
let rect = geometry.frame(in: .global)
ScrollView {
VStack(spacing: 5) {
Text("Rested \(Int(setInformationVM.elapsedRestTime)) sec")
Slider(value: $setInformationVM.elapsedRestTime,
in: 0...setInformationVM.totalRestTime,
step: Int.Stride(stepValue),
label: {
Text("Slider")
}, minimumValueLabel: {
Text("-\(stepValue)")
}, maximumValueLabel: {
Text("+\(stepValue)")
})
.tint(Color.white)
.padding(.bottom)
Divider()
Spacer()
Text("Restfullness")
.frame(minWidth: 0, maxWidth: .infinity)
restfullnessStepper(rect: rect, maxRestFullness: 5, minRestFullness: 1, restfullnessIndex: restfullness)
Button(action: {
print("Update Button Pressed")
//TODO
//perform further actions to update restfullness metric and elapsed rest time in the viewmodels before dismissing the view, and also update the iOS app by synching the view model.
dismiss()
}) {
HStack {
Text("Update")
.fontWeight(.medium)
}
}
.cornerRadius(40)
}
.border(Color.yellow)
}
}
}
SetInformationTestClass view model
class SetInformationTestClass: ObservableObject {
init(totalRestTime: Int, elapsedRestTime: Int, remainingRestTime: Int, isTimerRunning: Bool) {
self.totalRestTime = totalRestTime
self.elapsedRestTime = elapsedRestTime
self.remainingRestTime = remainingRestTime
}
#Published var totalRestTime: Int
#Published var elapsedRestTime: Int
#Published var remainingRestTime: Int
You can create a custom binding variable like :
let elapsedTime = Binding(
get: { Double(self.setInformationVM.elapsedRestTime) },
set: { self.setInformationVM.elapsedRestTime = Int($0) } // Or other custom logic
)
// then you reference it in the slider like:
Slider(elapsedTime, ...)

GeometryReader with NavigationView in SwiftUI is initially giving .zero for size

I have a GeometryReader in a NavigationView and initially the size is 0 when the view first displayed. I'm not sure if it's a bug or the correct behavior but I'm looking for a way to solve this as my child views are not rendering correctly.
This struct demonstrates the problem.
This printout from below is: (0.0, 0.0) for size.
Is there anyway to force the NavigationView to provide correct geometry when initially displayed?
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { geometry in
Text("Geometry Size Is Wrong")
.onAppear {
print(geometry.size) // prints out (0.0, 0.0)
}
}
}
}
}
Unfortunately, I don't think there's anything you can do to make NavigationView provide the correct geometry when initially displayed.
But if you do want access to the final geometry.size from within your view, you can use onChange(of:) as New Dev suggested:
struct ContentView: View {
#State var currentSize: CGSize?
var body: some View {
NavigationView {
GeometryReader { geometry in
Text("currentSize will soon be correct")
.onChange(of: geometry.size) { newSize in
currentSize = newSize
print(currentSize!) // prints (320.0, 457.0)
}
}
}
}
}
The above will work fine for many cases, but note that any local variables computed from geometry.size within the GeometryReader's subviews will not be accurate in the onChange block (it will capture the original, wrong value):
struct ContentView: View {
#State var currentSize: CGSize?
#State var halfWidth: CGFloat?
var body: some View {
NavigationView {
GeometryReader { geometry in
let halfWidthLocal = geometry.size.width / 2
Text("Half Width is really: \(halfWidthLocal)") // will read as "Half Width is really 160.000000"
.onChange(of: geometry.size) { newSize in
currentSize = newSize
halfWidth = halfWidthLocal
print(currentSize!) // prints (320.0, 457.0)
print(halfWidth!) // prints 0.0
}
}
}
}
}
In order to update state properties using the most up-to-date version of local variables, you can instead update the properties within a function that returns a view in your GeometryReader:
struct ContentView: View {
#State var currentSize: CGSize?
#State var halfWidth: CGFloat?
var body: some View {
NavigationView {
GeometryReader { geometry in
let halfWidthLocal = geometry.size.width / 2
makeText(halfWidthLocal: halfWidthLocal)
.onChange(of: geometry.size) { newSize in
currentSize = newSize
print(currentSize!) // prints (320.0, 457.0)
}
}
}
}
func makeText(halfWidthLocal: CGFloat) -> some View {
DispatchQueue.main.async { // Must update state properties on the main queue
halfWidth = halfWidthLocal
print(halfWidth!) // prints 0.0 the first time, then 160.0 the second time
}
return Text("Half Width is really: \(halfWidthLocal)") // will read as "Half Width is really 160.000000"
}
}
This type of situation came up for me, so just thought I'd pass on the knowledge to others.

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!