Copy CoreData data to CSV file - swiftui

I am trying to use fileExport logic to copy history data from CoreData to a CSV file. Since the data is coming from CoreData I need to use #FetchRequest and it is my understanding that #FetchRequest may only be used in a view.
I'm getting a number of errors related to misusing a view and transferring the data to fileExporter. It seems like I'm misusing a view to transfer data. Are there other features of CoreData that can be used to retrieve data outside of a view?
I have several similar structures that create CSV files without using coreData working. Therefore I believe my structures CreateHistoryTable and MessageDocument are working correctly. So I need help getting my data from CoreData to fileExporter.
struct CreateHistoryTable: View {
#Environment(\.managedObjectContext) var viewContext
#State private var showingExporter: Bool = false
#State private var document: MessageDocument?
var body: some View {
VStack {
Button ( action: {
self.showingExporter = true
document = CreateHistoryCSV() <-- need help here retrieving document to export
}) {
HStack (alignment: .firstTextBaseline) {
Text("Export History Entries")
.fontWeight(.bold)
.font(.title3)
Image(systemName: "square.and.arrow.up")
}
}
}.fileExporter(
isPresented: $showingExporter,
document: document,
contentType: .plainText,
defaultFilename: "TripSenseHistory.csv"
) { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
}
}
.navigationBarTitle(Text("Export History Entries"), displayMode: .inline)
}
}
Retrieve data from CoreData and copy to single text string
struct CreateHistoryCSV: View {
#Binding MessageDocument
var csvData: String = ""
var title = ",Trip Sense History Entries,\n"
var subtitle = "Date,Category,Payment Type, Amount\n"
var messageRow: String = ""
var sHisCatName: String = ""
var sHisDsc: String = ""
var sHisPayType: String = ""
var sHisMoney: String = ""
var dHisMoney: Double = 0.0
var sHisLoc: String = ""
var payType = ["Cash", "Debit", "Credit"]
var code: String = ""
var messageRow = ""
// fetch core data
#FetchRequest(
entity: CurrTrans.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CurrTrans.entryDT, ascending: true)]
) var currTrans: FetchedResults<CurrTrans>
var body: some View {
var csvData = title + subtitle
for item in 0..<currTrans.count {
let messageRow = createHistoryRow(item: item)
csvData += messageRow
}
print(csvData)
//return MessageDocument(message: csvData)
}
func createHistoryRow(item: Int) ->(String) {
// format expense date and time
let dHisDate = currTrans[item].entryDT ?? Date()
let sHisDate = dHisDate.formatted(.dateTime.year().day().month(.wide).hour().minute())
// get history category
let sHisCatName = currTrans[item].entryCatName ?? "cat name"
// get payment type
let sHisPayType = payType[Int(currTrans[item].entryPT)]
// get description
let sHisDsc = currTrans[item].entryDsc ?? "Unk"
// format transaction amount
let code = currTrans[item].entryCode ?? "Unk" // 3 digit country code for this transaction
let dHisMoney = currTrans[item].entryMoney
let sHisMoney = dHisMoney.formatted(.currency(code: sym))
// get location
let sHisLoc = currTrans[item].entryAddr ?? "Unk"
messageRow = "\"\(sHisDate)\"" + "," + sHisCatName + "," + sHisPayType + "," + "\"\(sHisDsc)\"" + "," + "\"\(sHisMoney)\"" + "," + "\"\(sHisLoc)\"" + "\n"
return messageRow
}
}
This code is part of the Swiftui file export logic
struct MessageDocument: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
var message: String = ""
init(message: String) {
self.message = message
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
message = string
}
// this will be called when the system wants to write our data to disk
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
return FileWrapper(regularFileWithContents: message.data(using: .utf8)!)
}
}

