How to assign sink result to variable - swift

I would like to assign a result form a notification center publisher to the variable alert. The Error that I get is:
Cannot use instance member 'alerts' within property initializer; property initializers run before 'self' is available
Could Someone help me out here?
import Foundation
import SwiftUI
import Combine
final class PublicAlerts: ObservableObject{
init () {
fetchAlerts()
}
var alerts = [String](){
didSet {
didChange.send(self)
}
}
private func fetchPublicAssets(){
backEndService().fetchAlerts()
}
let publicAssetsPublisher = NotificationCenter.default.publisher(for: .kPublicAlertsNotification)
.map { notification in
return notification.userInfo?["alerts"] as! Array<String>
}.sink {result in
alerts = result
}
let didChange = PassthroughSubject<PublicAlerts, Never>()
}
Later I will use alerts this in SwiftUI as a List

Move the subscribtion in init
final class PublicAlerts: ObservableObject{
var anyCancelable: AnyCancellable? = nil
init () {
anyCancelable = NotificationCenter.default.publisher(for: .kPublicAlertsNotification)
.map { notification in
return notification.userInfo?["alerts"] as! Array<String>
}.sink {result in
alerts = result
}
fetchAlerts()
}
var alerts = [String](){
didSet {
didChange.send(self)
}
}
private func fetchPublicAssets(){
backEndService().fetchAlerts()
}
let didChange = PassthroughSubject<PublicAlerts, Never>()
}

Related

SwiftUI - changes in nested View Model classes not detected using onChange method

I have a nested View Model class WatchDayProgramViewModel as an ObservableObject. Within WatchDayProgramViewModel, there is a WorkoutModel that is a child class. I want to detect any updates in the currentHeartRate to trigger data transfer to iPhone.
Hence, I tried from ContentView using WatchDayProgramViewModel as an EnvironmentObject and detecting changes in WorkoutModel via onChange() method. But it seems that SwiftUI views does not detect any property changes in WorkoutModel.
I understand that this issue could be due to ObservableObject not detecting changes in child/nested level of classes, and SO answer (SwiftUI change on multilevel children Published object change) suggests using struct instead of class. But changing WorkoutModel to struct result in various #Published properties and functions to show error.
Is there any possible way to detect changes in child View Model from the ContentView itself?
ContentView
struct ContentView: View {
#State var selectedTab = 0
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
WatchControlView().id(0)
NowPlayingView().id(1)
}
.environmentObject(watchDayProgramVM)
.onChange(of: self.watchDayProgramVM.workoutModel.currentHeartRate) { newValue in
print("WatchConnectivity heart rate from contentView \(newValue)")
}
}
}
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
#Published var workoutModel = WorkoutModel()
init() {
}
}
WorkoutModel
import Foundation
import HealthKit
class WorkoutModel: NSObject, ObservableObject {
let healthStore = HKHealthStore()
var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?
#Published var currentHeartRate: Double = 0
#Published var workout: HKWorkout?
//Other functions to start/run workout hidden
func updateForStatistics(_ statistics: HKStatistics?) {
guard let statistics = statistics else {
return
}
DispatchQueue.main.async {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
self.currentHeartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) ?? 0
default:
return
}
}//end of dispatchqueue
}// end of function
}
extension WorkoutModel: HKLiveWorkoutBuilderDelegate {
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else {
return
}
let statistics = workoutBuilder.statistics(for: quantityType)
updateForStatistics(statistics)
}
}
}
Try to change
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
with
#ObservedObject var watchDayProgramVM = WatchDayProgramViewModel()
Figure it out. Just had to create another AnyCancellable variable to call objectWillChange publisher.
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
#Published var workoutModel = WorkoutModel()
var cancellable: AnyCancellable?
init() {
cancellable = workoutModel.objectWillChange
.sink { _ in
self.objectWillChange.send()
}
}
}
While I have provided my answer, that worksaround with viewmodels, I would love to see/get advice on other alternatives.

Trying to set #published bool to true based on results from an API call

