#State variable is cleared once new SwiftUI view has been fully loaded - swiftui

Whenever I call the view CardronaView() the #State variable will be loaded with the correct data for about 3 seconds then it will be reset back to it's default which is an empty string. How can I fix this? I am calling the new view with this:
NavigationView {
List {
// MARK: - Display Cardrona
NavigationLink(destination: CardronaView()) {
ZStack(alignment: .topLeading) {
Image("Cardrona-header")
Text("Cardrona").font(.largeTitle).fontWeight(.semibold).foregroundColor(Color.black)
Text(cardTemp).foregroundColor(Color.black).padding(.top, 50).font(.title3)
}
}
}
}
The code in the new view is this:
struct CardronaView: View {
#State var cardDate: String = ""
var body: some View {
List {
Text(cardDate)
}.onAppear {
Api().getJsonCard { (Cardrona) in
cardDate = "\(Cardrona.data_updated)"
}
}
}
}

When making a network request, it is good practice to perform it before loading the next view. There are many good reasons to do so, as in case the call is not successful, the new view is not loaded and avoid a bad user experience. This is where you show an alert to your user saying that the call couldn't be made.
If the network call succeeds, you pass your result to the new view and populate it.
As a network call is asynchronous and you make this call .onAppear, if the call is taking too long, you will see the empty String because your view as already been loaded.
I changed your code in a way that the network call is being made on the precedent view. But maybe you should add the code when the user taps the ListCell and if the call is successful, then the NavigationLink triggers its destination parameter, which is CardonaView(cardDate:).
struct ContentView: View {
#State var cardDate: String = ""
var body: some View {
NavigationView {
List {
// MARK: - Display Cardrona
NavigationLink(destination: CardronaView(cardDate: cardDate)) {
ZStack(alignment: .topLeading) {
Image("Cardrona-header")
Text("Cardrona").font(.largeTitle).fontWeight(.semibold).foregroundColor(Color.black)
Text(cardTemp).foregroundColor(Color.black).padding(.top, 50).font(.title3)
}
}
}
}
.onAppear {
Api().getJsonCard { (Cardrona) in
cardDate = "\(Cardrona.data_updated)"
}
}
}
struct CardronaView: View {
var cardDate: String
var body: some View {
List {
Text(cardDate)
}
}
}

Related

Pass in default text in TextView while keeping state changes with SwiftUI

I am trying to set a default text on a TextView when the view appears, while being able to still keep track of changes to the TextView that I can then pass on to my ViewModel.
Here is a little example that looks like what I am trying to do. This does however not work, it does not update the state as I would have expected. Am I doing something wrong?
struct NoteView: View {
#State var note = ""
var noteFromOutside: String?
var body: some View {
VStack {
TextField("Write a note...", text: $note)
.onSubmit {
//Do something with the note.
}
}
.onAppear {
if let newNote = noteFromOutside {
note = newNote
}
}
}
}
struct ParentView: View {
var note = "Note"
var body: some View {
VStack {
NoteView(noteFromOutside: note)
}
}
}
Found this answer to another post which solved my problem. The key was in the #Binding and init().
https://stackoverflow.com/a/64526620/12764203

SwiftUI - How to pass data then initialise and edit data

I'm downloading data from Firebase and trying to edit it. It works, but with an issue. I am currently passing data to my EditViewModel with the .onAppear() method of my view. And reading the data from EditViewModel within my view.
class EditViewModel: ObservableObject {
#Published var title: String = ""
}
struct EditView: View {
#State var selected_item: ItemModel
#StateObject var editViewModel = EditViewModel()
var body: some View {
VStack {
TextField("Name of item", text: self.$editViewModel.title)
Divider()
}.onAppear {
DispatchQueue.main.async {
editViewModel.title = selected_item.title
}
}
}
}
I have given you the extremely short-hand version as it's much easier to follow.
However, I push to another view to select options from a list and pop back. As a result, everything is reset due to using the onAppear method. I have spent hours trying to use init() but I am struggling to get my application to even compile, getting errors in the process. I understand it's due to using the .onAppear method, but how can I use init() for this particular view/view-model?
I've search online but I've found the answers to not be useful, or different from what I wish to achieve.
Thank you.
You don't need to use State for input property - it is only for internal view usage. So as far as I understood your scenario, here is a possible solution:
struct EditView: View {
private var selected_item: ItemModel
#StateObject var editViewModel = EditViewModel()
init(selectedItem: ItemModel) {
selected_item = selectedItem
editViewModel.title = selectedItem.title
}
var body: some View {
VStack {
TextField("Name of item", text: self.$editViewModel.title)
Divider()
}.onAppear {
DispatchQueue.main.async {
editViewModel.title = selected_item.title
}
}
}
}

SwifUI onAppear gets called twice

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.

(SwiftUI change detection) What is wrong with this piece of code?

When debugging an issue with an app I am working on, I managed to shrink it down to this minimal example:
class RadioModel: ObservableObject {
#Published var selected: Int = 0
}
struct RadioButton: View {
let idx: Int
#EnvironmentObject var radioModel: RadioModel
var body: some View {
Button(action: {
self.radioModel.selected = self.idx
}, label: {
if radioModel.selected == idx {
Text("Button \(idx)").background(Color.yellow)
} else {
Text("Button \(idx)")
}
})
}
}
struct RadioListTest: View {
#ObservedObject var radioModel = RadioModel()
var body: some View {
return VStack {
Text("You selected: \(radioModel.selected)")
RadioButton(idx: 0)
RadioButton(idx: 1)
RadioButton(idx: 2)
}.environmentObject(radioModel)
}
}
struct ContentView: View {
#State var refreshDate = Date()
func refresh() {
print("Refreshing...")
self.refreshDate = Date()
}
var body: some View {
VStack {
Text("\(refreshDate)")
HStack {
Button(action: {
self.refresh()
}, label: {
Text("Refresh")
})
RadioListTest()
}
}
}
}
This code looks pretty reasonable to me, although it exhibit a peculiar bug: when I hit the Refresh button, the radio buttons stop working. The radio buttons are not refreshed, and keep a reference to the old RadioModel instance, so when I click them they update that, and not the new one created after Refresh causes a new RadioListTest to be constructed. I suspect there is something wrong in the way I use EnvironmentObjects but I didn't find any reference suggesting that what I am doing is wrong. I know I could fix this particular problem in various ways that force a refresh in the radio buttons, but I would like to be able to understand which cases require a refresh forcing hack, I can't sprinkle the code with these just because "better safe than sorry", the performance is going to be hell if I have to redraw everything every time I make a modification.
edit: a clarification. The thing that is weird in my opinion and for which I would want an explanation, is this: why on refresh the RadioListTest is re-created (together with a new RadioModel) and its body re-evaluated but RadioButtons are created and the body properties are not evaluated, but the previous body is used. They both have only a view model as state, the same view model actually, but one have it as ObservedObject and the other as EnvironmentObject. I suspect it is a misuse of EnvironmentObject that I am doing, but I can't find any reference to why it is wrong
this works: (yes, i know, you know how to solve it, but i think this would be the "right" way.
problem is this line:
struct RadioListTest: View {
#ObservedObject var radioModel = RadioModel(). <<< problem
because the radioModel will be newly created each time the RadioListTest view is refreshed, so just create the instance one view above and it won't be created on every refresh (or do you want it to be created every time?!)
class RadioModel: ObservableObject {
#Published var selected: Int = 0
init() {
print("init radiomodel")
}
}
struct RadioButton<Content: View>: View {
let idx: Int
#EnvironmentObject var radioModel: RadioModel
var body: some View {
Button(action: {
self.radioModel.selected = self.idx
}, label: {
if radioModel.selected == idx {
Text("Button \(idx)").background(Color.yellow)
} else {
Text("Button \(idx)")
}
})
}
}
struct RadioListTest: View {
#EnvironmentObject var radioModel: RadioModel
var body: some View {
return VStack {
Text("You selected: \(radioModel.selected)")
RadioButton<Text>(idx: 0)
RadioButton<Text>(idx: 1)
RadioButton<Text>(idx: 2)
}.environmentObject(radioModel)
}
}
struct ContentView: View {
#ObservedObject var radioModel = RadioModel()
#State var refreshDate = Date()
func refresh() {
print("Refreshing...")
self.refreshDate = Date()
}
var body: some View {
VStack {
Text("\(refreshDate)")
HStack {
Button(action: {
self.refresh()
}, label: {
Text("Refresh")
})
RadioListTest().environmentObject(radioModel)
}
}
}
}
What is wrong with this piece of code?
Your RadioListTest subview is not updated on refresh() because it does not depend on changed parameter (refreshDate in this case), so SwiftUI rendering engine assume it is equal to previously created and does nothing with it:
HStack {
Button(action: {
self.refresh()
}, label: {
Text("Refresh")
})
RadioListTest() // << here !!
}
so the solution is to make this view dependent somehow on changed parameter, if it is required of course, and here fixed variant
RadioListTest().id(refreshDate)

SwiftUI: .sheet() doesn't go to the previous view with expected data when dismiss current sheet

As minimal, my code is like below. In SinglePersonView When user tap one image of movie in MovieListView(a movie list showing actor attended movies), then it opens the SingleMovieView as sheet mode.
The sheet could be popped up as tapping. But I found after close the sheet and re-select other movie in MovieListView, the sheet always opened as my previous clicked movie info aka the first time chosen one. And I could see in console, the movie id is always the same one as the first time. I get no clues now, do I need some reloading operation on the dismissal or something else?
And is it the correct way to use .sheet() in subView in SwiftUI, or should always keep it in the main body, SinglePersonView in this case.
struct SinglePersonView: View {
var personId = -1
#ObservedObject var model = MovieListViewModel()
var body: some View {
ScrollView() {
VStack() {
...
MovieListView(movies: model.movies)
...
}
}.onAppear {
// json API request
}
}
}
struct MovieListView: View {
var movies: [PersonMovieViewModel]
#State private var showSheet = false
ScrollView() {
HStack() {
ForEach(movies) { movie in
VStack() {
Image(...)
.onTapGesture {
self.showSheet.toggle()
}
.sheet(isPresented: self.$showSheet) {
SingleMovieView(movieId: movie.id)
}
}
}
}
}
}
There should be only one .sheet in view stack, but in provided snapshot there are many which activated all at once - following behaviour is unpredictable, actually.
Here is corrected variant
struct MovieListView: View {
var movies: [PersonMovieViewModel]
#State private var showSheet = false
#State private var selectedID = "" // type of your movie's ID
var body: some View {
ScrollView() {
HStack() {
ForEach(movies) { movie in
VStack() {
Image(...)
.onTapGesture {
self.selectedID = movie.id
self.showSheet.toggle()
}
}
}
}
.sheet(isPresented: $showSheet) {
SingleMovieView(movieId: selectedID)
}
}
}
}