article

Wednesday, April 6, 2022

SwiftUI Infinite Scroll Load More Data github user API with sheet view

SwiftUI Infinite Scroll Load More Data github user API with sheet view

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

Related Post