Skip to content

Instantly share code, notes, and snippets.

@hectorddmx
Last active February 19, 2020 04:55
Show Gist options
  • Save hectorddmx/8fb7b3d484ac63a42a590d8b2d3108c0 to your computer and use it in GitHub Desktop.
Save hectorddmx/8fb7b3d484ac63a42a590d8b2d3108c0 to your computer and use it in GitHub Desktop.
TableView example with canceled data task
//
// TVShowsViewController.swift
// TVShows
//
// Created by Hector de Diego on 2/22/19.
// Copyright © 2019 hector.dd. All rights reserved.
//
import UIKit
class TVShowsViewController: BaseViewController {
let networkingService = NetworkingService()
// MARK: - Outlets
@IBOutlet weak var showsTableView: UITableView!
// MARK: - Properties
enum ScreenState {
case empty
case loading
case error(Error)
case populated([TVShow])
}
var currentTVShows: [TVShow] = []
var tvShow: TVShow?
public var screenState: ScreenState = ScreenState.empty {
didSet {
switch screenState {
case .populated(let tvShows):
currentTVShows = tvShows
case .error(let error):
handleErrorMessage(vc: self, error: error) {
[weak self] _ in
guard let self = self else { return }
self.loadShows()
}
fallthrough
default:
currentTVShows = []
}
showsTableView.reloadData()
}
}
func loadShows() {
networkingService.fetchTVShows() { [weak self] response in
guard let self = self else { return }
self.update(response: response)
}
}
func update(response: TVShowsModel) {
if let error = response.error {
screenState = .error(error)
return
}
guard let tvShows = response.tvShows, !tvShows.isEmpty else {
screenState = .empty
return
}
screenState = .populated(tvShows)
}
// MARK: - Configuration
override func prepareNavigationBar() {
super.prepareNavigationBar()
navigationItem.title = "TV Shows"
}
private func prepareShowsTableView() {
showsTableView.delegate = self
showsTableView.dataSource = self
let nib = UINib(nibName: TVShowTableViewCell.nibName, bundle: .main)
showsTableView.register(nib, forCellReuseIdentifier: TVShowTableViewCell.reuseIdentifier)
showsTableView.register(
nib,
forCellReuseIdentifier: TVShowTableViewCell.reuseIdentifier
)
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
prepareNavigationBar()
prepareShowsTableView()
loadShows()
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if
segue.identifier == Segues.tvShowDetailSegue.rawValue,
let destination = segue.destination as? TVShowDetailViewController {
destination.tvShow = tvShow
}
}
}
// MARK: - Delegates
extension TVShowsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tvShow = currentTVShows[indexPath.row]
performSegue(withIdentifier: Segues.tvShowDetailSegue.rawValue, sender: self)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
}
fileprivate func buildUnfavoriteAction() -> [UITableViewRowAction]? {
let unfavoriteAction = UITableViewRowAction(
style: .normal, title: "Delete") { [weak self] (rowAction, indexPath) in
guard let self = self else { return }
self.showActionAlert(vc: self) { [weak self] _ in
guard let self = self else { return }
self.currentTVShows[indexPath.row].setFavoriteStatus(favorite: false)
}
}
unfavoriteAction.backgroundColor = UIColor(rgb: Colors.unfavorite.rawValue)
return [unfavoriteAction]
}
fileprivate func buildFavoriteAction() -> [UITableViewRowAction]? {
let favoriteAction = UITableViewRowAction(
style: .normal, title: "Favorite") { [weak self] (rowAction, indexPath) in
guard let self = self else { return }
self.currentTVShows[indexPath.row].setFavoriteStatus(favorite: true)
}
favoriteAction.backgroundColor = UIColor(rgb: Colors.favorite.rawValue)
return [favoriteAction]
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
if currentTVShows[indexPath.row].isFavorite {
return buildUnfavoriteAction()
} else {
return buildFavoriteAction()
}
}
}
extension TVShowsViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return currentTVShows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let showCell: TVShowTableViewCell = tableView.dequeueReusableCell(
withIdentifier: TVShowTableViewCell.reuseIdentifier,
for: indexPath) as? TVShowTableViewCell
else { return UITableViewCell() }
showCell.load(tvShow: currentTVShows[indexPath.row])
return showCell
}
}
//
// TVShowTableViewCell.swift
// TVShows
//
// Created by Hector de Diego on 2/22/19.
// Copyright © 2019 hector.dd. All rights reserved.
//
import UIKit
class TVShowTableViewCell: UITableViewCell {
var task: URLSessionTask? // 1. The task that needs to be canceled when reusing the cell
static let reuseIdentifier = String(describing: TVShowTableViewCell.self)
static let nibName = String(describing: TVShowTableViewCell.self)
@IBOutlet weak var showThumbImageView: UIImageView!
@IBOutlet weak var showTitleLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
selectionStyle = .none
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
override func prepareForReuse() {
showTitleLabel.text = ""
showThumbImageView.image = UIImage()
}
func load(tvShow: TVShow) {
self.task?.cancel() // 2. The task should be canceled before anything else is configured in my cell
// Canceling the task will avoid the image from continue downloading and being loaded some seconds later....
if let name = tvShow.name {
showTitleLabel.text = name
}
if let image = tvShow.image,
let mediumImage = image.medium {
loadImage(fromURL: mediumImage) // 3. The image will be downloaded if we have a URL string...
}
}
func load(tvShowStore: TVShowStore) {
self.task?.cancel()
showTitleLabel.text = ""
showThumbImageView.image = UIImage()
if !tvShowStore.name.isEmpty {
showTitleLabel.text = tvShowStore.name
}
if !tvShowStore.imageMedium.isEmpty {
loadImage(fromURL: tvShowStore.imageMedium)
}
}
// The function that loads my image in my imageView
// If there is a cached version, it will load that
// If not, it will download that and cache it.
public func loadImage(fromURL url: String) {
guard let imageURL: URL = URL(string: url) else {
return
}
let cache: URLCache = URLCache.shared
let request = URLRequest(url: imageURL)
DispatchQueue.global(qos: .userInitiated).async {
if
let data: Data = cache.cachedResponse(for: request)?.data,
let image: UIImage = UIImage(data: data) {
print("Getting cached image: \(url)")
DispatchQueue.main.async {
self.showThumbImageView.transition(toImage: image)
}
} else {
print("Requesting image: \(url)")
// 4. We need to set the task to be the current download, we do this by assigning the dataTask to this variable
self.task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
if let data = data,
let response: URLResponse = response,
((response as? HTTPURLResponse)?.statusCode ?? 500) < 300,
let image: UIImage = UIImage(data: data) {
let cachedData: CachedURLResponse = CachedURLResponse( response: response, data: data)
cache.storeCachedResponse(cachedData, for: request)
DispatchQueue.main.async {
self.showThumbImageView.transition(toImage: image)
}
}
}) // 5. Instead of calling here resume() directly, we have to do that now in the variable
self.task?.resume() // 6. We call resume() so the download is sent.
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment