Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions gitprofile.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
8BEAA1A32C7E595F00AEDB6D /* RepositoryItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEAA1A22C7E595F00AEDB6D /* RepositoryItemView.swift */; };
8BF98C852C82264300E5139C /* RepositoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF98C842C82264300E5139C /* RepositoryListView.swift */; };
8E28FCCA2DB157FC00533133 /* CacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E28FCC92DB157F900533133 /* CacheManager.swift */; };
8E28FCCE2DB1EA5600533133 /* GetRecentSearchedUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E28FCCD2DB1EA5600533133 /* GetRecentSearchedUseCase.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -128,6 +129,7 @@
8BEAA1A22C7E595F00AEDB6D /* RepositoryItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryItemView.swift; sourceTree = "<group>"; };
8BF98C842C82264300E5139C /* RepositoryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryListView.swift; sourceTree = "<group>"; };
8E28FCC92DB157F900533133 /* CacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManager.swift; sourceTree = "<group>"; };
8E28FCCD2DB1EA5600533133 /* GetRecentSearchedUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetRecentSearchedUseCase.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -250,6 +252,7 @@
8BCD6DFC2C8A928500A92BC8 /* GetStarredReposUseCase.swift */,
8BCD6E062C8AC90D00A92BC8 /* GetUserOrgsUseCase.swift */,
8BCD6E0F2C8C44B100A92BC8 /* SearchUserUseCase.swift */,
8E28FCCD2DB1EA5600533133 /* GetRecentSearchedUseCase.swift */,
);
path = UseCase;
sourceTree = "<group>";
Expand Down Expand Up @@ -421,6 +424,7 @@
8B5AD3AD2C84E85800AA2571 /* ApiConfig.xcconfig in Resources */,
8B5AD3962C84A58400AA2571 /* github_languages.json in Resources */,
8B6EC86F2C71DEE600CF7B77 /* Assets.xcassets in Resources */,
8E455A9E2D6EB7A4002E37D2 /* UserListViewController.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -449,6 +453,7 @@
8BCD6DE22C85C0C500A92BC8 /* UserDataComponent.swift in Sources */,
8B6EC8832C72D94500CF7B77 /* UserListView.swift in Sources */,
8B5AD3832C83B7BE00AA2571 /* NetworkComponent.swift in Sources */,
8E28FCCE2DB1EA5600533133 /* GetRecentSearchedUseCase.swift in Sources */,
8BEAA1A12C7E316500AEDB6D /* SlidingTabView.swift in Sources */,
8BCD6E032C8AC28F00A92BC8 /* GetUserOrgsNetworkCall.swift in Sources */,
8BCD6DF92C8A8A2400A92BC8 /* PaginatedNetworkCall.swift in Sources */,
Expand Down
3 changes: 2 additions & 1 deletion gitprofile/Data/Remote/GetAllUsersNetworkCall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class GetAllUsersNetworkCall {
) {
urlComponents.queryItems = params

let urlRequest = NetworkComponent.createUrlRequest(url: self.urlComponents.url!, method: "GET")
var urlRequest = NetworkComponent.createUrlRequest(url: self.urlComponents.url!, method: "GET")
urlRequest.cachePolicy = .returnCacheDataElseLoad
logger.log(message: String(describing: urlRequest))
// switch strategy {
// case .cacheOverRemote:
Expand Down
4 changes: 4 additions & 0 deletions gitprofile/Data/Remote/NetworkComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ struct NetworkComponent {
let urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: "gitprofile")
let urlSessionConfig = NetworkComponent.createDefaultURLSessionConfig(urlCache)
self.session = URLSession(configuration: urlSessionConfig)

// Shared Global: async image download
URLCache.shared.memoryCapacity = memoryCapacity
URLCache.shared.diskCapacity = diskCapacity
}

