# 화면을 그리는 방식

웹은 마크업 언어를 통해 화면을 그리고 DOM 조작 등의 내부기능들은 모두 자바스크립트를 통해 이루어지는데 이는 화면을 그리는 메커니즘과 실제 동작의 메커니즘이 분리되어 있음을 나타낸다.

웹은 브라우저 엔진 위에서 동작하며 운영체제와는 완전 독립적이다.

반면 iOS는 오브젝트를 올려놓고 배치하며 오브젝트들의 속성과 메서드가 모두 내장되어 있다. 한 클래스에 화면을 그리는 메커니즘과 내부 동작(메서드)이 공존하는 형태이다.

ViewController의 view

ViewController 클래스 내에서는 기본적으로 view라는 객체를 사용할 수 있다. View 컴포넌트이고 ViewController 전체를 감싸는 최상위 뷰 컴포넌트이다. 쉽게 생각해서 html태그 정도로 이해하면 될 것 같다.

viewDidLoad 시점에서 UI그리기

viewDidLoad 함수 호출시 초기 UI 세팅에 필요한 코드들은 외부 함수로 꺼내 정리하는것이 일반적이다.

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var mainLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        configureUI()
    }

    func configureUI(){
        mainLabel.text = "초를 선택하세요 ☺️"
    }
}

# 슬라이더

UISlider 컴포넌트를 스토리보드에서 끌어다가 코드 영역에 두면 이벤트 선택이 가능하다. 슬라이더에서 사용되는 각종 이벤트들 중 value changed 이벤트를 사용하면 값이 변할때마다 어떤 함수를 실행할지 동작을 정의할 수 있다.

@IBOutlet weak var slider: UISlider!

@IBAction func buttonTapped(_ sender: UIButton) {
    slider.setValue(0.5, animated:true) // 슬라이더 중간 설정 및 애니메이션 효과 추가
}

타이머

타이머는 아래와 같이 사용한다. 방법이 다양하므로 다른 문서 참조가 더 필요함

class ViewController: UIViewController {
    // ...

    var timer = Timer() // 타이머 객체 생성

        @IBAction func startButtonTapped(_ sender: UIButton) {
        // 시작버튼 클릭시 기존 타이머가 있었다면 비활성화
        timer.invalidate()

        // scheduledTimer 메서드 사용
        // withTimeInterval, repeats, block 파라미터를 사용하고 다른 구성도 존재한다
        // block은 클로저를 받는다
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [self] _ in

            if(number > 0){
                number -= 1
                slider.setValue(Float(number) / Float(60), animated: true)
                mainLabel.text = "\(number) 초"
            }else{
                timer.invalidate()
                mainLabel.text = "🔥 타임아웃 🔥"
                AudioServicesPlayAlertSound(SystemSoundID(1300))
            }

        })
    }
}

클래스 내에서 Timer객체를 생성한 뒤 특정 이벤트에 따라 타이머가 생성되고 실행되는 과정에서 invalidate을 잘 시켜줘야 한다.

block 파라미터에 클로저 형태로 동작을 정의할 수 있다.

기호에 따라 AVFoundation 프레임워크를 임포트 하고, AudioServicesPlayAlertSound 메서드를 실행하며 SysteSoundID(사운드 아이디값)객체를 파라미터로 전달하면 타이머 끝남과 동시에 시스템 사운드를 재생할 수 있다.

# 델리게이트 패턴

textField

스위프트에서의 텍스트필드는 UITextField를 사용한다.

@IBOutlet weak var textField: UITextField!

텍스트필드 중 일반적으로 사용된 속성 몇가지를 정리하면

  1. textField.placeholder
  2. textField.clearButtonMode - 입력칸 비우기 버튼 모드를 설정

등등.. 여러가지가 있다.

텍스트필드에서 키보드를 통한 입력은 OS의 영역이다. 키보드 입력 자체만으로 데이터를 휘발시키는 것이 아니라 클래스 자체적으로 키보드를 통해 입력받은 값들에 대한 처리를 추가적으로 해줘야한다.

이때 필요한 것이 ViewController 클래스에 UITextFieldDelegate 프로토콜을 채택하여 메서드를 구현하는 것이다.

기본적으로 텍스트필드는 뷰컨트롤러 클래스와 완전히 독립된 형태로 존재한다. 값에 대한 사후동작 처리를 위해 텍스트 필드의 대리자 역할을 ViewController로 지정해야 한다.

// UITextFieldDelegate 프로토콜 채택
class ViewController: UIViewController, UITextFieldDelegate {

