Swift - Stop TableView Reloading After Dismissing A ViewController - ios

I have a tableview that populates after a fetch request completes. Each row contains a link that opens in a SFSafariViewController. Once the user taps the Done button in the SF Safari VC, the tableview reloads and calls the fetch request again. I don't want this to happen as the fetch request can sometimes take upwards of 15 sec and I'm afraid its doubling my API call count.
Code below - is there anything I can do to prevent the tableview reloading once the Safari VC is dismissed? I tried using a boolean to track it but it simply gets changed again when viewDidLoad /appear gets called. Should I call my fetch request outside of viewdidload/appear somehow?
import SafariServices
import UIKit
class ArticlesTVC: UITableViewController, SFSafariViewControllerDelegate {
var articles = [Article]()
let cellId = "articleCell"
var refresh: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
registerCell()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
refresh = true
if refresh == true {
DispatchQueue.main.async {
self.fetchArticles()
self.refresh = false
}
}
}
func registerCell() { tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId) }
override func numberOfSections(in tableView: UITableView) -> Int { return 1 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return articles.count }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
let article = articles[indexPath.row]
cell.textLabel?.text = article.title
cell.detailTextLabel?.text = article.description
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let article = articles[indexPath.row]
loadArticle(articleURL: article.url!)
}
func loadArticle(articleURL: String) {
if let url = URL(string: articleURL) {
let vc = SFSafariViewController(url: url)
vc.delegate = self
present(vc, animated: true)
}
}
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
dismiss(animated: true)
}
func fetchArticles() {
let baseURL = "url removed for privacy"
guard let url = URL(string: "\(baseURL)") else {
print("url failed")
return
}
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error != nil {
print(error)
return
}
if let safeData = data {
self.parseJSON(data: safeData)
}
}.resume()
}
func parseJSON(data: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(ArticleEnvelope.self, from: data)
let newsArticles = decodedData.news
for item in newsArticles {
let title = item.title
let description = item.description
let url = item.url
let image = item.image
let published = item.published
let article = Article(title: title, description: description, url: url, image: image, published: published)
DispatchQueue.main.async {
self.articles.append(article)
self.tableView.reloadData()
}
}
print("articles loaded successfully")
} catch {
print("Error decoding: \(error)")
}
}

Of course. You are 90% of the way there. Your viewWillAppear(_:) method checks a boolean flag refresh, and only fetches the data and reloads the table if refresh == true. However, it explicitly sets refresh = true. Your viewWillAppear(_:) function gets called every time you dismiss your other view controller and re-display the table view controller.
Move the line refresh = true into viewDidLoad(). Problem solved. (viewDidLoad() is only called once when the view controller is first created, not each time it is uncovered by dismissing/popping a view controller that is covering it.)
Edit:
Note that in this code:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
refresh = true
if refresh == true {
DispatchQueue.main.async { //This bit is not needed
self.fetchArticles()
self.refresh = false
}
}
}
The call to DispatchQueue.main.async is not needed, and will make fetching data slightly slower. viewWillAppear(_:) is already always called on the main thread.
Rewrite it like this:
override func viewDidLoad() {
super.viewDidLoad()
registerCell()
refresh = true //Moved from viewWillAppear
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
//Remove refresh = true from this function.
if refresh == true {
//No need to use DispatchQueue.main.async here
self.fetchArticles()
self.refresh = false
}
}

Related

why table view cells generate before animation