With further research I realized that I could place the #FetchRequest in CreateHistoryTable along with the fileExporter view logic. That allowed me to change CreateHistoryCSV to a function of CreateHistoryTable. No changes were made to createHistoryRow
// copy history entrys to csv file
struct CreateHistoryTable: View {
#EnvironmentObject var base: BaseCurrency
#EnvironmentObject var bank: BankWithdrawal
#EnvironmentObject var userData: UserData
#Environment(\.managedObjectContext) var viewContext
#State private var showingExporter: Bool = false
#State private var document: MessageDocument?
var title = ",Trip Sense History Entries,\n"
var subtitle = "Date,Category,Payment Type, Amount\n"
var messageRow: String = ""
var sHisCatName: String = ""
var sHisDsc: String = ""
var sHisPayType: String = ""
var sHisMoney: String = ""
var dHisMoney: Double = 0.0
var sHisLoc: String = ""
var payType = ["Cash", "Debit", "Credit"]
var sym: String = ""
// fetch core data
#FetchRequest(
entity: CurrTrans.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CurrTrans.entryDT, ascending: true)]
) var currTrans: FetchedResults<CurrTrans>
var body: some View {
VStack {
Button ( action: {
self.showingExporter = true
let dates = userData.formatCsvDate(startDate: startDate, endDate: endDate)
document = CreateHistoryCSV(dates: dates)
}) {
HStack (alignment: .firstTextBaseline) {
Text("Export History Entries")
.fontWeight(.bold)
.font(.title3)
Image(systemName: "square.and.arrow.up")
}
}
}.fileExporter(
isPresented: $showingExporter,
document: document,
contentType: .plainText,
defaultFilename: "TripSenseHistory.csv"
) { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
}
}
.navigationBarTitle(Text("Export History Entries"), displayMode: .inline)
}
func CreateHistoryCSV() -> (MessageDocument) {
var csvData = title + subtitle
for item in 0..<currTrans.count {
let messageRow = createHistoryRow(item: item)
csvData += messageRow
}
print(csvData)
return MessageDocument(message: csvData)
}

Related

Is there a way to save another field value (e.g _id) when selecting a value from Picker?