Hi first off I'm very new to swift and programing (coming from design field).
I'm trying to update doesNotificationsExist based on posts.count
I'm getting true inside the Api().getPosts {}
Where I print the following:
print("Api().getPosts")
print(doesNotificationExist)
but outside (in the loadData() {}) I still get false and not the #Publihed var doesNotificationExist:Bool = false doesn't update.
Please help me out, I would really appreciate some guidance to what I'm doing wrong and what I need to do.
Here is my code:
import SwiftUI
import Combine
public class DataStore: ObservableObject {
#Published var posts: [Post] = []
#Published var doesNotificationExist:Bool = false
init() {
loadData()
startApiWatch()
}
func loadData() {
Api().getPosts { [self] (posts) in
self.posts = posts
if posts.count >= 1 {
doesNotificationExist = true
}
else {
doesNotificationExist = false
}
print("Api().getPosts")
print(doesNotificationExist)
}
print("loadData")
print(doesNotificationExist)
}
func startApiWatch() {
Timer.scheduledTimer(withTimeInterval: 60, repeats: true) {_ in
self.loadData()
}
}
View where I'm trying to set an image based on store.doesNotificationsExist
StatusBarController:
import AppKit
import SwiftUI
class StatusBarController {
private var statusBar: NSStatusBar
private var statusItem: NSStatusItem
private var popover: NSPopover
#ObservedObject var store = DataStore()
init(_ popover: NSPopover)
{
self.popover = popover
statusBar = NSStatusBar.init()
statusItem = statusBar.statusItem(withLength: 28.0)
statusItem.button?.action = #selector(togglePopover(sender:))
statusItem.button?.target = self
if let statusBarButton = statusItem.button {
let itemImage = NSImage(named: store.doesNotificationExist ? "StatusItemImageNotification" : "StatusItemImage")
statusBarButton.image = itemImage
statusBarButton.image?.size = NSSize(width: 18.0, height: 18.0)
statusBarButton.image?.isTemplate = true
statusBarButton.action = #selector(togglePopover(sender:))
statusBarButton.target = self
}
}
`Other none relevant code for the question`
}
It’s a closure and hopefully the #escaping one. #escaping is used to inform callers of a function that takes a closure that the closure might be stored or otherwise outlive the scope of the receiving function. So, your outside print statement will be called first with bool value false, and once timer is completed closure will be called changing your Bool value to true.
Check code below -:
import SwiftUI
public class Model: ObservableObject {
//#Published var posts: [Post] = []
#Published var doesNotificationExist:Bool = false
init() {
loadData()
// startApiWatch()
}
func loadData() {
getPost { [weak self] (posts) in
//self.posts = posts
if posts >= 1 {
self?.doesNotificationExist = true
}
else {
self?.doesNotificationExist = false
}
print("Api().getPosts")
print(self?.doesNotificationExist)
}
print("loadData")
print(doesNotificationExist)
}
func getPost(completion:#escaping (Int) -> ()){
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) {_ in
completion(5)
}
}
}
struct Test1:View {
#ObservedObject var test = Model()
var body: some View{
Text("\(test.doesNotificationExist.description)")
}
}

Make button enabled depending on strings using Combine

