iOS/흡구오디 -> 어딨쥐

[흡구오디] 흡연구역 정보를 띄우기 위한 bottomSheet

23g 2025. 8. 31.

ㅎㅇ여

 

이번에는 마커를 클릭했을 때 하단의 정보창을 띄우기 위한 과정을 포스팅 해볼게요

 

찾아보니 이걸 bottomSheet라고 하더라구요?

 

apple 공식 문서에서는 아래와 같은 코드를 제시함

func showMyViewControllerInACustomizedSheet() {
    let viewControllerToPresent = MyViewController()
    
    // 1. sheetPresentationController 가져오기
    if let sheet = viewControllerToPresent.sheetPresentationController {
        
        // 2. detents: 시트 높이 단계 지정
        sheet.detents = [.medium(), .large()]
        //   → medium: 화면 반쯤
        //   → large: 화면 꽉 채우기
        
        // 3. dimming(뒤 배경 어둡게) 설정
        sheet.largestUndimmedDetentIdentifier = .medium
        //   → medium 크기일 땐 뒤 배경 어두워지지 않음
        //   → large 로 올리면 어두워짐
        
        // 4. 스크롤 확장 관련 옵션
        sheet.prefersScrollingExpandsWhenScrolledToEdge = false
        //   → 스크롤 뷰 맨 위에서 더 끌어도 자동 확장 안 되게
        
        // 5. compact height (아이폰 가로 모드 같은 상황)에서
        //    화면 하단에 붙을지 여부
        sheet.prefersEdgeAttachedInCompactHeight = true
        
        // 6. compact 환경일 때, 시트가 가로 폭에 맞게 붙을지 여부
        sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
    }
    
    // 7. 시트 띄우기
    present(viewControllerToPresent, animated: true, completion: nil)
}

 

주석은 제가 코드 파악하기 위해서 달아놓음

 

저는 미듐, 라지가 아닌 높이의 30%정도만 되는 바텀 시트를 정보창으로 띄우고 싶단 말이죠?

그럼 어케 해야할까

 

https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/detents

 

detents | Apple Developer Documentation

The array of heights where a sheet can rest.

developer.apple.com

 

private func showMyViewControllerInACustomizedSheet() {
    let viewControllerToPresent = SmokingAreaBottomSheetViewController()
    if let sheet = viewControllerToPresent.sheetPresentationController {
      let customDetent = UISheetPresentationController.Detent.custom(identifier: .init("thirtyPercent")) { context in
        return context.maximumDetentValue * 0.3
      }
      
      sheet.detents = [customDetent, .large()]
      sheet.selectedDetentIdentifier = .init("thirtyPercent") // 처음 뜰 때 30%로
      sheet.largestUndimmedDetentIdentifier = .large
      sheet.prefersScrollingExpandsWhenScrolledToEdge = false
      sheet.prefersEdgeAttachedInCompactHeight = true
      sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
    }
    present(viewControllerToPresent, animated: true, completion: nil)
  }

 

이렇게 커스텀으로 만들 수가 있다네요...

솔직히 짚히티의 도움 없이는 못햇을..^^

이렇게 뜹니다요

 

바텀 시트는 일단 그냥 빨간화면으로,,

 

 

아래는 마커 터치 이벤트가 발생하면 바텀 시트를 띄우는 코드인데요 by Gemini

요즘 공부 중인 observer / observable / subject / bind 등등에 대한 이해가 필요해서 일단 코드만 팁해두겠음..

import CoreLocation
import FirebaseCore
import FirebaseFirestore
import NMapsMap
import RxSwift
import RxCocoa
import SnapKit
import Then

import UIKit


final class HomeViewController: UIViewController {
    
    // MARK: Constant
    
    private enum Metric {
        static let addButtonTrailing: CGFloat = 24
        static let addButtonBottom: CGFloat = 40
    }
    
    
    // MARK: UI
    
    private let mapView = NMFNaverMapView()
    private let addButton = UIButton().then {
        $0.setImage(UIImage(named: "plusButton"), for: .normal)
    }
    
    // MARK: Property
    
    private let db = Firestore.firestore()
    
    private let locationManager = CLLocationManager()
    
    // 지도에 표시된 마커들을 관리하기 위한 배열
    private var markers: [NMFMarker] = []
    
    private let markerTapped = PublishSubject<(name: String, description: String)>()
    
    private let disposeBag = DisposeBag()
    
    
    // MARK: LifeCycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setup()
        self.addSubviews()
        self.makeConstraints()
        
        self.setLocationManager()
        
