학생 얼굴 좀 봅시다! (by UICollectionView)

SteadySlower·2021년 12월 16일
0
post-thumbnail

기능 소개

학생부장님은 아침마다 수백명의 얼굴을 봅니다. 모든 학생들의 얼굴을 외울 수는 없습니다. 교문지도에 걸린 학생들은 학생부장님에게 반, 번호, 이름을 말하고 학생부장님은 그 이름을 걸린 사유와 함께 적어둡니다. 하지만 때때로 불순한 의도를 가진 학생들은 자기 이름을 대는 대신에 다른 친구의 이름을 대곤 합니다.💢

이런 경우에는 나중에 억울한 입장이 생기는 학생은 물론 지도한 선생님들도 모두 곤란한 상황에 놓입니다. 그래서 학생부장님은 항상 이름을 적기 전에 얼굴을 확인하는 습관을 가지고 있습니다.

주된 방법은 학생증입니다. 학생에게 사진이 있는 학생증을 보여달라고 하는 것이지요. 하지만 모든 학생이 학생증을 가지고 다니지는 않습니다.

이럴 때에는 전체 사진첩을 옆에 두고 찾는데 여간 귀찮고 시간이 오래 걸리는 일이 아닙니다. 때로는 사진첩을 찾을 때 몰래 😜  빠져나가는 학생들도 있습니다.

이런 일은 귀찮을 뿐만 아니라 교육적으로도 좋지 않습니다. (모든 학생들이 공평하게 규칙을 적용받아야 하니까요!)

👉  따라서 오늘 구현할 기능은 학년, 반, 번호를 입력하면 바로 학생의 사진을 확인할 수 있는 기능입니다.

계획

처음 계획은 너무나 쉬웠다 😀

컴동쌤이 처음 세운 계획은 아래 그림과 같았습니다. (발퀄 죄송합니다. 😅 )

학년-반-번호를 일렬로 주르륵 쓰면 학번이 됩니다. 이것을 이용해 학생의 사진을 검색하고자 했습니다.

  1. 텍스트 필드를 이용해서 학번을 입력 받는다.
  2. 숫자 키보드에서 입력을 마치면 사진이 검색된다. 끝!

👉  왕초보 개발자도 순식간에 해낼 수 있는 간단한 구조였지요.

학생부장님의 요청 🤬

컴동쌤은 위의 그림을 들고 학생부장님한테 찾아갔습니다. 하지만 학생부장님은 뭔가 맘에 안 드는 듯 한참을 그림을 보시더니 그림을 한장 그려보였습니다.

"컴동쌤, 내가 스마트폰에 익숙하지 않아서 그런데 좀 더 직관적으로 해줄 수 없을까? 버튼 하나만 딱딱딱 세번 누르면 사진을 찾을 수 있도록 말이야."

첫 기능부터 난관이군요. 그냥 완성하고 보여주면 알아서 쓰셨을텐데라는 원망과 함께 컴동쌤은 자신이 없었지만 일단 해보기로 했습니다.

어떻게 구현할 것인가?

1. 버튼을 스택뷰로?

스택뷰는 자동으로 크기를 계산해서 배열 해주기 때문에 버튼 크기에 구애받지 않고 쉽게 사용할 수 있습니다. 다만 버튼에 등록할 수 있는 selector 함수는 sender만을 인자로 받기 때문에 버튼을 일일히 다 (학년별로, 반별로, 번호별로 🤮 ) 만들 것이 아니라면 별도의 커스텀 버튼 클래스를 구현해야 할 겁니다.

2. collection view로?

컬렉션 뷰는 cell을 선택하는 기능을 제공하기 때문에 버튼과 비슷하게 사용할 수 있습니다. 다만 cell의 크기를 조절하는데 있어서 신경을 좀 써야겠군요.

컴동쌤의 선택은?

