SwiftUI - DispatchQueue.main.async in HealthKit - swiftui

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.

Related

Updating SwiftUI from HealthKit Query

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.

SwiftUI How to toggle a Bool in a Struct from an ObservableObject class and show alert to notify user

I have a method in my class that opens a map when given an address string. Trying to show an alert in a view by toggling a boolean in a method in the class. I can't figure out how to toggle the boolean in the class method. This is what I tried. The Published bool in class method updates but does not update in the View. I did put up a repo of just this feature if anybody wants to play around with it.
https://github.com/Ongomobile/LocationTest/tree/main/LocationTest
import SwiftUI
#main
struct LocationTestApp: App {
var body: some Scene {
WindowGroup {
ContentView(location: LocationManager())
}
}
}
Here is my Class:
import UIKit
import MapKit
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject {
var locationManager = CLLocationManager()
lazy var geocoder = CLGeocoder()
#Published var locationString = "1140"
// #Published var locationString = "1 apple park way cupertino"
#Published var currentAddress = ""
#Published var isValid: Bool = true
func openMapWithAddress () {
geocoder.geocodeAddressString(locationString) { placemarks, error in
if let error = error {
self.isValid = false
// prints false but does not update
print("isValid")
print(error.localizedDescription)
}
guard let placemark = placemarks?.first else {
return
}
guard let lat = placemark.location?.coordinate.latitude else{return}
guard let lon = placemark.location?.coordinate.longitude else{return}
let coords = CLLocationCoordinate2DMake(lat, lon)
let place = MKPlacemark(coordinate: coords)
let mapItem = MKMapItem(placemark: place)
mapItem.name = self.locationString
mapItem.openInMaps(launchOptions: nil)
}
}
}
Here is the view:
import SwiftUI
struct ContentView: View {
#ObservedObject var locationManager = LocationManager()
#State private var showingAlert = false
var body: some View {
Button {
locationManager.openMapWithAddress()
} label: {
Text("Get Map")
}
.alert(isPresented: $showingAlert) {
Alert(title: Text("Important message"), message:
Text("Enter a valid address"), dismissButton:
.default(Text("OK")))
}
}
}
I updated this answer to reflect some refactor help that I got from #rlong405 I put up a repository with this solution maybe it could help others.
OpenMapsInSwiftUI
import SwiftUI
struct ContentView: View {
#ObservedObject var locationManager = LocationManager()
var body: some View {
VStack{
Form{
Section {
Text("Enter Address")
TextField("", text: $locationManager.locationString)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal)
}
Button {
locationManager.openMapWithAddress()
} label: {
Text("Get Map")
}
.alert(isPresented: $locationManager.invalid) {
Alert(title: Text("Important message"), message:
Text("Enter a valid address"), dismissButton:
.default(Text("OK"), action:{
locationManager.invalid = false
locationManager.locationString = ""
}))
}
}
}
}
}
import UIKit
import MapKit
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject {
lazy var geocoder = CLGeocoder()
#Published var locationString = ""
#Published var invalid: Bool = false
func openMapWithAddress () {
geocoder.geocodeAddressString(locationString) { placemarks, error in
if let error = error {
DispatchQueue.main.async {
self.invalid = true
}
print(error.localizedDescription)
}
guard let placemark = placemarks?.first else {
return
}
guard let lat = placemark.location?.coordinate.latitude else{return}
guard let lon = placemark.location?.coordinate.longitude else{return}
let coords = CLLocationCoordinate2DMake(lat, lon)
let place = MKPlacemark(coordinate: coords)
let mapItem = MKMapItem(placemark: place)
mapItem.name = self.locationString
mapItem.openInMaps(launchOptions: nil)
}
}
}

How to reload a view screen when some values which generate by ForEach method are changed?

I'm currently developing an application using SwiftUI.
I'm trying to make a view to show some value as a list using CRUD API calls.
In the case of my codes, when I add or remove a number of arrays(lists) the view reloads a screen, but when I edit some data in an array(list) the view doesn't reload the screen with new values...
how could I resolve this problem?
Here are the codes:
HomeView.swift
import SwiftUI
struct HomeView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView{
VStack{
ForEach(appState.arrayInfos ?? []){ info in
VStack{
InfoRow(
id: info.id,
name: info.name,
memo: info.memo ?? "",
)
}
NavigationLink(destination: DetailView(),
isActive: $appState.isNavigateToDetailView){
EmptyView()
}
}
}
}.onAppear(){
appState.makeGetCallInfos()
}
}
}
InfoRow.swift
import SwiftUI
struct InfoRow: View {
#EnvironmentObject var appState: AppState
#State var id: Int
#State var name: String
#State var memo: String
var body: some View {
VStack{
Text(String(id))
Text(name)
Text(memo)
}
}
}
JsonModel.swift
import Foundation
struct Infos: Codable,Identifiable {
var id: Int
var name: String
var memo: String?
}
AppState.swift
import SwiftUI
import Foundation
import Combine
import UIKit
class AppState: ObservableObject {
#Published var isNavigateToDetailView:Bool = false
#Published var infos:Infos?
#Published var arrayInfos:[Infos]?
func makeGetCallInfos() {
let endpoint: String = "https://sample.com/api/info/"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.addValue("token xxxxxxxxxxxx", forHTTPHeaderField: "authorization")
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling GET")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
DispatchQueue.main.async {
do{ self.arrayInfos = try JSONDecoder().decode([Infos].self,from:responseData)
}catch{
print("Error: did not decode")
return
}
}
}
task.resume()
}
I tried to change the code like this but then I have an error like below:
HomeView.swift
VStack{
ForEach(appState.arrayInfos ?? []){ info in
VStack{
InfoRow(
id: appState.infos!.id,
name: appState.infos!.name,
memo: appState.infos!.memo ?? "",
)
}
NavigationLink(destination: DetailView(),
isActive: $appState.isNavigateToDetailView){
EmptyView()
}
}
}
error message
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Xcode:Version 12.0.1
I could solve this problem to change the codes like below:
removing #State
InfoRow.swift
import SwiftUI
struct InfoRow: View {
#EnvironmentObject var appState: AppState
var id: Int
var name: String
var memo: String
var body: some View {
VStack{
Text(String(id))
Text(name)
Text(memo)
}
}
}