        self.bind()
        self.smokingAreas()
    }
    
    
    // MARK: Setup
    
    private func setup() { self.navigationItem.title = "Home" }
    
    private func addSubviews() {
        self.view.addSubview(self.mapView)
        self.view.addSubview(self.addButton)
    }
    
    private func makeConstraints() {
        self.mapView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
        
        self.addButton.snp.makeConstraints {
            $0.trailing.equalToSuperview().inset(Metric.addButtonTrailing)
            $0.bottom.equalToSuperview().inset(Metric.addButtonBottom)
        }
    }
    
    
    // MARK: Action
    
    private func bind() {
        // addButton 탭 이벤트 구독
        self.addButton.rx.tap
            .subscribe(onNext : { [weak self] in
                let markerPositionSeletorVC = MarkerPositionSelectorViewController()
                self?.navigationController?.pushViewController(markerPositionSeletorVC, animated: true)
            })
            .disposed(by: self.disposeBag)
        
        // markerTapped Subject 구독
        self.markerTapped
            .subscribe(onNext: { [weak self] (name, description) in
                self?.showBottomSheet(name: name, description: description)
            })
            .disposed(by: self.disposeBag)
    }
    
    
    // MARK: Area Marker
    
    private func smokingAreas() {
        db.collection("smokingAreas").addSnapshotListener { [weak self] snapshot, error in
            guard let self = self, let snapshot = snapshot else {
                // TODO: Handle error
                return
            }
            
            // 기존 마커들을 지도에서 제거하고 배열을 비웁니다.
            for marker in self.markers {
                marker.mapView = nil
            }
            self.markers.removeAll()
            
            for doc in snapshot.documents {
                let data = doc.data()
                guard let name = data["name"] as? String,
                      let description = data["description"] as? String,
                      let areaLat = data["areaLat"] as? Double,
                      let areaLng = data["areaLng"] as? Double else { continue }
                
                let areaMarker = NMFMarker()
                areaMarker.iconImage = NMFOverlayImage(name: "marker_Pin")
                areaMarker.position = NMGLatLng(lat: areaLat, lng: areaLng)
                
                // 마커 터치 시 markerTapped Subject에 데이터를 담아 onNext 이벤트를 보냅니다.
                areaMarker.touchHandler = { (overlay) -> Bool in
                    self.markerTapped.onNext((name: name, description: description))
                    return true
                }
                
                areaMarker.mapView = self.mapView.mapView
                // 새로 생성된 마커를 배열에 추가하여 관리합니다.
                self.markers.append(areaMarker)
            }
        }
    }
    
    private func showBottomSheet(name: String, description: String) {
        let bottomSheetVC = SmokingAreaBottomSheetViewController()
        
        // 데이터를 바텀 시트 ViewController에 전달합니다.
        bottomSheetVC.name = name
        bottomSheetVC.descriptionText = description
        
        if let sheet = bottomSheetVC.sheetPresentationController {
            let customDetent = UISheetPresentationController.Detent.custom(identifier: .init("thirtyPercent")) { context in
                return context.maximumDetentValue * 0.3
            }
            
            sheet.detents = [customDetent, .large()]
            sheet.selectedDetentIdentifier = .init("thirtyPercent")
            sheet.largestUndimmedDetentIdentifier = .large
            sheet.prefersScrollingExpandsWhenScrolledToEdge = false
            sheet.prefersEdgeAttachedInCompactHeight = true
            sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
        }
        present(bottomSheetVC, animated: true, completion: nil)
    }
}


// MARK: Location / Camera
extension HomeViewController: CLLocationManagerDelegate {
    
    private func setLocationManager() {
        self.locationManager.delegate = self
        self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
        self.locationManager.distanceFilter = kCLDistanceFilterNone
        self.locationManager.activityType = .otherNavigation
        self.locationManager.pausesLocationUpdatesAutomatically = false
        self.locationManager.requestWhenInUseAuthorization()
        self.locationManager.startUpdatingLocation()
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let bestLocation = locations.last else { return }
        let userLat = bestLocation.coordinate.latitude
        let userLng = bestLocation.coordinate.longitude
        
        print("1. 사용자의 위치 : (\(userLat), \(userLng))")
        
        self.cameraUpdate(lat: userLat, lng: userLng)
        
        // 처음 위치를 잡은 후, 위치 업데이트를 중단하여 불필요한 카메라 이동을 방지합니다.
        self.locationManager.stopUpdatingLocation()
    }
    
    private func cameraUpdate(lat: Double, lng: Double) {
        let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: lat, lng: lng))
        cameraUpdate.animation = .easeIn
        self.mapView.mapView.moveCamera(cameraUpdate)
    }
}

 

아이고 어려워

댓글