collection view를 선택하도록 하겠습니다. 이유는 아래와 같습니다.

  1. 학년의 경우에는 3개로 고정이 되어있기에 크게 관계 없습니다만, 반과 번호의 경우에는 학년별로, 반별로 동적으로 버튼을 구성해야 합니다. 가로, 세로 두 종류의 스택을 묶어서 사용해야 하는 1번 방법의 경우는 동적으로 대응하는 것이 쉽지 않습니다.
  2. 1번 안의 경우에는 너무 복잡한 뷰 구조를 가집니다. 한 줄의 버튼을 묶는 가로 스택뷰, 그 가로 스택뷰를 묶는 세로 스택뷰 그 전체 스택뷰를 묶는 하나의 뷰가 필요합니다. 이 모든 과정을 collection view는 혼자서 해줍니다.
  3. 버튼의 기능을 collection view도 가지고 있습니다. didSelect를 통해서 구현할 수 있습니다!

최종 목표

위 그림처럼 맨 위에 label로 현재 선택 중인 학생의 정보를 보여주고 학년-반-번호의 단계별로 collection view를 통해 동적으로 다른 버튼 (정확히 말하면 기능을 하는 cell)을 배치하도록 하겠습니다.

구현

Window에 rootVC 연결하기

SceneDelegate

스토리보드 대신 코드로 화면을 짤 때는 Entry Point의 역할을 하는 VC를 스토리 보드에서 지정하는 대신에 window에 rootViewController로 지정해야 합니다. 아래 코드는 그 과정을 보여줍니다.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

  var window: UIWindow?

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
      guard let scene = (scene as? UIWindowScene) else { return }
      window = UIWindow(windowScene: scene)
      window?.rootViewController = FaceCheckViewController()
      window?.makeKeyAndVisible()
  }
}

CollectionViewCell 구현

cell을 구현하는 것으로 collection view를 구현해보기 시작할까요?

cell 타입 구현

그 전에 cell의 유형을 정해야 할 것 같군요. 우리가 구현하고자하는 셀은 학년, 반, 번호 별로 총 3가지가 되겠네요. enum으로 구현해 보았습니다.

더불어 유형별로 폰트 사이즈와 cell의 색을 구분하기 위해 두 가지 연산 프로퍼티를 정의해두었습니다.

enum FaceCheckInputType {
    case grade, classNumber, number
    
    var cellTitleFontSize: UIFont {
        switch self {
        case .grade:
            return UIFont.systemFont(ofSize: 70)
        case .classNumber:
            return UIFont.systemFont(ofSize: 50)
        case .number:
            return UIFont.systemFont(ofSize: 30)
        }
    }
    
    var cellColor: UIColor {
        switch self {
        case .grade:
            return .green
        case .classNumber:
            return .yellow
        case .number:
            return .cyan
        }
    }
}

커스텀 셀 만들기

이제 본격적으로 cell 클래스 를 만들어 볼까요? 일단 저희가 만드는 셀은 아주 간단합니다. 그냥 한가운데 label 하나만 덩그러니 있는 cell입니다. 코드에 대해 간단하게 설명 드리겠습니다.

  1. cell 객체에 타입을 프로퍼티로 넣어주겠습니다. 뷰 컨트롤러에서 cell을 사용할 때 주입해줄 겁니다. 위에 정의해둔 배경색과 폰트크기를 적용하는 로직을 didSet에 넣어주었습니다.
  2. private으로 한 이유는 label의 다른 특성에는 cell 외부에서 접근하지 못하게 하기 위해서 입니다. (💊  encapsulation)
  3. private으로 선언된 label이기 때문에 cell 외부에서는 label에 접근할 수 없습니다. text를 외부에서 조정하기 위해서 API를 하나 만들어 줍시다.
class FaceCheckInputCell: UICollectionViewCell {
    
    // MARK: - Properties
    
    //1️⃣ 타입 정의
    var inputCellType: FaceCheckInputType? {
        didSet {
            inputCellLabel.font = inputCellType?.cellTitleFontSize
            backgroundColor = inputCellType?.cellColor
        }
    }
    
    //2️⃣ 레이블
    private let inputCellLabel = UILabel()
    
    // MARK: - LifeCycle
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        configureUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Helpers
    
    func configureUI() {
        self.layer.cornerRadius = 5
        self.layer.borderColor = UIColor.black.cgColor
        self.layer.borderWidth = 2
        
        addSubview(inputCellLabel)
        inputCellLabel.translatesAutoresizingMaskIntoConstraints = false
        inputCellLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        inputCellLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    }
    
    //3️⃣ API
    func setCellText(_ text: String) {
        inputCellLabel.text = text
    }
    
}

