swift reaching bottom of scroll view not working right - swift

Hello I have a scroll view and I would like to know when the user swipes to the bottom of it so that I can populate more data into it. The code I have below isn't working quite right. When the sim launches "hello" is printed about 5 times, and when I barely move it "hello" is printed like 10 more times, by the time I get to the bottom of the scroll view, "Hello" was printed like 100 times. How can I fix my code so that Hello is only printed when the bottom is reached.
import SwiftUI
import UIKit
struct FeedView: View {
#State private var scrollViewHeight = CGFloat.infinity
#Namespace private var scrollViewNameSpace
var body: some View {
mainInterFaceView
}
}
extension FeedView{
var mainInterFaceView: some View{
ZStack(){
ScrollView {
LazyVStack {
ForEach(viewModel.tweets) { tweet in
Button {
} label: {
someView()
.background(
GeometryReader { proxy in
Color.clear
.onChange(of: proxy.frame(in: .named(scrollViewNameSpace))) { newFrame in
if newFrame.minY < scrollViewHeight {
print("called")
}
}
}
)
}
}
}
}
.background(
GeometryReader { proxy in
Color.clear
.onChange(of: proxy.size, perform: { newSize in
let _ = print("ScrollView: ", newSize)
scrollViewHeight = newSize.height
})
}
)
.coordinateSpace(name: scrollViewNameSpace)
}
}
}

