RxSwift: Observe viewDidLoad from view model without Subjects - swift

I have a dependency problem with my UIViewController and my view model.
Basically I want to listen the viewDidLoad event inside my view model.
At the moment I have a Class A which instantiates view model and UIViewController with parameter the viewModel, so:
let viewModel = ViewModel()
let viewController = UIViewController(viewModel)
and I've created a RxCocoa extension for the viewDidLoad:
var viewDidLoad: Observable<Void> {
return self.sentMessage(#selector(Base.viewDidLoad)).map { _ in Void() }
}
now I'm stuck to bind this rx.viewDidLoad to an observable inside my view model. I am able to do it with Subjects but I want a reactive approach using just Observable.
I know that I could inject rx.viewDidLoad as constructor parameter of the view model but in this way I'd break my architecture and I don't want to allow the UIViewController to instantiate the view model internally but I want to keep it as a injected dependency.
Any suggestions?
Thanks
Solution
Thank to #tomahh I've used this solution:
My view controller:
override func configure(viewModel: ViewModel) {
viewModel.bindViewDidLoad(rx.viewDidLoad)
}
My view model:
func bindViewDidLoad(_ viewControllerDidLoad: Observable<Void>) {
//Create observers which depend on viewControllerDidLoad
}

Because ViewController already knows about view model, it could set a property on ViewModel at initialisation time
class ViewController: UIViewController {
init(_ viewModel: ViewModel) {
viewModel.viewDidLoad = self.rx.viewDidLoad
}
}
And then, observables in ViewModel could be defined as computed property deriving viewDidLoad
struct ViewModel {
var viewDidLoad: Observable<Void> = .never()
var something: Observable<String> {
return viewDidLoad.map { "Huhu, something is guuut" }
}
}

If anybody needs that rx properties here is a ready to use solution, inspired by the code of #marco-santarossa
extension Reactive where Base: UIView {
var willMoveToWindow: Observable<Bool> {
return self.sentMessage(#selector(Base.willMove(toWindow:)))
.map({ $0.filter({ !($0 is NSNull) }) })
.map({ $0.isEmpty == false })
}
var viewWillAppear: Observable<Void> {
return self.willMoveToWindow
.filter({ $0 })
.map({ _ in Void() })
}
var viewWillDisappear: Observable<Void> {
return self.willMoveToWindow
.filter({ !$0 })
.map({ _ in Void() })
}
}

let viewDidAppear = rx.sentMessage(#selector(UIViewController.viewDidAppear(_:)))
.mapToVoid()
.asDriver(onErrorJustReturn: ())

Related

Detect change in NSMutableOrderedSet with Swift Combine

I'm trying to observe change of an NSMutableOrderedSet in my ViewModel with combine.
I want to know when some element is added or removed of NSMutableOrderedSet
Some code of my ViewModel :
class TrainingAddExerciceViewModel: ObservableObject {
#Published var exercice: Exercice?
#Published var serieHistories = NSMutableOrderedSet()
...
init(...) {
...
//Where i'm trying to observe
$serieHistories
.sink { (value) in
print(value)
}
.store(in: &self.cancellables)
}
}
This is the function I use in my ViewModel to add element to NSMutableOrderedSet :
func add(managedObjectContext: NSManagedObjectContext) {
let newSerieHistory = ExerciceSerieHistory(context: managedObjectContext)
self.serieHistories.add(newSerieHistory)
self.updateView()
}
I have some other publisher working well with an other type (custom class).
Did I miss something ?
If I correctly understood logic of your code try the following (that init not needed)
variant 1 - add force update
func updateView() {
// ... other code
self.objectWillChange.send()
}
variant 2 - recreate storage
func add(managedObjectContext: NSManagedObjectContext) {
let newSerieHistory = ExerciceSerieHistory(context: managedObjectContext)
let newStorage = NSMutableOrderedSet(orderedSet: self.serieHistories)
newStorage.add(newSerieHistory)
self.serieHistories = newStorage // << fires publisher
self.updateView()
}

closure through typealias swift not working

why does typealias closure not transmit data and output nothing to the console? How to fix it?
class viewModel: NSObject {
var abc = ["123", "456", "789"]
typealias type = ([String]) -> Void
var send: type?
func createCharts(_ dataPoints: [String]) {
var dataEntry: [String] = []
for item in dataPoints {
dataEntry.append(item)
}
send?(dataEntry)
}
override init() {
super.init()
self.createCharts(abc)
}
}
class ViewController: UIViewController {
var viewModel: viewModel = viewModel()
func type() {
viewModel.send = { item in
print(item)
}
}
override func viewDidLoad() {
super.viewDidLoad()
print("hello")
type()
}
}
I have a project in which a similar design works, but I can not repeat it
The pattern is fine, but the timing is off.
You’re calling createCharts during the init of the view model. But the view controller is setting the send closure after the init of the view model is done.
Bottom line, you probably don’t want to call createCharts during the init of the view model.
Possible solution is to create custom initializer:
class viewModel: NSObject {
...
init(send: type?) {
self.send = send
self.createCharts(abc)
}
}
class ViewController: UIViewController {
var viewModel: viewModel = viewModel(send: { print($0) })
...
}

How do you call a method on a UIView from outside the UIViewRepresentable in SwiftUI?

I want to be able to pass a reference to a method on the UIViewRespresentable (or perhaps it’s Coordinator) to a parent View. The only way I can think to do this is by creating a field on the parent View struct with a class that I then pass to the child, which acts as a delegate for this behaviour. But it seems pretty verbose.
The use case here is to be a able to call a method from a standard SwiftUI Button that will zoom the the current location in a MKMapView that’s buried in a UIViewRepresentable elsewhere in the tree. I don’t want the current location to be a Binding as I want this action to be a one off and not reflected constantly in the UI.
TL;DR is there a standard way of having a parent get a reference to a child in SwiftUI, at least for UIViewRepresentables? (I understand this is probably not desirable in most cases and largely runs against the SwiftUI pattern).
I struggled with that myself, here's what worked using Combine and PassthroughSubject:
struct OuterView: View {
private var didChange = PassthroughSubject<String, Never>()
var body: some View {
VStack {
// send the PassthroughSubject over
Wrapper(didChange: didChange)
Button(action: {
self.didChange.send("customString")
})
}
}
}
// This is representable struct that acts as the bridge between UIKit <> SwiftUI
struct Wrapper: UIViewRepresentable {
var didChange: PassthroughSubject<String, Never>
#State var cancellable: AnyCancellable? = nil
func makeUIView(context: Context) → SomeView {
let someView = SomeView()
// ... perform some initializations here
// doing it in `main` thread is required to avoid the state being modified during
// a view update
DispatchQueue.main.async {
// very important to capture it as a variable, otherwise it'll be short lived.
self.cancellable = didChange.sink { (value) in
print("Received: \(value)")
// here you can do a switch case to know which method to call
// on your UIKit class, example:
if (value == "customString") {
// call your function!
someView.customFunction()
}
}
}
return someView
}
}
// This is your usual UIKit View
class SomeView: UIView {
func customFunction() {
// ...
}
}
I'm sure there are better ways, including using Combine and a PassthroughSubject. (But I never got that to work.) That said, if you're willing to "run against the SwiftUI pattern", why not just send a Notification? (That's what I do.)
In my model:
extension Notification.Name {
static let executeUIKitFunction = Notification.Name("ExecuteUIKitFunction")
}
final class Model : ObservableObject {
#Published var executeFuntionInUIKit = false {
willSet {
NotificationCenter.default.post(name: .executeUIKitFunction, object: nil, userInfo: nil)
}
}
}
And in my UIKit representable:
NotificationCenter.default.addObserver(self, selector: #selector(myUIKitFunction), name: .executeUIKitFunction, object: nil)
Place that in your init or viewDidLoad, depending on what kind of representable.
Again, this is not "pure" SwiftUI or Combine, but someone better than me can probably give you that - and you sound willing to get something that works. And trust me, this works.
EDIT: Of note, you need to do nothing extra in your representable - this simply works between your model and your UIKit view or view controller.
I was coming here to find a better answer, then the one I came up myself with, but maybe this does actually help someone?
It's pretty verbose though nevertheless and doesn't quite feel like the most idiomatic solution, so probably not exactly what the question author was looking for. But it does avoid polluting the global namespace and allows synchronous (and repeated) execution and returning values, unlike the NotificationCenter-based solution posted before.
An alternative considered was using a #StateObject instead, but I need to support iOS 13 currently where this is not available yet.
Excursion: Why would I want that? I need to handle a touch event, but I'm competing with another gesture defined in the SwiftUI world, which would take precedence over my UITapGestureRecognizer. (I hope this helps by giving some context for the brief sample code below.)
So what I came up with, was the following:
Add an optional closure as state (on FooView),
Pass it as a binding into the view representable (BarViewRepresentable),
Fill this from makeUIView,
So that this can call a method on BazUIView.
Note: It causes an undesired / unnecessary subsequent update of BarViewRepresentable, because setting the binding changes the state of the view representable though, but this is not really a problem in my case.
struct FooView: View {
#State private var closure: ((CGPoint) -> ())?
var body: some View {
BarViewRepresentable(closure: $closure)
.dragGesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onEnded { value in
self.closure?(value.location)
})
)
}
}
class BarViewRepresentable: UIViewRepresentable {
#Binding var closure: ((CGPoint) -> ())?
func makeUIView(context: UIViewRepresentableContext<BarViewRepresentable>) -> BazUIView {
let view = BazUIView(frame: .zero)
updateUIView(view: view, context: context)
return view
}
func updateUIView(view: BazUIView, context: UIViewRepresentableContext<BarViewRepresentable>) {
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else { return }
strongSelf.closure = { [weak view] point in
guard let strongView = view? else {
return
}
strongView.handleTap(at: point)
}
}
}
}
class BazUIView: UIView { /*...*/ }
This is how I accomplished it succesfully. I create the UIView as a constant property in the SwiftUI View. Then I pass that reference into the UIViewRepresentable initializer which I use inside the makeUI method. Then I can call any method (maybe in an extension to the UIView) from the SwiftUI View (for instance, when tapping a button). In code is something like:
SwiftUI View
struct MySwiftUIView: View {
let myUIView = MKMapView(...) // Whatever initializer you use
var body: some View {
VStack {
MyUIView(myUIView: myUIView)
Button(action: { myUIView.buttonTapped() }) {
Text("Call buttonTapped")
}
}
}
}
UIView
struct MyUIView: UIViewRepresentable {
let myUIView: MKMapView
func makeUIView(context: UIViewRepresentableContext<MyUIView>) -> MKMapView {
// Configure myUIView
return myUIView
}
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MyUIView>) {
}
}
extension MKMapView {
func buttonTapped() {
print("The button was tapped!")
}
}

