Cleaning UINavigationController after show in Swift - ios

I have very strange bug, that I didn't have in objective-c.
I have two navigations controller one after another. In first I have UITableView. On cell click I navigate to second controller, and with clicking back button I navigate to first navigation controller. (I don't do anything else.) My memory go up every time that I navigate to second controller but it doesn't go down when I go back.
Code that I have :
First View Controller :
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "segue", sender: self)
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segue",
let destination = segue.destination as? SecondViewController,
let rowIndex = table.indexPathForSelectedRow {
let item = allItems[rowIndex.section][rowIndex.row]
destination.itemId = segue.id as Int?
destination.coreDataManager = self.coreDataManager
}
}
Second View Controller
override func viewDidLoad() {
super.viewDidLoad()
// reload data
reloadData()
}
private func reloadData() {
// We need database ready to reload data from server
guard coreDataManager != nil else {
print("We try to prepare view, but core data is not ready jet.")
// stop loader
self.loader.stopLoader()
return
}
self.model = Model.init(itemId: itemId! as NSNumber!, managadContext: coreDataManager!.privateChildManagedObjectContext())
}
Model object is object from Objective-c library.
I know that this object is problematic, because if I comment out last row, the step memory graph disappear.
I use same library with same call in previous Objective-C application and I didn't have this memory leak.
I try with :
deinit {
self.model = nil
}
but it didn't help.
How to solve this memory leak, because if you look at the graph it is quite huge. After opening 4 cells I have 187 MB memory used.
EDIT:
I figured that deinit {} is never called.
SUGGESTION
I make coreDataManager as weak var:
weak var coreDataManager: CoreDataManager? // weak property of Second Controller
FULL CODE:
Second controller
import UIKit
import BIModel
class SecondViewController: UIViewController {
// MARK: - Properties
/// This object represent connection to database.
weak var coreDataManager: CoreDataManager?
/// This is a Server object that represent on witch server we try to connect.
var currentServer: Server?
/// This is a cube id that we would like to open in this dashboard.
var itemId: Int? = 1193 // TODO: replace this
/// This is a model object. This object holds all calculations, all data,...
var model : Model?
// MARK: - Life Cicle
override func viewDidLoad() {
super.viewDidLoad()
// reload data
reloadData()
}
private func reloadData() {
// We need database ready to reload data from server
guard coreDataManager != nil else {
print("We try to prepare view, but core data is not ready jet.")
return
}
guard itemId != nil else {
return
}
self.model = Model.init(cubeId: currentCubeId! as NSNumber!, managadContext: coreDataManager!.privateChildManagedObjectContext())
}
einit {
print("DEINIT HAPPENED")
model = nil
}
}
I clean code a little bit. This is now whole code. "DEINIT HAPPENED" is printed, but memory stack is the same.

Related

Networking in Swift Using WikipediaKit / Delegate Design Pattern

I'm trying to request basic info from Wikipedia - user selects tableView row and is taken to a new view that displays info associated with the row (album descriptions). Flow is:
tableView Cell passes user choice to network struct, performs segue to new view
network struct uses choice to query Wikipedia API
received info is passed via delegate method to new view
info is displayed in new view to user
Two issues I'm having:
Networking seems to not be carried out until after the new view is triggered and updated. Initially I didn't use delegate, and just had networking method return its result. But either way, I need networking to get its result back before we get to and update the new view. I've added Dispatch.main.async to the new view for the UI. I thought I may need a completion handler, but I don't fully understand this
Delegate protocol was introduced to fix problem 1, but it still doesn't work. For some reason nothing of the delegate method in the new view is triggering.
Here's the initial tableView trigger:
extension TopTenViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
albumManager.getAlbumInfo(albumTitle: topTenList[indexPath.row][1])
performSegue(withIdentifier: "topTenAlbumInfoSeg", sender: self)
}
Here is the delegate and all the networking:
protocol AlbumManagerDelegate {
func didUpdateAlbumInfo(albumInfo: String)
}
struct AlbumManager {
var newResult: String = ""
var delegate: AlbumManagerDelegate?
func getAlbumInfo(albumTitle: String) {
let wikipedia = Wikipedia.shared
let language = WikipediaLanguage("en")
let _ = wikipedia.requestArticle(language: language, title: albumTitle, imageWidth: 5) { result in
switch result {
case .success(let article):
print(article.displayTitle)
self.delegate?.didUpdateAlbumInfo(albumInfo: article.displayTitle)
print("delegate should be updated")
case .failure(let error):
print(error)
}
}
}
}
Here is the target view:
class AlbumInfoViewController: UIViewController, AlbumManagerDelegate {
#IBOutlet weak var albumDescriptionLabel: UILabel!
#IBOutlet weak var myWebView: WKWebView!
var albumDescription: String = ""
var albumManager = AlbumManager()
override func viewDidLoad() {
super.viewDidLoad()
albumManager.delegate = self
print(albumDescriptionLabel.text)
}
func didUpdateAlbumInfo(albumInfo: String) {
DispatchQueue.main.async {
print("This worked: \(albumInfo)")
self.albumDescriptionLabel.text = "Hi this is: \(albumInfo)"
}
}
}
Currently nothing in didUpdateAlbumInfo in the new view is being triggered. albumDescriptionLabel is not being updated. Could the issue be that the segue is performed too soon? Thanks in advance!

