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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | // // 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() } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // // 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> { 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() } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // // 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 ) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // // 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" } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // // 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) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // // 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 } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // // 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) } } } } |