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