CollectionView를 가진 ViewController 구현

UI 구현하기

VC의 UI를 구현해보도록 하겠습니다. 코드가 좀 깁니다. 차근차근 소개해드리겠습니다.

  1. collectionViewCell의 reuseIdentifier는 두번 이상 쓰이는 상수이기 때문에 별도로 선언해두는 것이 좋습니다. 다만 해당 VC에서만 쓰이는 상수이기 때문에 전역으로 선언하기 보다는 fileprivate으로 선언해서 외부에서는 보이지 않도록 하겠습니다. (게다가 다른 VC에서 collection view를 다른 사용하게 되는 경우 reuseIdentifier라는 변수명을 또 쓸 수도 있습니다!)
  2. 깜빡하고 말씀 안드렸는데 학생부 앱은 MVVM 패턴 채택했습니다. 해당 VC에서 사용하는 모든 데이터는 viewModel에서 받아올 예정입니다!
  3. { 코드 }() 방식으로 선언되는 property는 코드를 처음에 선언될 때 단 한번만 실행됩니다! 매번 동적으로 변하는 property를 원한다면 계산 프로퍼티를 활용해야 합니다!
  4. 현재 cell 타입을 정의해놓았습니다. 주목할 점은 didSet인데요. 해당 로직을 통해서 cell 타입이 바뀌면 바로 collectionView를 리로드하도록 해주었습니다.
  5. 코드로 UICollectionView를 선언할 때는 UICollectionViewFlowLayout를 필수적으로 인자로 넣어주어야 합니다!
import UIKit

//1️⃣ reuseIdentifier
fileprivate let reuseIdentifier = "FaceCheckInputCell"

class FaceCheckViewController: UIViewController {
    
    // MARK: - Properties
    
    //2️⃣ 뷰모델
    let viewModel = FaceCheckViewModel()
    
    //3️⃣ {}()
    let studentInfoLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 40)
        label.textAlignment = .center
        return label
    }()
    
    //4️⃣ didSet
    var currentInputCellType: FaceCheckInputType = .grade {
        didSet {
            inputCollectionView.reloadData()
        }
    }
    
    //5️⃣ UICollectionViewFlowLayout
    let inputCollectionView: UICollectionView = {
            
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = 5
        layout.minimumInteritemSpacing = 5
        layout.scrollDirection = .vertical

        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.isScrollEnabled = false
        
        return collectionView
    }()
    
    
    // MARK: - LifeCycle

    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureUI()
        studentInfoLabel.text = viewModel.studentInfoLabelText
    }    
    
    // MARK: - Helpers
    
    func configureUI() {
        view.backgroundColor = .white
        
        inputCollectionView.delegate = self
        inputCollectionView.dataSource = self
        inputCollectionView.register(FaceCheckInputCell.self, forCellWithReuseIdentifier: reuseIdentifier)
        
        view.addSubview(studentInfoLabel)
        studentInfoLabel.translatesAutoresizingMaskIntoConstraints = false
        studentInfoLabel.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
        studentInfoLabel.heightAnchor.constraint(equalToConstant: 50).isActive = true
        studentInfoLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        studentInfoLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
        
        view.addSubview(inputCollectionView)
        inputCollectionView.translatesAutoresizingMaskIntoConstraints = false
        inputCollectionView.topAnchor.constraint(equalTo: studentInfoLabel.bottomAnchor, constant: 50).isActive = true
        inputCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true
        inputCollectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10).isActive = true
        inputCollectionView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10).isActive = true
    }
}

UICollectionViewDataSource

데이터 소스가 담당하는 부분은 cell의 갯수와 cell의 형태입니다.

  1. collection view를 선택한 이유는 입력 버튼의 갯수를 동적으로 조절하기 위해서 입니다. 현재 입력 모드에 따라서 cell의 갯수를 다르게 합니다. 학년은 1, 2, 3학년 3개로 고정이지만 반의 갯수는 학년별로, 그리고 번호의 갯수는 반별로 다르니까 뷰모델에서 받아옵시다.
  2. 커스텀 셀에 입력 유형과 각 버튼에 표시할 제목을 전달합니다.