I have an api that I parse and I present the data on my tableview that generates cells . I have make an animation that normally must act before as a present animation for the cells but this did not happen . the result is that the cells appear and suddenly disappear and then appear with the animation .
in the link you can find the gif that I upload the shows what happen .
https://gifyu.com/image/SEGkZ
here is the code :
The OpeningViewController is this :
import UIKit
import ViewAnimator
class OpeningViewController: UIViewController {
//MARK: - IBProperties
#IBOutlet var openingImg: UIImageView!
#IBOutlet var startButton: UIButton!
//MARK: - Properties
var nft : Nft?
//MARK: - Life Cyrcle
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let animation = AnimationType.from(direction: .top, offset: 50)
openingImg.animate(animations: [animation] , delay: 0.3, duration: 2)
openingImg.layer.shadowColor = UIColor.black.cgColor
openingImg.layer.shadowOffset = CGSize(width: 0, height: 0)
openingImg.layer.shadowOpacity = 0.65
openingImg.layer.shadowRadius = 10
}
//MARK: - Methods
#IBAction func startApp(_ sender: Any) {
HapticsManager.shared.selectionVibrate()
let storyBoard = UIStoryboard(name: "Lobby", bundle: nil)
let controller = storyBoard.instantiateViewController(withIdentifier: "LobbyViewController") as! LobbyViewController
controller.modalTransitionStyle = .flipHorizontal
self.navigationController?.pushViewController(controller, animated: true)
}
}
The presentation happens in LobbyViewController :
import UIKit
import ViewAnimator
class LobbyViewController: UIViewController {
// MARK: - IBProperties
#IBOutlet weak var tableView: UITableView!
// MARK: - Properties
var data: [DataEnum] = []
var likes:[Int] = []
var numlikes: Int = 0
var nfts: [Nft] = []
let creators : [Creator] = []
var icons: [Icon] = []
var loadData = APICaller()
// MARK: - Life Cyrcle
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "AssetTableViewCell", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "AssetTableViewCell")
let nib2 = UINib(nibName: "CreatorsTableViewCell", bundle: nil)
tableView.register(nib2, forCellReuseIdentifier: "CreatorsTableViewCell")
tableView.dataSource = self //method to generate cells,header and footer before they are displaying
tableView.delegate = self //method to provide information about these cells, header and footer ....
downloadJSON {
self.tableView.reloadData()
print("success")
}
loadData.downloadData { (result) in
print(result)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let animation = AnimationType.from(direction: .top, offset: 300)
UIView.animate(views: tableView.visibleCells,
animations: [animation], delay: 1, duration: 2)
}
//stelnei ta dedomena apo to kathe row ston PresentViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? PresentViewController {
if tableView.cellForRow(at: tableView.indexPathForSelectedRow!) is AssetTableViewCell {
destination.nft = nfts[tableView.indexPathForSelectedRow!.row-1]
destination.delegate = self
} else {
//add alert action
let alert = UIAlertController(title: "Invalid Touch", message: "You press wrong row. Choose one of the following list.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: {
return
})
}
}
}
// MARK: - Methods
func downloadJSON(completed: #escaping () -> ()) {
let url = URL(string: "https://public.arx.net/~chris2/nfts.json")
URLSession.shared.dataTask(with: url!) { [self] data, response, error in
if error == nil {
do {
self.nfts = try JSONDecoder().decode([Nft].self, from: data!)
let creators = nfts.map { nft in
nft.creator
}
self.data.append(.type1(creators: creators))
self.nfts.forEach { nft in
self.data.append(.type2(nft: nft))
}
DispatchQueue.main.async {
completed()
}
}
catch {
print("error fetching data from api")
}
}
}.resume()
}
}
// MARK: - Extensions
extension LobbyViewController : UITableViewDelegate , UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
indexPath.row == 0 ? 100 : UITableView.automaticDimension
}
//gemizo ta rows tou table
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch self.data[indexPath.item] {
case .type1(let creators):
print("--->", creators)
let cell = tableView.dequeueReusableCell(withIdentifier: "CreatorsTableViewCell",
for: indexPath) as! CreatorsTableViewCell
cell.layer.cornerRadius = 15
cell.layer.shadowColor = UIColor.black.cgColor
cell.layer.shadowOffset = CGSize(width: 0, height: 0)
cell.layer.shadowOpacity = 0.8
cell.layer.shadowRadius = 15
cell.layer.cornerRadius = cell.frame.height/2
cell.updateCreators(creators)
return cell
case .type2(let nft):
let cell = tableView.dequeueReusableCell(withIdentifier: "AssetTableViewCell",
for: indexPath) as! AssetTableViewCell
cell.nameLabel?.text = nft.name
cell.nameLabel.layer.cornerRadius = cell.nameLabel.frame.height/2
cell.likesLabel?.text = "\((numlikes))"
let imgUrl = (nft.image_url)
print(imgUrl)
cell.iconView.downloaded(from: imgUrl)
cell.iconView.layer.cornerRadius = cell.iconView.frame.height/2
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "showDetails", sender: self)
}
}
extension LobbyViewController : TestDelegate{
func sendBackTheLikess(int: Int) {
numlikes = int
tableView.reloadData()
}
}
// MARK: - Enums
enum DataEnum {
case type1(creators: [Creator])
case type2(nft: Nft)
}
// MARK: - Struct
struct Constants {
static let url = "https://public.arx.net/~chris2/nfts.json"
}
The APICaller :
import Foundation
final class APICaller {
static let shared = APICaller()
public struct Constants {
static let url = "https://public.arx.net/~chris2/nfts.json"
}
public func downloadData(completion:#escaping (Result<[Nft], Error>) -> Void )
{
guard let url = URL(string:Constants.url)else{
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
//print(response)
print("here")
guard let data = data , error == nil else{
print("something went wrong with data")
return
}
print("here4")
//mexri edo exoume parei ta data kai tora me to do-catch tha ta kanoume convert se object
do{
//Decode the response
let nfts = try JSONDecoder().decode([Nft].self, from: data)
completion(.success(nfts))
print(nfts)
}catch{
completion(.failure(error))
}
}
task.resume()
}
}
Just move your animation from viewDidAppear(animated:) to tableView(_:willDisplay:forRowAt:) and call for each cell separately. Also don’t forget not to call this animation once it finished.

TableView being assigned to navigation controller not view controller

I have an app that is playing up. All my data is being displayed in the black circle.
I need it to be displayed in the red circle. Is it a problem in my controller class or do I need to somehow assign the tableview to this controller? I couldn't find any solutions like this online
image1
This is my code for the ViewController class:
import UIKit
import SafariServices
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private
let tableView: UITableView = {
let table = UITableView()
table.register(NewsTableViewCell.self,
forCellReuseIdentifier: NewsTableViewCell.identifier)
return table
}()
private
var viewModels = [NewsTableViewCellViewModel]()
private
var articles = [Article]()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.hidesBackButton = true
title = "News"
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
// Do any additional setup after loading the view
getTopArticles()
searchArticles()
}
func getTopArticles() {
ArticlesAPI.shared.getTopArticles {
[weak self] result in //looking at API
switch result {
case.success(let articles): // if there are results
self?.articles = articles
self?.viewModels = articles.compactMap({
NewsTableViewCellViewModel(title: $0.title, //tableview needs to be added
subtitle: $0.description ?? "No Description",
imageURL: URL(string: $0.urlToImage ?? ""))
})
// break
DispatchQueue.main.async {
self?.tableView.reloadData()
}
case.failure(let error): //prints error if there is one
print(error)
}
}
}
func searchArticles() {
ArticlesAPI.shared.searchArticles(with: "apple") {
[weak self] result in //looking at API
switch result {
case.success(let articles): // if there are results
self?.viewModels = articles.compactMap({
NewsTableViewCellViewModel(title: $0.title, //tableview needs to be added
subtitle: $0.description ?? "No Description",
imageURL: URL(string: $0.urlToImage ?? ""))
})
//break
DispatchQueue.main.async {
self?.tableView.reloadData()
}
case.failure(let error): //prints error if there is one
print(error)
}
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
}
// table
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard
let cell = tableView.dequeueReusableCell(withIdentifier: NewsTableViewCell.identifier,
for: indexPath)
as ? NewsTableViewCell
else {
fatalError()
}
cell.configure(with: viewModels[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let viewModel = viewModels[indexPath.row]
let article = articles[indexPath.row]
guard
let url = URL(string: article.url ?? "")
else {
return
}
let vc = SFSafariViewController(url: url)
present(vc, animated: true)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 150
}
}
The red controller is embedded inside the black controller. The black controller is a navigation controller which provides the navigation bar for the red controller. You can type navigationController inside of your view controller and you will see that it is of type UINavigationController?, and Optional (?) since it may or may not exist (in your case it does exist because you embedded your controller in the storyboard). So don’t worry, your content is already inside the red view controller.
The reason why you think it isn’t, is because you have created your tableView programatically and you gave it the size of the whole screen, so it covers up all of the other content:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
}
You can verify this using the layout inspector.
This isn’t needed because you already have a UITableView in your storyboard, you just need to create a reference to it in your view controller. So instead of:
private
let tableView: UITableView = {
let table = UITableView()
table.register(NewsTableViewCell.self, forCellReuseIdentifier: NewsTableViewCell.identifier)
return table
}()
you need:
#IBOutlet private var tableView: UITableView!
and you can register your cell alongside the other tableView setup code inside the viewDidLoad method:
// view.addSubview(tableView) -> autoLayout does this automatically, hence the name autoLayout
tableView.register(NewsTableViewCell.self, forCellReuseIdentifier: NewsTableViewCell.identifier)
tableView.delegate = self
tableView.dataSource = self
And remove the code in your viewDidLayoutSubviews method as well, so that the tableView doesn’t take up all of the space.

