How to Build a Blog Reader App with SwiftUI

SwiftUI, Apple’s modern UI framework, provides a declarative approach to building user interfaces. In this tutorial, we’ll explore how to create a simple blog reader app using SwiftUI. This app will fetch blog posts from an API and display them in a list. You’ll learn to fetch data asynchronously, parse JSON, display lists, and handle basic UI interactions.

Prerequisites

Before you begin, ensure you have:

  • Xcode 13 or later installed
  • Basic knowledge of Swift and SwiftUI

Step 1: Create a New Xcode Project

  1. Open Xcode and click on “Create a new Xcode project.”
  2. Select the “App” template under the iOS tab.
  3. Enter a project name (e.g., “BlogReaderApp”) and choose SwiftUI for the Interface.
  4. Click “Next” and choose a location to save your project.

Step 2: Define the Data Model

First, create a struct to represent a blog post. This struct should match the structure of the JSON data from your API.

For example, if your API returns JSON like this:


[
    {
        "id": 1,
        "title": "SwiftUI Basics",
        "content": "Introduction to SwiftUI development...",
        "author": "John Doe"
    },
    {
        "id": 2,
        "title": "Asynchronous Programming",
        "content": "Handling asynchronous tasks in Swift...",
        "author": "Jane Smith"
    }
]

Create a Post struct:


import Foundation

struct Post: Identifiable, Decodable {
    let id: Int
    let title: String
    let content: String
    let author: String
}

Key points:

  • Identifiable: Makes it easier to use the Post in a List.
  • Decodable: Allows automatic conversion from JSON data.

Step 3: Fetch Data from the API

Create a function to fetch the blog posts from your API. This involves using URLSession to make a network request and decode the JSON response into an array of Post objects.


import Foundation

class ContentViewModel: ObservableObject {
    @Published var posts: [Post] = []

    func fetchPosts() {
        guard let url = URL(string: "YOUR_API_ENDPOINT_HERE") else {
            print("Invalid URL")
            return
        }

        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                print("Error fetching data: \(error)")
                return
            }

            guard let data = data else {
                print("No data received")
                return
            }

            do {
                let decodedPosts = try JSONDecoder().decode([Post].self, from: data)
                DispatchQueue.main.async {
                    self.posts = decodedPosts
                }
            } catch {
                print("Error decoding JSON: \(error)")
            }
        }.resume()
    }
}

Explanation:

  • ContentViewModel: A class that conforms to ObservableObject to hold and manage the data.
  • @Published var posts: A published property that updates the UI whenever the posts array changes.
  • fetchPosts(): Fetches data from the provided URL, decodes the JSON response into [Post], and updates the posts array on the main thread.
  • DispatchQueue.main.async: Ensures UI updates happen on the main thread to prevent UI blocking and maintain responsiveness.

Step 4: Create the SwiftUI View

Now, create the main content view that displays the list of blog posts.


import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel = ContentViewModel()

    var body: some View {
        NavigationView {
            List(viewModel.posts) { post in
                VStack(alignment: .leading) {
                    Text(post.title)
                        .font(.headline)
                    Text("By: \(post.author)")
                        .font(.subheadline)
                    Text(post.content)
                        .lineLimit(2)
                        .truncationMode(.tail)
                }
            }
            .navigationTitle("Blog Posts")
        }
        .onAppear {
            viewModel.fetchPosts()
        }
    }
}

Key components:

  • @ObservedObject var viewModel: Observes the ContentViewModel for any changes.
  • NavigationView: Provides navigation features.
  • List(viewModel.posts): Creates a list of posts using the viewModel.posts array.
  • VStack: Arranges the post title, author, and content vertically for each post in the list.
  • .onAppear: Calls viewModel.fetchPosts() when the view appears to fetch and display the blog posts.

Step 5: Display Post Details

To view detailed content, navigate to a detail view upon selection. Create a PostDetailView.


import SwiftUI

struct PostDetailView: View {
    let post: Post

    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                Text(post.title)
                    .font(.largeTitle)
                    .padding(.bottom, 5)
                Text("By: \(post.author)")
                    .font(.headline)
                    .foregroundColor(.gray)
                    .padding(.bottom, 10)
                Text(post.content)
                    .font(.body)
            }
            .padding()
        }
        .navigationTitle("Post Details")
    }
}

Integrate the detail view into ContentView using NavigationLink.


import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel = ContentViewModel()

    var body: some View {
        NavigationView {
            List(viewModel.posts) { post in
                NavigationLink(destination: PostDetailView(post: post)) {
                    VStack(alignment: .leading) {
                        Text(post.title)
                            .font(.headline)
                        Text("By: \(post.author)")
                            .font(.subheadline)
                        Text(post.content)
                            .lineLimit(2)
                            .truncationMode(.tail)
                    }
                }
            }
            .navigationTitle("Blog Posts")
        }
        .onAppear {
            viewModel.fetchPosts()
        }
    }
}

Here, each post is wrapped in a NavigationLink, navigating to PostDetailView with the corresponding post.

Step 6: Error Handling

Implement robust error handling. In ContentViewModel, create an errorMessage.


import Foundation

class ContentViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var errorMessage: String?

    func fetchPosts() {
        guard let url = URL(string: "YOUR_API_ENDPOINT_HERE") else {
            errorMessage = "Invalid URL"
            return
        }

        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                DispatchQueue.main.async {
                    self.errorMessage = "Error fetching data: \(error.localizedDescription)"
                }
                return
            }

            guard let data = data else {
                DispatchQueue.main.async {
                    self.errorMessage = "No data received"
                }
                return
            }

            do {
                let decodedPosts = try JSONDecoder().decode([Post].self, from: data)
                DispatchQueue.main.async {
                    self.posts = decodedPosts
                }
            } catch {
                DispatchQueue.main.async {
                    self.errorMessage = "Error decoding JSON: \(error.localizedDescription)"
                }
            }
        }.resume()
    }
}

Update ContentView to display errors. Add an alert.


import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel = ContentViewModel()
    @State private var showingAlert = false

    var body: some View {
        NavigationView {
            List(viewModel.posts) { post in
                NavigationLink(destination: PostDetailView(post: post)) {
                    VStack(alignment: .leading) {
                        Text(post.title)
                            .font(.headline)
                        Text("By: \(post.author)")
                            .font(.subheadline)
                        Text(post.content)
                            .lineLimit(2)
                            .truncationMode(.tail)
                    }
                }
            }
            .navigationTitle("Blog Posts")
            .alert(isPresented: .constant(viewModel.errorMessage != nil), content: {
                Alert(title: Text("Error"), message: Text(viewModel.errorMessage ?? "Unknown error"), dismissButton: .default(Text("OK"), action: {
                    viewModel.errorMessage = nil
                }))
            })
        }
        .onAppear {
            viewModel.fetchPosts()
        }
    }
}

Conclusion

Congratulations! You’ve built a simple blog reader app using SwiftUI. This tutorial covered fetching data from an API, parsing JSON, displaying a list of posts, and navigating to post details. You can further enhance this app by adding features like caching, pull-to-refresh, and user authentication to provide a more complete and engaging user experience.