Model
import Foundation
struct Follower : Identifiable , Decodable {
let id: Int
let login: String
let avatarURL: String
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
}
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) " )
}
}
}