Unable to successfully connect ViewModel data to Table View - swiftui

The Apple Documentation and all tutorials on Table usage that I've found (example 1 & example 2) build Tables from non-dynamic model arrays.
I have a ViewModel that contains a computed property array (userDataList) which utilized functions and additional computed properties. I'm having difficulty writing the code in the View to make the Table iterate through each of the lines of the userDataList array.
The desired outcome is a Table that has 5 rows (Years 1 through 5) and 3 columns (Year, Annual Income, & 401k Balance) with the Table populated with the appropriate data from the ViewModel userDataList array.
Here is the code:
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
VStack(spacing: 15){
TabView() {
ProfileView()
.tabItem {
Image(systemName: "square.and.pencil")
Text("Profile")
}
InvestView()
.tabItem {
Image(systemName: "dollarsign")
Text("Tips")
}
}
.environmentObject(vm)
}
}
}
struct ProfileView: View {
var body: some View {
Text("Hello, World!")
}
}
//the ProfileView will eventually have a Form that allows the user to manipulate the values within the userData Published variable within the ViewModel
struct EmployeeInfo: Codable, Identifiable {
var id = UUID()
var monthlyHours: Double
var hourlyRateYear1: Double
var hourlyRateYear2: Double
var hourlyRateYear3: Double
var hourlyRateYear4: Double
var hourlyRateYear5: Double
var startingBalance401k: Double
var contribution: Double
var balance401k: Double
}
struct InvestInfo: Codable, Identifiable {
var id = UUID()
var year: Int
var annualIncome: Double
var balance401k: Double
}
struct Year: Codable, Identifiable {
var id = UUID()
var year: Int
}
struct InvestView: View {
#EnvironmentObject var vm: ViewModel
private let currencyStyle = Decimal.FormatStyle.Currency(code:"USD")
var body: some View {
Table(of: vm.userDataList) {
TableColumn("Years Until Retirement") { item in
Text(item.year)
}
TableColumn("Annual Income") { item in
Text(item.annualIncome), format: currencyStyle)
}
TableColumn("401k Balance") { item in
Text(item.balance401k), format: currencyStyle)
}
} rows: {
TableRow(Year(year: 1))
TableRow(Year(year: 2))
TableRow(Year(year: 3))
TableRow(Year(year: 4))
TableRow(Year(year: 5))
}
}
}
class ViewModel: ObservableObject {
#Published var userData: EmployeeInfo = EmployeeInfo(monthlyHours: 160.0, hourlyRateYear1: 15.0, hourlyRateYear2: 15.0, hourlyRateYear3: 17.0, hourlyRateYear4: 17.0, hourlyRateYear5: 19.0, startingBalance401k: 5000.0, contribution: 5000.0, balance401k: 10000.0)
func calcAnnualEarnings(hourlyRate: Double) -> Double {
return (userData.monthlyHours * hourlyRate) * 12
}
var annualIncomeYear1: Double {
calcAnnualEarnings(hourlyRate: userData.hourlyRateYear1)
}
var annualIncomeYear2: Double {
calcAnnualEarnings(hourlyRate: userData.hourlyRateYear2)
}
var annualIncomeYear3: Double {
calcAnnualEarnings(hourlyRate: userData.hourlyRateYear3)
}
var annualIncomeYear4: Double {
calcAnnualEarnings(hourlyRate: userData.hourlyRateYear4)
}
var annualIncomeYear5: Double {
calcAnnualEarnings(hourlyRate: userData.hourlyRateYear5)
}
func calc401k(princ: Double, deposits: Double) -> Double {
let newRate = 10.0
let rn1 = 1+((newRate/100)/12)
let comp = pow(rn1,12)
let totPrinc = comp * princ
return totPrinc + ((deposits/12) * (comp-1)/((newRate/100)/12))
}
var balanceLine1: Double {
return calc401k(princ:
userData.startingBalance401k, deposits:
userData.contribution)
}
var balanceLine2: Double {
return calc401k(princ:
balanceLine1, deposits:
userData.contribution)
}
var balanceLine3: Double {
return calc401k(princ:
balanceLine2, deposits:
userData.contribution)
}
var balanceLine4: Double {
return calc401k(princ:
balanceLine3, deposits:
userData.contribution)
}
var balanceLine5: Double {
return calc401k(princ:
balanceLine4, deposits:
userData.contribution)
}
var userDataList: [InvestInfo] {
return [
InvestInfo(year: 1, annualIncome: annualIncomeYear1, balance401k: balanceLine1),
InvestInfo(year: 2, annualIncome: annualIncomeYear2, balance401k: balanceLine2),
InvestInfo(year: 3, annualIncome: annualIncomeYear3, balance401k: balanceLine3),
InvestInfo(year: 4, annualIncome: annualIncomeYear4, balance401k: balanceLine4),
InvestInfo(year: 5, annualIncome: annualIncomeYear5, balance401k: balanceLine5)
]
}
}
There errors I receive are within the InvestView:
As you can see, the errors include:
Cannot convert value of type '[InvestInfo]' to expected argument type 'TableRow.TableRowValue.Type' (aka 'Year.Type')
No exact matches in call to initializer '\
Trailing closure passed to parameter of type 'KeyPath<TableRow.TableRowValue, String>' (aka 'KeyPath<Year, String>') that does not accept a closure
(Number 3 repeated)
I've tried to treat the Table(of: as a typical ForEach but I am obviously missing something.

