I'm writing a SwiftUI code for a carousel, and I have a CarouselModel class that goes like this:
CarouselModel.swift
import Foundation
import SwiftUI
class CarouselModel: ObservableObject {
//MARK: - Properties
#Published private var imagesCount: Int
#Published private var currentImage: Int
#Published private var offsetMainAxis: CGFloat
private var screenSize: CGFloat
private var isHorizontal: Bool
private var padding: CGFloat
private var nextImage: Int {
return (self.currentImage+1) % self.imagesCount
}
private var prevImage: Int {
return (self.currentImage-1+self.imagesCount) % self.imagesCount
}
private var centralIndex: CGFloat {
return CGFloat(self.imagesCount-1)/2
}
public var computedOffset: CGFloat {
return (centralIndex-CGFloat(currentImage))*(screenSize-padding) + offsetMainAxis
}
init(imagesCount: Int, isHorizontal: Bool = true, padding: CGFloat = 20, currentImage: Int = 0) {
self.imagesCount = imagesCount
self.currentImage = currentImage
self.offsetMainAxis = 0
self.padding = padding
self.isHorizontal = isHorizontal
if isHorizontal {
self.screenSize = UIScreen.main.bounds.width
} else {
self.screenSize = UIScreen.main.bounds.height
}
}
//MARK: - Methods
public func getCurrentImage() -> Int {
return self.currentImage
}
public func goNext() -> Void {
withAnimation(.easeOut(duration: 0.5)) {
currentImage = nextImage
}
}
public func goPrevious() -> Void {
withAnimation(.easeOut(duration: 0.5)) {
currentImage = prevImage
}
}
public func onDragChange(offset: CGFloat) -> Void {
self.offsetMainAxis = offset
}
public func onDragEnd() -> Void {
if abs(offsetMainAxis) > 0.2*screenSize {
if offsetMainAxis > 0 {
withAnimation(.easeOut(duration: 0.5)) {
offsetMainAxis = 0
currentImage = prevImage
}
} else {
withAnimation(.easeOut(duration: 0.5)) {
offsetMainAxis = 0
currentImage = nextImage
}
}
} else {
withAnimation(.easeOut(duration: 0.5)) {
offsetMainAxis = 0
}
}
}
public func skipTo(number i: Int) {
withAnimation(.easeOut(duration: 0.5)) {
currentImage = i
}
}
}
At this point every time I need to place a concrete Carousel in my app I create a CarouselView.swift file containing some View, and the thing goes more or less like this:
CarouselView.swift
import SwiftUI
struct Carousel: View {
#StateObject var carousel: CarouselModel
private var screenWidth: CGFloat = UIScreen.main.bounds.width
private var images: [String]
private let padding: CGFloat
private var descriptions: [String]?
#State private var imageSelected: [String:Bool]
init(images: [String], descriptions: [String]?, padding: CGFloat = 20) {
self.images = images
self.padding = padding
_carousel = StateObject(
wrappedValue:
CarouselModel(
imagesCount: images.count,
isHorizontal: true,
padding: padding
)
)
self.imageSelected = Dictionary(uniqueKeysWithValues: images.map{ ($0, false) })
guard let desc = descriptions else {
self.descriptions = nil
return
}
self.descriptions = desc
}
var body: some View {
// USE carousel TO DISPLAY SOME CAROUSEL IMPLEMENTATION
}
}
Now, a problem arises when inside some container I need to display a different set of images based on a #State private var selectedImgSet: Int and each set has a different amount of pictures in it. The point is, whenever I call CarouselView(...) inside the container and its parameters change, its init is invoked but the CarouselModel doesn't get updated with the new images count.
This is totally expected since it is a #StateObject but I'm not sure how the update should happen. I can't use the .onAppear(perform: ) hook in the CarouselView since that is executed only once, when the CarouselView first appears.
What is the correct way to update the carousel: CarouselModel variable whenever CarouselView's init parameters change?
EDIT: Here are pastes for a full working examples: ContentView, CarouselItem, CarouselModel, CarouselView, Items. Add the following images sets to your assets: item00, item01, item02, item10, item11, item20, item21, item22, item23. Any 1920x1080p placeholder will do the trick for now.
I eventually fixed my issue the following way: I introduced the following method in CarouselModel:
public func update(_ images: Int) {
print("Performing update with \(images) images")
self.imagesCount = images
self.currentImage = 0
self.offsetMainAxis = 0
}
Then I added to the outmost View inside CarouselView the following modifier:
.onChange(of: self.images) { v in
self.carousel.update(v.count)
}
Now the update happens as expected.
You've got things the wrong way round. Create the #StateObject in the View above Carousel View and pass it in as #ObservedObject. However, since your model does no loading or saving it doesn't need to be a reference type, i.e. an object. A value type will suffice, i.e. a struct, so it can be #State in the parent and pass in as #Binding so you can call its mutating functions.
Related
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")
}
}
}
}
I am adding the possibility to swipe in order to update a barchart. What I want to show is statistics for different station. To view different station I want the user to be able to swipe between the stations. I can see that the swiping works and each time I swipe I get the correct data from my controller. The problem is that my view is not redrawn properly.
I found this guide, but cannot make it work.
Say I swipe right from station 0 with data [100, 100, 100] to station 2, the retrieved data from my controller is [0.0, 100.0, 0.0]. The view I have still is for [100, 100, 100]`.
The station number is correctly updated, so I suspect it needs some state somehow.
Here is the code:
import SwiftUI
import SwiftUICharts
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName()).tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
struct BarCharts: View {
var data: [Double]
var title: String
init(data: [Double], disciplineName: String) {
self.data = data
title = disciplineName
print(data)
}
var body: some View {
VStack {
BarChartView(data: ChartData(points: self.data), title: self.title, style: Styles.barChartStyleOrangeLight, form: CGSize(width: 300, height: 400))
}
}
}
class ViewModel: ObservableObject {
#Published var station = 1
let controller = DetailedViewController()
var isPreview = false
func getData(kLatestRounds: Int, station: Int) -> [Double] {
if isPreview {
return [100.0, 100.0, 100.0]
} else {
let data = controller.getResults(kLatestRounds: kLatestRounds, station: station, fileName: userDataFile)
return data
}
}
func getName() -> String {
controller.getDiscipline().name
}
func getNumberOfStations() -> Int {
controller.getDiscipline().getNumberOfStations()
}
func getStation() -> Int {
station
}
func incrementStation() {
station = (station + 1) % getNumberOfStations()
}
func decrementStation() {
station -= 1
if station < 0 {
station = getNumberOfStations() - 1
}
}
}
The data is printed inside the constructor each time I swipe. Shouldn't that mean it should be updated?
I don’t use SwiftUICharts so I can’t test it, but the least you can try is manually set the id to the view
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName())
.id(viewmodel.station) // here. If it doesn’t work, you can set it to the whole TabView
.tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
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)
}
}
I came across a situation that you use class data as your data source, and display them in a swiftUI list view, when you update your data source, the swiftUI list view won't be updated, what can we do to make the class data updates interactive with swiftUI?
see code blow:
I define the environment object :
import Foundation
import Combine
class DataSource: ObservableObject {
public static let shared = DataSource()
#Published var datalist: [RowData] = []
func fetch() -> Void {
for n in 1...50 {
let data = RowData(title: "Index:\(n)", count: 0)
datalist.insert(data, at: 0)
}
}
func update() {
for data in datalist {
data.count = data.count+1
print("\(data.title) update count to :\(data.count)")
data.objectWillChange.send()
}
self.objectWillChange.send()
}
}
to display each data in a Row View:
import SwiftUI
struct RowView: View {
#State var data: RowData
var body: some View {
HStack{
Text(data.title)
Spacer()
Text("\(data.count)")
}.padding()
}
}
struct RowView_Previews: PreviewProvider {
static var previews: some View {
RowView(data: RowData(title: "text", count: 1))
}
}
class RowData: ObservableObject {
var title: String = ""
var count: Int = 0
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
in content view, display the data in a list view, I would like to refresh all the view updates when click update button. the button triggers the update methods to update the class data value from data source.
struct ContentView: View {
#EnvironmentObject var data: DataSource
#State var shouldUpdate:Bool = false
#State var localData:[RowData] = []
var body: some View {
VStack {
Button(action: {
// your action here
self.data.update()
self.shouldUpdate.toggle()
self.localData.removeAll()
self.localData = self.data.datalist
}) {
Text("update")
}
List {
ForEach(0..<self.localData.count, id:\.self) { index in
RowView(data: self.localData[index])
}
}
}
}
}
Well... I don't see the reason to have localData, but, anyway, here is modified code that works.
Tested with Xcode 12 / iOS 14
class DataSource: ObservableObject {
public static let shared = DataSource()
#Published var datalist: [RowData] = []
func fetch() -> Void {
for n in 1...50 {
let data = RowData(title: "Index:\(n)", count: 0)
datalist.insert(data, at: 0)
}
}
func update() {
for data in datalist {
data.count = data.count+1
print("\(data.title) update count to :\(data.count)")
}
self.objectWillChange.send()
}
}
struct RowView: View {
#ObservedObject var data: RowData
var body: some View {
HStack{
Text(data.title)
Spacer()
Text("\(data.count)")
}.padding()
}
}
class RowData: ObservableObject {
#Published var title: String = ""
#Published var count: Int = 0
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
struct ContentView: View {
#EnvironmentObject var data: DataSource
#State var localData:[RowData] = []
var body: some View {
VStack {
Button(action: {
// your action here
self.data.update()
self.localData = self.data.datalist
}) {
Text("update")
}
List {
ForEach(0..<self.localData.count, id:\.self) { index in
RowView(data: self.localData[index])
}
}
}
.onAppear {
self.data.fetch()
self.localData = self.data.datalist
}
}
}
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.