SwiftUI doesn't refresh until button action finishes - ios

I want the view to be updated every time #State list gets updated.
But when I use Button to trigger a consecutive update, the list gets updated only after all the consecutive updates are finished.
This code updates the list 10 seconds after the button is pressed.
What should I do to make this code update every second?
#State var i_list: [Int] = []
var body: some View {
VStack {
Button(action: {
for i in 0..<10 {
i_list.append(i)
do {
sleep(1)
}
}
}) {
Text(button_text)
}
List (i_list, id: \.self){ i in
Text(String(i))
}
}
}
Solved:
I solved this problem by updating the list in a different thread.
let globalQueue = DispatchQueue.global()
globalQueue.async {
for i in 0..<10 {
DispatchQueue.main.async {
i_list.append(i)
}
do {
sleep(1)
}
}
}

If you want to see your view updates immediately, instead of using a for loop to update your array, use a publisher. Here I use a timer and add a onReceive(timer) on the List
struct ContentView : View {
#State var i_list: [Int] = []
#State var currentNumber = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
List (i_list, id: \.self) { i in
Text(String(i))
}.onReceive(timer) { _ in
guard currentNumber < 10 else {
return
}
i_list.append(currentNumber)
currentNumber += 1
}
}
}
}
Edit: changed currentNumber to #State

Related

Realm Sync Not Updating SwiftUI View After Observe is Fired

I am having issues with Realm, which is not causing my View to update. I have a checkbox in a List and when I click on the checkbox, I want to update the isCompleted to true. This part is working and it updates the isCompleted property in Realm Sync (MongoDB Atlas). The problem is that the view never re-renders again.
Here is code for my TaskCellView which updates the isCompleted.
struct TaskCellView: View {
var task: Task
let realmManager = RealmManager.shared
var body: some View {
let _ = print(Self._printChanges())
HStack {
Image(systemName: task.isCompleted ? "checkmark.square": "square")
.onTapGesture {
try! realmManager.realm.write {
task.isCompleted.toggle()
}
}
Text(task.title)
}
}
}
In my RealmManager I have setupObservers which fires and assigns the new tasks to the tasks property.
private func setupObservers() {
let observableTasks = realm.objects(Task.self)
notificationToken = observableTasks.observe { [weak self] _ in
DispatchQueue.main.async {
print(observableTasks)
self?.tasks = observableTasks
}
}
}
Then in my ContentView I use the following code to send the tasks to the TodoListView.
if let tasks = realmManager.tasksArray {
let _ = print("INSIDE IF")
let _ = print(tasks.count)
TodoListView(tasks: tasks)
}
I have checked and the print lines are getting printed but the body of the TodoListView is never executed.
TodoListView
struct TodoListView: View {
let tasks: [Task]
var pendingTasks: [Task] {
// tasks.where { $0.isCompleted == false }
tasks.filter { !$0.isCompleted }
}
var completedTasks: [Task] {
tasks.filter { $0.isCompleted }
}
var body: some View {
let _ = print(Self._printChanges())
List {
ForEach(Sections.allCases, id: \.self) { section in
Section {
ForEach(section == .pending ? pendingTasks: completedTasks, id: \._id) { task in
TaskCellView(task: task)
}
} header: {
Text(section.rawValue)
}
}
}.listStyle(.plain)
}
}
I know it is a lot of code and but maybe someone can see the problem. So even though the checkbox is checked and the Realm database is updated and observe is fired and the tasks are updated to reflect the isCompleted new states. The TodoListView is never re-rendered.

Passing a timer to a child view in SwiftUI

In my top level view, I have declared a timer like so:
Struct ContentView: View {
#State var timer = Timer.publish(every: 1, on: .main, in:
.common).autoconnect()
var body: some View {
ZStack {
if self.timerMode == .warmup {
WarmupView(
timer: $timer
)
if self.timerMode == .work {
WorkView(
timer: $timer
)
}
}
}
In a child view, I want to be able to access and update this timer, which will serve as the single source of truth.
Struct WarmupView: View {
#Binding var timer: Publishers.Autoconnect<Timer.TimerPublisher>
#Binding var timeRemaining: Int
var body: some View {
VStack {
Text("\(self.timeRemaining)").font(.system(size: 160))
.onReceive(self.timer) { _ in
if self.timeRemaining > 0 {
self.timeRemaining -= 1
}
}
}
}
}
The timer is publishing to the warmup view without issues, but when timerMode is updated to .work (which has nearly identical code) and the view changes, the timer stops publishing.
Simple as changing the type of your #Binding var timer in your WarmupView to Publishers.Autoconnect<Timer.TimerPublisher>. The .autoconnect() wraps the timer publisher in another publisher, which changes the type.
Here's a simplified version of your code:
struct ContentView: View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var remaining = 100
var body: some View {
Text("\(remaining)")
.font(.system(size: 160))
.onReceive(timer) { _ in
if self.remaining > 0 {
self.remaining -= 1
}
}
}
}

