Skip to content

Commit

Permalink
[GWL-405] 무한스크롤 기능 구현 (#439)
Browse files Browse the repository at this point in the history
* feat: HomeFeature DownSampling 모듈 추가

* feat: 다운 샘플링 적용

* feat: ImageCacher 및 PrepareForReuse 적용

* feat: Repository And UseCase 파일 생성

* feat: FetchCheckManager 구현

* move: 파일 위치 변경

* feat: coordaintor 및 UseCase 적용

* feat: 무한 스크롤 구현

* delete: 목 데이터 삭제

* feat: 화면에 피드가 표시되었을 때 Page넘버를 증가하는 기능 추가

* delete: Home화면 Resources 폴더 제거

* style: 피드백 적용 및 쓰지 않는 import문 제거
  • Loading branch information
MaraMincho authored Jan 12, 2024
1 parent 11e8928 commit e8cb6f6
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ final class AppCoordinator: AppCoordinating {
}

func start() {
showSplashFlow()
showTabBarFlow()
}

private func showSplashFlow() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class TabBarCoordinator: TabBarCoordinating {
}

func start() {
let tabBarViewControllers = [TabBarPage.record, TabBarPage.profile].map {
let tabBarViewControllers = TabBarPage.allCases.map {
return makePageNavigationController(page: $0)
}
let tabBarController = makeTabBarController(tabBarViewControllers: tabBarViewControllers)
Expand All @@ -58,6 +58,13 @@ final class TabBarCoordinator: TabBarCoordinating {

private func startTabBarCoordinator(page: TabBarPage, pageNavigationViewController: UINavigationController) {
switch page {
case .home:
let coordinator = HomeCoordinator(
navigationController: pageNavigationViewController,
delegate: self
)
childCoordinators.append(coordinator)
coordinator.start()
case .record:
let recordCoordinator = RecordFeatureCoordinator(
navigationController: pageNavigationViewController,
Expand Down
11 changes: 10 additions & 1 deletion iOS/Projects/Features/Home/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ let project = Project.makeModule(
targets: .feature(
.home,
testingOptions: [.unitTest],
dependencies: [.designSystem, .log, .combineCocoa, .trinet, .combineExtension, .coordinator, .commonNetworkingKeyManager],
dependencies: [
.designSystem,
.log,
.combineCocoa,
.trinet,
.combineExtension,
.coordinator,
.commonNetworkingKeyManager,
.downSampling,
],
testDependencies: []
)
)
66 changes: 66 additions & 0 deletions iOS/Projects/Features/Home/Sources/Data/FeedRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// FeedRepository.swift
// HomeFeature
//
// Created by MaraMincho on 12/7/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import CommonNetworkingKeyManager
import Foundation
import Trinet

// MARK: - FeedRepository

public struct FeedRepository: FeedRepositoryRepresentable {
let decoder = JSONDecoder()
let provider: TNProvider<FeedEndPoint>
init(session: URLSessionProtocol = URLSession.shared) {
provider = .init(session: session)
}

public func fetchFeed(at page: Int) -> AnyPublisher<[FeedElement], Never> {
return Future<[FeedElement], Error> { promise in
Task { [provider] in
do {
let data = try await provider.request(.fetchPosts(page: page), interceptor: TNKeychainInterceptor.shared)
let feedElementList = try decoder.decode([FeedElement].self, from: data)
promise(.success(feedElementList))
} catch {
promise(.failure(error))
}
}
}
.catch { _ in return Empty() }
.eraseToAnyPublisher()
}
}

// MARK: - FeedEndPoint

public enum FeedEndPoint: TNEndPoint {
case fetchPosts(page: Int)
public var path: String {
return ""
}

public var method: TNMethod {
return .post
}

public var query: Encodable? {
return nil
}

public var body: Encodable? {
switch self {
case let .fetchPosts(page):
return page
}
}

public var headers: TNHeaders {
return .default
}
}
9 changes: 0 additions & 9 deletions iOS/Projects/Features/Home/Sources/Data/HomeRepository.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

struct FeedElement: Hashable {
public struct FeedElement: Hashable, Codable {
/// 개시물의 아이디 입니다.
let ID: Int

Expand Down
60 changes: 60 additions & 0 deletions iOS/Projects/Features/Home/Sources/Domain/HomeUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,64 @@
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import Foundation

// MARK: - HomeUseCaseRepresentable

public protocol HomeUseCaseRepresentable {
func fetchFeed() -> AnyPublisher<[FeedElement], Never>
mutating func didDisplayFeed()
}

// MARK: - HomeUseCase

public struct HomeUseCase: HomeUseCaseRepresentable {
private let feedRepositoryRepresentable: FeedRepositoryRepresentable

private var latestFeedPage = 0
private var feedElementPublisher: PassthroughSubject<[FeedElement], Never> = .init()
private let checkManager: FetchCheckManager = .init()

public init(feedRepositoryRepresentable: FeedRepositoryRepresentable) {
self.feedRepositoryRepresentable = feedRepositoryRepresentable
}

public func fetchFeed() -> AnyPublisher<[FeedElement], Never> {
if checkManager[latestFeedPage] == false {
return Empty().eraseToAnyPublisher()
}
checkManager[latestFeedPage] = true
return feedElementPublisher.eraseToAnyPublisher()
}

public mutating func didDisplayFeed() {
latestFeedPage += 1
}
}

// MARK: - FetchCheckManager

final class FetchCheckManager {
subscript(page: Int) -> Bool {
get {
return check(at: page)
}
set {
set(at: page)
}
}

private var requestedFetchPageNumber: [Int: Bool] = [:]
/// FetchCheckManager를 통해서 특정 페이지에 대해 요청을 했다고 저장합니다.
private func set(at page: Int) {
requestedFetchPageNumber[page] = true
}

/// FetchCheckManager를 통해서, 특정 페이지에 대한 과거 요청에 대해서 살펴 봅니다.
///
/// 만약 요청한 적이 없다면 false를, 요청을 했다면 true를 리턴
private func check(at page: Int) -> Bool {
return requestedFetchPageNumber[page] ?? false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// FeedRepositoryRepresentable.swift
// HomeFeature
//
// Created by MaraMincho on 1/2/24.
// Copyright © 2024 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import Foundation

// MARK: - FeedRepositoryRepresentable

public protocol FeedRepositoryRepresentable {
func fetchFeed(at page: Int) -> AnyPublisher<[FeedElement], Never>
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ public final class HomeCoordinator: HomeCoordinating {
}

public func pushHome() {
let viewModel = HomeViewModel()
let repository = FeedRepository(session: URLSession.shared)

let useCase = HomeUseCase(feedRepositoryRepresentable: repository)

let viewModel = HomeViewModel(useCase: useCase)

let viewController = HomeViewController(viewModel: viewModel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Cacher
import UIKit

// MARK: - FeedImageCell
Expand All @@ -24,6 +25,12 @@ final class FeedImageCell: UICollectionViewCell {
fatalError("cant use this init")
}

override func prepareForReuse() {
super.prepareForReuse()

feedImage.image = nil
}

private func setupViewHierarchyAndConstraints() {
contentView.addSubview(feedImage)
feedImage.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
Expand All @@ -43,13 +50,15 @@ final class FeedImageCell: UICollectionViewCell {
guard let imageURL else {
return
}

DispatchQueue.global().async {
guard let data = try? Data(contentsOf: imageURL) else { return }
DispatchQueue.main.async { [weak self] in
self?.feedImage.image = UIImage(data: data)
self?.layoutIfNeeded()
guard let data = MemoryCacheManager.shared.fetch(cacheKey: imageURL.absoluteString) else {
DispatchQueue.global().async {
guard let data = try? Data(contentsOf: imageURL) else { return }
DispatchQueue.main.async { [weak self] in
self?.feedImage.image = UIImage(data: data)
}
}
return
}
feedImage.image = UIImage(data: data)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ class FeedItemCollectionViewCell: UICollectionViewCell {
fatalError("생성할 수 없습니다.")
}

override func prepareForReuse() {
super.prepareForReuse()

profileImage.image = nil
}

// MARK: - Property

private var dataSource: UICollectionViewDiffableDataSource<Int, URL>? = nil
Expand Down Expand Up @@ -65,6 +71,7 @@ class FeedItemCollectionViewCell: UICollectionViewCell {
label.text = "2023.12.07"
label.font = .preferredFont(forTextStyle: .subheadline)
label.textColor = DesignSystemColor.primaryText
label.contentMode = .scaleAspectFit

label.translatesAutoresizingMaskIntoConstraints = false
return label
Expand Down Expand Up @@ -310,10 +317,10 @@ extension FeedItemCollectionViewCell {
return
}

let url = url.compactMap { $0 }
snapshot.deleteAllItems()
snapshot.appendSections([0])
let url = url.compactMap { $0 }
snapshot.appendItems(Array(Set(url)), toSection: 0)
snapshot.appendItems(url, toSection: 0)
dataSource?.apply(snapshot)
}
}
Expand Down
Loading

0 comments on commit e8cb6f6

Please sign in to comment.