본문 바로가기

학습 노트/iOS (2021)

001 ~ 006. Interface Builder, Outlet and Action, Delegate Pattern

Interface Builder


interface 구성 방법

  • xib File
    Storyboard 이전에 사용하던 방식.
    모든 씬들이 각각의 파일로 구성됨.
    씬들 사이의 관계에 시각적인 도움이 없으며, 화면 전환까지도 코드로 구현해야 함.
    화면의 로딩 속도와 편집 속도가 빠름, 협업에 조금 더 적합.
  • Storyboard
    하나의 파일 내에 여러 씬들이 존재.
    씬들 사이의 관계가 시각적인 표현으로 조금 더 직관적이지만, 씬이 늘어나면 속도가 느려짐.
  • SwiftUI
    Code로 구성

xib File 방식과 Storyboard는 interfaceBuilder에서 작성한다.

 

Interface Builder & Xcode

 

 

Outlet and Action


Outlet

코드를 통해 속성에 접근할 때 사용하는 연결 방식이다.
속성의 형태를 가지며,
@IBOutlet 특성을 가진다.

Action

컨트롤에서 발생한 이벤트를 처리할 때 사용하는 연결방식이다.
메서드의 형태를 가지며,
@IBAction 특성을 가진다.

  • Outlet과 Action은 반드시 Scene과 연결 괸 class 내부에 추가해야 한다.
  • 씬의 요소를 'ctrl + 드래그', '우클릭 + 드래그' 하는 것으로 씬과 코드를 연결할 수 있다
    코드에 존재하는 이름으로 드래그하면 해당 클래스에 연결한다.
    이름이 아닌 곳에 드래그하면 새 클래스로 연결한다.
  • 연결되면 gutter에 lineNumber 대신 ConnectionWell로 표시된다.
  • ConnectionWell은 연결된 상태라면 속이 탄 동그라미, 아니라면 속이 빈 동그라미로 표시된다.
  • 연결은 ConnectionWell, ConnectionPannel, 씬 자체의 ConnectionPannel 등에서도 가능하다.
  • 연결을 해제할 시 코드와 씬에서 모두 연결을 삭제해야 한다.
  • 코드에 연결되지 않은 Outlet이나 Action이 존재하는 것은 상관없다.
    단, 코드에서 접근하지 않아야 한다.

 

Delegate Pattern


Delegate 패턴은 iOS에서 제공하는 framework를 활용하는 데 있어서 필수적이다.

예를 들어 TableView는 스스로의 처리 능력이 없다.
따라서 TableView는 Delegate(위임) 객체에게 연산을 요구하고,
객체는 연산 후 결과를 반환한다.

이는 제공자가 모든 경우의 수를 판단하는 것이 불가능하고,
가능하다고 하더라도 이를 모두 구현해 두는 것은 대단히 비효율적이다.
따라서 사용자가 직접 이를 구현할 수 있게 해 유연성과 효율을 높이는 것이 Delegate Pattern의 핵심이다.

이러한 Delegate Pattern을 사용하는 속성은
Document에서 '~dataSource'로 끝나거나 '~delegate'로 끝나는 것으로 판단 가능하다.
다음과 같이 설명에 'required'가 포함된 경우는 반드시 구현해야 하는 필수 메서드이다.

 

TableView Delegate Pattern

TableView를 Controller에 Delegate와 DataSource로 연결

ViewController 클래스에 protocol 구현을 추가

//
//  TableViewController.swift
//  newProject
//
//  Created by Martin.Q on 2021/08/03.
//

import UIKit

class TableViewController: UIViewController {
    
    let list = ["A", "B", "C"]

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

extension TableViewController: UITableViewDataSource {
    
}

//
//  TableViewController.swift
//  newProject
//
//  Created by Martin.Q on 2021/08/03.
//

import UIKit

class TableViewController: UIViewController {
    
    let list = ["A", "B", "C"]

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

extension TableViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
    }
    
    
}

 

대부분 해당하는 protocol을 채용하기만 하면 autofix를 통해 필수 메서드를 자동으로 선언할 수 있다.
(autofix는 만능이 아니기 때문에 직접 선언해야 하는 경우도 많다.)

표시할 셀의 수 지정

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return list.count
}

tableView는 몇 개의 셀을 표시해야 할지 계산하거나 알지 못한다.
따라서 tableView(numberOfRowsInSection section:) 메서드를 통해 list 안의 요소의 수를 반환한다.

셀의 내용 지정

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    
    cell.textLabel?.text = list[indexPath.row]
    
    return cell
}

 

셀에 표시할 내용을 작성한다.
list의 내용을 순서대로 표시하도록 구현했다.


결과

 


TextField Delegate Pattern

TextField를 ViewController와 Delegate로 연결

ViewController에 Class 지정

TextField를 Class에 Outlet으로 연결

protocol 구현 추가

//
//  TFViewController.swift
//  newProject
//
//  Created by Martin.Q on 2021/08/03.
//

import UIKit

class TFViewController: UIViewController {

    
    @IBOutlet weak var TF: UITextField!
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        TF.delegate = self
        
    }

}

extension TFViewController: UITextFieldDelegate {
    
}

이후 TextField에 접근해 delegate로 자신을 지정하고,
extension을 통해 UITextFieldDelegate를 채용한다.

extension TFViewController: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        return true
    }
}

