Skip to content

Commit

Permalink
[GWL-407] Refresh기능 구현 (#447)
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 폴더 제거

* feat: UseCase, Repository에 refresh Feed 비지니스 로직 작성

* move: 파일 분리

* feat: 리프래시 기능 구현
  • Loading branch information
MaraMincho authored Jan 16, 2024
1 parent fe97d38 commit 7c7b730
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 23 deletions.
23 changes: 22 additions & 1 deletion iOS/Projects/Features/Home/Sources/Data/FeedRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import Trinet
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
Task {
do {
let data = try await provider.request(.fetchPosts(page: page), interceptor: TNKeychainInterceptor.shared)
let feedElementList = try decoder.decode([FeedElement].self, from: data)
Expand All @@ -35,12 +36,30 @@ public struct FeedRepository: FeedRepositoryRepresentable {
.catch { _ in return Empty() }
.eraseToAnyPublisher()
}

public func refreshFeed() -> AnyPublisher<[FeedElement], Never> {
return Future<[FeedElement], Error> { promise in
Task { [provider] in
do {
let data = try await provider.request(.refreshFeed, 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)
case refreshFeed

public var path: String {
return ""
}
Expand All @@ -57,6 +76,8 @@ public enum FeedEndPoint: TNEndPoint {
switch self {
case let .fetchPosts(page):
return page
case .refreshFeed:
return nil
}
}

Expand Down
7 changes: 6 additions & 1 deletion iOS/Projects/Features/Home/Sources/Domain/HomeUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Foundation

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

Expand All @@ -34,7 +35,11 @@ public struct HomeUseCase: HomeUseCaseRepresentable {
return Empty().eraseToAnyPublisher()
}
checkManager[latestFeedPage] = true
return feedElementPublisher.eraseToAnyPublisher()
return feedRepositoryRepresentable.fetchFeed(at: latestFeedPage)
}

public func refreshFeed() -> AnyPublisher<[FeedElement], Never> {
return feedRepositoryRepresentable.refreshFeed()
}

public mutating func didDisplayFeed() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ import Foundation

public protocol FeedRepositoryRepresentable {
func fetchFeed(at page: Int) -> AnyPublisher<[FeedElement], Never>
func refreshFeed() -> AnyPublisher<[FeedElement], Never>
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Cacher
import ImageDownsampling
import UIKit

// MARK: - FeedImageCell
Expand Down Expand Up @@ -55,6 +56,7 @@ final class FeedImageCell: UICollectionViewCell {
guard let data = try? Data(contentsOf: imageURL) else { return }
DispatchQueue.main.async { [weak self] in
self?.feedImage.image = UIImage(data: data)
self?.layoutIfNeeded()
}
}
return
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 DesignSystem
import UIKit

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// HomeViewController+CompositionlLayout.swift
// HomeFeature
//
// Created by MaraMincho on 1/3/24.
// Copyright © 2024 kr.codesquad.boostcamp8. All rights reserved.
//

import UIKit

extension HomeViewController {
static func makeFeedCollectionViewLayout() -> UICollectionViewCompositionalLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 9, leading: 0, bottom: 9, trailing: 0)

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(455))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])

let section = NSCollectionLayoutSection(group: group)

return UICollectionViewCompositionalLayout(section: section)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Combine
import CombineCocoa
import DesignSystem
import Log
import UIKit
Expand All @@ -24,6 +25,7 @@ final class HomeViewController: UIViewController {

private let fetchFeedPublisher: PassthroughSubject<Void, Never> = .init()
private let didDisplayFeedPublisher: PassthroughSubject<Void, Never> = .init()
private let refreshFeedPublisher: PassthroughSubject<Void, Never> = .init()

private var feedCount: Int = 0

Expand Down Expand Up @@ -89,6 +91,7 @@ private extension HomeViewController {
setupHierarchyAndConstraints()
setNavigationItem()
bind()
configureRefreshControl()
fetchFeedPublisher.send()
}

Expand Down Expand Up @@ -134,7 +137,8 @@ private extension HomeViewController {
let output = viewModel.transform(
input: HomeViewModelInput(
requestFeedPublisher: fetchFeedPublisher.eraseToAnyPublisher(),
didDisplayFeed: didDisplayFeedPublisher.eraseToAnyPublisher()
didDisplayFeed: didDisplayFeedPublisher.eraseToAnyPublisher(),
refreshFeedPublisher: refreshFeedPublisher.eraseToAnyPublisher()
)
)

Expand All @@ -144,6 +148,8 @@ private extension HomeViewController {
break
case let .fetched(feed):
self?.updateFeed(feed)
case let .refresh(feed):
self?.refreshFeed(feed)
}
}
.store(in: &subscriptions)
Expand All @@ -154,6 +160,20 @@ private extension HomeViewController {
navigationItem.leftBarButtonItem = titleBarButtonItem
}

func refreshFeed(_ item: [FeedElement]) {
guard let dataSource else {
return
}
var snapshot = dataSource.snapshot()
snapshot.deleteAllItems()
snapshot.appendSections([0])
snapshot.appendItems(item)
DispatchQueue.main.async { [weak self] in
dataSource.apply(snapshot)
self?.feedListCollectionView.refreshControl?.endRefreshing()
}
}

func updateFeed(_ item: [FeedElement]) {
guard let dataSource else {
return
Expand All @@ -168,35 +188,30 @@ private extension HomeViewController {
feedCount = snapshot.numberOfItems
}

func configureRefreshControl() {
// Add the refresh control to your UIScrollView object.
feedListCollectionView.refreshControl = UIRefreshControl()
feedListCollectionView.refreshControl?
.publisher(.valueChanged)
.sink { [weak self] _ in
self?.refreshFeedPublisher.send()
}
.store(in: &subscriptions)
}

enum Constants {
static let navigationTitleText = ""
}

enum Metrics {}
}

private extension HomeViewController {
static func makeFeedCollectionViewLayout() -> UICollectionViewCompositionalLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 9, leading: 0, bottom: 9, trailing: 0)

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(455))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])

let section = NSCollectionLayoutSection(group: group)

return UICollectionViewCompositionalLayout(section: section)
}
}

// MARK: UICollectionViewDelegate

extension HomeViewController: UICollectionViewDelegate {
func collectionView(_: UICollectionView, willDisplay _: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// 사용자가 아직 보지 않은 셀의 갯수
let toShowCellCount = (feedCount - 1) - indexPath.row
if toShowCellCount < 3 {
// 만약 셀이 모자르다면 요청을 보냄
if (feedCount - 1) - indexPath.row < 3 {
fetchFeedPublisher.send()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Foundation
public struct HomeViewModelInput {
let requestFeedPublisher: AnyPublisher<Void, Never>
let didDisplayFeed: AnyPublisher<Void, Never>
let refreshFeedPublisher: AnyPublisher<Void, Never>
}

public typealias HomeViewModelOutput = AnyPublisher<HomeState, Never>
Expand All @@ -23,6 +24,7 @@ public typealias HomeViewModelOutput = AnyPublisher<HomeState, Never>
public enum HomeState {
case idle
case fetched(feed: [FeedElement])
case refresh(feed: [FeedElement])
}

// MARK: - HomeViewModelRepresentable
Expand Down Expand Up @@ -52,7 +54,7 @@ extension HomeViewModel: HomeViewModelRepresentable {

let fetched: HomeViewModelOutput = input.requestFeedPublisher
.flatMap { [useCase] _ in
useCase.fetchFeed()
return useCase.fetchFeed()
}
.map { feed in
return HomeState.fetched(feed: feed)
Expand All @@ -65,9 +67,18 @@ extension HomeViewModel: HomeViewModelRepresentable {
}
.store(in: &subscriptions)

let refreshed: HomeViewModelOutput = input.refreshFeedPublisher
.flatMap { [useCase] _ in
return useCase.refreshFeed()
}
.map { feed in
return HomeState.refresh(feed: feed)
}
.eraseToAnyPublisher()

let initialState: HomeViewModelOutput = Just(.idle).eraseToAnyPublisher()

return initialState.merge(with: fetched)
return initialState.merge(with: fetched, refreshed)
.eraseToAnyPublisher()
}
}

0 comments on commit 7c7b730

Please sign in to comment.