• Mar 18, 2025

Building a SwiftUI News App with NewsAPI: Step-by-Step Guide

Today, we will walk through building a news app using SwiftUI and NewsAPI.org. SwiftUI makes it easy to create beautiful, responsive user interfaces with minimal code, and NewsAPI.org provides a simple way to fetch real-time news headlines from various sources. By combining these technologies, we will learn to make network requests, parse JSON data, and display articles in a clean, scrollable SwiftUI layout.

Before we start, visit https://newsapi.org/ and sign up for an account to get a free API key.

Next, let’s start by creating a new SwiftUI project and adding models for NewsAPI response. 

Here’s how the JSON response from NewsAPI.org appears, and we’ll be organizing our data models based on this hierarchical structure.

News struct will serve as the model that will encapsulate the entirety of the API response.

struct News: Codable, Identifiable {
    let id = UUID()
    let status: String
    let totalResults: Int
    let articles: [Article]
    
    enum CodingKeys: CodingKey {
        case status
        case totalResults
        case articles
    }
}

Article struct represents the detailed information of an article and also contains data that we want to display on the main page of the app.

struct Article: Codable, Identifiable {
    let id = UUID()
    let source: Source
    let author: String?
    let title: String
    let description: String?
    let url: String
    let urlToImage: String?
    let publishedAt: String
    let content: String?
    
    enum CodingKeys: CodingKey {
        case source
        case author
        case title
        case description
        case url
        case urlToImage
        case publishedAt
        case content
    }
}

The Source struct represents the name and ID of the origin of a news article. 

struct Source: Codable {
    let id: String?a
    let name: String
}

Since this app relies on fetching news articles from an external source, we need to make network calls to retrieve the content from NewsAPI.org. To handle these calls efficiently, we will create a dedicated Network Manager that uses URLSession. This manager will be responsible for sending requests, handling responses, and decoding the JSON data into Swift model objects that our SwiftUI views can display. 

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case delete = "DELETE"
}

enum NetworkError: Error {
    case invalidURL
    case requestFailed(statusCode: Int)
    case decodingError
    case unknownError
}

struct NetworkingManager {

    static let shared = NetworkingManager()

    private init() {}

    func request<T: Decodable>(
        endpoint: String,
        method: HTTPMethod = .get,
        parameters: [String: Any]? = nil,
        headers: [String: String]? = nil,
        responseType: T.Type
    ) async throws -> T {
        
        guard let url = URL(string: endpoint) else {
            throw NetworkError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue

        headers?.forEach { key, value in
            request.setValue(value, forHTTPHeaderField: key)
        }

        if let parameters = parameters, method != .get {
            request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }

        let (data, response) = try await URLSession.shared.data(for: request)

        if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
            throw NetworkError.requestFailed(statusCode: httpResponse.statusCode)
        }

        do {
            return try JSONDecoder().decode(T.self, from: data)
        } catch {
            throw NetworkError.decodingError
        }
    }
}

Next, we will extend our News model by adding an extension that handles fetching news articles directly from NewsAPI.org. This extension will contain a static function that constructs the appropriate API URL, including the endpoint and the required API key, to retrieve the latest technology news articles. 

Refer to the official NewsAPI documentation here: https://newsapi.org/docs/endpoints/sources

extension News {
    static func fetchNews() async -> News? {
        do {
            let news = try await NetworkingManager.shared.request(endpoint: "https://newsapi.org/v2/top-headlines?category=technology&country=us&apiKey=<<APIKEY>>", responseType: News.self)
            return news
        } catch {
            print(error.localizedDescription)
        }
        
        return nil
    }
}

Next, we will begin working on the user interface, starting with CardView to display articles on the home page.

struct CardView: View {
    var title: String
    var desc: String
    var author: String
    var imageUrl: String
    var body: some View {
        VStack {
            AsyncImage(url: URL(string: imageUrl)) { image in
                image
                    .resizable()
                    .scaledToFit()
            } placeholder: {
                Image(systemName: "photo")
                    .resizable()
                    .scaledToFit()
            }
            .clipped()
            VStack(alignment: .leading) {
                Text(title)
                    .font(.headline)
                Text(author)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
                Text(desc)
                    .font(.caption)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
            .padding(.leading)
        }
        .frame(maxWidth: .infinity)
        .cornerRadius(30)
        .background(
            RoundedRectangle(cornerRadius: 30)
                .foregroundStyle(.white)
                .shadow(radius: 5)
        )
        .padding(10)
    }
}

We will use this CardView inside our main view to display each of the fetched news articles. 

struct NewsUIExample: View {
    @State private var news: News = .init(status: "", totalResults: 0, articles: [])
    var body: some View {
        NavigationStack {
            List(news.articles) { article in
                NavigationLink(value: article.url) {
                    CardView(title: article.title, desc: article.description ?? "", author: article.author ?? article.source.name, imageUrl: article.urlToImage ?? "")
                }
            }
            .listStyle(.plain)
            .navigationTitle("DT News")
            .onAppear {
                fetchNews()
            }
            .refreshable {
                fetchNews()
            }
            .navigationDestination(for: String.self) { url in
                WebView(url: URL(string: url)!)
            }
        }
    }
    
    func fetchNews() {
        Task {
            news = await News.fetchNews() ?? .init(status: "", totalResults: 0, articles: [])
        }
    }
}

When a user taps on a news CardView, the app will open a WebView to display the full content of the selected news article. 

import WebKit

struct WebView: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        let request = URLRequest(url: url)
        uiView.load(request)
    }
}

Build and run