Combine asynchronous return values in SwiftUI - swiftui

I have 2 asynchronous return values from 2 different classes, one from HealthKit, the other from MotionManager. My goal is to combine these values and output them in a swiftui View, where it refreshes every second. I know I have to look at the combine framework here, but I don't know where to start. I can't find a lot of tutorials which describe Swiftui + Combine. I know I have to look at .combineLatest but do I have to write my own Publisher and Subscriber, or can I use #Published property wrapper I have here (#Published var motionData = MotionData() and #Published var heartRateValue: Double = 0.0) ?
My MotionManager Class:
struct MotionValues {
var rotationX: Double = 0.0
var rotationY: Double = 0.0
var rotationZ: Double = 0.0
var pitch: Double = 0.0
var roll: Double = 0.0
var yaw: Double = 0.0
}
class MotionManager: ObservableObject {
#Published var motionValues = MotionValues()
private let manager = CMMotionManager()
func startMotionUpdates() {
manager.deviceMotionUpdateInterval = 1.0
manager.startDeviceMotionUpdates(to: .main) { (data, error) in
guard let data = data, error == nil else {
print(error!)
return
}
self.motionValues.rotationX = data.rotationRate.x
self.motionValues.rotationY = data.rotationRate.y
self.motionValues.rotationZ = data.rotationRate.z
self.motionValues.pitch = data.attitude.pitch
self.motionValues.roll = data.attitude.roll
self.motionValues.yaw = data.attitude.yaw
}
}
func stopMotionUpdates() {
manager.stopDeviceMotionUpdates()
resetAllMotionData()
}
func resetAllMotionData() {
self.motionValues.rotationX = 0.0
self.motionValues.rotationY = 0.0
self.motionValues.rotationZ = 0.0
self.motionValues.pitch = 0.0
self.motionValues.roll = 0.0
self.motionValues.yaw = 0.0
}
}
My HealthKitManager Class:
class HealthKitManager: ObservableObject {
private var healthStore = HKHealthStore()
private var heartRateQuantity = HKUnit(from: "count/min")
private var activeQueries = [HKQuery]()
#Published var heartRateValue: Double = 0.0
func autorizeHealthKit() {
let heartRate = HKObjectType.quantityType(forIdentifier: .heartRate)!
let heartRateVariability = HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!
let HKreadTypes: Set = [heartRate, heartRateVariability]
healthStore.requestAuthorization(toShare: nil, read: HKreadTypes) { (success, error) in
if let error = error {
print("Error requesting health kit authorization: \(error)")
}
}
}
func fetchHeartRateData(quantityTypeIdentifier: HKQuantityTypeIdentifier ) {
let devicePredicate = HKQuery.predicateForObjects(from: [HKDevice.local()])
let updateHandler: (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void = {
query, samples, deletedObjects, queryAnchor, error in
guard let samples = samples as? [HKQuantitySample] else {
return
}
self.process(samples, type: quantityTypeIdentifier)
}
let query = HKAnchoredObjectQuery(type: HKObjectType.quantityType(forIdentifier: quantityTypeIdentifier)!, predicate: devicePredicate, anchor: nil, limit: HKObjectQueryNoLimit, resultsHandler: updateHandler)
query.updateHandler = updateHandler
healthStore.execute(query)
activeQueries.append(query)
}
private func process(_ samples: [HKQuantitySample], type: HKQuantityTypeIdentifier) {
for sample in samples {
if type == .heartRate {
DispatchQueue.main.async {
self.heartRateValue = sample.quantity.doubleValue(for: self.heartRateQuantity)
}
}
}
}
func stopFetchingHeartRateData() {
activeQueries.forEach { healthStore.stop($0) }
activeQueries.removeAll()
DispatchQueue.main.async {
self.heartRateValue = 0.0
}
}
}
I started with creating a combinedViewModel but I'm stuck here and don't know if this is the way to go:
class CombinedViewModel: ObservableObject {
#Published var motionManager: MotionManager = MotionManager()
#Published var healthManager: HealthKitManager = HealthKitManager()
var anyCancellable: AnyCancellable?
init() {
anyCancellable = Publishers
.CombineLatest(motionManager.$motionValues,healthManager.$heartRateValue)
.sink(receiveValue: {
// Do something
}
})
}
}
Where do I need to focus ? Do I need to learn the combine framework completely to write my own publishers and subscribers, or is there something available with #Published that can do the job ? Or do I need to go for another approach with my CombinedViewModel?
added contentView for reference:
struct ContentView: View {
#State var isActive: Bool = false
private var motion = MotionManager()
private var health = HealthKitManager()
#ObservedObject var combinedViewModel = CombinedViewModel(managerOne: motion, managerTwo: health)
private var motionValues: MotionValues {
return combinedViewModel.combinedValues.0
}
private var heartRateValue: Double {
return combinedViewModel.combinedValues.1
}
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Indicator(title: "X:", value: motionValues.rotationX)
Indicator(title: "Y:", value: motionValues.rotationY)
Indicator(title: "Z:", value: motionValues.rotationZ)
Divider()
Indicator(title: "Pitch:", value: motionValues.pitch)
Indicator(title: "Roll:", value: motionValues.roll)
Indicator(title: "Yaw:", value: motionValues.yaw)
Divider()
Indicator(title: "HR:", value: heartRateValue)
}
.padding(.horizontal, 10)
Button(action: {
self.isActive.toggle()
self.isActive ? self.start() : self.stop()
}) {
Text(isActive ? "Stop" : "Start")
}
.background(isActive ? Color.green : Color.blue)
.cornerRadius(10)
.padding(.horizontal, 5)
}.onAppear {
self.health.autorizeHealthKit()
}
}
private func start() {
self.motion.startMotionUpdates()
self.health.fetchHeartRateData(quantityTypeIdentifier: .heartRate)
}
private func stop() {
self.motion.stopMotionUpdates()
self.health.stopFetchingHeartRateData()
}
}