There's a much simpler way to do this than to use GeometryReaders, and to try and calculate the offset.
If more pages of data are available, you just need to display a view (ProgressView works nicely) at the bottom of your VStack, and then load more data when it appears (using .task modifier)
struct Tweet: Identifiable {
let id = UUID()
let text: String
}
#MainActor
final class TweetModel: ObservableObject {
private let pageSize = 20
private (set) var tweets: [Tweet] = []
private (set) var moreTweetsAvailable = false
private var nextPage = 0
func loadFirstPage() async {
nextPage = 0
await loadPage()
}
func loadNextPage() async {
await loadPage()
}
private func loadPage() async {
let tweets = await // load tweets for nextPage
moreTweetsAvailable = tweets.count == pageSize
self.tweets.append(contentsOf: tweets)
nextPage += 1
}
struct ContentView: View {
#StateObject var tweetModel = TweetModel()
var body: some View {
ScrollView {
LazyVStack {
ForEach(tweetModel.tweets) { tweet in
Text(tweet.text)
}
if tweetModel.moreTweetsAvailable {
ProgressView()
.task {
await tweetModel.loadNextPage()
}
}
}
}
.task {
await tweetModel.loadFirstPage()
}
}
}

Related

I create an instance of 3 different view models and assign each to a state object. How do I place this into a dispatch queue?

I have a Swift program that works as desired. I have 3 view models that each call a separate model. Each model calls a function that reads a separate large CSV file, performs some manipulation and returns a data frame. This takes some time and I would like to speed things up.
Swift offers a DispatchQueue that allows one to place code into an asynchronous global queue with QOS and I believe if I ran the creation of the view models in this fashion, I would display the initial view sooner.
The problem is: I have no idea how to incorporate it. Any help to point me in the right direction will be appreciated.
Below is my content view, one view model, and one model. The test dispatch queue code at the end runs successfully in a playground.
struct ContentView: View {
#StateObject var vooVM: VOOViewModel = VOOViewModel()
#StateObject var vfiaxVM: VFIAXViewModel = VFIAXViewModel()
#StateObject var principalVM: PrincipalViewModel = PrincipalViewModel()
#State private var selectedItemId: Int?
var body: some View {
NavigationView {
VStack (alignment: .leading) {
List {
Spacer()
.frame(height: 20)
Group {
Divider()
NavigationLink(destination: Summary(vooVM: vooVM, vfiaxVM: vfiaxVM, prinVM: principalVM), tag: 1, selection: $selectedItemId, label: {
HStack {
Image(systemName: "house")
.padding(.leading, 10)
.padding(.trailing, 0)
.padding(.bottom, 5)
Text("Summary")
.bold()
.padding(.bottom, 2)
} // end h stack
})
} // end group
NavigationLinks(listText: "VOO", dataFrame1: vooVM.timeSeriesDailyDF1, dataFrame5: vooVM.timeSeriesDailyDF5)
NavigationLinks(listText: "VFIAX", dataFrame1: vfiaxVM.timeSeriesDailyDF1, dataFrame5: vfiaxVM.timeSeriesDailyDF5)
NavigationLinks(listText: "Principal", dataFrame1: principalVM.timeSeriesDailyDF1, dataFrame5: principalVM.timeSeriesDailyDF5)
Divider()
Spacer()
} // end list
} // end v stack
} // end navigation view
.onAppear {self.selectedItemId = 1}
.navigationTitle("Stock Data")
.frame(width: 1200, height: 900, alignment: .center)
} // end body view
} // end content view
View Model
class VOOViewModel: ObservableObject {
#Published private var vooModel: VOOModel = VOOModel()
var timeSeriesDailyDF1: DataFrame {
return vooModel.vooDF.0
}
var timeSeriesDailyDF5: DataFrame {
return vooModel.vooDF.1
}
var symbol: String {
return vooModel.symbol
}
var currentShares: Double {
return vooModel.currentShares
}
var currentSharePrice: Double {
let lastRowIndex: Int = vooModel.vooDF.0.shape.rows - 1
let currentPrice: Double = (vooModel.vooDF.0[row: lastRowIndex])[1] as! Double
return currentPrice
}
var percentGain: Double {
let pastValue: Double = (vooModel.vooDF.0[row: 0])[1] as! Double
let numRows: Int = vooModel.vooDF.0.shape.rows - 1
let curValue: Double = (vooModel.vooDF.0[row: numRows])[1] as! Double
let oneYearGain: Double = (100 * (curValue - pastValue)) / pastValue
return oneYearGain
}
}
Model
struct VOOModel {
var vooDF = GetDF(fileName: "FormattedVOO")
let symbol: String = "VOO"
let currentShares: Double = 1
}
Playground Code
let myQue = DispatchQueue.global()
let myGroup = DispatchGroup()
myQue.async(group: myGroup) {
sleep(5)
print("Task 1 complete")
}
myQue.async(group: myGroup) {
sleep(3)
print("Task 2 complete")
}
myGroup.wait()
print("All tasks completed")
I was able to solve my problem by using only 1 viewmodel instead of 3. The viewmodel calls all three models which were modified such that their function call to read a CSV file and place it into a dataframe is contained in a function. This function is in turn called within a function in the viewmodel which is called in the viewmodels init. Below is the updated code. Note that the ContentView was simplified to make testing easy.
New Content View:
struct ContentView: View {
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
let printValue1 = (viewModel.dataFrames.0.0[row: 0])[0]
let tempValue = (viewModel.dataFrames.0.0[row: 0])[1] as! Double
let tempValueFormatted: String = String(format: "$%.2f", tempValue)
Text("\(dateToStringFormatter.string(from: printValue1 as! Date))" + " " + tempValueFormatted )
.frame(width: 1200, height: 900, alignment: .center)
}
}
New ViewModel:
class ViewModel: ObservableObject {
#Published private var vooModel: VOOModel = VOOModel()
#Published private var vfiaxModel: VFIAXModel = VFIAXModel()
#Published private var principalModel: PrincipalModel = PrincipalModel()
var dataFrames = ((DataFrame(), DataFrame()), (DataFrame(), DataFrame()), (DataFrame(), DataFrame()))
init() {
self.dataFrames = GetDataFrames()
}
func GetDataFrames() -> ((DataFrame, DataFrame), (DataFrame, DataFrame), (DataFrame, DataFrame)) {
let myQue: DispatchQueue = DispatchQueue.global()
let myGroup: DispatchGroup = DispatchGroup()
var vooDF = (DataFrame(), DataFrame())
var vfiaxDF = (DataFrame(), DataFrame())
var principalDF = (DataFrame(), DataFrame())
myQue.async(group: myGroup) {
vfiaxDF = self.vfiaxModel.GetData()
}
myQue.async(group: myGroup) {
principalDF = self.principalModel.GetData()
}
myQue.async(group: myGroup) {
vooDF = self.vooModel.GetData()
}
myGroup.wait()
return (vooDF, vfiaxDF, principalDF)
}
}
One of the new models. The other 2 are identical except for the CSV file they read.
struct VOOModel {
let symbol: String = "VOO"
let currentShares: Double = 1
func GetData() -> (DataFrame, DataFrame) {
let vooDF = GetDF(fileName: "FormattedVOO")
return vooDF
}
}

