Firebase Call running last in ViewDidLoad - swift

I have a view controller in which I want to make a firebase pull, and then I want to push the data from the firebase pull into a "DataController" that transforms the data so it fits into the UITableViewDelegate. The problem is that there are four DataControllers, and a segmented control selects which of them is being used.
How do I make it so that the firebase function runs first in ViewDidLoad() and I can pass a value into the DataController.
Relevant lines of code are below:
class ViewController: UIViewController {
var contactData: ContactData!
private var dataControllers: [DataController] = [
DataController1(data:contactData),
// this is where am setting the DataController init data currently, but I have to let firebase run first
DataController2(),
DataController3(),
DataController4()
]
private var segment: Segment = .profile {
didSet {
let dataController = self.dataControllers[self.segment.rawValue]
self.tableView.dataSource = dataController
self.tableView.delegate = dataController
self.tableView.reloadData()
}
}
self.tableView.dataSource = dataController
self.tableView.delegate = dataController
override func viewDidLoad() {
super.viewDidLoad()
FirebaseAPI.shared.getData(contactID: contact.contactID) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let result):
self?.contactData = result
case .failure(let error):
print(error)
}
}
}
//.....
}
How would I feed contactData into DataController(data: ContactData)? Right now the firebase code is running last in ViewDidLoad().
Sorry for the basic question, I am still learning Swift and Firebase.

Why don't you create that DataController in callback?
class ViewController: UIViewController {
var contactData: ContactData!
var dataController : DataController?
// this is where am setting the DataController init data currently, but I have to let firebase run first
override func viewDidLoad() {
super.viewDidLoad()
FirebaseAPI.shared.getData(contactID: contact.contactID) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let result):
dataController = DataController(data: result)
self.tableView.dataSource = dataController
self.tableView.delegate = dataController
self.tableView.reloadData();
case .failure(let error):
print(error)
}
}
}
//.....
}
}

Related

How to listen for data change with #Published variable then reload tableView

The most difficult task I face is to know the correct terminology to search for. I'm used to SwiftUI for an easy way to build an app in the fastest time possible. With this project I have to use UIKit and for this specific task.
Inside a view controller I created a tableView:
private let tableView: UITableView = {
let table = UITableView()
table.register(ProfileCell.self, forCellReuseIdentifier: ProfileCell.identifier)
return table
}()
Later I reload the data inside viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
try await viewModel.getProfiles()
// Here I reload the table when data comes in
self.tableView.reloadData()
} catch {
print(error)
}
}
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
}
So what is viewModel? In SwiftUI I'm used to having this inside a view struct:
#ObservedObject var viewModel = ProfilesViewModel()
..and that's what I have inside my view controller. I've searched for:
observedobject in uitableview
uitableview reload data on data change
..and more but noting useful for me to "pick up the pieces" with.
In same controller, I'm showMyViewControllerInACustomizedSheet which now uses UIHostingController:
private func showMyViewControllerInACustomizedSheet() {
// A SwiftUI view along with viewModel being passed in
let view = ProfilesMenu(viewModel: viewModel)
let viewControllerToPresent = UIHostingController(rootView: view)
if let sheet = viewControllerToPresent.sheetPresentationController {
sheet.detents = [.medium(), .large()]
sheet.largestUndimmedDetentIdentifier = .medium
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
sheet.prefersEdgeAttachedInCompactHeight = true
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
}
present(viewControllerToPresent, animated: true, completion: nil)
}
For the ProfilesViewModel:
class ProfilesViewModel: ObservableObject {
// ProfilesResponse is omitted
#Published var profiles = [ProfilesResponse]()
public func getProfiles(endpoint: String? = nil) async throws -> Void {
// After getting the data, I set the profiles variable
self.profiles = [..]
}
}
Whenever I call try await viewModel.getProfiles(endpoint: "..."), from ProfileMenu, I'd like to reload the tableView. What additional setup is required?
In the comments, Vadian mentioned "Combine" where I did a Google search and found this. What works, for a basic demonstaration:
[..]
import Combine
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private let viewModel = ProfilesViewModel()
private var cancellable: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
try await viewModel.getProfiles()
// Remove this
// self.tableView.reloadData()
} catch {
print(error)
}
}
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
// Add this
cancellable = viewModel.objectWillChange.sink(receiveValue: { [weak self] in
self?.render()
})
}
// Also add this
private func render() {
// TODO: Implement failures...
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
...
}
objectWillChange was the key to my problem.

