[Project 일지] 단어장 앱 만들기 (2)

2024.05.13 - [◽️ Programming/T I L] - [Project 일지] 단어장 앱 만들기 (1)

 

 

TableView → CollectionView 교체

이전 글에 캘린더 밑에 오늘 저장한 단어가 나오는 내용을 tableview로 구성했었지만 셀 간격을 띄울려고 여러 방면으로 시도 했을때 계속 셀 간의 간격을 띄우기가 불가해서 시간 더 잡아먹지 말고 collectionview로 교체하기로 결정했다.

func createCollectionViewFlowLayout(for collectionView: UICollectionView) -> UICollectionViewFlowLayout {
    let layout = UICollectionViewFlowLayout()
    collectionView.collectionViewLayout = layout
    layout.scrollDirection = .vertical
    layout.minimumLineSpacing = 20
    layout.minimumInteritemSpacing = 5
    layout.itemSize = CGSize(width: 330, height: 120)
    return layout
}

CollectionView에는 FlowLayout이 설정되어야 하므로 이렇게 설정을 하게 되면 이제 띄우는 구현까지는 익숙하게 가능한 것 같다.

 

Action, Animate을 통한 CollectionView 편의성 높이기

위에 구성된 컬렉션 뷰에 단어들이 뜨게 될 예정이지만 캘린더가 있어 많은 단어를 확인하기 어려워질 수 있다는 점을 생각해 편의성을 좀 올릴 수 있기 위해 특정 버튼을 누르면 컬렉션 뷰의 크기가 늘어나 더 많은 단어를 확인할 수 있도록 설정했다.

upButton.addTarget(self, action: #selector(upButtonTapped), for: .touchUpInside)

@objc func upButtonTapped() {
    UIView.animate(withDuration: 0.3) {
        if self.upButton.isSelected {
            self.dateView.constraints.forEach {
                if $0.firstAttribute == .height {
                    $0.constant = 330
                }
            }
            self.upButton.setImage(UIImage(systemName: "arrow.up"), for: .normal)
            
        } else {
            self.dateView.constraints.forEach {
                if $0.firstAttribute == .height {
                    $0.constant = 0
                }
            }
            self.upButton.setImage(UIImage(systemName: "arrow.down"), for: .normal)
        }
        self.view.layoutIfNeeded()
        self.upButton.isSelected.toggle()
    }
}

이렇게 코드를 구성하였고

UIView.animate(withDuration: 0.3) {
if self.upButton.isSelected {
    self.dateView.constraints.forEach {
        if $0.firstAttribute == .height {
            $0.constant = 330
        }
    }
    self.upButton.setImage(UIImage(systemName: "arrow.up"), for: .normal)
    
}

애니메이션 효과를 넣어 버튼을 눌렀을때 자연스럽게 창이 올라갈 수 있도록 설정했다. 먼저 버튼이 선택 된 상태임을 확인하는 isSeclected를 사용하고 선택되었다면 캘린더의 높이를 330으로 잡아 다시 캘린더가 보이도록 설정한다.

else {
      self.dateView.constraints.forEach {
          if $0.firstAttribute == .height {
              $0.constant = 0
          }
      }
      self.upButton.setImage(UIImage(systemName: "arrow.down"), for: .normal)
  }
  self.view.layoutIfNeeded()
  self.upButton.isSelected.toggle()
}

처음 버튼을 클릭하게 되면 캘린더의 높이를 0으로 만들어 보이지 않도록 설정하고 해당 내용들이 수행될 때마다 즉 if 문을 나오자마자 적용된 내용을 view에 적용할 수 있도록 layoutIfNeeded() 를 설정하였다.

 

이렇게 구현하면 편의성을 챙기면서 많은 단어를 보여지게 할 수 있는 것 같다!

UIPresentationController 를 통한 커스텀 뷰 만들기

그 다음 필터버튼을 누르면 저장된 단어들의 정렬을 변경하기 위해 필터를 선택할 수 있는 창을 만들어보자.

 

처음엔 모달을 사용해서 넘어갔지만 원래 있던 화면이 뒤로 넘어가고 새로운 창이뜨는 형식으로 뜨기 때문에 올바르지 않은 것 같아 팀원 분께서 추천해주신 방법을 확인해 UIPresentationController 를 사용해보기로 결정했다!

filterButton.addTarget(self, action: #selector(filterButtonTapped), for: .touchUpInside)

@objc func filterButtonTapped() {
    let filterModalVC = CalenderDetailModelViewController()
    filterModalVC.modalPresentationStyle = .custom
    filterModalVC.transitioningDelegate = self
    present(filterModalVC, animated: true, completion: nil)
}

필터 버튼은 이렇게 구성을 해준다.

extension CalenderViewController: UIViewControllerTransitioningDelegate {
    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return PresentationController(presentedViewController: presented, presenting: presenting)
    }
}

델리게이트 패턴을 사용해서 UIViewControllerTransitioningDelegate 를 통해 화면을 전달하는 델리게이트를 구성하였다.

 

여기서 확인되는 PresentationController는 밑에 코드에서 구현할 예정이다.

 

이제 UIPresentationController 를 구성해보자!

override var frameOfPresentedViewInContainerView: CGRect {
    guard let containerView = containerView else { return CGRect.zero }
    return CGRect(x: 0, y: containerView.bounds.height / 2, width: containerView.bounds.width, height: containerView.bounds.height / 2)
}

frameOfPresentedViewInContainerView 보여지는 뷰의 높이를 정하기 위해 코드를 구성하였다.

private let dimmingView = UIView()

override func presentationTransitionWillBegin() {
    guard let containerView = containerView, let presentedView = presentedView else { return }
    
    dimmingView.frame = containerView.bounds
    dimmingView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
    dimmingView.alpha = 0
    containerView.insertSubview(dimmingView, at: 0)
    
    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dimmingViewTapped))
    dimmingView.addGestureRecognizer(tapGesture)
    
    presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
        self.dimmingView.alpha = 1
    }, completion: nil)
    
    presentedView.frame = frameOfPresentedViewInContainerView
    containerView.addSubview(presentedView)
}