SwiftUI - Changing parent #State doesn't update child View

Specific question:
SwiftUI doesn't like us initializing #State using parameters from the parent, but what if the parent holding that #State causes major performance issues?
Example:
How do I make tapping on the top text change the slider to full/empty?
Dragging the slider correctly communicates upwards when the slider changes from full to empty, but tapping the [Overview] full: text doesn't communicate downwards that the slider should change to full/empty.
I could store the underlying Double in the parent view, but it causes major lag and seems unnecessary.
import SwiftUI
// Top level View. It doesn't know anything about specific slider percentages,
// it only knows if the slider got moved to full/empty
struct SliderOverviewView: View {
// Try setting this to true and rerunning.. It DOES work here?!
#State var overview = OverviewModel(state: .empty)
var body: some View {
VStack {
Text("[Overview] full: \(overview.state.rawValue)")
.onTapGesture { // BROKEN: should update child..
switch overview.state {
case .full, .between: overview.state = .empty
case .empty: overview.state = .full
}
}
SliderDetailView(overview: $overview)
}
}
}
// Bottom level View. It knows about specific slider percentages and only
// communicates upwards when percentage goes to 0% or 100%.
struct SliderDetailView: View {
#State var details: DetailModel
init(overview: Binding<OverviewModel>) {
details = DetailModel(overview: overview)
}
var body: some View {
VStack {
Text("[Detail] percentFull: \(details.percentFull)")
Slider(value: $details.percentFull)
.padding(.horizontal, 48)
}
}
}
// Top level model that only knows if slider went to 0% or 100%
struct OverviewModel {
var state: OverviewState
enum OverviewState: String {
case empty
case between
case full
}
}
// Lower level model that knows full slider percentage
struct DetailModel {
#Binding var overview: OverviewModel
var percentFull: Double {
didSet {
if percentFull == 0 {
overview.state = .empty
} else if percentFull == 1 {
overview.state = .full
} else {
overview.state = .between
}
}
}
init(overview: Binding<OverviewModel>) {
_overview = overview
// set inital percent
switch overview.state.wrappedValue {
case .empty:
percentFull = 0.0
case .between:
percentFull = 0.5
case .full:
percentFull = 1.0
}
}
}
struct SliderOverviewView_Previews: PreviewProvider {
static var previews: some View {
SliderOverviewView()
}
}
Why don't I just store percentFull in the OverviewModel?
I'm looking for a pattern so my top level #State struct doesn't need to know EVERY low level detail specific to certain Views.
Running the code example is the clearest way to see my problem.
This question uses a contrived example where an Overview only knows if the slider is full or empty, but the Detail knows what percentFull the slider actually is. The Detail has very detailed control and knowledge of the slider, and only communicates upwards to the Overview when the slider is 0% or 100%
What's my specific case for why I need to do this?
For those curious, my app is running into performance issues because I have several gestures that give the user control over progress. I want my top level ViewModel to store if the gesture is complete or not, but it doesn't need to know the specifics of how far the user has swiped. I'm trying to hide this specific progress Double from my higher level ViewModel to improve app performance.
Here is working, simplified and refactored answer for your issue:
struct ContentView: View {
var body: some View {
SliderOverviewView()
}
}
struct SliderOverviewView: View {
#State private var overview: OverviewModel = OverviewModel(full: false)
var body: some View {
VStack {
Text("[Overview] full: \(overview.full.description)")
.onTapGesture {
overview.full.toggle()
}
SliderDetailView(overview: $overview)
}
}
}
struct SliderDetailView: View {
#Binding var overview: OverviewModel
var body: some View {
VStack {
Text("[Detail] percentFull: \(tellValue(value: overview.full))")
Slider(value: Binding(get: { () -> Double in
return tellValue(value: overview.full)
}, set: { newValue in
if newValue == 1 { overview.full = true }
else if newValue == 0 { overview.full = false }
}))
}
}
func tellValue(value: Bool) -> Double {
if value { return 1 }
else { return 0 }
}
}
struct OverviewModel {
var full: Bool
}
Update:
struct SliderDetailView: View {
#Binding var overview: OverviewModel
#State private var sliderValue: Double = Double()
var body: some View {
VStack {
Text("[Detail] percentFull: \(sliderValue)")
Slider(value: $sliderValue, in: 0.0...1.0)
}
.onAppear(perform: { sliderValue = tellValue(value: overview.full) })
.onChange(of: overview.full, perform: { newValue in
sliderValue = tellValue(value: newValue)
})
.onChange(of: sliderValue, perform: { newValue in
if newValue == 1 { overview.full = true }
else { overview.full = false }
})
}
func tellValue(value: Bool) -> Double {
value ? 1 : 0
}
}
I present here a clean alternative using 2 ObservableObject, a hight level OverviewModel that
only deal with if slider went to 0% or 100%, and a DetailModel that deals only with the slider percentage.
Dragging the slider correctly communicates upwards when the slider changes from full to empty, and
tapping the [Overview] full: text communicates downwards that the slider should change to full/empty.
import Foundation
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var overview = OverviewModel()
var body: some View {
SliderOverviewView().environmentObject(overview)
}
}
// Top level View. It doesn't know anything about specific slider percentages,
// it only cares if the slider got moved to full/empty
struct SliderOverviewView: View {
#EnvironmentObject var overview: OverviewModel
var body: some View {
VStack {
Text("[Overview] full: \(overview.state.rawValue)")
.onTapGesture {
switch overview.state {
case .full, .between: overview.state = .empty
case .empty: overview.state = .full
}
}
SliderDetailView()
}
}
}
// Bottom level View. It knows about specific slider percentages and only
// communicates upwards when percentage goes to 0% or 100%.
struct SliderDetailView: View {
#EnvironmentObject var overview: OverviewModel
#StateObject var details = DetailModel()
var body: some View {
VStack {
Text("[Detail] percentFull: \(details.percentFull)")
Slider(value: $details.percentFull).padding(.horizontal, 48)
.onChange(of: details.percentFull) { newVal in
switch newVal {
case 0: overview.state = .empty
case 1: overview.state = .full
default: break
}
}
}
// listen for the high level OverviewModel changes
.onReceive(overview.$state) { theState in
details.percentFull = theState == .full ? 1.0 : 0.0
}
}
}
enum OverviewState: String {
case empty
case between
case full
}
// Top level model that only knows if slider went to 0% or 100%
class OverviewModel: ObservableObject {
#Published var state: OverviewState = .empty
}
// Lower level model that knows full slider percentage
class DetailModel: ObservableObject {
#Published var percentFull = 0.0
}

