iOS SearchController 두번째, 검색바 데이터 filtering 처리 해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


SearchController 두번째, 검색바 데이터 filtering 처리 해보기

사실 우리가 검색 컨트롤러를 사용하는 이유는 아래와 같죠.

  1. 내가 검색한 내용이 실제 검색이 되어야한다.

제일 중요한 부분입니다.
그러면 이제 그 제일 중요한 부분을 다뤄보도록 하겠습니다.

우선…

우선 기본적으로 제가 한 셋팅을 보여드리겠습니다.

class SearchViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    var arr = ["zehye", "hi", "hello", "nice", "to", "meet", "you"]

    override func viewDidLoad() {
        super.viewDidLoad()
        self.initUI()
        self.setSearchControllerUI()
    }
    
    func initUI() {
        self.tableView.delegate = self 
        self.tableView.dataSource = self 
    }

    func setSearchControllerUI() {
        let searchController = UISearchController(searchResultsController: nil)
        
        // set placeholder
        let placeholder = "검색창입니다"
        searchController.searchBar.placeholder = placeholder
        // searchController가 검색하는 동안 네비게이션에 가려지지 않도록 
        searchController.hidesNavigationBarDuringPresentation = false
        // searchBar cancel button text change (cancel을 취소로 텍스트 변경)
        searchController.searchBar.setValue("취소", forKey: "cancelButtonText")
        
        // placeholder릐 글꼴과 글자 크기 변경 
        let attributedString = NSMutableAttributedString(string: placeholder, attributes: [
            NSAttributedString.Key.font: UIFont(name: "Spoqa Han Sans Neo", size: 16) as Any,
            NSAttributedString.Key.foregroundColor: UIColor(white: 255/255, alpha: 0.3)
        ])
        searchController.searchBar.searchTextField.attributedPlaceholder = attributedString
        // searchBar의 textField의 배경색, 폰트, 글자크기 변경
        searchController.searchBar.searchTextField.backgroundColor = .clear
        searchController.searchBar.searchTextField.font = UIFont(name: "Spoqa Han Sans Neo", size: 16)
        
        // clear button 설정 
        if let button = searchController.searchBar.searchTextField.value(forKey: "_clearButton") as? UIButton {
            let templateImage = button.imageView?.image?.withRenderingMode(.alwaysTemplate)
            button.setImage(templateImage, for: .normal)
            button.tintColor = .white
        }
        
        // 서치 컨트롤러 왼쪽에 back button color 변경 
        searchController.searchBar.searchTextField.leftView?.tintColor = UIColor.white
        // searchBar cancel button color
        searchController.searchBar.tintColor = UIColor.white
        
        // navigation title 
        self.navigationItem.searchController = searchController
        self.navigationItem.title = "검색하기"

        // searchBar back button color
        self.navigationItem.searchController?.searchBar.barTintColor = UIColor.white

        // navigation item 스크롤 될때 사라지지 않게 
        self.navigationItem.hidesSearchBarWhenScrolling = false

        // navigation item back button > back text remove (back 문구 없애기)
        self.navigationController?.navigationBar.topItem?.backButtonDisplayMode = .minimal

        // navigation item back button 이미지 변경
        self.navigationController?.navigationBar.backIndicatorImage = UIImage(named: "icBack")
        self.navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "icBack")
    }
}

extension SearchViewController: UITableViewDelegate, UITableViewDataSource {
    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.arr.count
    }

    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "SeachListCell") as! SearchListTableViewCell
        cell.textLabel?.text = self.arr[indexPath.row]

        return cell
    }
}
  1. initUI()를 통해 테이블뷰 셋팅을 진행했고
  2. setSearchControllerUI()를 통해 서치 컨트롤러 셋팅을 진행했습니다.
  3. 나머지 tableview delegate, datasource 처리를 하였구요.

그럼 이제 중요한건 무엇일까요?

  1. 유저가 서치바에 무언가를 검색한다.
  2. 그러면 입력할때마다 해당되는 검색 내용이 아래 리로드 될 수 있도록 한다.

Filtering 진행

우선 서치바에 텍스트가 업데이트 될때 마다 불리는 메소드가 있어야 합니다.

searhController.searchResultsUpdater = self 

위 코드를 setSearchControllerUI에 추가해 줍니다.

그리고 아래 코드도 같이 추가해 줍니다.

extension SearchController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) { 
        // code
    }
}