When to use weak self with URLSession.shared.dataTask

I have the following, but it completes using a closure. The question is whether the code will cause a memory cycle without the use of [weak self].
class ViewModel {
init() {}
var completion: ((Users) -> Void)?
func downloadFiles() {
guard let url = URL(string: "https://reqres.in/api/users?page=2") else {return}
let task = URLSession.shared.dataTask(
with: url,
completionHandler: { data, response, _ in // add [weak self]
guard let data = data,
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else { return }
if let decoded = try? JSONDecoder().decode(Users.self, from: data) {
self.completion?(decoded)
}
})
task.resume()
}
}
using the following viewcontroller
class ViewController: UIViewController {
private let viewModel: ViewModel
override func viewDidLoad() {
super.viewDidLoad()
}
override func loadView() {
let view = UIView()
view.backgroundColor = .red
self.view = view
}
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel.completion = {
print($0)
}
viewModel.downloadFiles()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and model
struct Users: Decodable {
let page: Int
let per_page: Int
let total: Int
let total_pages: Int
let data: [UserData]
}
struct UserData: Decodable {
let id: Int
let email: String
let first_name: String
let last_name: String
let avatar: String
}
So should [weak self] be used, and if not why not. This is a minimum example, and as such the memory debugger doesn't help me out here, but I want to know for the general case.
You need to use [weak self]. There are times when you won't end up with a memory leak not using [weak self], but you can't rely on that. Suppose your request takes too long to load, and the user clicks the back button. Your ViewController will wan to deinitalize, and clear itself from memory, but it can't. It can't because it has a strong reference to your ViewModel which is still downloading and has a strong reference there also. So basically you end up with a memory leak, that is your ViewController remains in memory, which you can't clear anymore.
In your code URLSessionDataTask completion block holds reference to view model. This means that it won't be released until task completes. There is no memory leak because you don't have situation where object A holds object B and object B holds object A.
However, we can ask a question: Do you need results of your data download when view controller is released and thus should be view model. If not you should go with [weak self] for view model

Swift - Pass the CoreDataStack or just Context?

I'm trying to figure out Core Data. I've been following some different tutorials and they all do things a bit differently.
I have a CoreDataStack and it's initialized in SceneDelegate
lazy var coreDataStack = CoreDataStack(modelName: "model")
I believe I then use dependency injection? to set a corresponding property in the viewControllers
guard let tabController = window?.rootViewController as? UITabBarController,
let viewController = navigationController.topViewController as? ViewController else {
fatalError("Application storyboard mis-configuration. Application is mis-configured")
}
viewController.coreDataStack = coreDataStack
viewController.context = coreDataStack.ManagedObjectContext
My questions is should I pass the entire coreDataStack object to the next view? Or just the context?
Initially I was passing the entire coreDataStack, Everything seemed to work just fine. But I wasn't sure if that was correct since most tutorials seem to only reference the context. (But even then, most tutorials are vastly different, even when they are made by the same author.)
import UIKit
import CoreData
class CoreDataStack {
private let modelName: String
init(modelName: String) {
self.modelName = modelName
setupNotificationHandling()
}
lazy var managedContext: NSManagedObjectContext = {
return self.storeContainer.viewContext
}()
private lazy var storeContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: self.modelName)
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
print("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()
// MARK: - Notification Handling
func saveForDidEnterBackground() {
saveContext()
}
#objc func saveChanges(_ notification: Notification) {
saveContext()
}
// MARK: - Helper Methods
private func setupNotificationHandling() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(saveChanges(_:)),
name: UIApplication.willTerminateNotification,
object: nil)
}
// MARK: -
private func saveContext() {
guard managedContext.hasChanges else { return }
do {
try managedContext.save()
} catch {
print("Unable to Save Managed Object Context")
print("\(error), \(error.localizedDescription)")
}
}
}

Getting error: Argument passed to call that takes no arguments

