How is it possible to set a #State var inside a geometryReader?
This is my code:
#State var isTest:Int = 0
var body: some View {
VStack{
ForEach(self.test, id: \.id) { Test in
VStack{
GeometryReader { geometry in
self.isTest = 1
I try with a function but it doesn't work.
#State var isTest: Int = 0
func testValue() {
self.isTest = 1
}
var body: some View {
VStack{
ForEach(self.test, id: \.id) { Test in
VStack{
GeometryReader { geometry in
testValue()
Any idea? Thanks!
I also had a similar problem yesterday. But I was trying to pass a value from inside the GeometryReader.
I tried a couple of ways but it didn't work.
When I use #State var to declare the variable, the compiler again complained in a purple line saying that Modifying the view during update will make it become Undefined.
When I tried to declare a variable using var only, the compiler just told me that it's immutable.
And then, I tried storing it onto my #EnvironmentObject. And I just got a dead loop.
So, my last hope was using the notification way and some how it worked. But I don't know if it's the standard way of implementation.
#State private var viewPositionY:CGFloat = 0
First, post the value frame.origin.y via notification.
GeometryReader{ geometry -> Text in
let frame = geometry.frame(in: CoordinateSpace.global)
NotificationCenter.default.post(name: Notification.Name("channelBlahblahblah"), object:nil, userInfo:["dict":frame.origin.y])
return Text("My View Title")
}
And then declare a publisher to receive the notification.
private let myPublisher = NotificationCenter.default.publisher(for: Notification.Name("channelBlahblahblah"))
Finally, use the the .onReceive modifier to receive the notification.
.onReceive(myPublisher) { (output) in
viewPositionY = output.userInfo!["dict"] as! CGFloat
//you can do you business here
}
While putting code into function is a nice touch, there may arrive another problem and that is altering the #State variable during update phase:
[SwiftUI] Modifying state during view update, this will cause undefined behavior
Using NotificationCenter to move #State variable update after view update phase can help, but one could use much more simple solution like performing variable update right after render phase by using DispatchQueue.
#State var windowSize = CGSize()
func useProxy(_ geometry: GeometryProxy) -> some View {
DispatchQueue.main.async {
self.windowSize = geometry.size
}
return EmptyView()
}
var body: some View {
return GeometryReader { geometry in
self.useProxy(geometry)
Text("Hello SwiftUI")
}
}
You can update #State variables in the onAppear method if you need the initial geometry values
#State var windowSize = CGSize()
var body: some View {
return GeometryReader { geometry in
VStack {
Text("Hello SwiftUI")
}
.onAppear {
windowSize = geometry.size
}
}
}
You can use onAppear(perform:) to update #State variables with the initial view size and onChange(of:perform:) to update the variables when the 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
}
}
}
}
Try this
#State private var viewSize: CGSize = .zero
var body: some View {
VStack {
// ...
}
.background(GeometryReader { proxy in
Color.clear.preference(
key: ViewSizePreferenceKey.self,
value: proxy.size
)
})
.onPreferenceChange(ViewSizePreferenceKey.self) { size in
viewSize = size
}
}
private struct ViewSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = value.width + value.height > nextValue().width + nextValue().height ? value : nextValue()
}
}
So it's totally possible to update a #State inside a GeometryReader. The solution is simple. However, there's a caveat:
you might end up with an infinite loop
(nothing too troublesome, I'll present a solution here)
You'll just need a DispatchQueue.main.async and explicitly declare the type of the view inside GeometryReader. If you execute the View below (don't forget to stop it) you'll see that it never stops updating the value of the Text.
NOT THE FINAL SOLUTION:
struct GenericList: View {
#State var timesCalled = 0
var body: some View {
GeometryReader { geometry -> Text in
DispatchQueue.main.async {
timesCalled += 1 // infinite loop
}
return Text("\(timesCalled)")
}
}
}
This happens because the View will "draw" the GeometryReader, which will update a #State of the View. Thus, the new #State invalidates the View causing the View to be redrawn. Consequently going back to the first step (drawing the GeometryReader and updating the state).
To solve this you need to put some constraints in the draw of the GeometryReader. Instead of returning your View inside the GeometryReader, draw it then add the GeometryReader as a transparent overlay or background. This will have the same effect but you'll be able to put constraints in the presentation.
Personally, I'd rather use an overlay because you can add as many as you want. Note that an overlay does not permit an if else inside of it, but it is possible to call a function. That's why there's the func geometryReader() below. Another thing is that in order to return different types of Views you'll need to add #ViewBuilder before it.
In this code, the GeometryReader is called only once and you get the #State var timesCalled updated.
FINAL SOLUTION:
struct GenericList: View {
#State var timesCalled = 0
#ViewBuilder
func geometryReader() -> some View {
if timesCalled < 1 {
GeometryReader { geometry -> Color in
DispatchQueue.main.async {
timesCalled += 1
}
return Color.clear
}
} else {
EmptyView()
}
}
var body: some View {
Text("\(timesCalled)")
.overlay(geometryReader())
}
}
Note: you don't need to put the same constraints, for example, in my case, I wanted to move the view with the drag gesture. So, I've put the constraints to start when the user touches down and to stop when the user ends the drag gesture.
Related
i am learning swiftui now and I am newbie for stackoverflow, I find a question,this is my code. I want to change the #State nopubName in sink ,but it's not work,the print is always "Nimar", I don't know why
struct ContentView: View {
#State var nopubName: String = "Nimar"
private var cancellable: AnyCancellable?
var stringSubject = PassthroughSubject<String, Never>()
init() {
cancellable = stringSubject.sink(receiveValue: handleValue(_:))
}
func handleValue(_ value: String) {
print("handleValue: '\(value)'")
self.nopubName = value
print("in sink "+nopubName)
}
var body: some View {
VStack {
Text(self.nopubName)
.font(.title).bold()
.foregroundColor(.red)
Spacer()
Button("sink"){
stringSubject.send("World")
print(nopubName)
}
}
}
}
You should only access a state property from inside the view’s body, or from methods called by it.
https://developer.apple.com/documentation/swiftui/state
You can get that functionality working in an ObservableObject and update an #Published To keep the UI updated
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
You don't need to use Combine, If you are within the View, you can change the value of #State variables directly
struct ContentView: View {
#State var nopubName: String = "Nimar"
var body: some View {
VStack {
Text(self.nopubName)
.font(.title).bold()
.foregroundColor(.red)
Spacer()
Button("sink"){
nopubName = "World"
}
}
}
}
I want build a function stand alone from ContentView which I could use this func to initialize some value, for example in this down code I want get size of View with the function, but for unknown reason for me it returns zero, I think the background modification do not work as I wanted in this build. any help?
func viewSizeReaderFunction<Content: View>(content: Content) -> CGSize {
var sizeOfView: CGSize = CGSize()
content
.background(
GeometryReader { geometry in
Color
.clear
.onAppear() { sizeOfView = geometry.size }
})
return sizeOfView
}
let sizeOfText: CGSize = viewSizeReaderFunction(content: Text("Hello, world!"))
struct ContentView: View {
var body: some View {
Color.red
.onAppear() {
print(sizeOfText)
}
}
}
The general idea is to have the view report its size using preference, and create a view modifier to capture that. But, like #RobNapier said, the struct has to be in the view hierarchy, and so within a rendering context, to be able to talk about sizes.
struct SizeReporter: ViewModifier {
#Binding var size: CGSize
func body(content: Content) -> some View {
content
.background(GeometryReader { geo in
Color.clear
.preference(key: SizePreferenceKey.self, value: geo.size)
})
.onPreferenceChange(SizePreferenceKey.self, perform: { value in
size = value
})
}
}
And we'd need to define SizePreferenceKey:
extension SizeReporter {
private struct SizePreferenceKey: PreferenceKey {
static let defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
}
You could create a convenience method on View:
extension View {
func getSize(_ size: Binding<CGSize>) -> some View {
self.modifier(SizeReporter(size: size))
}
}
and use it like so:
#State var size: CGSize = .zero
var body: some View {
Text("hello").getSize($size)
}
Views are just data that describe view layout. They are not objects that represent the actual "view" in the way that UIView is. They do not have their own logic or state (which is why they require #State variables rather than just var).
The code you've written assigns a zero-size to sizeOfView, then creates a View struct that is immediately thrown away, and then returns sizeOfView. There is nothing that evaluates the View struct. I would expect that the compiler is giving you a warning about this, something like "result of call to background is unused."
The way you do what you're describing is with Preferences, .onPreferenceChange and usually #State. There are a lot of answers to that around Stack Overflow.
https://stackoverflow.com/search?q=%5Bswiftui%5D+size+of+view
Here's one example, note in particular the use of .hidden():
extension View {
func saveSize(handler: #escaping (CGSize) -> Void) -> some View {
return self
.background(
GeometryReader { geometry in
Color.clear
.onAppear {
handler(geometry.size)
}
})
}
}
struct ContentView: View {
#State var sizeOfText: CGSize = .zero
var body: some View {
ZStack {
Color.red
.onAppear() {
print(sizeOfText)
}
Text("Hello, world!")
.hidden()
.saveSize { sizeOfText = $0 }
}
}
}
Note that this code is slightly dangerous in that it relies on the order that onAppear gets called, and that's not promised. In practice, you generally need to handle the case where the size hasn't been set yet. This can be made more robust with Preferences, but that tends to be a lot of hassle.
I want to know if there is simple or proper way to read a State variable value from a different View, I know the usage of .onChange or Binding or ObservableObject(class) and ..., but I like to know is there any other better way?
For example in this code I have a View called TextView which has a State value, and I am calling this View inside my ContentView, Now I put a Text in my ContentView which I want to read the State Value of TextView. Is there a spacial method for this job?
struct ContentView: View {
#State var readStringOfTextView: String = ""
var body: some View {
TextView()
Text(readStringOfTextView)
.foregroundColor(Color.blue)
}
}
struct TextView: View {
#State var stringOfText: String = "Hello, world!"
var body: some View {
Text(stringOfText)
.padding()
.foregroundColor(Color.red)
}
}
The entire point of State is that it's internal to a View. If you're trying to read it elsewhere, something has gone wrong in your design. The tool you want in this case is #Binding. ContentView should pass a Binding to TextView. Any changes in TextView will be seen by ContentView (in your example, this doesn't make sense, because stringOfText can't change, but I assume that the rest of your code changes it somehow). In your example, that would look something like this:
struct ContentView: View {
#State var readStringOfTextView: String = ""
var body: some View {
TextView(stringOfText: $readStringOfTextView)
Text(readStringOfTextView)
.foregroundColor(Color.blue)
}
}
struct TextView: View {
#Binding var stringOfText : String
var body: some View {
Text(stringOfText)
.padding()
.foregroundColor(Color.red)
.onAppear {
stringOfText = "Hello, world!"
}
}
}
In is possible to directly pass data up the view hierarchy using Preferences, but it's much more complicated, and not the right tool for the problem you've described. Even so, this is what it would look like:
Create a PreferenceKey to pass the data
Set the PreferenceKey in the child view(s) using .preference
Read the PreferenceKey in the parent view using .onPreferenceChange or .overlayPreferenceValue or .backgroundPreferenceValue.
struct TextPreference: PreferenceKey {
static var defaultValue = "default"
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
struct ContentView: View {
#State var readStringOfTextView: String = ""
var body: some View {
VStack {
TextView()
.onPreferenceChange(TextPreference.self) { value in
readStringOfTextView = value
}
Text(readStringOfTextView)
.foregroundColor(Color.blue)
}
}
}
struct TextView: View {
#State var stringOfText : String = "Hello, world!"
var body: some View {
Text(stringOfText)
.padding()
.foregroundColor(Color.red)
.preference(key: TextPreference.self, value: stringOfText)
}
}
Revised Example
Based on the thoughtful response from #pawello2222 I received in the comments below I have revised an example to demonstrate the issue I am wrestling with.
In order to demonstrate the issue I am having I am using 2 views a parent and a child. In my code code the parent view executes multiple steps but sometimes the animation subview is not visible in the first step. However when it does become visible the animation has already taken on the appearance of the end state. You can see this behavior in the following example.
Parent View
struct ContentView: View {
#State var firstTime = true
#State var breath = false
var body: some View {
VStack {
// This subview is not displayed until after the first time
if !firstTime {
SecondView(breath: $breath)
}
Spacer()
// A button click simulates the steps in my App by toggling the #Binding var
Button("Breath") {
withAnimation {
self.breath.toggle()
self.firstTime = false
}
}
// This vies shows what happens when the subview is being displayed with an intial state of false for the #Binding var
Spacer()
SecondView(breath: $breath)
}
}
}
Here is the subview containing the animation and using a #Binding var to control the animation appearance.
struct SecondView: View {
#Binding var breath: Bool
var body: some View {
Image(systemName: "flame")
.resizable()
.rotationEffect(.degrees(breath ? 360 : 0), anchor: .center)
.scaleEffect(breath ? 1 : 0.2)
.opacity(breath ? 1 : 0.75)
.animation(.easeInOut(duration: 2))
.foregroundColor(Color.red)
}
}
When you execute this the first time thru the top subview is not being displayed and when you click the button the lower subview executes the expected animation and then toggles the firstTime var so that the top subview becomes visible. Notice that the animation is fully expanded and if I was to then have another step (click) with the same value of true for the #Binding property the view would not change at all. This is the issue I am wrestling with. I would like to keep the subview from being at the end state if the first step is one that has toggled the Bool value even if the subview was not being displayed. In other words I would like to only initialize the subview when it is actually being displayed with a value of true so that the animation will always start out small.
This is why I was hoping to have the subview initialized the Binding var to false until it actually gets invoked for the first time (or to reset its state to the shrunk version of the animation) whichever is more feasible.
It looks like you may want to initialise _breath with the provided parameter:
struct ContentView: View {
#Binding var breath: Bool
init(breath: Binding<Bool>) {
_breath = breath
}
}
However, if you want to use a constant value (in your example false) you can do:
struct ContentView: View {
#Binding var breath: Bool
init(breath: Binding<Bool>) {
_breath = .constant(false)
}
}
But then, why do you need the breath: Binding<Bool> parameter?
EDIT
Here is an example how to control an animation of a child view using a #Binding variable:
struct ContentView: View {
#State var breath = false
var body: some View {
VStack {
Button("Breath") {
withAnimation {
self.breath.toggle()
}
}
SecondView(breath: $breath)
}
}
}
struct SecondView: View {
#Binding var breath: Bool
var body: some View {
Image(systemName: "flame")
.imageScale(.large)
.rotationEffect(.degrees(breath ? 360 : 0), anchor: .center)
.scaleEffect(breath ? 1 : 0.2)
.opacity(breath ? 1 : 0.75)
.animation(.easeInOut(duration: 2))
}
}
Q1: Why are onAppears called twice?
Q2: Alternatively, where can I make my network call?
I have placed onAppears at a few different place in my code and they are all called twice. Ultimately, I'm trying to make a network call before displaying the next view so if you know of a way to do that without using onAppear, I'm all ears.
I have also tried to place and remove a ForEach inside my Lists and it doesn't change anything.
Xcode 12 Beta 3 -> Target iOs 14
CoreData enabled but not used yet
struct ChannelListView: View {
#EnvironmentObject var channelStore: ChannelStore
#State private var searchText = ""
#ObservedObject private var networking = Networking()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
.padding(.top, 20)
List() {
ForEach(channelStore.allChannels) { channel in
NavigationLink(destination: VideoListView(channel: channel)
.onAppear(perform: {
print("PREVIOUS VIEW ON APPEAR")
})) {
ChannelRowView(channel: channel)
}
}
.listStyle(GroupedListStyle())
}
.navigationTitle("Channels")
}
}
}
}
struct VideoListView: View {
#EnvironmentObject var videoStore: VideoStore
#EnvironmentObject var channelStore: ChannelStore
#ObservedObject private var networking = Networking()
var channel: Channel
var body: some View {
List(videoStore.allVideos) { video in
VideoRowView(video: video)
}
.onAppear(perform: {
print("LIST ON APPEAR")
})
.navigationTitle("Videos")
.navigationBarItems(trailing: Button(action: {
networking.getTopVideos(channelID: channel.channelId) { (videos) in
var videoIdArray = [String]()
videoStore.allVideos = videos
for video in videoStore.allVideos {
videoIdArray.append(video.videoID)
}
for (index, var video) in videoStore.allVideos.enumerated() {
networking.getViewCount(videoID: videoIdArray[index]) { (viewCount) in
video.viewCount = viewCount
videoStore.allVideos[index] = video
networking.setVideoThumbnail(video: video) { (image) in
video.thumbnailImage = image
videoStore.allVideos[index] = video
}
}
}
}
}) {
Text("Button")
})
.onAppear(perform: {
print("BOTTOM ON APPEAR")
})
}
}
I had the same exact issue.
What I did was the following:
struct ContentView: View {
#State var didAppear = false
#State var appearCount = 0
var body: some View {
Text("Appeared Count: \(appearrCount)"
.onAppear(perform: onLoad)
}
func onLoad() {
if !didAppear {
appearCount += 1
//This is where I loaded my coreData information into normal arrays
}
didAppear = true
}
}
This solves it by making sure only what's inside the the if conditional inside of onLoad() will run once.
Update: Someone on the Apple Developer forums has filed a ticket and Apple is aware of the issue. My solution is a temporary hack until Apple addresses the problem.
I've been using something like this
import SwiftUI
struct OnFirstAppearModifier: ViewModifier {
let perform:() -> Void
#State private var firstTime: Bool = true
func body(content: Content) -> some View {
content
.onAppear{
if firstTime{
firstTime = false
self.perform()
}
}
}
}
extension View {
func onFirstAppear( perform: #escaping () -> Void ) -> some View {
return self.modifier(OnFirstAppearModifier(perform: perform))
}
}
and I use it instead of .onAppear()
.onFirstAppear{
self.vm.fetchData()
}
you can create a bool variable to check if first appear
struct VideoListView: View {
#State var firstAppear: Bool = true
var body: some View {
List {
Text("")
}
.onAppear(perform: {
if !self.firstAppear { return }
print("BOTTOM ON APPEAR")
self.firstAppear = false
})
}
}
Let us assume you are now designing a SwiftUI and your PM is also a physicist and philosopher. One day he tells you we should to unify UIView and UIViewController, like Quantum Mechanics and the Theory of Relativity. OK, you are like-minded with your leader, voting for "Simplicity is Tao", and create an atom named "View". Now you say: "View is everything, view is all". That sounds awesome and seems feasible. Well, you commit the code and tell the PM….
onAppear and onDisAppear exists in every view, but what you really need is a Page lifecycle callback. If you use onAppear like viewDidAppear, then you get two problems:
Being influenced by the parent, the child view will rebuild more than one time, causing onAppear to be called many times.
SwiftUI is closed source, but you should know this: view = f(view). So, onAppear will run to return a new View, which is why onAppear is called twice.
I want to tell you onAppear is right! You MUST CHANGE YOUR IDEAS. Don’t run lifecycle code in onAppear and onDisAppear! You should run that code in the "Behavior area". For example, in a button navigating to a new page.
You can create the first appear function for this bug
extension View {
/// Fix the SwiftUI bug for onAppear twice in subviews
/// - Parameters:
/// - perform: perform the action when appear
func onFirstAppear(perform: #escaping () -> Void) -> some View {
let kAppearAction = "appear_action"
let queue = OperationQueue.main
let delayOperation = BlockOperation {
Thread.sleep(forTimeInterval: 0.001)
}
let appearOperation = BlockOperation {
perform()
}
appearOperation.name = kAppearAction
appearOperation.addDependency(delayOperation)
return onAppear {
if !delayOperation.isFinished, !delayOperation.isExecuting {
queue.addOperation(delayOperation)
}
if !appearOperation.isFinished, !appearOperation.isExecuting {
queue.addOperation(appearOperation)
}
}
.onDisappear {
queue.operations
.first { $0.name == kAppearAction }?
.cancel()
}
}
}
For everyone still having this issue and using a NavigationView. Add this line to the root NavigationView() and it should fix the problem.
.navigationViewStyle(StackNavigationViewStyle())
From everything I have tried, this is the only thing that worked.
We don't have to do it on .onAppear(perform)
This can be done on init of View
In case someone else is in my boat, here is how I solved it for now:
struct ChannelListView: View {
#State private var searchText = ""
#State private var isNavLinkActive: Bool = false
#EnvironmentObject var channelStore: ChannelStore
#ObservedObject private var networking = Networking()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
.padding(.top, 20)
List(channelStore.allChannels) { channel in
ZStack {
NavigationLink(destination: VideoListView(channel: channel)) {
ChannelRowView(channel: channel)
}
HStack {
Spacer()
Button {
isNavLinkActive = true
// Place action/network call here
} label: {
Image(systemName: "arrow.right")
}
.foregroundColor(.gray)
}
}
.listStyle(GroupedListStyle())
}
.navigationTitle("Channels")
}
}
}
}
I've got this app:
#main
struct StoriesApp: App {
var body: some Scene {
WindowGroup {
TabView {
NavigationView {
StoriesView()
}
}
}
}
}
And here is my StoriesView:
// ISSUE
struct StoriesView: View {
#State var items: [Int] = []
var body: some View {
List {
ForEach(items, id: \.self) { id in
StoryCellView(id: id)
}
}
.onAppear(perform: onAppear)
}
private func onAppear() {
///////////////////////////////////
// Gets called 2 times on app start <--------
///////////////////////////////////
}
}
I've resolved the issue by measuring the diff time between onAppear() calls. According to my observations double calls of onAppear() happen between 0.02 and 0.45 seconds:
// SOLUTION
struct StoriesView: View {
#State var items: [Int] = []
#State private var didAppearTimeInterval: TimeInterval = 0
var body: some View {
List {
ForEach(items, id: \.self) { id in
StoryCellView(id: id)
}
}
.onAppear(perform: onAppear)
}
private func onAppear() {
if Date().timeIntervalSince1970 - didAppearTimeInterval > 0.5 {
///////////////////////////////////////
// Gets called only once in 0.5 seconds <-----------
///////////////////////////////////////
}
didAppearTimeInterval = Date().timeIntervalSince1970
}
}
In my case, I found that a few views up the hierarchy, .onAppear() (and .onDisappear()) was only being called once, as expected. I used that to post notifications that I listen to down in the views that need to take action on those events. It’s a gross hack, and I’ve verified that the bug is fixed in iOS 15b1, but Apple really needs to backport the fix.