Today I Learn

Today I Learn(3): 동적으로 뷰 추가 및 삭제

Goniii 2025. 3. 5. 21:09

View의 자연스러운 추가, 삭제

미니 프로젝트의 주제는 팀원 소개이다. 우리팀은 기본적인 정보를 필수로 받고 팀원마다 추가하고 싶은 정보를 커스텀하여 추가할 수 있는 기능을 기획했다. 추가하고 싶은 정보는 사용자마다 다르기 때문에, 레이아웃의 추가와 삭제가 자유롭도록 구현해야 한다. UI는 아래와 같다. ‘Add Content’ 버튼을 눌렀을 때, 정보를 입력할 수 있는 뷰가 추가되고, 각 뷰에 있는 삭제 버튼으로 해당 뷰를 삭제하는 기능이 포함된다.

 

첫 번째 시도: UITableView

  • 정보를 추가하는 뷰는 동일하고 delete Button에 대한 액션을 넣기 위해서 indexPath에 쉽게 접근할 수 있는 테이블뷰를 선택했다.
  • ViewController에 contentCount 변수를 두고, numberOfRowInSection에서 contentCount를 리턴하여 선택한다
  • ‘Add Content’ 버튼을 누르면 contentCount를 1 증가 시키고 tableView를 reload 하는 방법으로 구현했다
  • 테이블뷰가 스크롤뷰 내부에 있기 때문에, 컨텐츠 사이즈를 조절해주어야 스크롤이 가능했다. 따라서, contentCount를 올리면서 height도 조절하는 방법을 선택했다
class ViewController: UIViewController {
		private var contentCount = 0 // 추가한 컨텐츠의 수
		private let createMemberCardView = CreateMemberCardView()
	
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view = createMemberCardView
        setDelegate()
        setAction()
    }
    
    private func setDelegate(){
        createMemberCardView.contentTableView.dataSource = self
    }
    
    private func setAction() {
     // Add Content 버튼 액션
        createMemberCardView.addContentButton.addTarget(self, action: #selector(touchUpInsideAddContentButton), for: .touchUpInside)
     }
     
    // Add Content 버튼 액션
    @objc private func touchUpInsideAddContentButton() {
				contentCount += 1
				
				// 테이블뷰 height 업데이트
				createMemberCardView.updateContentTableViewHeight(contentCount: contentCount)
				createMemberCardView.layoutIfNeeded()
    }
}

// 테이블 뷰 데이터소스
extension CreateMemberCardViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return contentsCount
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: ContentCell.id) as? ContentCell else { return UITableViewCell() }
//        cell.titleView.removeButton.addTarget(self, action: <#T##Selector#>, for: <#T##UIControl.Event#>)
	        return cell
    }
}
    
class CreateMemberCardView: UIView {
	...
	
    // 테이블뷰 Height 업데이트
    public func updateContentTableViewHeight(contentCount: Int) {
        let height = CGFloat(contentCount) * (207 + 10) // CellHeight + Padding
        
        
        // 기존 heightAnchor 제약을 찾아서 constant 값을 업데이트
        if let heightConstraint = contentTableView.constraints.first(where: { $0.firstAttribute == .height }) {
            print(heightConstraint.constant)
            heightConstraint.constant = height
            heightConstraint.isActive = true
            print("update: \\(heightConstraint.constant)")
        } else {
            // 만약 기존 제약이 없다면 새로 추가
            contentTableView.heightAnchor.constraint(equalToConstant: height).isActive = true
        }
        
        self.contentTableView.layoutIfNeeded()
        self.contentTableView.reloadData()
    }
 }

테이블뷰의 heightAnchor 제약조건이 없을 때, 새로 추가하면 contentView가 자연스럽게 추가되었지만, 2개 이상 추가할 때는 테이블 뷰의 height은 고정되고 테이블 뷰 내에서 따로 스크롤로 다음 내용을 볼 수 있었다

heightConstraint.constant 값의 증가는 확인했지만, scrollView의 크기는 그대로였다.

이 이유는 스크롤 뷰 내부에서 테이블뷰를 선언해서 테이블뷰의 height을 업데이트 해도 스크롤뷰의 height이 업데이트 되지 않아서 발생하는 문제인 것 같다 (뇌피셜)

→ tableView의 부모뷰인 createMemberCardView를 업데이트 해도 그대로다..

 

두 번째 방법: UIStackView 사용

사실 이 기능을 구현할 때 스택뷰가 가장 먼저 떠올랐다. 스크롤뷰 내부에 컬렉션뷰나 테이블뷰가 있을 때 동적으로 스크롤뷰의 크기 설정에 너무 애를 먹어서 테이블뷰로 도전해봤지만, 또 실패했다

