1.6 Intermediate Table Views

Joohyun·2022년 5월 12일
0

Custom Table View Cells

  1. Attributes inspector에서 table view cell의 Style을 'Custom'으로 설정한다.

  2. stack을 적절히 삽입하여 원하는 cell style을 구성한다.

    • Constraint to margins

      • cell 내부의 1번째 stack의 'Constraint to margins'를 체크하여 cell과 stack 사이의 적절한 여백을 남긴다.

    • Content Hugging Priority

      • 상대적인 숫자를 통해서 Auto Layout 엔진에게 content를 감쌀 view의 우선순위를 알려준다.

        ex) emoji label의 horizontal content hugging priority를 251에서 252로 변경시켜 emoji label이 content를 감싸 수축되도록 Auto Layout에게 우선순위를 부여한다.


  3. UITableViewCell의 subclass를 생성하고, storyboard에서 cell의 custom class로 연결해준다.

  4. 각 label의 outlet을 생성하고, label을 업데이트하는 method를 생성한 후, 기존의 tableView(_:cellForRowAt:) method를 수정한다.

    • 기존 tableView(_:cellForRowAt:) method의 코드를 cell에게 넘김으로써 추상화를 만족시키는 일반적인 pattern이다.
    func update(with emoji: Emoji) {
        symbolLabel.text = emoji.symbol
        nameLabel.text = emoji.name
        descriptionLabel.text = emoji.description
    }
    override func tableView(_ tableView: UITableView, cellForRowAt
       indexPath: IndexPath) -> UITableViewCell {
       
        //Step 1: Dequeue cell
        //dequeueReusableCell method는 UITableViewCell instance를 반환하므로 update method 사용을 위해 force-downcast를 통해 EmojiTableViewCell class로 변환한다.
        let cell = tableView.dequeueReusableCell(withIdentifier: "EmojiCell", for: indexPath) as! EmojiTableViewCell
     
        //Step 2: Fetch model object to display
        let emoji = emojis[indexPath.row]
     
        //Step 3: Configure cell
        cell.update(with: emoji)
        cell.showsReorderControl = true
     
        //Step 4: Return cell
        return cell
    }

Edit Table Views

editing style

1. .none

  • edit control 비허용

2. .delete

  • delete editing control

3. .insert

  • insert editing control

수행 순서

  • table view가 editing 상태로 들어갈 때, 다음과 같은 순서로 data source, delegate method가 호출된다.

    1. tableView(_:canEditRowAt:) (data source method)

    • edit 상태로부터 특정 row들을 제외시킨다.
    • 대부분의 app에선 해당 method가 필요하지 않다.

    2. tableView(_:editingStyleForRowAt:) (delegate method)

    • row의 editing style을 결정한다.
    • 해당 method를 작성하지 않으면, table view는 delete control을 사용한다.
    • 이 method를 통해 table view는 완전히 editing mode에 진입한다.

    3. 사용자가 editing control을 tap한다.

    • ex) delete control을 tap하는 경우, row는 확인을 위해 'Delete' 버튼을 띄운다.

    4. tableView(_:commit:forRowAt:) (data source method)

    • 3번의 사용자 액션 결과를 data model에 반영한다.
    • 해당 protocol method는 optional 형태이지만, row를 삭제 또는 추가하기 위해선 필수적으로 다음 2가지가 작성되어야 한다.
      • item을 삭제하거나 추가함으로써 상응하는 data source 업데이트
      • 적절한 row를 삭제하거나 추가하기위해 table view를 deleteRows(at:with) 또는 insertRows(at:with)로 전송

Delete Items

1. tableView(_:canEditRowAt:) (data source method)

  • 대부분의 app에선 edit 상태로부터 특정 row들을 제외시킬 필요가 없으므로 넘어간다.

2. tableView(_:editingStyleForRowAt:) (delegate method)

  • delete control style 설정
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
   return .delete
}

4. tableView(_:commit:forRowAt:) (data source method)

  • 사용자가 control을 tab할 때 실행될 table view을 통해 표시되는 model data와 table view를 업데이트하는 로직 작성
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        emojis.remove(at: indexPath.row)
           
        // at: 구체적으로 삭제할 row의 indexpath array
        // with: row animations (UITableView.RowAnimation enum)
        tableView.deleteRows(at: [indexPath], with: . automatic)
    }
}

row animation documentation 바로가기

Add Items

table view의 row를 추가하는 2가지 방법

1. 빈 row에서 .insert control 사용

  1. navigation bar에 Add(+) 버튼을 추가해서 사용자가 property를 작성할 수 있는 새로운 view controller(static table view) 띄움
    (해당 view controller는 edit mode에서 재사용 가능)

    • input 화면을 scroll view가 아닌 table view 로 구현하는 이유
      • scroll view와 달리 table view controller는 코드 작성 없이 자동으로 키보드가 나타날 때, text field를 키보드 위로 올려준다.