I'm getting this error in the AppDelegate, but I'm not sure what the problem might be. Any help is appreciated, thanks!
(the exact line that has the error is: "let detailsViewModel = DetailsJobView(details: details)" in the "private func loadDetails" section)
Btw, the error underlines the "d" in the second "details" in (details: details)
I've noted it in the code, but it might be hard to find.
import UIKit
import Firebase
import CoreLocation
import Moya
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
let window = UIWindow()
let locationService = LocationService()
let homeStoryboard = UIStoryboard(name: "Home", bundle: nil)
let service = MoyaProvider<YelpService.BusinessesProvider>()
let jsonDecoder = JSONDecoder()
var navigationController: UINavigationController?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Change color of tab bar items
UITabBar.appearance().tintColor = .black
FirebaseApp.configure()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
locationService.didChangeStatus = { [weak self] success in
if success {
self?.locationService.getLocation()
}
}
locationService.newLocation = { [weak self] result in
switch result {
case .success(let location):
self?.loadJobs(with: location.coordinate)
case .failure(let error):
assertionFailure("Error getting the users location \(error)")
}
}
switch locationService.status {
case .notDetermined, .denied, .restricted:
let locationViewController = homeStoryboard.instantiateViewController(withIdentifier: "LocationViewController") as? LocationViewController
locationViewController?.delegate = self
window.rootViewController = locationViewController
default:
let nav = homeStoryboard
.instantiateViewController(withIdentifier: "JobNavigationController") as? UINavigationController
self.navigationController = nav
window.rootViewController = nav
locationService.getLocation()
(nav?.topViewController as? JobTableViewController)?.delegete = self
}
window.makeKeyAndVisible()
return true
}
private func loadDetails(for viewController: UIViewController, withId id: String) {
service.request(.details(id: id)) { [weak self] (result) in
switch result {
case .success(let response):
guard let strongSelf = self else { return }
if let details = try? strongSelf.jsonDecoder.decode(Details.self, from: response.data) {
let detailsViewModel = DetailsJobView(details: details) //ERROR IS HERE
(viewController as? DetailsJobViewController)?.viewModel = detailsViewModel
}
case .failure(let error):
print("Failed to get details \(error)")
}
}
}
private func loadJobs(with coordinate: CLLocationCoordinate2D) {
service.request(.search(lat: coordinate.latitude, long: coordinate.longitude)) { [weak self] (result) in
guard let strongSelf = self else { return }
switch result {
case .success(let response):
let root = try? strongSelf.jsonDecoder.decode(Root.self, from: response.data)
let viewModels = root?.jobs
.compactMap(JobListViewModel.init)
.sorted(by: { $0.distance < $1.distance })
if let nav = strongSelf.window.rootViewController as? UINavigationController,
let jobListViewController = nav.topViewController as? JobTableViewController {
jobListViewController.viewModels = viewModels ?? []
} else if let nav = strongSelf.homeStoryboard
.instantiateViewController(withIdentifier: "JobNavigationController") as? UINavigationController {
strongSelf.navigationController = nav
strongSelf.window.rootViewController?.present(nav, animated: true) {
(nav.topViewController as? JobTableViewController)?.delegete = self
(nav.topViewController as? JobTableViewController)?.viewModels = viewModels ?? []
}
}
case .failure(let error):
print("Error: \(error)")
}
}
}
}
extension AppDelegate: LocationActions, ListActions {
func didTapAllow() {
locationService.requestLocationAuthorization()
}
func didTapCell(_ viewController: UIViewController, viewModel: JobListViewModel) {
loadDetails(for: viewController, withId: viewModel.id)
}
}
Here is the DetailsJobView as requested by #heitormurara:
import UIKit
import MapKit
#IBDesignable class DetailsJobView: CoreView {
#IBOutlet weak var collectionView: UICollectionView?
#IBOutlet weak var pageControl: UIPageControl?
#IBOutlet weak var priceLabel: UILabel?
#IBOutlet weak var hoursLabel: UILabel?
#IBOutlet weak var locationLabel: UILabel?
#IBOutlet weak var ratingsLabel: UILabel?
#IBOutlet weak var mapView: MKMapView?
#IBAction func handleControl(_ sender: UIPageControl) {
}
}
My guess is you are trying to instance a UIView child in an improper way. Try initialising it without arguments and having your details variable as a public variable:
let detailsViewModel = DetailsJobView()
detailsViewModel.details = details
Also, you need to create the variable details in your DetailsJobView:
import UIKit
import MapKit
#IBDesignable class DetailsJobView: CoreView {
...
var details: Details?