위 코드는 우리가 서치바에 무언가를 검색할때마다 불려지는 함수입니다.
이 함수 안에서 우리가 서치바에 무언가 검색할때 무엇을 처리할지 적어주면 됩니다.

class SearchViewController: UIViewContoller {
    ...
    var filterredArr: [String] = []
    ...
}

extension SearchController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) { 
        guard let text = searchController.searchBar.text?.lowercased() else { return }
        self.filterredArr = self.arr.filter { $0.localizedCaseInsensitiveContains(text) }
        dump(filterredArr)
    }
}

이러면 로그에는 서치바가 업데이트 될때마다 반응을 하지만 실제 화면에서는 이루어지질 않죠?

그 이유는 아직 테이블 뷰에서 필터링된 데이터들에 대한 처리를 해주고 있지 않기 때문입니다.

class SearchViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    var arr = ["zehye", "hi", "hello", "nice", "to", "meet", "you"]

    var filterredArr: [String] = []

    var isFiltering: Bool {
        let searchController = self.navigationItem.searchController
        let isActive = searchController?.isActive ?? false
        let isSearchBarHasText = searchController?.serarchBar.text?.isEmpty == false
        return isActive && isSearchBarHasText 
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.initUI()
        self.setSearchControllerUI()
    }
    
    func initUI() {
        self.tableView.delegate = self 
        self.tableView.dataSource = self 
    }

    func setSearchControllerUI() {
        let searchController = UISearchController(searchResultsController: nil)
        
        // set placeholder
        let placeholder = "검색창입니다"
        searchController.searchBar.placeholder = placeholder
        // searchController가 검색하는 동안 네비게이션에 가려지지 않도록 
        searchController.hidesNavigationBarDuringPresentation = false
        // searchBar cancel button text change (cancel을 취소로 텍스트 변경)
        searchController.searchBar.setValue("취소", forKey: "cancelButtonText")
        
        // placeholder릐 글꼴과 글자 크기 변경 
        let attributedString = NSMutableAttributedString(string: placeholder, attributes: [
            NSAttributedString.Key.font: UIFont(name: "Spoqa Han Sans Neo", size: 16) as Any,
            NSAttributedString.Key.foregroundColor: UIColor(white: 255/255, alpha: 0.3)
        ])
        searchController.searchBar.searchTextField.attributedPlaceholder = attributedString
        // searchBar의 textField의 배경색, 폰트, 글자크기 변경
        searchController.searchBar.searchTextField.backgroundColor = .clear
        searchController.searchBar.searchTextField.font = UIFont(name: "Spoqa Han Sans Neo", size: 16)
        
        // clear button 설정 
        if let button = searchController.searchBar.searchTextField.value(forKey: "_clearButton") as? UIButton {
            let templateImage = button.imageView?.image?.withRenderingMode(.alwaysTemplate)
            button.setImage(templateImage, for: .normal)
            button.tintColor = .white
        }
        
        // 서치 컨트롤러 왼쪽에 back button color 변경 
        searchController.searchBar.searchTextField.leftView?.tintColor = UIColor.white
        // searchBar cancel button color
        searchController.searchBar.tintColor = UIColor.white
        
        // navigation title 
        self.navigationItem.searchController = searchController
        self.navigationItem.title = "검색하기"

        // searchBar back button color
        self.navigationItem.searchController?.searchBar.barTintColor = UIColor.white

        // navigation item 스크롤 될때 사라지지 않게 
        self.navigationItem.hidesSearchBarWhenScrolling = false

        // navigation item back button > back text remove (back 문구 없애기)
        self.navigationController?.navigationBar.topItem?.backButtonDisplayMode = .minimal

        // navigation item back button 이미지 변경
        self.navigationController?.navigationBar.backIndicatorImage = UIImage(named: "icBack")
        self.navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "icBack")

        searhController.searchResultsUpdater = self 
    }
}

extension SearchViewController: UITableViewDelegate, UITableViewDataSource {
    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.isFilterting ? self.filterredArr.count : self.arr.count
    }

    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "SeachListCell") as! SearchListTableViewCell
        if self.isFiltering {
            cell.textLabel?.text = self.filterredArr[indexPath.row]
        } else {
            cell.textLabel?.text = self.arr[indexPath.row]
        }

        return cell
    }
}

