-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from stoola20/feature/list-user
Fetch GitHub Users
- Loading branch information
Showing
37 changed files
with
2,362 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
//// | ||
// String+Ext.swift | ||
// AccessAppExercise | ||
// | ||
// Created by Jesse Chen on 2024/5/14. | ||
// | ||
|
||
import Foundation | ||
|
||
extension String { | ||
func getNextLinkFromLinkHeader() -> String? { | ||
// Define the regular expression pattern | ||
let pattern = "<([^>]*)>; rel=\"next\"" | ||
|
||
// Create a regular expression object | ||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { | ||
return nil | ||
} | ||
|
||
// Search for the string that matches the regular expression pattern | ||
let range = NSRange(self.startIndex..<self.endIndex, in: self) | ||
let matches = regex.matches(in: self, options: [], range: range) | ||
|
||
// Get the matching result | ||
if let match = matches.first { | ||
// Extract the matched string from the matching result | ||
let nsString = self as NSString | ||
let matchRange = match.range(at: 1) | ||
if matchRange.location != NSNotFound { | ||
return nsString.substring(with: matchRange) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
//// | ||
// TableView+Ext.swift | ||
// AccessAppExercise | ||
// | ||
// Created by Jesse Chen on 2024/5/14. | ||
// | ||
|
||
import UIKit | ||
|
||
extension UITableView { | ||
/// Registers a cell class for use in creating new table view cells. | ||
/// | ||
/// - Parameters: | ||
/// - cell: The cell class to register. | ||
/// - bundle: The bundle containing the nib file. If nil, the main bundle is used. | ||
func register(cell: AnyClass, inBundle bundle: Bundle? = nil) { | ||
// Register the cell class using a nib file with the same name as the cell class. | ||
register( | ||
UINib(nibName: String(describing: cell.self), bundle: bundle), | ||
forCellReuseIdentifier: String(describing: cell.self) | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
//// | ||
// UIImage+Ext.swift | ||
// AccessAppExercise | ||
// | ||
// Created by Jesse Chen on 2024/5/14. | ||
// | ||
|
||
import UIKit | ||
import Kingfisher | ||
|
||
extension UIImageView { | ||
/// Loads an image from the specified URL asynchronously using Kingfisher and sets it as the image of the image view. | ||
/// | ||
/// - Parameters: | ||
/// - urlString: The URL string from which to load the image. | ||
/// - placeHolder: An optional placeholder image to display while the image is being loaded. Defaults to nil. | ||
func loadImage(_ urlString: String?, placeHolder: UIImage? = nil) { | ||
// Ensure that the urlString is not nil and can be converted to a URL. | ||
guard let urlString = urlString, let url = URL(string: urlString) else { return } | ||
|
||
// Set the Kingfisher indicator type to activity indicator. | ||
self.kf.indicatorType = .activity | ||
|
||
// Use Kingfisher to asynchronously load the image from the URL and set it as the image of the image view. | ||
self.kf.setImage( | ||
with: url, | ||
placeholder: placeHolder, | ||
options: [ | ||
.transition(.fade(1)), // Apply a fade transition when the image is loaded. | ||
.cacheOriginalImage // Cache the original image for future use. | ||
] | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
//// | ||
// UIViewController+Ext.swift | ||
// AccessAppExercise | ||
// | ||
// Created by Jesse Chen on 2024/5/15. | ||
// | ||
|
||
import UIKit | ||
|
||
extension UIViewController { | ||
/// Displays an alert with the specified title and message. | ||
/// | ||
/// - Parameters: | ||
/// - title: The title of the alert. | ||
/// - message: The message to display in the alert. | ||
/// - confirm: A closure to execute when the user confirms the alert. Defaults to nil. | ||
/// - cancel: A closure to execute when the user cancels the alert. Defaults to nil. | ||
func showAlert( | ||
title: String, | ||
message: String, | ||
confirm: (() -> Void)? = nil, | ||
cancel: (() -> Void)? = nil | ||
) { | ||
let alert = UIAlertController(title: title, | ||
message: message, | ||
preferredStyle: .alert) | ||
|
||
// Add cancel action if provided | ||
if let cancel { | ||
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in | ||
cancel() | ||
} | ||
alert.addAction(cancelAction) | ||
} | ||
|
||
// add confirm action | ||
let confirmAction = UIAlertAction(title: "OK", style: .default) { _ in | ||
confirm?() | ||
} | ||
alert.addAction(confirmAction) | ||
|
||
present(alert, animated: true, completion: nil) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
//// | ||
// UserInteractor.swift | ||
// AccessAppExercise | ||
// | ||
// Created by Jesse Chen on 2024/5/13. | ||
// | ||
|
||
import Alamofire | ||
import Foundation | ||
import RxSwift | ||
|
||
protocol UserInteractorProtocol { | ||
func getUserList(since: Int, pageSize: Int) -> Observable<([GitHubUser], [String: Any]?)> | ||
func getNextUserPage(link: String) -> Observable<([GitHubUser], [String: Any]?)> | ||
func getUserDetail(userName: String) -> Observable<DetailUser> | ||
} | ||
|
||
/// Interactor responsible for fetching user data from the GitHub API. | ||
final class UserInteractor: UserInteractorProtocol, RequestProtocol { | ||
/// Fetches a list of GitHub users. | ||
/// | ||
/// - Parameters: | ||
/// - since: The user ID to start fetching users from. | ||
/// - pageSize: The number of users to fetch per page. Default is 20. | ||
/// - Returns: An observable sequence of GitHub users. | ||
func getUserList(since: Int, pageSize: Int = 20) -> Observable<([GitHubUser], [String: Any]?)> { | ||
|
||
// Construct API endpoint for fetching user list | ||
let api = GitHubAPI.getUserList(since: since, pageSize: pageSize) | ||
let url = URL(string: api.baseURL + api.path) | ||
|
||
// Make a network request to fetch the user list | ||
return request( | ||
url: url, | ||
method: api.method, | ||
parameters: api.parameters, | ||
header: api.header, | ||
type: [GitHubUser].self | ||
) | ||
} | ||
|
||
/// Fetches the next page of GitHub users based on the provided link. | ||
/// | ||
/// - Parameter link: The link to the next page of users. | ||
/// - Returns: An observable sequence containing a tuple of GitHub users and response headers. | ||
func getNextUserPage(link: String) -> Observable<([GitHubUser], [String: Any]?)> { | ||
let url = URL(string: link) | ||
|
||
// Make a network request to fetch the user list | ||
return request( | ||
url: url, | ||
method: .get, | ||
parameters: nil, | ||
header: HTTPHeaderManager.shared.getDefaultHeaders(), | ||
type: [GitHubUser].self | ||
) | ||
} | ||
|
||
/// Fetches detailed information for a specific GitHub user. | ||
/// | ||
/// - Parameter userName: The username of the GitHub user. | ||
/// - Returns: An observable sequence containing the detailed information of the user. | ||
func getUserDetail(userName: String) -> Observable<DetailUser> { | ||
// Construct API endpoint for fetching user detail info | ||
let api = GitHubAPI.getUserDetail(userName: userName) | ||
let url = URL(string: api.baseURL + api.path) | ||
|
||
// Make a network request to fetch the user detail | ||
return request( | ||
url: url, | ||
method: api.method, | ||
parameters: api.parameters, | ||
header: api.header, | ||
type: DetailUser.self | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
//// | ||
// UserModel.swift | ||
// AccessAppExercise | ||
// | ||
// Created by Jesse Chen on 2024/5/13. | ||
// | ||
|
||
import Foundation | ||
import RxDataSources | ||
|
||
/// Structure representing a section in a user list, containing a header and a list of items. | ||
/// This custom structure can be pass to RxDataSources as section type. | ||
struct UserListSection { | ||
/// The header title of the section. | ||
var header: String | ||
/// The items contained within the section. | ||
var items: [Item] | ||
} | ||
|
||
extension UserListSection: SectionModelType { | ||
typealias Item = GitHubUser | ||
|
||
init(original: UserListSection, items: [GitHubUser]) { | ||
self = original | ||
self.items = items | ||
} | ||
} | ||
|
||
/// Struct representing a GitHub user. | ||
struct GitHubUser: Decodable { | ||
/// The username of the GitHub user. | ||
let login: String | ||
/// The URL of the avatar image for the GitHub user. | ||
let avatarUrl: String | ||
/// A boolean value indicates whether showing STAFF badge or not. | ||
let siteAdmin: Bool | ||
} | ||
|
||
/// Struct representing detailed information about a GitHub user. | ||
struct DetailUser: Decodable, Equatable { | ||
/// The URL of the avatar image for the GitHub user. | ||
let avatarUrl: String | ||
/// The full name of the GitHub user. | ||
let name: String? | ||
/// The biography of the GitHub user. | ||
let bio: String? | ||
/// The username of the GitHub user. | ||
let login: String | ||
/// A boolean value indicates whether showing STAFF badge or not. | ||
let siteAdmin: Bool | ||
/// The location of the GitHub user. | ||
let location: String? | ||
/// The URL of the personal website or blog of the GitHub user. | ||
let blog: String? | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
//// | ||
// APIEndpoint.swift | ||
// AccessAppExercise | ||
// | ||
// Created by Jesse Chen on 2024/5/13. | ||
// | ||
|
||
import Foundation | ||
import Alamofire | ||
|
||
/// Enum representing different endpoints of the GitHub API. | ||
enum GitHubAPI { | ||
/// Endpoint to fetch a list of GitHub users. | ||
/// | ||
/// - `since`: A user ID. Only return users with an ID greater than this ID. | ||
/// - `pageSize`: The number of results per page (max 100) | ||
case getUserList(since: Int, pageSize: Int) | ||
|
||
/// Endpoint to fetch detailed information about a specific GitHub user. | ||
/// | ||
/// - `userName`: The handle for the GitHub user account. | ||
case getUserDetail(userName: String) | ||
} | ||
|
||
extension GitHubAPI { | ||
/// The base URL of the GitHub API. | ||
var baseURL: String { | ||
return "https://api.github.com" | ||
} | ||
|
||
/// The path component of the URL for the API endpoint. | ||
var path: String { | ||
switch self { | ||
case .getUserList: | ||
return "/users" | ||
case .getUserDetail(let userName): | ||
return "/users/\(userName)" | ||
} | ||
} | ||
|
||
/// The HTTP method used for the API request. | ||
var method: Alamofire.HTTPMethod { | ||
return .get | ||
} | ||
|
||
/// The HTTP header fields for the API request. | ||
var header: [String: String] { | ||
HTTPHeaderManager.shared.getDefaultHeaders() | ||
} | ||
|
||
/// The parameters to be included in the API request. | ||
var parameters: [String: String]? { | ||
switch self { | ||
case .getUserList(let since, let pageSize): | ||
return ["since": String(since), | ||
"per_page": String(pageSize)] | ||
case .getUserDetail: | ||
return nil | ||
} | ||
} | ||
} |
Oops, something went wrong.