애플 기본 날씨앱을 클론코딩을 하면서
MapKit을 통해 위치 자동완성 검색 기능을 구현해보았습니다.
MapKit 이 뭘까요? 우선 애플 공식 문서를 확인해볼까여..
MapKit은 다양한 기능을 제공하지만
(지도나 인공위성 사진을 사용할 수도 있고, 특정 위치를 불러올 수도 있는...)
저는 그 중에서도 사용자가 목적지 또는 관심 지점을 쉽게 검색할 수 있도록 텍스트 완료(자동 완성) 기능을 사용해보았습니다.
저는 SearchBar에 검색을 갈기면
결과가 해당 VC의 TableView에 뜨도록 구조를 잡아줬습니다.
즉, UISearchBarDelegate, UITableViewDelegate, UITableViewDataSource 다 채택해줬다는 뜻.
import MapKit 을 해주고 시작합니다.
단순히, 자동완성을 기반으로 하는 위치 검색 기능을 쓰려고 해서
MKLocalSearchCompleter (공식문서 링크)를 사용합니다.
-> 이 아이를 통해서 우리가 위치 검색을 하면 자동완성된 검색 결과들을 받아볼 수 있어요.
1️⃣ 그렇기 위해서 우선!
MKLocalSearchCompleter의 객체를 만들어주고
해당 VC에 MKLocalSearchCompleterDelegate 를 채택해주고
만들어 준 객체에 delegate 설정을 해줘야 합니다!
// MARK: - Properties
private var searchCompleter = MKLocalSearchCompleter() /// 검색을 도와주는 변수
private var searchResults = [MKLocalSearchCompletion]() /// 검색 결과를 담는 변수
searchCompleter가 해당 객체이고
자동완성된 결과를 MKLocalSearchCompleterDelegate 의 completerDidUpdateResults 메소드를 통해서 넘겨주는데 이때 결과를 받아줄 객체도 필요하기 때문에 searchResults 도 만들어줍니다.
searchResults 는 MKLocalSearchCompletion 의 객체인데 배열 형태로 결과를 받아줍니다.
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configUI()
setupBlurEffect()
setupAutoLayout()
setupSearchBar()
setupSearchCompleter() // 여기 요놈 자식입니다!!
setupTableView()
}
func setupSearchCompleter() {
searchCompleter.delegate = self
searchCompleter.resultTypes = .address /// resultTypes은 검색 유형인데 address는 주소를 의미
}
-> searchCompleter.resultTypes = .address 로 설정해줍니다.
우리는 위치가 필요한 것이기 때문에
이 외에도 pointOfInterest 가 있는데 얘는 아래 사진 처럼 특정 핫플?..?을 가져옵니다..
2️⃣ 검색 값을 가져와서 넘겨주기
UISearchBar를 사용했기 때문에 UISearchBarDelegate의 메소드를 사용해줍니다.
// MARK: - UISearchBarDelegate
extension SearchViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// searchText를 queryFragment로 넘겨준다.
searchCompleter.queryFragment = searchText
}
}
해당 메소드는 검색창의 text가 변하는 경우에 searchBar가 delegate에게 알리는데 사용합니다.
searchBar에 입력한 text가 바뀔 때마다 queryFragment에 넘어가는 text가 달라지면서
searchCompleter가 인식하겠죠?
🙋♀️ queryFragment라는 프로퍼티에 searchText를 할당하면 해당 문자열을 기반으로 검색을 하게 됩니다.
3️⃣ 결과 받아보기
결과를 받아오는 메소드는 completerDidUpdateResults 입니다.
// MARK: - MKLocalSearchCompleterDelegate
extension SearchViewController: MKLocalSearchCompleterDelegate {
// 자동완성 완료 시에 결과를 받는 함수
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
// completer.results를 통해 검색한 결과를 searchResults에 담아줍니다
searchResults = completer.results
searchTV.reloadData()
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
// 에러 확인
print(error.localizedDescription)
}
}
searchResults 에 검색 결과를 담아주고
searchResults를 통해서 검색 결과를 보여줄 tableView를 reload 해주면 됩니다.
TableViewDelegate 채워주기...
// MARK: - UITableViewDataSource
extension SearchViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchResults.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "SearchTVC", for: indexPath) as? SearchTVC
else { return UITableViewCell() }
cell.countryLabel.text = searchResults[indexPath.row].title
cell.backgroundColor = .clear
cell.selectionStyle = .none
if let highlightText = searchBar.text {
cell.countryLabel.setHighlighted(searchResults[indexPath.row].title, with: highlightText)
}
return cell
}
}
전체 코드
import UIKit
import MapKit
import SnapKit
import Then
class SearchViewController: UIViewController {
// MARK: - Properties
private var searchCompleter = MKLocalSearchCompleter() /// 검색을 도와주는 변수
private var searchResults = [MKLocalSearchCompletion]() /// 검색 결과를 담는 변수
let topView = UIView().then {
$0.backgroundColor = UIColor.lightGray.withAlphaComponent(0.7)
}
let titleLabel = UILabel().then {
$0.text = "도시, 우편번호 또는 공항 위치 입력"
$0.font = .systemFont(ofSize: 14, weight: .semibold)
$0.textColor = .white
$0.textAlignment = .center
}
let searchBar = UISearchBar().then {
$0.becomeFirstResponder()
$0.keyboardAppearance = .dark
$0.showsCancelButton = false
$0.searchBarStyle = .minimal
$0.searchTextField.leftView?.tintColor = UIColor.white.withAlphaComponent(0.5)
$0.searchTextField.backgroundColor = .lightGray
$0.searchTextField.textColor = .white
$0.searchTextField.tintColor = .white
$0.searchTextField.font = .systemFont(ofSize: 14, weight: .semibold)
$0.searchTextField.attributedPlaceholder = NSAttributedString(string: "검색",
attributes: [NSAttributedString.Key.foregroundColor : UIColor.white.withAlphaComponent(0.5)])
}
let cancelButton = UIButton().then {
$0.setTitle("취소", for: .normal)
$0.tintColor = .white
$0.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
$0.addTarget(self, action: #selector(touchupCancelButton(_:)), for: .touchUpInside)
}
let lineView = UIView().then {
$0.backgroundColor = .lightGray
}
let searchTV = UITableView().then {
$0.backgroundColor = .clear
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configUI()
setupBlurEffect()
setupAutoLayout()
setupSearchBar()
setupSearchCompleter()
setupTableView()
}
// MARK: - Custom Method
func configUI() {
view.backgroundColor = UIColor.black.withAlphaComponent(0.4)
}
func setupBlurEffect() {
let blurEffect = UIBlurEffect(style: .dark)
let visualEffectView = UIVisualEffectView(effect: blurEffect)
visualEffectView.frame = view.frame
view.addSubview(visualEffectView)
topView.addSubview(visualEffectView)
}
func setupAutoLayout() {
view.addSubviews([topView, searchTV])
topView.addSubviews([titleLabel, searchBar, cancelButton, lineView])
topView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(100)
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().inset(10)
make.centerX.equalToSuperview()
}
searchBar.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(20)
make.leading.equalToSuperview().inset(6)
make.trailing.equalTo(cancelButton.snp.leading).offset(-2)
make.height.equalTo(40)
}
cancelButton.snp.makeConstraints { make in
make.centerY.equalTo(searchBar.snp.centerY)
make.trailing.equalToSuperview().inset(10)
}
lineView.snp.makeConstraints { make in
make.leading.bottom.trailing.equalToSuperview()
make.height.equalTo(0.5)
}
searchTV.snp.makeConstraints { make in
make.top.equalTo(topView.snp.bottom)
make.leading.bottom.trailing.equalToSuperview()
}
}
func setupSearchBar() {
searchBar.delegate = self
}
func setupTableView() {
searchTV.delegate = self
searchTV.dataSource = self
searchTV.register(SearchTVC.self, forCellReuseIdentifier: "SearchTVC")
searchTV.separatorStyle = .none
}
func setupSearchCompleter() {
searchCompleter.delegate = self
searchCompleter.resultTypes = .address /// resultTypes은 검색 유형인데 address는 주소를 의미
}
// MARK: - @objc
@objc func touchupCancelButton(_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}
}
// MARK: - UITableViewDelegate
extension SearchViewController: UITableViewDelegate {
/// 검색 결과 선택 시에 (취소/추가)버튼이 있는 VC이 보여야 함
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedResult = searchResults[indexPath.row]
let searchReqeust = MKLocalSearch.Request(completion: selectedResult)
let search = MKLocalSearch(request: searchReqeust)
search.start { (response, error) in
guard error == nil else {
print(error.debugDescription)
return
}
guard let placeMark = response?.mapItems[0].placemark else {
return
}
let searchLatitude = placeMark.coordinate.latitude
let searchLongtitude = placeMark.coordinate.longitude
let vc = ViewController()
vc.isAddNewCityView = true
vc.location = (placeMark.locality ?? placeMark.title!)
vc.searchLatitude = searchLatitude
vc.searchLongtitude = searchLongtitude
vc.modalPresentationStyle = .fullScreen
self.present(vc, animated: true, completion: nil)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.searchBar.resignFirstResponder()
}
}
// MARK: - UITableViewDataSource
extension SearchViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchResults.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "SearchTVC", for: indexPath) as? SearchTVC
else { return UITableViewCell() }
cell.countryLabel.text = searchResults[indexPath.row].title
cell.backgroundColor = .clear
cell.selectionStyle = .none
if let highlightText = searchBar.text {
cell.countryLabel.setHighlighted(searchResults[indexPath.row].title, with: highlightText)
}
return cell
}
}
// MARK: - UISearchBarDelegate
extension SearchViewController: UISearchBarDelegate {
// 검색창의 text가 변하는 경우에 searchBar가 delegate에게 알리는데 사용하는 함수
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// searchText를 queryFragment로 넘겨준다.
searchCompleter.queryFragment = searchText
}
}
// MARK: - MKLocalSearchCompleterDelegate
extension SearchViewController: MKLocalSearchCompleterDelegate {
// 자동완성 완료 시에 결과를 받는 함수
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
// completer.results를 통해 검색한 결과를 searchResults에 담아줍니다
searchResults = completer.results
searchTV.reloadData()
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
// 에러 확인
print(error.localizedDescription)
}
}
필요한 tableViewCell 코드
import UIKit
import SnapKit
import Then
class SearchTVC: UITableViewCell {
static let identifier = "SearchTVC"
// MARK: - Properties
let countryLabel = UILabel().then {
$0.font = .systemFont(ofSize: 14, weight: .semibold)
$0.textColor = .lightGray
}
// MARK: - Lifecycle
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configUI()
setupAutoLayout()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected {
self.backgroundColor = .init(white: 1.0, alpha: 0.1)
} else {
self.backgroundColor = .none
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Custom Method
func configUI() {
}
func setupAutoLayout() {
addSubview(countryLabel)
countryLabel.snp.makeConstraints { make in
make.top.bottom.equalToSuperview().inset(12)
make.leading.equalToSuperview().inset(45)
}
}
}
참고 자료 - https://daheenallwhite.github.io/ios/2019/08/07/MKLocalSearchCompleter/
'⭐️ 개발 > iOS & Swift' 카테고리의 다른 글
[iOS] App에 Google Analytics 사용해보기 (4) | 2021.08.21 |
---|---|
[iOS] Bounds 와 Frame 총정리 (0) | 2021.08.21 |
[iOS] Blur Effect 사용해보기 (2) | 2021.08.05 |
[iOS] UIButton에 NSMutableAttributedString 적용해보기 (0) | 2021.08.04 |
[iOS] TableView 최상단 cell, safeArea 무시하고 배치하는 법? (0) | 2021.08.02 |