아무튼 스택뷰의 뷰 추가 방법은 간단하다

버튼 액션으로 새로운 ContentView를 만들고 ContentView 내부 deleteButton에 액션을 추가하여 스택뷰에 ArrangeSubView로 넣어주면 끝이다

사실 추가한 ContentView를 삭제하는 것이 UITableView를 사용할 때보다 복잡하다. 테이블 뷰는 cellForRowsAt에서 indexPath를 사용하여 여러 ContentCell에서 선택한 셀을 구분하는 건 어렵지 않지만, UIStackView에는 자식뷰를 구별하는 건 쉽지 않다.

따라서, ContentView에 id값을 넣어주어 동일한 ContentView여도 구분이 가능하도록 구현했다.

class ContentView: UIView {
    public var id = UUID() // Content 구분을 위해 ID 값 사용
    
    // 타이틀 뷰
    public let titleView = CreateMemberInfoView(title: "Title", placeholder: "Enter your Content Title", isEnableRemove: true)
    
    // 컨텐츠 뷰
    public let contentsView = CreateMemberInfoView(title: "Content", placeholder: "Enter your Content", isLongText: true)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        self.isUserInteractionEnabled = true
        setSubView()
        setUI()
    }

StackView의 arrangedSubviews를 순회하면서 삭제하고자 하는 ContentView의 UUID와 동일한 UUID를 가진 ContentView를 찾아 삭제하는 방법이다

 

UIButton.AddTarget  데이터 전달 문제 해결

UIButton의 addTarget에서는 sender로 UIButton을 넘겨줄 수는 있지만, UUID와 같이 새로 선언한 값들을 넘겨줄 수 없다. 이를 해결하기 위해 UITapGestureRecognizer를 이용했다

  1. UITapGestureRecognizer를 상속하는 CustomTapGesture Class를 선언하고 해당 클래스에 id 프로퍼티를 추가한다.
  2. ContentView를 생성하면 내부적으로 id 값을 생성하고 생성된 id 값을 CustomTapGesutre의 프로퍼티에 저장한다.
  3. CustomTapGesutre 삭제 버튼의 탭 제스처로 추가한다면, 삭제 버튼을 눌렀을 때 gesture가 전달되어 id 값이 함께 전달되어 사용이 가능하다
class CustomTapGesture: UITapGestureRecognizer {
    var id: UUID? // ContentView 제거를 위한 ID
}
    // Add Content 버튼 액션
    @objc private func touchUpInsideAddContentButton() {
        // ContentView 생성 -> ID 생성
        let contentView = ContentView()
        
        // 삭제 제스처 추가
        let removeButtonTapGesutre = CustomTapGesture(target: self, action: #selector(removeButtonTapGesture(_:)))
        
        // 탭 제스처에 id 값 추가
        removeButtonTapGesutre.id = contentView.id
        
        // 삭제 버튼에 삭제 제스처 추가
        contentView.titleView.removeButton.addGestureRecognizer(removeButtonTapGesutre)
        
        // 생성한 View, StackView에 추가
        createMemberCardView.contentStackView.addArrangedSubview(contentView)
        
        // 딜리게이트 설정 (텍스트 뷰)
        contentView.contentsView.textView.delegate = self
        
        // 스크롤뷰 이동
        createMemberCardView.scrollView.scroll(to: .bottom)
    }
    
        // ContentView 삭제 버튼 액션
    @objc private func removeButtonTapGesture(_ gesture: CustomTapGesture) {
        guard let id = gesture.id else { return } // 제스처에 저장된 ID 값 추출
        
        // contentStackView를 순회하면서 제스처 ID와 같으면 뷰 삭제
        createMemberCardView.contentStackView.arrangedSubviews.forEach{ view in
            if (view as? ContentView)?.id == id {
                view.removeFromSuperview()
            }
        }
    }

 

 

 

 

 

UIButton.AddTarget  데이터 전달 문제 해결 리팩토링

https://soo-hyn.tistory.com/125

 

Today I Learn (4) : UUID 데이터 전달 문제 리팩토링

https://soo-hyn.tistory.com/124 Today I Learn(3): 동적으로 뷰 추가 및 삭제보호되어 있는 글입니다. 내용을 보시려면 비밀번호를 입력하세요.soo-hyn.tistory.com 이전 포스팅에서 사용한 UITapGestureRecognizer를

soo-hyn.tistory.com

 

728x90