    // 텍스트필드 Outlet 연결
    @IBOutlet weak var textField: UITextField!

    // 텍스트필드 대리자를 ViewController로 지정
    override func viewDidLoad() {
        super.viewDidLoad()
        textField.delegate = self
    }

}

UITextFieldDelegate 프로토콜에서 선택적으로 구현할 수 있는 요구사항은 textFieldShould....textFieldDid....의 종류로 나뉜다. Should는 동작을 허용할지 말지를 결정하고 Did는 동작이 이루어진 시점을 활용할때 사용한다.

Should는 불리언 값을 리턴하는 함수이며 Did는 리턴 형태가 Void이다.

onChange와 유사한 메서드는 아래와 같다.

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

onSubmit과 유사한 메서드는 textFieldShouldReturn(_ textField: UITextField) -> Bool가 있다. textFieldShouldReturntextFieldDidEndEditing은 유사해보이지만 textFieldEndEditing은 텍스트필드로부터 포커싱이 벗어났을때 실행되는 메서드이다.

ViewController에서의 구현

일반적으로 텍스트필드 델리게이터 패턴 대상 함수는 ViewController를 확장한 뒤 UITextFieldDelegate 프로토콜을 채택하여 구현한다.

extension ViewController: UITextFieldDelegate{
    func textFieldDidBeginEditing(_ textField: UITextField) {
        // ...
    }
    func textFieldDidEndEditing(_ textField: UITextField) {
        // ...
    }
}

예제 코드를 살펴보자.

protocol RemoteControlDelegate {
    func channelUp()
    func channelDown()
}

class RemoteControl {

    var delegate: RemoteControlDelegate?

    func doSomething() {
        print("리모콘의 조작이 일어나고 있음")
    }

    func channelUp() {   // 어떤 기기가 리모콘에 의해 작동되는지 몰라도 됨
        delegate?.channelUp()
    }

    func channelDown() {   // 어떤 기기가 리모콘에 의해 작동되는지 몰라도 됨
        delegate?.channelDown()
    }
}

class TV: RemoteControlDelegate {

    func channelUp() {
        print("TV의 채널이 올라간다.")
    }

    func channelDown() {
        print("TV의 채널이 내려간다.")
    }

}

let remote = RemoteControl()
let samsungTV = TV()

remote.delegate = samsungTV

remote.channelUp()        // 리모콘 실행 ====> delegate?.channelUp()
remote.channelDown()

델리게이트 패턴의 흐름을 위 코드에 따라 정리하면 아래와 같다.

  1. 텍스트필드 등 델리게이트 패턴 적용 대상에 대해 사용될 기능 묶음을 프로토콜로 정의한다. ~~Delegate형태로 이름이 지어진다.
  2. 프로토콜 타입의 인스턴스를 속성으로 갖는 클래스를 정의한다. 실제 예시와 비교하면 UITextField클래스가 이에 해당한다. UITextField는 내부적으로 weak open var delegate: UITextFieldDelegate? 속성을 갖고있다.
  3. 델리게이트 프로토콜에 대한 인스턴스를 속성으로 갖는 클래스 메서드는 프로토콜 인스턴스의 구현 대상 함수를 호출하게 된다. 위 코드에서 delegate?.channelUp()에 해당한다. 실제 예시에서는 func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool 함수 등이 이에 해당한다.
    • UITextFieldDelegate 델리게이트 프로토콜을 UIViewController에서 채택하기때문에 뷰컨트롤러 내에서 프로토콜에 정의된 함수들을 직접 구현하여 사용하게 된다.
    • UITextFieldDelegate 프로토콜의 구현 함수들은 필수 구현이 아니기 때문에 구현하지 않아도 에러가 나지 않는다.
  4. UITextField 객체를 IBOutlet으로 가져온 후 viewDidLoad에서 델리게이트(대리자) 지정을 한다.
    • UITextField는 클래스이고, 내부에 UITextFieldDelegate 프로토콜 타입의 속성을 갖는다고 위에서 언급하였다.
    • UITextField 클래스에 대한 객체의 delegate 속성을 textField.delegate으로 접근할 수 있고, 대리자는 self로 지정한다.
    • textField.delegate = self로 지정하면 텍스트필드에 대한 대리자가 뷰컨트롤러가 된다. 따라서 이후 뷰컨트롤러가 텍스트 필드에서 이루어지는 각종 이벤트들을 대신 관리할 수 있게 된다.