extension SearchController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) { 
        guard let text = searchController.searchBar.text?.lowercased() else { return }
        self.filterredArr = self.arr.filter { $0.localizedCaseInsensitiveContains(text) }
       
        self.tableView.reloadData()
    }
}

iOS SearchController 첫번째, 기본 셋팅해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


SearchController 첫번째, 기본 셋팅해보기

프로젝트에서 검색창을 구현하기 위해 searchController를 사용하게 되었습니다.
그치만 결론은 앞으로는 서치바 자체만을 사용하게 될 것같다는 말씀…!

서치 컨트롤러는 사용해보니 화면상에서 서치 컨트롤러 자체가 제약을 두는 것이 많았습니다.
화면에 컨트롤러 하나가 더 겹쳐지게 됨으로써 커스텀을 하는데에도 찾아봐야할 것들이 많았구요.
검색을 위한 행위를 만들것이라면 그저 searchBar를 올려두고 그 안에서 해결하는것이 좀더 좋은 방법이 될 것 같다고 생각합니다..

좀더.. 간단해요! 그게…ㅎㅎ

그럼에도 서치 컨트롤러를 쓴다면! 아래와 같이 진행해보세요.

UISearchController

저는 서치 컨트롤러를 셋팅해주는 함수 하나를 만들어줄 예정이며, 이 함수를 viewDidLoad에서 불러서 사용할 것입니다.

func setSearchControllerUI() {
    let searchController = UISearchController(searchResultsController: nil)
    self.navigationItem.searchController = searchController
}

위와 같이 initialize를 해줍니다.

서치 컨트롤러 안에 서치바가 있기 때문에 서치바와 관련된 설정 코드들을 추가해줄수도 있습니다.

func setSearchControllerUI() {
    let searchController = UISearchController(searchResultsController: nil)
    searchController.searchBar.placeholder = "검색창입니다"

    self.navigationItem.searchController = searchController
}

이때 searchResultController는 무엇일까요?

우선 이 searchResultController에는 검색결과를 표시하고 싶은 뷰컨이 들어가면 됩니다.

예제 코드

기본적으로 우리가 서치 컨트롤러를 사용하면 알아야할 프로퍼티들입니다.

func setSearchControllerUI() {
    let searchController = UISearchController(searchResultsController: nil)
    
    // set placeholder
    let placeholder = "검색창입니다"
    searchController.searchBar.placeholder = placeholder
    // searchController가 검색하는 동안 네비게이션에 가려지지 않도록 
    searchController.hidesNavigationBarDuringPresentation = false
    // searchBar cancel button text change (cancel을 취소로 텍스트 변경)
    searchController.searchBar.setValue("취소", forKey: "cancelButtonText")
    
    // placeholder릐 글꼴과 글자 크기 변경 
    let attributedString = NSMutableAttributedString(string: placeholder, attributes: [
        NSAttributedString.Key.font: UIFont(name: "Spoqa Han Sans Neo", size: 16) as Any,
        NSAttributedString.Key.foregroundColor: UIColor(white: 255/255, alpha: 0.3)
    ])
    searchController.searchBar.searchTextField.attributedPlaceholder = attributedString
    // searchBar의 textField의 배경색, 폰트, 글자크기 변경
    searchController.searchBar.searchTextField.backgroundColor = .clear
    searchController.searchBar.searchTextField.font = UIFont(name: "Spoqa Han Sans Neo", size: 16)
    
    // clear button 설정 
    if let button = searchController.searchBar.searchTextField.value(forKey: "_clearButton") as? UIButton {
        let templateImage = button.imageView?.image?.withRenderingMode(.alwaysTemplate)
        button.setImage(templateImage, for: .normal)
        button.tintColor = .white
    }
    
    // 서치 컨트롤러 왼쪽에 back button color 변경 
    searchController.searchBar.searchTextField.leftView?.tintColor = UIColor.white
    // searchBar cancel button color
    searchController.searchBar.tintColor = UIColor.white
    
    // navigation title 
    self.navigationItem.searchController = searchController
    self.navigationItem.title = "검색하기"

    // searchBar back button color
    self.navigationItem.searchController?.searchBar.barTintColor = UIColor.white

    // navigation item 스크롤 될때 사라지지 않게 
    self.navigationItem.hidesSearchBarWhenScrolling = false

    // navigation item back button > back text remove (back 문구 없애기)
    self.navigationController?.navigationBar.topItem?.backButtonDisplayMode = .minimal

    // navigation item back button 이미지 변경
    self.navigationController?.navigationBar.backIndicatorImage = UIImage(named: "icBack")
    self.navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "icBack")
}