SwiftUI pagination for List object

I've implemented a List with a search bar in SwiftUI. Now I want to implement paging for this list. When the user scrolls to the bottom of the list, new elements should be loaded. My problem is, how can I detect that the user scrolled to the end? When this happens I want to load new elements, append them and show them to the user.
My code looks like this:
import Foundation
import SwiftUI
struct MyList: View {
#EnvironmentObject var webService: GetRequestsWebService
#ObservedObject var viewModelMyList: MyListViewModel
#State private var query = ""
var body: some View {
let binding = Binding<String>(
get: { self.query },
set: { self.query = $0; self.textFieldChanged($0) }
)
return NavigationView {
// how to detect here when end of the list is reached by scrolling?
List {
// searchbar here inside the list element
TextField("Search...", text: binding) {
self.fetchResults()
}
ForEach(viewModelMyList.items, id: \.id) { item in
MyRow(itemToProcess: item)
}
}
.navigationBarTitle("Title")
}.onAppear(perform: fetchResults)
}
private func textFieldChanged(_ text: String) {
text.isEmpty ? viewModelMyList.fetchResultsThrottelt(for: nil) : viewModelMyList.fetchResultsThrottelt(for: text)
}
private func fetchResults() {
query.isEmpty ? viewModelMyList.fetchResults(for: nil) : viewModelMyList.fetchResults(for: query)
}
}
Also a little bit special this case, because the list contains the search bar. I would be thankful for any advice because with this :).
As you have already a List with an artificial row for the search bar, you can simply add another view to the list which will trigger another fetch when it appears on screen (using onAppear() as suggested by Josh). By doing this you do not have to do any "complicated" calculations to know whether a row is the last row... the artificial row is always the last one!
I already used this in one of my projects and I've never seen this element on the screen, as the loading was triggered so quickly before it appeared on the screen. (You surely can use a transparent/invisible element, or perhaps even use a spinner ;-))
List {
TextField("Search...", text: binding) {
/* ... */
}
ForEach(viewModelMyList.items, id: \.id) { item in
// ...
}
if self.viewModelMyList.hasMoreRows {
Text("Fetching more...")
.onAppear(perform: {
self.viewModelMyList.fetchMore()
})
}
}
Add a .onAppear() to the MyRow and have it call the viewModel with the item that just appears. You can then check if its equal to the last item in the list or if its n items away from the end of the list and trigger your pagination.
This one worked for me:
You can add pagination with two different approaches to your List: Last item approach and Threshold item approach.
That's way this package adds two functions to RandomAccessCollection:
isLastItem
Use this function to check if the item in the current List item iteration is the last item of your collection.
isThresholdItem
With this function you can find out if the item of the current List item iteration is the item at your defined threshold. Pass an offset (distance to the last item) to the function so the threshold item can be determined.
import SwiftUI
extension RandomAccessCollection where Self.Element: Identifiable {
public func isLastItem<Item: Identifiable>(_ item: Item) -> Bool {
guard !isEmpty else {
return false
}
guard let itemIndex = lastIndex(where: { AnyHashable($0.id) == AnyHashable(item.id) }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
return distance == 1
}
public func isThresholdItem<Item: Identifiable>(
offset: Int,
item: Item
) -> Bool {
guard !isEmpty else {
return false
}
guard let itemIndex = lastIndex(where: { AnyHashable($0.id) == AnyHashable(item.id) }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
let offset = offset < count ? offset : count - 1
return offset == (distance - 1)
}
}
Examples
Last item approach:
struct ListPaginationExampleView: View {
#State private var items: [String] = Array(0...24).map { "Item \($0)" }
#State private var isLoading: Bool = false
#State private var page: Int = 0
private let pageSize: Int = 25
var body: some View {
NavigationView {
List(items) { item in
VStack(alignment: .leading) {
Text(item)
if self.isLoading && self.items.isLastItem(item) {
Divider()
Text("Loading ...")
.padding(.vertical)
}
}.onAppear {
self.listItemAppears(item)
}
}
.navigationBarTitle("List of items")
.navigationBarItems(trailing: Text("Page index: \(page)"))
}
}
}
extension ListPaginationExampleView {
private func listItemAppears<Item: Identifiable>(_ item: Item) {
if items.isLastItem(item) {
isLoading = true
/*
Simulated async behaviour:
Creates items for the next page and
appends them to the list after a short delay
*/
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.page += 1
let moreItems = self.getMoreItems(forPage: self.page, pageSize: self.pageSize)
self.items.append(contentsOf: moreItems)
self.isLoading = false
}
}
}
}
Threshold item approach:
struct ListPaginationThresholdExampleView: View {
#State private var items: [String] = Array(0...24).map { "Item \($0)" }
#State private var isLoading: Bool = false
#State private var page: Int = 0
private let pageSize: Int = 25
private let offset: Int = 10
var body: some View {
NavigationView {
List(items) { item in
VStack(alignment: .leading) {
Text(item)
if self.isLoading && self.items.isLastItem(item) {
Divider()
Text("Loading ...")
.padding(.vertical)
}
}.onAppear {
self.listItemAppears(item)
}
}
.navigationBarTitle("List of items")
.navigationBarItems(trailing: Text("Page index: \(page)"))
}
}
}
extension ListPaginationThresholdExampleView {
private func listItemAppears<Item: Identifiable>(_ item: Item) {
if items.isThresholdItem(offset: offset,
item: item) {
isLoading = true
/*
Simulated async behaviour:
Creates items for the next page and
appends them to the list after a short delay
*/
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
self.page += 1
let moreItems = self.getMoreItems(forPage: self.page, pageSize: self.pageSize)
self.items.append(contentsOf: moreItems)
self.isLoading = false
}
}
}
}
String Extension:
/*
If you want to display an array of strings
in the List view you have to specify a key path,
so each string can be uniquely identified.
With this extension you don't have to do that anymore.
*/
extension String: Identifiable {
public var id: String {
return self
}
}
Christian Elies, code reference

