Github API
https://api.github.com/users?per_page=15
SDWebImage
https://github.com/SDWebImage/SDWebImageSwiftUI.git
https://docs.github.com/en/rest/reference/users#list-users
ContentView.swift
// // ContentView.swift // SwiftUIProject // // Created by Cairocoders // import Combine import SwiftUI struct ContentView: View { @ObservedObject private var userViewModel = UserViewModel() @State var columns = Array(repeating: GridItem(.flexible(), spacing: 15), count: 2) var body: some View { ScrollView(.vertical, showsIndicators: false) { HStack{ Text("GitHub User") .font(.title) .fontWeight(.bold) Spacer() Button { } label: { Image(systemName: "rectangle.grid.1x2") .font(.system(size: 24)) .foregroundColor(.black) } } .padding(.horizontal) .padding(.top,25) LazyVGrid(columns: self.columns,spacing: 25){ ForEach(userViewModel.users, id: \.id) { user in UserRow(user: user) } LoaderView(isFailed: userViewModel.isRequestFailed) .onAppear(perform: fetchData) .onTapGesture(perform: onTapLoadView) } .padding([.horizontal,.top]) } .background(Color.black.opacity(0.05).edgesIgnoringSafeArea(.all)) } private func fetchData() { userViewModel.getUsers() } private func onTapLoadView() { // tap to reload if userViewModel.isRequestFailed { userViewModel.isRequestFailed = false fetchData() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }APIService.swift
// // APIService.swift // SwiftUIProject // // Created by Cairocoders // import Foundation import Combine class APIService { static let shared = APIService() func getUsers(perPage: Int = 30, sinceId: Int? = nil) -> AnyPublisher<[User], Error> { var components = URLComponents(string: "https://api.github.com/users")! components.queryItems = [ URLQueryItem(name: "per_page", value: "\(perPage)"), URLQueryItem(name: "since", value: (sinceId != nil) ? "\(sinceId!)" : nil) ] let request = URLRequest(url: components.url!, timeoutInterval: 5) return URLSession.shared.dataTaskPublisher(for: request) .map(\.data) .decode(type: [User].self, decoder: JSONDecoder()) .eraseToAnyPublisher() } }LoaderView.swift
// // LoaderView.swift // SwiftUIProject // // Created by Cairocoders // import SwiftUI struct LoaderView: View { let isFailed: Bool var body: some View { Text(isFailed ? "Failed. Tap to retry." : "Loading..") .foregroundColor(isFailed ? .red : .green) .padding() .font(.title) } } struct LoaderView_Previews: PreviewProvider { static var previews: some View { LoaderView(isFailed: false) } }User.swift
// // User.swift // SwiftUIProject // // Created by Cairocoders // import Foundation struct User: Decodable, Identifiable { let id: Int let name: String let avatarUrl: String enum CodingKeys: String, CodingKey { case id case name = "login" case avatarUrl = "avatar_url" } }UserRow.swift
// // UserRow.swift // SwiftUIProject // // Created by Cairocoders // import SwiftUI import SDWebImageSwiftUI //https://github.com/SDWebImage/SDWebImageSwiftUI.git struct UserRow: View { let user: User @State var show = false var body: some View { VStack(spacing: 15){ ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) { Button { show.toggle() } label: { AnimatedImage(url: URL(string: user.avatarUrl)!) .resizable() .frame(height: 250) .cornerRadius(15) } Button { } label: { Image(systemName: "heart.fill") .foregroundColor(.red) .padding(.all,10) .background(Color.white) .clipShape(Circle()) } .padding(.all,10) } Text(user.name) .fontWeight(.bold) } .sheet(isPresented: $show, content: { DetailsView(user: User(id: user.id, name: user.name, avatarUrl: user.avatarUrl)) }) } } struct UserRow_Previews: PreviewProvider { static var previews: some View { let mockUser = User(id: 1, name: "cairocoders", avatarUrl: "") UserRow(user: mockUser) } }UserViewModel.swift
// // UserViewModel.swift // SwiftUIProject // // Created by Cairocoders // import Foundation import Combine class UserViewModel: ObservableObject { @Published var users: [User] = [] @Published var isRequestFailed = false private let pageLimit = 10 private var currentLastId: Int? = nil private var cancellable: AnyCancellable? func getUsers() { cancellable = APIService.shared.getUsers(perPage: pageLimit, sinceId: currentLastId) .receive(on: DispatchQueue.main) .sink { completion in switch completion { case .failure(let error): self.isRequestFailed = true print(error) case .finished: print("finished loading") } } receiveValue: { users in self.users.append(contentsOf: users) self.currentLastId = users.last?.id } } }DetailsView.swift
// // DetailsView.swift // SwiftUIProject // // Created by Cairocoders // import SwiftUI import SDWebImageSwiftUI struct DetailsView: View { var user: User var body: some View { VStack { HStack { AnimatedImage(url: URL(string: user.avatarUrl)!) .resizable() .frame(width: 250, height: 250) .cornerRadius(15) } HStack { Text("User Name :") Text(user.name) } } } }