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
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()
    }
}
APIService.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
//
//  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
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)
    }
}
User.swift
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"
    }
}
UserRow.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
//
//  UserRow.swift
//  SwiftUIProject
//
//  Created by Cairocoders
//
 
import SwiftUI
 
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
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
            }
 
    }
}
DetailsView.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
//
//  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