Skip to content

Commit 7c7b730

Browse files
authored
[GWL-407] Refresh기능 구현 (#447)
* 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: 리프래시 기능 구현
1 parent fe97d38 commit 7c7b730

File tree

8 files changed

+103
-23
lines changed

8 files changed

+103
-23
lines changed

iOS/Projects/Features/Home/Sources/Data/FeedRepository.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ import Trinet
1616
public struct FeedRepository: FeedRepositoryRepresentable {
1717
let decoder = JSONDecoder()
1818
let provider: TNProvider<FeedEndPoint>
19+
1920
init(session: URLSessionProtocol = URLSession.shared) {
2021
provider = .init(session: session)
2122
}
2223

2324
public func fetchFeed(at page: Int) -> AnyPublisher<[FeedElement], Never> {
2425
return Future<[FeedElement], Error> { promise in
25-
Task { [provider] in
26+
Task {
2627
do {
2728
let data = try await provider.request(.fetchPosts(page: page), interceptor: TNKeychainInterceptor.shared)
2829
let feedElementList = try decoder.decode([FeedElement].self, from: data)
@@ -35,12 +36,30 @@ public struct FeedRepository: FeedRepositoryRepresentable {
3536
.catch { _ in return Empty() }
3637
.eraseToAnyPublisher()
3738
}
39+
40+
public func refreshFeed() -> AnyPublisher<[FeedElement], Never> {
41+
return Future<[FeedElement], Error> { promise in
42+
Task { [provider] in
43+
do {
44+
let data = try await provider.request(.refreshFeed, interceptor: TNKeychainInterceptor.shared)
45+
let feedElementList = try decoder.decode([FeedElement].self, from: data)
46+
promise(.success(feedElementList))
47+
} catch {
48+
promise(.failure(error))
49+
}
50+
}
51+
}
52+
.catch { _ in return Empty() }
53+
.eraseToAnyPublisher()
54+
}
3855
}
3956

4057
// MARK: - FeedEndPoint
4158

4259
public enum FeedEndPoint: TNEndPoint {
4360
case fetchPosts(page: Int)
61+
case refreshFeed
62+
4463
public var path: String {
4564
return ""
4665
}
@@ -57,6 +76,8 @@ public enum FeedEndPoint: TNEndPoint {
5776
switch self {
5877
case let .fetchPosts(page):
5978
return page
79+
case .refreshFeed:
80+
return nil
6081
}
6182
}
6283

iOS/Projects/Features/Home/Sources/Domain/HomeUseCase.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Foundation
1313

1414
public protocol HomeUseCaseRepresentable {
1515
func fetchFeed() -> AnyPublisher<[FeedElement], Never>
16+
func refreshFeed() -> AnyPublisher<[FeedElement], Never>
1617
mutating func didDisplayFeed()
1718
}
1819

@@ -34,7 +35,11 @@ public struct HomeUseCase: HomeUseCaseRepresentable {
3435
return Empty().eraseToAnyPublisher()
3536
}
3637
checkManager[latestFeedPage] = true
37-
return feedElementPublisher.eraseToAnyPublisher()
38+
return feedRepositoryRepresentable.fetchFeed(at: latestFeedPage)
39+
}
40+
41+
public func refreshFeed() -> AnyPublisher<[FeedElement], Never> {
42+
return feedRepositoryRepresentable.refreshFeed()
3843
}
3944

4045
public mutating func didDisplayFeed() {

iOS/Projects/Features/Home/Sources/Domain/RepositoryInterface/FeedRepositoryRepresentable.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ import Foundation
1313

1414
public protocol FeedRepositoryRepresentable {
1515
func fetchFeed(at page: Int) -> AnyPublisher<[FeedElement], Never>
16+
func refreshFeed() -> AnyPublisher<[FeedElement], Never>
1617
}

iOS/Projects/Features/Home/Sources/Presntaion/HomeScene/VIew/FeedImageCell.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import Cacher
10+
import ImageDownsampling
1011
import UIKit
1112

1213
// MARK: - FeedImageCell
@@ -55,6 +56,7 @@ final class FeedImageCell: UICollectionViewCell {
5556
guard let data = try? Data(contentsOf: imageURL) else { return }
5657
DispatchQueue.main.async { [weak self] in
5758
self?.feedImage.image = UIImage(data: data)
59+
self?.layoutIfNeeded()
5860
}
5961
}
6062
return

iOS/Projects/Features/Home/Sources/Presntaion/HomeScene/VIew/FeedItemCollectionViewCell.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
77
//
88

9+
import Cacher
910
import DesignSystem
1011
import UIKit
1112

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// HomeViewController+CompositionlLayout.swift
3+
// HomeFeature
4+
//
5+
// Created by MaraMincho on 1/3/24.
6+
// Copyright © 2024 kr.codesquad.boostcamp8. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
extension HomeViewController {
12+
static func makeFeedCollectionViewLayout() -> UICollectionViewCompositionalLayout {
13+
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
14+
let item = NSCollectionLayoutItem(layoutSize: itemSize)
15+
item.contentInsets = .init(top: 9, leading: 0, bottom: 9, trailing: 0)
16+
17+
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(455))
18+
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
19+
20+
let section = NSCollectionLayoutSection(group: group)
21+
22+
return UICollectionViewCompositionalLayout(section: section)
23+
}
24+
}

iOS/Projects/Features/Home/Sources/Presntaion/HomeScene/ViewController/HomeViewController.swift

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import Combine
10+
import CombineCocoa
1011
import DesignSystem
1112
import Log
1213
import UIKit
@@ -24,6 +25,7 @@ final class HomeViewController: UIViewController {
2425

2526
private let fetchFeedPublisher: PassthroughSubject<Void, Never> = .init()
2627
private let didDisplayFeedPublisher: PassthroughSubject<Void, Never> = .init()
28+
private let refreshFeedPublisher: PassthroughSubject<Void, Never> = .init()
2729

2830
private var feedCount: Int = 0
2931

@@ -89,6 +91,7 @@ private extension HomeViewController {
8991
setupHierarchyAndConstraints()
9092
setNavigationItem()
9193
bind()
94+
configureRefreshControl()
9295
fetchFeedPublisher.send()
9396
}
9497

@@ -134,7 +137,8 @@ private extension HomeViewController {
134137
let output = viewModel.transform(
135138
input: HomeViewModelInput(
136139
requestFeedPublisher: fetchFeedPublisher.eraseToAnyPublisher(),
137-
didDisplayFeed: didDisplayFeedPublisher.eraseToAnyPublisher()
140+
didDisplayFeed: didDisplayFeedPublisher.eraseToAnyPublisher(),
141+
refreshFeedPublisher: refreshFeedPublisher.eraseToAnyPublisher()
138142
)
139143
)
140144

@@ -144,6 +148,8 @@ private extension HomeViewController {
144148
break
145149
case let .fetched(feed):
146150
self?.updateFeed(feed)
151+
case let .refresh(feed):
152+
self?.refreshFeed(feed)
147153
}
148154
}
149155
.store(in: &subscriptions)
@@ -154,6 +160,20 @@ private extension HomeViewController {
154160
navigationItem.leftBarButtonItem = titleBarButtonItem
155161
}
156162

163+
func refreshFeed(_ item: [FeedElement]) {
164+
guard let dataSource else {
165+
return
166+
}
167+
var snapshot = dataSource.snapshot()
168+
snapshot.deleteAllItems()
169+
snapshot.appendSections([0])
170+
snapshot.appendItems(item)
171+
DispatchQueue.main.async { [weak self] in
172+
dataSource.apply(snapshot)
173+
self?.feedListCollectionView.refreshControl?.endRefreshing()
174+
}
175+
}
176+
157177
func updateFeed(_ item: [FeedElement]) {
158178
guard let dataSource else {
159179
return
@@ -168,35 +188,30 @@ private extension HomeViewController {
168188
feedCount = snapshot.numberOfItems
169189
}
170190

191+
func configureRefreshControl() {
192+
// Add the refresh control to your UIScrollView object.
193+
feedListCollectionView.refreshControl = UIRefreshControl()
194+
feedListCollectionView.refreshControl?
195+
.publisher(.valueChanged)
196+
.sink { [weak self] _ in
197+
self?.refreshFeedPublisher.send()
198+
}
199+
.store(in: &subscriptions)
200+
}
201+
171202
enum Constants {
172203
static let navigationTitleText = ""
173204
}
174205

175206
enum Metrics {}
176207
}
177208

178-
private extension HomeViewController {
179-
static func makeFeedCollectionViewLayout() -> UICollectionViewCompositionalLayout {
180-
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
181-
let item = NSCollectionLayoutItem(layoutSize: itemSize)
182-
item.contentInsets = .init(top: 9, leading: 0, bottom: 9, trailing: 0)
183-
184-
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(455))
185-
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
186-
187-
let section = NSCollectionLayoutSection(group: group)
188-
189-
return UICollectionViewCompositionalLayout(section: section)
190-
}
191-
}
192-
193209
// MARK: UICollectionViewDelegate
194210

195211
extension HomeViewController: UICollectionViewDelegate {
196212
func collectionView(_: UICollectionView, willDisplay _: UICollectionViewCell, forItemAt indexPath: IndexPath) {
197-
// 사용자가 아직 보지 않은 셀의 갯수
198-
let toShowCellCount = (feedCount - 1) - indexPath.row
199-
if toShowCellCount < 3 {
213+
// 만약 셀이 모자르다면 요청을 보냄
214+
if (feedCount - 1) - indexPath.row < 3 {
200215
fetchFeedPublisher.send()
201216
}
202217
}

iOS/Projects/Features/Home/Sources/Presntaion/HomeScene/ViewModel/HomeViewModel.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Foundation
1414
public struct HomeViewModelInput {
1515
let requestFeedPublisher: AnyPublisher<Void, Never>
1616
let didDisplayFeed: AnyPublisher<Void, Never>
17+
let refreshFeedPublisher: AnyPublisher<Void, Never>
1718
}
1819

1920
public typealias HomeViewModelOutput = AnyPublisher<HomeState, Never>
@@ -23,6 +24,7 @@ public typealias HomeViewModelOutput = AnyPublisher<HomeState, Never>
2324
public enum HomeState {
2425
case idle
2526
case fetched(feed: [FeedElement])
27+
case refresh(feed: [FeedElement])
2628
}
2729

2830
// MARK: - HomeViewModelRepresentable
@@ -52,7 +54,7 @@ extension HomeViewModel: HomeViewModelRepresentable {
5254

5355
let fetched: HomeViewModelOutput = input.requestFeedPublisher
5456
.flatMap { [useCase] _ in
55-
useCase.fetchFeed()
57+
return useCase.fetchFeed()
5658
}
5759
.map { feed in
5860
return HomeState.fetched(feed: feed)
@@ -65,9 +67,18 @@ extension HomeViewModel: HomeViewModelRepresentable {
6567
}
6668
.store(in: &subscriptions)
6769

70+
let refreshed: HomeViewModelOutput = input.refreshFeedPublisher
71+
.flatMap { [useCase] _ in
72+
return useCase.refreshFeed()
73+
}
74+
.map { feed in
75+
return HomeState.refresh(feed: feed)
76+
}
77+
.eraseToAnyPublisher()
78+
6879
let initialState: HomeViewModelOutput = Just(.idle).eraseToAnyPublisher()
6980

70-
return initialState.merge(with: fetched)
81+
return initialState.merge(with: fetched, refreshed)
7182
.eraseToAnyPublisher()
7283
}
7384
}

0 commit comments

Comments
 (0)