Static Table Views

  • 보여줄 row의 수와 cell의 type을 정확히 알고있는 table view
  • row의 갯수가 변하지 않을 때 유용하다. (ex. Setting App)
  • data source protocol을 작성하지 않고, table view data를 전달하기 위해 viewDidLoad() method를 사용하는 table view controller를 갖는다.
    (빈 data source method 사용을 통해 empty table view 가짐)
  • 2번 방법을 통해 Add Item 구현

1. Static Table Views 생성

  1. storyboard에서 Object library를 통해 table view controller를 가지는 navigation controller를 추가한다.

  1. UITableViewController의 subclass를 생성하여 해당 static table view의 Custom Class로 등록한다.

  2. @IBSegueAction을 사용하여 전달될 data(여기선 Emoji)를 위해 custom initializer를 생성한다.
    (Xcode 'fix-it'버튼을 통해 issue 수정)

init?(coder: NSCoder, emoji: Emoji?) {
    self.emoji = emoji
    super.init(coder: coder)
}
  1. storyboard로 돌아가 list table view scene에 bar button을 추가하고 button의 system item을 'Add'로 설정한다.

  2. modal presentation segue를 생성한다.

    • Add bar button -> new navigation controller
    • cell -> new navigation controller

Human Interface Guideline
새로운 data를 생성하거나, 존재하는 data를 수정할 때 static table controller를 modal로 띄우는 modal presentation segue 사용

  1. 기존 navigation controller의 tableView(_:didSelectRowAt:) method를 제거한다.

    • cell -> navigation controller segue가 생성됨으로 인해, 더이상 기존 navigation controller가 선택된 cell 정보를 다루지 않는다.
  2. navigation controller와 static table view controller 사이의 relation controller에 대한 @IBSegueAction을 기존 view controller에 생성한다.

    • static table view controller로 initialize할 data를 전달 목적

@IBSegueAction func addEditEmoji(_ coder: NSCoder, sender: Any?) -> AddEditEmojiTableViewController? {

    // sender가 cell이라면 edit mode
    if let cell = sender as? UITableViewCell,
        let indexPath = tableView.indexPath(for: cell) {
        // Editing Emoji
        let emojiToEdit = emojis[indexPath.row]
        return AddEditEmojiTableViewController(coder: coder, emoji: emojiToEdit)
        
    // sender가 cell이 아니라면 add mode
    } else {
        // Adding Emoji
        return AddEditEmojiTableViewController(coder: coder, emoji: nil)
    }
}
  1. Attributes inspector에서 new table view controller scene의 Content를 'Static Cell'로 변경한다.

  2. table view의 style을 설정(Grouped)하고 원하는 구조로 section의 수와 각 section의 row 수를 설정한다.

  3. 각 cell에 text field를 추가하고 constraints를 설정한다.

2. Pass Data to Static Table View

  1. new table view controller에 각 text field의 outlet을 등록한다.

  2. viewDidLoad() method 내부에 instance가 값을 갖고 있는지 체크한다.

// Edit Mode
// 값이 존재한다면, 각 text field를 해당 instance의 property로 업데이트
if let emoji = emoji {
    symbolTextField.text = emoji.symbol
    nameTextField.text = emoji.name
    descriptionTextField.text = emoji.description
    usageTextField.text = emoji.usage
    title = "Edit Emoji"
    
// Add Mode
// 값이 nil
} else {
    title = "Add Emoji"
}

3. Add Action Buttons with Unwind Segue

Human Interface Guideline
새로운 table view를 아래로 swipe함으로써 지울 수 있지만, dismiss button을 추가해주는게 좋다.

  1. 'Cancel', 'Save' style의 bar button item을 각각 생성한다.

    • navigation bar에 modal view controller를 지우는 역할
  2. 기존 table view controller에 unwindToEmojiTableView(segue:) method를 추가하고, Save, Cancel button을 Exit icon를 통해 method와 연결시킨다.

  1. Save button에 identifier를 설정한다.

    • unwind segue가 Save, Cancel button 중 어디서 출발한 것인지 체크하기 위한 목적

4. Update Save Button

  1. symbolTextField가 single emoji character를 가져야만 save button이 활성화되도록 method를 추가한다.
func containsSingleEmoji(_ textField: UITextField) -> Bool {
    guard let text = textField.text, text.count == 1 else {
        return false
    }
 
    let isCombinedIntoEmoji = text.unicodeScalars.count > 1 &&
       text.unicodeScalars.first?.properties.isEmoji ?? false
    let isEmojiPresentation = text.unicodeScalars.first?.properties.isEmojiPresentation ?? false
    
    return isEmojiPresentation || isCombinedIntoEmoji
}
  1. 위의 조건을 만족하고 모든 text field가 value를 가질 때 Save button을 활성화시키는 method를 생성한다.
func updateSaveButtonState() {
    let nameText = nameTextField.text ?? ""
    let descriptionText = descriptionTextField.text ?? ""
    let usageText = usageTextField.text ?? ""
    saveButton.isEnabled = containsSingleEmoji(symbolTextField) && 
    	!nameText.isEmpty && !descriptionText.isEmpty && 
    	!usageText.isEmpty
}