텍스트 입력 수 제한하기

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    guard let text = textField.text else {return false}

    print("range length : \(range.length)")
    print("string count: \(string.count)")
    print("text count: \(text.count)")
    return true
}

텍스트필드 프로토콜 중 shouldChangeCharactersIn 함수를 사용하면 입력에 대한 제어를 할 수 있다. guard let 바인딩을 통해 텍스트필드 컴포넌트에 입력된 옵셔널 스트링을 언래핑한뒤 출력을 하는 코드이다.

shouldChangeCharactersIn은 실제 데이터의 입력 전에 판단을 하기 때문에 특별한 함수 파라미터 없이는 10글자 제한같은 로직을 구현할 수가 없다. guard let 바인딩을 통해 뽑아놓은 스트링은 현재 9자인데, shouldChangeCharactersIn 조건에 따라 입력을 추가적으로 받으면 최종 스트링이 10자가 되고 이때부터 대리자는 키보드 입력 자체를 막아버리기 때문이다.

따라서 글자수 제한을 할때에는 replacement 파라미터를 반드시 사용해야 한다.

출력해보면 알겠지만, string 파라미터의 count 속성은 글자가 추가될때 1이되고 글자가 삭제될때 0이된다.

range 파라미터는 NSRange타입인데, 출력되는 중괄호 형태에서 0번째는 문자열 수정이 이루어지는 위치를 나타내고 1번째 요소는 수정이 문자 추가 형태로 이루어진것인지 삭제 형태로 이루어진 것인지 판단한다. 가령 {7, 0}라는 출력 형태라면 7번째 위치에서 문자 추가가 이루어진 것이다.

becomeFirstResponder

UIResponder라는 클래스가 존재한다. 텍스트필드와 같은 오브젝트에 대해 textField.becomeFirstResponder()로 UI를 세팅하게 되면 텍스트필드에서 키보드가 자동으로 올라오는 등 사용자에게 입력 포커스를 자동으로 제공한다.

반대로 포커스를 없애는 메서드도 있다.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    textField.resignFirstResponder()
}

// 뷰의 키보드를 내림
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    self.endEditing(true)
}

# 커스텀 델리게이트 패턴

커스텀 델리게이트 패턴이 필요한 이유는 다음과 같다.

우리가 화면전환시 데이터를 주고받게 되는데, 테이블 뷰를 예시로 서브 뷰 컨트롤러로부터 받은 데이터를 루트 뷰 컨트롤러인 테이블뷰에 업데이트를 하는 상황이 있다.

이때 화면 이동에 앞서 navigationController?.viewControllers[인덱스]를 통해 컨트롤러 인스턴스에 직접 접근하여 저장속성 데이터를 변경해주는 방식으로 구현할 수 있는데, 이때 발생하는 문제는 viewWillAppear와 같은 함수 내에서 tableView.reloadData같은 함수를 실행하게 되면 이전 뷰로 이동할 때마다 매번 실제로 필요하지 않는 리로드 동작을 추가적으로 하게 된다는 것이다.

이때 필요한것이 커스텀 델리게이트 패턴이다. 좀 더 동작에 최적화 되도록 대리자와 동작을 밀착시키는 것이다.

커스텀 델리게이트를 정의할때 먼저 파악해야 하는 것은 동작과 동작에 대한 대리자이다.

위의 예시에서 서브 뷰에서 이전 뷰로 데이터를 업데이트하여 전달할때 이에 대한 동작은 데이터의 업데이트이고, 동작에 대한 대리자는 이전 뷰가 된다.

그렇다면 동작의 정의는 어디에서 해야할까? MVC패턴에 따라 리팩토링한 프로젝트라고 가정할때 데이터가 관리되는 모델이 있을텐데 이 모델에 대해 데이터를 수정하는 로직을 추가적으로 프로토콜로써 정의를 한 뒤에 이를 대리자가 채택하여 구현하는 방식으로 진행하면 되는 것이다.

import UIKit

struct Member{
    var name: String?
}

protocol MemberDelegate{
    func update(_ member: Member)
    func addNewMember(index: Int, _ member: Member)
}

프로토콜을 채택했으면 위의 동작을 실제로 실행하게 될 대리자 뷰 컨트롤러를 확장하여 위 함수들을 구현하면 된다.

extension ViewController: MemberDelegate{
    func update(index: Int, _ member: Member) {
        memberManager.updateMemberInfo(index: index, member)
        tableView.reloadData()
    }