Reusable UITableViewController

I want to avoid duplicate rewriting same code and create reusable UITableViewController.
I have ExerciseViewController with 3 buttons. Each button push a UITableViewController on the navigation stack. There are three UITableViewControllers: 1) CategoryUITableVC, 2) EquipmentUITableVC, 3) MusclesUITableVC.
All of these three view controllers have almost exactly same layout - cells with labels and accessory buttons. The only difference is that first view controller has got image next to title. Is it worth doing one reusable VC and instantiate it 3 times or maybe better solution create 3 separated VC (but It will be just rewriting almost same code).
I use coordinator pattern.
class ExerciseCoordinator: NSObject, Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
...
//unnecessary code to show
...
// *HERE I INSTANTIATE VIEW CONTROLLERS, I PRESENT THEM MODALLY BUT I WANT TO HAVE NAVIGATION BAR, SO I NEED TO CREATE NEW NAVIGATION CONTROLLERS*
lazy var equipmentVC: ReusableTableViewController = {
let vc = AppStoryboard.Main.instantiate(ReusableTableViewController.self)
vc.delegate = self
return vc
}()
lazy var equipmentNavController: UINavigationController = {
let navController = UINavigationController(rootViewController: equipmentVC)
navController.navigationItem.largeTitleDisplayMode = .always
return navController
}()
lazy var categoryVC: ReusableTableViewController = {
let vc = AppStoryboard.Main.instantiate(ReusableTableViewController.self)
vc.delegate = self
return vc
}()
lazy var categoryNavController: UINavigationController = {
let navController = UINavigationController(rootViewController: categoryVC)
navController.navigationItem.largeTitleDisplayMode = .always
return navController
}()
lazy var muscleVC: ReusableTableViewController = {
let vc = AppStoryboard.Main.instantiate(ReusableTableViewController.self)
vc.delegate = self
return vc
}()
lazy var muscleNavController: UINavigationController = {
let navController = UINavigationController(rootViewController: muscleVC)
navController.navigationItem.largeTitleDisplayMode = .always
return navController
}()
}
extension ExerciseCoordinator: CustomExerciseDelegate {
func selectCategory() {
navigationController.viewControllers.last?.present(categoryNavController, animated: true, completion: nil)
categoryVC.dataType = .category
}
func selectEquipment() {
navigationController.viewControllers.last?.present(equipmentNavController, animated: true, completion: nil)
equipmentVC.dataType = .equipment
}
func selectMuscles() {
navigationController.viewControllers.last?.present(muscleNavController, animated: true, completion: nil)
muscleVC.dataType = .muscle
}
}
I assign data type to know from where it comes from (CategoryVC/EquipmentVC/MuscleVC) when I will dismiss UITableVC.
Here it is my reusable UITableViewController:
import UIKit
import RealmSwift
class ExerciseCategoryTableViewController: UITableViewController {
var delegate: ExerciseSelectionCriteriaDelegate?
//I use it delegate to send data back after dismiss view
var dataType: DataType?
var data = [String]() {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData() }}
}
override func viewDidLoad() {
super.viewDidLoad()
getData()
}
func getData() {
if dataType == .category {
let allCategories = RealmService.shared.realm.objects(Category.self)
data = allCategories.compactMap({$0.category})
} else if dataType == .equipment {
let allEquipment = RealmService.shared.realm.objects(Equipment.self)
data = allEquipment.compactMap({$0.equipment})
} else {
let allMuscles = RealmService.shared.realm.objects(Muscles.self)
data = allMuscles.compactMap({$0.muscles})
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// below is my own shortcut for dequeue cell, it works
let cell: ExerciseSelectionTableViewCell = tableView.dequeueResuableCell(for: indexPath)
cell.category.text = data[indexPath.row]
if let image = UIImage(named: "\(data[indexPath.row].lowercased())") {
cell.categoryImage.image = image
cell.categoryImage.isHidden = false
} else {
cell.categoryImage.isHidden = true
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = .none
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 70
}
#IBAction func closeViewController(_ sender: UIBarButtonItem) {
closeViewController()
}
#IBAction func saveSelectedCategories(_ sender: UIBarButtonItem) {
saveSelectedData()
}
func saveSelectedData() {
let selectedIndexes = tableView.indexPathsForSelectedRows
if let selectedData = selectedIndexes?.compactMap({data[$0.row]}) {
dismiss(animated: true) {
self.delegate?.selectedFields(for: selectedData, dataType: self.dataType)
}
} else {
dismiss(animated: true) {
self.delegate?.selectedFields(for: nil, dataType: self.dataType)
}
}
}
func closeViewController() {
guard let _ = tableView.indexPathsForSelectedRows else { return dismiss(animated: true, completion: nil)}
Alert.showAlertWithCompletion(on: self, with: "TEXT", message: "TEXT") { _ in
self.dismiss(animated: true, completion: nil)
}
}
}
I will be thankful if you tell me if this approach is correct or maybe better solution are separated view controllers or create protocol with default implementation?

