[Swift] TagListView 만들기

선주·2023년 1월 13일
0

Swift

목록 보기
19/20

아래 첨부한 움짤과 같은 모습의 TagListView를 만들어보겠습니다!

  • 태그 중복 선택 불가
  • 태그가 선택된 상태라면 태그의 테두리, 텍스트, 좌측의 필터 이미지를 컬러링해줄 것
  • 선택된 태그를 다시 클릭하면 선택이 해제될 것




🎨 Storyboard

TagListView가 삽입될 공간을 UIView로 만들고 Constraints를 잡아줍니다.
그리고 SearchViewController로 IBOutlet을 세 개 연결해주었습니다.

  • @IBOutlet weak var tagListView: UIView!
  • @IBOutlet weak var tagListViewHeight: NSLayoutConstraint!
  • @IBOutlet weak var filterImageView: UIImageView!




✏️ 태그 만들기

각각의 태그는 터치 제스처를 받아 처리할 수 있어야 하므로 UIButton으로 만들어주겠습니다.

태그의 title이 될 String을 매개변수로 주면 title과 border 사이에 left/right에는 각각 15만큼의 여백, top/bottom에는 각각 6.5만큼의 여백을 갖는 UIButton을 반환하는 메소드입니다.

// 태그버튼 생성
private func createButton(with title: String) -> UIButton {
    let font = UIFont(name: "Pretendard-Medium", size: 12)!
    let fontAttributes: [NSAttributedString.Key: Any] = [.font: font]
    let fontSize = title.size(withAttributes: fontAttributes)

    let tag = UIButton(type: .custom)
    tag.setTitle(title, for: .normal)
    tag.titleLabel?.font = font
    tag.setTitleColor(Pallete.filterGray, for: .normal)
    tag.layer.borderColor = Pallete.borderGray.cgColor
    tag.layer.borderWidth = 1
    tag.layer.cornerRadius = 14
    tag.frame = CGRect(x: 0.0, y: 0.0, width: fontSize.width + 30.0, height: fontSize.height + 13.0)
    tag.contentEdgeInsets = UIEdgeInsets(top: 6.5, left: 15, bottom: 6.5, right: 15)

    return tag
}

(참고)
Pallete는 프로젝트 전역에서 사용되는 UIColor들을 모아둔 커스텀클래스입니다.
ex) static let borderGray = UIColor(red: 179/255, green: 179/255, blue: 179/255, alpha: 1)




✏️ 뷰에 태그 붙이기

아직까지는 UIButton을 만들기만 하고 뷰에 뿌려주지는 않은 상태입니다. 따라서 뷰에 UIButton들을 붙이는(addSubview) 메소드를 만들어주겠습니다.

매개변수

  • view (UIView) : 태그가 붙을 영역
  • tagButtons ([UIButton]) : 붙일 태그들이 담긴 리스트

tagButtons를 순회하면서 view의 좌표를 계산해 적당한 위치에 tagButton을 하나씩 붙여줍니다.

첫 tagButton은 (0, 0)에 붙이고,
다음 tagButton은 (0, 0)으로부터 (첫 tagButton의 너비 + 태그간 수평 여백, 0)만큼 떨어진 곳에 붙여줍니다.

그런데 만약 첫 tagButton의 너비 + 태그간 수평 여백이 view의 너비보다 크다면, 현재 줄에 공간이 부족해 tagButton을 붙일 수 없는 것이므로 다음 줄로 내려줘야 합니다.
이 경우, 다음 tagButton은 (0, 0)으로부터 (0, 첫 tagButton의 높이 + 태그간 수직 여백)만큼 떨어진 곳에 붙여주면 됩니다.

이렇게 tagButton의 크기에 따라 view가 몇 줄짜리가 될 지가 달라집니다. 즉, view의 높이가 동적으로 변해야 하기 때문에, 모든 tagButton들을 다 붙였다면 마지막으로 view의 높이를 계산하여 적용해주어야 합니다.

