News Widget by SwiftUI

Step_01: Get the news api by login this website: https://newsapi.org
Then you will get same this api “09510d0f4f3641258f6f703aa2ef9611
After get the api key, now hit the api in Postman and you will see the JSON response data same as below:

{
  "status": "ok",
  "totalResults": 24,
  "articles": [
    {
      "source": {
        "id": "the-wall-street-journal",
        "name": "The Wall Street Journal"
      },
      "author": "Jean Eaglesham",
      "title": "Climate Promises by Businesses Face New Scrutiny",
      "description": "Emissions pledges from U.N. Glasgow conference will be closely watched",
      "url": "https://www.wsj.com/articles/climate-promises-by-businesses-face-new-scrutiny-11636104600?mod=hp_lead_pos2",
      "urlToImage": "https://images.wsj.net/im-429222/social",
      "publishedAt": "2021-11-05T09:30:00Z",
      "content": "The United Nations conference on climate change in Glasgow has been full of promises by companies to reduce their carbon emissions. How many will live up to them and how will anyone know if they are … [+333 chars]"
    },
    {
      "source": {
        "id": "the-wall-street-journal",
        "name": "The Wall Street Journal"
      },
      "author": "Jean Eaglesham",
      "title": "Climate Promises by Businesses Face New Scrutiny",
      "description": "Emissions pledges from U.N. Glasgow conference will be closely watched",
      "url": "https://www.wsj.com/articles/climate-promises-by-businesses-face-new-scrutiny-11636104600?mod=hp_lead_pos2",
      "urlToImage": "https://images.wsj.net/im-429222/social",
      "publishedAt": "2021-11-05T09:30:00Z",
      "content": "The United Nations conference on climate change in Glasgow has been full of promises by companies to reduce their carbon emissions. How many will live up to them and how will anyone know if they are … [+333 chars]"
    }
    ]
  }

Step_02: Now create new project selecting iOS target choosen project name:

Step_03: Create new swift file name NewsArticleList & NewsArticle and also added below code:

struct NewsArticleList: Codable {
    var status: String
    var totalResults: Int
    var articles: [NewsArticle]
}
struct NewsArticle: Codable {
    var author: String?
    var title: String
    var description: String?
    var url: String?
    var urlToImage: String?
    var content: String?    
    var publishedAt: String?
}
extension NewsArticle: Identifiable {
    var id: String {
        return title
    }
}

Step_04: Create APIService class for In order to retrieve the data from News API, we need a simple API service which will retrieve the JSON and convert it to a Codable Dataset. So create the following:

final class APIService {
    enum APIError: Error {
        case unknownError
    }

    func getObject<T: Codable>(object: T.Type, url: URL) -> AnyPublisher<T, Error> {
        return URLSession.shared.dataTaskPublisher(for: url)
                .tryMap { (data, response) -> Data in
                    guard let httpResponse = response as? HTTPURLResponse else {
                        throw APIError.unknownError
                    }
                    guard 200...299 ~= httpResponse.statusCode else {
                        throw URLError(URLError.Code(rawValue: httpResponse.statusCode), userInfo: httpResponse.allHeaderFields as? [String: Any] ?? [:] )
                    }
                    return data
                }
                .decode(type: T.self, decoder: JSONDecoder())
                .eraseToAnyPublisher()
    }
}

Step_05:  For that, we create a view model for retrieving the data from News API with an observable state, which will contain the data.

import Foundation
import Combine

final class NewsViewModel: ObservableObject {

    // MARK: - Constants
    private enum Constants {
      static let endpointString = "https://newsapi.org/v2/top-headlines?country=us&sortBy=publishedAt"
      static let apiKey = "09510d0f4f3641258f6f703aa2ef9612"
    }

    enum Category: String {
       case general
       case business
       case entertainment
       case health
       case science
       case sports
       case technology
   }

    // MARK: - state
    enum ResultState {
        case loading
        case error(Error)
        case success([NewsArticle])
    }

    // MARK: - properties
    private var cancelables: Set<AnyCancellable> = Set<AnyCancellable>()
    private let apiService: APIService

    @Published var state: ResultState = .loading

    init(apiService: APIService = APIService() ) {
        self.apiService = apiService
    }

    func getDataIfNeeded(category: Category = .general) {
         guard let url = getUrlForCategory(category: category) else {
             //completion?(.failure(APIService.APIError.unknownError))
             return
         }

         state = .loading
         apiService.getObject(object: NewsArticleList.self, url: url)
             .subscribe(on: DispatchQueue.global(qos: .background))
             .receive(on: DispatchQueue.main)
             .sink { [weak self] result in
                 switch result {
                 case .failure(let error):
                     self?.state = .error(error)
                     //completion?(.failure(error))
                 default: break
                 }

             } receiveValue: { [weak self] list in
                 self?.state = .success(list.articles)
             }.store(in: &cancelables)
     }

     private func getUrlForCategory(category: Category = .general) -> URL? {
          var urlComponents = URLComponents(string: Constants.endpointString)
          var queryItems = urlComponents?.queryItems
          queryItems?.append(URLQueryItem(name: "category", value: category.rawValue))
          queryItems?.append(URLQueryItem(name: "apiKey", value: Constants.apiKey))
          urlComponents?.queryItems = queryItems

          return urlComponents?.url
     }

}

