The New York Times API Integration: A Game-Changer for My App

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:

  1. A NavigationLink is created, where an NYTArticle view is passed the current Doc (AKA currentArticle) in the list.

  2. A VStack is instantiated, where several elements are retrieved from currentArticle.

  3. The headline is displayed

  4. The multimedia array for currentArticle is checked to see if it is empty

    1. If it is, an Image with a question mark is displayed.

    2. 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 with AsyncImage 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!