I have a login button named proceed, and have 2 #Published properties that represent current typed text in UITextField.
What i want is, to assign result of .filter { !$0.0.isEmpty && !$0.1.isEmpty} to Bool variable isEnabled of my UIButton
Currently i ended up with this:
passwordTxtf.textView.$text
.combineLatest(loginTxtf.textView.$text)
.filter { !$0.0.isEmpty && !$0.1.isEmpty}
.sink(receiveValue: { [weak self] in
guard let weakSelf = self else { return }
weakSelf.loginEntered = $0
weakSelf.passEntered = $1
weakSelf.setProceedEnableState()
})
.store(in: &subscriptions)
Where setProceedEnableState() is a function that checks class variables and therefore determine button enabled state.
But i want somehow assign that during "pipeline", or do more elegant way
You could structure the code such that each of the signals from textview's are assigned to Subjects. Here is a small example based on your usecase,
let isProcessEnabled = PassthroughSubject<Bool, Never>()
let loginEntered = PassthroughSubject<Bool, Never>()
let passEntered = CurrentValueSubject<Bool, Never>(false)
passwordTxtf.textView.$text.map { !$0.isEmpty }
.subscribe(passEntered)
.store(in: &store)
loginTxtf.textView.$text.map { !$0.isEmpty }
.subscribe(loginEntered)
.store(in: &store)
Publishers.CombineLatest(passEntered, loginEntered)
.map { $0 && $1 }
.subscribe(isProcessEnabled)
.store(in: &store)
isProcessEnabled.sink { isEnabled in
print("Is processing enabled", isEnabled)
}.store(in: &store)
The biggest advantage of this is that you could move isProcessEnabled, loginEntered and passEntered to your viewmodel or some other object, which would make the whole code more testable. Also, you can see, that you could combine each of these subjects in some interesting way to create some new publisher / subject.
class ViewModel {
private var store = Set<AnyCancellable>()
var isProcessingEnabled: AnyPublisher<Bool, Never> {
return processEnabled.eraseToAnyPublisher()
}
var isLoginEntered: AnyPublisher<Bool, Never> {
return loginEntered.eraseToAnyPublisher()
}
var isPassEntered: AnyPublisher<Bool, Never> {
return passwordEntered.eraseToAnyPublisher()
}
func subscribeLoginText(publisher: AnyPublisher<String, Never>) {
publisher.map { !$0.isEmpty }.subscribe(loginEntered).store(in: &store)
}
func subscribePasswordText(publisher: AnyPublisher<String, Never>) {
publisher.map { !$0.isEmpty }.subscribe(passwordEntered).store(in: &store)
}
private var processEnabled = CurrentValueSubject<Bool, Never>(false)
private var loginEntered = PassthroughSubject<Bool, Never>()
private var passwordEntered = PassthroughSubject<Bool, Never>()
init() {
Publishers.CombineLatest(passwordEntered, loginEntered)
.map { $0 && $1 }
.subscribe(processEnabled)
.store(in: &store)
}
}
And, you can see that now you have modularized the app and your viewmodel does not need to know specifics of your view / viewcontroller. On the top of that, your code is now, so much testable. You could create an instance of viewmodel and assert your assumptions.
Here is a small sample test case for the above viewmodel implementation,
class ViewModelTest: XCTestCase {
var store: Set<AnyCancellable> = []
var login = PassthroughSubject<String, Never>()
var password = PassthroughSubject<String, Never>()
var isProcessingEnabled = CurrentValueSubject<Bool, Never>(false)
var viewModel: ViewModel = ViewModel()
override func setUp() {
store = []
viewModel = ViewModel()
login = PassthroughSubject<String, Never>()
password = PassthroughSubject<String, Never>()
isProcessingEnabled = CurrentValueSubject<Bool, Never>(false)
viewModel.isProcessingEnabled.subscribe(isProcessingEnabled).store(in: &store)
viewModel.subscribeLoginText(publisher: login.eraseToAnyPublisher())
viewModel.subscribePasswordText(publisher: password.eraseToAnyPublisher())
}
func testThatProcessingIsDisabledInitially() {
XCTAssertFalse(isProcessingEnabled.value, "Processing is enabled")
}
func testThatProcessingIsDisabledWhenOnlyLoginIsEmpty() {
login.send("")
password.send("b")
XCTAssertFalse(isProcessingEnabled.value, "Processing is enabled")
}
func testThatProcessingIsDisabledWhenOnlyPasswordIsEmpty() {
login.send("a")
password.send("")
XCTAssertFalse(isProcessingEnabled.value, "Processing is enabled")
}
func testThatProcessingIsEnabledWhenLoginAndPasswordAreNotEmpty() {
login.send("a")
password.send("b")
XCTAssertTrue(isProcessingEnabled.value, "Processing is disabled")
}
}

Using Combine to parse phone number String