I have these classes - User and Centre
import Foundation
import RealmSwift
class User: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id = UUID().uuidString
#Persisted var userName = ""
#Persisted var firstName = ""
#Persisted var lastName = ""
#Persisted var userMobile = ""
#Persisted var userCentre = ""
#Persisted var userCentreId: ObjectId?
#Persisted var userPreferences: UserPreferences?
#Persisted var lastSeenAt: Date?
#Persisted var conversations = List<Conversation>()
#Persisted var presence = "On-Line"
var isProfileSet: Bool { !(userPreferences?.isEmpty ?? true) }
var presenceState: Presence {
get { return Presence(rawValue: presence) ?? .hidden }
set { presence = newValue.asString }
}
convenience init(userName: String, id: String) {
self.init()
self.userName = userName
_id = id
userPreferences = UserPreferences()
userPreferences?.displayName = userName
presence = "On-Line"
}
}
enum Presence: String {
case onLine = "On-Line"
case offLine = "Off-Line"
case hidden = "Hidden"
var asString: String {
self.rawValue
}
}
import Foundation
import RealmSwift
import MapKit
class Centre: Object, ObjectKeyIdentifiable {
#Persisted var _id: ObjectId = ObjectId.generate()
#Persisted var centreName = ""
#Persisted var centreDesc = ""
#Persisted var centreLocation: Coordinates?
override static func primaryKey() -> String? {
return "_id"
}
convenience init(centreName: String, centreDesc: String, x: Double, y: Double) {
self.init()
self.centreName = centreName
self.centreDesc = centreDesc
self.centreLocation?.x = x
self.centreLocation?.y = y
}
var coordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: (centreLocation?.y)!, longitude: (centreLocation?.x)!)
}
}
I have a view to set up user profile and the user's associated centre with a picker showing the available centres. I would like to also save the _id of the centre when the centre name is selected. How do I do that? Is it possible to pass a hidden value in Picker or should I use two Pickers - will take up screen space.
struct SetUserProfileView: View {
#AppStorage("shouldShareLocation") var shouldShareLocation = false
#Environment(\.realm) var realm
#ObservedRealmObject var user: User
#ObservedResults(Centre.self) var centres
#Binding var isPresented: Bool
#Binding var userID: String?
#State private var displayName = ""
#State private var photo: Photo?
#State private var photoAdded = false
#State private var firstName = ""
#State private var lastName = ""
#State private var userMobile = ""
#State private var selectedCentre = ""
#State public var selectedCentreId: ObjectId
var body: some View {
Form {
Section {
if let photo = photo {
AvatarButton(photo: photo) {
self.showPhotoTaker()
}
}
if photo == nil {
Button(action: { self.showPhotoTaker() }) {
Text("Add Photo")
}
}
TextField("Display Name", text: $displayName)
TextField("First Name", text: $firstName)
TextField("Last Name", text: $lastName)
TextField( "Mobile Number", text: $userMobile)
Text("Select Centre")
Picker(selection: $selectedCentre, label: Text("Select Centre")) {
Text("Nothing Selected").tag("")
ForEach(centres, id: \.self) { centre in
Text(centre.centreName).tag(centre.centreName)
}
}
.onAppear(perform: initData)
.pickerStyle(.menu)
.accentColor(.white)
I assume you want this
Text("Select Centre")
Picker(selection: $selectedCentre, label: Text("Select Centre")) {
Text("Nothing Selected").tag("")
ForEach(centres, id: \.self) { centre in
Text(centre.centreName).tag(centre.centreName)
}
}
.onChange(of: selectedCentre) { value in
// do whatever needed here // << here !!
}

Editing Location Not Updating Map

I have a list of transaction entries that includes location coordinates. Tapping on an entry in the list creates a view with a map and transactions details. Tapping on Edit Location within the detail view presents a form for a new address. Tapping Save returns to detail view with the new map and transaction details.
What I see occurring upon tapping Save is the details with the wrong (old) map. I am using a UIKit map because I want to allow the customer the option of the Apple standard three different maps views (Default, Transit, and Satellite). Returning to the transaction view and back to detail view then displays the correct view.
When using breakpoints to follow the flow, I see upon tapping a transaction entry calls to MakeUIView and updateUIView in MapView. Upon tapping Save in Edit Location only updateUIView is called. Detail view does appear to have the correct coordinates, it is just not calling makeUIView().
DetailView is called from HistoryView (transaction entry list)
struct DetailView: View {
#ObservedObject var item: CurrTrans // use to refresh view
#State private var mapType: MKMapType = .standard
var coordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: item.entryLat,
longitude: item.entryLong)
}
var body: some View {
GeometryReader { g in
VStack {
ShowMap(item: item, coordinate: coordinate, mapType: mapType)
.frame(width: g.size.width, height: g.size.height * 0.68)
.padding(.bottom, 5)
// show transaction details
showData(g: g, item: item)
Spacer()
NavigationLink(destination: EditLocation(g: g, item: item)) {Text("Edit Location")}
.padding(.bottom, g.size.height * 0.01)
}
.font(.subheadline)
.navigationBarTitle("Transaction Details", displayMode: .inline)
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct EditLocation: View {
var g: GeometryProxy
var item: CurrTrans
#ObservedObject private var lm = LocationManager()
#Environment(\.dismiss) var dismiss
// persistant entry storage in coreData
#Environment(\.managedObjectContext) var viewContext
#State private var getStreet: String = ""
#State private var getCity: String = ""
#State private var getState: String = ""
#State private var getCountry: String = ""
#State private var invalidAddr: Bool = false
var body: some View {
VStack {
ShowEntryDetails(item: item)
ZStack {
Rectangle()
.frame(width: g.size.width * 0.55, height: g.size.height * 0.50)
// get new address input
GetFormEntry( getStreet: $getStreet, getCity: $getCity, getState: $getState, getCountry: $getCountry)
} .navigationBarTitle(Text("Edit Transaction Location"), displayMode: .inline) // end zstack
.navigationBarItems(trailing: Button(action: {
// prep address string for conversion to coordinates
let locStr = getStreet + "," + getCity + "," + getState + " " + getCountry
lm.getCoordinate(addressString: locStr) { coordinates, error in
print("edit coordiantes = \(coordinates)")
if error == nil {
print(coordinates.latitude)
print(coordinates.longitude)
item.entryLat = coordinates.latitude
item.entryLong = coordinates.longitude
//item.address = getStreet + "\n" + getCity + " " + getState + "\n" + getCountry
item.entryCity = getCity
item.entryState = getState
item.entryCountry = getCountry
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
// FIX: report error (Unable to Save Location Changes)
}
dismiss()
} else {
// Invalid address-- try again
invalidAddr = true
getStreet = ""
getCity = ""
getState = ""
getCountry = ""
}
}
}) {
Text ("Save")
}.disabled(getStreet.isEmpty || getCity.isEmpty || getCountry.isEmpty)
)
}
.alert("Invalid Address, Try Again", isPresented: $invalidAddr, actions: {
})
}
}
struct GetFormEntry: View {
#Binding var getStreet: String
#Binding var getCity: String
#Binding var getState: String
#Binding var getCountry: String
enum Field: Hashable {
case getStreet
case getCity
case getState
case getCountry
}
#FocusState private var ckFocus: Field?
let maxDscDigits = 40
var body: some View {
Form {
Section(header: Text("Enter Transaction Address")) {
TextField("Street Address", text: $getStreet)
.background(Color.gray.opacity(0.15))
.focused($ckFocus, equals: .getStreet)
.keyboardType(.default)
// prevent pasting of non-valid text
.onChange(of: getStreet) {
let txt = $0
if dscAllowed(txt, maxDscDigits: maxDscDigits) {
getStreet = txt
} else {
getStreet = String(txt.dropLast())
}
} // end onChange
TextField("City", text: $getCity)
.background(Color.gray.opacity(0.15))
.focused($ckFocus, equals: .getCity)
.keyboardType(.default)
// prevent pasting of non-valid text
.onChange(of: getCity) {
let txt = $0
if dscAllowed(txt, maxDscDigits: maxDscDigits) {
getCity = txt
} else {
getCity = String(txt.dropLast())
}
} // end onChange
TextField("State", text: $getState)
.background(Color.gray.opacity(0.15))
.focused($ckFocus, equals: .getState)
.keyboardType(.default)
// prevent pasting of non-valid text
.onChange(of: getState) {
let txt = $0
if dscAllowed(txt, maxDscDigits: maxDscDigits) {
getState = txt
} else {
getState = String(txt.dropLast())
}
} // end onChange
TextField("Country", text: $getCountry)
.background(Color.gray.opacity(0.15))
.focused($ckFocus, equals: .getCountry)
.keyboardType(.default)
// prevent pasting of non-valid text
.onChange(of: getCountry) {
let txt = $0
if dscAllowed(txt, maxDscDigits: maxDscDigits) {
getCountry = txt
} else {
getCountry = String(txt.dropLast())
}
} // end onChange
}
}
}
}
struct ShowMap: View {
#ObservedObject var item: CurrTrans
var coordinate: CLLocationCoordinate2D
var mapType: MKMapType
#ObservedObject private var lm = LocationManager()
var body: some View {
// location services disabled?
if item.entryLat == 0.0 && item.entryLong == 0.0 {
VStack {
Text("Map Not Available")
.font(.title2)
.fontWeight(.bold)
Text("Location Services Disabled or Map Not Available")
.font(.subheadline)
}
} else {
//Text("Before: \(item.entryLat) \(item.entryLong)")
// we have a map so lets display it
let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
let region = MKCoordinateRegion(center: coordinate, span: span)
MapView(region: region, mapType: mapType, coordinate: coordinate)
}
}
}
/*---------------------------------------
Use UIKit map view to display the 3 map types
---------------------------------------*/
struct MapView: UIViewRepresentable {
#ObservedObject private var lm = LocationManager()
let region: MKCoordinateRegion
let mapType : MKMapType
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: true)
// display a map pin
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
mapView.addAnnotation(annotation)
mapView.mapType = mapType
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
}
}
In your Location Delegate, try to implement this instance method from CLLocationManagerDelegate
Hope it works for your problem.

How to force re-create view in SwiftUI?

I made a view which fetches and shows a list of data. There is a context menu in toolbar where user can change data categories. This context menu lives outside of the list.
What I want to do is when user selects a category, the list should refetch data from backend and redraw entire of the view.
I made a BaseListView which can be reused in various screens in my app, and since the loadData is inside the BaseListView, I don't know how to invoke it to reload data.
Did I do this with good approaching? Is there any way to force SwiftUI recreates entire of view so that the BaseListView loads data & renders subviews as first time it's created?
struct ProductListView: View {
var body: some View {
BaseListView(
rowView: { ProductRowView(product: $0, searchText: $1)},
destView: { ProductDetailsView(product: $0) },
dataProvider: {(pageIndex, searchText, complete) in
return fetchProducts(pageIndex, searchText, complete)
})
.hideKeyboardOnDrag()
.toolbar {
ProductCategories()
}
.onReceive(self.userSettings.$selectedCategory) { category in
//TODO: Here I need to reload data & recreate entire of view.
}
.navigationTitle("Products")
}
}
extension ProductListView{
private func fetchProducts(_ pageIndex: Int,_ searchText: String, _ complete: #escaping ([Product], Bool) -> Void) -> AnyCancellable {
let accountId = Defaults.selectedAccountId ?? ""
let pageSize = 20
let query = AllProductsQuery(id: accountId,
pageIndex: pageIndex,
pageSize: pageSize,
search: searchText)
return Network.shared.client.fetchPublisher(query: query)
.sink{ completion in
switch completion {
case .failure(let error):
print(error)
case .finished:
print("Success")
}
} receiveValue: { response in
if let data = response.data?.getAllProducts{
let canLoadMore = (data.count ?? 0) > pageSize * pageIndex
let rows = data.rows
complete(rows, canLoadMore)
}
}
}
}
ProductCategory is a separated view:
struct ProductCategories: View {
#EnvironmentObject var userSettings: UserSettings
var categories = ["F&B", "Beauty", "Auto"]
var body: some View{
Menu {
ForEach(categories,id: \.self){ item in
Button(item, action: {
userSettings.selectedCategory = item
Defaults.selectedCategory = item
})
}
}
label: {
Text(self.userSettings.selectedCategory ?? "All")
.regularText()
.autocapitalization(.words)
.frame(maxWidth: .infinity)
}.onAppear {
userSettings.selectedCategory = Defaults.selectedCategory
}
}
}
Since my app has various list-view with same behaviours (Pagination, search, ...), I make a BaseListView like this:
struct BaseListView<RowData: StringComparable & Identifiable, RowView: View, Target: View>: View {
enum ListState {
case loading
case loadingMore
case loaded
case error(Error)
}
typealias DataCallback = ([RowData],_ canLoadMore: Bool) -> Void
#State var rows: [RowData] = Array()
#State var state: ListState = .loading
#State var searchText: String = ""
#State var pageIndex = 1
#State var canLoadMore = true
#State var cancellableSet = Set<AnyCancellable>()
#ObservedObject var searchBar = SearchBar()
#State var isLoading = false
let rowView: (RowData, String) -> RowView
let destView: (RowData) -> Target
let dataProvider: (_ page: Int,_ search: String, _ complete: #escaping DataCallback) -> AnyCancellable
var searchable: Bool?
var body: some View {
HStack{
content
}
.if(searchable != false){view in
view.add(searchBar)
}
.hideKeyboardOnDrag()
.onAppear(){
print("On appear")
searchBar.$text
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.sink { text in
print("Search bar updated")
self.state = .loading
self.pageIndex = 1
self.searchText = text
self.rows.removeAll()
self.loadData()
}.store(in: &cancellableSet)
}
}
private var content: some View{
switch state {
case .loading:
return Spinner(isAnimating: true, style: .large).eraseToAnyView()
case .error(let error):
print(error)
return Text("Unable to load data").eraseToAnyView()
case .loaded, .loadingMore:
return
ScrollView{
list(of: rows)
}
.eraseToAnyView()
}
}
private func list(of data: [RowData])-> some View{
LazyVStack{
let filteredData = rows.filter({
searchText.isEmpty || $0.contains(string: searchText)
})
ForEach(filteredData){ dataItem in
VStack{
//Row content:
if let target = destView(dataItem), !(target is EmptyView){
NavigationLink(destination: target){
row(dataItem)
}
}else{
row(dataItem)
}
//LoadingMore indicator
if case ListState.loadingMore = self.state{
if self.rows.isLastItem(dataItem){
Seperator(color: .gray)
LoadingView(withText: "Loading...")
}
}
}
}
}
}
private func row(_ dataItem: RowData) -> some View{
rowView(dataItem, searchText).onAppear(){
//Check if need to load next page of data
if rows.isLastItem(dataItem) && canLoadMore && !isLoading{
isLoading = true
state = .loadingMore
pageIndex += 1
print("Load page \(pageIndex)")
loadData()
}
}.padding(.horizontal)
}
private func loadData(){
dataProvider(pageIndex, searchText){ newData, canLoadMore in
self.state = .loaded
rows.append(contentsOf: newData)
self.canLoadMore = canLoadMore
isLoading = false
}
.store(in: &cancellableSet)
}
}
In your BaseListView you should have an onChange modifier that catches changes to userSettings.$selectedCategory and calls loadData there.
If you don't have access to userSettings in BaseListView, pass it in as a Binding or #EnvironmentObject.

Link #Binding to #Published with SwiftUI

I'm trying to figure out how to link the #Binding passed into a custom View to an #Published from that view's model. Essentially I'm trying to create a reusable integer only TextField. I'm using the below code, which works to set the integer value into the text field, but what I can't figure out is how to update the binding when the text changes.
private class IntegerTextFieldValue: ObservableObject {
#Published var value = "" {
didSet {
let numbersOnly = value.filter { $0.isNumber }
if value != numbersOnly {
value = numbersOnly
}
}
}
}
struct IntegerTextField: View {
#Binding var value: Int?
#StateObject private var fieldValue = IntegerTextFieldValue()
var placeholder = ""
var body: some View {
TextField(placeholder, text: $fieldValue.value)
.keyboardType(.numberPad)
.onAppear {
if let value = value {
fieldValue.value = "\(value)"
}
}
}
}
If I understand you correctly
.onChange (of: fieldValue.value) { vl in
value = vl
}
this modifier updates the binding value to $fieldValue.value
Here is modified code to demo a possible approach (tested with Xcode 12.1 / iOS 14.1):
private class IntegerTextFieldValue: ObservableObject {
#Published var value = "" {
didSet {
let numbersOnly = value.filter { $0.isNumber }
if value != numbersOnly {
value = numbersOnly
}
if let number = Int(value) {
numberValue = number
}
}
}
#Published var numberValue: Int = 0
}
struct IntegerTextField: View {
#Binding var value: Int?
#StateObject private var fieldValue = IntegerTextFieldValue()
var placeholder = ""
var body: some View {
TextField(placeholder, text: $fieldValue.value)
.keyboardType(.numberPad)
.onAppear {
if let value = value {
fieldValue.value = "\(value)"
}
}
.onChange(of: fieldValue.numberValue) {
if $0 != self.value {
self.value = $0
}
}
}
}

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()
}
}