Displaying a list from Core Data is very simple, but what if you need to get your data from an API? For data out of CoreData or in an array, you can just throw it into a List or ForEach loop. For data you have to retrieve from somewhere else, this becomes a little more complicated.

With a List and a ForEach block, you get 2 options for what you can pass in. You can either pass in a Range (think for 1 to 5), or a RandomAccessCollection. The RandomAccessCollection, at first glance, seems like this is what we’d want to use. We could try to hide the logic that gets the data from the API inside of here. NSFetchedResults implement this interface, too.

What does Core Data do?

CoreData uses a RandomAccessCollection interface, which requires a few things:

struct stuctThatImplementsRac: RandomAccessCollection { typealias Element typealias Index var startIndex var endIndex subscript(position: Int) -> PostTitle }

The variable here that starts to concern me is the endIndex. For an API that returns a full list of elements, this won’t be an issue. But what about an API that is paginated? Does this mean that we’d have to load all elements up front? Well, let’s see how CoreData handles this. I updated some old code to print out the end index when the view loads. If the view re-loads, we’ll see multiple end indexes get printed.

This doesn’t look good. CoreData knows the total amount of elements loaded when the view first loads. Hopefully we can update the endIndex without causing the list to reset. If not, we’ll have to have an API that returns the total number of posts.

Implementing RandomAccessCollection

Let’s take a step back and think about what we want to do: When we reach the end of the list, we want to load more posts. In a List, there isn’t any kind of signal to the collection that lets it know what element we’re on. However, we can use the .onAppear method of an element to figure out what a user is seeing. If they’re seeing the last element, we’ll load more posts.

To make this easier, I’ll try this without an API first.

I’ll create a list of local objects where each object will have it’s own UUID. This will be abstracted into a RandomAccessCollection so we can pass it directly into our List / ForEach loop. Since this object will change, we’ll implement ObservableObject so the view knows it may have to reload itself.

Next, I’ll create a function that determines if we need to load more data based on the current element being shown. If the current element is the last, we’ll ‘load’ more data. We’ll also create another variable to let us know when our api is done ‘loading’ data.