Local variables in view body

In a SwiftUI View's body, I store intensive work in a local variable, which I use in its subviews (Like through a context menu).
I do this to avoid doing the expensive work multiple times per view refresh, if I use the variable multiple times in the view body.
Here is an example of what I'm doing:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
let num = viewModel.expensiveWork
Text("Number: \(num)")
.contextMenu {
Button("Press me \(num)") {
viewModel.doWork()
}
}
.frame(width: 150, height: 100)
}
}
class ViewModel: ObservableObject {
#Published var num = 0
var expensiveWork: Int { num * 20 }
func doWork() {
num = Int.random(in: 0..<100)
}
}
It works fine right now, but I was just wondering if this is good practice, and if it could cause any desynchronization issues.
I apologize if this is a stupid question.
Because expensiveWork is a computed property and you are calling it in the view body, you are still doing the work every time the view body gets recomputed.
Code:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Text("Number: \(viewModel.expensiveWork)")
.contextMenu {
Button("Press me \(viewModel.expensiveWork)") {
viewModel.doWork()
}
}
.frame(width: 150, height: 100)
}
}
class ViewModel: ObservableObject {
#Published var num = 0
#Published var expensiveWork = 0
func doWork() {
num = Int.random(in: 0..<100)
Task {
// Intensive work here while it is asynchronous
let result = num * 20
await MainActor.run {
expensiveWork = result
}
}
}
}
Can it cause desynchronisation issues?
Yes - num and expensiveWork may be set at different times. To avoid this, change doWork() to the below code:
func doWork() {
let temp = Int.random(in: 0..<100)
Task {
// Intensive work here while it is asynchronous
let result = temp * 20
await MainActor.run {
num = temp
expensiveWork = result
}
}
}
This comes at the trade-off that now num won't update until the expensive work is done.
Do the expensive work in the model and reference it. Structs are disposable; everything gets reinitialized all of the time and you can't control that.
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Text("Number: \(viewModel.expensiveWork)")
.contextMenu {
Button("Press me \(viewModel.expensiveWork)") {
viewModel.doWork()
}
}
.frame(width: 150, height: 100)
}
}
class ViewModel: ObservableObject {
#Published var num = 0
var expensiveWork: Int = 0
func doWork() {
num = Int.random(in: 0..<100)
expensiveWork = num * 20
}
}