iOS SearchBar 사용해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


UISearchBar 사용해보기

서치 컨트롤러를 사용하다가 이래저래.. 서치바로 넘어왔습니다.

서치바는 아래와 같이 사용했습니다.

class SearchViewController: UIViewController {
    @IBOutlet weak var searchBar: UISearchBar!
    var isFiltering: Bool = false

    var arr = ["zehye", "hi", "hello", "nice", "to", "meet", "you"]
    var filterredArr: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.initUI()
        self.setSearchControllerUI()
    }

    func initUI() {
        self.tableView.delegate = self
        self.tableView.dataSource = self
    }

    func setSearchControllerUI() {
        self.searchBar.delegate = self
        self.searchBar.showsCancelButton = false
    }
}

extension SearchViewController: UITableViewDelegate, UITableViewDataSource {
    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.isFilterting ? self.filterredArr.count : self.arr.count
    }

    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "SeachListCell") as! SearchListTableViewCell
        if self.isFiltering {
            cell.textLabel?.text = self.filterredArr[indexPath.row]
        } else {
            cell.textLabel?.text = self.arr[indexPath.row]
        }

        return cell
    }
}

extension SearchViewController: UISearchBarDelegate {
    // 서치바에서 검색을 시작할 때 호출 
    func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
        self.isFiltering = true
        self.searchBar.showsCancelButton = true
        self.tableView.reloadData()
    }
    
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        guard let text = searchController.searchBar.text?.lowercased() else { return }
        self.filterredArr = self.arr.filter { $0.localizedCaseInsensitiveContains(text) }
       
        self.tableView.reloadData()
    }
    
    // 서치바에서 검색버튼을 눌렀을 때 호출 
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        dismissKeyboard()

        guard let text = searchController.searchBar.text?.lowercased() else { return }
        self.filterredArr = self.arr.filter { $0.localizedCaseInsensitiveContains(text) }
       
        self.tableView.reloadData()
    }
    
    // 서치바에서 취소 버튼을 눌렀을 때 호출 
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        self.searchBar.text = ""
        self.searchBar.resignFirstResponder()
        self.isFiltering = false
        self.tableView.reloadData()
    }
    
    // 서치바 검색이 끝났을 때 호출
    func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
        self.tableView.reloadData()
    }
    
    // 서치바 키보드 내리기 
    func dismissKeyboard() {
        searchBar.resignFirstResponder()
    }
}

iOS CSV 파일을 파싱해서 테이블뷰에 뿌려보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


CSV 파일을 파싱해보기

csv파일을 파싱해 테이블뷰에 뿌려보려고 합니다.

우선 csv파일은 무엇일까요?
csv파일은 데이터들을 쉽표로 구분한 텍스트 데이터 혹은 텍스트 파일을 의미합니다.

확장자가 .csv인 것이죠.

예로들자면

우유,100,234
김밥,100,530

이런식으로 음식에 대한 데이터가 이름, 1회 제공량당 그램수, 1회 제공량당 칼로리 이렇게 쉽표로 구분되어져 보이는 텍스트 데이터를 의미합니다.

저는 이번에 식품의약품안전처에서 제공해주는 음식데이터들을 이름, 1회 제공량당 그램수, 1회 제공량당 칼로리로만 구분해놓은 csv파일을 파싱해 테이블뷰에 보여줄 예정인데요. 실제 식품의약품안전처에서는 이에 대한 정보를 api 혹은 엑셀파일을 제공해주고 있는데요. 해당 api를 통신을 통해 가져오려고 하니 너무 오랜 시간이 걸리더라구요.. 그래서 이번에는 엑셀파일을 csv로 변환하여 그 해당하는 파일을 프로젝트 내에서 파싱해 테이블뷰에서 보여줄 예정입니다.

우선 해당 엑셀파일을 csv로 변환해서(저장하는 방식을 .csv로 저장하면 쉽게 받을 수 있습니다) 받아두었고, 해당 csv파일을 엑스코드 프로젝트 내부에 옮겨놓았습니다.

이렇게 말이죠. 저는 Calorie라는 파일로 들어가져있습니다.

해당 파일에는 아래와 같이 데이터들이 보여집니다.

