I'm trying to create an AppCoordinator which holds the different ViewControllers in my app:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.main.bounds)
let appCoordinator = AppCoordinator()
window?.rootViewController = appCoordinator.initialTabBarController()
window?.makeKeyAndVisible()
return true
}
// ...
import SnapKit
class AppCoordinator {
private let initialTabController = TabBarController()
private let cars = CarProvider.sharedInstance.getCars()
init() {
var viewControllers = [ViewController]()
viewControllers.append(carListViewController())
initialTabController.setViewControllers(viewControllers, animated: true)
}
private func carListViewController() -> CarListViewController {
let controller = CarListViewController(cars: cars)
print(self)
controller.delegate = self
return controller
}
}
// MARK: - CarListViewControlerDelegate
extension AppCoordinator: CarListViewControlerDelegate {
func didPullToRefresh() {
print("Did pull")
}
}
My CarListViewController looks like this:
import UIKit
protocol CarListViewControlerDelegate: class {
func didPullToRefresh()
}
class CarListViewController: ViewController {
weak var delegate: CarListViewControlerDelegate?
// MARK: Interface Properties
private let tableView = TableView()
private let refreshControl = UIRefreshControl()
private var cars: [Car]?
// MARK: Initializers
init(cars: [Car]) {
super.init(nibName: nil, bundle: nil)
self.cars = cars
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Actions
extension CarListViewController {
#objc private func refresh() {
delegate?.didPullToRefresh()
refreshControl.endRefreshing()
}
}
My refresh method gets called when I pull to refresh in my UITableView, but the protocol method didPullToRefresh doesn't. When I check on the delegate, it's value is nil.
You need a strong reference so make it an instance var inside AppDelegate
let appCoordinator = AppCoordinator()
OR
var appCoordinator:AppCoordinator!
self.appCoordinator = AppCoordinator()
As Sh_Khan said you need a strong reference. You should change the way you set the delegate for created CarListViewController.
I've changed your code as below. let me know if the problem solved.
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.main.bounds)
let appCoordinator = AppCoordinator()
window?.rootViewController = appCoordinator.initialTabBarController()
window?.makeKeyAndVisible()
return true
}
// ...
import SnapKit
class AppCoordinator {
private let initialTabController = TabBarController()
private let cars = CarProvider.sharedInstance.getCars()
var controller = CarListViewController!
init() {
var viewControllers = [ViewController]()
let carListVC = CarListViewController(cars: cars)
self.controller = carListVC
self.controller.delegate = self
viewControllers.append(self.controller)
initialTabController.setViewControllers(viewControllers, animated: true)
}
}
// MARK: - CarListViewControlerDelegate
extension AppCoordinator: CarListViewControlerDelegate {
func didPullToRefresh() {
print("Did pull")
}
}
Related
I'm trying to open another controller by tapping on the cell of my tableView. I'm coding with MVVM and Coordinator pattern.
In the beginning we see this screen - it is declarated in the method start()
let service = Service()
private(set) weak var navigationController: UINavigationController?
func start() -> UINavigationController {
let vm = ContinentsViewModel(service: service)
let vc = ContinentsViewController(viewModel: vm)
let navigationController = UINavigationController()
self.navigationController = navigationController
navigationController.setViewControllers([vc], animated: false)
bindContinentsViewModel(viewModel: vm)
return navigationController
}
Later, my goal is to open all list of countries of the continent, but now l just need to open empty ViewController by tap on the cell (ex. Africa or Antarctica). Here is my methods for it, but they don't work.
private func showCountries() {
let vc = ViewController()
navigationController?.pushViewController(vc, animated: true)
}
private func bindContinentsViewModel(viewModel: ContinentsViewModel) {
viewModel
.flow
.bind { [weak self] flow in
switch flow {
case .onContinentTap:
self?.showCountries() // don't work
// print("show \(continent)") // work - continent is a param of .onContinentTap, which prints an geo-id of the continent, just if you need to know.
}
}
.disposed(by: viewModel.bag)
}
Thank you so much!
The following works as expected. What are you doing differently?
#main
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var viewModel: ViewModel?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
viewModel = ViewModel()
let controller = viewModel?.start()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = controller
window?.makeKeyAndVisible()
return true
}
}
final class ViewModel {
private(set) weak var navigationController: UINavigationController?
func start() -> UINavigationController {
let vm = ContinentsViewModel()
let vc = ContinentsViewController(viewModel: vm)
let navigationController = UINavigationController()
self.navigationController = navigationController
navigationController.setViewControllers([vc], animated: false)
bindContinentsViewModel(viewModel: vm)
return navigationController
}
private func showCountries() {
let vc = UIViewController()
vc.view.backgroundColor = .blue
navigationController?.pushViewController(vc, animated: true)
}
private func bindContinentsViewModel(viewModel: ContinentsViewModel) {
viewModel.flow
.bind { [weak self] flow in
switch flow {
case .onContinentTap:
self?.showCountries()
}
}
.disposed(by: viewModel.bag)
}
}
final class ContinentsViewModel {
enum Flow {
case onContinentTap
}
let flow: Observable<Flow>
let bag = DisposeBag()
init() {
flow = .just(.onContinentTap)
.delay(.seconds(3), scheduler: MainScheduler.instance)
}
}
final class ContinentsViewController: UIViewController {
var viewModel: ContinentsViewModel
init(viewModel: ContinentsViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
}
}
Whenever I run this code the init() of the VCModel gets called but Swinject is not Injecting the VCModel instance to my ViewController. Can Someone Please tell me what am I doing wrong?
Error I get is:
Unexpectedly Found nil while unwrapping an optional value in
ViewController viewModel.cellModels
AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
container = Container() { con in
con.register(VCModeling.self) { _ in
VCModel()
}
con.storyboardInitCompleted(ViewController.self) { r, c in
c.viewModel = r.resolve(VCModeling.self)!
}
}
let window = UIWindow(frame: UIScreen.main.bounds)
window.backgroundColor = UIColor.white
window.makeKeyAndVisible()
self.window = window
let bundle = Bundle(for: ViewController.self)
let storyboard = SwinjectStoryboard.create(name: "Main", bundle: bundle, container: container)
window.rootViewController = storyboard.instantiateInitialViewController()
return true
}
ViewController
private let disposeBag = DisposeBag()
var viewModel: VCModeling!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
viewModel.cellModels
.bind(to: tableView.rx.items(cellIdentifier: "myCell", cellType: MyCellClass.self)) {
i, cellModel, cell in
cell.viewModel = cellModel
}.disposed(by: disposeBag)
}
Can you try out the following code in AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
container = Container() { con in
con.register(VCModeling.self) { _ in
VCModel()
}
con.storyboardInitCompleted(ViewController.self) { r, c in
c.viewModel = r.resolve(VCModeling.self)!
let window = UIWindow(frame: UIScreen.main.bounds)
window.backgroundColor = UIColor.white
window.makeKeyAndVisible()
self.window = window
window.rootViewController = c
}
}
return true
}
Code in your AppDelegate → application:didFinishLaunchingWithOptions method does seems to work properly. I have verified it with following code:
class ViewController: UIViewController {
var viewModel: VCModeling!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
print(viewModel.uuid)
}
}
protocol VCModeling {
var uuid: UUID { get }
}
class VCModel: VCModeling {
let uuid: UUID
init() {
self.uuid = UUID()
}
}
I can not tell what your VCModel's init method looks like but looking at
...
// Do any additional setup after loading the view.
viewModel.cellModels
...
From the error you are getting:
Unexpectedly Found nil while unwrapping an optional value in ViewController viewModel.cellModels
It looks like cellModels which I assume is implicitly unwrapped property, you must initialize it VCModel's init method.
While trying to bind some protocols to various classes I have encountered a strange behaviour of getting nil in the protocols.
This is the dependency injector class:
class DependencyInjector: NSObject {
let container: Container
override init() {
container = Container()
super.init()
register()
}
}
extension DependencyInjector {
func register() {
container.register(NetworkProtocols.self) { _ in ApiManager() }.inObjectScope(.container)
container.register(ConnectivityManager.self) { r in
let controller = ConnectivityManager()
controller.network = r.resolve(NetworkProtocols.self)
return controller
}
container.storyboardInitCompleted(ChooseCameraViewController.self) {r,c in
c.network = r.resolve(NetworkProtocols.self)
}
container.storyboardInitCompleted(InstallationViewController.self) {r,c in
c.network = r.resolve(NetworkProtocols.self)
}
container.storyboardInitCompleted(CameraSetupViewController.self) {r,c in
c.network = r.resolve(NetworkProtocols.self)
}
container.storyboardInitCompleted(LoginViewController.self) { r,c in
c.network = r.resolve(NetworkProtocols.self)
}
}
}
This is the AppDelegate:
var dependencyInjector: DependencyInjector!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
dependencyInjector = DependencyInjector()
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window
let bundle = Bundle(for: ViewController.self)
let storyboard = SwinjectStoryboard.create(name: "Main", bundle: bundle, container: dependencyInjector.container)
window.rootViewController = storyboard.instantiateInitialViewController()
return true
}
In the class below I get nil in the protocol property:
class ConnectivityManager: NSObject {
var network: NetworkProtocols!
func connectHotSpot() {
self.network.whoAmI(success: {
print("api succses")
}, failure: { (error) in
print(error.localizedDescription)
})
}
}
The network var is always nil while in the other classes that use storyboardInitCompleted the network var works.
What am I doing wrong?
As #JakubVano said I had to obtain an instance of ConnectivityManager:
container.register(ConnectivityManager.self) { r in
let controller = ConnectivityManager()
controller.network = r.resolve(NetworkProtocols.self)
return controller
}
container.storyboardInitCompleted(ChooseCameraViewController.self) {r,c in
c.connectivityManager = r.resolve(ConnectivityManager.self)!
}
my code:
public func start() {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
guard let listVC = storyboard.instantiateViewController(withIdentifier: "ListVC") as? ListVC else { return }
let viewModel = ListViewModel(dependencies: appDependencies)
viewModel.delegate = self
listVC.listViewModel = viewModel
navigationController?.pushViewController(listVC, animated: true)
}
protocol ListViewModelDelegate: class {
func needChangeScreen(cellViewModel: UserCellViewModel)
}
final class ListViewModel {
weak var delegate: ListViewModelDelegate?
func userPressed(at index: IndexPath) {
delegate?.needChangeScreen(cellViewModel: cellViewModels[index.row])
}
}
User pressed is called from UIViewController , then i want to send callback to Coordinator to start another coordinator but delegate? is always nil. I know that delegates should be weak but in this case is not working for me. Any ideas?
Okey i have fix but i do not know is it good.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let navigationController = UINavigationController()
navigationController.setNavigationBarHidden(true, animated: false)
window?.rootViewController = navigationController
let appDependencies = configureAppDependencies()
let coordinator = AppCoordinator(navigationController: navigationController, appDependencies: appDependencies)
coordinator.start()
window?.makeKeyAndVisible()
return true
}
That was my app delegate function(In AppCoordinator start is created and pushed ListCoordinator) but when i changed let coordinator for instance var :
var coordinator: AppCoordinator?
weak delegate is not nil and everything works.
Similar questions to this have been asked so I apologize, but none of them have been able to help me.
I am struggling to return the value from this asynchronous request to Firebase with a completion handler. The value I am retrieving from Firebase is an array and it does exist. But
Here is my function for making the request to Firebase:
class SearchManager {
var searchResults = [String]()
var listOfMosaics = [String]()
// Retrieves company list from Firebase
func getMosaicTitles(completionHandler: #escaping (_ mosaics: [String]) -> ()) {
Database.database().reference().child("mosaics").observeSingleEvent(of: .value, with: { (snapshot) in
guard let allMosaics = snapshot.value as? [String] else {
print("unable to unwrapp datasnapshot")
return
}
completionHandler(allMosaics)
})
}
// resets search results array
func resetSearch() {
searchResults = []
}
// takes list of all mosaics and filters based on search text
func filterAllMosaics(searchText: String) {
searchResults = listOfMosaics.filter { $0.contains(searchText) }
}
}
And in the AppDelegate I call it like this posting a Notification:
let searchManager = SearchManager()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
makeRootViewLaunchScreen()
FirebaseApp.configure()
searchManager.getMosaicTitles { (results) in
self.searchManager.listOfMosaics = results
NotificationCenter.default.post(name: NSNotification.Name("mosaicsReturned"), object: nil)
self.stopDisplayingLaunchScreen()
}
// Adds border to bottom of the nav bar
UINavigationBar.appearance().shadowImage = UIImage.imageWithColor(color: UIColor(red:0.00, green:0.87, blue:0.39, alpha:1.0))
// Override point for customization after application launch.
return true
}
func makeRootViewLaunchScreen() {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
let viewController = mainStoryboard.instantiateViewController(withIdentifier: "launchScreen")
UIApplication.shared.keyWindow?.rootViewController = viewController
}
// reassigns root view after Firebase request complete
func stopDisplayingLaunchScreen() {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = mainStoryboard.instantiateViewController(withIdentifier: "centralViewController")
UIApplication.shared.keyWindow?.rootViewController = viewController
}
In the viewDidLoad of the viewController that supports the tableView that uses the retrieved array to populate it I add a Notification Observer.
var listOfMosaics = [String]()
var searchResults = [String]() {
didSet {
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
listOfMosaics = searchManager.listOfMosaics
configureSearchBar()
configureSearchBarTextField()
self.tableView.separatorColor = UIColor(red:0.00, green:0.87, blue:0.39, alpha:1.0)
NotificationCenter.default.addObserver(self, selector: #selector(updateListOfMosaics), name: NSNotification.Name("mosaicsReturned"), object: nil)
}
#objc func updateListOfMosaics(notification: Notification) {
listOfMosaics = searchManager.listOfMosaics
}
But when I call the below code it doesn't work the arrays print as empty and as a result it doesn't update my tableView.
extension SearchResultsTableViewController: UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchManager.resetSearch()
searchManager.filterAllMosaics(searchText: searchBar.text!)
tableView.reloadData()
print(listOfMosaics)
print(searchResults)
}
}
Thank you in advanced for the help.
This should work for you now. I think you didn't pass the instance of SearchManager from your AppDelegate to your ViewController. I'm guessing you created a new instance of SearchManager in your ViewController, which has an empty array.
Search Manager:
class SearchManager {
var searchResults = [String]()
var listOfMosaics = [String]()
func getMosaicTitles(completionHandler: #escaping (_ mosaics: [String]) -> ()) {
Database.database().reference().child("mosaics").observeSingleEvent(of: .value, with: { (snapshot) in
guard let allMosaics = snapshot.value as? [String] else {
print("unable to unwrapp datasnapshot")
completionHandler([]) // <- You should include this too.
return
}
completionHandler(allMosaics)
})
}
func resetSearch() {
searchResults = []
}
func filterAllMosaics(searchText: String) {
searchResults = listOfMosaics.filter { $0.contains(searchText) }
}
}
View Controller:
class TableViewController: UITableViewController {
var searchManager: SearchManager?
var listOfMosaics = [String]()
var searchResults = [String]() {
didSet {
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
guard let searchManager = searchManager else { return }
listOfMosaics = searchManager.listOfMosaics
print("List of mosaics: \(listOfMosaics)")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
}
AppDelegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let searchManager = SearchManager()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
makeRootViewLaunchScreen()
FirebaseApp.configure()
searchManager.getMosaicTitles { results in
self.searchManager.listOfMosaics = results
self.stopDisplayingLaunchScreen()
}
return true
}
func makeRootViewLaunchScreen() {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
let viewController = mainStoryboard.instantiateViewController(withIdentifier: "launchScreen")
window?.rootViewController = viewController
window?.makeKeyAndVisible()
}
func stopDisplayingLaunchScreen() {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
guard let viewController = mainStoryboard.instantiateViewController(withIdentifier: "centralViewController") as? TableViewController else { return }
let navigationController = UINavigationController(rootViewController: viewController)
viewController.searchManager = searchManager
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
}
}
As #TNguyen says in his comment, it sounds like you aren't waiting for the async function getMosaicTitles() to complete.
You might want to disable the search bar button while the call is running, and enable it from the completion handler once the call is complete. Then the user won't be able to click the search button until the results have finished loading.
You can fetch the data from the database in a background thread and add a completion block, so that the tableView reloads only after the updated content is fetched.