How do display event cards when push a button

I'm doing reminder IOS app.How do display event card push a button?
event add view.[Home.swift]
[Home.push plus circle button open this view.]1
event card display on Home.[EventList.swift]
[event cards.this cards do display on Home.swift]2
I tried save tapCount to userDefaults.in EventList.swift get tapCount.Because use ForEach scope.
Home.swift
var tapCount = 0
Button(action: {}) {
tapCount += 1
UserDefaults.standard.set.....
}
EventList.swift
var body: some View {
let tapCount: Int16 = UserDefault.standard.Integer(...) as! Int16
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(0 ..< tapCount) { item in
EventView()
}
}
}
}
But not to do.please teach it.
My yesterday solution for using UserDefaults.standart in SwiftUI:
made final class UserData with needed variable;
set this variable from UserDefaults at the init();
set #EnvironmentObject var of UserData class at needed View;
some quick example:
import SwiftUI
final class UserData: ObservableObject {
#Published var tapCount: Int? { // remember, it can be nil at the first launch!
didSet {
UserDefaults.standard.set(tapCount, forKey: "tapCount")
}
}
init() {
// Sure you can set it to 0 with UserDefaults.standard.integer(forKey: "tapCount") ?? 0, but be careful with your ForEach loop
self.tapCount = UserDefaults.standard.integer(forKey: "tapCount")
}
}
// how to use:
struct SomeView: View {
#EnvironmentObject var userData: UserDataSettings
var body: some View {
if self.userData.tapCount == nil {
return AnyView(Text("no taps!"))
} else {
return AnyView(ScrollView(.horizontal, showsIndicators: false) {
HStack {
// you need to check for nil, else you app may crash. this if...else variant should work
ForEach(0 ..< userData.tapCount!) { item in
Text("\(item)")
}
}
})
}
}
}
and the last thing: you need to set .environmentObject(UserData()) when display your view (in SceneDelegate for example, if this is the first view) and then bring this userData variable to other child views)

How to modify/reset #State when other variables are updated in SwiftUI