The 더 건강한 델리햄,100,230,
The 더 건강한 브런치 슬라이스 오리지널,100,120,
The 더 건강한 햄 치즈,100,330,

식품이름, 1회제공량당 그램수, 1회 제공량당 칼로리 이렇게 말이죠.

본격적으로 파싱해보기

실제로 저는 모델을 따로 만들어 두고있고, 해당 모델에서는 name, per, cal 이렇게 필드를 세개로 나누어 csv 파일 데이터들을 받아줄 예정입니다.

class SearchViewController: UIViewController {
    var foodList: [FoodModel] = []
}

이렇게 foodList라는 변수에 담아서 보여줄 예정입니다.

class SearchViewController: UIViewController {
    var foodList: [FoodModel] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        self.loadCalorieFromCSV()
    }

    private func parseCSVAt(url: URL) {
        do {
            // url을 받은 data
            let data = try Data(contentsOf: url)
            // 해당 data를 encoding 합니다.
            let dataEncoded = String(data: data, encoding: .utf8)
            // ,로 구분해 만든 리스트를 제가 만들어 놓은 foodList에 담아줍니다. (FoodModel이라는 model에 맞게)
            if let dataArr = dataEncoded?.components(separatedBy: "\n").map({$0.components(separatedBy: ",")}) {
                self.foodList = dataArr.compactMap({ FoodModel(value: $0) })
            }
        } catch {
            print("Error reading CSV file")
        }
    }
    
    private func loadCalorieFromCSV() {
        // bundle에 있는 경로 > Calorie라는 이름을 가진 csv 파일 경로 
        let path = Bundle.main.path(forResource: "Calorie", ofType: "csv")!
        parseCSVAt(url: URL(fileURLWithPath: path))

        self.tableView.reloadData()
    }
}

iOS CollectionView Cell Dynamic Size 지정해주기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


컬렉션뷰 셀의 사이즈를 동적으로 조절하고 싶다면?

위 사진같이 셀을 동적으로 사이징 처리 해주고 싶다면?

구글링해보면 너무 어렵게만 설명되어있다.
사실 별거 없다.

@IBOutlet weak var collectionView: UICollectionView!

func initUI() {
        self.collectionView.delegate = self
        self.collectionView.dataSource = self
        let layout = UICollectionViewFlowLayout()
        layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        self.collectionView.collectionViewLayout = layout
}

이렇게 해주면 셀 안의 데이터 크기에 맞게 알아서 사이징이 된다!

문제발견

그런데 문제를 하나 발견했다.
위에서 제시한 방법은 뷰컨에 바로 컬렉션뷰, 셀이 놓여져 있을떄만 적용이 되는 것 같다.

예로 들자면 컬렉션뷰 셀을 재사용하기 위해 셀을 nib로 따로 만들어놓고 이를 불러와서 사용해야한다면?

적용이 되지 않더라…..

그래서 내가 사용한 방법은 아래와 같다.
내 상황은 아래와 같다.

  1. 뷰컨에서 테이블뷰를 사용하고 있고 테이블 셀 안에 컬렉션 뷰가 있다.
  2. 컬렉션 뷰 셀은 재사용을 위해 따로 nib를 만들어놓았다.
  3. 따라서 테이블뷰 셀 파일에서 컬렉션뷰를 register하고 있다.

tableviewCell file

class DataTableViewCell: UITableViewCell {
  @IBOutlet weak var collectionView: UICollectionView!

  let list = ["밥", "된장찌개", "계란말이", "교촌허니콤보", "떡볶이"]
  let calList = ["12kCal", "365kCal", "1234kCal", "135kCal", "567kCal"]

  override func awakeFromNib() {
        super.awakeFromNib()
        initUI()
  }

  func initUI() {
    self.collectionView.register(UINib(nibName: "CalorieCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "CalorieCell")
  }
}

extension DataTableViewCell: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let name = self.list[indexPath.row]
        let cal = self.calList[indexPath.row]

        let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14)]

        let nameSize = (name as NSString).size(withAttributes: attributes as [NSAttributedString.Key: Any])
        let calSize = (cal as NSString).size(withAttributes: attributes as [NSAttributedString.Key: Any])
        return CGSize(width: (nameSize.width + calSize.width) + 20, height: 35)
    }
}

이렇게 셀 안에 들어갈 string의 size를 측정해 직접 넣어주는 방법을 사용했다.