Search code examples
iosswiftuitableviewuitabbarcontroller

Swift - Stop TableView Reloading After Dismissing A ViewController


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)")
    }
}

Solution

  • 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
        }
    }