MVVM + RXSwift+ Coordinator how to set data? - ios

Good night!
Can you tell me how can I write data from controller 2 to controller 1?
I have a coordinate at the main screen.
final class MenuCoffeLikeCoordinator: TabBarPresentableCoordinator {
var tabBarItem: UITabBarItem = {
let title = "Меню"
let image = UIImage(asset: Resources.Assets.TabBarItems.mainTabBar)
let selectedImage = UIImage(asset: Resources.Assets.TabBarItems.mainTabBarSelected)
let item = UITabBarItem(title: title, image: image, selectedImage: selectedImage)
return item
}()
var navigationController: UINavigationController
init(navigationController: UINavigationController = UINavigationController()) {
self.navigationController = navigationController
}
var didFinish: (() -> Void)?
func start() {
self.navigationController.pushViewController(createMenuCoffeLikeFlow(), animated: true)
}
func stop() {}
func createMenuCoffeLikeFlow() { -> UIViewController {
let menuController = MenuCoffeLikeAssembler.createModule()
menuController.rx.didTapMapLayer.onNext {
let controller = self.createCoffeeBarMap()
self.navigationController.pushViewController(controller, animated: true)
}
return menuController
}
private func createCoffeeBarMap() -> UIViewController {
let controller = CoffeeBarContainerAssembler.createModule()
controller.obsRelay.subscribe(onNext: { event in
self.navigationController.popViewController(animated: true)
})
return controller
}
}
In the createMenuCoffeLikeFlow function, I create the main screen, and when I click on the button, I go to screen 2 (createCoffeeBarMap)
Inside the function (createCoffeeBarMap), I subscribe to the PublishSubject, and when the data changes, I get a new text.
I need to write this text in the menuCoffeeControler which is in the createMenuCoffeLikeFlow function. How can i do this?

Here's how I would implement it using my Cause Logic Effect (CLE) architecture. With CLE you don't need to implement a Coordinator because a reusable Coordinator class already exists in the library. This means less code for you to write.
Unlike yours, this sample is complete and will compile. The only thing missing is the creation and layout of the views inside the view controllers.
import Cause_Logic_Effect
import RxCocoa
import RxSwift
import UIKit
/// This function produces the view controller that is attached to your tab bar controller. I don't put the
/// `UITabBarItem` in here. Instead I attach that when connecting to the tab bar controller.
func menuCoffeLikeTab() -> UIViewController {
// the `configure` function calls its closure inside the viewDidLoad method.
let menuController = MenuController().configure { $0.connect() }
let controller = UINavigationController(rootViewController: menuController)
return controller
}
/// It depends on how you want to layout your view controllers on whether anything else goes in here. If you
/// use storyboards, then add `#IBOutlet` before the views here. If you create your views programatically
/// then add a `loadView()` override.
final class MenuController: UIViewController {
var mapLayerButton: UIButton!
var textField: UITextField!
let disposeBag = DisposeBag()
}
extension MenuController {
func connect() {
// This is the meat. The `coffeeBarResponse` observable pushes the
// CoffeeBarController onto the navigation stack when approprate and
// then emits any values produced by it. Notice how this looks alot like
// a network call except you are querying the user instead of the server.
let coffeeBarResponse = mapLayerButton.rx.tap
.flatMapFirst(pushScene(on: navigationController!, animated: true) {
CoffeeBarController().scene { $0.connect() }
})
.share()
// The pushScene function above will create a coordinator for the
// CoffeeBarController. When needed, the coordinator will create the
// view controller, call its `connect` and emit any values from that.
// When the Observable completes, the coordinator will pop the view
// controller off.
coffeeBarResponse
.bind(to: textField.rx.text)
.disposed(by: disposeBag)
}
}
final class CoffeeBarController: UIViewController {
var saveButton: UIButton!
var textField: UITextField!
}
extension CoffeeBarController {
func connect() -> Observable<String> {
// when the user taps the save button, this will emit whatever value is
// in the text field and then complete the observable.
saveButton.rx.tap
.withLatestFrom(textField.rx.text.orEmpty)
.take(1)
}
}
Like I said above, this uses a reusable Coordinator class that is part of the library instead of you having to write your own all the time. This architecture will significantly reduce the amount of boilerplate code you have to write. Learn more at https://github.com/danielt1263/CLE-Architecture-Tools and join the RxSwift Slack to learn more about RxSwift in general.

this is a typical scenario where DI comes to rescue. You have to have some kind of a shared container which will register and resolve dependencies. I use Dip https://github.com/AliSoftware/Dip.git and here is an example with your code. The idea is the following - you register closure in one VC and pass it to another.

Related

Should we be using UIView or UIViewController for the following feature?

We have the following app, where user can switch to different "page" (purple, yellow, ... colours) from side menu.
I was wondering, should the "page" be implemented as UIView, or should the "page" be implemented as UIViewController?
The pages shall responsible to
Read/ write from/ to CoreData.
Possible holding a UIPageView, which user can swipe through multiple child pages as shown in https://i.stack.imgur.com/v0oNo.gif
Holding a UICollectionView.
User can drag and move the items in the UICollectionView
User can perform various contextual action (Delete, clone, ...) on the items in UICollectionView.
Can easily port to iPad in the future.
Currently, my implementation of using UIView are as follow.
private func archive() {
if let trashView = self.trashView {
trashView.removeFromSuperview()
self.trashView = nil
}
if self.archiveView != nil {
return
}
let archiveView = ArchiveView.instanceFromNib()
self.view.addSubview(archiveView)
archiveView.translatesAutoresizingMaskIntoConstraints = false
archiveView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
archiveView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
archiveView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
archiveView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
self.archiveView = archiveView
}
private func trash() {
if let archiveView = self.archiveView {
archiveView.removeFromSuperview()
self.archiveView = nil
}
if self.trashView != nil {
return
}
let trashView = TrashView.instanceFromNib()
self.view.addSubview(trashView)
trashView.translatesAutoresizingMaskIntoConstraints = false
trashView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
trashView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
trashView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
trashView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
self.trashView = trashView
}
I notice that if I implement the "pages" using UIView, I will lost some capability of UIViewController like
viewDidLoad callback.
viewWillLoad callback.
viewDidLayoutSubviews callback.
...
However, I am not clear whether losing those capabilities will stop me from implementing a proper "page"?
May I know, should I implement those "pages" using UIView, or using UIViewController?
I would do this with UIViewController because of all the UIKit callback reasons you listed in the question already.
I assume that you have a UINavigationController instance that's set as window.rootViewController for your app. You have a reference to this instance using which you can easily switch between different screens.
Example
class SlideMenuViewController: UIViewController {
enum Option {
case archive
case trash
}
var onSelect: ((_ option: Option) -> Void)?
}
class ArchiveViewController: UIViewController {}
class TrashViewController: UIViewController {}
class AppNavigator {
let mainNavigationController: UINavigationController
init(navigationController: UINavigationController) {
self.mainNavigationController = navigationController
}
private lazy var slideMenuVC: SlideMenuViewController = {
let slideMenu = SlideMenuViewController()
slideMenu.onSelect = { [weak self] (option) in
self?.openScreen(for: option)
}
return slideMenu
}()
private lazy var archiveVC: ArchiveViewController = {
return ArchiveViewController()
}()
private lazy var trashVC: TrashViewController = {
return TrashViewController()
}()
func openScreen(for option: SlideMenuViewController.Option) {
let targetVC: UIViewController
switch option {
case .archive: targetVC = archiveVC
case .trash: targetVC = trashVC
}
mainNavigationController.setViewControllers([targetVC], animated: true)
}
}
Perhaps I totally misunderstood your question, but your UIView should only hold logic related to how the view itself will appear to the user. The UIView should not contain any logic related to the other views or to the model.
UIKit assumes that you use the MVC model to implement apps on the Apple platforms. This means that any code related to controlling which view has to appear and which data the view should get from the model, should be written in the ViewController.
In Xcode you have both the UICollectionViewController and the UIPageViewController to implement page swipes and dragging an dropping views.
View controllers can give the control to other view controllers to present views. The view controller also determine the data that should be presented by the view. Please, check out this article about the MVC model.
Kind regards,
MacUserT

Delegating action through protocol not working swift

I needed to delegate a click action for my UIView class to my UIViewController class since Swift does not support multiple class inheritance. So i wanted it such that once a button is clicked on my subview, a function in my BrowserViewController class is called.
I am using a protocol to achieve this, but on the function does not triggered when the button is tapped. Please help me out.
View Controller
class BrowseViewController: UIViewController {
var categoryItem: CategoryItem! = CategoryItem() //Category Item
private func setupExplore() {
//assign delegate of category item to controller
self.categoryItem.delegate = self
}
}
// delegate function to be called
extension BrowseViewController: ExploreDelegate {
func categoryClicked(category: ProductCategory) {
print("clicked")
let categoryView = ProductByCategoryView()
categoryView.category = category
categoryView.modalPresentationStyle = .overCurrentContext
self.navigationController?.pushViewController(categoryView, animated: true)
}
}
Explore.swift (subview)
import UIKit
protocol ExploreDelegate: UIViewController {
func categoryClicked(category: ProductCategory)
}
class Explore: UIView {
var delegate: ExploreDelegate?
class CategoryItem: UIView {
var delegate: ExploreDelegate?
var category: ProductCategory? {
didSet {
self.configure()
}
}
var tapped: ((_ category: ProductCategory?) -> Void)?
func configure() {
self.layer.cornerRadius = 6
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.categoryTapped)))
self.layoutIfNeeded()
}
#objc func categoryTapped(_ sender: UIGestureRecognizer) {
delegate?.categoryClicked(category: ProductCategory.everything)
self.tapped?(self.category)
}
}
}
Simply add a print statement inside categoryTapped.
You will then know if it is actually being tapped.
A million things could go wrong, for example, you may have forget to set the UIView to allow intertaction.
After checking that. Next add another print statement inside categoryTapped which shows you whether or not the delegate variable is null.
You'll quickly discover the problem using simple print statements.
print("I got to here!")
It's that easy.
And what about
if delegate == nil { print("it is nil!! oh no!" }
else { print("phew. it is NOT nil.") }
Debugging is really that easy at this level.
Next add a print statement inside setupExplore()
func setupExplore() {
print("setup explore was called")
....
See what happens.
I don't see any piece of code which sets the delegate.
First of all, define delegate as a property inside CategoryItem class, Then you must set the current instance of BrowseViewController to the delegate variable of CategoryItem. Now you can expect your method being called.
There are a few things that could cause the delegate method to not be triggered in this code:
Ensure that isUserInteractionEnabled = true on your CategoryItem. This is probably best done in either the configure() function in the CategoryItem or in the setupExplore() function of the BrowseViewController.
Make sure that the setupExplore() function on the BrowseViewController is being called, and that the category is being set on the CategoryItem to trigger the configure function. Otherwise, either the delegate or the gesture recognizer might not being set.
Side Note - weak vs strong delegate
On a side note, it is usually best practice to make your delegate properties weak var rather that having them be a strong reference, as this makes them prone to strong retain cycles.
Therefore, you might want to consider making the var delegate: ExploreDelegate? on your CategoryItem into weak var delegate: ExploreDelegate?. For more information on this problem, view this post.

How to change a tab bar item from the tab bar controller

I got tab bar controller with three view controllers setup. One of those view controllers changes its tab bar item badgeValue when I open it. I would like to change this badgeValue already when I arrive at the first tab.
I created a Tabbarcontoller: UITabBarController class but don't know how to easily access the items of the sub views. Here is the code from my Tabbarcontoller class:
class TabBarController: UITabBarController, MainMethodsDelegate {
var myFriendsRequests: [UserInfo] = []
var friendRequestsCount: Int = 0
func getFriendsRequests_Methods_Destination(myFriendsRequest: UserInfo) {
myFriendsRequests.append(myFriendsRequest)
self.friendRequestsCount = myFriendsRequests.count
DispatchQueue.main.async {
//friendsBarItem.badgeValue = String(self.friendRequestsCount)
}
}
let mainMethods = MainMethods()
override func viewDidLoad() {
super.viewDidLoad()
mainMethods.delegate = self
mainMethods.getFriendsRequests()
}
}
And here the working code from the sub view controller:
class FriendsViewController: UIViewController, MainMethodsDelegate {
var myFriendsRequests: [UserInfo] = []
var friendRequestsCount: Int = 0
func getFriendsRequests_Methods_Destination(myFriendsRequest: UserInfo) {
myFriendsRequests.append(myFriendsRequest)
self.friendRequestsCount = myFriendsRequests.count
DispatchQueue.main.async {
self.friendsBarItem.badgeValue = String(self.friendRequestsCount)
}
}
let mainMethods = MainMethods()
override func viewDidLoad() {
super.viewDidLoad()
mainMethods.delegate = self
mainMethods.getFriendsRequests()
}
#IBOutlet weak var friendsBarItem: UITabBarItem!
}
I'm looking for a simple way to access the sub view controllers or at least the bar items where it says:
//friendsBarItem.badgeValue = String(self.friendRequestsCount)
I'm not sure if delegates is the right way to go?
I would leave the bar item out of the friends controller. It does not need to know about the tabBar. If I were you, I would create a protocol to let know your tabBarController that the number of friend requests has changed.
First, define the protocol:
protocol FriendsRequestDelegate {
func friendsRequestsDidChange(number: Int)
}
Then, add the variable to your FriendsViewController:
weak var delegate: FriendsRequestDelegate?
And, we need to trigger that func, still in FriendsViewController, after fetching the number of friends request, add;
delegate?.friendsRequestsDidChange(number: myFriendsRequests.count)
Finally, make your TabBarController conform to this protocol;
extension TabBarController: FriendsRequestDelegate {
func friendsRequestsDidChange(number: Int) {
friendsBarItem.badgeValue = String(number)
}
}
See what I mean? This way your friendsRequestController doesn't know about the tabBar and it'll keep your code clean.
I've found a quite simple way to access the barItems. I only had to access the array of tabBar items with:
tabBar.items![2].badgeValue = ""
Here is my code for the UITabBarController:
class TabBarController: UITabBarController, MainMethodsDelegate {
var myFriendsRequests: [UserInfo] = []
var friendRequestsCount: Int = 0
func getFriendsRequests_Methods_Destination(myFriendsRequest: UserInfo) {
myFriendsRequests.append(myFriendsRequest)
self.friendRequestsCount = myFriendsRequests.count
DispatchQueue.main.async {
self.tabBar.items![2].badgeValue = String(self.friendRequestsCount)
}
}
let mainMethods = MainMethods()
override func viewDidLoad() {
super.viewDidLoad()
mainMethods.delegate = self
mainMethods.getFriendsRequests()
}
}
The sub view controller does not need to check and update the badgeValue anymore. (This is fine in my case since the getFriendsRequests_Methods_Destination(myFriendsRequest: UserInfo) method gets triggered as soon as there is a new friends request anyways using a (Firestore) snapshot listener.)
My initial way was to access the tabItem from each of the sub view controllers individually using tabBarController!.tabBar.items![2].badgeValue = "" But this would have been redundant in my case.

Delegate loosing View Controller objects swift

All,
I have set up a protocol, and the view controller is the delegate of this protocol, like so :
import Foundation
protocol PayButtonProtocol {
func enablePayButton()
func disablePayButton()
}
And the view controller is the delegate :
class ViewController: UIViewController, PayButtonProtocol
The protocol functions are as follows :
func enablePayButton() {
println("Button enabled")
PAYBarButton.enabled = false
}
func disablePayButton() {
PAYBarButton.enabled = false
}
I set a class and assign the delegate :
class Trigger
{
var delegate:PayButtonProtocol?
func EnablePayButton()
{
delegate?.enablePayButton()
}
}
Then I set the trigger to run the function :
let localtrigger = Trigger()
localtrigger.delegate = ViewController()
localtrigger.EnablePayButton()
This works and the 'button enabled' is displayed in the console. But the Bar Button (PAYBarButton) is nil and it seems that the view controller has lost its hieracy as I cannot access any of the view controllers objects. The View Controller was built with interface builder. Anyone got any ideas ? Is it
localtrigger.delegate = ViewController()
that rebuilds the viewconotroller and makes the original one not accessible ? If so how do i do this ?
if you are creating the localTrigger object inside your ViewController class you can just do:
let localtrigger = Trigger()
localtrigger.delegate = self // self is an instance of ViewController
localtrigger.EnablePayButton()

Passing data between views in ONE ViewController in Swift

All of the searches I've done focus on passing data between view controllers. That's not really what I'm trying to do. I have a ViewController that has multiple Views in it. The ViewController has a slider which works fine:
var throttleSetting = Float()
#IBAction func changeThrottleSetting(sender: UISlider)
{
throttleSetting = sender.value
}
Then, in one of the Views contained in that same ViewController, I have a basic line that (for now) sets an initial value which is used later in the DrawRect portion of the code:
var RPMPointerAngle: CGFloat {
var angle: CGFloat = 2.0
return angle
}
What I want to do is have the slider's value from the ViewController be passed to the View contained in the ViewController to allow the drawRect to be dynamic.
Thanks for your help!
EDIT: Sorry, when I created this answer I was having ViewControllers in mind. A much easier way would be to create a method in SomeView and talk directly to it.
Example:
class MainViewController: UIViewController {
var view1: SomeView!
var view2: SomeView!
override func viewDidLoad() {
super.viewDidLoad()
// Create the views here
view1 = SomeView()
view2 = SomeView()
view.addSubview(view1)
view.addSubview(view2)
}
#IBAction func someAction(sender: UIButton) {
view1.changeString("blabla")
}
}
class SomeView: UIView {
var someString: String?
func changeString(someText: String) {
someString = someText
}
}
Delegate:
First you create a protocol:
protocol NameOfDelegate: class { // ": class" isn't mandatory, but it is when you want to set the delegate property to weak
func someFunction() // this function has to be implemented in your MainViewController so it can access the properties and other methods in there
}
In your Views you have to add:
class SomeView: UIView, NameOfDelegate {
// your code
func someFunction() {
// change your slider settings
}
}
And the last step, you'll have to add a property of the delegate, so you can "talk" to it. Personally I imagine this property to be a gate of some sort, between the two classes so they can talk to each other.
class MainViewController: UIViewController {
weak var delegate: NameOfDelegate?
#IBAction func button(sender: UIButton) {
if delegate != nil {
let someString = delegate.someFunction()
}
}
}
I used a button here just to show how you could use the delegate. Just replace it with your slider to change the properties of your Views
EDIT: One thing I forgot to mention is, you'll somehow need to assign SomeView as the delegate. But like I said, I don't know how you're creating the views etc so I can't help you with that.
In the MVC model views can't communicate directly with each other.
There is always a view controller who manages the views. The views are just like the controllers minions.
All communication goes via a view controller.
If you want to react to some view changing, you can setup an IBAction. In the method you can then change your other view to which you might have an IBOutlet.
So in your example you might have an IBAction for the slider changing it's value (as in your original question) from which you could set some public properties on the view you would like to change. If necessary you could also call setNeedsDisplay() on the target view to make it redraw itself.

Resources