Implement delegates within SwiftUI Views

I am trying to implement a functionality that requires a delegate method (like NSUserActivity). Therefore I need a UIViewController that conforms to NSUserActivityDelegate (or similar other delegates), handles and hold all the required information. My problem is that I am using SwiftUI for my interface and therefore I am not using UIViewControllers. So how can I implement this functionality and still use SwiftUI for the UI. What I tried: view1 is just a normal SwiftUI View that can present (via NavigationLink) view2 which is the view where in want to implement this functionality. So I tried instead of linking view1 and view2, linking view1 to a UIViewControllerRepresentable which then handles the implementation of this functionality and adds UIHostingController(rootView: view2) as a child view controller.
struct view1: View {
var body: some View {
NavigationLink(destination: VCRepresentable()) {
Text("Some Label")
}
}
}
struct view2: View {
var body: some View {
Text("Hello World!")
}
}
struct VCRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return implementationVC()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}
class implementationVC: UIViewController, SomeDelegate for functionality {
// does implementation stuff in delegate methods
...
override func viewDidLoad() {
super.viewDidLoad()
attachChild(UIHostingController(rootView: view2()))
}
private func attachChild(_ viewController: UIViewController) {
addChild(viewController)
if let subview = viewController.view {
subview.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(subview)
subview.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
subview.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
subview.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
subview.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
viewController.didMove(toParent: self)
}
}
I am having trouble with transferring the data between my VC and my view2. So I'm wondering if there is a better way to implement such a functionality within a SwiftUI View.
You need to create a view that conforms to UIViewControllerRepresentable and has a Coordinator that handles all of the delegate functionality.
For example, with your example view controller and delegates:
struct SomeDelegateObserver: UIViewControllerRepresentable {
let vc = SomeViewController()
var foo: (Data) -> Void
func makeUIViewController(context: Context) -> SomeViewController {
return vc
}
func updateUIViewController(_ uiViewController: SomeViewController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(vc: vc, foo: foo)
}
class Coordinator: NSObject, SomeDelegate {
var foo: (Data) -> Void
init(vc: SomeViewController, foo: #escaping (Data) -> Void) {
self.foo = foo
super.init()
vc.delegate = self
}
func someDelegateFunction(data: Data) {
foo(data)
}
}
}
Usage:
struct ContentView: View {
var dataModel: DataModel
var body: some View {
NavigationLink(destination: CustomView(numberFromPreviousView: 10)) {
Text("Go to VCRepresentable")
}
}
}
struct CustomView: View {
#State var instanceData1: String = ""
#State var instanceData2: Data?
var numberFromPreviousView: Int // example of data passed from the previous view to this view, the one that can react to the delegate's functions
var body: some View {
ZStack {
SomeDelegateObserver { data in
print("Some delegate function was executed.")
self.instanceData1 = "Executed!"
self.instanceData2 = data
}
VStack {
Text("This is the UI")
Text("That, in UIKit, you would have in the UIViewController")
Text("That conforms to whatever delegate")
Text("SomeDelegateObserver is observing.")
Spacer()
Text(instanceData1)
}
}
}
}
Note: I renamed VCRepresentable to SomeDelegateObserver to be more indicative of what it does: Its sole purpose is to wait for delegate functions to execute and then run the closures (i.e foo in this example) you provide it. You can use this pattern to create as many functions as you need to "observe" whatever delegate functions you care about, and then execute code that can update the UI, your data model, etc. In my example, when SomeDelegate fires someDelegateFunction(data:), the view will display "Excuted" and update the data instance variable.

How to present PublishSubject as Observable in MVVM?

I have something like this:
protocol ViewModel: class {
var eventWithInitialValue: Observable<Int> { get }
}
class ViewModelImpl: ViewModel {
let eventWithInitialValue: BehaviorSubject<Int> = BehaviorSubject(value: 0)
init() {
eventWithInitialValue.onNext(1)
}
}
class ViewController: UIViewController {
weak var viewModel: ViewModel?
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel?
.eventWithInitialValue
.subscribe(onNext: {
print($0)
}).disposed(by: bag)
}
}
I want to communicate with viewModel fields from ViewController as an Observables. But inside viewModel this fields should be a [OneOf]Subject types (for safety reasons).
Implementation above have a next compile time error -> Type 'ViewModelImpl' does not conform to protocol 'ViewModel'
Can anyone help with the implementation of these requirements?
Your problem is not with Rx, your error is related to your protocol
This will solve the current issue
protocol ViewModel: class {
var eventWithInitialValue: BehaviorSubject<Int> { get }
}
class ViewModelImpl: ViewModel {
var eventWithInitialValue: BehaviorSubject<Int> = BehaviorSubject(value: 0)
init() {
eventWithInitialValue.onNext(1)
}
}
I think you got this Type 'ViewModelImpl' does not conform to protocol 'ViewModel' because you define the eventWithInitialValue's type in your implementation as BehaviorSubject.
What I can suggest is something like this
protocol ViewModel {
var data: Observable<Int> { get}
}
class ViewModelImpl: ViewModel {
private let dataSubject = BehaviorSubject(value: 1)
var data: Observable<Int> {
return dataSubject
}
}