iOS/흡구오디 -> 어딨쥐

[흡구오디] 지도 위 정보를 띄우는 '바텀 시트' 만들기 (feat. FloatingPanel)

23g 2025. 9. 5.

안냐세요

바텀시트 완성해 왔서요

제가 원하는 기능은 지도 위의 특정 마커를 터치하면 그 장소에 관한 정보를 바텀 시트로 띄워주는 것이었어요

내 추구미 네이버 지도

도전기

1. 커스텀 바텀 시트 구현

: 커스텀 해줘야 할거 너무 많아서 포기

예를들면 화면을 터치하면 얼만큼 내려오고,

마커를 터치하면 얼만큼 올라오고 이런 것들을 다 계산해서 코드로 지정해줘야 해서

넘나 복잡한 것

기각!

2. PanModal 라이브러리 사용

: 쉽게 바텀 시트를 구현할 수 있는 유용한 라이브러리 인데요

얘는 바텀시트가 올라왔을때 뒤에 화면이 활성화가 안되서 기각

저는 바텀 시트 올라온 상태에서도 다른 마커 터치하면 그 마커에 대한 바텀 시트로 바껴야 하거등요

3. FloatingPanel 라이브러리 사용

: 결론적으로 얘를 사용했구요

위에서 말한 제 요구사항을 만족해서 이걸 사용 함

https://github.com/scenee/FloatingPanel

 

GitHub - scenee/FloatingPanel: A clean and easy-to-use floating panel UI component for iOS

A clean and easy-to-use floating panel UI component for iOS - scenee/FloatingPanel

github.com

 

FloatinfPanel을 이용한 바텀 시트 구현기

그럼 본격적으로 어떻게 구현했나 알아봅시다.

1. FloatingPanel 라이브러리 import

먼저, 라이브러리를 프로젝트에 추가해야 합니다.

전 SDK로 깔고

import FloatingPanel

ㄱㄱ

 

2. 두 개의 ViewController 준비

바텀 시트 구현의 핵심은 두 개의 ViewController를 사용하는 것!

아무래도 띄울 화면 하나, 띄워질 화면 하나 웅

  1. HomeViewController
    : 메인 화면입니다. 지도를 보여주고, 사용자의 인터랙션(마커 탭 등)을 감지하는 역할을 합니다.
  2. SmokingAreaBottomSheetViewController
    : 바텀 시트의 내용물입니다. 마커에 해당하는 상세 정보를 보여주는 UI를 담고 있습니다.

HomeViewController가 FloatingPanelController라는 '틀'을 만들고,
그 안에 SmokingAreaBottomSheetViewController라는 '내용물'을 쏙 집어넣는 구조인 것임

 

3. HomeViewController: 바텀 시트의 '틀' 만들기

HomeViewController에서 바텀 시트를 어떻게 설정하고 보여주는지 단계별로 살펴봅시다

 

1단계: 바텀 시트 초기 설정 (showBottomSheet 함수)

viewDidLoad에서 호출되는 이 함수는 바텀 시트를 만들고 초기화하는 역할을 함

 

전 extension으로 따로 뺌

// HomeViewController.swift

extension HomeViewController: FloatingPanelControllerDelegate {
  func showBottomSheet() {
    // 1. FloatingPanelController 인스턴스를 생성합니다. 이거시 바로 바텀 시트의 '본체'
    floatingPanelController = FloatingPanelController()

    // 2. 바텀 시트의 외형을 커스텀 (모서리 둥글게)
    floatingPanelController?.surfaceView.layer.cornerRadius = 15
    floatingPanelController?.surfaceView.layer.masksToBounds = true

    // 3. delegate를 self로 설정하여 바텀 시트의 움직임 등을 감지할 수 있게 함
    floatingPanelController?.delegate = self

    // 4. 가장 중요! 바텀 시트 안에 보여줄 '내용물' ViewController를 지정 (set)
    floatingPanelController?.set(contentViewController: smokingAreaBottomSheetVC)

    // 5. 현재 ViewController(HomeViewController)에 바텀 시트를 추가
    floatingPanelController?.addPanel(toParent: self)

    // 6. 처음에는 보이지 않도록 '숨김(.hidden)' 상태로 설정
    floatingPanelController?.move(to: .hidden, animated: true)
  }
}

 

인스턴스 생성 -> 델리게이트 설정 -> 바텀 시트 지정 -> 바텀 시트 추가 -> 초기 상태 설정

 

솔직히 잘 이해안되는데요

이해하는게 아니라 이렇게 하는거라고 하니까,,,그런갑다

 

저는 처음에는 아예 안보였으면 좋겠어서 hidden 처리함

 

.tip이나 .half도 있어요

2단계: 마커 탭과 바텀 시트 연결 (bind 함수)

사용자가 마커를 탭 했을 때, 바텀 시트가 '짠'하고 나타나야 합니다.