    func addNewMember(_ member: Member) {
        memberManager.makeNewMember(member: member)
        tableView.reloadData()
    }
}

또한 마지막으로 델리게이트 패턴시 가장 중요한 delegate 속성을 통해 대리자를 지정하는 코드가 있다.

대리자와 실제 동작을 하는 컨트롤러 둘의 관계를 다시 살펴보면 다음과 같다.

  1. B라는 뷰 컨트롤러는 A에서 관리하는 데이터 모델의 수정을 한 뒤 A에게 다시 넘겨준다.
  2. A는 수정된 데이터를 받아 UI에 반영한다.
  3. 이때 A컨트롤러 생명주기와 최적화를 시키기 위해 커스텀 델리게이트 패턴을 적용한다.
  4. 모델에 델리게이트 프로토콜을 정의하고 모델 수정의 동작이 실제로 이루어지는 컨트롤러, 즉 A에서 이를 채택한다.
  5. 그렇다면 모델 수정을 의뢰하는 컨트롤러는 자동으로 B가 된다.
  6. 따라서 최종적으로 B라는 컨트롤러는 delegate라는 저장속성을 가져야한다.

delegate 저장속성은 var delegate: MemberDelegate?과 같이 MemberDelegate? 옵셔널 타입으로 선언하며 참고로 MemberDelegate라는 타입은 예시로 작성한 이름이므로, 모델 프로토콜에 정의한 대로 델리게이트 타입으로 저장속성을 선언하면 된다.

특정 동작에 따른 대리자의 동작이 요구될 때에 대리자에서 채택한 프로토콜의 구현 함수를 호출하면 된다.

delegate?.update(index: memberId, member)

강한 순환 참조문제

화면 이동 로직구현을 위한 델리게이트 패턴 적용시 이전 뷰 컨트롤러와 현재 뷰 컨트롤러 간의 강한 순환 참조 문제가 발생할 수 있다.

이때 var delegate: CustomDelegate?로 선언했던 저장속성을 weak var delegate: CustomDelegateweak 선언을 하면 된다.

다만 약한참조는 클래스에서 채택한 프로토콜에 대해서만 사용 가능하기 때문에 델리게이트 프로토콜 정의 시 AnyObject 클래스를 상속받아야 한다.

protocol MemberDelegate: AnyObject{
    func update(index: Int, _ member: Member)
    func addNewMember(_ member: Member)
}

커스텀 델리게이트 패턴과 클로저

테이블 뷰 셀이 있다고 가정해보자. 셀 위의 버튼 클릭시 다음화면으로 이동하는 로직을 구현한다고 하였을때 구현할 수 있는 방법은 어떤 것이 있을까?

커스텀 델리게이트 패턴을 사용하는 방법은 프로토콜 하나를 정의하고 해당 프로토콜을 채택하여 채택 함수에 performSegue를 삽입하는 방식이다.

또 다른 방식으로는 클로저를 활용하는 방식이 있는데, 사용하는 과정은 아래와 같다.

  1. 저장속성에 함수의타입을 선언하고 임시 함수를 저장해놓는다. (클로저 형태, 저장속성 초기화를 위해)
  2. 클로저 함수를 실행할 UI요소를 셀 코드와 연결한 뒤 호출한다. (@IBAction or 셀렉터 형태)
  3. 이때 실제 동작 형태는 셀 내에서 정의되는 것이 아니라 셀의 형태를 실질적으로 정의하는 UITableViewDelegate 프로토콜 채택 뷰 컨트롤러에서 정의된다.

코드부터 살펴보자.

// 테이블뷰 셀

class MyCell: UITableViewCell{
    // 나머지코드..
    var updateButtonPressed: (ToDoCell) -> Void = { (sender) in } // 저장속성의 초기화

    // 테이블뷰 셀 기본코드 (1)
    override func awakeFromNib(){
        super.awakeFromNib()
    }

    // 테이블뷰 셀 기본코드 (2)
    override func setSelected(_ selected: Bool, animated: Bool){
        super.setSelected(selected, animated: animated)
    }

    @IBAction func updateButtonTapped(_ sender: UIButton){
        updateButtonPressed(self)
    }
}
// 테이블뷰를 저장속성으로 갖는 뷰 컨트롤러
class ViewController: UIViewController{
    @IBOutlet weak var tableView: UITableView!

    // 뷰 컨트롤러 코드들..
}

extension ViewController: UITableViewDataSource{
    // 테이블 뷰 셀 실제 형태를 정의하는 코드
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
        let cell = tableView.dequeueReusableCell(withIdentifier: "ToDoCell", for: indexPath) as! ToDoCell

