[TIL / 25.05.27] 날씨 앱 main 페이지 구조 트러블슈팅

2025. 5. 27. 02:43·iOS/Swift

1. 문제 상황

현재 기획 상 위 아래로 한페이지씩 pageable하게 구현함. 

mainPageViewController(UIPageViewController) 하위 뷰컨트롤러들에 위 아랫 UIViewController를 연결해둠.

근데 첫번째 페이지에서 화면을 아래로 잡아끌면 새로고침이 되어야함. PTR(Pull To Refresh)가 필요한데,,

 

문제의 현 구조. 대충 구조만 보고 넘겨주세요

class MainPageViewController: UIPageViewController {
    private let disposeBag = DisposeBag()
    private let viewModel: PageViewModel
    private let mainViewModel: MainViewModel
    private let mainDetailViewModel: MainDetailViewModel

    private lazy var mainVC = MainViewController(viewModel: self.mainViewModel)
    private lazy var mainDetailVC = MainDetailViewController(viewModel: self.mainDetailViewModel)
    private lazy var pages: [UIViewController] = [
        mainVC, mainDetailVC
    ]

    init(viewModel: PageViewModel,
         mainViewModel: MainViewModel,
         mainDetailViewModel: MainDetailViewModel) {
        self.viewModel = viewModel
        self.mainViewModel = mainViewModel
        self.mainDetailViewModel = mainDetailViewModel
        super.init(transitionStyle: .scroll, navigationOrientation: .vertical, options: nil)
        dataSource = self
        delegate = self
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented.")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .systemBackground

        setViewControllers([pages[0]], direction: .forward, animated: false)

        viewModel.currentPage
            .distinctUntilChanged()
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] index in
                guard let self, index >= 0, index < self.pages.count else { return }
                self.setViewControllers([self.pages[index]], direction: .forward, animated: true)
            }).disposed(by: disposeBag)
    }

}

extension MainPageViewController: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard let index = pages.firstIndex(of: viewController), index > 0 else { return nil }
        return pages[index - 1]
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let index = pages.firstIndex(of: viewController), index < pages.count - 1 else { return nil }
        return pages[index + 1]
    }
    
    func pageViewController(_ pageViewController: UIPageViewController,
                            didFinishAnimating finished: Bool,
                            previousViewControllers: [UIViewController],
                            transitionCompleted completed: Bool) {
        if completed,
           let visible = viewControllers?.first,
           let index = pages.firstIndex(of: visible) {
            viewModel.currentPage.accept(index)
        }
    }
}

 

아까 말했듯 viewWillAppear에서 처리되는 날씨 정보 조회 api 호출이 너무 빈번하기도 해서 더더욱 PTR 도입이 필요한 상황.

 

시도는 두 가지 방식으로 진행했음 ( 코드가 너무 많이 바뀌어서 해당 시점 코드가 없는...ㅠ )

 

1. MainPageViewController이 UIPageViewController라는 점을 이용해 가지고 있는 ScrollView를 이용해 refreshControl을 붙이자

2. MainViewController(첫 번째 페이지)의 뷰 계층 중 최상위를 ScrollView로 설정하여 refreshControl을 적용하자.

 

1번의 경우, UIPageVC의 ScrollView는 refreshControl이 적용되지 않는 문제가 있었음. 

2번의 경우, 기존 PageVC의 ScrollView에 있는 스크롤 감지와 새로 추가한 MainVC의 스크롤이 서로 충돌하여 원활한 구현이 힘들었음.

 

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}

 

해당 함수를 추가해서 겹치지 않게도 해봤으나 실패.

 

결국 PageViewController 대신 다른 구조를 택해야하나 고민 후 UIScrollView에 구현해둔 두 개의 UIViewController를 addChild해 관리하는 방향으로 설정 (기록 용으로 씁니다.. 현재 12:27.. 누가 죽나 보자..)

 

 

2.  문제 해결

 

현 시각 01:47..

생각 이상으로 빨리 끝나서 매우 당황중인데 gpt 도움 받아서 구조 파악하고 부분 문제점 찾아내는 데에 집중

퍼즐 조각들을 맞춰왔읍니다.

 

import UIKit

class MainPageViewController: UIViewController {
    private let scrollView = UIScrollView()
    private let contentView = UIView()

    private let mainViewModel: MainViewModel
    private let mainDetailViewModel: MainDetailViewModel
    private let viewModel: PageViewModel

    private lazy var mainVC = MainViewController(viewModel: self.mainViewModel)
    private lazy var mainDetailVC = MainDetailViewController(viewModel: self.mainDetailViewModel)

    private let refreshControl = UIRefreshControl()

    init(viewModel: PageViewModel,
         mainViewModel: MainViewModel,
         mainDetailViewModel: MainDetailViewModel) {
        self.viewModel = viewModel
        self.mainViewModel = mainViewModel
        self.mainDetailViewModel = mainDetailViewModel

        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented.")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        configure()
    }

}

private extension MainPageViewController {
    func configure() {
        setStyle()
        setHierarchy()
        setConstraints()
        setActions()
    }

    func setStyle() {
        view.backgroundColor = .systemBackground
        scrollView.isPagingEnabled = true
        navigationController?.setNavigationBarHidden(true, animated: false)
        scrollView.refreshControl = refreshControl
    }