SwiftUI: Published string changes inside view model are not updating view

I have a timer inside my view model class which every second changes two #Published strings inside the view model. View model class is an Observable Object which Observed by the view but the changes to these string objects are not updating my view.
I have a very similar structure in many other views(Published variables inside a ObservableObject which is observed by view) and it always worked. I can't seem to find what am I doing wrong?
ViewModel
final class QWMeasurementViewModel: ObservableObject {
#Published var measurementCountDownDetails: String = ""
#Published var measurementCountDown: String = ""
private var timer: Timer?
private var scheduleTime = 0
func setTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
DispatchQueue.main.async {
self.scheduleTime += 1
if self.scheduleTime == 1 {
self.measurementCountDownDetails = "Get ready"
self.measurementCountDown = "1"
}
else if self.scheduleTime == 2 {
self.measurementCountDownDetails = "Relax your arm"
self.measurementCountDown = "2"
}
else if self.scheduleTime == 3 {
self.measurementCountDownDetails = "Breathe"
self.measurementCountDown = "3"
}
else if self.scheduleTime == 4 {
self.measurementCountDownDetails = ""
self.measurementCountDown = ""
timer.invalidate()
}
}
}
}
}
View
struct QWMeasurementView: View {
#ObservedObject var viewModel: QWMeasurementViewModel
var body: some View {
VStack {
Text(viewModel.measurementCountDownDetails)
.font(.body)
Text(viewModel.measurementCountDown)
.font(.title)
}
.onAppear {
viewModel.setTimer()
}
}
}
Edit
After investigation, this seems to be related to how it is being presented. Cause if it's a single view this code works but I am actually presenting this as a sheet. (Still cannot understand why would it make a difference..)
struct QWBPDStartButtonView: View {
#ObservedObject private var viewModel: QWBPDStartButtonViewModel
#State private var startButtonPressed: Bool = false
init(viewModel: QWBPDStartButtonViewModel) {
self.viewModel = viewModel
}
var body: some View {
Button(action: {
self.startButtonPressed = true
}) {
ZStack {
Circle()
.foregroundColor(Color("midGreen"))
Text("Start")
.font(.title)
}
}
.buttonStyle(PlainButtonStyle())
.sheet(isPresented: $startButtonPressed) {
QWMeasurementView(viewModel: QWMeasurementViewModel())
}
}
}
You’re passing in a brand new viewmodel to the sheet’s view.
Try passing in the instance from line 3