You can create a new publisher (I would recommend an AnyPublisher) in your CombinedViewModel that combines the output from both. Here's a simplified version of your code with a CombinedViewModel:
class ManagerOne {
#Published var someValue = "Some Value"
}
class ManagerTwo {
#Published var otherValue = "Other Value"
}
class CombinedViewModel {
var combinedPublisher: AnyPublisher<(String, String), Never>
init(managerOne: ManagerOne, managerTwo: ManagerTwo) {
combinedPublisher = managerOne.$someValue
.combineLatest(managerTwo.$otherValue)
.eraseToAnyPublisher()
}
}
If you need CombinedViewModel to be an observed object you would adapt the code to be more like this:
class CombinedViewModel: ObservableObject {
#Published var combinedValue: (String, String) = ("", "")
var cancellables = Set<AnyCancellable>()
init(managerOne: ManagerOne, managerTwo: ManagerTwo) {
managerOne.$someValue
.combineLatest(managerTwo.$otherValue)
.sink(receiveValue: { [weak self] combined in
self?.combinedValue = combined
})
.store(in: &cancellables)
}
}
A side note about this:
#Published var motionManager: MotionManager = MotionManager()
#Published var healthManager: HealthKitManager = HealthKitManager()
Since both of these managers are classes, $motionManager and $healthManager will only emit values when you assign a new instance of MotionManager or HealthKitManager to them. Not when a property of either manager changes.

Related

#Published value don't pass through views