static func createDefaultURLSessionConfig(_ urlCache: URLCache) -> URLSessionConfiguration {
Expand Down
13 changes: 13 additions & 0 deletions gitprofile/Data/Repository/UserDetailsRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation

protocol UserDetailsRepository {
func getAllUserDetails() -> [UserDetailsResponse]
func getAllUserDetails(username: String) -> [UserDetailsResponse]
func getUserDetails(username: String) -> UserDetailsResponse?
func saveUserDetails(userDetails: UserDetailsResponse)
Expand Down Expand Up @@ -76,4 +77,16 @@ class UserDetailsRepositoryImpl : UserDetailsRepository {
return matchingUsers
}

func getAllUserDetails() -> [UserDetailsResponse] {
var collections: [UserDetailsResponse] = []
let prefixes = getPrefixes()

for prefix in prefixes {
let key = cacheKey(for: prefix)
if let userDetails: UserDetailsResponse = cacheManager.retrieve(forKey: key){
collections.append(userDetails)
}
}
return collections
}
}
25 changes: 17 additions & 8 deletions gitprofile/Data/UserNetworkDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import Foundation

protocol UserDataManager {

func loadUsers() async -> Result<[UserResponse], Error>
func loadUsers(_ strategy: FetchStrategy) async -> Result<[UserResponse], Error>
func findAllUserDetails() async -> Result<[UserDetailsResponse], Error>
func findAllUserDetails(username: String) async -> Result<[UserDetailsResponse], Error>
func findUserDetails(username: String) async -> Result<UserDetailsResponse, Error>
func findUserRepos(username: String) async -> Result<PagingSourceEntity<[RepositoriesResponse]>, Error>
Expand All @@ -30,8 +31,16 @@ class UserNetworkDataManager : UserDataManager {
self.component = factory.create()
}

func loadUsers() async -> Result<[UserResponse], Error> {
func loadUsers(_ strategy: FetchStrategy) async -> Result<[UserResponse], Error> {
let userRepo = component.providesUsersRepository()

if strategy == .cacheOverRemote {
let cachedUsers = userRepo.getUsers()
if !cachedUsers.isEmpty {
return .success(cachedUsers)
}
}

let queryParams = [
URLQueryItem(name: "since", value: "\(userRepo.getNextPage())"),
URLQueryItem(name: "per_page", value: pageSize)
Expand All @@ -52,6 +61,12 @@ class UserNetworkDataManager : UserDataManager {
}
}

func findAllUserDetails() async -> Result<[UserDetailsResponse], any Error> {
let userDetailsRepo = component.providesUserDetailsRepository()
let userDetails = userDetailsRepo.getAllUserDetails()
return .success(userDetails)
}

func findAllUserDetails(username: String) async -> Result<[UserDetailsResponse], Error> {
let userDetailsRepo = component.providesUserDetailsRepository()
let userDetails = userDetailsRepo.getAllUserDetails(username: username)
Expand Down Expand Up @@ -105,12 +120,6 @@ class UserNetworkDataManager : UserDataManager {
]
logger.log(message: "ApiCall: GetRepositories for user: \(username), params: \(queryParams)")
component.providesGetRepositoriesNetworkCall().execute(username: username, params: queryParams, completion: handler)
}, map: { repos in
repos.sorted(by: {
guard let updatedAtA = $0.updatedAt else { return false }
guard let updateAtB = $1.updatedAt else { return false }
return updatedAtA > updateAtB
})
})
}

Expand Down
8 changes: 7 additions & 1 deletion gitprofile/Domain/DI/UserDomainComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ protocol UserDomainComponent {
func providesGetStarredReposUseCase() -> GetStarredReposUseCase
func providesGetUserOrgsUseCase() -> GetUserOrgsUseCase
func providesSearchUserUseCase() -> SearchUserUseCase
func providesGetRecentSearchedUseCase() -> GetRecentSearchedUseCase
}

private class UserComponentImpl : UserDomainComponent {
Expand All @@ -26,7 +27,8 @@ private class UserComponentImpl : UserDomainComponent {
private static let getStarredReposUseCase = GetStarredReposUseCase(ServiceLocator.dataManager)
private static let getUserOrgsUseCase = GetUserOrgsUseCase(ServiceLocator.dataManager)
private static let searchUserUseCase = SearchUserUseCase(ServiceLocator.dataManager)

private static let getSearchedUsersUseCase = GetRecentSearchedUseCase(ServiceLocator.dataManager)

func providesGetUsersUseCase() -> GetUsersUseCase {
return UserComponentImpl.getUserUseCase
}
Expand All @@ -50,6 +52,10 @@ private class UserComponentImpl : UserDomainComponent {
func providesSearchUserUseCase() -> SearchUserUseCase {
return UserComponentImpl.searchUserUseCase
}

func providesGetRecentSearchedUseCase() -> GetRecentSearchedUseCase {
return UserComponentImpl.getSearchedUsersUseCase
}
}

class UserDomainComponentFactory {
Expand Down
43 changes: 43 additions & 0 deletions gitprofile/Domain/UseCase/GetRecentSearchedUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// GetRecentSearchedUseCase.swift
// GitHub Profile
//
// Created by Aljan Porquillo on 4/18/25.
//

import Foundation

class GetRecentSearchedUseCase {

private let cacheManager = CacheManager.shared
private let dataManager: UserDataManager

init(_ dataManager: UserDataManager) {
self.dataManager = dataManager
}

func execute() async -> LoadableViewState<[UserUiModel]> {
return await dataManager.findAllUserDetails()
.fold(
onSuccess: { userDetails in
return .loaded(
oldData: userDetails
.compactMap { detail in
guard let id = detail.id, let login = detail.login else {
return nil
}
return UserUiModel(
id: id,
login: login,
avatarUrl: detail.avatarUrl
)
}
.reversed()
)
},
onFailure: { error in
return .failure(message: error.localizedDescription)
}
)
}
}
4 changes: 2 additions & 2 deletions gitprofile/Domain/UseCase/GetUsersUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class GetUsersUseCase {
self.dataManager = dataManager
}

func execute() async -> LoadableViewState<[UserUiModel]> {
return await dataManager.loadUsers()
func execute(_ strategy: FetchStrategy) async -> LoadableViewState<[UserUiModel]> {
return await dataManager.loadUsers(strategy)
.fold(onSuccess: { users in
.success(data: users.map { user in
UserUiModel(
Expand Down
2 changes: 1 addition & 1 deletion gitprofile/Domain/UseCase/SearchUserUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class SearchUserUseCase {
return .loaded(oldData: uiModels)
}

let filtered = await dataManager.loadUsers()
let filtered = await dataManager.loadUsers(.cacheOverRemote)
.fold(onSuccess: { users in
users.filter {
guard let login = $0.login, let _ = $0.id else {
Expand Down
3 changes: 2 additions & 1 deletion gitprofile/Domain/UserDomainManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import Foundation

protocol UserDomainManager {
func getUsers() async -> LoadableViewState<[UserUiModel]>
func getUsers(strategy: FetchStrategy) async -> LoadableViewState<[UserUiModel]>
func getUserRepos(username: String) async -> LoadableViewState<[UserReposUiModel]>
func getUserDetails(username: String) async -> GenericViewState<UserDetailsUiModel>
func getStarredRepos(username: String) async -> LoadableViewState<[UserStarredReposUiModel]>
func getUserOrgs(username: String) async -> LoadableViewState<[UserOrgsUiModel]>
func searchUser(query: String) async -> LoadableViewState<[UserUiModel]>
func getRecentSearches() async -> LoadableViewState<[UserUiModel]>
}
10 changes: 8 additions & 2 deletions gitprofile/Domain/UserUseCaseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class UserUseCaseManager : UserDomainManager {
self.component = factory.create()
}

func getUsers() async -> LoadableViewState<[UserUiModel]> {
func getUsers(strategy: FetchStrategy) async -> LoadableViewState<[UserUiModel]> {
return await component.providesGetUsersUseCase()
.execute()
.execute(strategy)
}

func getUserRepos(username: String) async -> LoadableViewState<[UserReposUiModel]> {
Expand All @@ -44,4 +44,10 @@ class UserUseCaseManager : UserDomainManager {
return await component.providesSearchUserUseCase()
.execute(username: query)
}

func getRecentSearches() async -> LoadableViewState<[UserUiModel]> {
return await component.providesGetRecentSearchedUseCase()
.execute()
}

}
2 changes: 1 addition & 1 deletion gitprofile/View/Users/SearchUserProxyStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class SearchUserProxyStore: ObservableObject {

private func startObserver() {
searchSubject
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.debounce(for: .milliseconds(700), scheduler: RunLoop.main)
.sink { [weak self] searchText in
self?.userStore.send(.search(query: searchText))
}
Expand Down
Loading