I would like to reset some #State each time an #ObservedObject is "reassigned". How can this be accomplished?
class Selection: ObservableObject {
let id = UUID()
}
struct ContentView: View {
#State var selectedIndex: Int = 0
var selections: [Selection] = [Selection(), Selection(), Selection()]
var body: some View {
VStack {
// Assume the user can select something so selectedIndex changes all the time
// SwiftUI will just keep updating the same presentation layer with new data
Button("Next") {
self.selectedIndex += 1
if self.selectedIndex >= self.selections.count {
self.selectedIndex = 0
}
}
SelectionDisplayer(selection: self.selections[self.selectedIndex])
}
}
}
struct SelectionDisplayer: View {
#ObservedObject var selection: Selection {
didSet { // Wish there were something *like* this
print("will never happen")
self.tabIndex = 0
}
}
#State var tapCount: Int = 0 // Reset this to 0 when `selection` changes from one object to another
var body: some View {
Text(self.selection.id.description)
.onReceive(self.selection.objectWillChange) {
print("will never happen")
}
Button("Tap Count: \(self.tapCount)") {
self.tapCount += 1
}
}
}
I'm aware of onReceive but I'm not looking to modify state in response to objectWillChange but rather when the object itself is switched out. In UIKit with reference types I would use didSet but that doesn't work here.
I did try using a PreferenceKey for this (gist) but it seems like too much of a hack.
Currently (Beta 5), the best way may be to use the constructor plus a generic ObservableObject for items that I want to reset when the data changes. This allows some #State to be preserved, while some is reset.
In the example below, tapCount is reset each time selection changes, while allTimeTaps is not.
class StateHolder<Value>: ObservableObject {
#Published var value: Value
init(value: Value) {
self.value = value
}
}
struct ContentView: View {
#State var selectedIndex: Int = 0
var selections: [Selection] = [Selection(), Selection(), Selection()]
var body: some View {
VStack {
// Assume the user can select something so selectedIndex changes all the time
// SwiftUI will just keep updating the same presentation though
Button("Next") {
self.selectedIndex += 1
if self.selectedIndex >= self.selections.count {
self.selectedIndex = 0
}
}
SelectionDisplayer(selection: self.selections[self.selectedIndex])
}
}
}
struct SelectionDisplayer: View {
struct SelectionState {
var tapCount: Int = 0
}
#ObservedObject var selection: Selection
#ObservedObject var stateHolder: StateHolder<SelectionState>
#State var allTimeTaps: Int = 0
init(selection: Selection) {
let state = SelectionState()
self.stateHolder = StateHolder(value: state)
self.selection = selection
}
var body: some View {
VStack {
Text(self.selection.id.description)
Text("All Time Taps: \(self.allTimeTaps)")
Text("Tap Count: \(self.stateHolder.value.tapCount)")
Button("Tap") {
self.stateHolder.value.tapCount += 1
self.allTimeTaps += 1
}
}
}
}
While looking for a solution I was quite interested to discover that you cannot initialize #State variables in init. The compiler will complain that all properties have not yet been set before accessing self.
The only way to get this done for me was to force the parent view to redraw the child by temporarily hiding it. It's a hack, but the alternative was to pass a $tapCount in, which is worse since then the parent does not only have to know that it has to be redrawn, but must also know of state inside.
This can probably be refactored into it's own view, which make it not as dirty.
import Foundation
import SwiftUI
import Combine
class Selection {
let id = UUID()
}
struct RedirectStateChangeView: View {
#State var selectedIndex: Int = 0
#State var isDisabled = false
var selections: [Selection] = [Selection(), Selection(), Selection()]
var body: some View {
VStack {
// Assume the user can select something so selectedIndex changes all the time
// SwiftUI will just keep updating the same presentation layer with new data
Button("Next") {
self.selectedIndex += 1
if self.selectedIndex >= self.selections.count {
self.selectedIndex = 0
}
self.isDisabled = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.005) {
self.isDisabled = false
}
}
if !isDisabled {
SelectionDisplayer(selection: selections[selectedIndex])
}
}
}
}
struct SelectionDisplayer: View {
var selection: Selection
#State var tapCount: Int = 0 // Reset this to 0 when `selection` changes from one object to another
var body: some View {
VStack{
Text(selection.id.description)
Button("Tap Count: \(self.tapCount)") {
self.tapCount += 1
}
}
}
}
This does what you want I believe:
class Selection: ObservableObject { let id = UUID() }
struct ContentView: View {
#State var selectedIndex: Int = 0
var selections: [Selection] = [Selection(), Selection(), Selection()]
#State var count = 0
var body: some View {
VStack {
Button("Next") {
self.count = 0
self.selectedIndex += 1
if self.selectedIndex >= self.selections.count {
self.selectedIndex = 0
}
}
SelectionDisplayer(selection: self.selections[self.selectedIndex], count: $count)
}
}
}
struct SelectionDisplayer: View {
#ObservedObject var selection: Selection
#Binding var count: Int
var body: some View {
VStack {
Text("\(self.selection.id)")
Button("Tap Count: \(self.count)") { self.count += 1 }
}
}
}
My Xcode didn't like your code so I needed to make a few other changes than just moving the count into the parent

Resources