        // ...
        cell.updateButtonPressed = {[weak self](senderCell) in
            self?.performSegue(withIdentifier: "ToDoCell", sender: indexPath)
        }

        return cell
    }
}

먼저, 힙 메모리 구조에 독립적으로 존재하게 되는 요소들은 다음과 같다.

  1. 테이블 뷰 셀 인스턴스
  2. 테이블 뷰 셀 인스턴스의 updateButtonPressed 클로저 (코드영역의 함수 코드묶음 참조값과 self 인스턴스에 대한 참조값을 함께 갖는다)
  3. 뷰컨트롤러 인스턴스

테이블 뷰 셀과 뷰 컨트롤러간 통신을 updateButtonPressed 클로저가 중개하게 된다. 테이블 뷰 셀에서 self 인스턴스를 전달하면 뷰 컨트롤러에서 정의한 실제 셀의 함수내용이 호출된다.

참고로 클로저에서 캡처리스트와 파라미터를 둘다 가질 수 있으며 cell.updateButtonPressed클로저를 통해 전달해주고 있다.

# 코드로 UI 그리기

스토리보드의 라이브러리에서 직접 컴포넌트를 가져와 UI를 그리는 방식도 있지만 코드를 활용하여 UI를 그리는 방식도 있다. 뷰를 등록하는 과정은 아래와 같다.

  1. ViewController 클래스의 저장속성으로 UIView 객체를 하나 생성한다.
  2. viewDidLoad 시점에서 생성한 view 저장속성의 내부 속성들을 조정한다. backgroundColor, 좌표 등
    • ViewController는 기본적으로 최상위에 view라는 UIView 객체를 갖는다.
  3. UIView 객체는 addSubView 메서드를 제공한다. view 객체 밑에 자식 뷰를 붙일 수 있는 메서드이며 자바스크립트의 appendChild와 유사한 역할을 한다.
  4. 오토레이아웃 설정을 위해 뷰의 anchor 속성값들을 지정한다.
    • trailingAnchor : 후행 좌표 (일반적으로 오른쪽)
    • leadingAnchor : 선행 좌표 (일반적으로 왼쪽)
    • topAnchor : 상단으로부터 얼마나 떨어트릴지
    • bottomAnchor : 하단으로부터 얼마나 떨어트릴지

trailingAnchor, right

trailingAnchorrightAnchor, leadingAnchorleftAnchor는 유사한 개념이다. 일반적으로는 동일한 역할이라고 생각되지만 left, right은 절대적인 방향을 나타내고 leading, trailing은 상대적인 방향을 나타낸다.

스택오버플로우 (opens new window)를 참조하면 trailingleading은 읽기 문화가 왼쪽에서 오른쪽인 문화 기준으로 왼쪽 - 오른쪽으로 동일한 방향으로 사용되지만, 히브리어와 같이 오른쪽 - 왼쪽으로 읽기 문화를 가진 문화권에서는 이를 다르게 설정해줘야 한다.

절대적인 방향의 left, right만 사용하면 이러한 부분에서 혼동이 있을 수 있기 때문에 애플이 개발자를 세심하게 배려하는 것이 느껴진다.

translatesAutoresizingMaskIntoConstraints

UIView 인스턴스에는 translatesAutoresizingMaskIntoConstraints라는 복잡한 이름의 불리언 타입 속성이 존재한다. 스토리보드가 아닌 코드로 UI를 작성할때 사용되는 속성이다.

코드로 오토레이아웃 작성을 하게 되면 위의 속성이 기본적으로 true 설정이 되어 있다. 다음 문서를 (opens new window) 참조해보면 위 속성에 대한 내용을 자세히 알아볼 수 있다.

간단히 정리하면 위 속성이 true일때, superView의 리사이징에 따라 subView에 대한 제약조건들이 자동으로 생성된다는 것이다.

이때 원하는 수치에 따라 레이아웃 조정을 하고싶다면 위의 속성값을 false로 지정해야 시스템의 제약조건과 충돌을 일으키지 않아 제대로 적용된다.

anchor값을 실제로 지정하는 코드는 아래와 같다.

// 필수
loginView.translatesAutoresizingMaskIntoConstraints = false

// 나머지 anchor값들 지정
loginView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30).isActive = true
loginView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30).isActive = true
loginView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
loginView.heightAnchor.constraint(equalToConstant: 50).isActive = true

