article

Tuesday, April 5, 2022

SwiftUI News API App - Load More Data List View Infinite Scroll

SwiftUI News API App - Load More Data List View Infinite Scroll

News API : https://newsapi.org/

ContentView.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//
//  ContentView.swift
//  SwiftUIProject
//
//  Created by Cairocoders
//
 
import SwiftUI
 
struct ContentView: View {
 
    var body: some View {
        NewsFeedView()
    }
}
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
 
struct NewsFeedView: View {
    @ObservedObject var newsFeed = NewsFeed()
     
    var body: some View {
        NavigationView {
            List(newsFeed) { (article: NewsListItem) in
                NavigationLink(destination: NewsListItemView(article: article)) {
                    NewsListItemListView(article: article)
                        .onAppear {
                            self.newsFeed.loadMoreArticles(currentItem: article)
                    }
                }
            }
        .navigationBarTitle("Apple News")
        }
    }
}
 
struct NewsListItemView: View {
    var article: NewsListItem
     
    var body: some View {
        VStack {
            UrlWebView(urlToDisplay: URL(string: article.url)!)
                .edgesIgnoringSafeArea(.all)
                .navigationBarTitle(article.title)
        }
    }
}
 
struct NewsListItemListView: View {
    var article: NewsListItem
     
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text("\(article.title)")
                    .font(.headline)
                Text("\(article.author ?? "No Author")")
                    .font(.subheadline)
            }
        }
    }
}
NewsFeedModels.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//
//  NewsFeedModels.swift
//  SwiftUIProject
//
//  Created by Cairocoders
//
 
import Foundation
 
class NewsFeed: ObservableObject, RandomAccessCollection {
    typealias Element = NewsListItem
     
    @Published var newsListItems = [NewsListItem]()
     
    var startIndex: Int { newsListItems.startIndex }
    var endIndex: Int { newsListItems.endIndex }
    var loadStatus = LoadStatus.ready(nextPage: 1)
     
  
    init() {
        loadMoreArticles()
    }
     
    subscript(position: Int) -> NewsListItem {
        return newsListItems[position]
    }
     
    func loadMoreArticles(currentItem: NewsListItem? = nil) {
         
        if !shouldLoadMoreData(currentItem: currentItem) {
            return
        }
        guard case let .ready(page) = loadStatus else {
            return
        }
        loadStatus = .loading(page: page)
        let urlString = "\(urlBase)\(page)"
         
        let url = URL(string: urlString)!
        let task = URLSession.shared.dataTask(with: url, completionHandler: parseArticlesFromResponse(data:response:error:))
        task.resume()
    }
     
    func shouldLoadMoreData(currentItem: NewsListItem? = nil) -> Bool {
        guard let currentItem = currentItem else {
            return true
        }
         
        for n in (newsListItems.count - 4)...(newsListItems.count-1) {
            if n >= 0 && currentItem.uuid == newsListItems[n].uuid {
                return true
            }
        }
        return false
    }
     
    func parseArticlesFromResponse(data: Data?, response: URLResponse?, error: Error?) {
        guard error == nil else {
            print("Error: \(error!)")
            loadStatus = .parseError
            return
        }
        guard let data = data else {
            print("No data found")
            loadStatus = .parseError
            return
        }
         
        let newArticles = parseArticlesFromData(data: data)
        DispatchQueue.main.async {
            self.newsListItems.append(contentsOf: newArticles)
            if newArticles.count == 0 {
                self.loadStatus = .done
            } else {
                guard case let .loading(page) = self.loadStatus else {
                    fatalError("loadSatus is in a bad state")
                }
                self.loadStatus = .ready(nextPage: page + 1)
            }
        }
    }
     
    func parseArticlesFromData(data: Data) -> [NewsListItem] {
        var response: NewsApiResponse
        do {
            response = try JSONDecoder().decode(NewsApiResponse.self, from: data)
        } catch {
            print("Error parsing the JSON: \(error)")
            return []
        }
         
        if response.status != "ok" {
            print("Status is not ok: \(response.status)")
            return []
        }
         
        return response.articles ?? []
    }
     
    enum LoadStatus {
        case ready (nextPage: Int)
        case loading (page: Int)
        case parseError
        case done
    }
}
 
class NewsApiResponse: Codable {
    var status: String
    var articles: [NewsListItem]?
}
 
class NewsListItem: Identifiable, Codable {
    var uuid = UUID()
     
    var author: String?
    var title: String
    var url: String
     
    enum CodingKeys: String, CodingKey {
        case author, title, url
    }
}
UrlWebView.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//
//  UrlWebView.swift
//  SwiftUIProject
//
//  Created by Cairocoders
//
 
import SwiftUI
import WebKit
 
struct UrlWebView: UIViewRepresentable {
    typealias UIViewType = WKWebView
     
    var urlToDisplay: URL
     
    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
         
        webView.load(URLRequest(url: urlToDisplay))
         
        return webView
    }
     
    func updateUIView(_ uiView: WKWebView, context: Context) {
         
    }
}

Related Post