Proper Placement of dispatchGroup to reloadData

I have a tableview function that is pulling data from a database to render cells. I want to accomplish the goal of not reloading my tableview so much. I learned that dispatch groups would be the way to go beause I don't want to return to the completion block that reloads the tableView until all the data has been pulled however when I use the dispatchGroup it never reaches the completion it just stops. The placement of my variables may be in the wrong place but i just can't really see where I should put it. I have been moving it to different places and still nothing.
import UIKit
import Firebase
class FriendsEventsView: UITableViewController{
var cellID = "cellID"
var friends = [Friend]()
var attendingEvents = [Event]()
//label that will be displayed if there are no events
var currentUserName: String?
var currentUserPic: String?
var currentEventKey: String?
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Friends Events"
view.backgroundColor = .white
// Auto resizing the height of the cell
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "close_black").withRenderingMode(.alwaysOriginal), style: .done, target: self, action: #selector(self.goBack))
tableView.register(EventDetailsCell.self, forCellReuseIdentifier: cellID)
self.tableView.tableFooterView = UIView(frame: CGRect.zero)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.global(qos: .background).async {
print("This is run on the background queue")
self.fetchEventsFromServer { (error) in
if error != nil {
print(error)
return
} else {
DispatchQueue.main.async {
self.tableView.reloadData()
print("This is run on the main queue, after the previous code in outer block")
}
}
}
}
}
#objc func goBack(){
dismiss(animated: true)
}
override func numberOfSections(in tableView: UITableView) -> Int {
// print(friends.count)
return friends.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// print(friends[section].events.count)
return friends[section].collapsed ? 0 : friends[section].events.count
}
func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! EventDetailsCell? ?? EventDetailsCell(style: .default, reuseIdentifier: cellID)
// print(indexPath.row)
cell.details = friends[indexPath.section].events[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "header") as? CollapsibleTableViewHeader ?? CollapsibleTableViewHeader(reuseIdentifier: "header")
// print(section)
header.arrowLabel.text = ">"
header.setCollapsed(friends[section].collapsed)
print(friends[section].collapsed)
header.section = section
// header.delegate = self
header.friendDetails = friends[section]
return header
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50
}
func fetchEventsFromServer(_ completion: #escaping (_ error: Error?) -> Void ){
//will grab the uid of the current user
guard let myUserId = Auth.auth().currentUser?.uid else {
return
}
let ref = Database.database().reference()
//checking database for users that the current user is following
ref.child("following").child(myUserId).observeSingleEvent(of: .value, with: { (followingSnapshot) in
//handling potentail nil or error cases
guard let following = followingSnapshot.children.allObjects as? [DataSnapshot]
else {return}
//validating if proper data was pulled
let group = DispatchGroup()
for followingId in following {
group.enter()
UserService.show(forUID: followingId.key, completion: { (user) in
PostService.showFollowingEvent(for: followingId.key, completion: { (event) in
self.attendingEvents = event
var friend = Friend(friendName: (user?.username)!, events: self.attendingEvents, imageUrl: (user?.profilePic)!)
self.friends.append(friend)
})
})
}
this loop should return to the completon block in viewWillAppear following the execution of this if statement
if self.friends.count == following.count{
group.leave()
let result = group.wait(timeout: .now() + 0.01)
//will return this when done
completion(nil)
}
}) { (err) in
completion(err)
print("Couldn't grab people that you are currently following: \(err)")
}
}
Any help is greatly appreciated
You want to place the group.leave() inside of the PostService.showFollowingEvent callback.
Now you call enter following.count-times, but you call leave only once. For the group to continue you have to leave the group as many times as you entered it:
for followingId in following {
group.enter()
UserService.show(forUID: followingId.key, completion: { (user) in
PostService.showFollowingEvent(for: followingId.key, completion: { (event) in
self.attendingEvents = event
var friend = Friend(friendName: (user?.username)!, events: self.attendingEvents, imageUrl: (user?.profilePic)!)
self.friends.append(friend)
// leave here
group.leave()
})
})
}
Moreover, I would not recommend using group.wait since you are facing a possible deadlock. If any of the callbacks that are supposed to call group.leave are happening on the same thread as group.wait was called, they will never get called and you will end up with the frozen thread. Instead, use group.notify:
group.notify(queue: DispatchQueue.main) {
if self.friends.count == following.count {
completion(nil)
}
}
This will allow the execution on the main thread, but once all the tasks are finished, it will execute the provided callback closure.