제약조건에 대해서는 오버로딩 된 함수들이 많기때문에 추후 학습해 나가야 함.

leadingAnchor, topAnchor등 위치와 관련된 anchor값들은 어떤 대상을 기준으로 값을 설정할지에 대해 전달해야 하는데 이에 해당하는 파라미터가 equalTo이다. loginView의 superView가 최상위 뷰인 view이므로 loginView의 leadingAnchor를 view의 leadingAnchor로부터 얼마나 떨어뜨릴지 constant 파라미터에 값을 전달하면 된다.

반드시 anchor값을 모두 지정한 뒤에 isActive 속성을 지정해야 UI에 적용이 된다.

클로저로 UIView 인스턴스 생성하기

class ViewController: UIViewController {

    // 뷰컨트롤러에서 UIView 인스턴스를 클로저 형태로 호출하기
    let loginView: UIView = {
        let myView = UIView()

        myView.backgroundColor = .darkGray
        myView.layer.cornerRadius = 5
        myView.layer.masksToBounds = true

        return myView
    }()
}

저장속성을 lazy로 선언하게 되면 최상위 view 인스턴스와 연관된 제약조건 및 코드들도 클로저 내에 위치시킬 수 있다. view.addSubView() 또는 각종 anchor값 설정시 사용될 수 있다.

isActive 생략하기

오토레이아웃을 잡은 뒤에는 항상 .isActive 속성을 true값으로 설정해야한다. 매번 이런 작업을 반복적으로 하는 것은 귀찮기 때문에 애플에서 제공하는 다른 방법이 있다.

emailInfoLabel.translatesAutoresizingMaskIntoConstraints = false
emailTextField.translatesAutoresizingMaskIntoConstraints = false
passwordInfoLabel.translatesAutoresizingMaskIntoConstraints = false

// NSLayoutConstraint.activate() 메서드
NSLayoutConstraint.activate([
    emailInfoLabel.leadingAnchor.constraint(equalTo: emailTextFieldView.leadingAnchor, constant: 8),
    emailInfoLabel.trailingAnchor.constraint(equalTo: emailTextFieldView.trailingAnchor, constant: 8),
    emailInfoLabel.centerYAnchor.constraint(equalTo: emailTextFieldView.centerYAnchor)
])

NSLayoutConstraint.activate() 메서드의 아규먼트로 액티브 시킬 오토레이아웃 대상들을 배열로 전달하면 된다. 각 레이아웃에 대해 translateAutoresizingMaskIntoConstraints속성은 false로 설정해야한다.

높이 자동조정

특정 뷰나 스택뷰의 높이가 지정되지 않은채로 유동적인 컨텐츠 길이에 따라 높이나 너비값이 조정되어야 하는 경우 기본적으로 설정되는 제약조건을 =에서 >=<=로 변경해주면 된다.

또한 자동 조정될때에 같은 스택 뷰 내의 형제 요소보다 더 넓어지려는 속성을 가져야 하므로 Content Compression resistance Priority 속성의 Vertical 값을 높은 값으로 설정해준다.(값이 높을수록 우선순위)

# alert 창 띄우기

iOS에서는 알럿창을 위한 타입을 미리 마련해두었다. UIAlertControllerUIAlertAction을 사용하면 된다.

// button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)

@objc func buttonTapped(){
    let alert = UIAlertController(title: "비밀번호 바꾸기", message: "비밀번호를 바꾸시겠어요?", preferredStyle: .alert)
    let success = UIAlertAction(title: "확인", style: .default){ action in
        print("확인 버튼이 눌렸습니다 :)")
        print("확인 액션: \(action)")
    }

    let failure = UIAlertAction(title: "취소", style: .cancel){ cancel in
        print("취소 버튼이 눌렸습니다 :(")
        print("취소 액션: \(cancel)")
    }

    alert.addAction(success)
    alert.addAction(failure)

    present(alert, animated: true, completion: nil)
}
  1. UIAlertController로 알럿창 객체를 생성한다.
    • title 파라미터는 알럿창 타이틀을 의미한다.
    • message 파라미터는 알럿창 디스크립션을 의미한다.
    • preferredStyle은 알럿창의 타입을 의미한다. .alert는 익숙한 형태의 알럿창이고, .actionSheet은 모달 형태로 작동한다.
  2. UIAlertAction으로 성공과 실패에 대한 액션 각각을 정의한다.
    • title은 확인/취소처럼 액션에 대한 버튼 텍스트를 나타낸다.
    • style은 액션 버튼에 대한 스타일이다. .destructive스타일은 위험한 액션에 대해 나타낼때 사용하면 될 것 같다.
  3. 생성한 액션 객체를 UIAlertController에 등록해야한다. addAction 메서드로 등록한다.
  4. present메서드를 호출하여 알럿창을 화면상에 띄운다. present 메서드는 화면간 전환이 있을때 사용하는 메서드이다.

