I'm trying to execute a bunch of operations sequentially and update my UI every time an operation it starts and it finishes (I have to update an operation status icon color).
Below my (working) code:
class SyncManager {
private let disposeBag = DisposeBag()
// MARK: - Private Init
private init() { }
// MARK: - Public Constants
static let shared = SyncManager()
let refreshTokenStatus = BehaviorRelay<SyncStatus>(value: .todo)
let updateCatalogDataStatus = BehaviorRelay<SyncStatus>(value: .todo)
let insertDataStatus = BehaviorRelay<SyncStatus>(value: .todo)
let getDataStatus = BehaviorRelay<SyncStatus>(value: .todo)
func startDatabaseSync(completion: #escaping ((Result<Void, Error>) -> Void)) {
refreshTokenStatus.accept(.todo)
updateCatalogDataStatus.accept(.todo)
insertDataStatus.accept(.todo)
getDataStatus.accept(.todo)
RefreshTokenManager.shared.refreshToken().do(onSuccess: { [self]_ in
print("RefreshTokenManager onSuccess")
refreshTokenStatus.accept(.completed)
}, onError: { [self] error in
print("RefreshTokenManager onError: \(error)")
refreshTokenStatus.accept(.error)
}, onSubscribe: { [self] in
print("RefreshTokenManager onSubscribe")
refreshTokenStatus.accept(.running)
}).asObservable().concatMap { result in
UpdateCatalogDataSyncManager.shared.updateCatalogData().do(onSuccess: { [self] in
print("UpdateCatalogDataSyncManager onSuccess")
updateCatalogDataStatus.accept(.completed)
}, onError: { [self] error in
print("UpdateCatalogDataSyncManager onError: \(error)")
updateCatalogDataStatus.accept(.error)
}, onSubscribe: { [self] in
print("UpdateCatalogDataSyncManager onSubscribe")
updateCatalogDataStatus.accept(.running)
}).asObservable().concatMap { result in
GetDataSyncManager.shared.getData().do { [self] in
print("GetDataSyncManager onSuccess")
getDataStatus.accept(.completed)
} onError: { [self] error in
print("GetDataSyncManager onError: \(error)")
getDataStatus.accept(.error)
} onSubscribe: { [self] in
print("GetDataSyncManager onSubscribe")
getDataStatus.accept(.running)
} onDispose: {
print("GetDataSyncManager onDispose")
}.asObservable().concatMap { _ in
InsertDataWorkSyncManager.shared.insertData().do { [self] in
print("InsertDataWorkSyncManager onSuccess")
insertDataStatus.accept(.completed)
} onError: { [self] error in
print("InsertDataWorkSyncManager onError: \(error)")
insertDataStatus.accept(.error)
} onSubscribe: { [self] in
print("InsertDataWorkSyncManager onSubscribe")
insertDataStatus.accept(.running)
} onDispose: {
print("InsertDataWorkSyncManager onDispose")
}
}
}
}.subscribe { _ in
print("SyncManager onNext")
} onError: { error in
print("SyncManager onError: \(error)")
completion(.failure(error))
} onCompleted: {
print("SyncManager onCompleted")
completion(.success(()))
} onDisposed: {
print("SyncManager onDisposed")
}.disposed(by: disposeBag)
}
}
enum SyncStatus {
case todo
case completed
case error
case running
case partial
}
My ViewController:
SyncManager.shared.refreshTokenStatus.skip(1).subscribe(onNext: { status in
// Update UI
}).disposed(by: disposeBag)
SyncManager.shared.updateCatalogDataStatus.skip(1).subscribe(onNext: { status in
// Update UI
}).disposed(by: disposeBag)
SyncManager.shared.insertDataStatus.skip(1).subscribe(onNext: { status in
// Update UI
}).disposed(by: disposeBag)
I'm new to RxSwift (I've been using it for only a week) so I would like to know if there's a better approach to achieve my above goal.
Here is an idea that I think will work. It is very imperative conceptually which makes it hard to transcribe to the functional declarative paradigm of FRP. I kept the same external interface so it could be a drop in replacement.
class SyncManager {
private init() { }
static let shared = SyncManager()
let refreshTokenStatus = BehaviorRelay<SyncStatus>(value: .todo)
let updateCatalogDataStatus = BehaviorRelay<SyncStatus>(value: .todo)
let insertDataStatus = BehaviorRelay<SyncStatus>(value: .todo)
let getDataStatus = BehaviorRelay<SyncStatus>(value: .todo)
private let disposeBag = DisposeBag()
func startDatabaseSync(completion: #escaping (Result<Void, Error>) -> Void) {
let sync = Sync.startDatabaseSync()
disposeBag.insert(
sync.refreshTokenStatus.bind(to: refreshTokenStatus),
sync.updateCatalogDataStatus.bind(to: updateCatalogDataStatus),
sync.insertDataStatus.bind(to: insertDataStatus),
sync.getDataStatus.bind(to: getDataStatus),
sync.getDataStatus.subscribe(
onError: { error in
completion(.failure(error))
},
onCompleted: {
completion(.success(()))
}
)
)
}
}
struct Sync {
let refreshTokenStatus: Observable<SyncStatus>
let updateCatalogDataStatus: Observable<SyncStatus>
let getDataStatus: Observable<SyncStatus>
let insertDataStatus: Observable<SyncStatus>
static func startDatabaseSync() -> Sync {
let refreshTokenStatus = handle(RefreshTokenManager.shared.refreshToken(), after: .just(.completed))
.catchAndReturn(.error)
let updateCatalogDataStatus = handle(UpdateCatalogDataSyncManager.shared.updateCatalogData(), after: refreshTokenStatus)
.catchAndReturn(.error)
let getDataStatus = handle(GetDataSyncManager.shared.getData(), after: updateCatalogDataStatus)
.catchAndReturn(.error)
let insertDataStatus = handle(InsertDataWorkSyncManager.shared.insertData(), after: getDataStatus)
.catchAndReturn(.error)
return Sync(
refreshTokenStatus: refreshTokenStatus,
updateCatalogDataStatus: updateCatalogDataStatus,
getDataStatus: getDataStatus,
insertDataStatus: insertDataStatus
)
}
}
func handle(_ operation: Single<Void>, after: Observable<SyncStatus>) -> Observable<SyncStatus> {
after
.ignoreElements()
.asCompletable()
.andThen(
operation
.map { SyncStatus.completed }
.asObservable()
.startWith(SyncStatus.running)
)
.startWith(.todo)
}
enum SyncStatus {
case todo
case completed
case error
case running
}
If you were to re-arrange the rest of the code to be more in the Rx style, then you could probably make this much cleaner...
Related
I'm building a Widget with dynamic configuration. I provide a dynamic list of options with an Intents Extension - inside Intent Handler (code below).
However only sampleData shows up, but not the userScrums when tapping on "Edit Widget".
Why it doesn't load userScrums?
import Intents
class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.
return self
}
}
extension IntentHandler: ScrumSelectionIntentHandling {
func provideScrumOptionsCollection(for intent: ScrumSelectionIntent) async throws -> INObjectCollection<ScrumType> {
let sampleScrums = DailyScrum.sampleData.map { scrum in
ScrumType(identifier: scrum.title, display: scrum.title)
}
let userScrums = try? await ScrumStore.load().map { scrum in
ScrumType(identifier: scrum.title, display: scrum.title)
}
let allScrums = sampleScrums + (userScrums ?? [])
let collection = INObjectCollection(items: allScrums)
return collection
}
// func provideScrumOptionsCollection(for intent: ScrumSelectionIntent, with completion: #escaping (INObjectCollection<ScrumType>?, Error?) -> Void) {}
}
import Foundation
import SwiftUI
class ScrumStore: ObservableObject {
#Published var scrums: [DailyScrum] = []
private static func fileURL() throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("scrums.data")
}
static func load() async throws -> [DailyScrum] {
try await withCheckedThrowingContinuation { continuation in
load { result in
switch result {
case .failure(let error):
continuation.resume(throwing: error)
case .success(let scrums):
continuation.resume(returning: scrums)
}
}
}
}
static func load(completion: #escaping (Result<[DailyScrum], Error>)->Void) {
DispatchQueue.global(qos: .background).async {
do {
let fileURL = try fileURL()
guard let file = try? FileHandle(forReadingFrom: fileURL) else {
DispatchQueue.main.async {
completion(.success([]))
}
return
}
let dailyScrums = try JSONDecoder().decode([DailyScrum].self, from: file.availableData)
DispatchQueue.main.async {
completion(.success(dailyScrums))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
#discardableResult
static func save(scrums: [DailyScrum]) async throws -> Int {
try await withCheckedThrowingContinuation { continuation in
save(scrums: scrums) { result in
switch result {
case .failure(let error):
continuation.resume(throwing: error)
case .success(let scrumsSaved):
continuation.resume(returning: scrumsSaved)
}
}
}
}
static func save(scrums: [DailyScrum], completion: #escaping (Result<Int, Error>)->Void) {
DispatchQueue.global(qos: .background).async {
do {
let data = try JSONEncoder().encode(scrums)
let outfile = try fileURL()
try data.write(to: outfile)
DispatchQueue.main.async {
completion(.success(scrums.count))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
}
Here is my view model:
let loadMoreTrigger = PublishSubject<Void>()
let refreshTrigger = PublishSubject<Void>()
let loading = BehaviorRelay<Bool>(value: false)
let stories = BehaviorRelay<[Story]>(value: [])
var offset = 0
let error = PublishSubject<String>()
let selectedFeedType: BehaviorRelay<FeedType> = BehaviorRelay(value: .best)
override init() {
super.init()
let refreshRequest = loading.asObservable().sample(refreshTrigger).flatMap { loading -> Observable<[Story]> in
if loading {
return Observable.empty()
} else {
self.offset = 0
return self.fetchStories(type: self.selectedFeedType.value, offset: self.offset)
}
}
let loadMoreRequest = loading.asObservable().sample(loadMoreTrigger).flatMap { loading -> Observable<[Story]> in
if loading {
return Observable.empty()
} else {
self.offset += 10
return self.fetchStories(type: self.selectedFeedType.value, offset: self.offset)
}
}
let request = Observable.merge(refreshRequest, loadMoreRequest).share(replay: 1)
let response = request.flatMap { (stories) -> Observable<[Story]> in
request.do(onError: { error in
self.error.onNext(error.localizedDescription)
}).catchError { (error) -> Observable<[Story]> in
Observable.empty()
}
}.share(replay: 1)
Observable.combineLatest(request, response, stories.asObservable()) { request, response, stories in
return self.offset == 0 ? response : stories + response
}.sample(response).bind(to: stories).disposed(by: disposeBag)
Observable.merge(request.map{_ in true}, response.map{_ in false}, error.map{_ in false}).bind(to: loading).disposed(by: disposeBag)
}
Then when i checking loading observer i have false -> true, instead of true -> false. I just don't understand why it happening.
loading.subscribe {
print($0)
}.disposed(by: disposeBag)
In my viewController i call refreshTrigger on viewWillAppear using rx.sentMessage
Here is getFeed function:
func getFeed(type: FeedType, offset: Int) -> Observable<[Story]> {
return provider.rx.request(.getFeed(type: type, offset: offset)).asObservable().flatMap { (response) -> Observable<[Story]> in
do {
let feedResponse = try self.jsonDecoder.decode(BaseAPIResponse<[Story]>.self, from: response.data)
guard let stories = feedResponse.data else { return .error(APIError.requestFailed)}
return .just(stories)
} catch {
return .error(error)
}
}.catchError { (error) -> Observable<[Story]> in
return .error(error)
}
}
Your request and response observables are emitting values at the exact same time. Which one shows up in your subscribe first is undefined.
Specifically, request doesn't emit a value until after the fetch request completes. Try this instead:
Observable.merge(
loadMoreTrigger.map { true },
refreshTrigger.map { true },
response.map { _ in false },
error.map { _ in false }
)
.bind(to: loading)
.disposed(by: disposeBag)
There are lots of other problems in your code but the above answers this specific question.
I'm new to coding and have been trying to go through instagram tutorials to understand some concepts. Since updating to Xcode 12, my Firebase has seemed to not work anymore and is not showing on the home feed.
I placed a rectangle in to see if it was the if !homeViewModel.isLoading was the cause it appears to be so.
Here is my current code:
import SwiftUI
import URLImage
import Firebase
struct HomeView: View {
#ObservedObject var homeViewModel = HomeViewModel()
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
Story()
Rectangle().frame(width: 200, height: 200).foregroundColor(.red)
if !homeViewModel.isLoading {
ForEach(self.homeViewModel.posts, id: \.postId) { post in
VStack(alignment: .center) {
HeaderCell(post: post)
FooterCell(post: post)
}.background(Color.white).cornerRadius(10)
.padding(.leading, 10).padding(.trailing, 10)
}
}
}.background(Color.gray)
HomeViewModel:
import Foundation
import SwiftUI
import Firebase
class HomeViewModel: ObservableObject {
#Published var posts: [Post] = []
#Published var isLoading = false
var listener: ListenerRegistration!
// init() {
// loadTimeline()
// }
func loadTimeline() {
self.posts = []
isLoading = true
Api.Post.loadTimeline(onSuccess: { (posts) in
self.isLoading = false
if self.posts.isEmpty {
self.posts = posts
}
}, newPost: { (post) in
if !self.posts.isEmpty {
self.posts.insert(post, at: 0)
}
}) { (listener) in
self.listener = listener
}
}
}
LoadTimeline func:
func loadTimeline(onSuccess: #escaping(_ posts: [Post]) -> Void, newPost: #escaping(Post) -> Void, listener: #escaping(_ listenerHandle: ListenerRegistration) -> Void) {
guard let userId = Auth.auth().currentUser?.uid else {
return
}
let listenerFirestore = Ref.FIRESTORE_TIMELINE_DOCUMENT_USERID(userId: userId).collection("timelinePosts").order(by: "date", descending: true).addSnapshotListener({ (querySnapshot, error) in
guard let snapshot = querySnapshot else {
return
}
var posts = [Post]()
snapshot.documentChanges.forEach { (documentChange) in
switch documentChange.type {
case .added:
print("type: added")
let dict = documentChange.document.data()
guard let decoderPost = try? Post.init(fromDictionary: dict) else {return}
newPost(decoderPost)
posts.append(decoderPost)
case .modified:
print("type: modified")
case .removed:
print("type: removed")
}
}
onSuccess(posts)
})
listener(listenerFirestore)
}
For some reason it seems as though the function isn't being triggered and timeline isn't loading. unsure why though... Prior to the update it was working fine?
Any help would be much appreciated!
I need to update a SwiftUI List after making a network request. For requests, I use Moya approach with combination of triggers(Input&Output - "Kickstarter").
I cant use Combine framework due to the structure of the project, while they have a lot of helpful advises(not sure about my case).
Simple ContactList:
struct ContactList: View {
var viewModel: UserViewModel
var body: some View {
NavigationView {
List(viewModel.users) { contact in
NavigationLink(destination: ContactDetail(user: contact)) {
ContactRow(user: contact)
}
}
.navigationBarTitle(Text("Team Members"))
}
}
}
Then ViewModel
class UserViewModel {
let disposeBag = DisposeBag()
var users: [TeamMember] = []
init(users: [TeamMember] = []) {
let networkModel = UserNetworkModel()
networkModel.output.teamMembers.subscribe { (event) in
self.users.append(contentsOf: event.element.orEmpty)
}.disposed(by: disposeBag)
networkModel.output.error.subscribe(onNext: { error in
print(error.localizedDescription)
}).disposed(by: disposeBag)
networkModel.input.loadTrigger.onNext(Void())
self.users = users
}
}
And NetworkModel
class UserNetworkModel {
let disposeBag = DisposeBag()
let input: Input
let output: Output
struct Input {
let loadTrigger: AnyObserver<Void>
let searchTrigger: AnyObserver<String>
}
struct Output {
let teamMembers: Observable<[TeamMember]>
let error: Observable<Error>
}
internal let loadSubject = PublishSubject<Void>()
internal let searchSubject = BehaviorSubject<String>(value: "")
internal let errorSubject = PublishSubject<Error>()
internal let teamMembersSubject = BehaviorSubject<[TeamMember]>(value: [])
init() {
let service = MoyaProvider<TeamTarget>()
self.input = Input(loadTrigger: loadSubject.asObserver(), searchTrigger: searchSubject.asObserver())
self.output = Output(teamMembers: teamMembersSubject.asObservable(), error: errorSubject.asObservable())
let result = loadSubject.flatMapLatest { _ -> Observable<[TeamMember]> in
service.rx.request(.get).debug().mapArray(TeamMember.self).asObservable()
}.share(replay: 1)
Observable.combineLatest(result, searchSubject).map { (arg) in
let (members, filter) = arg
if filter.isEmpty {
return members
} else {
let searchText = try! self.searchSubject.value()
return members.filter({
return [$0.firstName, $0.lastName]
.compactMap({ $0 })
.first(where: { $0.hasPrefix(searchText) }) != nil
})
}
}.bind(to: teamMembersSubject).disposed(by: disposeBag)
result.subscribe(onError: { error in
self.errorSubject.onNext(error)
}).disposed(by: self.disposeBag)
}
}
Is it possible to update users array in this way? Or only Combine can do it for me easily?
Thanks for your time.
While I've found some Combine solution. Still waiting for Rx solution.
So, I fixed it just added #ObservedObject for ContactList property and #Published for users' array in Network model. Much less code, natively but not what I was looking for.
Full answer:
struct ContactList: View {
#ObservedObject var networkModel: NetworkModel
var body: some View {
NavigationView {
List(networkModel.users) { contact in
NavigationLink(destination: ContactDetail(user: contact)) {
ContactRow(user: contact)
}
}
.navigationBarTitle(Text("Team Members"))
}
}
}
#if DEBUG
struct ContactList_Previews: PreviewProvider {
static var previews: some View {
ContactList(networkModel: .init(users: contactData))
}
}
#endif
class NetworkModel: ObservableObject {
#Published var users = [User]()
init(users: [User] = []) {
getMembers()
}
private func getMembers() {
let provider = MoyaProvider<TeamTarget>()
provider.request(.get) { [weak self] (result) in
switch result {
case .success(let response):
do {
let result = try JSONDecoder().decode([User].self, from: response.data)
self?.users.append(contentsOf: result)
} catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.errorDescription.orEmpty)
}
}
}
}
I am trying to observe on custom class delegates. I started with
public var didTapAvatar: Observable<()> {
return delegate
.methodInvoked(#selector(JSQMessagesCollectionViewDelegateFlowLayout.collectionView(_:didTapAvatarImageView:at:)))
.map { _ in ()
}
}
which will cause an error as below
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[RxCocoa.RxCollectionViewDelegateProxy collectionView:layout:heightForCellTopLabelAtIndexPath:]: unrecognized selector sent to instance 0x618000298b00
I later tried this
public var jsqdelegate: DelegateProxy {
return RxJSQMessageCollectionViewCellProxy(parentObject: base)
}
public var didTapAvatar: Observable<()> {
return jsqdelegate
.methodInvoked(#selector(JSQMessagesCollectionViewDelegateFlowLayout.collectionView(_:didTapAvatarImageView:at:)))
.map { _ in ()
}
which will succeed on running but will immediately complete and dispose as shown by printing them out onto the console:
self.collectionView.rx.didTapAvatar.asObservable()
.subscribe(onNext: { (event) in
print("next")
}, onError: { (error) in
print("error")
}, onCompleted: {
print("complete")
}, onDisposed: {
print("disposed")
}).disposed(by: disposeBag)
RxJSQMessageCollectionViewCellProxy.swift
public class RxJSQMessageCollectionViewCellProxy: DelegateProxy, JSQMessagesCollectionViewDelegateFlowLayout, DelegateProxyType {
public class func currentDelegateFor(_ object: AnyObject) -> AnyObject? {
let collectionView: JSQMessagesCollectionView = object as! JSQMessagesCollectionView
return collectionView.delegate
}
public class func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) {
let collectionView: JSQMessagesCollectionView = object as! JSQMessagesCollectionView
collectionView.delegate = delegate as? JSQMessagesCollectionViewDelegateFlowLayout
}
}
JSQMessagesCollectionView+RxCreate.swift
extension Reactive where Base: JSQMessagesCollectionView {
public var didTapAvatar: Observable<()> {
return delegate.methodInvoked(#selector(JSQMessagesCollectionView.messagesCollectionViewCellDidTapAvatar(_:))).map { _ in () }
}
}