/// 태그뷰에 태그버튼들 붙이기
private func attachTagButtons(at view: UIView, _ tagButtons: [UIButton]) {
    var lineCount: CGFloat = 1 // 태그의 줄 수
    let marginX: CGFloat = 5 // 태그간 수평 여백
    let marginY: CGFloat = 8 // 태그간 수직 여백

    var positionX: CGFloat = 0 // 태그가 붙을 위치의 x좌표
    var positionY: CGFloat = 0 // 태그가 붙을 위치의 y좌표

    for (index, tagButton) in tagButtons.enumerated() {
        tagButton.frame = CGRect(x: positionX, y: positionY, width: tagButton.frame.width, height: tagButton.frame.height)
        view.addSubview(tagButton)

        if index < tagButtons.count - 1 {
            // 다음 태그버튼 좌표 설정
            positionX += tagButton.frame.width + marginX

            // 현재 줄에 공간이 부족해 다음 태그버튼이 붙을 수 없으면 다음 줄로 내리기
            if positionX + tagButtons[index + 1].frame.width > view.frame.width {
                positionX = 0
                positionY += tagButton.frame.height + marginY
                lineCount += 1
            }
        }
    }

    // 태그뷰 높이 계산
    let height = view.subviews.first?.frame.height ?? 0
    let margins: CGFloat = (lineCount - 1) * marginY
    view.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: (lineCount * height) + margins)
}



✏️ 태그 터치 제스처 처리하기

각 태그를 터치하면 취할 액션을 정의해줍니다. 우선 터치된 태그의 선택 상태를 반전시켜주고, 선택 상태에 따라 border와 titleColor를 변경시켜줍니다.

isTagSelected는 모든 태그 중 하나라도 선택된 태그가 있는지를 나타내는 Bool변수입니다. 하나라도 선택된 태그가 있으면 태그필터이미지도 컬러링시켜줍니다.

마지막으로 우리의 태그는 중복선택이 불가능하기 때문에 태그 하나가 선택되면 나머지 태그들은 미선택상태로 바꿔주어야 합니다. filter를 사용해서 터치된 태그가 아닌 다른 태그들의 상태를 바꿔줍니다.

/// 태그버튼 클릭시
@objc func touchTagButton(sender: UIButton) {
    // 태그 선택 여부 반전
    sender.isSelected = !sender.isSelected
    sender.setTitleColor(sender.isSelected ? Pallete.orange : Pallete.filterGray, for: .normal)
    sender.borderColor = sender.isSelected ? Pallete.orange : Pallete.borderGray
    let isTagSelected = sender.isSelected
    filterImageView.image = isTagSelected ? UIImage(named: "icon_filter") : UIImage(named: "icon_filter_gray")

    // 태그 하나만 선택할 수 있도록 함
    tagButtonArray.filter { $0 != sender }.forEach {
        $0.isSelected = false
        $0.setTitleColor(Pallete.filterGray, for: .normal)
        $0.borderColor = Pallete.borderGray
    }
}



✏️ 뷰 완성

필요한 메소드들을 모두 생성했습니다. 이 메소드들을 이용해 태그뷰를 완성시켜봅시다 😛

  • createButton(with title: String) -> UIButton : 태그 만들기
  • attachTagButtons(at view: UIView, _ tagButtons: [UIButton]) : 뷰에 태그 붙이기
  • touchTagButton(sender: UIButton) : 태그 터치 제스처 처리하기
