Skip to main content

Visualizing Articles & Advanced Codables

A snippet for visualizing articles by parsing JSON with advanced Codable techniques and displaying them using custom UITableViewCells.

Details

URL: 🔗 Full Implementation

Source: 🔗 Ace the iOS Interview

Author: Aryaman Sharda

Tags:
Articles, iOS, Swift, UITableView, Codables

Platforms Supported: iOS

Swift Version: 5.0

Key Points​

  • Use Codable to parse JSON with a mix of objects.
  • Create custom UITableViewCells for different article sections.
  • Implement a scalable solution to support additional section types.

Use Cases​

  • Displaying articles fetched from a remote server in an iOS application.
  • Dynamically rendering various types of content (headings, paragraphs, images, quotes).
  • Handling complex JSON structures with Swift's Codable.

Alternative Approaches​

  • Using third-party libraries like SwiftyJSON for JSON parsing.
  • Hardcoding the layout for specific articles (less flexible).
  • Creating a custom view instead of using UITableView.

Performance Considerations​

  • Ensure smooth scrolling by optimizing cell reuse.
  • Load images asynchronously to avoid blocking the main thread.
  • Minimize JSON parsing overhead by caching results.

Code​

Models​

Heading.swift
struct Heading: Decodable {
let text: String
let size: Int
}
Paragraph.swift
struct Paragraph: Decodable {
let text: String
}
Image.swift
struct Image: Decodable {
let url: String
let caption: String
let source: String
}
Quote.swift
struct Quote: Decodable {
let text: String
let author: String
}

Result​

Result.swift
struct Result: Decodable {
let response: [Response]

enum Response: Decodable {
case heading(Heading)
case paragraph(Paragraph)
case image(Image)
case quote(Quote)

enum DecodingError: Error {
case wrongJSON
}

enum CodingKeys: String, CodingKey {
case heading
case paragraph
case image
case quote
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch container.allKeys.first {
case .heading:
let value = try container.decode(Heading.self, forKey: .heading)
self = .heading(value)
case .paragraph:
let value = try container.decode(Paragraph.self, forKey: .paragraph)
self = .paragraph(value)
case .image:
let value = try container.decode(Image.self, forKey: .image)
self = .image(value)
case .quote:
let value = try container.decode(Quote.self, forKey: .quote)
self = .quote(value)
case .none:
throw DecodingError.wrongJSON
}
}
}
}

View Controller​

ViewController.swift
final class ViewController: UIViewController {
@IBOutlet fileprivate(set) var tableView: UITableView!

var dataSource = [Result.Response]() {
didSet {
tableView.reloadData()
}
}

override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
loadArticle()
}

private func loadArticle() {
if let url = Bundle.main.url(forResource: "Article", withExtension: "json"),
let data = try? Data(contentsOf: url),
let result = try? JSONDecoder().decode(Result.self, from: data) {
dataSource = result.response
}
}
}

Cells​

HeadingCell.swift
final class HeadingCell: UITableViewCell {
@IBOutlet private(set) var headingLabel: UILabel!

func configure(model: Heading) {
headingLabel.text = model.text
headingLabel.font = UIFont.boldSystemFont(ofSize: CGFloat(model.size))
}
}
ImageCell.swift
final class ImageCell: UITableViewCell {
@IBOutlet private(set) var thumbnailImageView: UIImageView!
@IBOutlet private(set) var captionLabel: UILabel!
@IBOutlet private(set) var sourceLabel: UILabel!

func configure(model: Image) {
thumbnailImageView.loadImageFromURL(urlString: model.url, placeholder: nil)
captionLabel.text = model.caption
sourceLabel.text = model.source
}
}
QuoteCell.swift
final class QuoteCell: UITableViewCell {
@IBOutlet private(set) var quoteTextLabel: UILabel!
@IBOutlet private(set) var quoteAuthorLabel: UILabel!

func configure(model: Quote) {
quoteTextLabel.text = model.text
quoteAuthorLabel.text = model.author
}
}
ParagraphCell.swift
final class ParagraphCell: UITableViewCell {
@IBOutlet private(set) var paragraphLabel: UILabel!

func configure(model: Paragraph) {
paragraphLabel.text = model.text
}
}

DataSource​

UITableViewDataSource.swift
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch dataSource[indexPath.row] {
case .heading(let heading):
guard let cell = tableView.dequeueReusableCell(withIdentifier: HeadingCell.reuseIdentifier, for: indexPath) as? HeadingCell else {
return UITableViewCell()
}
cell.configure(model: heading)
return cell
case .paragraph(let paragraph):
guard let cell = tableView.dequeueReusableCell(withIdentifier: ParagraphCell.reuseIdentifier, for: indexPath) as? ParagraphCell else {
return UITableViewCell()
}
cell.configure(model: paragraph)
return cell
case .image(let image):
guard let cell = tableView.dequeueReusableCell(withIdentifier: ImageCell.reuseIdentifier, for: indexPath) as? ImageCell else {
return UITableViewCell()
}
cell.configure(model: image)
return cell
case .quote(let quote):
guard let cell = tableView.dequeueReusableCell(withIdentifier: QuoteCell.reuseIdentifier, for: indexPath) as? QuoteCell else {
return UITableViewCell()
}
cell.configure(model: quote)
return cell
}
}
}

Usage Example​

// No additional usage example necessary as the UITableView setup demonstrates usage.

References​

Read Full Snippet→