Step_06: Now added below code in you ContentView for UI:

import SwiftUI
import Combine


struct ContentView: View {

    @ObservedObject var newsViewModel = NewsViewModel()

    var body: some View {
        GeometryReader { geometry in

            switch newsViewModel.state {
            case .loading:
                Text("Loading...")
                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
            case .success(let result):
                List(result) { item in
                    Text(item.title)
                }
            case .error:
                Text("Error occured!")
                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
            }

            }.onAppear {
                newsViewModel.getDataIfNeeded()
            }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Now if you run your project you will see news list of data. That was our general iPhone OS part. Now we will make widget & show data in widget fetching from NewsAPI data.

Widget_Step_01: Now we will create Widget Extension. Select project file then click File->New->Target. select iOS item and then search for widget extension.
For get the news value, we need to set target NewsWidgetExtension for all of classes:
1. NewsArticle
2. NewsArticleList
3. APIServices
4. NewsViewModel

Widget_Step_02: Modify NewsViewModel class like below:

import Foundation
import Combine

final class NewsViewModel: ObservableObject {

    // MARK: - Constants
    private enum Constants {
      static let endpointString = "https://newsapi.org/v2/top-headlines?country=us&sortBy=publishedAt"
      static let apiKey = "09510d0f4f3641258f6f703aa2ef9612"
    }

    // MARK: - properties
    enum Category: String {
        case general
        case business
        case entertainment
        case health
        case science
        case sports
        case technology
    }

    enum ResultState {
        case loading
        case error(Error)
        case success([NewsArticle])
    }

    private var cancelables: Set<AnyCancellable> = Set<AnyCancellable>()
    private let apiService: APIService

    @Published var state: ResultState = .loading

    // MARK: - init
    init(apiService: APIService = APIService() ) {
        self.apiService = apiService
    }

    // MARK: - get data
    func getDataIfNeeded(category: Category = .general, completion: ((Result<[NewsArticle], Error>) -> Void )? = nil ) {
        guard let url = getUrlForCategory(category: category) else {
            completion?(.failure(APIService.APIError.unknownError))
            return
        }

        state = .loading
        apiService.getObject(object: NewsArticleList.self, url: url)
            .subscribe(on: DispatchQueue.global(qos: .background))
            .receive(on: DispatchQueue.main)
            .sink { [weak self] result in
                switch result {
                case .failure(let error):
                    self?.state = .error(error)
                    completion?(.failure(error))
                default: break
                }

            } receiveValue: { [weak self] list in
                self?.state = .success(list.articles)
                completion?(.success(list.articles))
            }.store(in: &cancelables)
    }

    private func getUrlForCategory(category: Category = .general) -> URL? {
         var urlComponents = URLComponents(string: Constants.endpointString)
         var queryItems = urlComponents?.queryItems
         queryItems?.append(URLQueryItem(name: "category", value: category.rawValue))
         queryItems?.append(URLQueryItem(name: "apiKey", value: Constants.apiKey))
         urlComponents?.queryItems = queryItems

         return urlComponents?.url
    }

}

Widget_Step_03: In NewsWidget struck Refactor SimpleEntry by NewsListEntry. And added below code. If you see any error configuration then fixed it by state case set .idle:

struct NewsListEntry: TimelineEntry {
    enum State{
        case idle
        case error
        case success([NewsArticle])
    }
    let date: Date
    let state: State
}

Widget_Step_04: After successfully build the app, now create NewsViewModel object in Provider struct. Call the api for get data in timeline function and added code same as below:

struct Provider: AppIntentTimelineProvider {
    
    private let newsViewModel: NewsViewModel = NewsViewModel()
    
    func placeholder(in context: Context) -> NewsListEntry {
        NewsListEntry(date: Date(), state: .idle)
    }

    func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> NewsListEntry {
        NewsListEntry(date: Date(), state: .idle)
    }
    
    func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<NewsListEntry> {
        var entries: [NewsListEntry] = []
        newsViewModel.getDataIfNeeded{ result in
            switch result {
            case .success(let newsItems):
                let items = Array(newsItems.prefix(4))
                entries.append(NewsListEntry(date: Date(), state: .success(items)))
            case .failure:
                entries.append(NewsListEntry(date: Date(), state: .error))
                break
            }
        }
        return Timeline(entries: entries, policy: .atEnd)
    }
}

Widget_Step_05: Now set your NewsWidgetEntryView below like:

struct NewsWidgetEntryView : View {
    var entry: NewsListEntry

    var body: some View {
        GeometryReader { geometry in
            switch entry.state {
            case .idle:
                Text("No data yet, waiting")
                    .font(.system(size: 12))
                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
            case .error:
                Text("Oops something went wrong - please reconfigure")
                    .font(.system(size: 12))
                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
            case .success(let array):
                VStack {
                    Text("My News Widget")
                    Spacer()
                    ForEach(array) { array in
                        Text(array.title)
                            .frame(maxWidth: geometry.size.width, alignment: .leading)
                            .font(.system(size: 12))
                            .multilineTextAlignment(.leading)
                    }
                    Spacer()
                }
            }
        }
    }
}

26 thoughts on “News Widget by SwiftUI

Leave a Reply

Your email address will not be published. Required fields are marked *