I'm beggining with SwiftUI and I wanted to develop a small simple app to practice. I have a problem with #Published property that don't pass through views and so don't update the view.
I explain : In the first view I calculate the vMoyenne property and update it. I wanted to show this value in the next view ("Passage") to be able to use it for some other calculation but I tried many thing and the value in the "Passage" View doesn't update...
Here is the code :
ContentView.swift :
struct ContentView: View {
var body: some View {
TabView {
SpeedView().tabItem {
Label("Vitesse", systemImage: "figure.run.circle.fill")
}
PassageView(parameters: Parameters()).tabItem {
Label("Passage", systemImage: "timer.circle.fill")
}
}
}
}
Parameters.swift
class Parameters: ObservableObject {
#Published var distance: Double?
static let units = ["m", "km"]
#Published var unit = 1
#Published var hour: Int = 0
#Published var minute: Int = 0
#Published var second: Int = 0
#Published var vMoyenne = 0.0
#Published var allure = 0.0
#Published var convertedDecimalToSeconds = 0
var time: Int?
...
func calcVMoy() -> Void{
var d = distance!
let t = Double(time!) / 3600
var unite: String {
return Parameters.units[unit]
}
var calc = 0.0
if unite == "km" {
calc = d / t
} else {
d = d / 1000
calc = d / t
}
vMoyenne = calc
}
...
init() {
}
}
**SpeedView.swift **
struct SpeedView: View {
#ObservedObject var parameters = Parameters()
...
...
Button {
showVMoy = true
disableChange = true
if parameters.distance == nil {
parameters.distance = 0
} else {
parameters.runCalc()
}
} label: {
Text("Calculer")
}
... *// Here I can show and see the calculated vMoyenne property without problem...*
...
}
And the PassageView.swift where I want to show the vMoyenne property...
struct PassageView: View {
#ObservedObject var parameters:Parameters
var body: some View {
Text("\(parameters.vMoyenne)") *//want to show the vMoyenne value that we calculate previously but it always show 0,000...*
}
}
Thanks a lot for your help !!
PS : I tried many things like using didSet but I don't understand what I did wrong...
I found some post on stackoverflow but when I tried it doesn't work...
If you update the ContentView to it should work. The problem was that the SpeedView and PassageView were not sharing the same parameters object
struct ContentView: View {
#StateObject var parameters: Parameters = .init()
var body: some View {
TabView {
SpeedView(parameters: parameters).tabItem {
Label("Vitesse", systemImage: "figure.run.circle.fill")
}
PassageView(parameters: parameters).tabItem {
Label("Passage", systemImage: "timer.circle.fill")
}
}
}
}

Why can't I write to an #ObservedObject?

