본문 바로가기
UIKIT

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

by 밤새는 탐험가89 2024. 10. 10.
728x90
SMALL

 

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
            )
        }
    }
728x90
LIST