extension FaceCheckViewController: UICollectionViewDataSource {
	//1️⃣ cell 갯수
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        switch currentInputCellType {
        case .grade: return 3
        case .classNumber: return viewModel.numOfClass
        case .number: return viewModel.numOfStudent
        }
    }
    
    //2️⃣ cell
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? FaceCheckInputCell else { return UICollectionViewCell() }
        cell.inputCellType = currentInputCellType
        let cellText = viewModel.inputCellText(num: indexPath.row + 1, type: currentInputCellType)
        cell.setCellText(cellText)
        return cell
    }
}

UICollectionViewDelegateFlowLayout

collection view를 택한 또 다른 이유는 크기만 정해주면 자동으로 배치해준다는 것입니다. frame을 일일히 정해줄 필요가 없어서 좋습니다. 여기서 크기는 일단은 대강 잡았습니다. 학년은 한 줄에 1개, 반은 한 줄에 3개, 번호는 한 줄에 5개의 cell이 오도록 했습니다. 높이는 학년은 대략 높이의 1/5 나머지 두 종류의 cell의 정사각형이 되도록 했습니다.

코드에 상수를 쓰는 것은 좋지 않지만😅  나중에 리팩토링 할 때 수정하도록 합시다.

extension FaceCheckViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        var width: CGFloat
        var height: CGFloat
        switch currentInputCellType {
        case .grade:
            width = view.frame.width - 20
            height = view.frame.height / 5
        case .classNumber:
            width = (view.frame.width - 20 - 10) / 3
            height = width
        case .number:
            width = (view.frame.width - 20 - 20) / 5
            height = width
        }
        return CGSize(width: width, height: height)
    }
}

UICollectionViewDelegate

collection view를 버튼의 집합처럼 사용할 수 있는 이유는 바로 이 델리게이트 메소드 덕분입니다. 특정 cell을 선택 (= 터치)할 때 실행됩니다. 선택된 경우 currentInputCellType에 따라서 학년, 반, 번호를 뷰모델에 세팅하고 맨 위에 있는 학생 레이블도 업데이트 합니다. 그리고 currentInputCellType을 다음 입력해야 하는 것으로 설정해줍니다!

아까 currentInputCellType을 선언하면서 didSet에 reloadData를 하도록 해주었으므로 여기서 수정하는 즉시 collection view가 리로드 되면서 다음에 입력할 버튼들이 나옵니다!

extension FaceCheckViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        switch currentInputCellType {
        case .grade:
            viewModel.grade = indexPath.row + 1
            studentInfoLabel.text = viewModel.studentInfoLabelText
            currentInputCellType = .classNumber
        case .classNumber:
            viewModel.classNumber = indexPath.row + 1
            studentInfoLabel.text = viewModel.studentInfoLabelText
            currentInputCellType = .number
        case .number:
            viewModel.number = indexPath.row + 1
            studentInfoLabel.text = viewModel.studentInfoLabelText
        }
    }
}

ViewModel 구현하기

VC를 구현하면서 계속 viewModel을 썼는데요. viewModel은 여지껏 안 보여드려서 약간 답답함을 느끼는 분도 있으셨을 것 같네요. 얼른 같이 살펴 볼까요?

class FaceCheckViewModel {
    
    var grade: Int?
    var classNumber: Int?
    var number: Int?
    
    // MARK: - StudentInfoLabelText
    
    var studentInfoLabelText: String {
        let gradeText: String = grade != nil ? "\(grade!)학년" : "?학년"
        let classNumberText: String = classNumber != nil ? "\(classNumber!)반" : "?반"
        let numberText: String = number != nil ? "\(number!)번" : "?번"
        
        return "\(gradeText) \(classNumberText) \(numberText)"
    }
    
    // MARK: - inputCellLabel
    
    func inputCellText(row: Int, type: FaceCheckInputType) -> String {
        let num = row + 1
        switch type {
        case .grade:
            return "\(num)학년"
        case .classNumber:
            return "\(num)반"
        case .number:
            return "\(num)번"
        }
    }
    
    // MARK: - cellNumberdummyData
    
    var numOfClass: Int {
        switch grade {
        case 1: return 10
        case 2: return 12
        case 3: return 9
        default: return 0
        }
    }
    
    var numOfStudent: Int {
        return 30 + grade! - classNumber!
    }
}

너무 한번에 설명드리기 너무 기네요.. 🙀  차근차근 끊어서 설명드려볼께요.

