iOS/UIKIT

UISheetPresentationController.... TabBarController를 가린다...

밤새는 탐험가89 2024. 10. 10. 10:05

 

dayTrip이라는 어플에서 Map 검색 부분 처럼 UI를 구성하려고 했습니다. 

찾아보니까 UISheetPresentationController의 detent를 사용하여 구성한 것으로 생각했습니다. 

 

 

import UIKit
import MapKit

class MapViewController: UIViewController {
    
    // MARK: - Variables
    let locationManager = CLLocationManager()
    
    // MARK: - UI Components
    let mapView: MapView = {
        let mapView = MapView()
        mapView.translatesAutoresizingMaskIntoConstraints = false
        return mapView
    }()
    
    
    // MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        view.addSubview(mapView)
        
        
        mapView.locationMapView.showsUserLocation = true // 사용자 위치 표시
        mapView.locationMapView.delegate = self
        
        configureConstraints()
        
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization() // 권한 요청
        locationManager.startUpdatingLocation()
    }
    
    // <---- UISheetPresentationConroller 관련 코드 ----->
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        showModalVC()
    }
    
    func showModalVC() {
        
        let mapSearchVC = UINavigationController(rootViewController: MapSearchViewController())
        
        mapSearchVC.modalPresentationStyle = .formSheet
        mapSearchVC.isModalInPresentation = true  // 완전히 내려가서 닫히는 걸 방지
        
        
        if let sheet = mapSearchVC.sheetPresentationController {
            // 기본 medium, large 외에 사용자 정의 detent 추가
            let smallId = UISheetPresentationController.Detent.Identifier("small")
            let customSmallDetent = UISheetPresentationController.Detent.custom(identifier: smallId) { context in
                return 200 // 최소 높이를 여기에서 설정 (150으로 예시)
            }
            
            // Detents 설정 (최소, 중간, 최대)
            sheet.detents = [customSmallDetent, .medium() ,.large()]
            sheet.largestUndimmedDetentIdentifier = .large
            sheet.prefersGrabberVisible = true // 손잡이 표시
            
        }
       present(mapSearchVC, animated: true)
    }
    // <---- UISheetPresentationConroller 관련 코드 ----->
    
    // MARK: - Layouts
    private func configureConstraints() {
        let mapViewConstraints = [
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            mapView.topAnchor.constraint(equalTo: view.topAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ]
        NSLayoutConstraint.activate(mapViewConstraints)
    }
}


extension MapViewController: CLLocationManagerDelegate, MKMapViewDelegate {
    
    // 위치 업데이트 시 호출되는 메서드
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let userLocation = locations.first {
            zoomToUserLocation(userLocation)
        }
    }
    
    // 줌 인 함수
    func zoomToUserLocation(_ location: CLLocation) {
        let regionRadius: CLLocationDistance = 2000 // 줌인 정도 (미터 단위로 설정 가능)
        let coordinateRegion = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: regionRadius, longitudinalMeters: regionRadius)
        mapView.locationMapView.setRegion(coordinateRegion, animated: true)
    }
}

 

 

 

sheetPresentationController을 사용할 경우 탭바가 가려진다는 문제가 있습니다. 

 

 

import UIKit
import MapKit

class MapViewController: UIViewController {
    
    // MARK: - Variables
    let locationManager = CLLocationManager()
    
    // MARK: - UI Components
    let mapView: MKMapView = {
        let mapView = MKMapView()
        mapView.translatesAutoresizingMaskIntoConstraints = false
        return mapView
    }()
    
    let searchView = UIView() // SearchView가 될 UIView
    
    var searchViewHeightConstraint: NSLayoutConstraint! // 높이 조절을 위한 Constraint
    
    // MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        // Add subviews
        view.addSubview(mapView)
        view.addSubview(searchView)
        
        
        configureMapView()
        configureSearchView()
        
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization() // 권한 요청
        locationManager.startUpdatingLocation()
    }
    
    func configureMapView() {
        mapView.showsUserLocation = true // 사용자 위치 표시
        mapView.delegate = self
        
        // MapView constraints
        NSLayoutConstraint.activate([
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            mapView.topAnchor.constraint(equalTo: view.topAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    func configureSearchView() {
        searchView.backgroundColor = .white // 검색창의 배경 색상
        searchView.layer.cornerRadius = 15
        searchView.layer.maskedCorners = CACornerMask(arrayLiteral: [.layerMaxXMinYCorner, .layerMinXMinYCorner])
        searchView.translatesAutoresizingMaskIntoConstraints = false
        
        // 높이 조절을 위한 제약 설정
        searchViewHeightConstraint = searchView.heightAnchor.constraint(equalToConstant: 150)
        
        // SearchView constraints
        NSLayoutConstraint.activate([
            searchView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            searchView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            searchView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            searchViewHeightConstraint // 기본 높이
        ])
        
        // 패닝 제스처 추가 (높이 조절을 위한 제스처)
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        searchView.addGestureRecognizer(panGesture)
    }
    
    // MARK: - Pan Gesture Handler for SearchView height adjustment
    @objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)
        let newHeight = searchViewHeightConstraint.constant - translation.y
        
        // SearchView 높이를 조정하는 코드
        if newHeight >= 150 && newHeight <= 650 { // 최소 높이 150, 최대 높이 650
            searchViewHeightConstraint.constant = newHeight
            gesture.setTranslation(.zero, in: view) // 다음 변환을 위한 초기화
        }
        
        if gesture.state == .ended {
            // 높이 자동 조정 (미디엄, 라지 등 높이로 자동 스냅)
            if newHeight > 400 {
                searchViewHeightConstraint.constant = 650 // large 높이
            } else if newHeight > 200 {
                searchViewHeightConstraint.constant = 350 // medium 높이
            } else {
                searchViewHeightConstraint.constant = 150 // small 높이
            }
            
            UIView.animate(withDuration: 0.3) {
                self.view.layoutIfNeeded() // 애니메이션으로 높이 변경
            }
        }
    }
}

// MARK: - CLLocationManagerDelegate and MKMapViewDelegate
extension MapViewController: CLLocationManagerDelegate, MKMapViewDelegate {
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let userLocation = locations.first {
            zoomToUserLocation(userLocation)
        }
    }
    
    func zoomToUserLocation(_ location: CLLocation) {
        let regionRadius: CLLocationDistance = 2000
        let coordinateRegion = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: regionRadius, longitudinalMeters: regionRadius)
        mapView.setRegion(coordinateRegion, animated: true)
    }
}

 

 

주요 포인트:

  1. 탭 바 위에 뷰 배치: searchView를 tabBarController의 tabBar 바로 위에 배치했고. 이렇게 하면 시트처럼 보이면서도 탭 바를 가리지 않습니다.
  2. 높이 조절 가능: 제스처를 통해 searchView의 높이를 조절할 수 있고. UISheetPresentationController와 유사한 경험을 제공하면서 탭 바를 가리지 않게 할 수 있습니다.

2. Replace Tab Bar with Custom View

또 다른 방법은 커스텀 탭 바를 만들어 사용하는 것으로. 이렇게 하면 더 많은 컨트롤이 가능해져서 시트가 탭 바를 가리는 문제를 우회할 수 있지만 이 방식은 구현이 복잡해질 수 있습니다.

3. 전환 후 다시 탭 바 보이기

UISheetPresentationController가 탭 바를 가리는 문제를 우회하기 위해 시트를 닫거나 사라질 때마다 탭 바를 다시 보이게 설정하는 것도 방법 중 하나지만 이 방식은 자연스러운 사용자 경험을 제공하지 않을 수 있습니다.

이러한 방식으로 UISheetPresentationController 없이 탭 바를 가리지 않도록 searchView를 구현할 수 있습니다.

 

 

 

 

UIPanGestureRecognizer는 iOS에서 제스처 인식을 담당하는 클래스 중 하나로, 사용자가 화면에서 드래그(또는 팬) 동작을 했을 때 이를 감지하고 처리하는 데 사용돼. 주로 화면을 스와이프하거나 드래그하는 제스처를 감지해 화면 내에서 객체를 이동하거나 크기를 조절할 때 사용돼.

주요 특징:

  • 팬 동작 감지: 사용자가 화면에서 손가락을 움직일 때(팬), UIPanGestureRecognizer는 이 움직임의 시작, 진행, 종료를 감지할 수 있어.
  • 위치와 속도 제공: 제스처가 시작된 지점에서부터 얼마나 움직였는지(translation)와 제스처의 속도(velocity)를 제공해.
  • 상태 관리: 제스처의 상태를 나타내는 속성(state)이 있어, 제스처가 시작되었는지(.began), 진행 중인지(.changed), 종료되었는지(.ended) 등을 확인할 수 있어.

기본 사용법:

UIPanGestureRecognizer는 주로 UIView에 추가해서 그 위에서 드래그 동작을 감지하고, 그에 따라 UIView를 움직이거나, 크기를 조절하는 등의 작업을 할 수 있어.

 

// 1. 팬 제스처 인식기 추가
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
someView.addGestureRecognizer(panGesture)

// 2. 팬 동작 처리 함수
@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
    let translation = gesture.translation(in: view) // 팬 동작으로 이동한 거리
    
    // someView를 팬 제스처에 맞춰 이동시킴
    someView.center = CGPoint(x: someView.center.x + translation.x, y: someView.center.y + translation.y)
    
    // 다음 제스처를 위한 translation을 0으로 초기화
    gesture.setTranslation(.zero, in: view)
    
    // 제스처가 끝났을 때 처리
    if gesture.state == .ended {
        // 제스처 종료 후의 처리 (예: 애니메이션, 위치 조정 등)
    }
}

 

 