# onChange 처리하기

텍스트필드 onChange는 스위프트에서 UITextFieldDelegate을 채택한 뷰컨트롤러 클래스에서 textFieldDidBeginEditing(_ textField: UITextField) 함수를 구현하면 된다.

파라미터가 UITextField이므로, 현재 입력이 일어나고 있는 텍스트필드를 가져오게 된다. 뷰 컨트롤러 클래스의 저장속성으로 등록한 UITextField인지 판별하여 이벤트 처리를 하면 된다.

extension ViewController: UITextFieldDelegate{
    func textFieldDidBeginEditing(_ textField: UITextField) {

        // emailTextField는 뷰컨트롤러 클래스의 UITextField타입의 저장속성
        if(textField == emailTextField){
            emailTextFieldView.backgroundColor = #colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1)
            emailInfoLabel.font = UIFont.systemFont(ofSize: 11)
            emailInfoLabelCenterYConstraint.constant = -13
        }

        // passwordTextField는 뷰컨트롤러 클래스의 UITextField타입의 저장속성
        if(textField == passwordTextField){
            passwordTextView.backgroundColor = #colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1)
            passwordInfoLabel.font = UIFont.systemFont(ofSize: 11)
            passwordInfoLabelCenterYConstraint.constant = -13
        }

        UIView.animate(withDuration: 0.3) {
            self.stackView.layoutIfNeeded()
        }
    }
}

제약조건 저장속성으로 참조하기

오토레이아웃의 제약조건을 저장속성으로 담아두면 동적인 상황에서 레이아웃을 변경해줄 수 있다. 위의 textFieldDidBeginEditing 함수 처리 예시코드에서 passwordInfoLabelCenterYConstraint, emailInfoLabelCenterYConstraint은 뷰컨트롤러의 저장속성으로 따로 빼둔 변수이다.

lazy var emailInfoLabelCenterYConstraint = emailInfoLabel.centerYAnchor.constraint(equalTo: emailTextFieldView.centerYAnchor)
lazy var passwordInfoLabelCenterYConstraint = passwordInfoLabel.centerYAnchor.constraint(equalTo: passwordTextView.centerYAnchor)

NSLayoutConstraint라는 타입을 갖고 저장속성이 만들어지며 동적인 레이아웃 변동이 필요할때 사용하게 될 패턴이다.

클래스 내에서 저장속성으로 활용할 때에는 타입 선언만 해둔 뒤 나중에 초기화해도 된다.

var stackviewTopAnchor: NSLayoutConstraint // NSLayoutConstraint타입 저장속성

// topAnchor 제약조건 초기화
func myFunction(){
    stackviewTopAnchor = stackView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 10)
    NSLayoutConstraint.activate([
        stackViewTopAnchor
    ])
}

반드시 제약조건 참조를 NSLayoutConstraint.activate 함수 배열값의 원소로 전달하여 활성화해야 제약조건이 적용된다.

내부 값만 이후 변경하고 싶으면 stackViewTopAnchor.constant = CGFloat값의 형태로 constant 속성 값만 조정하면 된다.

계속해서 해당 제약조건을 참조하고 있으므로 값의 변화를 준 뒤 앱 생명주기 함수의 layoutIfNeeded()와 함께 구현하면 애니메이션 효과도 부여할 수 있다.

#selector action

스위프트 컴포넌트에 대한 액션 리스너로써 addTarget 메서드를 사용할 수 있었다. 이때 action 파라미터에 #selector(함수명) 형태로 이벤트 감지 시 실행되는 함수를 참조할 수 있는데 추측이지만 이쪽에 이벤트 호출자 컴포넌트가 객체로써 들어가는 것 같다.

다시 말하면 UITextField에 .editingChanged 이벤트에 따라 텍스트 값이 변경되면 셀렉터가 참조하는 함수가 호출되는데, 이때 호출되는 함수에 UITextField 자기 자신이 들어간다는 것이다.

따라서 해당 UITextField에 대해 로직 구현을 할 수 있게 된다.

