I'm trying to stop updating the view when the user scrolling the view. When user scrolling down to top or top to down, then whole screen updates which blocks the UI for some time. Is there any way to stop updating and when the user stops the scrolling the view automatically updating.
Code:-
struct Container1: View {
#State private var myDateForDay : String?
#State var items = Array(1...200)
#ObservedObject var data11 = DataClass.sharedInstance
var body: some View {
ZStack {
Color.init(ColorConstantsName.MainThemeBgColour)
if myDateForDay == nil {
Text("No data")
} else {
GeometryReader { geo in
ScrollView {
VStack {
Text(myDateForDay!)
ForEach(self.items.indices, id: \.self) { ride in
Text("Item \(self.items[ride])")
}
}.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,value: -$0.frame(in: .named("scroll")).origin.y)
}).onPreferenceChange(ViewOffsetKey.self) {
print("offset >> \($0)")
}
}.coordinateSpace(name: "scroll")
}
}
}
.onReceive(self.data11.$myDateByDate) { response in
print("response = ",response)
myDateForDay = response
}
.onAppear{
data11.updateValue1()
}
}
}
class DataClass : NSObject, ObservableObject, UNUserNotificationCenterDelegate {
#Published var myDateByDate = "" { didSet { self.didChange.send(self) } }
var didChange = PassthroughSubject<DataClass, Never>()
override init() {
self.myDateByDate = "Starting Data"
}
func updateValue1(){
DispatchQueue.main.asyncAfter(deadline: .now()+5.0) {
self.myDateByDate = "Update Date Value"
self.updateValue12()
}
}
func updateValue12(){
DispatchQueue.main.asyncAfter(deadline: .now()+5.0) {
self.myDateByDate = "Update Date Value222"
self.updateValue1()
}
}
static let sharedInstance: DataClass = {
let instance = DataClass()
// in case we need to setup up
return instance
}()
}
Question: Stop Updating when the user scrolling the view?
Can someone please explain to me how to stop updating the view, I've tried to with above code but no results yet.
Any help would be greatly appreciated.
Thanks in advance.
Related
I'm fairly new to Swift and Core Data. I’m having a problem resolving a state issue in a new project of mine.
I have a parent view (CategoryView)that includes a context menu item to allow editing of certain category properties (EditCategoryView). When the EditCategoryView sheet is presented and an edit to a category property is made, the CategoriesView updates correctly when the sheet is dismissed. Works fine.
There is a navigation link off of CategoriesView (ItemsView) that also includes a context menu to allow editing of certain item properties (EditItemView). Unlike the prior example, when the EditItemView sheet is presented and an edit is made to an item property, the ItemsView does not update when the sheet is dismissed. The old item property still displays. If I navigate back to CategoriesView and then return to ItemsView, the updated item property displays correctly.
I’m stumped and clearly don’t understand how state is managed in a CoreData environment. My code for the 2 views seems to be similar, yet they are behaving distinctly different. I wonder if the problem relates to the difference in the structures used in the 2 ForEach lines. That is, in CategoriesView I'm looping on the results of a Fetch and in EventsView I'm looping on the results of a computed value.
Any suggestions? thanks in advance for any guidance.
I created a simple example project that demonstrates the problem. To reproduce:
tap on Load Sample Data
choose a Category
tap and hold an Item to bring up context menu
choose Edit and change the name of the item
you’ll note when sheet dismisses the updated name is not reflected
return to Category list and then select the item again to see the updated name
https://github.com/jayelevy/CoreDataState
edit to include the code for the minimal example referenced in the repo
xcdatamodeld
2 Entities
Category
Attribute: name: String
Relationships: items, destination: Item (many-to-one)
Item
Attribute: name: String
Relationships: category, destination: Category (to one)
#main
struct CoreDataStateApp: App {
#StateObject var dataController: DataController
init() {
let dataController = DataController()
_dataController = StateObject(wrappedValue: dataController)
}
var body: some Scene {
WindowGroup {
CategoriesView()
.environment(\.managedObjectContext, dataController.container.viewContext)
.environmentObject(dataController)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification), perform: save)
}
}
func save(_ note: Notification) {
dataController.save()
}
}
struct CategoriesView: View {
#EnvironmentObject var dataController: DataController
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(sortDescriptors: [SortDescriptor(\.name)])
var categories: FetchedResults<Category>
var body: some View {
NavigationView {
VStack {
List {
ForEach(categories) { category in
NavigationLink {
ItemsView(category: category)
} label : {
Text(category.categoryName)
}
}
}
}
.navigationTitle("My Categories")
.toolbar {
ToolbarItem(placement: .automatic) {
Button {
dataController.deleteAll()
try? dataController.createSampleData()
} label: {
Text("Load Sample Data")
}
}
}
}
}
}
problem occurs with the following view. When an item is edited in EditItemView, the updated property (name) does not display when returning to ItemsView from the sheet.
If you return to CategoryView and then return to ItemsView, the correct property name is displayed.
struct ItemsView: View {
#ObservedObject var category: Category
#State private var isEditingItem = false
var body: some View {
VStack {
List {
ForEach(category.categoryItems) { item in
NavigationLink {
//
} label: {
Text(item.itemName)
}
.contextMenu {
Button {
isEditingItem.toggle()
} label: {
Label("Edit Item", systemImage: "pencil")
}
}
.sheet(isPresented: $isEditingItem) {
EditItemView(item: item)
}
}
}
}
.navigationTitle(category.categoryName)
}
}
struct EditItemView: View {
var item: Item
#EnvironmentObject var dataController: DataController
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.dismiss) private var dismiss
#State private var itemName: String
init(item: Item) {
// _item = ObservedObject(initialValue: item)
self.item = item
_itemName = State(initialValue: item.itemName)
}
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Item Name", text: $itemName)
}
}
}
.navigationTitle("Edit Item")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
// add any needed cancel logic
Button("Cancel") {
dismiss()
}
}
ToolbarItem {
Button {
saveItem()
dismiss()
} label: {
Text("Update")
}
.disabled(itemName.isEmpty)
}
}
}
}
func saveItem() {
item.name = itemName
dataController.save()
}
}
extension Category {
var categoryName: String {
name ?? "New Category"
}
var categoryItems: [Item] {
items?.allObjects as? [Item] ?? []
}
extension Item {
var itemName: String {
name ?? "New Item"
}
}
extension Binding {
func onChange(_ handler: #escaping () -> Void) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler()
}
)
}
}
class DataController: ObservableObject {
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Model")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Fatal error loading store: \(error.localizedDescription)")
}
}
}
static var preview: DataController = {
let dataController = DataController(inMemory: true)
let viewContext = dataController.container.viewContext
do {
try dataController.createSampleData()
} catch {
fatalError("Fatal error creating preview: \(error.localizedDescription)")
}
return dataController
}()
func createSampleData() throws {
let viewContext = container.viewContext
for i in 1...4 {
let category = Category(context: viewContext)
category.name = "Category \(i)"
category.items = []
for j in 1...5 {
let item = Item(context: viewContext)
item.name = "Item \(j)"
item.category = category
}
}
try viewContext.save()
}
func save() {
if container.viewContext.hasChanges {
try? container.viewContext.save()
}
}
func delete(_ object: NSManagedObject) {
container.viewContext.delete(object)
}
func deleteAll() {
let fetchRequest1: NSFetchRequest<NSFetchRequestResult> = Item.fetchRequest()
let batchDeleteRequest1 = NSBatchDeleteRequest(fetchRequest: fetchRequest1)
_ = try? container.viewContext.execute(batchDeleteRequest1)
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = Category.fetchRequest()
let batchDeleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
_ = try? container.viewContext.execute(batchDeleteRequest2)
}
func count<T>(for fetchRequest: NSFetchRequest<T>) -> Int {
(try? container.viewContext.count(for: fetchRequest)) ?? 0
}
}
ItemsView needs its own #FetchRequest for CategoryItem with a predicate where category = %#.
Also, instead of passing your DataController object around just put your helper methods in an extension of NSManagedObjectContext. Then you can change DataController back to the struct it should be.
I imagine there are other opportunities to improve my code (obviously, still learning), per other posts. However, the resolution was quite simple.
Modified saveItem in EditItemView to include objectWillChange.send()
func saveItem() {
item.name = itemName
item.category = itemCategory
item.category?.objectWillChange.send()
dataController.save()
}
I have a MainView and DetailView. The MainView displays a list of items. From MainView you can go to DetailView using the push navigation. The DetailView allows to add the item. After adding the new item, I am trying to go back to the MainView and refresh the MainView. It goes back but it never displays the new item unless I restart the app.
I added onAppear on the MainView and I can see it is getting fired. But it still does not update the view.
Here is some code in the MainView:
var body: some View {
List {
ForEach(movieListVM.movies, id: \.id) { movie in
NavigationLink(
destination: AddUpdateMovieScreen(movieId: movie.id),
label: {
MovieCell(movie: movie)
})
}.onDelete(perform: deleteMovie)
}
.listStyle(PlainListStyle())
.navigationTitle("Movies")
.navigationBarItems(trailing: Button("Add Movie") {
isPresented = true
})
.sheet(isPresented: $isPresented, onDismiss: {
movieListVM.populateMovies()
}, content: {
AddUpdateMovieScreen()
})
.onAppear(perform: {
movieListVM.populateMovies()
})
.embedInNavigationView()
Here is the code in the ViewModel:
class MovieListViewModel: ObservableObject {
#Published var movies = [MovieViewModel]()
#Published var updated: Bool = false
func deleteMovie(movie: MovieViewModel) {
let movie = CoreDataManager.shared.getMovieById(id: movie.id)
if let movie = movie {
CoreDataManager.shared.deleteMovie(movie)
}
}
func populateMovies() {
let movies = CoreDataManager.shared.getAllMovies()
for movie in movies {
print(movie.title) // THIS PRINTS THE UPDATE OBJECTS
}
DispatchQueue.main.async {
self.movies = movies.map(MovieViewModel.init) // THIS POPULATES THE movies correctly.
}
}
}
Any ideas why the MainView is not updating, even though I am firing the populateMovies function of the MovieListViewModel.
import SwiftUI
import CoreData
struct AddUpdateMovieScreen: View {
#StateObject private var addMovieVM = AddUpdateMovieViewModel()
#Environment(\.presentationMode) var presentationMode
#State private var movieVS = MovieViewState()
var movieId: NSManagedObjectID?
private func saveOrUpdate() {
do {
if movieId != nil {
// UPDATE IS THE ISSUE I AM TRYING TO RESOLVE
try addMovieVM.update(movieVS)
} else {
addMovieVM.save(movieVS)
}
} catch {
print(error)
}
}
var body: some View {
Form {
TextField("Enter name", text: $movieVS.title)
TextField("Enter director", text: $movieVS.director)
HStack {
Text("Rating")
Spacer()
RatingView(rating: $movieVS.rating)
}
DatePicker("Release Date", selection: $movieVS.releaseDate)
HStack {
Spacer()
Button("Save") {
saveOrUpdate()
presentationMode.wrappedValue.dismiss()
}
Spacer()
}
}
.onAppear(perform: {
// if the movieId is not nil then fetch the movie information
if let movieId = movieId {
// fetch the movie
do {
let movieVM = try addMovieVM.getMovieById(movieId: movieId)
movieVS = MovieViewState.fromMovieViewModel(vm: movieVM)
} catch {
print(error)
}
}
})
.navigationTitle("Add Movie")
.embedInNavigationView()
}
}
struct AddMovieScreen_Previews: PreviewProvider {
static var previews: some View {
AddUpdateMovieScreen()
}
}
Since you don't show your code for "AddUpdateMovieScreen", here are my guesses:
if you are passing "movieListVM" to "AddUpdateMovieScreen" as ObservableObject, then use this:
.sheet(isPresented: $isPresented) {
AddUpdateMovieScreen(movieListVM: movieListVM)
}
and:
struct AddUpdateMovieScreen: View {
#ObservedObject var movieListVM: MovieListViewModel
...
if you are passing "movieListVM" to "AddUpdateMovieScreen" as EnvironmentObject, then use this:
.sheet(isPresented: $isPresented) {
AddUpdateMovieScreen().environment(movieListVM)
}
and:
struct AddUpdateMovieScreen: View {
#EnvironmentObject var movieListVM: MovieListViewModel
...
There is no need for "movieListVM.populateMovies()" in the sheet onDismiss.
I'm attempting to build a paging app, where each page has its own NavigationView. It sort of works, but I get a completely redundant "Back" button in the navigation bar. My code so far is -
struct AllListsView: View {
#ObservedObject var viewModel: AllListsViewModel
#State private var pageName: String = ""
init(viewModel: AllListsViewModel) {
self.viewModel = viewModel
}
var body: some View {
IfLet($viewModel.listViews) { listViews in
TabView {
ForEach(listViews) { $0 }
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
} else: {
Text("No lists")
}
}
}
struct ListView: View, Identifiable, Hashable {
let id: String
private var viewModel: ListViewModel
init(viewModel: ListViewModel) {
self.id = String(viewModel.list.index)
self.viewModel = viewModel
}
var body: some View {
NavigationView {
Text("Any Text") // if I don't have this, nothing is displayed
List(viewModel.listItems) {
Text("\($0.quantity) \($0.name)")
}
.navigationBarTitle(viewModel.list.name)
}
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: ListView, rhs: ListView) -> Bool {
lhs.id == rhs.id
}
}
However, if I put the NavigationView outside the TabView, it displays correctly, but then scrolling the list doesn't shrink the large title text and I have to update the navigationBar title when the page changes -
// AllListsView -
var body: some View {
IfLet($viewModel.listViews) { listViews in
NavigationView {
TabView {
ForEach(listViews, id: \.self) { listView in
listView
.onAppear {
pageName = listView.name
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
.navigationBarTitle(pageName)
}
} else: {
Text("No lists")
}
}
// ListView -
var body: some View {
List(viewModel.listItems) {
Text("\($0.quantity) \($0.name)")
}
}
I'd like to be able to do this in SwiftUI, as it looks like the way ahead. But I know that for me to do this using UIKit would be fairly trivial, so it's frustrating.
Any help much appreciated!
I have a sign out button on a modal sheet that takes the user back to the login screen. To accomplish this I first dismiss the sheet and then, using asyncAfter(deadline:) I set an environment variable that causes the login page to appear. Everything works fine, but once the sheet is dismissed, the transition from the view under the sheet to the login page is pretty jarring. Mostly because there isn't one. The top view just disappears, revealing the login view. I know I can create custom transitions, but I can't figure out where to attach it. Say, for example, I want to fade out the view underneath the sheet. (Although, I'm open to any kind of transition!)
This is the struct that directs the traffic:
struct ConductorView: View {
#EnvironmentObject var tower: Tower
let onboardingCompleted = UserDefaults.standard.bool(forKey: "FirstVisit")
var body: some View {
VStack {
if tower.currentPage == .onboarding {
Onboarding1View()
} else if tower.currentPage == .login {
LoginView()
} else if tower.currentPage == .idle {
LoginView()
}
}.onAppear{
if self.onboardingCompleted {
self.tower.currentPage = .login
} else {
self.tower.currentPage = .onboarding
}
}
}
}
And this is the sign out button on the sheet:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.tower.currentPage = .login
}
}) {
Text("Sign Out")
}
Here is a simplified demo on your replicated code (and I made some longer delay to make it mode visible). Of course you will need to tune it for your needs by changing type of transition or animation, etc. Tested with Xcode 12 / iOS 14
class Tower: ObservableObject {
enum PageType {
case onboarding, login, idle
}
#Published var currentPage: PageType = .onboarding
}
struct ConductorView: View {
#EnvironmentObject var tower: Tower
let onboardingCompleted = false
var body: some View {
VStack {
if tower.currentPage == .onboarding {
Onboarding1View()
} else if tower.currentPage == .login {
Text("LoginView")
.transition(.move(edge: .trailing)) // << here !!
} else if tower.currentPage == .idle {
Text("IdleView")
}
}
.animation(.default, value: tower.currentPage) // << here !!
.onAppear{
if self.onboardingCompleted {
self.tower.currentPage = .login
} else {
self.tower.currentPage = .onboarding
}
}
}
}
struct Onboarding1View: View {
#EnvironmentObject var tower: Tower
#Environment(\.presentationMode) var presentationMode
#State private var isPresented = true
var body: some View {
Text("Login")
.sheet(isPresented: $isPresented) {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.tower.currentPage = .login
}
}) {
Text("Sign Out")
}
}
}
}
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.