How to pass data to the final view controller

I am new to Swift and am building an app to learn. Right now I am making the registration section of the app.
I thought the UX would be better if there were multiple VC's asking a single question, i.e. one for your name, one for your birthdate, etc as opposed to jamming all that into a single view controller. The final view controller collects all of that information and sends a dictionary as FUser object to be saved on Firebase.
I figured I could instantiate the final view controller on each of the previous five view controllers and pass that data directly to the end. I kept getting errors and figured out that the variables were nil. It works just fine if I pass the data directly to the next view controller but it doesn't seem to let me send it several view controllers down. Obviously there's a nuance to how the memory is being managed here that I'm not tracking.
Is there a way to do what I am trying to do or do I have to pass the data through each view controller along the way?
import UIKit
class FirstViewController: UIViewController {
//MARK: - IBOutlets
#IBOutlet weak var firstNameTextField: UITextField!
//MARK: - ViewLifeCycle
override func viewDidLoad() {
super.viewDidLoad()
}
//MARK: - IBActions
#IBAction func continueToMiddleViewController(_ sender: Any) {
let vcFinal = storyboard?.instantiateViewController(withIdentifier:
"finalVC") as! finalViewController
vcFinal.firstName = firstNameTextField.text
let vc = storyboard?.instantiateViewController(withIdentifier:
"middleVC") as! middleViewController
vc.modalPresentationStyle = .fullScreen
present(vc, animated: false)
}
...
}
import UIKit
class FinalViewController: UIViewController {
var firstName: String?
...
//MARK: - ViewLifeCycle
override func viewDidLoad() {
super.viewDidLoad()
}
...
}
TL;DR: The fastest one that would solve your problem is creating a singleton
There are many strategies for this. For a starter, it might be a good idea to read some begginer articles, like this one. I can update this answer if you don't find it useful, but it'd look just like the article
Viewcontroller's variable can't be initiated until any of the init method is called.
There are detailed answers on this thread.
Passing Data between ViewControllers
Another way to approach this problem could be to make use of closures. Note that personally I've moved away from using storyboards but I'll try to explain still. Closures are also referred to as callbacks, blocks, or in some context like here - completions.
You can declare a closure like let onSubmitInfo: (String?) -> Void below, it stores a reference to a block of code that can be executed at a later stage just like a function and it takes an optional string as a parameter just like a function can.
The closures are specified in the initialisers where a block of code is passed into the respective classes below and the closures are then called in the IBActions that will trigger the block of code that is defined where the below classes are initialised:
class First: UIViewController {
// MARK: - IBOutlets
#IBOutlet weak var firstNameTextField: UITextField!
// MARK: - Properties
private let onSubmitInfo: (String?) -> Void
init(onSubmitInfo: (String?) -> Void) {
self.onSubmitInfo = onSubmitInfo
}
// MARK: - IBActions
#IBAction func continue(_ sender: Any) {
onSubmitInfo(firstNameTextField.text)
}
}
class Second: UIViewController {
// MARK: - IBOutlets
#IBOutlet weak var lastNameTextField: UITextField!
// MARK: - Properties
private let onSubmitInfo: (String?) -> Void
init(onSubmitInfo: (String?) -> Void) {
self.onSubmitInfo = onSubmitInfo
}
// MARK: - IBActions
#IBAction func continue(_ sender: Any) {
onSubmitInfo(lastNameTextField.text)
}
}
To manage showing the above views and collecting the values returned by their closures (i.e. onSubmitInfo) we create a FlowController class that will also show the next view when the closure is called.
In FlowController we define the closures or blocks of code to be executed when it is called inside the IBAction in the respective First and Second classes above.
The optional string that is provided in the respective First and Second classes is used as the (firstName) and (secondName) closure properties below:
class FlowController: UIViewController {
private var fistName: String?
private var lastName: String?
...
private func showFirstView() {
let firstViewController = First(onSubmitInfo: { (firstName) in
self.firstName = firstName
showSecondView()
})
navigationController?.pushViewController(
firstViewController,
animated: true)
}
private func showSecondView() {
let secondViewController = Second(onSubmitInfo: { (lastName) in
self.lastName = lastName
showFinalView()
})
navigationController?.pushViewController(
secondViewController,
animated: true)
}
private func showFinalView() {
let finalViewController = Final(
firstName: firstName,
lastName: lastName)
navigationController?.pushViewController(
finalViewController,
animated: true)
}
}
The FlowController finally shows the Final view controller after it has collected the firstName form the First view controller and the lastName form the Second view controller in the showFinalView function above.
class Final: UIViewController {
let firstName: String
let lastName: String
...
}
I hope this is a shove in the right direction. I have moved away from storyboards because I find creating views in code is more verbose and clear on peer reviews and it was also easier for me to manage constraints and just to manage views in general.