What is the best way to switch views in SwiftUI?

I have tried several options to switch views in SwiftUI. However, each one had issues like lagging over time when switching back and forth many times. I am trying to find the best and cleanest way to switch views using SwiftUI. I am just trying to make a multiview user interface.
In View1.swift:
import SwiftUI
struct View1: View {
#State var GoToView2:Bool = false
var body: some View {
ZStack {
if (GoToView2) {
View2()
//What should I do if I created another swiftui view under the name View2?
//Just calling View2() like that causes lag as described in the linked question before it was deleted, if from view2 I switch back to view1 and so on.
//If I directly put the code of View2 here, then adding other views would get too messy.
} else {
VStack {
Button(action: {self.GoToView2.toggle()}) {
Text("Go to view 2")
}
}
}
}
}
}
In View2.swift:
import SwiftUI
struct View2: View {
#State var GoToView1:Bool = false
var body: some View {
ZStack {
if (GoToView1) {
View1()
} else {
VStack {
Button(action: {self.GoToView1.toggle()}) {
Text("Go to view 1")
}
}
}
}
}
}
I hope the problem can be understood. To replicate the behavior, please compile the code in a SwiftUI app, then switch be repeatedly switching between the two buttons quickly for 30 seconds, then you should notice a delay between each switch, and resizing the window should look chunky. I am using the latest version of macOS and the latest version of Xcode.
So I tried to show that each of the calls to the Views would add an instance to the view stack... I might be wrong here but the following should show this:
struct View1: View {
#State var GoToView2:Bool = false
var counter: Int
init(counter: Int) {
self.counter = counter + 1
}
var body: some View {
VStack {
if (GoToView2) {
Text("\(self.counter)")
View2(counter: self.counter)
} else {
VStack {
Button(action: {
withAnimation {
self.GoToView2.toggle()
}
}) {
Text("Go to view 2")
}
}
}
}
}
}
struct View2: View {
#State var GoToView1:Bool = false
var counter: Int
init(counter: Int) {
self.counter = counter + 1
}
var body: some View {
VStack {
if (GoToView1) {
Text("\(self.counter)")
View1(counter: self.counter)
} else {
VStack {
Button(action: {
withAnimation {
self.GoToView1.toggle()
}
}) {
Text("Go to view 1")
}
}.transition(.move(edge: .leading))
}
}
}
}
The I tried to show that the other method wouldn't do that:
struct View1: View {
#State var GoToView2: Bool = false
var counter: Int
init(counter: Int) {
self.counter = counter + 1
}
var body: some View {
VStack {
if (GoToView2) {
Text("\(self.counter)")
View2(counter: self.counter, GoToView1: self.$GoToView2)
} else {
VStack {
Button(action: {
withAnimation {
self.GoToView2.toggle()
}
}) {
Text("Go to view 2")
}
}
}
}
}
}
struct View2: View {
#Binding var GoToView1: Bool
var counter: Int
init(counter: Int, GoToView1: Binding<Bool>) {
self._GoToView1 = GoToView1
self.counter = counter + 1
}
var body: some View {
VStack {
Text("\(self.counter)")
Button(action: {
withAnimation {
self.GoToView1.toggle()
}
}) {
Text("Go to view 1")
}
}.transition(.move(edge: .leading))
}
}
I don't know if the lag is really coming from this or if there is a better method of proof, but for now this is what I came up with.
Original answer
I would recommend doing the following:
struct View1: View {
#State var GoToView2:Bool = false
var body: some View {
ZStack {
if (GoToView2) {
View2(GoToView1: self.$GoToView2)
} else {
VStack {
Button(action: {
withAnimation {
self.GoToView2.toggle()
}
}) {
Text("Go to view 2")
}
}
}
}
}
}
struct View2: View {
#Binding var GoToView1: Bool
var body: some View {
VStack {
Button(action: {
withAnimation {
self.GoToView1.toggle()
}
}) {
Text("Go to view 1")
}
}.transition(.move(edge: .leading))
}
}