이 연결고리는 요즘 공부 중인 RxSwift를 사용해볼게요 (어려어)

 

  1. 사용자가 마커를 탭 하면 markerTapped라는 PublishSubject가
    해당 마커의 SmokingArea 데이터를 담아 이벤트를 방출!
    (smokingAreas 함수 내 touchHandler 참고)
    참고 : touchHandler는 네이버 공식 문서에서 발췌함
  2. bind 함수는 이 이벤트를 구독(subscribe) 하고 있다가, 데이터가 들어오면 행동 ㄱㄱ
// HomeViewController.swift

private func bind() {
    // markerTapped 이벤트를 구독
    markerTapped
        .subscribe(onNext: { [weak self] areaData in // 데이터(areaData)가 들어왔을 때 실행
            guard let self = self else { return }

            // 1. 바텀 시트 내용물(VC)에 데이터를 전달하여 UI를 업데이트
            self.smokingAreaBottomSheetVC.configure(with: areaData)

            // 2. 숨겨져 있던 바텀 시트를 중간(.half) 높이까지 올림
            self.floatingPanelController?.move(to: .half, animated: true)
        })
        .disposed(by: disposeBag) // 메모리 관리를 위해 disposeBag에 추가
}

 

정리하자면,

마커 탭 → markerTapped 이벤트 발생 → bind 함수가 감지 → 바텀 시트 내용 업데이트 후 화면에 표시의 흐름

 

아이고 어렵다

4. SmokingAreaBottomSheetViewController: '내용물' 채우기

이제 바텀 시트 안에 들어갈 SmokingAreaBottomSheetViewController를 살펴보아요

이 ViewController의 역할은 데이터를 받아 화면을 그리는 것!

configure(with data: SmokingArea) 함수

아래는 HomeViewController로부터 SmokingArea 데이터를 전달받는 핵심 메서드임

// SmokingAreaBottomSheetViewController.swift

public func configure(with data: SmokingArea) {
  DispatchQueue.main.async { // UI 업데이트는 반드시 메인 스레드에서!
    // 1. 전달받은 데이터로 이름과 설명 레이블의 text를 설정
    self.nameLabel.text = data.name
    self.descriptionLabel.text = data.description

    // 2. 이전에 표시되던 태그가 있다면 모두 제거
    for section in self.tagSections {
      section.removeFromSuperview()
    }
    self.tagSections.removeAll()

    // 3. 새로운 데이터로 태그 섹션(환경, 유형, 시설)을 다시 만듦
    let envSection = self.makeTagSection(title: "환경", tags: data.selectedEnvironmentTags)
    let typeSection = self.makeTagSection(title: "유형", tags: data.selectedTypeTags)
    let facilitySection = self.makeTagSection(title: "시설", tags: data.selectedFacilityTags)

    self.tagSections = [envSection, typeSection, facilitySection]

    // 4. 새로 만든 태그 섹션들을 화면에 추가
    for section in self.tagSections {
      self.rootFlexContainer.flex.addItem(section).marginTop(20)
    }

    // 5. FlexLayout을 사용하여 레이아웃을 다시 계산하고 적용
    self.rootFlexContainer.flex.layout()
  }
}

 

태그들 만들고 추가하고 관리하느라 겁나 복잡해졌는데요

 

어쨋든 큰 틀은 "데이터 연결 -> (태그들 만들고) -> 다시 띄워 -> 지워 -> 다시 만들어 ..."를 메인 쓰레드에서 해!!!

 

이거예요

아닌가..

 

하여간 이 configure 메서드 덕분에 SmokingAreaBottomSheetViewController는 어떤 데이터가 들어오든 유연하게 안의 내용을 바꿀 수 있습니다.

 

5. 전체 흐름 다시 보기

  1. HomeViewController: showBottomSheet()으로 바텀 시트의 '틀'을 미리 만들어 둠
  2. 사용자: 지도 위의 마커를 탭!
  3. HomeViewController: 마커 탭을 감지하고, bind() 함수를 통해 바텀 시트의 '내용물'인 smokingAreaBottomSheetVC에게 configure(with:) 메서드로 마커 정보를 전달
  4. SmokingAreaBottomSheetViewController: 전달받은 정보로 자신의 UI(이름, 설명, 태그 등)를 업데이트
  5. HomeViewController: 내용이 채워진 바텀 시트를 move(to: .half) 명령으로 화면에 나타나게 함
  6. 사용자: 지도를 탭 하면 didTapMap 델리게이트 메서드가 호출되어 바텀 시트가 다시 아래로 사라짐 ㅃㅃ

대박 복잡하쥬

 

솔직히 저 다는 이해못함 ㅠㅠ

하지만 이것만 붙잡고 있을 순 없으니께

 

이렇게 글로 정리 함 하고 넘어가 볼게요...

그렇게 완성된 모습!!!

 

 

바텀 시트도 바텀 시트인데 flexlayout 땜에 미쳐돌뻔함

 

그럼,,,안녕히

댓글