struct ContentView: View { @ObservedObject var postList = PostList() var body: some View { VStack { List(postList) { (postTitle: PostTitle) in Text("\(postTitle.id)") .padding() .onAppear { self.postList.loadMorePosts(postTitle) } } } } } class PostList: ObservableObject, RandomAccessCollection { typealias Element = PostTitle typealias Index = Int var startIndex: Int = 0 var endIndex: Int { postTitles.endIndex } var doneLoading = false @Published var postTitles = [PostTitle]() init() { loadMorePosts() } subscript(position: Int) -> PostTitle { return postTitles[position] } func loadMorePosts(_ postTitle: PostTitle? = nil) { if !shouldLoadMorePosts(postTitle) { return } // 'Load' more data postTitles.append(PostTitle()) postTitles.append(PostTitle()) postTitles.append(PostTitle()) postTitles.append(PostTitle()) postTitles.append(PostTitle()) postTitles.append(PostTitle()) // Create a condition for loading to be done if postTitles.count > 40 { doneLoading = true } } func shouldLoadMorePosts(_ postTitle: PostTitle? = nil) -> Bool { // Don't need to load more data if we're done loading if doneLoading { return false } // If they didn't pass us a title, we want load more guard let postTitle = postTitle else { return true } // If the ID matched, we're at the end of the list if postTitles.last != nil && postTitles.last!.id == postTitle.id { return true } return false } } struct PostTitle: Identifiable { var id = UUID().uuidString }

This worked a lot better than I expected. I thought I’d have to debug it or have to do some workaround.

Now suppose we want to load when we’re near the end instead of at the end. This should be an easy update. We can check if the post that we pass in matched one of the last X elements. This can be done in the shouldLoadMorePosts function:

func shouldLoadMorePosts(_ postTitle: PostTitle? = nil) -> Bool { // Don't need to load more data if we're done loading if doneLoading { return false } // If they didn't pass us a title, we want load more guard let postTitle = postTitle else { return true } // If the ID matched, we're near the end of the list for i in (postTitles.count-4)...(postTitles.count-1) { if i >= 0 && postTitles[i].id == postTitle.id { return true } } return false }

Hooking this up to a real API

NewsAPI.org is a news aggregation site that has a free to use GET API. This API is really simple to use, and doesn’t need any authentication besides an API key. It returns a paginated list of articles, so this is perfect for this demonstration. I’ll use the everything API and search for Swift articles. The base url for our get request will then be: https://newsapi.org/v2/everything?q=swift&apiKey=6ffeaceffa7949b68bf9d68b9f06fd33&language=en&page=PageNumber

To actually get data from this API, we can use URLSession. In a previous post, I talked about how you can asynchronously load data from an API. Using a very similar technique, we can hit our API to get our news data. However, we’ll have to keep track of a few more things.

The first thing we need to keep track of is the next page we need to load. This API is paginated, and the first page we retrieve will be page 1. Every time we successfully load a page, we’ll increment this number.

Since the call is asynchronous, we’ll need to keep track if we’re currently loading a data. We can set a flag before we start our call. When the async call returns, we’ll reset it.

The other side effect of running this asynchronously is we can’t update any ObservedObject directly inside our object. At some point, we’ll have to update the object on our main thread. Since this thread shouldn’t have any heavy processing on it, we’ll parse the JSON response in background. Once everything is processed, we can pass the list to the main thread to update our ObservedObjects.

Great, lets try writing some code:

struct NewsList: View { @ObservedObject var newsFeed = NewsFeed() var body: some View { List(newsFeed) { article in NewsItemView(newsItem: article) .onAppear { self.newsFeed.loadMoreNewsItems(article)} } } } struct NewsItemView: View { var newsItem: NewsListItem var body: some View { VStack(alignment: .leading) { Text("\(newsItem.title)") .font(.headline) Text("\(newsItem.author)") .font(.subheadline) } .padding() } } class NewsFeed: ObservableObject, RandomAccessCollection { // Needed for RandomAccessCollection typealias Element = NewsListItem typealias Index = Int // Needed for RandomAccessCollection var startIndex: Int = 0 var endIndex: Int { newsListItems.endIndex } private final var urlBase = "https://newsapi.org/v2/everything?q=swift&apiKey=6ffeaceffa7949b68bf9d68b9f06fd33&language=en&page=" // List for news articles @Published var newsListItems = [NewsListItem]() // Needed for loading data var nextPageToLoad = 1 var doneLoading = false var currentlyLoading = false init() { loadMoreNewsItems() } subscript(position: Int) -> NewsListItem { return newsListItems[position] } func loadMoreNewsItems(_ newsListItem: NewsListItem? = nil) { if !shouldLoadMoreItems(newsListItem) { return } let urlString = "\(urlBase)\(nextPageToLoad)" let url = URL(string: urlString)! var request = URLRequest(url: url) request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request, completionHandler: parseArticlesFromResponse) currentlyLoading = true task.resume() } func shouldLoadMoreItems(_ newsListItem: NewsListItem? = nil) -> Bool { // Don't need to load more data if we're done loading or currently loading if doneLoading || currentlyLoading { return false } // If they didn't pass us a title, we want load more guard let newsListItem = newsListItem else { return true } // If the ID matched, we're near the end of the list for i in (newsListItems.count-4)...(newsListItems.count-1) { if i >= 0 && newsListItems[i].uuid == newsListItem.uuid { return true } } return false } func parseArticlesFromResponse(data: Data?, urlResponse: URLResponse?, error: Error?) { guard error == nil else { print("\(error!)") DispatchQueue.main.async { self.currentlyLoading = false } return } guard let content = data else { print("No data") DispatchQueue.main.async { self.currentlyLoading = false } return } let articles = getArticlesFromJson(content: content) DispatchQueue.main.async { for article in articles { self.newsListItems.append(article) } self.nextPageToLoad += 1 self.doneLoading = (articles.count == 0) self.currentlyLoading = false } } func getArticlesFromJson(content: Data) -> [NewsListItem] { let jsonObject = try! JSONSerialization.jsonObject(with: content) // Bad result guard let resultMap = jsonObject as? [String: Any] else { return [] } // Result not OK if resultMap["status"] as! String != "ok" { return [] } // Try to get the articles guard let articleMapList = resultMap["articles"] as? [[String: Any]] else { return [] } var newsItems = [NewsListItem]() for articleMap in articleMapList { guard let title = articleMap["title"] as? String else { continue } guard let author = articleMap["author"] as? String else { continue } newsItems.append(NewsListItem(title: title, author: author)) } return newsItems } } class NewsListItem: Identifiable { var title: String = "" var author: String = "" var uuid: String = UUID().uuidString init(title: String, author: String) { self.title = title self.author = author } }

Of course this pulls up articles about Taylor Swift and not just Swift 😒 I should have seen that coming.

Conclusions

This was much simpler to do than I thought it was going to be. I didn’t feel like I had to do any big hacks or work-arounds for this, which is a huge relief. This news api and app could also be a lot more fun to work with in the future.

Thanks for reading

– SchwiftyUI