[Project 일지] 여행 기록 앱 만들기 (2) - MapKit , CustomFlowLayout

오늘은 여행 기록 내 지도를 넣어주고, 앨범에 사진을 삽입했을 때 일반적으로 떠있는 앨범이 아니라 살짝 셀을 변형시켜 조금 더 느낌있게 구현하기 위해 커스텀 플로우 레이아웃을 사용해 보았다.

 

MapKit 사용하기

여행기록을 사용할 때 맵에 지도를 넣어 위치를 나타낼 수 있는 지도를 넣어주기 위해서 맵킷을 사용해 GPS를 기록하려고 맵을 사용하려고 한다.

import UIKit
import MapKit
@objc func mapContainerViewTapped() {
    let mapDetailVC = MapDetailViewController()
    let navigationController = UINavigationController(rootViewController: mapDetailVC)
    navigationController.modalPresentationStyle = .fullScreen
    present(navigationController, animated: true, completion: nil)
}

맵컨테이너를 누르면 맵 디테일 VC로 이동하게 된다.

 

MapDetailViewController 생성하기

먼저 지도 컨테이너 뷰를 누르면 맵이 뜨도록 VC를 만들어 맵을 가득 채우도록 구현하였다.

class MapDetailViewController: UIViewController {
    
    let mapView = MKMapView()
    let searchBar = UISearchBar()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupUI()
        setupConstraints()
        setupSearchBar()
    }
    
    func setupUI() {
        view.backgroundColor = .white
        view.addSubview(mapView)
        view.addSubview(searchBar)
    }
    
    func setupConstraints() {
        mapView.translatesAutoresizingMaskIntoConstraints = false
        searchBar.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            
            mapView.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    func setupSearchBar() {
        searchBar.delegate = self
        searchBar.placeholder = "Search for a place or address"
    }
}

MKMapView() 를 호출해서 변수에 담아 레이아웃을 설정해주면 손쉽게 구현이 가능해진다.

그리고 맵에 기록을 저장하고 뒤로가서 이전 VC로 돌아가도록 구현했다.

 

override func viewDidLoad() {
    super.viewDidLoad()
    
    setupUI()
    setupConstraints()
    setupSearchBar()
    setupBackButton() // 새로운 함수 호출
}

func setupBackButton() {
    let backButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(backButtonTapped))
    navigationItem.leftBarButtonItem = backButton
}

@objc func backButtonTapped() {
    dismiss(animated: true, completion: nil)
}

backButton 을 생성해서 뒤로 넘어가 이전 화면으로 돌아갈 수 있도록 구현하였다.

지도에서 보이는 범위를 5km x 5km 를 설정할 수 있도록 로케이션을 넣어주었다.

func setInitialLocation() {
    let seoulLocation = CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780)
    let region = MKCoordinateRegion(center: seoulLocation, latitudinalMeters: 10000, longitudinalMeters: 10000)
    mapView.setRegion(region, animated: true)
}

이렇게 넣어주면 특정 범위만 지도에서 보이게 된다.

그리고 특정 지역명을 설정하고 기록할 수 있도록 서치바를 넣어주도록 하자

extension MapDetailViewController: UISearchBarDelegate {
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        searchBar.resignFirstResponder()
        
        let searchRequest = MKLocalSearch.Request()
        searchRequest.naturalLanguageQuery = searchBar.text
        
        let activeSearch = MKLocalSearch(request: searchRequest)
        activeSearch.start { (response, error) in
            if let response = response {
                let latitude = response.boundingRegion.center.latitude
                let longitude = response.boundingRegion.center.longitude
                
                let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
                let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 10000, longitudinalMeters: 10000)
                
                self.mapView.setRegion(region, animated: true)
            }
        }
    }
}

서치바 델리게이트를 통해 네비게이션 내 서치바를 넣어 특정 지역명을 검색해 핀을 넣어줄 수 있도록 서치바를 설정해 주었다.

 

Custom UICollectionViewCell 구현

CollectionView를 활용해 갤러리에 사진을 추가하면 셀에 사진이 추가되도록 구현할 예정이다. 각각 다른 셀 크기를 주고 공백을 줘서 비대칭적으로 구현하였다.

 

먼저 특정 셀에 특정 값을 주기 위해 코드를 구현하였다.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    if collectionView == galleryCollectionView {
        if selectedImages.isEmpty {
            return CGSize(width: 173, height: 115)
        } else {
            switch indexPath.row {
            case 0, 3:
                return CGSize(width: 173, height: 115)
            case 1, 2:
                return CGSize(width: 173, height: 223)
            default:
                return CGSize(width: 173, height: 115)
            }
        }
    } else {
        return CGSize(width: 73, height: 73)
    }
}

이렇게 각 셀의 인덱스 별로 각 다른 값을 적용해준다.

이 뿐 만 아니라 플로우 레이아웃을 커스텀해 레이아웃을 잡아주도록 하자

class CustomFlowLayout: UICollectionViewFlowLayout {
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
        
        attributes?.forEach { layoutAttribute in
            if layoutAttribute.representedElementCategory == .cell {
                if let newFrame = layoutAttributesForItem(at: layoutAttribute.indexPath)?.frame {
                    layoutAttribute.frame = newFrame
                }
            }
        }
        return attributes
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItem(at: indexPath)?.copy() as! UICollectionViewLayoutAttributes
        let spacing: CGFloat = 16
        let additionalOffset: CGFloat = 50

        switch indexPath.item {
        case 0:
            attributes.frame.origin.y = 0
            attributes.frame.origin.x = 0
        case 1:
            attributes.frame.origin.y = additionalOffset
            attributes.frame.origin.x = attributes.frame.width + spacing
        case 2:
            if let firstItemAttributes = layoutAttributesForItem(at: IndexPath(item: 0, section: indexPath.section)) {
                attributes.frame.origin.y = firstItemAttributes.frame.maxY + spacing
                attributes.frame.origin.x = 0
            }
        case 3:
            if let secondItemAttributes = layoutAttributesForItem(at: IndexPath(item: 1, section: indexPath.section)) {
                attributes.frame.origin.y = secondItemAttributes.frame.maxY + spacing
                attributes.frame.origin.x = secondItemAttributes.frame.minX
            }
        default:
            break
        }
        return attributes
    }
}

별도의 커스텀 플로우레이아웃을 설정해 사이 간격과 x y 값을 설정해 넣어주었다.

func createCollectionViewFlowLayout(for scrollDirection: UICollectionView.ScrollDirection) -> UICollectionViewFlowLayout {
    let layout = CustomFlowLayout()
    layout.scrollDirection = scrollDirection
    layout.minimumLineSpacing = 10
    layout.minimumInteritemSpacing = 5
    layout.estimatedItemSize = .zero
    return layout
}

커스텀으로 설정한 플로우 레이아웃을 컬렉션 뷰 레이아웃에 넣어주면 커스텀 된 뷰를 사용가능하다 😀

오늘은 여기까지!