try something like this example code, to remove the cascade of errors:
struct InvestView: View {
#EnvironmentObject var vm: ViewModel
private let currencyStyle = Decimal.FormatStyle.Currency(code:"USD")
var body: some View {
Table(vm.userDataList) {
TableColumn("Years Until Retirement") { item in
Text("\(item.year)")
}
TableColumn("Annual Income") { item in
Text(Decimal(item.annualIncome), format: currencyStyle)
}
TableColumn("401k Balance") { item in
Text(Decimal(item.balance401k), format: currencyStyle)
}
}
}
}

Related

List row reset value

I have a List, with custom Stepper inside each row. Therefore, when I scroll my stepper is reset. (The increment and decrement works when is visible. When it disappear, it's reset. Don't keep the state. It's alway's reset).
Xcode: v14.2 / Simulator iOS: 16.2
struct Product: Codable, Hashable, Identifiable {
let id: String
let name: String
let step: Int
let quantity: Int
let priceHT: Double
}
class ProductViewModel: ObservableObject {
#Published var products = [Product]()
...
}
struct ProductListView: View {
#EnvironmentObject var productViewModel: ProductViewModel
var body: some View {
List(productViewModel.products) { product in
ProductRowView(product: product)
  }
}
}
My List row:
I tried to modify #State with #binding, but without success.
struct ProductRowView: View {
#State var product: Product
var body: some View {
HStack {
VStack {
Text(product.name)
Text(String(format: "%.2f", product.priceHT) + "€ HT")
}
Spacer()
MyStepper(product: $product, value: product.quantity)
.font(.title)
}
}
}
My Custom stepper:
struct MyStepper: View {
#Binding var product: Product
#State var value: Int = 0
var body: some View {
HStack {
VStack() {
HStack {
Button(action: {
value -= product.step
if let row = orderViewModel.productsOrder.firstIndex(where: { $0.name == product.name }) {
let order = Product(id: product.id, name: product.name, step: product.step, quantity: value, priceHT: product.priceHT)
if (value == 0) {
orderViewModel.productsOrder.remove(at: row)
} else {
orderViewModel.productsOrder[row] = order
}
}
}, label: {
Image(systemName: "minus.square.fill")
})
Text(value.formatted())
Button(action: {
value += product.step
let order = Product(id: product.id, name: product.name, step: product.step, quantity: value, priceHT: product.priceHT)
if let row = orderViewModel.productsOrder.firstIndex(where: { $0.name == product.name }) {
orderViewModel.productsOrder[row] = order
} else {
orderViewModel.productsOrder.append(order)
}
}, label: {
Image(systemName: "plus.app.fill")
})
}
Text(product.unit)
}
}
}
}
Thks
EDIT / RESOLVED
Here is the solution for my case :
Change type of quantity. let to var
struct Product: Codable, Hashable, Identifiable {
...
var quantity: Int
...
}
Delete #State in MyStepper and replace value by product.quantity
Use bindings for that, e.g.
struct ProductListView: View {
#EnvironmentObject var model: Model
var body: some View {
List($model.products) { $product in
ProductRowView(product: $product)
}
}
}
struct ProductRowView: View {
#Binding var product: Product // now you have write access to the Product struct
...
However, to make the View reusable and to help with previewing, it's best to pass in only the simple types the View needs, e.g.
struct TitlePriceView: View {
let title: String
#Binding var price: Double
// etc.
TitlePriceView(title: product.title, price: $product.price)

SwiftUICharts are not redrawn when given new data

I am adding the possibility to swipe in order to update a barchart. What I want to show is statistics for different station. To view different station I want the user to be able to swipe between the stations. I can see that the swiping works and each time I swipe I get the correct data from my controller. The problem is that my view is not redrawn properly.
I found this guide, but cannot make it work.
Say I swipe right from station 0 with data [100, 100, 100] to station 2, the retrieved data from my controller is [0.0, 100.0, 0.0]. The view I have still is for [100, 100, 100]`.
The station number is correctly updated, so I suspect it needs some state somehow.
Here is the code:
import SwiftUI
import SwiftUICharts
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName()).tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
struct BarCharts: View {
var data: [Double]
var title: String
init(data: [Double], disciplineName: String) {
self.data = data
title = disciplineName
print(data)
}
var body: some View {
VStack {
BarChartView(data: ChartData(points: self.data), title: self.title, style: Styles.barChartStyleOrangeLight, form: CGSize(width: 300, height: 400))
}
}
}
class ViewModel: ObservableObject {
#Published var station = 1
let controller = DetailedViewController()
var isPreview = false
func getData(kLatestRounds: Int, station: Int) -> [Double] {
if isPreview {
return [100.0, 100.0, 100.0]
} else {
let data = controller.getResults(kLatestRounds: kLatestRounds, station: station, fileName: userDataFile)
return data
}
}
func getName() -> String {
controller.getDiscipline().name
}
func getNumberOfStations() -> Int {
controller.getDiscipline().getNumberOfStations()
}
func getStation() -> Int {
station
}
func incrementStation() {
station = (station + 1) % getNumberOfStations()
}
func decrementStation() {
station -= 1
if station < 0 {
station = getNumberOfStations() - 1
}
}
}
The data is printed inside the constructor each time I swipe. Shouldn't that mean it should be updated?
I don’t use SwiftUICharts so I can’t test it, but the least you can try is manually set the id to the view
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName())
.id(viewmodel.station) // here. If it doesn’t work, you can set it to the whole TabView
.tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}

(SwiftUI) How to refresh the count down

I want the count down to restart when I enter next quiz. Now the count down just continues. Do you know how to fix this problem? Thank you. Following is the reproducible code.
TemplateView.swift
Since the header and the footer is same for all quizes. I am using the template view to avoid redundancy.
import SwiftUI
struct TemplateView: View {
#ObservedObject var model = TemplateViewModel()
var body: some View {
VStack {
InfoView(model: model)
Text(model.getContent(idx: model.currentIdx))
Image(systemName: "arrowshape.turn.up.right.fill")
.onTapGesture {
if model.currentIdx >= model.data.count-1 {
model.currentIdx = 0
} else {
model.currentIdx += 1
}
}
}
}
}
TemplateViewModel
import Foundation
import SwiftUI
class TemplateViewModel: ObservableObject {
#Published var currentIdx: Int = 0
var data: [QuizModel] = [
QuizModel(prepareSeconds: 10, content: "Answer the short question"),
QuizModel(prepareSeconds: 20, content: "Listen to the audio"),
QuizModel(prepareSeconds: 30, content: "Read the article"),
]
var time: Int {
return data[currentIdx].prepareSeconds
}
func getTime(idx: Int) -> Int {
return data[idx].prepareSeconds
}
func getContent(idx: Int) -> String {
return data[idx].content
}
}
InfoView
import SwiftUI
struct InfoView: View {
#ObservedObject var model: TemplateViewModel
#State var prepareSeconds: Int = 0
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
init(model: TemplateViewModel) {
self.model = model
self._prepareSeconds = State(initialValue: model.getTime(idx: model.currentIdx))
}
var body: some View {
Text(prepareSeconds.description)
.onReceive(timer, perform: { _ in
if prepareSeconds > 0 {
prepareSeconds -= 1
}
})
}
}
QuizModel.swift
This the data model, prepareSeconds means how many seconds the participant can prepare for this quiz. content is the quiz content.
struct QuizModel {
var prepareSeconds: Int
var content: String
}
The simplest way is to just add an id to InfoView so that it is forced to reset its state when the ID changes:
InfoView(model: model).id(model.currentIdx)
However, architecturally, I'm not sure that makes the most sense. I'd store the timer in your ObservableObject:
struct TemplateView: View {
#ObservedObject var model = TemplateViewModel()
var body: some View {
VStack {
InfoView(model: model)
Text(model.getContent(idx: model.currentIdx))
Image(systemName: "arrowshape.turn.up.right.fill")
.onTapGesture {
if model.currentIdx >= model.data.count-1 {
model.currentIdx = 0
} else {
model.currentIdx += 1
}
model.countDownFrom(seconds: model.getTime(idx: model.currentIdx))
}
}.onAppear {
model.countDownFrom(seconds: model.getTime(idx: model.currentIdx))
}
}
}
class TemplateViewModel: ObservableObject {
#Published var currentIdx: Int = 0
#Published var prepareSeconds: Int = 0
private var cancellable : AnyCancellable?
var data: [QuizModel] = [
QuizModel(prepareSeconds: 10, content: "Answer the short question"),
QuizModel(prepareSeconds: 20, content: "Listen to the audio"),
QuizModel(prepareSeconds: 30, content: "Read the article"),
]
var time: Int {
return data[currentIdx].prepareSeconds
}
func getTime(idx: Int) -> Int {
return data[idx].prepareSeconds
}
func getContent(idx: Int) -> String {
return data[idx].content
}
func countDownFrom(seconds: Int) {
prepareSeconds = seconds
cancellable = Timer.publish(every: 1, on: .main, in: .common).autoconnect().sink(receiveValue: { (_) in
if self.prepareSeconds > 0 {
self.prepareSeconds -= 1
}
})
}
}
struct InfoView: View {
#ObservedObject var model: TemplateViewModel
var body: some View {
Text(model.prepareSeconds.description)
}
}
struct QuizModel {
var prepareSeconds: Int
var content: String
}
More refactoring that could be done, but that'll get you started.

SwiftUI Picker desn't bind with ObservedObject

I'm trying to fill up a Picker with data fetched asynchronously from external API.
This is my model:
struct AppModel: Identifiable {
var id = UUID()
var appId: String
var appBundleId : String
var appName: String
var appSKU: String
}
The class that fetches data and publish is:
class AppViewModel: ObservableObject {
private var appStoreProvider: AppProvider? = AppProvider()
#Published private(set) var listOfApps: [AppModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
self.loading = true
appStoreProvider?.dataProviderAppList { [weak self] (appList: [AppModel]) in
guard let self = self else {return}
DispatchQueue.main.async() {
self.listOfApps = appList
self.loading = false
}
}
}
init() {
fetchAppList()
}
}
The View is:
struct AppView: View {
#ObservedObject var appViewModel: AppViewModel = AppViewModel()
#State private var selectedApp = 0
var body: some View {
ActivityIndicatorView(isShowing: self.appViewModel.loading) {
VStack{
// The Picker doesn't bind with appViewModel
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName).tag(app.appName)
}
}
// The List correctly binds with appViewModel
List {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName.capitalized)
}
}
}
}
}
}
While the List view binds with the observed object appViewModel, the Picker doesn't behave in the same way. I can't realize why. Any help ?
I filed bug report, FB7670992. Apple responded yesterday, suggesting that I confirm this behavior in iOS 14, beta 1. It appears to now have been resolved.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Picker("", selection: $viewModel.wheelPickerValue) {
ForEach(viewModel.objects) { object in
Text(object.string)
}
}
.pickerStyle(WheelPickerStyle())
.labelsHidden()
}
}
Where
struct Object: Identifiable {
let id = UUID().uuidString
let string: String
}
class ViewModel: ObservableObject {
private var counter = 0
#Published private(set) var objects: [Object] = []
#Published var segmentedPickerValue: String = ""
#Published var wheelPickerValue: String = ""
fileprivate func nextSetOfValues() {
let newCounter = counter + 3
objects = (counter..<newCounter).map { value in Object(string: "\(value)") }
let id = objects.first?.id ?? ""
segmentedPickerValue = id
wheelPickerValue = id
counter = newCounter
}
init() {
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
guard let self = self else { timer.invalidate(); return }
self.nextSetOfValues()
}
timer.fire()
}
}
Results in:
I can't put this into your code because it is incomplete but here is a sample.
Pickers aren't meant to be dynamic. They have to be completely reloaded.
class DynamicPickerViewModel: ObservableObject {
#Published private(set) var listOfApps: [YourModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
loading = true
DispatchQueue.main.async() {
self.listOfApps.append(YourModel.addSample())
self.loading = false
}
}
init() {
fetchAppList()
}
}
struct DynamicPicker: View {
#ObservedObject var vm = DynamicPickerViewModel()
#State private var selectedApp = ""
var body: some View {
VStack{
//Use your loading var to reload the picker when it is done
if !vm.loading{
//Picker is not meant to be dynamic, it needs to be completly reloaded
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.vm.listOfApps){ app in
Text(app.name!).tag(app.name!)
}
}
}//else - needs a view while the list is being loaded/loading = true
List {
ForEach(self.vm.listOfApps){ app in
Text(app.name!.capitalized)
}
}
Button(action: {
self.vm.fetchAppList()
}, label: {Text("fetch")})
}
}
}
struct DynamicPicker_Previews: PreviewProvider {
static var previews: some View {
DynamicPicker()
}
}

SwiftUI pass param from views

I have this situation:
First View, in this view i got the current position and i save the latitude and longitude in two double type var: doublelat and doublelng
import SwiftUI
struct CoordinateView: View {
#ObservedObject var locationManager = LocationManager()
#State private var gotopage = false
var userLatitude: String {
let lat = String(format: "%.6f", locationManager.lastLocation?.coordinate.latitude ?? 0)
return "\(lat)"
}
var userLongitude: String {
let lng = String(format: "%.6f", locationManager.lastLocation?.coordinate.longitude ?? 0)
return "\(lng)"
}
var doubleLat : Double {
return locationManager.lastLocation?.coordinate.latitude ?? 0
}
var doubleLng : Double {
return locationManager.lastLocation?.coordinate.longitude ?? 0
}
private var getcoordinates: Bool {
if locationManager.statusCode == 0 {
return false
} else {
return true
}
}
var body: some View {
VStack {
HStack() {
Text("Your position")
.font(.headline)
Spacer()
}
VStack(alignment: .leading) {
HStack(){
Text("Lat")
Spacer()
Text("\(userLatitude)")
}.padding(5)
HStack(){
Text("Long")
Spacer()
Text("\(userLongitude)")
}.padding(5)
}
if getcoordinates {
HStack(alignment: .center){
Button("Go to next page") {
self.gotopage = true
}
}.padding()
VStack {
if self.gotopage {
AddressView(latitude: self.doubleLat, longitude: self.doubleLng)
}
}
}
}
.padding()
}
};
struct CoordinateView_Previews: PreviewProvider {
static var previews: some View {
CoordinateView()
}
}
Now just press on button Button("Go to next page"), go to view AddressView(latitude: self.doubleLat, longitude: self.doubleLng), with two param latitude and longitude.
This is AddressView:
import SwiftUI
struct AddressView: View {
var latitude: Double
var longitude: Double
#ObservedObject var apiManager = APIManager(lat: <latitude>, lng: <longitude>)
var body: some View {
.....
.....
}
struct AddressView_Previews: PreviewProvider {
static var previews: some View {
AddressView(latitude: 14.564378, longitude: 42.674532)
} }
In this second view i have to pass the two param (latitude and longitude) from previous view in this declaration:
#ObservedObject var apiManager = APIManager(lat: <latitude>, lng: <longitude>)
How can I do?
Try the following declaration for AddressView
struct AddressView: View {
var latitude: Double
var longitude: Double
#ObservedObject var apiManager: APIManager
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
self.apiManager = APIManager(lat: latitude, lng: longitude)
}
...