화면에 뷰가 등장할 때 뷰에 해당 하지 않는 부분은 조금 어둡게 표현하고 싶어 backgroundcolor를 주고 레이아웃을 잡아준다.

 

그리고 해당 빈공간을 클릭하면 창이 다시 내려갈 수 있도록 탭제스처를 설정해준 뒤, 보여지는 뷰를 넣어주면 완성된다!

 

Label , button Factory로 중복 코드 줄이기

코드 베이스로 구현을 시작하면서 레이블과 버튼이 특히 필터를 설정하는 과정에서 동일한 코드가 중복된 것을 확인하였다.

 

해당 내용을 개선하기 위해 팀원분이 사용하시던 방법을 사용하니 편리하고 코드가 간편해져 더욱 손쉽고 가독성 좋은 코드가 완성 된 것 같아 기록에 남겨두려고 한다.

 

먼저 자주 사용되는 세팅값을 메서드로 만들어 파라미터로 값을 받을 수 있도록 한다.

class LabelFactory {
    
    func makeLabel (title: String, color: UIColor = .black, size: CGFloat, textAlignment: NSTextAlignment = .center, isBold: Bool) -> UILabel {
        let label = UILabel()
        label.text = title
        label.textColor = color
        if isBold == true {
            label.font = UIFont.boldSystemFont(ofSize: size)
        } else {
            label.font = UIFont.systemFont(ofSize: size)
        }
        label.textAlignment = textAlignment
        label.numberOfLines = 0
        
        return label
    }
    
}
class ButtonFactory {
    
    func makeButton(normalImageName: String, selectedImageName: String, tintColor: UIColor) -> UIButton {
        let button = UIButton()
        button.setImage(UIImage(systemName: normalImageName), for: .normal)
        button.setImage(UIImage(systemName: selectedImageName), for: .selected)
        button.tintColor = tintColor
        return button
    }
}

이렇게 클래스를 설정해 메서드화 해두면 간편하게 사용만 하면 끝..

let recentAdd = LabelFactory().makeLabel(title: "최근 저장 순", color: .black, size: 20, textAlignment: .left, isBold: false)
let lastAdd = LabelFactory().makeLabel(title: "나중 저장 순", color: .black, size: 20, textAlignment: .left, isBold: false)
let memoryVoca = LabelFactory().makeLabel(title: "외운 단어 순", color: .black, size: 20, textAlignment: .left, isBold: false)
let unmemoryVoca = LabelFactory().makeLabel(title: "못 외운 단어 순", color: .black, size: 20, textAlignment: .left, isBold: false)
let randomVoca = LabelFactory().makeLabel(title: "랜덤", color: .black, size: 20, textAlignment: .left, isBold: false)

레이블은 이런식으로 간편하게 생성이 가능하고

let recentAddButton = ButtonFactory().makeButton(normalImageName: "circle", selectedImageName: "circle.circle", tintColor: .black)
let lastAddButton = ButtonFactory().makeButton(normalImageName: "circle", selectedImageName: "circle.circle", tintColor: .black)
let memoryVocaButton = ButtonFactory().makeButton(normalImageName: "circle", selectedImageName: "circle.circle", tintColor: .black)
let unmemoryVocaButton = ButtonFactory().makeButton(normalImageName: "circle", selectedImageName: "circle.circle", tintColor: .black)
let randomVocaButton = ButtonFactory().makeButton(normalImageName: "circle", selectedImageName: "circle.circle", tintColor: .black)

버튼도 동일하게 한줄 코드에 모든 값을 다 넣을 수 있게 된다.

 

앞으로도 유용하게 사용될 것 같다 🙂