// 텍스트필드 저장속성
private lazy var passwordTextField: UITextField = {
    var tf = UITextField()
    // ...
    tf.addTarget(self, action: #selector(textFieldEditingChanged), for: .editingChanged)
    return tf
}()

// passwordTextField의 텍스트 속성값이 변경되면 textFieldEditingChanged함수가 호출되고
// 파라미터로 passwordTextField 저장속성이 넘겨진다.
@objc func textFieldEditingChanged(textField: UITextField){
    if(textField.text?.count == 1){
        if(textField.text?.first == " "){
            textField.text = ""
            return
        }
    }
    guard
        let email = emailTextField.text, !email.isEmpty,
        let password = passwordTextField.text, !password.isEmpty else {
        loginButton.backgroundColor = .clear
        loginButton.isEnabled = false
        return
    }
    loginButton.backgroundColor = .red
    loginButton.isEnabled = true
}

애니메이션 활용하기

이후 설명이 추가되면 내용 더 정리가 필요함.

UIView에는 animate 메서드가 있는데 duration 설정과 함께 레이아웃 변경이 한번에 일어날 때에 사용한다.

UIView.animate(withDuration: 0.3) {
    self.stackView.layoutIfNeeded()
}

실무적인 관점에서 앱 제작 시 주의사항

  1. 대부분의 저장속성은 private으로 선언한다.
  2. View와 관련된 객체들은 순서에 따라 lazy선언을 할지 선택하여 활용한다.
  3. ViewController 클래스는 final로 선언한다.

MARK 주석 활용하기

앱 제작시 MARK라는 특별한 형태의 주석을 사용하면 코드 접근이 용이해진다.

// MARK: - 코드에 마크를 달아줍니다 ~
func markChecker(){
    print("hi!")
}

MARK: -라는 형태로 주석을 작성하면 Xcode에서 코드 점핑 기능을 통해 원하는 코드에 접근하기가 쉽다.

code snippet

Xcode에서는 코드 스니펫 기능을 제공한다. 엑스코드 코드 입력창에서 우클릭을 하면 메뉴중에 create code snippet이 있다.

타이틀과 코드 스니펫에 대한 설명을 적고 실제 코드를 적는다. 이때 <##> 형태로 하여 # 사이에 내용을 입력하면 코드 placeholder로 사용할 수 있다.

코드 스니펫 정의 후 하단에 completion에 어떤 코드를 입력시에 코드 스니펫을 발동시킬 수 있는지 정의할 수 있다.

// MARK: - <#내용입력#>이라고 코드 스니펫을 정의하고, completion에 mark라고 정의하면 Xcode 어디에서든 mark라는 코드를 입력했을때 코드 스니펫에 대한 자동완성 기능을 제공하게 된다.

clipsToBounds

몇몇 UI요소에 대해 모서리를 둥글게 하고 싶으면 요소.layer.cornerRadius 값을 조정하면 된다. UITextField는 직접 해당 값을 조정하면 바로 UI에 변경사항이 반영 되지만, UILabel의 경우에는 반영되어 있지 않은 것처럼 보인다.

이때 필요한 것이 UILabel.clipsToBounds = true설정을 하는 것이다. 쉽게 말해 실제로 모서리는 둥글어져 있지만 내부를 채우고 있는 요소가 모서리를 넘어 사각형 형태를 계속 유지하기 때문에 모서리가 둥글어지지 않은 것처럼 보이는 것이라고 이해해도 될 것 같다.

자세한 것은 다음 문서를 (opens new window) 참조하자.

UI 주의점

  1. 스택뷰 내에서 뷰 간의 간격을 띄우기 위해 topAnchor를 조정하면 에러가 발생한다. 스택뷰 자체의 spacing을 조절하도록 하자.
  2. 스택뷰 생성 시 addSubview로 하위 뷰 인스턴스들을 하나씩 등록하지 말고, arrangedSubviews의 배열 파라미터로 한번에 인스턴스들을 전달해야한다.

# Reference

  1. 인프런 - 앨런 swift 문법 마스터 스쿨 (opens new window)
  2. stackoverflow - Difference between leftAnchor and leadingAnchor? (opens new window)
  3. UIView의 translatesAutoresizingMaskIntoConstraints 속성에 대한 이해 (opens new window)
  4. Autoresizing과 AutoresizingMask (opens new window)
  5. iOS - clipsToBounds란 무엇인가? (opens new window)
  6. parkjju - 프로토콜의 상속 (opens new window)
  7. stackoverflow - Can you have a closure that has both capture list and parameter list? (opens new window)