Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GWL-407] Refresh기능 구현 #447

Merged
merged 15 commits into from
Jan 16, 2024
Merged
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
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()
}
}