How to refresh container view (reload table view) after getting data from firebase: Swift

I have been working on an app. I am a beginner so please ignore any mistakes.
The problem is that i have a view controller, which has 2 container view controllers controlled by a segmented control.
enter image description here
All three of them have separate classes: (say)
MainViewController
FirstViewController
SecondViewController
In the main view controller, i am getting some data from firebase, which i am storing in an array, and this array is to be passed to the first and second container views, which have their table views, which will load data based on this array which is passed.
Now before the data comes back in the MainViewController, the First and Second view controllers are already passed with an empty array, and no data loads up in their table views (obviously because the array is empty).
I want the container view controllers to load up after the data is received, and array is loaded. Any help ?, Thanks
P.s I am not performing any segue because these are container views, and they are automatically loaded as the main view container loads.
EDIT: Being more precise and clear with original code:
Originally I have 3 view controllers
SearchResultsScreenViewController (Main VC)
GuidesListSearchScreenViewController (First Container VC)
ServicesListSearchScreenViewController (Second Container VC)
In the Main VC i used a segmented control to see container vc's on screen, here:
import UIKit
import Firebase
class SearchResultsScreenViewController: UIViewController
{
#IBOutlet weak var GuideListView: UIView!
#IBOutlet weak var ServicesListView: UIView!
var searchQueryKeyword: String?
var guidesDataArray = [GuideDM]()
override func viewDidLoad()
{
super.viewDidLoad()
ServicesListView.isHidden = true
populateGuidesList()
}
#IBAction func SegmentChanged(_ sender: UISegmentedControl)
{
switch sender.selectedSegmentIndex
{
case 0:
GuideListView.isHidden = false
ServicesListView.isHidden = true
break
case 1:
GuideListView.isHidden = true
ServicesListView.isHidden = false
break
default:
break
}
}
func populateGuidesList()
{
let dbRef = Firestore.firestore().collection("guide")
dbRef.getDocuments
{ (snapshot, error) in
if let err = error
{
print(err.localizedDescription)
print("Error: Unable to find guides list")
}
else
{
if let snap = snapshot
{
print("List is started now")
for doc in snap.documents
{
if doc.exists
{
let data = doc.data()
let city = data["city"] as? String ?? ""
let province = data["province"] as? String ?? ""
let country = data["country"] as? String ?? ""
if city.localizedCaseInsensitiveContains(self.searchQueryKeyword!) || province.localizedCaseInsensitiveContains(self.searchQueryKeyword!) || country.localizedCaseInsensitiveContains(self.searchQueryKeyword!)
{
let guideId = doc.documentID
let guideEmail = data["email"] as? String ?? ""
let name = data["name"] as? String ?? ""
let dob = data["dob"] as? String ?? ""
let feeCurrency = data["feeCurrency"] as? String ?? ""
let status = data["status"] as? String ?? ""
let totalReviews = data["totalReviews"] as? Int ?? 0
let rating = data["rating"] as? Int ?? 0
let baseFee = data["baseFee"] as? Int ?? 0
let isGuideFeatured = data["isGuideFeatured"] as? Bool ?? false
//make a model of guide and append in array
let guide = GuideDM(id: guideId, email: guideEmail, name: name, dob: dob, city: city, province: province, country: country, feeCurrency: feeCurrency, status: status, baseFee: baseFee, rating: rating, totalReviews: totalReviews, isGuideFeatured: isGuideFeatured)
self.guidesDataArray.append(guide)
}
}
}
print("list is finalized now")
}
}
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
if segue.identifier == "searchScreentoGuideListSegment"
{
let guidesListContainerVC = segue.destination as! GuidesListSearchScreenViewController
guidesListContainerVC.guidesDataArray = self.guidesDataArray
}
}
}
In the above class my code makes a call to function "populateGuidesList()" which makes a network call to get data, and at the same time loads up my container views. The problem is, before the network call returns data, the empty array gets passed to my "GuidesListSearchScreenViewController" i.e. (First container VC), which is a table view, and loads an empty table because the array is not filled yet.
My First container VC class:
import UIKit
import Firebase
class GuidesListSearchScreenViewController: UIViewController
{
#IBOutlet weak var guidesListTableView: UITableView!
var guidesDataArray = [GuideDM]()
override func viewDidLoad()
{
super.viewDidLoad()
guidesListTableView.delegate = self
guidesListTableView.dataSource = self
guidesListTableView.register(UINib(nibName: "GuidesListCellSearchScreenTableViewCell", bundle: nil), forCellReuseIdentifier: "guidesListCell")
}
}
extension GuidesListSearchScreenViewController: UITableViewDataSource, UITableViewDelegate
{
// below functions are to setup the table view
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return guidesDataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = guidesListTableView.dequeueReusableCell(withIdentifier: "guidesListCell") as! GuidesListCellSearchScreenTableViewCell
//adding properties to cell and then returning cell
return cell
}
}
GOAL: Either load the container view, after the data is received in the array, or refresh the table by again passing the array to container VC and reloading table.
Other solution: I had tried loading up all this array data inside First container VC class, and reloading table view data from there, which works perfectly fine, but to me which is a very inefficient approach, as i need this array in both container views, so making network calls for each container vc seems very inefficient. Therefore, i am trying to get the data once and pass in both container views. Kindly correct me if you feel me wrong.
P.s I have deleted other functionality and simplified the code.
And help would be highly appreciated.
The container view controllers will load when the main view controller is loaded. Therefore you will have to update the container view controllers when the main view controller receives the data.
A simple way to do this is to update the arrays in the container view controllers, and use a didSet on the arrays to force the container view controllers to reload themselves.
For example, if your FirstViewController displays the array data in a table view, you might do this:
var array: [ArrayItem] {
didSet {
tableView.reloadData()
}
}
Then in your main view controller, when the data is received, set this property:
getData() { resultArray in
firstViewController.array = resultArray
}
Please note, since you didn't provide any code, these are just examples and you will have to adjust them to fit your specific situation.
EDIT: Per comment below, you should be careful not to set the array in your FirstViewController before its view has loaded or the call to tableView.reloadData() will cause your app to crash.
I can only speculate without seeing the code, so here goes nothing... 😉
Try doing the following on viewDidLoad() of MainViewController:
Use the property related to FirstViewController (for the sake of my explanation let’s assume it’s named ‘firstVC’). What you want to do is go, firstVC.view.isHidden = true
Do the same to SecondViewController.
Why? Doing so will hide the container VC’s (ViewController) from the MainViewController’s view.
Now what you want to do is, at the place where you get data from Firebase (at the closure), add the following:
firstVC.view.isHidden = false
Do the same to SecondViewController.
This brings it back to view with the data you fetched already populating it.
Hopefully this helps you out some way.