TextField는 글자가 입력될 때마다 위의 메서드를 호출한다.
true를 반환할 경우 편집된 내용을 반영하고, false인 경우 무시한다.
이를 이용해 TextField의 문자수를 제한하거나 입력되는 문자를 구별할 수도 있다.


결과

 


Custom Delegate Pattern

//
//  MainViewController.swift
//  newProject
//
//  Created by Martin.Q on 2021/08/02.
//

import UIKit

class MainViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    
    @objc func presentComposeVC() {
        performSegue(withIdentifier: "InputSegue", sender: nil)
    }
    
    @IBAction func add(_ sender: Any) {
        presentComposeVC()
    }
    
    override func viewDidLoad() {
    
    }
}

Main View Controller와 연결되어 있는 클래스이다.

해당 파일 내에서는 Label을 연결했고, 내비게이션 바에 '+' 버튼을 추가하고 있다.
해당 버튼을 누르면 presentComposeVC 메서드를 호출하고,
해당 메서드는 segue를 실행한다.

최종적으로 Input 씬에서 textField에 내용을 입력하고, 'Done'을 누르면
씬에서 해당 내용을 표시하도록 Delegate 패턴을 이용해 구현한다.

protocol 선언

//
//  MyDelegate.swift
//  newProject
//
//  Created by Martin.Q on 2021/08/03.
//

import UIKit

protocol MyDelegate {
    func composer(_ vc: UIViewController, didInput value: String?)
    func composeCancel(_ vc: UIViewController)
}

각각 입력된 값을 전달할 때, 입력을 취소했을 때 사용한다.
해당 프로토콜을 채용할 때에는 두 메서드를 반드시 구현해야 한다.

delegate 선언

//
//  InputViewController.swift
//  newProject
//
//  Created by Martin.Q on 2021/08/03.
//

import UIKit

class InputViewController: UIViewController {
    
    var delegate: MyDelegate?

    @IBOutlet weak var textField: UITextField!
    
    
    @IBAction func cancel(_ sender: Any) {
    }
    
    @IBAction func done(_ sender: Any) {
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()

    }
    
}

값을 실제로 입력받는 Input 씬은 Main 씬의 Label을 직접 변경하지 못한다.
따라서 Main씬이 Delegate를 넘겨받아 Label을 바꿀 수 있도록, Input 씬에서 delegate를 넘겨줘야 한다.
Input의 클래스 파일에서 delegate를 선언한다.
해당 delegate는 방금 작성 한 MyDelegate를 사용하고, Optional 형식이다.

delegate 패턴 구현

//
//  MainViewController.swift
//  newProject
//
//  Created by Martin.Q on 2021/08/02.
//

import UIKit

class MainViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let vc = segue.destination.children.first as? InputViewController {
            vc.delegate = self
        }
    }
    
    @objc func presentComposeVC() {
        performSegue(withIdentifier: "InputSegue", sender: nil)
    }
    
    @IBAction func add(_ sender: Any) {
        presentComposeVC()
    }
    
    override func viewDidLoad() {
    
    }
}

이후 Delegate를 넘겨받을 Main의 클래스 파일로 돌아와,
prepare 메서드 안에서 delegate를 자신으로 설정한다.
prepare 메서드는 segue를 사용한 이후 씬이 전환되기 전 실행된다.

이후 나타나는 에러를 해결하기 위해선 우리가 채용 조건으로 걸었던 두 개의 메서드를 구현해야 한다.

//
//  MainViewController.swift
//  newProject
//
//  Created by Martin.Q on 2021/08/02.
//

import UIKit

class MainViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let vc = segue.destination.children.first as? InputViewController {
            vc.delegate = self
        }
    }
    
    @objc func presentComposeVC() {
        performSegue(withIdentifier: "InputSegue", sender: nil)
    }
    
    @IBAction func add(_ sender: Any) {
        presentComposeVC()
    }
    
    override func viewDidLoad() {
        
        }
}

extension MainViewController: MyDelegate {
    func composer(_ vc: UIViewController, didInput value: String?) {
        label.text = value
    }
    
    func composeCancel(_ vc: UIViewController) {
        label.text = "Canceled"
    }
}

위와 같이 extension을 사용해 MyDelegate를 채용하고,
채용 조건이었던 두 개의 메서드를 구현한다.

메서드들은 각각 전달받은 값과 Canceled로 Main 씬의 Label을 변경한다.

//
//  InputViewController.swift
//  newProject
//
//  Created by Martin.Q on 2021/08/03.
//

import UIKit

class InputViewController: UIViewController {
    
    var delegate: MyDelegate?

    @IBOutlet weak var textField: UITextField!
    
    
    @IBAction func cancel(_ sender: Any) {
        delegate?.composeCancel(self)
        dismiss(animated: true, completion: nil)
    }
    
    @IBAction func done(_ sender: Any) {
        delegate?.composer(self, didInput: textField.text)
        dismiss(animated: true, completion: nil)
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()

    }
    
}

이젠 Main 씬에서 Delegate를 받을 준비가 되었으므로,
Input 씬에서 실질적으로 Delegate를 전달해야 한다.

Delegate를 전달하는 것은 전달할 클래스에서 구현한 메서드를 호출 하는 것으로 간단히 구현된다.

각각의 버튼에 따라 알맞은 메소드를 호출하고, dismiss 메소드를 사용해 이전 화면으로 되돌아가도록 한다.


결과