주요 메서드:

  1. translation(in:): 제스처가 시작된 위치에서부터 팬 동작에 의해 이동한 거리.
  2. setTranslation(_:in:): 팬 동작에 의해 이동한 거리를 설정. 일반적으로 이동 후 다음 동작을 위해 값을 초기화할 때 사용돼.
  3. velocity(in:): 팬 동작의 속도.

상태 체크:

팬 제스처의 상태는 UIGestureRecognizer.State를 통해 관리되며, 팬 동작이 시작되었는지, 진행 중인지, 끝났는지 등의 상태를 알 수 있어.

  • .began: 제스처가 시작됨
  • .changed: 제스처가 진행 중
  • .ended: 제스처가 끝남
  • .cancelled: 제스처가 취소됨

UIPanGestureRecognizer를 사용하면 제스처에 반응하여 뷰의 크기 조정, 이동 등과 같은 인터랙티브한 UI를 쉽게 구현할 수 있어.

 

 

애니메이션 효과 적용

// MARK: - Pan Gesture Handler for SearchView height adjustment
    @objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        
        let translation = gesture.translation(in: view)
        /*
         gesture.translation(in: view)는 팬 제스처가 발생한 동안 사용자가 손가락을 얼마나 이동했는지를 반환하는 메서드야.
         translation은 제스처가 시작된 이후로 손가락이 움직인 좌표 차이(x, y)를 나타내는 CGPoint 객체를 반환해. 이 좌표는 특정 뷰(in: view)를 기준으로 계산돼.
         
         이 값은 제스처가 시작한 지점과 현재 지점 사이의 차이를 나타내며, 위 코드에서는 translation.y가 사용돼서 세로 방향으로 얼마만큼 움직였는지를 계산하는 데 사용되고 있어.
         그 값(translation.y)을 사용해 searchViewHeightConstraint.constant에 더하거나 빼서 searchView의 높이를 실시간으로 조절하는 방식이야. 즉, 팬 제스처로 사용자가 손가락을 얼마나 위아래로 움직였는지에 따라 searchView의 높이가 달라지게 돼.
         */
        
        let newHeight = mapSearchView.mapSearchViewHeightConstraint.constant - translation.y
        /*
         .constant는 해당 제약 조건의 실제 값을 의미
         */
        
        
        // mapSearchView 높이를 조정하는 코드
        if newHeight >= 150 && newHeight <= 650 { // 최소 높이 150, 최대 높이 650 이렇게 조건을 걸어서 mapSearchView가 너무 작아지거나 너무 커지지 않도록 최소 높이와 최대 높이를 설정
            mapSearchView.mapSearchViewHeightConstraint.constant = newHeight
            /*
             이 코드는 팬 제스처로 계산된 newHeight 값을 searchViewHeightConstraint.constant에 할당해서, 실시간으로 searchView의 높이를 조정하는 거야. 사용자가 손가락을 팬 하면 그에 맞춰 뷰의 높이가 즉시 변하는 것.
             */
            gesture.setTranslation(.zero, in: view) // 다음 변환을 위한 초기화
            /*
             의미: 이 코드는 팬 제스처가 끝나지 않았더라도 현재 제스처의 이동값을 초기화하는 역할을 해.
             팬 제스처가 한 번 시작되면 gesture.translation(in:)은 팬이 시작된 지점부터 계속 이동값을 누적해서 계산해. 그래서 제스처가 발생할 때마다 그 누적된 값을 초기화해 주기 위해 gesture.setTranslation(.zero, in: view)를 호출해 이동 거리를 0으로 리셋하는 거야.
             이렇게 함으로써 다음 팬 동작에서 새로운 움직임이 이전 팬 동작에 영향을 미치지 않도록 해주는 거지.
             */
        }
        
        if gesture.state == .ended {
            var targetHeight: CGFloat
            
            // 높이 자동 조정 (미디엄, 라지 등 높이로 자동 스냅)
            if newHeight > 400 {
                targetHeight = 650 // large 높이
            } else if newHeight > 200 {
                targetHeight = 350 // medium 높이
            } else {
                targetHeight = 200 // small 높이
            }
            
            // 스프링 애니메이션을 통해 바운스 효과 적용
            UIView.animate(
                withDuration: 0.5,
                delay: 0,
                usingSpringWithDamping: 0.6, // 바운스 효과 설정 (0에 가까울수록 바운스가 더 큼)
                initialSpringVelocity: 0.8, // 초기 속도 설정
                options: .curveEaseInOut,
                animations: {
                    self.mapSearchView.mapSearchViewHeightConstraint.constant = targetHeight
                    self.view.layoutIfNeeded() // 애니메이션으로 높이 변경
                },
                completion: nil
            )
        }
    }