Swift WatchKit Tables Found nil while unwrapping optional

I've seen a bunch of threads reporting this error, however, after trying all solutions, I still have the same problem. Running with the debugger I can see that the .setNumberOfRows method works, as it assigns the appropriate number of rows to my table.
import WatchKit
import Foundation
class CountInterfaceController: WKInterfaceController {
//MARK: Properties
#IBOutlet var dataTable: WKInterfaceTable!
var counters = [Counter]()
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Configure interface objects here.
if let savedCounters = loadCounters(){
counters+=savedCounters
}else{
loadSampleCounters()
}
dataTable.setNumberOfRows(counters.count, withRowType: "CountRowController")
for(index, counter) in counters.enumerated(){
let row = dataTable.rowController(at: index) as! CountRowController
row.countNameLabel.setText(counter.name)
row.countCounterLabel.setText("\(counter.count)")
}
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
private func loadSampleCounters(){
guard let counter1 = Counter(name:"Pots of coffee", count: 20) else {
fatalError("Unable to instantiate counter 1")
}
guard let counter2 = Counter(name:"Tea kettles", count: 5) else {
fatalError("Unable to instantiate counter 2")
}
counters += [counter1, counter2]
}
I have created a Table Row Class and added it in the Identity field of the Table Row Controller as you can see in the picture below. The class contains the outlets for the two labels in the table row.
Table Row Controller with class
I have tried populating my table with random data from a simple array of strings but that doesn't work either(same error). I honestly don't see what I'm missing here... The only thing that I can think of is that something's changed in Swift 4 as most of the answers and info that I've found were related to Swift 3.
error

Property value prints but when assigned to label it shows nil

Currently working with two view controllers and a swift file dealing with the details of a store such as the phone number. There is a main ViewController and a DetailsViewController.
I currently acquire data from google places api and am able to successfully store the values in a PlaceDetails Class. Testing out the data - I am able to print to the console. However, when I try to assign a value taken from the API to a UILabel the application crashes and shows that the value of the property is nil. I am not sure why this is the case. I feel in the two view controllers I am instantiating the PlaceDetails class correctly and accessing it's properties correctly but there is something I am not aware of that is going wrong.
class ViewController: UIViewController
{
let placeDetails = PlaceDetails()
let detailsVC = DetailsViewController()
func tapLabel( sender: UITapGestureRecognizer )
{
// print statement successfully prints out the stored value as - Optional("1 888-555-5555")
print(placeDetails.phoneNumber)
// assigning value to label causes a crash stating value is nil
detailsVC.phoneNumberLabel.text = placeDetails.phoneNumber!
self.performSegueWithIdentifier("showDetailsVC", sender: self)
}
}
class DetailsViewController: UIViewController
{
#IBOutlet weak var phoneNumberLabel : UILabel!
let placeDetails = PlaceDetails()
override func viewDidLoad()
{
super.viewDidLoad()
//This approach does not cause a crash but outputs nil to the console for both the print statement and the assignment statement
print(placeDetails.phoneNumber)
phoneNumberLabel.text = placeDetails.phoneNumber!
}
}
class PlaceDetails
{
override init()
{
super.init()
}
var phoneNumber : String? //viewcontroller actions give this class property its value
}
You need to assign placeDetails to your destination view controller in prepareForSegue. I know you aren't doing this as you have created placeDetails as a let constant rather than a variable so it can't ever change from the empty PlaceDetails you originally assign.
You should declare it as an optional variable and then unwrap it properly when you use it;
class ViewController: UIViewController
{
let placeDetails = PlaceDetails()
func tapLabel( sender: UITapGestureRecognizer )
{
self.performSegueWithIdentifier("showDetailsVC", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "showDetailsVC") {
let destVC = segue.destinationViewController as! DetailsViewController
destVC.placeDetails = self.placeDetails
}
}
}
class DetailsViewController: UIViewController
{
#IBOutlet weak var phoneNumberLabel : UILabel!
var placeDetails: PlaceDetails?
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
if let placeDetails = self.placeDetails {
if let phoneNumber = placeDetails.phoneNumber {
self.phoneNumberLabel.text = phoneNumber
}
}
}
}
You can't use the value in viewDidLoad as this method will execute before the property has been set; the view controller is loaded before prepareForSegue is called, so viewWillAppear is a better place.
Try to cast your phoneNumber in a string.
detailsVC.phoneNumberLabel.text = String(placeDetails.phoneNumber!)
Also, the nil value could come from the encoding method of the response of the API.
EDIT
I think you have to set the text of your UILabel in the viewDidLoad() method of your showDetailsVC. Declare a string variable in showDetailVC, and then pass your placeDetails.phoneNumber variable to the string you just declare. (Instead of directly set the text in the tapLabel() method). Then in your
showDetailVC set the text to your UILabel in the viewDidLoad()

Resources