/// 태그뷰 초기화
private func initTagView() {
	// 태그버튼들 생성
	let tagStringArray = ["주차 가능한 절", "반려동물 동반 가능한 절", "요즘 인기 있는 절", "세계 문화 유산에 등재된 절", "경기도에 있는 절", "체험 가능한 절"]
    tagButtonArray = tagStringArray.map { createButton(with: $0) }
    tagButtonArray.forEach {
    	$0.addTarget(self, action: #selector(touchTagButton), for: .touchUpInside)
    }
        
    // 태그뷰에 태그버튼들 붙이기
    let frame = CGRect(x: 0, y: 0, width: tagListView.frame.width, height: tagListView.frame.height)
    let tagView = UIView(frame: frame)
    attachTagButtons(at: tagView, tagButtonArray)
        
    // addSubview
    tagListView.addSubview(tagView)
    tagListViewHeight.constant = tagView.frame.height
}



🗒 코드 전문

import UIKit

class SearchViewController: UIViewController {
	@IBOutlet weak var tagListView: UIView!
    @IBOutlet weak var tagListViewHeight: NSLayoutConstraint!
    @IBOutlet weak var filterImageView: UIImageView!
    
    var tagButtonArray = [UIButton]()
    
    override func viewDidLoad() {
    	super.viewDidLoad()
        
        initTagView()
    }
    
    /// 태그뷰 초기화
    private func initTagView() {
    	// 태그버튼들 생성
        let tagStringArray = ["주차 가능한 절", "반려동물 동반 가능한 절", "요즘 인기 있는 절", "세계 문화 유산에 등재된 절", "경기도에 있는 절", "체험 가능한 절"]
        tagButtonArray = tagStringArray.map { createButton(with: $0) }
        tagButtonArray.forEach {
        	$0.addTarget(self, action: #selector(touchTagButton), for: .touchUpInside)
        }
        
        // 태그뷰에 태그버튼들 붙이기
        let frame = CGRect(x: 0, y: 0, width: tagListView.frame.width, height: tagListView.frame.height)
        let tagView = UIView(frame: frame)
        attachTagButtons(at: tagView, tagButtonArray)
        
        // addSubview
        tagListView.addSubview(tagView)
        tagListViewHeight.constant = tagView.frame.height
    }
    
    // 태그버튼 생성
	private func createButton(with title: String) -> UIButton {
        let font = UIFont(name: "Pretendard-Medium", size: 12)!
        let fontAttributes: [NSAttributedString.Key: Any] = [.font: font]
        let fontSize = title.size(withAttributes: fontAttributes)

        let tag = UIButton(type: .custom)
        tag.setTitle(title, for: .normal)
        tag.titleLabel?.font = font
        tag.setTitleColor(Pallete.filterGray, for: .normal)
        tag.layer.borderColor = Pallete.borderGray.cgColor
        tag.layer.borderWidth = 1
        tag.layer.cornerRadius = 14
        tag.frame = CGRect(x: 0.0, y: 0.0, width: fontSize.width + 30.0, height: fontSize.height + 13.0)
        tag.contentEdgeInsets = UIEdgeInsets(top: 6.5, left: 15, bottom: 6.5, right: 15)

        return tag
    }
    
    /// 태그뷰에 태그버튼들 붙이기
    private func attachTagButtons(at view: UIView, _ tagButtons: [UIButton]) {
        var lineCount: CGFloat = 1
        let marginX: CGFloat = 5
        let marginY: CGFloat = 8

        var positionX: CGFloat = 0
        var positionY: CGFloat = 0

        for (index, tagButton) in tagButtons.enumerated() {
            tagButton.tag = index
            tagButton.frame = CGRect(x: positionX, y: positionY, width: tagButton.frame.width, height: tagButton.frame.height)
            view.addSubview(tagButton)

            if index < tagButtons.count - 1 {
                // 다음 태그버튼 좌표 설정
                positionX += tagButton.frame.width + marginX

                // 현재 줄에 공간이 부족해 다음 태그버튼이 붙을 수 없으면 다음 줄로 내리기
                if positionX + tagButtons[index + 1].frame.width > view.frame.width {
                    positionX = 0
                    positionY += tagButton.frame.height + marginY
                    lineCount += 1
                }
            }
        }

        // 태그뷰 높이 계산
        let height = view.subviews.first?.frame.height ?? 0
        let margins: CGFloat = (lineCount - 1) * marginY
        view.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: (lineCount * height) + margins)
    }
    
    /// 태그버튼 클릭시
    @objc func touchTagButton(sender: UIButton) {
        // 태그 선택 여부 반전
        sender.isSelected = !sender.isSelected
        sender.setTitleColor(sender.isSelected ? Pallete.orange : Pallete.filterGray, for: .normal)
        sender.borderColor = sender.isSelected ? Pallete.orange : Pallete.borderGray
        let isTagSelected = sender.isSelected
        filterImageView.image = isTagSelected ? UIImage(named: "icon_filter") : UIImage(named: "icon_filter_gray")

        // 태그 하나만 선택할 수 있도록 함
        tagButtonArray.filter { $0 != sender }.forEach {
            $0.isSelected = false
            $0.setTitleColor(Pallete.filterGray, for: .normal)
            $0.borderColor = Pallete.borderGray
        }
    }
}

다음 포스팅에서는 오픈소스 라이브러리를 활용하여 더 간단하게 구현하는 방법을 알아보겠습니다! 🚀




참고
StackOverflow | George's Answer

profile
기록하는 개발자 👀

1개의 댓글

comment-user-thumbnail
2023년 1월 13일

유익한 정보 감사합니다 ◠‿◠

답글 달기