Skip to content

Conversation

@KuKaH
Copy link
Contributor

@KuKaH KuKaH commented Jan 9, 2026

✅ Check List

  • 팀원 전원의 Approve를 받은 후 머지해주세요.
  • 변경 사항은 500줄 이내로 유지해주세요.
  • Approve된 PR은 Assigner가 직접 머지해주세요.
  • 수정 요청이 있다면 반영 후 다시 push해주세요.

📌 Related Issue


📎 Work Description

ViewModel

protocol SearchPeopleViewModelProtocol: InputOutputProtocol where Input == SearchPeopleViewModel.Input, Output == SearchPeopleViewModel.Output {
    var people: [PeopleDTO] { get }
    var numberOfPeople: Int { get }
    func person(at index: Int) -> PeopleDTO?
}

이런식으로 ViewModelProtocol을 생성 후

class SearchPeopleViewModel: SearchPeopleViewModelProtocol {
    
    enum Input {
        case searchTextChanged(String)
        case scrollReachedBottom
    }
    
    struct Output {
        let people = PassthroughSubject<[PeopleDTO], Never>.init()
        let error = PassthroughSubject<Error, Never>.init()
        let isLoading = CurrentValueSubject<Bool, Never>.init(false)
    }
}

View모델이 채택하게 함

Input은 enum으로 Output은 struct로 했는데 Input도 struct로 하면 괜찮을 수도 있을 거 같음

func transform(input: AnyPublisher<Input, Never>) -> Output {
    
        input
            .filter { if case .searchTextChanged = $0 { return true }
            return false }
            .compactMap { if case .searchTextChanged(let text) = $0 { return text }
            return nil
            }
            .debounce(for: .seconds(0.3), scheduler: DispatchQueue.main)
            .removeDuplicates()
            .sink { [weak self] searchText in
                self?.handleSearchTextChanged(searchText)
            }
            .store(in: &cancellables)
        
        input
            .filter { if case .scrollReachedBottom = $0 { return true }; return false }
            .throttle(for: .seconds(0.3), scheduler: DispatchQueue.main, latest: true)
            .sink { [weak self] _ in
                self?.handleScrollReachedBottom()
            }
            .store(in: &cancellables)
        
        return output
    }

Input 은 enum으로 선언했기 때문에 filter를 통해서 각각 debouncethrottle은 걸어줌

ViewController

 private func bindOutput(_ output: SearchPeopleViewModel.Output ) {
        output.people
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.searchView.collectionView.reloadData()
            }
            .store(in: &cancellables)
        
        output.error
            .receive(on: DispatchQueue.main)
            .sink { [weak self] error in
                print("")
            }
            .store(in: &cancellables)
        
        output.isLoading
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isLoading in
                print("")
            }
            .store(in: &cancellables)
    }
    
    private func bindInput() {
        searchView.searchBar.textDidChangePublisher()
            .sink { [weak self] text in
                self?.input.send(.searchTextChanged(text))
            }
            .store(in: &cancellables)
        
        searchView.collectionView.reachedBottomPublisher
            .sink { [weak self] _ in
                self?.input.send(.scrollReachedBottom)
            }
            .store(in: &cancellables)
    }

Output을 struct로 선언했기 때문에 개별 스트림으로 구독이 가능함

extension UIScrollView {
    var reachedBottomPublisher: AnyPublisher<Void, Never> {
        return publisher(for: \.contentOffset)
            .map { [weak self] contentOffset -> Bool in
                guard let self = self else { return false }
                
                let offsetY = contentOffset.y
                let contentHeight = self.contentSize.height
                let height = self.frame.size.height
                
                return offsetY > contentHeight - height - 100
            }
            .removeDuplicates()
            .filter { $0 }
            .map { _ in () }
            .eraseToAnyPublisher()
    }
}

무한 스크롤 같은 경우는,
reachedBottomPublisher를 만들어서 사용함
(textDidChangePublisher를 만든 것과 비슷함)
UICollectionView, UITableView 모두 UIScrollView 를 상속 받기 때문에
UIScrollView에 extension으로 만들어두어서 스크롤 가능한 어느 곳에서든 구독할 수 있다.

승준이가 얘기했던 버벅이는 현상은
return offsetY > contentHeight - height - 100 처럼
미리 호출하는 방식으로 조금 해결했다.


📷 Screenshots

기능/화면 iPhone 11 Pro iPhone 16 Pro
A 기능

💬 To Reviewers

  • 주석은 시행착오 과정이라 남겨두면 좋을 거 같아서 일부러 남겼습니다-

@KuKaH KuKaH requested review from LJIN24 and Rudy-009 January 9, 2026 18:05
@KuKaH KuKaH self-assigned this Jan 9, 2026
@KuKaH KuKaH linked an issue Jan 9, 2026 that may be closed by this pull request
// }
//}

extension UIScrollView {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

진짜 너무 좋은 꿀코드~~

//
//}

protocol SearchPeopleViewModelProtocol: InputOutputProtocol where Input == SearchPeopleViewModel.Input, Output == SearchPeopleViewModel.Output {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POP 마스터 드립니다~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Combine 과제 4

3 participants