I have a struct called Activity which has an id (UUID), name (String), description (String) and timesCompleted (Int).
I also have a class called Activities that contains an array of Activity structs called activityList. Activities is marked with ObservableObject.
I have activities declared as a #StateObject in my ContentView and I pass it to my ActivityDetailView where it is declared as an #ObservedObject.
However I can only partially write to activities.activityList in the child view. I can append, but I can't overwrite, update or remove an element from the array. No error is thrown but the view immediately crashes and the app returns to the main ContentView.
How do you update/write to an #ObservedObject? As you can see from the comments in my updateTimesCompleted() function I've tried all kinds of things to update/overwrite an existing element. All crash silently and return to ContentView. Append does not fail, but isn't the behavior I want, I want to update/overwrite an array element, not append a new copy.
Activity Struct:
struct Activity : Codable, Identifiable, Equatable {
var id = UUID()
var name: String
var description: String
var timesCompleted: Int
}
Activities Class:
class Activities: ObservableObject {
#Published var activityList = [Activity]() {
didSet {
if let encoded = try? JSONEncoder().encode(activityList) {
UserDefaults.standard.set(encoded, forKey: "activityList")
}
}
}
init() {
if let savedList = UserDefaults.standard.data(forKey: "activityList") {
if let decodedList = try? JSONDecoder().decode([Activity].self, from: savedList) {
activityList = decodedList
return
}
}
activityList = []
}
init(activityList: [Activity]) {
self.activityList = activityList
}
subscript(index: Int) -> Activity {
get {
assert(index < activityList.count, "Index out of range")
return activityList[index]
}
set {
assert(index < activityList.count, "Index out of range")
activityList[index] = newValue
}
}
}
ContentView:
struct ContentView: View {
#StateObject var activities = Activities()
#State private var showingAddActivity = false
var body: some View {
NavigationView {
List {
ForEach(activities.activityList) { activity in
NavigationLink {
ActivityDetailView(activity: activity, activities: activities)
} label: {
Text(activity.name)
}
}
}
.navigationTitle("Habits")
.toolbar {
Button {
showingAddActivity = true
let _ = print("add activity")
}
label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddActivity) {
AddActivityView(activities: activities)
}
}
}
ActivityDetailView:
struct ActivityDetailView: View {
#State private var timesCompleted = 0
let activity: Activity
#ObservedObject var activities: Activities
var body: some View {
NavigationView {
Form {
Text("Activity: \(activity.name)")
Text("Description: \(activity.description)")
Stepper {
Text("Times Completed: \(timesCompleted)")
} onIncrement: {
timesCompleted += 1
updateTimesCompleted()
} onDecrement: {
if timesCompleted > 0 {
timesCompleted -= 1
updateTimesCompleted()
}
}
}
.navigationTitle("Activity Details")
}
}
func updateTimesCompleted() {
let newActivity = Activity(name: activity.name, description: activity.description, timesCompleted: timesCompleted)
let _ = print("count: \(activities.activityList.count)")
let index = activities.activityList.firstIndex(of: activity)
let _ = print(index ?? -666)
if let index = index {
activities.activityList[index] = Activity(name: activity.name, description: activity.description, timesCompleted: timesCompleted)
//activities.activityList.swapAt(index, activities.activityList.count - 1)
//activities.activityList[index].incrementTimesCompleted()
//activities.activityList.append(newActivity)
//activities.activityList.remove(at: index)
//activities.activityList.removeAll()
//activities.activityList.append(newActivity)
}
}
}
You could try this approach, where the activity is passed to the ActivityDetailView
as a binding.
In addition, #ObservedObject var activities: Activities is used directly in AddActivityView to add an Activity to the list.
struct Activity : Codable, Identifiable, Equatable {
let id = UUID() // <-- here
var name: String
var description: String
var timesCompleted: Int
enum CodingKeys: String, CodingKey { // <-- here
case name,description,timesCompleted
}
}
class Activities: ObservableObject {
#Published var activityList = [Activity]() {
didSet {
if let encoded = try? JSONEncoder().encode(activityList) {
UserDefaults.standard.set(encoded, forKey: "activityList")
}
}
}
init() {
if let savedList = UserDefaults.standard.data(forKey: "activityList") {
if let decodedList = try? JSONDecoder().decode([Activity].self, from: savedList) {
activityList = decodedList
return
}
}
activityList = []
}
init(activityList: [Activity]) {
self.activityList = activityList
}
subscript(index: Int) -> Activity {
get {
assert(index < activityList.count, "Index out of range")
return activityList[index]
}
set {
assert(index < activityList.count, "Index out of range")
activityList[index] = newValue
}
}
}
struct ContentView: View {
#StateObject var activities = Activities()
#State private var showingAddActivity = false
var body: some View {
NavigationView {
List {
ForEach($activities.activityList) { $activity in // <-- here
NavigationLink {
ActivityDetailView(activity: $activity) // <-- here
} label: {
Text(activity.name)
}
}
}
.navigationTitle("Habits")
.toolbar {
Button {
showingAddActivity = true
}
label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddActivity) {
AddActivityView(activities: activities)
}
.onAppear {
// for testing
if activities.activityList.isEmpty {
activities.activityList.append(Activity(name: "activity-1", description: "activity-1", timesCompleted: 1))
activities.activityList.append(Activity(name: "activity-2", description: "activity-2", timesCompleted: 2))
activities.activityList.append(Activity(name: "activity-3", description: "activity-3", timesCompleted: 3))
}
}
}
}
// -- here for testing
struct AddActivityView: View {
#ObservedObject var activities: Activities
var body: some View {
Text("AddActivityView")
Button("add activity") {
activities.activityList.append(Activity(name: "workingDog", description: "workingDog", timesCompleted: 5))
}
}
}
struct ActivityDetailView: View {
#Binding var activity: Activity // <-- here
var body: some View {
Form {
Text("Activity: \(activity.name)")
Text("Description: \(activity.description)")
Stepper {
Text("Times Completed: \(activity.timesCompleted)")
} onIncrement: {
activity.timesCompleted += 1 // <-- here
} onDecrement: {
if activity.timesCompleted > 0 {
activity.timesCompleted -= 1 // <-- here
}
}
}
}
}

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: How to publish a variable in a class member object (another instance of a class) and update UI in View

I have a class PlayAudio to read an audio file and play. In PlayAudio, I have #objc updateUI function to add to CADisplayLink. I have another class Updater where I initialize and control isPaused of CADisplayLink. I've instantiated #Published var playAudio: PlayAudio so I can call it from View as updater.playAudio. My question is, although I can print playAudio.positionSliderValue real time in active CADisplayLink, playAudio.positionSliderValue does not update the UI in View. How can I achieve it? I want to activate and deActivate CADisplayLink from a separate class to maintain weak ownership (If I'm not mistaken...).
When #State var volume is updated, volume slider also updates, so I think I'm successfully updating the value itself, but I can't figure it out that update to trigger updates in UI. Any thoughts or suggestions are appreciated. Thanks.
import SwiftUI
import AVKit
struct ContentView: View {
#ObservedObject var updater = Updater()
#State var volume = 0.0
var body: some View {
Text("\(volume)")
VStack {
Slider(value:
// in order to get continuous value changes, I do this instead of $updater.playAudio.volumeSliderValue
Binding(get: {
updater.playAudio.volumeSliderValue
}, set: { (newValue) in
updater.playAudio.volumeSliderValue = newValue
updater.playAudio.setVolume()
volume = newValue
})
, in: 0...1)
Button(action: {
updater.playAudio.play()
// activate CADisplayLink
updater.activate()
// run CADisplayLink
updater.updater?.isPaused = false
}, label: {
Text("Play File")
})
Slider(value:
// in order to get continuous value changes, I do this instead of $playAudio.positionSliderValue
Binding(get: {
updater.playAudio.positionSliderValue
}, set: { (newValue) in
updater.playAudio.positionSliderValue = newValue
updater.playAudio.seek()
})
, in: 0.0...updater.playAudio.positionSliderTotal) { _ in
updater.playAudio.seek()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Updater: ObservableObject {
var updater: CADisplayLink?
#Published var playAudio: PlayAudio
init(){
self.playAudio = PlayAudio()
self.updater = CADisplayLink(target: playAudio, selector: #selector(playAudio.updateUI))
}
func activate() {
self.updater?.add(to: .main, forMode: .default)
}
func deActivate() {
self.updater?.invalidate()
}
}
class PlayAudio: ObservableObject {
var sampleRate = Double()
var totalFrame = AVAudioFramePosition()
var startTime = AVAudioTime()
var newFramePosition = AVAudioFramePosition()
let url = Bundle.main.urls(forResourcesWithExtension: "mp4", subdirectory: nil)?.first
var audioFile = AVAudioFile()
var engine = AVAudioEngine()
var avAudioPlayerNode = AVAudioPlayerNode()
#Published var volumeSliderValue: Double = 0.7
#Published var positionSliderTotal: Double = 0.0
#Published var positionSliderValue: Double = 0.0
#objc func updateUI() {
positionSliderValue = Double(currentFrame)
// this prints ok, but I want it to update the UI in the View
print(positionSliderValue)
}
init () {
readFile()
schedulePlayer()
getTotalFrameDouble()
}
var currentFrame: AVAudioFramePosition {
guard let lastRenderTime = avAudioPlayerNode.lastRenderTime,
let playerTime = avAudioPlayerNode.playerTime(forNodeTime: lastRenderTime)
else {
return 0
}
return playerTime.sampleTime + newFramePosition
}
func getTotalFrameDouble() {
positionSliderTotal = Double(totalFrame)
print(positionSliderValue)
}
func readFile() {
guard let url = url else {
return
}
do {
self.audioFile = try AVAudioFile(forReading: url)
} catch let error {
print(error)
}
self.sampleRate = audioFile.processingFormat.sampleRate
self.totalFrame = audioFile.length
}
func setupEngine() {
engine.attach(avAudioPlayerNode)
engine.connect(avAudioPlayerNode, to: engine.mainMixerNode, format: audioFile.processingFormat)
engine.prepare()
do {
try engine.start()
} catch let error {
print(error)
}
}
func schedulePlayer() {
newFramePosition = 0
engine.reset()
setupEngine()
avAudioPlayerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
}
func play() {
let outputFormat = avAudioPlayerNode.outputFormat(forBus: AVAudioNodeBus(0))
let lastRenderTime = avAudioPlayerNode.lastRenderTime?.sampleTime ?? 0
// need to convert from AVAudioFramePosition to AVAudioTime
startTime = AVAudioTime(sampleTime: AVAudioFramePosition(Double(lastRenderTime)), atRate: Double(outputFormat.sampleRate))
avAudioPlayerNode.play(at: startTime)
}
func seek() {
// player time (needs to be converted to player node time
newFramePosition = AVAudioFramePosition(positionSliderValue)
let framesToPlay = totalFrame - newFramePosition
avAudioPlayerNode.stop()
if framesToPlay > 100 {
avAudioPlayerNode.scheduleSegment(audioFile, startingFrame: newFramePosition, frameCount: AVAudioFrameCount(framesToPlay), at: nil, completionHandler: nil)
}
play()
}
func setVolume() {
avAudioPlayerNode.volume = Float(volumeSliderValue)
}
}

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