The New York Times API Integration: A Game-Changer for My App
Breaking News: The New York Times API Revolutionizes App Development!
Hello everyone! I hope your Spring allergies aren't keeping you inside. If they are, I'm sure you can find some time to read the news, especially with the latest feature I am building into LegisLink.
Why "The New York Times"?
The New York Times was the outlet with the most readily available API. For some background, I was doing research into integrating live election tracking into LegisLink. However, I don't think I will be able to do that at this time. After some further research I discovered the Associated Press may be able to provide that live data but the cost is likely greater than I am willing to spend.
In fact, here is a link to a Reddit thread talking about this very topic!
Regardless, The New York Times (henceforth referred to as "NYT") does have an API for a number of things. RSS feeds, movie reviews, and even a books API are all available for you to mess with here (after you create a NYT Dev account and politely request an API key of course). Anyways, among the numerous APIs available there was one that caught my eye: the Article Search API. After some thinking, I decided I could include the ability in my app to request daily news relating to Congress. Just in case a user wanted to quickly access the happenings on Capitol Hill. From there, I could begin analyzing and integrating the API into LegisLink.
Understanding the API and the URL
I know that I want to grab some articles on the NYT from the past 24 hours, and display those articles in a list. Then I want the user to get a hint as to what the article is about (maybe a summary) and then be able to read the full story on the NYT if they are interested. Easy enough!
First, I want to build a query/API call to get a brief understanding as to what I am doing. If you look at the documentation, the API for articles is quite extensive, and even I don't absolutely understand every facet (ha) of it. After playing around with it in Postman, here is the URL I came up with:
This URL calls the search
endpoint, sets begin_date
and end_date
values to beginDate
and endDate
variables. Further, it restricts the filter queries to those where the document_type
is set to article
. Additionally, the facet
value is set to false
so that data we don't intend to use isn't included in the response of the API. facet_fields
is set to news_desk
because I only want to see articles related to the news. I want to apply this filter so facet_filter
is set to true
, and I want to query newer articles that mention congress so q
is set to congress
and sort
is set to newest
respectively.
At least, that's what I think is going on!
The NewYorkTimesAPI Service
In order to utilize this API, I created a NewYorkTimes API Service Swift file that will contain the relevant code. There, NewYorkTimesService
and NewYorkTimesServiceProtocol
are defined. Here is what the protocol definition looks like:
protocol NewYorkTimesServiceProtocol {
func getDailyCongressionalNews(completion: @escaping (Swift.Result<[Doc], Error>) -> Void)
func buildURL() -> URL
}
The code for these functions will exist in a NewYorkTimesService
class.
First, we'll cover the buildURL()
function.
The beginDate
and endDate
variables are going to be something like 20240101
and 20240102
to represent 24 hours. In terms of code, I have to translate the Date()
formatting of Swift to one the API can understand. From 2024-04-13 18:23:06
to 20240413
for example. Here is the code snippet that does this:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy.MM.d"
let today = Date()
let todayRaw = dateFormatter.string(from: today)
let beginDate = todayRaw.replacingOccurrences(of: ".", with: "")
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)!
let endDateRaw = dateFormatter.string(from: tomorrow)
let endDate = endDateRaw.replacingOccurrences(of: ".", with: "")
There are a few other examples of manipulating date/time in LegisLink. However, I don't think I have had the opportunities to discuss them. Since this is pretty straightforward, I'll keep the walkthrough brief.
First, I create a DateFormatter
object, and then assign it a format that matches what the API is expecting. Next, I instantiate a Date
object called today
, which works because this function will be called whenever the user wants to see congressional news on the same day. The todayRaw
value translates the actual date/time from today
into a string, and then I remove the original periods with the replacingOccurrences()
function. The process for the endDate
variable is the same, except I increment the value of today
by 1 to get the tomorrow
value before any conversions.
After this point, I use the endDate
, beginDate
and NYTAPIKey
variables to construct a usable URL. This function is called as part of the getDailyCongressionalNews()
function.
The getDailyCongressionalNews()
function has the primary role of using URLSession.shared.dataTask
to make a call to the NYT Articles API with a URL constructed by the buildURL()
function seen earlier. Here is the most relevant code:
do {
let nytResponse = try JSONDecoder().decode(NYTRequest.self, from: data)
let docs = nytResponse.response.docs
return completion(.success(docs))
}
Using JSONDecoder()
the raw response from the NYT API is serialized into an NYTRequest
object, which we will discuss momentarily. Then, the relevant part of this response, docs
is returned to wherever the getDailyCongressionalNews()
is being called from.
Here is what the NYTRequest
object looks like:
import Foundation
// MARK: - NYTRequest
struct NYTRequest: Codable {
let status, copyright: String
let response: NYTResponse
}
// MARK: - Response
struct NYTResponse: Codable {
let docs: [Doc]
let meta: Meta
}
// MARK: - Doc
struct Doc: Codable, Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(webURL)
}
static func == (lhs: Doc, rhs: Doc) -> Bool {
return lhs.webURL == rhs.webURL && lhs.webURL == rhs.webURL
}
let abstract: String
let webURL: String
let snippet, leadParagraph: String
let printSection, printPage: String?
let source: NYTSource
let multimedia: [Multimedia]
let headline: Headline
let keywords: [Keyword]
let pubDate: String
let documentType: DocumentType
let newsDesk, sectionName: String
let subsectionName: String?
let byline: Byline
let typeOfMaterial: String
let id: String
let wordCount: Int
let uri: String
enum CodingKeys: String, CodingKey {
case abstract
case webURL = "web_url"
case snippet
case leadParagraph = "lead_paragraph"
case printSection = "print_section"
case printPage = "print_page"
case source, multimedia, headline, keywords
case pubDate = "pub_date"
case documentType = "document_type"
case newsDesk = "news_desk"
case sectionName = "section_name"
case subsectionName = "subsection_name"
case byline
case typeOfMaterial = "type_of_material"
case id = "_id"
case wordCount = "word_count"
case uri
}
}
// MARK: - Byline
struct Byline: Codable {
let original: String?
let person: [NYTPerson]
let organization: String?
}
// MARK: - Person
struct NYTPerson: Codable {
let firstname: String
let middlename: String?
let lastname: String?
let qualifier, title: String?
let role: NYTRole
let organization: String
let rank: Int
}
enum NYTRole: String, Codable {
case reported = "reported"
}
enum DocumentType: String, Codable {
case article = "article"
}
// MARK: - Headline
struct Headline: Codable {
let main: String
let kicker: String?
let contentKicker: String??
let printHeadline: String?
let name, seo, sub: String??
enum CodingKeys: String, CodingKey {
case main, kicker
case contentKicker = "content_kicker"
case printHeadline = "print_headline"
case name, seo, sub
}
}
// MARK: - Keyword
struct Keyword: Codable {
let name: Name
let value: String
let rank: Int
let major: Major
}
enum Major: String, Codable {
case n = "N"
}
enum Name: String, Codable {
case glocations = "glocations"
case organizations = "organizations"
case persons = "persons"
case subject = "subject"
case creativeWorks = "creative_works"
}
// MARK: - Multimedia
struct Multimedia: Codable {
let rank: Int
let subtype: String
let caption, credit: String??
let type: NYTTypeEnum
let url: String
let height, width: Int
let legacy: Legacy
let subType, cropName: String
enum CodingKeys: String, CodingKey {
case rank, subtype, caption, credit, type, url, height, width, legacy, subType
case cropName = "crop_name"
}
}
// MARK: - Legacy
struct Legacy: Codable {
let xlarge: String?
let xlargewidth, xlargeheight: Int?
let thumbnail: String?
let thumbnailwidth, thumbnailheight, widewidth, wideheight: Int?
let wide: String?
}
enum NYTTypeEnum: String, Codable {
case image = "image"
}
enum NYTSource: String, Codable {
case theNewYorkTimes = "The New York Times"
}
// MARK: - Meta
struct Meta: Codable {
let hits, offset, time: Int
}
It's worth noting that I use this website to translate the JSON into Swift. Since I can only use actual JSON returned from a API call, sometimes the translation isn't perfect and I have to trial-and-error my way through.
Additionally, you can see why I have to return an array of Docs
. The API return value is initially status
, copyright
and response
objects. Within response
is a meta
object and an array of Docs
, which can be thought of as the actual list of articles I need. Thus, it is most appropriate to return the list of Docs
.
The above is a solid run-down as to how the model and API work is done for this feature.
How I display the data to the end user
Now, we can discuss the view and view model for this part of LegisLink. For this purpose, I created a NYTFeedViewModel
, and then two views: NYTFeedView
and NYTArticle
. Since NYTFeedViewModel
is pretty small and only calls one function, I will include its entirety in this blog:
import Foundation
class NYTFeedViewModel: ObservableObject {
private let nytService: NewYorkTimesServiceProtocol
@Published var articles: [Doc]
init(nytService: NewYorkTimesServiceProtocol) {
self.nytService = nytService
self.articles = [Doc]()
self.fetchNYTArticles()
}
func fetchNYTArticles() {
let group = DispatchGroup()
group.enter()
nytService.getDailyCongressionalNews() { result in
switch result {
case .success(let docList):
self.articles = docList
case .failure(let error):
print("Unexpected error: \(error).")
}
group.leave()
}
group.wait()
group.notify(queue: DispatchQueue.main) {
}
}
}
Here you can see that the NYTFeedViewModel
has 2 associated variables, nytService
and articles
. The articles variable is an array of the Docs
returned by the getDailyCongressionalNews()
function in the NewYorkTimesService
, which is ultimately called in the fetchNYTArticles()
function also defined in this view model. Additionally, nytService
is simply an instantiation of the NewYorkTimesService
. It is important to note that the NewYorkTimesService
isn't actually created in the view model, it is passed from the view. Specifically, it is passed from the NYTFeedView
and I will go over that next!
NYTFeedView
is a bit more verbose and complex than the view model. Ultimately, I wanted to have a feed of the different articles as a list. This also meant I needed to create a more custom view for when users select an article. Here is the most important code in NYTFeedView
:
ForEach(nytfvm.articles, id: \.self) { currentArticle in
NavigationLink(destination: NYTArticle(article: currentArticle)) {
VStack {
Text(currentArticle.headline.main)
if (!currentArticle.multimedia.isEmpty) {
AsyncImage(url: URL(string: createImageURL(givenURL: currentArticle.multimedia[0].url))) { phase in
if let image = phase.image {
image
.scaledToFit()
.frame(width: 300, height: 300)
.clipped()
} else if phase.error != nil {
Image(systemName: "questionmark.diamond")
.imageScale(.large)
.frame(width: 300, height: 300)
}
}
} else {
Image(systemName: "questionmark.diamond")
.imageScale(.large)
.frame(width: 300, height: 300)
}
}
}
}
(I had to fix up some of the spacing of the above code manually, so if it looks a little off, that's why)
For the entire NYTFeedView
, we have a ForEach
loop, that goes through the list of Docs
created by the NYTFeedViewModel
, which in this case is defined as nytfvm
. As seen before, each NYTFeedViewModel
has an articles
variable that the list is saved to. For every Doc
object in the articles
list, the following sequence occurs:
A
NavigationLink
is created, where anNYTArticle
view is passed the currentDoc
(AKAcurrentArticle
) in the list.A
VStack
is instantiated, where several elements are retrieved fromcurrentArticle
.The headline is displayed
The
multimedia
array forcurrentArticle
is checked to see if it is emptyIf it is, an
Image
with a question mark is displayed.If it isn't, a
createImageURL()
function is called. In short, this function takes the URL in the multimedia array for this article, and appends "https://www.nytimes.com/" to the front, which makes it actually usable. This is then fetched withAsyncImage
and displayed. If this attempt fails, the question mark image is still showed.
That's all there is for the NYTFeedView
. It is important to finally look at the highlight of this feature, which is being able to read a little bit from the New York Times article.
I decided to make a folder called Components
in my project for this, as I don't consider the NYTArticle
to really be a standalone view, as it makes up the larger NYTFeedView
. At least, it makes sense in my head.
Anyways, here is the code from the singular VStack
that makes up each instance of the NYTArticle
component.
VStack (alignment: .center, spacing: 5) {
Text("\(article.headline.main)")
.font(.headline)
.padding(.bottom, 4)
Text("\(article.byline.original!)")
.font(.caption)
.padding(.bottom, 4)
Text("\(article.snippet)")
.font(.subheadline)
.padding(.bottom, 4)
if (!article.multimedia.isEmpty) {
AsyncImage(url: URL(string: "https://www.nytimes.com/\(article.multimedia[0].url)"))
.scaledToFit()
.aspectRatio(contentMode: .fill)
.frame(width: 350, height: 350)
.clipped()
} else {
Image(systemName: "questionmark.diamond")
.imageScale(.large)
.frame(width: 300, height: 300)
}
Text("\(article.leadParagraph)")
.font(.body)
.padding(.bottom, 4)
let linkOne = article.webURL
let initLinkOne = "[\("Read more")](\(linkOne))"
Text(.init(initLinkOne))
.font(.caption)
.padding(.bottom, 4)
Text("\(article.source.rawValue)")
.font(.caption)
.padding(.bottom, 4)
}
Essentially, for every article displayed in the NYTFeedView
, a user can select it and see more info. This includes the headline, byline (author), a snippet from the article as well as the lead paragraph of the article, an image and then a link to the actual New York Times piece.
The "Finished" Product
For your viewing pleasure, here are some screenshots of the MVP for this feature:
A screenshot of the feed:
A screenshot from selecting the first story:
A screenshot from the feed when loading an image fails:
Again, it is worth noting that nothing pictured is final. I am by no means a SwiftUI artist, but at least it (generally) functions as intended!
End of a Journey
Whew! That was a doozy!
I have learned a lot from building out this feature. Building the NYTArticle
and NYTFeedView
together helped me better cement my understanding of how views work in Swift. Additionally, there were many quirks in working with the NYT Articles API, and I'm hoping it leaves me better equipped for working on the next feature.
Thank you so much for reading all the way through, and I look forward to writing the next blog!