Properties

사용자의 입력값을 저장하는 property입니다. 사용자의 입력을 저장하고 아래 설명할 computed property와 method로 view에 데이터를 전달할 때 사용합니다.

var grade: Int?
var classNumber: Int?
var number: Int?

StudentInfoLabelText

처음에는 아래처럼 코드를 짰었습니다.

//🤮 code too long...

private var gradeText: String {
    guard let grade = self.grade else {
        return "?학년"
    }
    return "\(grade)학년"
}

private var classNumberText: String {
    guard let classNumber = classNumber else {
        return "?반"
    }
    return "\(classNumber)반"
}

private var numberText: String {
    guard let number = number else {
        return "?번"
    }
    return "\(number)번"
}

var studentInfoLabelText: String {   
    return "\(gradeText) \(classNumberText) \(numberText)"
}

하지만 단순한 기능인데 너무 길다고 생각을 했고 3항 연산자를 활용해서 아래처럼 바꾸어 주었습니다. force unwrapping을 사용하는 것이 약간 찝찝하지만 nil 확인이 된 상황이므로 문제는 없습니다!

var studentInfoLabelText: String {
    let gradeText: String = grade != nil ? "\(grade!)학년" : "?학년"
    let classNumberText: String = classNumber != nil ? "\(classNumber!)반" : "?반"
    let numberText: String = number != nil ? "\(number!)번" : "?번"
    
    return "\(gradeText) \(classNumberText) \(numberText)"
}

inputCellLabel

cell에 들어갈 텍스트를 반환합니다. type은 VC만 가지고 있는 것이 좋다고 생각해서 VC에서 인자로 받아왔습니다.

// MARK: - inputCellLabel

func inputCellText(row: Int, type: FaceCheckInputType) -> String {
    let num = row + 1
    switch type {
    case .grade:
        return "\(num)학년"
    case .classNumber:
        return "\(num)반"
    case .number:
        return "\(num)번"
    }
}

cellNumberdummyData

아직 서버가 없습니다😭  일단 동적인 척(?)하기 위해서 더미데이터를 넣어줍시다.

// MARK: - cellNumberdummyData

var numOfClass: Int {
    switch grade {
    case 1: return 10
    case 2: return 12
    case 3: return 9
    default: return 0
    }
}

var numOfStudent: Int {
    return 30 + grade! - classNumber!
}

결과

결과는 아래와 같습니다. (🙀  디자인은 제발 눈 감아주시기 바랍니다......ㅠㅠ)

학년, 반, 번호를 순서에 맞게 선택할 수 있게 되었습니다!!! 추가적으로 계획한대로 반, 번호의 갯수에 따라 동적으로 버튼을 만들어냅니다.

하나만 더! (feat 취소버튼)

취소버튼이 필요해!

열심히 코드를 쓰며 시뮬레이터를 돌려보던 컴동쌤! 하지만 곧 잘못 누르면 앱을 껏다켜야한다는 사실을 깨달았습니다. 만약 이 상태로 쓰라고 하면 아마 최악의 사용자 경험을 제공할 것입니다🤮 

취소버튼을 간단하게 구현해봅시다!

구현

버튼 객체 구현

VC에 캡슐 모양의 취소버튼을 정의합시다. 디자인은 임의로 정했습니다. selector 함수도 하나 연결해줍니다.

let cancelButton: UIButton = {
    let button = UIButton()
    button.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.3)
    button.layer.borderWidth = 1
    button.layer.borderColor = UIColor.black.cgColor
    button.layer.cornerRadius = 50 / 2
    button.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
    return button
}()

selector 구현

  1. grade일 때는 취소버튼을 눌러도 아무 동작도 해서는 안됩니다. (아직 선택한게 없기 때문에)
  2. classNumber일 때는 viewModel에 선택되어 있던 grade를 제거해야 합니다. 그리고 다시 선택할 수 있도록 cell 타입도 돌려놔야 하죠.
  3. number일 때도 마찬가지입니다. 다만 일단은 classNumber와 number를 둘 다 제거합시다. (다음 기능이 구현되면 수정해야할 부분입니다❗️ → 원래는 number를 선택하면 바로 학생 사진이 나오는 VC로 이동할 것이기 때문입니다.)