Getting black screen on using tab bar while searching using SearchController

I have an app with a tab bar controller embedded in a navigation controller. The app has 2 tabs, the first one(search) has a search bar implemented using the UISearchController. If I switch from this tab to the other tab(downloads) while I'm searching, to the other tab two things happen -
The navigation bar in the second tab(downloads) disappears
When i come back to the first tab(search), it shows a black screen
I have done all this using the storyboard.
this is my SearchViewController
import UIKit
import Alamofire
import SwiftyJSON
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating, UISearchBarDelegate{
//MARK: Variables
var papers = [Paper]()
var filteredPapers = [Paper]()
let searchController = UISearchController(searchResultsController: nil)
// MARK: Outlets
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
#IBOutlet var table: UITableView!
#IBOutlet weak var loadingMessageLabel: UILabel!
#IBOutlet weak var retryButton: UIButton!
//MARK: Actions
#IBAction func retryButton(sender: UIButton) {
self.loadingMessageLabel.hidden = false
self.loadingMessageLabel.text = "While the satellite moves into position..."
self.activityIndicator.hidden = false
self.activityIndicator.startAnimating()
self.retryButton.hidden = true
self.getPapersData()
}
// MARK: Table View
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// If in searching mode, then return the number of results else return the total number
// if searchController.active && searchController.searchBar.text != "" {
if searchController.active {
return filteredPapers.count
}
return papers.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let paper: Paper
// if searchController.active && searchController.searchBar.text != "" {
if searchController.active {
paper = filteredPapers[indexPath.row]
} else {
paper = papers[indexPath.row]
}
if let cell = self.table.dequeueReusableCellWithIdentifier("Cell") as? PapersTableCell {
cell.initCell(paper.name, detail: paper.detail)
print(cell)
return cell
}
return PapersTableCell()
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
}
func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let downloadButton = UITableViewRowAction(style: .Normal, title: "Download") { action, index in
var url = String(self.papers[indexPath.row].url)
url = url.stringByReplacingOccurrencesOfString(" ", withString: "%20")
print(url)
let destination = Alamofire.Request.suggestedDownloadDestination(directory: .DocumentDirectory, domain: .UserDomainMask)
// Spinner in cell
// var selectCell = self.table.cellForRowAtIndexPath(indexPath) as? PapersTableCell
// selectCell!.downloadSpinner.hidden = false
// Dismiss the download button
self.table.editing = false
Alamofire.download(.GET, url, destination: destination).response { _, _, _, error in
if let error = error {
print("Failed with error: \(error)")
} else {
print("Downloaded file successfully")
}
// selectCell?.downloadSpinner.hidden = true
}
}
downloadButton.backgroundColor = UIColor(red:0.30, green:0.85, blue:0.39, alpha:1.0)
return [downloadButton]
}
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// the cells you would like the actions to appear needs to be editable
return true
}
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
// you need to implement this method too or you can't swipe to display the actions
}
// MARK: Search
func filterContentForSearchText(searchText: String, scope: String = "All") {
filteredPapers = papers.filter { paper in
let categoryMatch = (scope == "All") || (paper.exam == scope)
return categoryMatch && paper.name.lowercaseString.containsString(searchText.lowercaseString)
}
table.reloadData()
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
let searchBar = searchController.searchBar
let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
filterContentForSearchText(searchController.searchBar.text!, scope: scope)
}
func searchBar(searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])
}
// MARK: Defaults
override func viewDidLoad() {
super.viewDidLoad()
self.getPapersData()
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
definesPresentationContext = true
table.tableHeaderView = searchController.searchBar
searchController.searchBar.scopeButtonTitles = ["All", "ST1", "ST2", "PUT", "UT"]
searchController.searchBar.delegate = self
activityIndicator.startAnimating()
}
override func viewWillDisappear(animated: Bool) {
// if searchController.active {
self.searchController.resignFirstResponder()
// }
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: API call
func getPapersData(){
Alamofire.request(.GET, "http://silive.in/bytepad/rest/api/paper/getallpapers?query=")
.responseJSON { response in
self.activityIndicator.stopAnimating()
self.activityIndicator.hidden = true
// If the network works fine
if response.result.isFailure != true {
self.loadingMessageLabel.hidden = true
self.table.hidden = false
//print(response.result) // result of response serialization
let json = JSON(response.result.value!)
for item in json {
// Split the title on the . to remove the extention
let title = item.1["Title"].string!.characters.split(".").map(String.init)[0]
let category = item.1["ExamCategory"].string
let url = item.1["URL"].string
let detail = item.1["PaperCategory"].string
let paper = Paper(name: title, exam: category!, url: url!, detail: detail!)
self.papers.append(paper)
}
self.table.reloadData()
}
// If the network fails
else {
self.retryButton.hidden = false
self.loadingMessageLabel.text = "Check your internet connectivity"
}
}
}
}
And this is my DownloadViewController
import UIKit
import QuickLook
class DownloadViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, QLPreviewControllerDataSource {
var items = [(name:String, url:String)]()
#IBOutlet weak var downloadsTable: UITableView!
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print(items[indexPath.row].url)
// performSegueWithIdentifier("DocumentViewSegue", sender: items[indexPath.row].url)
let previewQL = QLPreviewController() // 4
previewQL.dataSource = self // 5
previewQL.currentPreviewItemIndex = indexPath.row // 6
showViewController(previewQL, sender: nil) // 7
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell = self.downloadsTable.dequeueReusableCellWithIdentifier("Download Cell") as? DownloadsTableCell {
cell.initCell(items[indexPath.row].name, detail: "", fileURL: items[indexPath.row].url)
return cell
}
return DownloadsTableCell()
}
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
// let fileManager = NSFileManager.defaultManager()
//
// // Delete 'hello.swift' file
//
// do {
// try fileManager.removeItemAtPath(String(items[indexPath.row].url))
// }
// catch let error as NSError {
// print("Ooops! Something went wrong: \(error)")
// }
items.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
override func viewDidAppear(animated: Bool) {
items.removeAll()
let documentsUrl = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
// now lets get the directory contents (including folders)
do {
let directoryContents = try NSFileManager.defaultManager().contentsOfDirectoryAtURL(documentsUrl, includingPropertiesForKeys: nil, options: NSDirectoryEnumerationOptions())
// print(directoryContents)
for var file in directoryContents {
print(file.lastPathComponent)
print(file.absoluteURL)
// Save the data in the list as a tuple
self.items.append((file.lastPathComponent!, file.absoluteString))
}
} catch let error as NSError {
print(error.localizedDescription)
}
downloadsTable.reloadData()
}
// MARK: Preview
func numberOfPreviewItemsInPreviewController(controller: QLPreviewController) -> Int {
return items.count
}
func previewController(controller: QLPreviewController, previewItemAtIndex index: Int) -> QLPreviewItem {
return NSURL(string: items[index].url)!
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewWillAppear(animated: Bool) {
//do something
super.viewWillAppear(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Looks like the view that your UISearchController is attached to gets removed from the view hierarchy. You can think of the UISearchController as being presented modally when you start searching, and the definesPresentationContext property indicates which UIViewController would be the one to present it (more on this).
One of the ways to fix this would be reconfiguring your storyboard so that each tab has its own UINavigationController (in case you need it for both):
Instead of (what I suspect you have now):
And if you want to dismiss UISearchController when the tab switches, add this override to the ViewController:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
searchController.active = false
}

Resources