    func setHierarchy() {
        view.addSubview(scrollView)
        scrollView.addSubview(contentView)

        [mainVC, mainDetailVC].forEach {
            addChild($0)
            $0.didMove(toParent: self)
        }
        contentView.addSubviews(views: mainVC.view, mainDetailVC.view)
    }

    func setConstraints() {
        scrollView.snp.makeConstraints {
            $0.top.equalTo(view.safeAreaLayoutGuide.snp.top)
            $0.directionalHorizontalEdges.bottom.equalToSuperview()
        }

        contentView.snp.makeConstraints {
            $0.edges.equalToSuperview()
            $0.width.equalToSuperview()
            $0.height.equalTo(scrollView.snp.height).multipliedBy(2)
        }

        mainVC.view.snp.makeConstraints {
            $0.top.directionalHorizontalEdges.equalToSuperview()
            $0.height.equalTo(scrollView.snp.height)
        }

        mainDetailVC.view.snp.makeConstraints {
            $0.top.equalTo(mainVC.view.snp.bottom)
            $0.directionalHorizontalEdges.bottom.equalToSuperview()
            $0.height.equalTo(scrollView.snp.height)
        }
    }

    func setActions() {
        refreshControl.addTarget(self, action: #selector(handleRefreshView), for: .valueChanged)
    }

    @objc func handleRefreshView() {
        mainVC.refresh()

        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.scrollView.refreshControl?.endRefreshing()
        }
    }
}

 

일단 UIViewController - UIScrollView - UIView(ContentView) 구성으로 구현했음

 

이미 구현해둔 ViewController 두개를 모두 가져와 써야하기에 꽤 전에 썼던 addChild, didMove 메서드를 써서 자식 뷰컨트롤러로 추가했고 이 자식 뷰컨들이 가지고 있는 view를 서브뷰로 추가 후 레이아웃 설정을 해줌

 

https://subkyu-ios.tistory.com/7

 

[TIL / 25.02.07] 라이브러리 사용해서 상단 탭바 구현하기 2

https://subkyu-ios.tistory.com/6 [TIL / 25.02.06] 라이브러리 사용해서 상단 탭바 구현하기 1개발 팀 애들끼리 개발 관련 포스트를 쓸 수 있게 블로그를 만들고 있는데 난 거기서 iOS를 담당한다.예전부터

subkyu-ios.tistory.com

 

이때 tabman 라이브러리 사용 시에도 addChild해서 넣어줬던 것 같은데 ..

 

쨋든 이렇게 자식 뷰컨으로 관리할 수 있게 되고 영역 자체도 잘 설정해줘서 pageable하게 만들어주면 됨..

(02:39분 버그 이제 다 고친듯..?)

 

어쨋든 page기능도 제공해주고 레이아웃 설정만 잘해두면 pageVC 크게 부러울 게 없어서 최상위를 UIScrollView로 그 하위에 두 페이지를 관리하는 방향으로 진행함

 

이게 두 페이지를 연동하면서 상단 safearea관련해서 bounce되면 레이아웃이 깨진다던가 문제가 많아서 결국 고치긴했는데.. 좀 더 초기에 안전한 방향으로 갈 수 있는 가능성이 있지않을까...

 

결론: PTR 방식을 쓸거면 웬만해선 UIPageViewController를 상위에 쓰지는 말아야겠다.

 

만약 다른 방법이 있다면 공유 부탁드려요 ㅠㅠ

 


 

해치웠나...? 제발..

'iOS > Swift' 카테고리의 다른 글

[25.06.24] MOUP 트러블슈팅 - Listener와 Rx의 timeout  (0) 2025.06.24
[TIL / 25.05.18] 의존성 주입 담당 DIContainer를 처음 적용해보았습니다  (4) 2025.05.18
[TIL / 25.05.13] BookSearchApp Lv 3 트러블슈팅 - Core Data 크래시  (0) 2025.05.14
[TIL / 25.05.12] BookSearchApp 트러블슈팅 - 일부 배경색이 투명한 현상  (2) 2025.05.12
[TIL / 25.05.11] RxSwift.. 처음 공부해볼게요 2  (0) 2025.05.11
'iOS/Swift' 카테고리의 다른 글
  • [25.06.24] MOUP 트러블슈팅 - Listener와 Rx의 timeout
  • [TIL / 25.05.18] 의존성 주입 담당 DIContainer를 처음 적용해보았습니다
  • [TIL / 25.05.13] BookSearchApp Lv 3 트러블슈팅 - Core Data 크래시
  • [TIL / 25.05.12] BookSearchApp 트러블슈팅 - 일부 배경색이 투명한 현상
subkyu-ios
subkyu-ios
subkyu-ios 님의 블로그 입니다.
  • subkyu-ios
    subkyu-ios 님의 블로그
    subkyu-ios
  • 전체
    오늘
    어제
    • 분류 전체보기 (53)
      • iOS (35)
        • Swift (35)
      • 내일배움캠프 (7)
      • Git, Github (3)
      • Algorithm (6)
      • 회고 (1)
      • 면접 질문 정리 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ios
    Wil
    의존성 주입
    stackview
    UIKit
    알고리즘
    til
    tabman
    트러블슈팅
    KPT
    회고
    TableView
    프로그래머스
    사전캠프
    내일배움캠프
    github
    RxSwift
    Swift
    algorithm
    본캠프
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
subkyu-ios
[TIL / 25.05.27] 날씨 앱 main 페이지 구조 트러블슈팅
상단으로

티스토리툴바