@objc func cancelTapped() {
    switch currentInputCellType {
    case .grade: return
    case .classNumber:
        viewModel.grade = nil
        currentInputCellType = .grade
    case .number:
        viewModel.classNumber = nil
        viewModel.number = nil
        currentInputCellType = .classNumber
    }
}

isHidden 구현

생각해보니 grade를 선택할 때는 취소 버튼이 안보이는 것이 좋겠군요. (취소할 것이 없으니까요!) 속성을 하나 정의해두고 사용합시다.

private var isCancelButtonHidden: Bool {
    return currentInputCellType == .grade ? true : false
}

아래 코드처럼 두 군데 사용했습니다. 처음에 VC가 로드될 때와 currentInputCellType의 didSet에서 사용하면 우리가 의도한대로 버튼이 상황에 맞게 숨겨질 것입니다.

override func viewDidLoad() {
    super.viewDidLoad()
    configureUI()
    studentInfoLabel.text = viewModel.studentInfoLabelText
    cancelButton.isHidden = isCancelButtonHidden
}
var currentInputCellType: FaceCheckInputType = .grade {
    didSet {
        inputCollectionView.reloadData()
        cancelButton.isHidden = self.isCancelButtonHidden
        studentInfoLabel.text = viewModel.studentInfoLabelText
    }
}

버튼 타이틀 구현

취소 버튼에 타이틀을 구현해봅시다. 타이틀을 단순하게 "취소"라고 하기 보다는 "~~ 다시 선택"의 형식으로 동적으로 구현할까요? isHidden과 마찬가지로 계산 프로퍼티를 하나 추가할께요.

private var cancelButtonTitle: String {
    switch currentInputCellType {
    case .grade:
        return ""
    case .classNumber:
        return "학년 다시 선택"
    case .number:
        return "반 다시 선택"
    }
}

isHidden과 마찬가지의 위치에서 사용하면 됩니다. 다만 isHidden과 타이틀 모두 cancelButton과 관련된 코드인데 이렇게 중복으로 써주기 보다는 하나의 함수로 묶어 줍시다.

func configureCancelButton() {
    cancelButton.isHidden = self.isCancelButtonHidden
    cancelButton.setTitle(cancelButtonTitle, for: .normal)
}

위 함수를 같은 곳에 VC가 로드될 때와 currentInputCellType의 didSet에서 활용하면 의도한 대로 동작할 것 같습니다!

override func viewDidLoad() {
    super.viewDidLoad()
    configureUI()
    configureCancelButton()
}
var currentInputCellType: FaceCheckInputType = .grade {
        didSet {
            inputCollectionView.reloadData()
            configureCancelButton()
            studentInfoLabel.text = viewModel.studentInfoLabelText
        }
    }

subView에 추가

이제 완성된 버튼을 서브뷰에 추가하면 끝입니다.

func configureUI() {
    //... 생략...   

    view.addSubview(cancelButton)
    cancelButton.translatesAutoresizingMaskIntoConstraints = false
    cancelButton.widthAnchor.constraint(equalToConstant: 200).isActive = true
    cancelButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
    cancelButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true
    cancelButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10).isActive = true
}

결과

  1. 취소 버튼을 누르면 student label의 해당 항목이 ?로 리셋되면서 다시 선택할 수 있게 됩니다.
  2. 선택 단계별로 ~~다시 선택이 버튼에 표시됩니다.
  3. 학년을 선택할 때 (취소할 것이 없을 때) 버튼이 표시되지 않습니다.

마치며...

  1. 사실 1일1포스팅이 목표였는데 생각보다 시간이 오래 걸리네요.
  2. 제 iOS 실력은 왕초보 단계입니다. 솔직히 말씀 드리면 클론코딩 한두번을 제외하고 앱을 만들어보는 것은 처음입니다.
  3. 디자인은 앞으로 이렇게 총 천연색으로 만들 예정입니다. 일단은 기능에 충실(?)하고 싶네요.
  4. 공부 + 취준 포트폴리오 연습 개념으로 올린 포스팅인데 다른 분들이 봐주시리라고는 상상도 못했습니다. 댓글로 응원해주신 분들 감사합니다.
  5. 부족한 실력이지만 고군분투 해보겠습니다...
profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글