I want to output the variable 'healthStore.valueTest' via ContentView in SwiftUI.
The class healtStore is structured as follows:
class HealthStore {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
var valueTest: HKQuantity?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
func calculateBloodPressureSystolic() {
guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
// let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())
// let anchorDate = Date.mondayAt12AM()
// let daily = DateComponents(day: 1)
// let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate)
query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
self.valueTest = statistics?.averageQuantity()
}
}
healthStore!.execute(query!)
}
}
ContentView is built as follows:
import SwiftUI
import HealthKit
struct ContentView: View {
private var healthStore: HealthStore?
init() {
healthStore = HealthStore()
}
var body: some View {
Text("Hello, world!")
.padding().onAppear(){
if let healthStore = healthStore {
healthStore.requestAuthorization { success in
if success {
healthStore.calculateBloodPressureSystolic()
print(healthStore.query)
print(healthStore.valueTest)
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The value for the variable self.valueTest is assigned in the process DispatchQueue.main.async. Nevertheless, I get only a nil back when querying via ContentView.
You could set up your HealthStore class and use it as an EnvironmentObject. Assuming your app uses the SwiftUI lifecycle you can inject HealthStore into the environment in the #main entry point of your app.
import SwiftUI
#main
struct NameOfYourHeathApp: App {
let healthStore = HealthStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(healthStore)
}
}
}
Change your HealthStore class to this. (I removed your commented out code in my sample below)
import HealthKit
class HealthStore: ObservableObject {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
var valueTest: HKQuantity?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
// I moved the HealthStore conditional check out of your View logic
// and placed it here instead.
func setUpHealthStore() {
let typesToRead: Set = [
HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!
]
// I left the `toShare` as nil as I did not dig into adding bloodpressure reading to HealthKit.
healthStore?.requestAuthorization(toShare: nil, read: typesToRead, completion: { success, error in
if success {
self.calculateBloodPressureSystolic()
}
})
}
func calculateBloodPressureSystolic() {
guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
self.valueTest = statistics?.averageQuantity()
}
}
healthStore!.execute(query!)
}
}
Then use it in your ContentView like this.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var healthStore: HealthStore
var body: some View {
Text("Hello, world!")
.onAppear {
healthStore.setUpHealthStore()
}
}
}
I didn't go through the trouble of setting up the proper permissions in the .plist file, but you'll also need to set up the Health Share Usage Description as well as Health Update Usage Description. I assume you have already done this but I just wanted to mention it.
Related
I am working on a SwiftUI Project using MVVM. I have the following files for a marketplace that has listings.
ListingRepository.swift - Connecting to Firebase Firestore
Listing.swift - Listing Model File
MarketplaceViewModel - Marketplace View Model
MarketplaceView - List view of listings for the marketplace
Originally, I was making my repository file the EnvironmentObject which worked. While researching I am realizing it makes more sense to make the ViewModel the EnvironmentObject. However, I am having trouble making an EnvironmentObject. Xcode is giving me the following error in my MarketplaceView.swift file when I try and access marketplaceViewModel and I can't understand why?
SwiftUI:0: Fatal error: No ObservableObject of type
MarketplaceViewModel found. A View.environmentObject(_:) for
MarketplaceViewModel may be missing as an ancestor of this view.
Here are the files in a simplified form.
App File
#main
struct Global_Seafood_ExchangeApp: App {
#StateObject private var authSession = AuthSession()
#StateObject private var marketplaceViewModel = MarketplaceViewModel(listingRepository: ListingRepository())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(marketplaceViewModel)
.environmentObject(authSession)
}
}
}
ListingRepository.swift
class ListingRepository: ObservableObject {
let db = Firestore.firestore()
#Published var listings = [Listing]()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
db.collection(FirestoreCollection.listings).addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Listings.")
return
}
self.listings = documents.compactMap { listing in
do {
return try listing.data(as: Listing.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
Listing.swift
struct Listing: Codable, Identifiable {
#DocumentID var id: String?
var title: String?
}
MarketplaceModelView.swift
class MarketplaceViewModel: ObservableObject {
var listingRepository: ListingRepository
#Published var listingRowViewModels = [ListingRowViewModel]()
private var cancellables = Set<AnyCancellable>()
init(listingRepository: ListingRepository) {
self.listingRepository = listingRepository
self.startCombine()
}
func startCombine() {
listingRepository
.$listings
.receive(on: RunLoop.main)
.map { listings in
listings.map { listing in
ListingRowViewModel(listing: listing)
}
}
.assign(to: \.listingRowViewModels, on: self)
.store(in: &cancellables)
}
}
MarketplaceView.swift
struct MarketplaceView: View {
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
var body: some View {
// ERROR IS HERE
Text(self.marketplaceViewModel.listingRowViewModels[1].listing.title)
}
}
ListingRowViewModel.swift
class ListingRowViewModel: ObservableObject {
var id: String = ""
#Published var listing: Listing
private var cancellables = Set<AnyCancellable>()
init(listing: Listing) {
self.listing = listing
$listing
.receive(on: RunLoop.main)
.compactMap { listing in
listing.id
}
.assign(to: \.id, on: self)
.store(in: &cancellables)
}
}
ContentView.swift
struct ContentView: View {
#EnvironmentObject var authSession: AuthSession
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
var body: some View {
Group{
if (authSession.currentUser != nil) {
TabView {
MarketplaceView()
.tabItem {
Image(systemName: "shippingbox")
Text("Marketplace")
}.tag(0) // MarketplaceView
AccountView(user: testUser1)
.tabItem {
Image(systemName: "person")
Text("Account")
}.tag(2) // AccountView
} // TabView
.accentColor(.white)
} else if (authSession.currentUser == nil) {
AuthView()
}
}// Group
.onAppear(perform: authenticationListener)
}
// MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
func authenticationListener() {
// Setup Authentication Listener
authSession.listen()
}
}
Any help would be greatly appreciated.
in your app you have:
ContentView().environmentObject(marketplaceViewModel)
so in "ContentView" you should have as the first line:
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
Note in "ContentView" you have, "#EnvironmentObject var authSession: AuthSession"
but this is not passed in from your App.
Edit: test passing "marketplaceViewModel", using this limited setup.
class MarketplaceViewModel: ObservableObject {
...
let showMiki = "here is Miki Mouse"
...
}
and
struct MarketplaceView: View {
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
var body: some View {
// ERROR NOT HERE
Text(marketplaceViewModel.showMiki)
// Text(self.marketplaceViewModel.listingRowViewModels[1].listing.title)
}
}
Anyone looking for a way to use MVVM with Firebase Firestore and make your View Model the EnvironmentObject I've added my code below. This project has a list view and a detail view. Each view has a corresponding view model. The project also uses a repository and uses Combine.
App.swift
import SwiftUI
import Firebase
#main
struct MVVMTestApp: App {
#StateObject private var marketplaceViewModel = MarketplaceViewModel(listingRepository: ListingRepository())
// Firebase
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(marketplaceViewModel)
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
Group {
MarketplaceView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
MarketplaceView.swift
import SwiftUI
struct MarketplaceView: View {
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
var body: some View {
NavigationView {
List {
ForEach(self.marketplaceViewModel.listingRowViewModels, id: \.id) { listingRowViewModel in
NavigationLink(destination: ListingDetailView(listingDetailViewModel: ListingDetailViewModel(listing: listingRowViewModel.listing))) {
ListingRowView(listingRowViewModel: listingRowViewModel)
}
} // ForEach
} // List
.navigationTitle("Marketplace")
} // NavigationView
}
}
struct MarketplaceView_Previews: PreviewProvider {
static var previews: some View {
MarketplaceView()
}
}
ListingRowView.swift
import SwiftUI
struct ListingRowView: View {
#ObservedObject var listingRowViewModel: ListingRowViewModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(listingRowViewModel.listing.name)
.font(.headline)
Text(String(listingRowViewModel.listing.description))
.font(.footnote)
}
}
}
struct ListingRowView_Previews: PreviewProvider {
static let listingRowViewModel = ListingRowViewModel(listing: testListing1)
static var previews: some View {
ListingRowView(listingRowViewModel: listingRowViewModel)
}
}
ListingDetailView.swift
import SwiftUI
struct ListingDetailView: View {
var listingDetailViewModel: ListingDetailViewModel
var body: some View {
VStack(spacing: 5) {
Text(listingDetailViewModel.listing.name)
.font(.headline)
Text(String(listingDetailViewModel.listing.description))
.font(.footnote)
}
}
}
struct ListingDetailView_Previews: PreviewProvider {
static let listingDetailViewModel = ListingDetailViewModel(listing: testListing1)
static var previews: some View {
ListingDetailView(listingDetailViewModel: listingDetailViewModel)
}
}
MarketplaceViewModel.swift
import Foundation
import SwiftUI
import Combine
class MarketplaceViewModel: ObservableObject {
var listingRepository: ListingRepository
#Published var listingRowViewModels = [ListingRowViewModel]()
private var cancellables = Set<AnyCancellable>()
init(listingRepository: ListingRepository) {
self.listingRepository = listingRepository
self.startCombine()
}
func startCombine() {
listingRepository
.$listings
.receive(on: RunLoop.main)
.map { listings in
listings.map { listing in
ListingRowViewModel(listing: listing)
}
}
.assign(to: \.listingRowViewModels, on: self)
.store(in: &cancellables)
}
}
ListingRowViewModel.swift
import Foundation
import SwiftUI
import Combine
class ListingRowViewModel: ObservableObject {
var id: String = ""
#Published var listing: Listing
private var cancellables = Set<AnyCancellable>()
init(listing: Listing) {
self.listing = listing
$listing
.receive(on: RunLoop.main)
.compactMap { listing in
listing.id
}
.assign(to: \.id, on: self)
.store(in: &cancellables)
}
}
ListingDetailViewModel.swift
import Foundation
import SwiftUI
import Combine
class ListingDetailViewModel: ObservableObject, Identifiable {
var listing: Listing
init(listing: Listing) {
self.listing = listing
}
}
Listing.swift
import Foundation
import SwiftUI
import FirebaseFirestore
import FirebaseFirestoreSwift
struct Listing: Codable, Identifiable {
#DocumentID var id: String?
var name: String
var description: String
}
ListingRepository.swift
import Foundation
import Firebase
import FirebaseFirestore
import FirebaseFirestoreSwift
class ListingRepository: ObservableObject {
// MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
// Access to Firestore Database
let db = Firestore.firestore()
#Published var listings = [Listing]()
init() {
startSnapshotListener()
}
// MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
func startSnapshotListener() {
db.collection("listings").addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Listings.")
return
}
self.listings = documents.compactMap { listing in
do {
return try listing.data(as: Listing.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
I want to load the following GUI in SwiftUI:
import SwiftUI
struct ContentView: View {
#ObservedObject var test = Test()
#ObservedObject var healthStore = HealthStore()
func callUpdate() {
print(test.value)
print(healthStore.systolicValue)
print(healthStore.diastolicValue)
}
var body: some View {
Text("Platzhalter")
.padding()
.onAppear(perform: {
healthStore.setUpHealthStore()
callUpdate()
})
Button("Test"){
callUpdate()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The variables healthStore.systolicValue and healthStore.diastolicValue are called via the function callUpdate(). On the first call both variables are nil. Only when I call the function via the Test button, the correct value is output in the console.
The variables healthStore.systolicValue and healthStore.diastolicValue are calculated in the class HealthStore:
import Foundation
import HealthKit
class HealthStore: ObservableObject {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
public var systolicValue: HKQuantity?
public var diastolicValue: HKQuantity?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
func setUpHealthStore() {
let typesToRead: Set = [
HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!,
HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!
]
healthStore?.requestAuthorization(toShare: nil, read: typesToRead, completion: { success, error in
if success {
print("requestAuthrization")
self.calculateBloodPressureSystolic()
self.calculateBloodPressureDiastolic()
}
})
}
func calculateBloodPressureSystolic() {
guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
self.systolicValue = statistics?.averageQuantity()
}
}
healthStore!.execute(query!)
}
func calculateBloodPressureDiastolic() {
guard let bloodPressureDiastolic = HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
query = HKStatisticsQuery(quantityType: bloodPressureDiastolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
self.diastolicValue = statistics?.averageQuantity()
}
}
healthStore!.execute(query!)
}
}
How do I need to modify my code to get the correct value for healthStore.systolicValue and healthStore.diastolicValue directly when I call ContentView?
This is the full code that I use for testing and works for me,
using macos 11.4, xcode 12.5, target ios 14.5, tested on iPhone device.
Let us know if this does not work for you.
import SwiftUI
import HealthKit
#main
struct TestErrorApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class HealthStore: ObservableObject {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
#Published var systolicValue: HKQuantity?
#Published var diastolicValue: HKQuantity?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
func setUpHealthStore() {
let typesToRead: Set = [
HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!,
HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!
]
healthStore?.requestAuthorization(toShare: nil, read: typesToRead, completion: { success, error in
if success {
print("--> requestAuthorization")
self.calculateBloodPressureSystolic()
self.calculateBloodPressureDiastolic()
}
})
}
func calculateBloodPressureSystolic() {
guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
// self.systolicValue = statistics?.averageQuantity()
self.systolicValue = HKQuantity(unit: HKUnit(from: ""), doubleValue: 1.2)
print("----> calculateBloodPressureSystolic statistics: \(statistics)")
print("----> calculateBloodPressureSystolic error: \(error)")
print("----> calculateBloodPressureSystolic: \(self.systolicValue)")
}
}
healthStore!.execute(query!)
}
func calculateBloodPressureDiastolic() {
guard let bloodPressureDiastolic = HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
query = HKStatisticsQuery(quantityType: bloodPressureDiastolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
// self.diastolicValue = statistics?.averageQuantity()
self.diastolicValue = HKQuantity(unit: HKUnit(from: ""), doubleValue: 3.4)
print("----> calculateBloodPressureDiastolic statistics: \(statistics)")
print("----> calculateBloodPressureDiastolic error: \(error)")
print("----> calculateBloodPressureDiastolic: \(self.diastolicValue)")
}
}
healthStore!.execute(query!)
}
}
struct ContentView: View {
#ObservedObject var healthStore = HealthStore()
var bloodPressureStandard = HKQuantity(unit: HKUnit(from: ""), doubleValue: 0.0)
var body: some View {
VStack {
Text("systolicValue: \(healthStore.systolicValue ?? bloodPressureStandard)")
Text("diastolicValue: \(healthStore.diastolicValue ?? bloodPressureStandard)")
}.onAppear {
healthStore.setUpHealthStore()
}
}
}
This is the output I get:
--> requestAuthorization
----> calculateBloodPressureSystolic statistics: nil
----> calculateBloodPressureSystolic error: Optional(Error Domain=com.apple.healthkit Code=11 "No data available for the specified predicate." UserInfo={NSLocalizedDescription=No data available for the specified predicate.})
----> calculateBloodPressureSystolic: Optional(1.2 ())
----> calculateBloodPressureDiastolic statistics: nil
----> calculateBloodPressureDiastolic error: Optional(Error Domain=com.apple.healthkit Code=11 "No data available for the specified predicate." UserInfo={NSLocalizedDescription=No data available for the specified predicate.})
----> calculateBloodPressureDiastolic: Optional(3.4 ())
And the UI shows:
systolicValue: 1.2 ()
diastolicValue: 3.4 ()
I think you are almost there. Your HealthStore needs to Publish the new values for
systolicValue and diastolicValue. No need for a "callUpdate()" function.
I would modify your code something as follows:
(Note once the systolicValue and diastolicValue are calculated, the model will update and the view will also automatically update itself)
struct ContentView: View {
#ObservedObject var healthStore = HealthStore()
var body: some View {
VStack {
Text("Platzhalter")
Text("systolicValue: \(healthStore.systolicValue)")
Text("diastolicValue: \(healthStore.diastolicValue)")
}.onAppear {
healthStore.setUpHealthStore()
}
}
}
class HealthStore: ObservableObject {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
#Published var systolicValue: HKQuantity? // <----
#Published var diastolicValue: HKQuantity? // <----
...
}
are you sure the data is available? To check, could you put this in the HealthStore:
DispatchQueue.main.async{
// self.systolicValue = statistics?.averageQuantity()
self.systolicValue = HKQuantity(unit: HKUnit(from: ""), doubleValue: 1.2)
print("----> calculateBloodPressureSystolic statistics: \(statistics)")
print("----> calculateBloodPressureSystolic error: \(error)")
print("----> calculateBloodPressureSystolic: \(self.systolicValue)")
}
and similarly for diastolicValue.
I found my error:
I declared the variables systolicValue and diastolicValue in the class HealthStore wrong. I declared them as public instead as #Published. The right code is:
#Published var systolicValue: HKQuantity?
#Published var diastolicValue: HKQuantity?
Thank you for you help.
I need to call loadData in my ContentView when the app becomes active. ExtensionDelegate is a class which handle app events such as applicationDidBecomeActive. But I don't understand how to get ContentView inside ExtensionDelegate.
This is my ContentView:
struct ContentView: View {
let network = Network()
#State private var currentIndex: Int = 0
#State private var sources: [Source] = []
var body: some View {
ZStack {
// Some view depends on 'sources'
}
.onAppear(perform: loadData)
}
func loadData() {
network.getSources { response in
switch response {
case .result(let result):
self.sources = result.results
case .error(let error):
print(error)
}
}
}
}
And ExtensionDelegate:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
}
func applicationDidBecomeActive() {
// Here I need to call 'loadData' of my ContentView
}
func applicationWillResignActive() {
}
...
The simplest solution as I see would be to use notification
in ContentView
let needsReloadNotification = NotificationCenter.default.publisher(for: .needsNetworkReload)
var body: some View {
ZStack {
// Some view depends on 'sources'
}
.onAppear(perform: loadData)
.onReceive(needsReloadNotification) { _ in self.loadData()}
}
and in ExtensionDelegate
func applicationDidBecomeActive() {
NotificationCenter.default.post(name: .needsNetworkReload, object: nil)
}
and somewhere in shared
extension Notification.Name {
static let needsNetworkReload = Notification.Name("NeedsReload")
}
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()
}
}
Workaround at bottom of Question
I thought SwiftUI was supposed to automatically update views when data they were dependent on changed. However that isn't happening in the code below:
First I make a simple BindableObject
import SwiftUI
import Combine
class Example: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var test = 1 {
didSet {
didChange.send(())
}
}
}
Then the root view of the app:
struct BindTest : View {
#Binding var test: Example
var body: some View {
PresentationButton(destination: BindChange(test: $test)) {
ForEach(0..<test.test) { index in
Text("Invalidate Me! \(index)")
}
}
}
}
And finally the view in which I change the value of the BindableObject:
struct BindChange : View {
#Binding var test: Example
#Environment(\.isPresented) var isPresented
var body: some View {
Button(action: act) {
Text("Return")
}
}
func act() {
test.test = 2
isPresented?.value = false
}
}
When the return button is tapped there should be 2 instances of the Text View - but there is only 1. What am I doing wrong?
Also worth noting: If I change the #Binding to #EnvironmentObject the program just crashes when you tap the button producing this error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
Full code below:
import SwiftUI
import Combine
class Example: BindableObject {
var didChange = PassthroughSubject<Example, Never>()
var test = 1 {
didSet {
didChange.send(self)
}
}
static let `default` = {
return Example()
}()
}
//Root View
struct BindTest : View {
#EnvironmentObject var test: Example
var body: some View {
PresentationButton(destination: BindChange()) {
ForEach(0..<test.test) { t in
Text("Invalidate Me! \(t)")
}
}
}
}
//View that changes the value of #Binding / #EnvironmentObject
struct BindChange : View {
#EnvironmentObject var test: Example
#Environment(\.isPresented) var isPresented
var body: some View {
Button(action: act) {
Text("Return")
}
}
func act() {
test.test = 2
isPresented?.value = false
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
//ContentView().environmentObject(EntryStore())
BindTest().environmentObject(Example())
}
}
#endif
EDIT 2: Post's getting a little messy at this point but the crash with EnvironmentObject seems to be related to an issue with PresentationButton
By putting a NavigationButton inside a NavigationView the following code produces the correct result - invalidating the List when test.test changes:
//Root View
struct BindTest : View {
#EnvironmentObject var test: Example
var body: some View {
NavigationView {
NavigationButton(destination: BindChange()) {
ForEach(0..<test.test) { t in
Text("Functional Button #\(t)")
}
}
}
}
}