I'm trying to wrap my mind around how Combine works. I believe I'm doing something wrong when I use the .assign operator to mutate the #Published property I'm operating on. I've read the documentation on Publishers, Subscribers, and Operators. But I'm a bit loose on where exactly to create the Publisher if I don't want it to be a function call.
import SwiftUI
import Combine
struct PhoneNumberField: View {
let title: String
#ObservedObject var viewModel = ViewModel()
var body: some View {
TextField(title,text: $viewModel.text)
}
class ViewModel: ObservableObject {
#Published var text: String = ""
private var disposables = Set<AnyCancellable>()
init() {
$text.map { value -> String in
self.formattedNumber(number: value)
}
//something wrong here
.assign(to: \.text, on: self)
.store(in: &disposables)
}
func formattedNumber(number: String) -> String {
let cleanPhoneNumber = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let mask = "+X (XXX) XXX-XXXX"
var result = ""
var index = cleanPhoneNumber.startIndex
for ch in mask where index < cleanPhoneNumber.endIndex {
if ch == "X" {
result.append(cleanPhoneNumber[index])
index = cleanPhoneNumber.index(after: index)
} else {
result.append(ch)
}
}
return result
}
}
}
struct PhoneNumberParser_Previews: PreviewProvider {
static var previews: some View {
PhoneNumberField(title: "Phone Number")
}
}
Use .receive(on:):
$text.map { self.formattedNumber(number: $0) }
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.text = value
})
.store(in: &disposables)
This will allow you to listen to changes of the text variable and update it in the main queue. Using main queue is necessary if you want to update #Published variables read by some View.
And to avoid having a retain cycle (self -> disposables -> assign -> self) use sink with a weak self.

How to handle navigation with observables using Rx-MVVM-C

Hello I am trying to do a project with RxSwift and I am stuck trying to do in a properly way the connection between the Coordinator and the ViewModel.
Goal
Using observables, the Coordinator receives and event (in that case, when a row has been tapped) then does whatever.
Scenario
Giving a Post (String)
typealias Post = String
I have the following Coordinator:
class Coordinator {
func start() {
let selectedPostObservable = PublishSubject<Post>()
let viewController = ViewController()
let viewModel = ViewModel()
viewController.viewModel = viewModel
selectedPostObservable.subscribe { post in
//Do whatever
}
}
}
The selectedPostObservable is what I don't know how to connect it in a "clean" way with the viewModel.
As ViewModel:
class ViewModel {
struct Input {
let selectedIndexPath: Observable<IndexPath>
}
struct Output {
//UI Outputs
}
func transform(input: Input) -> Output {
let posts: [Post] = Observable.just(["1", "2", "3"])
//Connect with selectedindex
let result = input.selectedIndexPath
.withLatestFrom(posts) { $1[$0.row] }
.asDriver(onErrorJustReturn: nil)
return Output()
}
}
The result variable is what I should connect with selectedPostObservable.
And the ViewController (although I think is not relevant for the question):
class ViewController: UIViewController {
//...
var viewModel: ViewModel!
var tableView: UITableView!
//...
func bindViewModel() {
let input = ViewModel.Input(selectedIndexPath: tableView.rx.itemSelected.asObservable())
viewModel.transform(input: input)
}
}
Thank you so much.
Working with the structure you are starting with, I would put the PublishSubject in the ViewModel class instead of the Coordinator. Then something like this:
class ViewModel {
struct Input {
let selectedIndexPath: Observable<IndexPath>
}
struct Output {
//UI Outputs
}
let selectedPost = PublishSubject<Post>()
let bag = DisposeBag()
func transform(input: Input) -> Output {
let posts: [Post] = Observable.just(["1", "2", "3"])
//Connect with selectedindex
input.selectedIndexPath
.withLatestFrom(posts) { $1[$0.row] }
.bind(to: selectedPost)
.disposed(by: bag)
return Output()
}
}
class Coordinator {
func start() {
let viewController = ViewController()
let viewModel = ViewModel()
viewController.viewModel = viewModel
viewModel.selectedPost.subscribe { post in
//Do whatever
}
.disposed(by: viewModel.bag)
}
}