SwiftUI - Location in API call with CLLocationManager and CLGeocoder

I'm struggling with this for a long time without finding where I'm wrong (I know I'm wrong).
I have one API call with the location of the phone (this one is working), but I want the same API call with a manual location entered by a textfield (using Geocoding for retrieving Lat/Long). The geocoding part is ok and updated but not passed in the API call.
I also want this API call to be triggered when the TextField is cleared by the dedicated button back with the phone location.
Please, what am I missing? Thanks for your help.
UPDATE: This works on Xcode 12.2 beta 2 and should work on Xcode 12.0.1
This is the code:
My Model
import Foundation
struct MyModel: Codable {
let value: Double
}
My ViewModel
import Foundation
import SwiftUI
import Combine
final class MyViewModel: ObservableObject {
#Published var state = State.ready
#Published var value: MyModel = MyModel(value: 0.0)
#Published var manualLocation: String {
didSet {
UserDefaults.standard.set(manualLocation, forKey: "manualLocation")
}
}
#EnvironmentObject var coordinates: Coordinates
init() {
manualLocation = UserDefaults.standard.string(forKey: "manualLocation") ?? ""
}
enum State {
case ready
case loading(Cancellable)
case loaded
case error(Error)
}
private var url: URL {
get {
return URL(string: "https://myapi.com&lat=\(coordinates.latitude)&lon=\(coordinates.longitude)")!
}
}
let urlSession = URLSession.shared
var dataTask: AnyPublisher<MyModel, Error> {
self.urlSession
.dataTaskPublisher(for: self.url)
.map { $0.data }
.decode(type: MyModel.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
func load(){
assert(Thread.isMainThread)
self.state = .loading(self.dataTask.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("⚠️ API Call finished")
break
case let .failure(error):
print("❌ API Call failure")
self.state = .error(error)
}
},
receiveValue: { value in
self.state = .loaded
self.value = value
print("👍 API Call loaded")
}
))
}
}
The Location Manager
import Foundation
import SwiftUI
import Combine
import CoreLocation
import MapKit
final class Coordinates: NSObject, ObservableObject {
#EnvironmentObject var myViewModel: MyViewModel
#Published var latitude: Double = 0.0
#Published var longitude: Double = 0.0
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
deinit {
locationManager.stopUpdatingLocation()
}
}
extension Coordinates: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
self.placemark = places?[0]
})
self.locationManager.stopUpdatingLocation()
}
}
extension Coordinates {
func getLocation(from address: String, completion: #escaping (_ location: CLLocationCoordinate2D?)-> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location?.coordinate else {
completion(nil)
return
}
completion(location)
}
}
}
The View
import Foundation
import SwiftUI
struct MyView: View {
#EnvironmentObject var myViewModel: MyViewModel
#EnvironmentObject var coordinates: Coordinates
private var icon: Image { return Image(systemName: "location.fill") }
var body: some View {
VStack{
VStack{
Text("\(icon) \(coordinates.placemark?.locality ?? "Unknown location")")
Text("Latitude: \(coordinates.latitude)")
Text("Longitude: \(coordinates.longitude)")
}
VStack{
Text("UV Index: \(myViewModel.value.value)")
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
HStack{
TextField("Manual location", text: $myViewModel.manualLocation)
if !myViewModel.manualLocation.isEmpty{
Button(action: { clear() }) { Image(systemName: "xmark.circle.fill").foregroundColor(.gray) }
}
}
}.padding()
}
func commit() {
coordinates.getLocation(from: self.myViewModel.manualLocation) { places in
coordinates.latitude = places?.latitude ?? 0.0
coordinates.longitude = places?.longitude ?? 0.0
}
myViewModel.load()
}
func clear() {
myViewModel.manualLocation = ""
myViewModel.load()
}
}

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