I have a Driver of type Bool and a BehaviorRelay of type Page (which is a custom enum).
enum Page {
case option1(CustomClass1, CustomClass2)
case option2(CustomClass3)
case option3(CustomClass4)
var property1: CustomClass2? {
switch self {
case .option1(_, let custom):
return custom
case .option2, .option3:
return nil
}
}
}
I have the Driver<Bool> in another ViewModel.
class ViewModel1 {
struct Output {
let hasItems: Driver<Bool>
}
let output: Output
init() {
let hasItemsRelay: BehaviorRelay<Bool> = BehaviorRelay<Bool>(value: false)
self.output = Output(
hasItems: hasItemsRelay.asDriver()
)
}
}
And I have a BehaviorRelay<Page?> in my base class.
class ViewModel2 {
let currentPageRelay: BehaviorRelay<Page?> = BehaviorRelay<Page?>(value: nil)
init() {
self.currentPageRelay = BehaviorRelay<Page?>(value: nil)
}
}
In ViewModel2 class I'm trying to catch an event on the hasItems driver of ViewModel1.Input and when I get an event, I need the current value of currentPageRelay and later on do stuff with it. So basically withLatestFrom is the thing I need to use.
class ViewModel2 {
private func test() {
let customViewModel: ViewModel1 = ViewModel1()
customViewModel
.output
.hasItems
.withLatestFrom(currentPageRelay) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.unwrap()
.drive(onNext: { (property1, hasItems) in
// do stuff
}
.disposed(by: disposeBag)
}
}
Xcode completely loses it on the withLatestFrom. No code completion and it gives the following compile error:
Expression type '(Bool, _)' is ambiguous without more context
I'm completely in the dark about this one. I've already tried everything, providing the correct classes in the parameter list below it, so that it knows what to expect etc, but no luck so far.
I think because .withLatestFrom requires both types it operates on to be of the same observable trait. So both should be either Observable, Driver, Signal, etc.
If you want to keep your Driver in your viewModel a Driver you could add an .asObservable() after the .hasItems:
class ViewModel2 {
let currentPageRelay: BehaviorRelay<Page?> = BehaviorRelay<Page?>(value: nil)
let disposeBag = DisposeBag()
init() {
// self.currentPageRelay = BehaviorRelay<Page?>(value: nil)
}
private func test() {
let customViewModel: ViewModel1 = ViewModel1()
customViewModel
.output
.hasItems
.asObservable()
.withLatestFrom(currentPageRelay) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.asDriver(onErrorJustReturn: nil)
.drive(onNext: {
guard let (property1, hasItems) = $0 else {
return
}
// do stuff
})
.disposed(by: disposeBag)
}
}
Or add a .asDriver() to currentPageRelay in the withLatestFrom(..):
customViewModel
.output
.hasItems
.withLatestFrom(currentPageRelay.asDriver()) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.drive(onNext: {
guard let (property1, hasItems) = $0 else {
return
}
// do stuff
})
.disposed(by: disposeBag)
Related
Take this case of a type constrained class Parameter, wrapping a value of given type.
Parameter conforms to the AnyParameter so it can be passed anywhere in the app without knowing the type. Parameters can be displayed in value cells AnyValueCell
How would you do to observe the change without having to know the underlying value type? It would be nice to avoid the code repetition in the value cell updateObserver function
Could AnyPublisher can be used here and how?
import UIKit
import Combine
print("Hello Playground")
protocol AnyParameter {
var anyValue: Any { get }
func set(value: Any)
}
protocol ParameterProtocol: AnyParameter {
associatedtype ValueType
var value: ValueType { get }
func set(value: ValueType)
}
public class Parameter<T>: ParameterProtocol {
typealias ValueType = T
#Published var value: T
var anyValue: Any { value }
init(value: T) {
self.value = value
}
func set(value: Any) {
guard let value = value as? T else { return }
set(value: value)
}
func set(value: T) {
self.value = value
}
}
public class AnyValueCell {
var parameter: AnyParameter {
didSet {
updateObserver()
}
}
var observer: AnyCancellable?
init(parameter: AnyParameter) {
self.parameter = parameter
updateObserver()
}
func updateObserver() {
observer?.cancel()
// This is the point of the question - How to make this generic?
// ---->
if let p = parameter as? Parameter<Int> {
observer = p.$value.sink() { value in
print("Update Cell -> \(value)")
}
return
}
if let p = parameter as? Parameter<Double> {
observer = p.$value.sink() { value in
print("Update Cell -> \(value)")
}
return
}
if let p = parameter as? Parameter<Bool> {
observer = p.$value.sink() { value in
print("Update Cell -> \(value)")
}
return
}
// <----
print("Wrong param type")
}
}
let intParam = Parameter<Int>(value: 42)
let doubleParam = Parameter<Double>(value: 3.14)
let boolParam = Parameter<Bool>(value: false)
var params: [AnyParameter] = [intParam, doubleParam, boolParam]
print ("--> Init Cells")
let cells: [AnyValueCell] = params.map { AnyValueCell(parameter: $0) }
print ("--> Change values")
intParam.set(value: 21)
doubleParam.set(value: 1.618)
boolParam.set(value: true)
Result, as expected:
Hello Playground
--> Init Cells
Update Cell -> 42
Update Cell -> 3.14
Update Cell -> false
--> Change values
Update Cell -> 21
Update Cell -> 1.618
Update Cell -> true
Add an anyValuePublisher property. You can (and maybe should) add it to AnyParameter, or you can define it in a separate protocol like this:
protocol AnyParameterPublishing: AnyParameter {
var anyValuePublisher: AnyPublisher<Any, Never> { get }
}
extension Parameter: AnyParameterPublishing {
var anyValuePublisher: AnyPublisher<Any, Never> {
return $value.map { $0 as Any }.eraseToAnyPublisher()
}
}
Then you can use it like this:
class AnyValueCell {
// ...
func updateObserver() {
guard let publishing = (parameter as? AnyParameterPublishing) else {
print("Wrong param type")
return
}
observer = publishing.anyValuePublisher
.sink { print("Update Cell -> \($0)") }
}
}
I'm trying to bind a view model to a collection view. But I don't know how to do it. I'm using MVVM pattern and RxSwift, and I've only tried table view binding before. Here's my view model and the view controller code I've done so far.
class SearchViewModel: ViewModelType {
private let disposeBag = DisposeBag()
struct input {
let loadData: Signal<Void>
}
struct output {
let result: Signal<String>
let loadApplyList: PublishRelay<friends>
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let result = PublishSubject<String>()
let loadApplyList = PublishRelay<friends>()
input.loadData.asObservable().subscribe(onNext: { [weak self] in
guard let self = self else { return }
api.getFriend().subscribe(onNext: { (response, statuscode) in
switch statuscode {
case .ok:
if let response = response {
loadApplyList.accept(response)
}
default:
print("default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
return output(result: result.asSignal(onErrorJustReturn: ""), loadApplyList: loadApplyList)
}
}
This is my ViewModel code
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
}
And this is my ViewController code.
How should the collection view bind?
Here is what your view model should look like:
class SearchViewModel {
// no need for a disposedBag. If you are putting a disposeBag in your view model, you are likely doing something wrong.
struct input {
let loadData: Signal<Void>
}
struct output {
let loadApplyList: Driver<[User]> // you should be passing an array here, not an object.
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let friendResult = input.loadData
.flatMapLatest {
api.getFriend()
.compactMap { $0.0.map(Result<friends, Error>.success) }
.asDriver(onErrorRecover: { Driver.just(Result<friends, Error>.failure($0)) })
}
let loadApplyList = friendResult
.compactMap { (result) -> [User]? in
guard case let .success(list) = result else { return nil }
return list.friends
}
return output(loadApplyList: loadApplyList)
}
}
Now in your view controller, you can bind it like this:
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
output.loadApplyList
.drive(collectionView.rx.items(cellIdentifier: "Cell", cellType: MyCellType.self)) { index, item, cell in
// configure cell with item here
}
.disposed(by: disposeBag)
}
Xcode 11.3, Swift 5.1.3
I am trying currently to create a custom property wrapper that allows me to link variables to a Firebase database. When doing this, to make it update the view, I at first tried to use the #ObservedObject #Bar var foo = []. But I get an error that multiple property wrappers are not supported. Next thing I tried to do, which would honestly be ideal, was try to make my custom property wrapper update the view itself upon being changed, just like #State and #ObservedObject. This both avoids needing to go down two layers to access the underlying values and avoid the use of nesting property wrappers. To do this, I checked the SwiftUI documentation and found out that they both implement the DynamicProperty protocol. I tried to use this too but failed because I need to be able to update the view (call update()) from within my Firebase database observers, which I cannot do since .update() is mutating.
Here is my current attempt at this:
import SwiftUI
import Firebase
import CodableFirebase
import Combine
#propertyWrapper
final class DatabaseBackedArray<Element>: ObservableObject where Element: Codable & Identifiable {
typealias ObserverHandle = UInt
typealias Action = RealtimeDatabase.Action
typealias Event = RealtimeDatabase.Event
private(set) var reference: DatabaseReference
private var currentValue: [Element]
private var childAddedObserverHandle: ObserverHandle?
private var childChangedObserverHandle: ObserverHandle?
private var childRemovedObserverHandle: ObserverHandle?
private var childAddedActions: [Action<[Element]>] = []
private var childChangedActions: [Action<[Element]>] = []
private var childRemovedActions: [Action<[Element]>] = []
init(wrappedValue: [Element], _ path: KeyPath<RealtimeDatabase, RealtimeDatabase>, events: Event = .all,
actions: [Action<[Element]>] = []) {
currentValue = wrappedValue
reference = RealtimeDatabase()[keyPath: path].reference
for action in actions {
if action.event.contains(.childAdded) {
childAddedActions.append(action)
}
if action.event.contains(.childChanged) {
childChangedActions.append(action)
}
if action.event.contains(.childRemoved) {
childRemovedActions.append(action)
}
}
if events.contains(.childAdded) {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childChanged) {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childRemoved) {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
private func setValue(to value: [Element]) {
guard let encodedValue = try? FirebaseEncoder().encode(currentValue) else {
fatalError("Could not encode value to Firebase.")
}
reference.setValue(encodedValue)
}
var wrappedValue: [Element] {
get {
return currentValue
}
set {
self.objectWillChange.send()
setValue(to: newValue)
}
}
var projectedValue: Binding<[Element]> {
return Binding(get: {
return self.wrappedValue
}) { newValue in
self.wrappedValue = newValue
}
}
var hasActiveObserver: Bool {
return childAddedObserverHandle != nil || childChangedObserverHandle != nil || childRemovedObserverHandle != nil
}
var hasChildAddedObserver: Bool {
return childAddedObserverHandle != nil
}
var hasChildChangedObserver: Bool {
return childChangedObserverHandle != nil
}
var hasChildRemovedObserver: Bool {
return childRemovedObserverHandle != nil
}
func connectObservers(for event: Event) {
if event.contains(.childAdded) && childAddedObserverHandle == nil {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childChanged) && childChangedObserverHandle == nil {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childRemoved) && childRemovedObserverHandle == nil {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
func removeObserver(for event: Event) {
if event.contains(.childAdded), let handle = childAddedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childAddedObserverHandle = nil
}
if event.contains(.childChanged), let handle = childChangedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childChangedObserverHandle = nil
}
if event.contains(.childRemoved), let handle = childRemovedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childRemovedObserverHandle = nil
}
}
func removeAction(_ action: Action<[Element]>) {
if action.event.contains(.childAdded) {
childAddedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childChanged) {
childChangedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childRemoved) {
childRemovedActions.removeAll { $0.id == action.id }
}
}
func removeAllActions(for event: Event) {
if event.contains(.childAdded) {
childAddedActions = []
}
if event.contains(.childChanged) {
childChangedActions = []
}
if event.contains(.childRemoved) {
childRemovedActions = []
}
}
}
struct School: Codable, Identifiable {
/// The unique id of the school.
var id: String
/// The name of the school.
var name: String
/// The city of the school.
var city: String
/// The province of the school.
var province: String
/// Email domains for student emails from the school.
var domains: [String]
}
#dynamicMemberLookup
struct RealtimeDatabase {
private var path: [String]
var reference: DatabaseReference {
var ref = Database.database().reference()
for component in path {
ref = ref.child(component)
}
return ref
}
init(previous: Self? = nil, child: String? = nil) {
if let previous = previous {
path = previous.path
} else {
path = []
}
if let child = child {
path.append(child)
}
}
static subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
static subscript(dynamicMember keyPath: KeyPath<Self, Self>) -> Self {
return Self()[keyPath: keyPath]
}
static let reference = Database.database().reference()
struct Event: OptionSet, Hashable {
let rawValue: UInt
static let childAdded = Event(rawValue: 1 << 0)
static let childChanged = Event(rawValue: 1 << 1)
static let childRemoved = Event(rawValue: 1 << 2)
static let all: Event = [.childAdded, .childChanged, .childRemoved]
static let constructive: Event = [.childAdded, .childChanged]
static let destructive: Event = .childRemoved
}
struct Action<Value>: Identifiable {
let id = UUID()
let event: Event
let action: (inout Value) -> Void
private init(on event: Event, perform action: #escaping (inout Value) -> Void) {
self.event = event
self.action = action
}
static func on<Value>(_ event: RealtimeDatabase.Event, perform action: #escaping (inout Value) -> Void) -> Action<Value> {
return Action<Value>(on: event, perform: action)
}
}
}
Usage example:
struct ContentView: View {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
var body: some View {
Text("School: ").bold() +
Text(schools.isEmpty ? "Loading..." : schools.first!.name)
}
}
When I try to use this though, the view never updates with the value from Firebase even though I am positive that the .childAdded observer is being called.
One of my attempts at fixing this was to store all of these variables in a singleton that itself conforms to ObservableObject. This solution is also ideal as it allows the variables being observed to be shared throughout my application, preventing multiples instances of the same date and allowing for a single source of truth. Unfortunately, this too did not update the view with the fetched value of currentValue.
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private init() {
//Send `objectWillChange` when `schools` property changes
_schools.objectWillChange.sink {
self.objectWillChange.send()
}
}
static let current = Session()
}
struct ContentView: View {
#ObservedObject
var session = Session.current
var body: some View {
Text("School: ").bold() +
Text(session.schools.isEmpty ? "Loading..." : session.schools.first!.name)
}
}
Is there any way to make a custom property wrapper that also updates a view in SwiftUI?
Making use of the DynamicProperty protocol we can easily trigger view updates by making use of SwiftUI's existing property wrappers. (DynamicProperty tells SwiftUI to look for these within our type)
#propertyWrapper
struct OurPropertyWrapper: DynamicProperty {
// A state object that we notify of updates
#StateObject private var updater = Updater()
var wrappedValue: T {
get {
// Your getter code here
}
nonmutating set {
// Tell SwiftUI we're going to change something
updater.notifyUpdate()
// Your setter code here
}
}
class Updater: ObservableObject {
func notifyUpdate() {
objectWillChange.send()
}
}
}
The solution to this is to make a minor tweak to the solution of the singleton. Credits to #user1046037 for pointing this out to me. The problem with the singleton fix mentioned in the original post, is that it does not retain the canceller for the sink in the initializer. Here is the correct code:
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private var cancellers = [AnyCancellable]()
private init() {
_schools.objectWillChange.sink {
self.objectWillChange.send()
}.assign(to: &cancellers)
}
static let current = Session()
}
I have a viewModel where I am performing a check based on bool. True / False. This value is passed into a PublishSubject<Bool>() now I want to get the latestValue with flatMapLatest and navigate using another different ViewModel based on the value of True/False
routeManager.errorOnNil().asObservable()
.flatMapLatest { (code) -> Observable<RxSwift.Event<User>> in
let params = code.values
let challengeId = Int(params["challengeId"] as? String ?? "0")
return self.provider.postVerifyApp(challengeId: login.challengeId!)
.trackActivity(self.loading)
.trackError(self.error)
.materialize()
}.subscribe(onNext: { [weak self] (event) in
switch event {
case .next(let token):
log(token)
if token.data != nil {
self?.dataStatus.onNext(true)
} else {
self?.dataStatus.onNext(false)
}
case .error(let error):
log(error.localizedDescription)
default: break
}
}).disposed(by: rx.disposeBag)
Where to switch
dataStatus.flatMapLatest { (status) -> ViewModel in
if status {
let viewModel = XXXViewModel(provider: self.provider)
return viewModel
} else {
let viewModel = YYYYViewModel(provider: self.provider)
return viewModel
}
}.subscribe()
.disposed(by: rx.disposeBag)
this gives error Instance method 'flatMapLatest' requires that 'ViewModel' conform to 'ObservableConvertibleType'
.flatMapLatest and .flatMap take Observable types as return values for their closures. You need to return the viewModel wrapped inside an observable. .flatMapLatest will flatten that down and when you subscribe you'll get the viewModel object.
dataStatus.flatMapLatest { (status) -> Observable<ViewModel> in
if status {
let viewModel = XXXViewModel(provider: self.provider)
return Observable.just(viewModel)
} else {
let viewModel = YYYYViewModel(provider: self.provider)
return Observable.just(viewModel)
}
}.subscribe()
.disposed(by: rx.disposeBag)
I'm new to RxSwift and trying implement app that using MVVM architecture. I have view model:
class CategoriesViewModel {
fileprivate let api: APIService
fileprivate let database: DatabaseService
let categories: Results<Category>
// Input
let actionRequest = PublishSubject<Void>()
// Output
let changeset: Observable<(AnyRealmCollection<Category>, RealmChangeset?)>
let apiSuccess: Observable<Void>
let apiFailure: Observable<Error>
init(api: APIService, database: DatabaseService) {
self.api = api
self.database = database
categories = database.realm.objects(Category.self).sorted(byKeyPath: Category.KeyPath.name)
changeset = Observable.changeset(from: categories)
let requestResult = actionRequest
.flatMapLatest { [weak api] _ -> Observable<Event<[Category]>> in
guard let strongAPI = api else {
return Observable.empty()
}
let request = APIService.MappableRequest(Category.self, resource: .categories)
return strongAPI.mappedArrayObservable(from: request).materialize()
}
.shareReplayLatestWhileConnected()
apiSuccess = requestResult
.map { $0.element }
.filterNil()
.flatMapLatest { [weak database] newObjects -> Observable<Void> in
guard let strongDatabase = database else {
return Observable.empty()
}
return strongDatabase.updateObservable(with: newObjects)
}
apiFailure = requestResult
.map { $0.error }
.filterNil()
}
}
and I have following binginds in view controller:
viewModel.apiSuccess
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.apiFailure
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
But if I comment bindings, part with database updating stops executing. I need to make it execute anyway, without using dispose bag in the view model. Is it possible?
And little additional question: should I use weak-strong dance with api/database and return Observable.empty() like in my view model code or can I just use unowned api/unowned database safely?
Thanks.
UPD:
Function for return observable in APIService:
func mappedArrayObservable<T>(from request: MappableRequest<T>) -> Observable<[T]> {
let jsonArray = SessionManager.jsonArrayObservable(with: request.urlRequest, isSecured: request.isSecured)
return jsonArray.mapResponse(on: mappingSheduler, { Mapper<T>().mapArray(JSONArray: $0) })
}
Work doesn't get done unless there is a subscriber prepared to receive the results.
Your DatabaseService needs to have a dispose bag in it and subscribe to the Observable<[Category]>. Something like:
class ProductionDatabase: DatabaseService {
var categoriesUpdated: Observable<Void> { return _categories }
func updateObservable(with categories: Observable<[Category]>) {
categories
.subscribe(onNext: { [weak self] categories in
// store categories an then
self?._categories.onNext()
})
.disposed(by: bag)
}
private let _categories = PublishSubject<Void>()
private let bag = DisposeBag()
}
Then apiSuccess = database.categoriesUpdated and database.updateObservable(with: requestResult.map { $0.element }.filterNil())