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)")
}
}
}