중위연산자 ??
두 값을 비교하여 왼쪽의 값이 nil이라면, 오른쪽 값을 반환

  1. modal이 나타날 때 save button의 활성화 여부를 결정하기 위해 생성한 method를 viewDidLoad()에서 호출한다.
override func viewDidLoad() {
    super.viewDidLoad()
 
    if let emoji = emoji {
        symbolTextField.text = emoji.symbol
        nameTextField.text = emoji.name
        descriptionTextField.text = emoji.description
        usageTextField.text = emoji.usage
        title = "Edit Emoji"
    } else {
        title = "Add Emoji"
    }
 
    updateSaveButtonState()
}
  1. 키가 눌릴 때마다 save button의 활성화 여부를 결정하기 위해 @IBAction을 생성하고 해당 method에 모든 text field를 연결한다.
@IBAction func textEditingChanged(_ sender: UITextField) {
    updateSaveButtonState()
}

5. Save

  • Save 버튼이 눌렸을 때, saveUnwind segue가 수행되고 collection이 업데이트될 수 있다.

  • segue가 호출되기 전, 새로운 instance를 생성하고 property를 세팅하기 위해 text field 값이 사용된다.

  1. static table view controller에 saveUnwind segue가 수행되었는지 확인 후 property를 업데이트하는 prepare(for segue:) method를 추가한다.

    • Cancel 버튼을 누를 경우 업데이트를 하지 않는다.

    • text field의 optional value가 nil값이 아님을 Save 버튼에서 확인했기 때문에 예외처리에 신경쓰지 않아도 된다.

override func prepare(for segue: UIStoryboardSegue,
   sender: Any?) {
 
    guard segue.identifier == "saveUnwind" else { return }
 
    let symbol = symbolTextField.text!
    let name = nameTextField.text ?? ""
    let description = descriptionTextField.text ?? ""
    let usage = usageTextField.text ?? ""
    
    emoji = Emoji(symbol: symbol, name: name, description: description, usage: usage)
}
  1. unwindToEmojiTableView(segue:) method로 돌아와 saveUnwind segue가 수행되었는지 확인 후, table view에 여전히 선택된 row가 있는지 체크한다.

    • 선택된 row가 있는 경우

      • 특정 instance 값을 수정한 후 unwind한다.
    • 선택된 row가 없는 경우

      • 새로운 instance를 생성하고, 새로운 row를 위한 index path를 계산 후 collection의 끝에 해당 item을 추가한다.
@IBAction func unwindToEmojiTableView(segue: UIStoryboardSegue) {
    guard segue.identifier == "saveUnwind",
        let sourceViewController = segue.source as? AddEditEmojiTableViewController,
        let emoji = sourceViewController.emoji else { return }
 
    if let selectedIndexPath = tableView.indexPathForSelectedRow {
        emojis[selectedIndexPath.row] = emoji
        tableView.reloadRows(at: [selectedIndexPath], with: .none)
    } else {
        let newIndexPath = IndexPath(row: emojis.count, section: 0)
        emojis.append(emoji)
        tableView.insertRows(at: [newIndexPath], with: .automatic)
    }
}

Compression Resistance Value

  • 모든 view는 content hugging priority와 유사한 view의 수축을 어떻게 제한할 것인지 표현하는 compression resistance value를 가진다.

  • 기본값은 750이며, 값이 클수록 Auto Layout engine은 수축을 피하기위해 다른 것보다 우선순위를 갖는다.

Automatic Row Height

  • table view cell의 label에 긴 text가 입력된다면, text가 잘리는 것을 볼 수 있다.

  • tableView(_:heightForRowAt:)을 작성하여 cell이 전체 text를 보여줄 수 있도록 height를 계산하는 방법이 있지만 성가시고 에러가 발생할 수 있다.

Compression Resistance Value를 통해 low height를 조절하는 방법

  1. Auto Layout engine에게 view content의 우선순위를 제공한다.

    • Interface Builder 입장

      • code에 접근할 수 없으므로 cell height가 자동으로 계산될 것을 알지 못하고 상수의 height를 갖는다.
    • Auto Layout engine 입장

      • text의 길이가 현재 label 크기를 넘어설경우 어떤 일이 발생할지 보장하지 못한다.
    • 해당 label의 vertical compression resistance를 주변값보다 높게 설정하여 text의 양에 따라 커질 수 있게 한다.

  2. table view가 cell의 content에 따라 자동으로 row height을 결정할 수 있도록 한다.

    • viewDidLoad() method에 아래의 코드를 추가한다.

    • UITableView.automaticDimension을 통해 cell height을 계산함으로써 stack view height가 증가한다.

      tableView.rowHeight = UITableView.automaticDimension
      // 퍼포먼스 향상을 위해 cell의 평균 height를 제공
      tableView.estimatedRowHeight = 44.0

profile
Developer

0개의 댓글