How to update variable in MVVM?

I am trying to use MVVM. I am going to VC2 from VC1. I am updating the viewModel.fromVC = 1, but the value is not updating in the VC2.
Here is what I mean:
There is a viewModel, in it there is a var fromVC = Int(). Now, in vc1, I am calling the viewModel as
let viewModel = viewModel().
Now, on the tap of button, I am updating the viewModel.fromVC = 8. And, moving to the next screen. In the next screen, when I print fromVC then I get the value as 0 instead of 8.
This is how the VC2 looks like
class VC2 {
let viewModel = viewModel()
func abc() {
print(viewModel.fromVC)
}
}
Now, I am calling abc() in viewDidLoad and the fromVC is printed as 0 instead of 8. Any help?
For the MVVM pattern you need to understand that it's a layer split in 2 different parts: Inputs & Outputs.
Int terms of inputs, your viewModel needs to catch every event from the viewController, and for the Outputs, this is the way were the viewModel will send data (correctly formatted) to the viewController.
So basically, if we have a viewController like this:
final class HomeViewController: UIViewController {
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
titleLabel.text = "toto"
}
}
We need to extract the responsibilities to a viewModel, since the viewController is handling the touchUp event, and owning the data to bring to th label.
By Extracting this, you will keep the responsibility correctly decided and after all, you'll be able to test your viewModel correctly 🙌
So how to do it? Easy, let's take a look to our futur viewModel:
final class HomeViewModel {
// MARK: - Private properties
private let title: String
// MARK: - Initializer
init(title: String) {
self.title = title
}
// MARK: - Outputs
var titleText: ((String) -> Void)?
// MARK: - Inputs
func viewDidLoad() {
titleText?("")
}
func buttonDidPress() {
titleText?(title)
}
}
So now, by doing this, you are keeping safe the different responsibilities, let's see how to bind our viewModel to our previous viewController :
final class HomeViewController: UIViewController {
// MARK: - public var
var viewModel: HomeViewModel!
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
// MARK: - Private func
private func bind(to viewModel: HomeViewModel) {
viewModel.titleText = { [weak self] title in
self?.titleLabel.text = title
}
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
viewModel.buttonDidPress()
}
}
So one thing is missing, you'll asking me "but how to initialise our viewModel inside the viewController?"
Basically you should once again extract responsibilities, you could have a Screens layer which would have the responsibility to create the view like this:
final class Screens {
// MARK: - Properties
private let storyboard = UIStoryboard(name: StoryboardName, bundle: Bundle(for: Screens.self))
// MARK: - Home View Controller
func createHomeViewController(with title: String) -> HomeViewController {
let viewModel = HomeViewModel(title: title)
let viewController = storyboard.instantiateViewController(withIdentifier: "Home") as! HomeViewController
viewController.viewModel = viewModel
return viewController
}
}
And finally do something like this:
let screens = Screens()
let homeViewController = screens.createHomeViewController(with: "Toto")
But the main subject was to bring the possibility to test it correctly, so how to do it? very easy!
import XCTest
#testable import mvvmApp
final class HomeViewModelTests: XCTestCase {
func testGivenAHomeViewModel_WhenViewDidLoad_titleLabelTextIsEmpty() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
viewModel.titleText = { title in
XCTAssertEqual(title, "")
expectation.fulfill()
}
viewModel.viewDidLoad()
waitForExpectations(timeout: 1.0, handler: nil)
}
func testGivenAHomeViewModel_WhenButtonDidPress_titleLabelTextIsCorrectlyReturned() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
var counter = 0
viewModel.titleText = { title in
if counter == 1 {
XCTAssertEqual(title, "toto")
expectation.fulfill()
}
counter += 1
}
viewModel.viewDidLoad()
viewModel.buttonDidPress()
waitForExpectations(timeout: 1.0, handler: nil)
}
}
And that's it 💪