【Xcode/SwiftUI】GithubAPIを叩いて自分のフォロワーをリスト表示してみる(検索機能付き)

Model

import Foundation

struct Follower: Identifiable, Decodable {
    let id: Int
    let login: String // ユーザの名前
    let avatarURL: String // アバターのURL

    enum CodingKeys: String, CodingKey {
        case id
        case login
        case avatarURL = "avatar_url"
    }
}

View

HomeView.swift (メインのView)

import SwiftUI

struct HomeView: View {
    @State private var searchText = ""
    @StateObject var followerViewModel = FollowerViewModel()

    var body: some View {
        NavigationView {
            VStack {
                SearchBar(text: $searchText)
                List(followerViewModel.followers.filter { searchText.isEmpty ? true : $0.login.contains(searchText) }) { follower in
                    NavigationLink(destination: FollowerDetailView(avatarURL: follower.avatarURL, userName: follower.login)) {
                        FollowerRow(follower: follower)
                    }
                }
            }
            .onAppear {
                Task {
                    await followerViewModel.fetchFollowers()
                }
            }
            .navigationBarTitle("Followers")
        }
    }
}

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView()
    }
}

FollowerDetailView.swift (Navigationの遷移先)

import SwiftUI

struct FollowerDetailView: View {
    let avatarURL: String
    let userName: String

    var body: some View {
        VStack(spacing: 32) {
            AsyncImage(url: URL(string: avatarURL)) { phase in
                switch phase {
                case .empty:
                    Image(systemName: "person.crop.circle")
                case .success(let loadedImage):
                    loadedImage
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 200, height: 200)
                        .clipShape(Circle())
                case .failure:
                    Image(systemName: "person.crop.circle")
                @unknown default:
                    Image(systemName: "person.crop.circle")
                }
            }
            .frame(width: 200, height: 200)
            Text(userName)
                .font(.largeTitle)
            Spacer().frame(height: 240)
        }
        .navigationBarTitle("Follower")
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct FollowerDetailView_Previews: PreviewProvider {
    static var previews: some View {
        FollowerDetailView(avatarURL: "https://avatars.githubusercontent.com/u/74645651?s=96&v=4", userName: "Masaya1582")
    }
}

FollowerContentView.swift (SearchBarとListの中身のView)

import SwiftUI

struct FollowerRow: View {
    let follower: Follower
    @State private var image: Image = Image(systemName: "person.crop.circle")

    var body: some View {
        HStack {
            if let url = URL(string: follower.avatarURL) {
                AsyncImage(url: url) { phase in
                    switch phase {
                    case .empty:
                        image
                    case .success(let loadedImage):
                        loadedImage
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 50, height: 50)
                            .clipShape(Circle())
                    case .failure:
                        image
                    @unknown default:
                        image
                    }
                }
            } else {
                image // Show the default image if the URL is invalid
            }

            Text(follower.login)
                .padding(.leading, 10)

            Spacer()
        }
        .padding(.vertical, 8)
    }
}

struct SearchBar: View {
    @Binding var text: String

    var body: some View {
        HStack {
            TextField("Search followers...", text: $text)
                .padding(8)
                .background(Color(.systemGray6))
                .cornerRadius(8)
                .padding(.horizontal, 10)

            Button(action: {
                text = ""
            }) {
                Image(systemName: "xmark.circle.fill")
                    .foregroundColor(.gray)
                    .padding(8)
            }
        }
        .padding(.top, 8)
        .padding(.bottom, 4)
    }
}

ViewModel

import SwiftUI

class FollowerViewModel: ObservableObject {
    @Published var followers = [Follower]()

    func fetchFollowers() async {
        guard let url = URL(string: "https://api.github.com/users/<#自分のユーザ名#>/followers?per_page=10") else {
            fatalError("URL not found")
        }
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            self.followers = try JSONDecoder().decode([Follower].self, from: data)
        } catch {
            print("Error decoding data: \(error)")
        }
    }
}