Ch.19 테이블 뷰를 이용한 데이터 목록 구현

  • 테이블 뷰 컨트롤러는 뷰 컨트롤러를 바탕으로 만들어진 특수한 컨트롤러
  • 테이블 뷰 컨트롤러에는 테이블 뷰가 루트뷰로 정의되어있는데 이 테이블뷰는 목록형식의 데이터를 화면에 표현하는데에 사용됨

테이블 뷰의 계층구조
1.테이블 뷰 컨트롤러
2.테이블 뷰
3.테이블 뷰 셀
4.콘텐츠 뷰

  • 테이블 뷰는 목록을 구성하기 위한 객체이므로 여러개의 행을 가질수 있음
  • 이 여러개의 행을 테이블뷰 셀 이라고함
  • 테이블 뷰 셀은 다시 내부에 콘텐츠 뷰를 가짐
  • 실제로 우리가 보는 화면에서 목록 각 행의 내용은 저마다의 콘텐츠가 콘텐츠뷰 내부에 적절히 배치된 결과물
  • 섹션은 하나의 테이블 뷰 내에서 셀들을 그룹으로 묶을 수 있는 단위
  • 테이블 뷰 컨트롤러는 UIKit 프레임워크에 UITableViewController 클래스로 구현되어 있음. 이 클래스는 델리게이트 패턴에 기반한 다양한 메소드가 정의되어 있음. 이들의 역할은 데이터를 이용하여 목록을 구성하고 목록에 대한 사용자와의 상호 반응을 지원하는 것

19.1 프로토타입 셀

  • 프로토타입 셀은 테이블 뷰의 셀을 원하는 대로 쉽게 디자인할 수 있도록 해주는 객체로 테이블 뷰를 설계하는 데에 걸리는 시간과 노력을 대폭 줄여줌
  • 테이블 뷰가 화면에 표현될 때 셀의 구성을 미리 보여주기 위한 가상 틀에 불과함

프로토타입 셀 영역


1.Cell Content : 셀에 표현될 콘텐츠
2.Accessory View : 콘테츠의 부가 정보 여부를 암시

  • 일반적으로 프로토타입 셀을 이용하여 콘텐츠를 표현할 때의 작업 대부분은 Cell Content영역에서 이루어짐
  • 모바일 디바이스에 따라 가로너비가 달라질 경우 Accessory View영역은 너비가 고정값을 유지하는 반면 Cell Content영역은 가변적인 너비값으로 처리됨(가로길이 변화에 따른 주의가 필요함)

Cell Content


그림이나 사진등 이미지 콘텐츠를 표현하는 Image영역과 텍스트를 표현하는 Text영역으로 나누어짐

표준 편집 인터페이스


목록에서 단순히 행을 삭제하기 위한 목적이면 오른쪽의 Reordering Control영역에 ㅈ공되는 삭제버튼만 사용하면 되지만 복합적인 편집 기능을 제공해야 할 때는 위 그림과 같은 표준 인터페이스를 모두 사용함
행을 추가, 삭제하는 기능은 Editing Control여역에 빨간색 마이너스 아이콘이나 초록색 플러스 버트으로 제공되며, Reordering Control 영역에는 셀의 순서를 재배치 하는 컨트롤이 제공됨

19.1.1 프로토타입 셀을 이용한 테이블 뷰 실습

19.2 데이터 소스

  • 일반적으로 테이블 뷰를 이용하여 화면에 콘텐츠를 표현하는 방법에는 두 가지가 있음
  • 정적인 방법과 동적인 방법
  • 정적인 방법은 테이블 뷰 셀 각각을 프로그래밍으로 구성하지 않고 스토리보드에서 직접 구성한 것을 말함
  • 테이블 뷰 컨트롤러를 처음 생성하면 테이블 뷰의 속성은 동적 타입으로 지정되어 있지만 우리가 임의로 정적 타입으로 변경할수 있음
  • Content속성에서 Static Cells가 정적 타입의 테이블 뷰를 의미함
  • 테이블 뷰를 정적 타입으로 바꾸면 기존의 프로토타입 셀은 사라지고 그 자리를 정적인 형식의 셀이 채우게 됨. 이 셀들은 프로토타입 셀과 달리 화면에 직접 표현되므로 내가 필요한 콘텐츠를 직접 올려서 구현할 수 있음
  • 반면 고정되지 않고 매번 갱신되는 내용을 표현하려면 테이블 뷰 셀을 프로그래밍적으로 구성해 주어야 하는데 이를 위해 데이터소스가 필요함
  • 테이블 뷰의 각 행마다 대응할 수 있도록 배열 형태이기만 하면 데이터 소스가 됨
  • 이렇게 만들어진 데이터 소스를 테이블 뷰 각 행에 연결하는 과정을 데이터 바인딩이라 함

19.2.1 데이터 소스 만들기

19.2.2 테이블 뷰와 데이터 소스 연동

  • 데이터 소스와 테이블 뷰를 연동하는 과정은 UITableViewDataSource라는 프로토콜에 의존하여 이루어짐. 테이블 뷰 컨트롤러는 이 프로토콜을 참고하여 지정된 메소드를 호출함으로써 데이터 소스와 테이블 뷰를 연동함. UITableViewController 클래스가 이미 해당 프로토콜을 상속 받고 있음

데이터 소스 연동을 위한 핵심 메소드
테이블 뷰와 데이터 소스를 연동하는 데 필요한 기본 메소드는 다음과 같음
tableView(:numberOfROwsInSection:)
tableView(
:cellForRowAt:)

  • 이 메소드들은 iOS시스템이 필요에 의해 호출하는 메소드들. 일종의 델리게이트 패턴을 따르고 있음. 동작이나 이벤트에 관한 메소드가 아니기 때문에 델리게이트라는 접미어를 븉이지는 않지만 델리게이트 패턴과 동일한 방식으로 동작함
  • 이 메소드들은 이미 UITableView 클래스에서 구현이 되어 있음

tableView(_:numberOfRowsInSection:)
이 메소드는 테이블 뷰가 생성해야 할 행(row)의 개수를 반환. 이 메소드는 iOS시스템이 테이블 뷰를 구성하기 위해 먼저 호출하는 메소드. 이 메소드는 우리가 사용하기 위한 것이 아니라 시스템이 사용하기 위한 메소드임
이 메소드에 의해 테이블 뷰의 행 수가 결정되는 것임

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { }
첫번째 인자값은 이 메소드를 호출한 테이블 뷰 객체에 대한 정보
두번째 인자값은 섹션에 대한 정보

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { }
이 메소드는 각 행이 화면에 표현해야 할 내용을 구성하는데에 사용. 이 메소드가 반환하는 값은 전체 테이블 뷰의 목록이 아니라 하나하나의 개별적인 테이블 셀 객체인데 이는 화면에 표현해야할 목록의 수만큼 이 메소드가 반복적으로 호출된다는 것을 의미함
첫번째 매개변수를 통해 테이블 뷰가 특정되면 두번째 매개변수인 indexPath를 통해 몇번째 행을 구성하기 위한 호출인지를 구분할 수 있음.
IndexPath 객체 타입으로 정의된 이 매개변수는 선택된 행에 대한 관련 속성들을 모두 제공함. 그중에서도 .row는 가장 많이 사용되는 속성으로 행의 번호를 알려주는 역할을 함
0부터 시작하는 이 행 번호는 배열로 이루어진 데이터 소스의 아이템 인덱스와 대부분의 경우 일치하므로 이 속성을 사용하면 데이터 소스의 필요한 부분을 편리하게 읽어 들일 수 있음.

사용자의 액션 처리를 위한 핵심 메소드
tableView(:didSelectRowAt:)
UITableViewDelegate 프로토콜에 정의된 이 메소드는 사용자가 목록중에서 특정 행을 선택 했을때 호출됨
override func tableView(
 tableView: UITableView, didSelectRowAt indexPath: IndexPath) { }
이 메소드는 델리게이트 메소드이기 때문에 앞의 두 메소드처럼 적절한 시점에 맟추어 자동으로 호출됨. 앞의 두 메소드는 테이블 뷰를 화면에 구현할 때 호출되는데 반해 이 메소드는 사용자의 액션이 있을 때 호출됨
첫번째 인자값이 사용자가 터치한 테이블 뷰에 대한 참조값
두번째 인자값이 터치된 행에 대한 정보

메소드 구현 실습

 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.list.count
    }
  • 생성해야 할 행의 개수를 반환 하는 메소드
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let row = self.list[indexPath.row]
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "ListCell")!
        
        cell.textLabel?.text = row.title
        
        return cell
        
    }
  • 이 메소드는 개별 행을 만들어내는 역할을 함. 위의 메소드가 반환하는 값만큼 이 메소드가 반복 호출됨. 이 메소드가 한번 호출 될때 마다 하나의 행이 만들어진다고 생각하면 됨
  • 몇번째 행을 구성해야 하는지 알려주기 위해 IndexPath 타입의 객체가 인자값으로 전달됨. 행 번호를 알고자 할 경우 indexPath.row 속성을 사용하면 됨
  • 이 속성은 배열과 마찬가지로 0부터 시작
  • dequeueReusableCell(withIdentifier:) 메소드는 인자값으로 입력받은 아이디를 이용하여 스토리보드에 정의된 프로토타입 셀을 찾고 이를 인스턴스로 생성하여 제공함

개선 및 업그레이드

19.3 커스텀 프로토타입 셀

19.3.1 커스텀 프로토타입 셀 구현하기

  • 커스텀 타입의 프로토 타입 셀에 디자인된 객체를 소스코드에서 읽어오기 위해서 viewWithTag(_:) 메소드를 사용하는데 이때 객체마다 각각의 태그값이 필요
  • 다양한 타입의 뷰 객체(이미지버튼, 스위치버튼 등) 을 모두 메소드 하나로 읽어오는 만큼 반환타입은 UIView 임. 뷰를 상속받은 모든 객체 타입을 포괄할 수 있는 UIView 타입의 객체로 넘겨 받은 다음 필요에 따라 적절한 구체적 타입으로 캐스팅하는것임
  • 입력되지 않은 잘못된 태그값을 인자로 호출할 경우를 대비하여 viewWithTag(_:)메소드의 반환값은 옵셔널 타입으로 정의됨
  • 커스텀 타입의 프로토타입 셀에 디자인된 객체들은 다음의 과정을 통해 소스코드에서 참조 할 수 있음.

19.3.2 커스텀 클래스로 프로토타입 셀의 객체 제어하기

  • 프로토타입 셀에 디자인된 객체를 제어하는 또 다른 방법은 커스텀 클래스를 만들어 사용하는 것
  • 프로토타입 셀 자체를 커스텀 클래스와 연결한 다음, 셀위에 올려진 객체를 아룰렛 변수로 연결해서 참조하는 것
  • 아울렛 변수를 뷰 컨트롤러에 직접 정의하면 셀 내부 객체들이 정적인 객체가 되므로 사용하는데 문제가 생기지만, 프로토타입 셀을 연결한 커스텀 클래스에 아울렛 변수를 정의하면 이는 동적으로 사용할 수 있는 형태의 객체가 되기 때문에 아울렛 변수를 통해 객체를 관리할 수 있음.
  • 따라서 태그 속성 사용시 단점으로 꼽혔던 객체관리 문제나 잘못된 태그값을 호출하는 문제로부터 자유로워 질 수 있으며, 유지보수도 무척 편리해짐
  • 셀 커스텀하기 위한 클래스는 UITableViewCell을 서브클래싱함
  • 프로토타입 셀에 새로운 객체를 추가하고자 할 경우 다음과 같은 순서로 처리

19.3.3 프로토타입 셀에 섬네일 이미지 추가하기

  • 이미지뷰를 구현하는 기본 클래스는 UIImageView
  • 이미지 객체를 화면에 표현해주기 위한 각종 기능과 속성 구현을 담당
  • 이미지뷰는 .image 라는 속성을 가지고 있으며 UIImage타입으로 작성된 이미지 객체를 속성에 대입받아 화면에 표현함.
  • 이미지뷰의 하위 속성으로 정의되어 있는 UIImage는 이미지 데이터를 저장하는 객체
  • 이미지뷰는 이미지를 화면에 표현해주는 기능을 구현한 뷰 이지만 UIImage는 이미지 데이터 자체를 iOS에 맞게 다듬은 객체
  • 이미지를 앱 화면에 표시하려면 먼저 이미지를 담아 UIImage객체를 만들고 이 객체를 다시 UIImageView 객체의 .image속성에 대입하는 과정을 거쳐야 함

var img = UIImage(named: <프로젝트 내 파일 경로>)

  • UIImage(named:) 방식으로 생성한 이미지 객체는 한번 읽어온 이미지를 메모리에 저장해둔 다음 두번째 호출부터는 메모리에 저장된 이미지를 가져옴(캐싱함)
  • 이미지 객체로 인한 메모리 점유가 걱정되는 경우 UIImage(contentsOfFile:)생성자를 사용해 이미지 객체 생성 -> 캐싱되지 않음.

UIImage(contentsOfFile:<프로젝트 내 파일 경로>)

  • 커스텀 클래스를 이용하여 프로토타입 셀을 제어하는 과정정리

19.4 테이블뷰의 행 높이를 결정하는 방식

  • 코코아 터치 프레임워크에서는 기본적으로 테이블 뷰의 행 높이를 결정하는 두가지 방식을 제공
  • 하나는 모두 동일한 높이를 갖는 방식이고 또다른 하나는 각 셀마다 다른 높이를 갖는 방식
  • 내부 콘텐츠에 따라 동적으로 셀의 높이가 늘어나거나 줄어드는 방식으로 셀을 구현하는 방식도 제공됨

19.4.1 tableView(_:estimatedHeightForRowAt:)

  • 이 메소드는 테이블 뷰에서 특정 행의 높이를 설정하고 싶을때 사용하는 메소드
  • UITableViewDelegate 프로토콜에 정의되어 있으며 UITableView 클래스에서 이미구현되어 있으므로 커스텀 클래스에서는 override키워드를 붙여 재정의 하는 방식으로 사용하여야 함.
  • 행의 높이를 결정하는 것은 UITableView객체의 rowHeight속성 -> 테이블 뷰 내의 모든 셀에 공통으로 적용되어 모두 동일한 높이를 갖는 행으로 만들어줌
  • self.tableView.rowHeight = <원하는 행 높이>
  • tableView(_:estimatedHeightForRowAt:) 메소드가 구현되면 UITableView 객체의 rowHeight속성은 더이상 행의 높이값으로서 역할을 하지 못함.
  • 메소드에서 반환하는 값이 높이값으로 대신 사용이 됨
  • 코코아터치 프레임 워크는 행에 대한 정보를 indexPath 매개변수에 담아 tableView(_:estimatedHeightForRowAt:) 메소드를 호출하고 그 결과값을 받아 셀의 높이를 결정함.
  • 이점 : 개별적인 행 높이를 제어 할 수 있음.
  • 이 메소드가 호출될때 두번째 인자값으로 IndexPath 타입의 행 정보가 함께 전달되기 때문에 이를 이용하여 행 정보를 얻고 그에 따라 알맞은 높이값을 반환하면 됨

A ?? B
두개의 물음표로 이어진 ?? 는 nil-Coalescing Operator라는 의미의 연산자
-> 만약 A가 nil이 아닐경우 옵셔널을 해제하고, nil일 경우 대신 B값을 사용하라!
이 연산자를 사용하면 옵셔널 타입이 해제된다
이 연산자의 앞쪽에는 옵셔널 값이, 뒤쪽에는 일반값이 위치한다
이 연산자의 뒤쪽에 위치한 일반 값의 타입은 앞쪽 옵셔널 값에서 옵셔널을 해제한 타입 과 일치해야 한다

  • 옵셔널 타입을 해제하면서 동시에 대체 값을 제공해주는 아주 편리한 연산자임
  • 변수에 옵셔널 타입을 해제한 값을 할당하는 과정에서 해당 값이 nil일 경우를 대비하여 기본 값을 주고자 할 때 매우 유용하게 사용할 수 있음
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell() -> "cell"아이디를 가진 셀을 읽어와 옵셔널을 해제하되, 만약 그값이 nil일 경우 UITableViewCell 인스턴스를 새로 생성한다.

셀프 사이징 셀
estimatedRowHeight 프로퍼티
UITableViewAutomaticDimension 객체
위 두가지만 이해하고 있으면 됨!

  • estimatedRowHeight 프로퍼티는 셀 전체의 높이를 결정하기 전에 임시로 사용할 셀의 높이값을 나타냄 -> 테이블 뷰는 이 값을 바탕으로 아직 내부 사이즈가 결정되지 않은 셀들을 임시배치하고 그 안에 콘텐츠를 구성함
  • UITableViewAutomaticDimension 은 테이블 뷰의 rowHeight속성에 대입되어 높이값이 동적으로 설정될 것을 테이블 뷰에 알려주는 역할 -> 테이블 뷰의 rowHeight속성이 해당 값으로 설정되면 테이블 뷰는 전체 목록이 모두 만들어진 시점에서 셀 내부의 콘텐츠 레이아웃을 계산하고, 그에 따라 셀마다 높이값을 재설정함.
  • 위 코드는 viewWillAppear(_:)메소드와 적절한 시점에 넣어서 구현해 주면 됨


블로그 이미지

百見 이 不如一打 요 , 百打 가 不如一作 이다.

,

Ch.15 화면전환

  • iOS에서 화면을 전환하는 방법에는 크게 두가지가 있음
  • 하나는 소스 코드를 통해 전환하는 방식이고 (동적으로 화면전환)
  • 또 다른 하나는 스토리보드가 제공하는 기능을 이용하여 전환하는 방식(정적으로 화면전환)
  • 동적인 방식은 특정 상황에 대응할 수 있지만 조금 복잡하고 어려움
  • 정적인 방식은 일괄적으로 적용되는 것이라 특정 상황에 대응하기 어렵지만 그만큼 구현하기 쉽다는 장점이 있음

15.1 iOS에서의 화면 전환 개념

  • iOS에서 화면 전환 방식은 분류기준에 따라 크게 4가지로 나누어 볼 수 있음
1. 뷰 컨트롤러의 뷰 위에 다른 뷰를 가져와 바꿔치기 하기
2. 뷰 컨트롤러에서 다른 뷰 컨트롤러를 호출하여 화면 전환하기
3. 내비게이션 컨트롤러를 사용하여 화면 전환하기
4. 화면 전환용 객체 세그웨이를 사용하여 화면 전환하기
  • 위 1은 특수한 상황에서 제한적으로 사용하는 방법임. 일부 뷰 컨트롤러들은 콘텐츠를 직접 배치하여 화면을 보여주는 역할 대신 다른 뷰 컨트롤러를 구조화 하는 역할을 하는데 이때 화면을 구조화하는 방식이 이것임
  • 이같은 뷰 컨트롤러를 컨테이너 뷰 컨트롤러라고 함
  • 1 을 제외한 나머지 대부분의 화면 전환은 모두 뷰 컨트롤러를 호출하는 방식으로 이루어짐. 전환할 화면을 담당하는 뷰 컨트롤러의 인스턴스를 생성하고, 이를 불러들여서 기존의 화면 위에 덮으면 화면이 전환된다는 뜻. 이에 따라 현재의 화면이 다른 화면으로 완전히 교체되는 것이 아니라 현재 화면이 있는 상태에서 그 위에 새로운 화면이 얹어지는 모양새가 됨
  • 이같은 특성 때문에 기존 화면과 새로운 화면 사이에는 서로 참조 관계가 성립함. 화면이 전환되는 방식에 따라 이들은 서로 직접 참조할수 있거나 또는 화면전환을 관리하는 전담 객체를 통해 간접적으로 참조하기도 함

iOS에서 화면 전환은 다음 두가지 특성을 가짐

  1. 다음 화면으로 이동하는 방법과 이전 화면으로 되돌아가는 방법이 다름
  2. 화면 전환방식에 따라 이전 화면으로 되돌아 가는 방법이 다름

15.2 화면 전환 기법1 : 뷰를 이용한 화면 전환

  • 하나의 뷰 컨트롤러 안에 두 개의 루트뷰를 준비한 다음 상태에 따라 뷰를 적절히 교체해 주는 것
  • 이 방식은 하나의 뷰 컨트롤러가 두개의 루트뷰를 관리해야 하므로 그리 좋은 방법은 아님
  • iOS에서는 하나의 뷰 컨트롤러 아래에 하나의 루트뷰를 관리하는 MVC패턴을 기본으로 하는데 위에방식은 이같은 구조를 완전히 거스르는 방식
  • 또 다른 방식은 다른 뷰 컨트롤러에 올려진 루트 뷰를 가져와 표시하는 방식
  • 이 방법 역시 뷰 컨트롤러 내부에 있어야 할 뷰가 다른 뷰 컨트롤러로 옮겨가 버리므로 뷰를 제어할 책임을 지는 컨트롤러가 모호해진다는 단점이 있음

15.3 화면 전환 기법2 : 뷰 컨트롤러 직접 호출에 의한 화면 전환

  • 현재의 뷰 컨트롤러에서 이동할 대상 뷰 컨트롤러를 직접 호출해서 화면을 표시하는 방식

present(<새로운 뷰 컨트롤러 인스턴스>, animated:<애니메이션 여부>)
present(_:animated:completion:)
completion -> 화면 전환이 완전히 끝난후에 호출해주는 역할

  • 프레젠테이션 방식으로 화면을 전환했을 때 iOS시스템은 화면에 표시된 뷰 컨트롤러(VC2)와 표시하고 있는 뷰 컨트롤러(VC1) 사이에 참조할 수 있는 포인터를 생성하여 서로 참조할 수 있게 해줌
  • VC1은 presentedViewController 속성에다 자신이 표시하고 있는 새로운 뷰 컨트롤러의 포인터를 저장하고, 새로운 뷰 컨트롤러인 VC2에는 presentingViewController 속성에다 자신을 표시한 뷰 컨트롤러의 포인터를 저장
  • VC1 에서는 presentViewController속성을 이용하여 VC2를 참조 할 수 있고 VC2에서는 presentingViewController속성을 이용하여 VC1을 참조할 수 있다는 뜻
  • 이전 화면으로 복귀할 때는 다음과 같은 복귀 메소드를 사용함

dismiss(animated:)

  • 이전 화면으로 돌아가는 기능이기 때문에 뷰 컨트롤러의 인스턴스를 인자값으로 받지는 않음. 화면 복귀시 애니메이션을 적용할지 말지를 결정하는 값만 전달해주면 됨

dismiss(animated:completion:)
completion -> 화면 복귀가 완전히 처리되었을 때 실행할 구문을 인자값으로 입력받는 매개변수

  • 두 메소드는 기존화면이 새로운 화면 위로 올라오는 것이 아니라 기존 화면을 덮고 있던 새 화면을 걷어내는 것. 걷어낸 화면의 뷰 컨트롤러 객체는 운영체제에 의해 곧 메모리에서 해제됨
  • iOS에서 화면이 사라지게 처리하는 것은 사라질 화면의 뷰 컨트롤러 자신이 아니라 자신을 띄우고 있는 이전 뷰 컨트롤러임
  • 즉 VC1 이 VC2 를 호출하여 화면에 표시해주었다면 반대로 VC2를 화면에서 사라지게 하는것도 VC1

self.presentingViewController?.dismiss(animated:)

15.3.1 화면전환 실습

//
//  ViewController.swift
//  Scene-Trans01
//
//  Created by 이재성 on 2017. 6. 13..
//  Copyright © 2017년 jaeseong. All rights reserved.
//

import UIKit

class ViewController: UIViewController {
    @IBAction func moveNext(_ sender: Any) {
        
        // 여러개의 스토리 보드가 있을경우 이렇게 사용..Main은 스토리보드 파일명
        
        // let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
        // let uvc = storyboard.instantiateViewController(withIdentifier: "SecondVC")
        
        
        // let uvc = storyboard!.instantiateViewController(withIdentifier: "SecondVC")
        // self.storyboard 값은 옵셔널 타입, 경우에 따라 nil값이 될수도 있음
        // 이값을 nil검사 없이 바로 ! 연산자를 사용하여 강제 해제하였으므로 만약 self.storyboard 값이 nil이라면 오류가 발생. 이를 옵셔널 체인과 옵셔널 바인딩으로 보강하면 다음과 같음
        
        /* if let uvc = storyboard?.instantiateViewController(withIdentifier: "SecondVC"){
                uvc.modalTransitionStyle = UIModalTransitionStyle.coverVertical
        
                self.present(uvc, animated: true)
            }
        */
        
        //뷰 컨트롤러 인스턴스는 moveNext메소드 전체 실행에서 비어있어서는 안되는 필수조건이기 때문에 guard 조건문으로 필터링
        //안전한 코드를 위해 self.storyboard를 옵셔널 체인으로 처리하여 instantiateViewController(withIdentifier:)메소드를 호출하여 뷰 컨트롤러의 인스턴스를 받아온 다음 옵셔널 타입을 해제하기 위해 상수 uvc에 옵셔널 바인딩, 만약 바인딩에 실패하면 메소드의 실행은 중지됨.
        guard let uvc = self.storyboard?.instantiateViewController(withIdentifier: "SecondVC") else {
            return
        }
        
        // 화면 전환할 때의 애니메이션 타입
        
        uvc.modalTransitionStyle = UIModalTransitionStyle.coverVertical
        
        // 인자값으로 뷰컨트롤러 인스턴스를 넣고 프레젠트 메소드 호출
        
        self.present(uvc, animated: true)
        
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
//
//  SecondViewController.swift
//  Scene-Trans01
//
//  Created by 이재성 on 2017. 6. 13..
//  Copyright © 2017년 jaeseong. All rights reserved.
//

import UIKit

class SecondViewController: UIViewController {

    //self.dismiss가 아님!!
	  //vc2(secondViewController를 사라지게 하는건 vc1 임

    @IBAction func dismiss(_ sender: Any) {
        self.presentingViewController?.dismiss(animated: true)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

15.4 네비게이션 컨트롤러를 이용한 화면 전환

  • 네비게이션 컨트롤러는 뷰 컨트롤러의 특별한 종류로 계층적인 성격을 띠는 콘텐츠 구조를 관리하기 위한 컨트롤러(내비게이션 바가 내장되어 있음)
  • 이 컨트롤러가 제어하는 모든 뷰 컨트롤러에 내비게이션 바를 생성하는 특징이 있음
  • 루트뷰 컨트롤러는 내비게이션 컨트롤러에 직접 연결된 컨트롤러이므로 화면 UI상단에 내비게이션 바가 표시됨
  • 내비게이션 컨트롤러는 화면에 현재 표시되고 있는 뷰 컨트롤러들을 내비게이션 스택을 이용하여 관리함
  • 내비게이션 컨트롤러 최상위 뷰컨트롤러(마지막컨트롤러), 최하위 컨트롤러(루트뷰 컨트롤러)
  • 최상위 뷰 컨트롤러에 추가할 때는 pushViewController(animated:) -> 새로운화면표시
  • 최상위 뷰 컨트롤러에서 제거할 때는 popViewController(animated:) -> 이전화면으로 돌아올때
//
//  ViewController.swift
//  Scene-Trans02
//
//  Created by 이재성 on 2017. 6. 13..
//  Copyright © 2017년 jaeseong. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func moveByNavi(_ sender: Any) {
        
        // 
        guard let uvc = self.storyboard?.instantiateViewController(withIdentifier: "SecondVc") else {
            return
        }
        self.navigationController?.pushViewController(uvc, animated: true)
    }
    @IBAction func movePresent(_ sender: Any) {
        guard let uvc = self.storyboard?.instantiateViewController(withIdentifier: "SecondVc") else {
            return
        }
        self.present(uvc, animated: true)
    }

}
//
//  SecondViewController.swift
//  Scene-Trans02
//
//  Created by 이재성 on 2017. 6. 13..
//  Copyright © 2017년 jaeseong. All rights reserved.
//

import UIKit

class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @IBAction func back(_ sender: Any) {
        self.presentingViewController?.dismiss(animated: true)
        
    }
    @IBAction func back2(_ sender: Any) {
        self.navigationController?.popViewController(animated: true)
    }
}

 

15.5 세그웨이를 이용한 화면 전환

  • 세그웨이를 이용하면 뷰 컨트롤러에 대한 정보가 없어도 됨.
  • 또한 뷰 컨트롤러의 객체를 생성할 필요도 없음. 세그웨이가 스토리보드상의 연결정보를 이용하여 대상 뷰 컨트롤러의 인스턴스를 자동으로 만들어줌
  • 출발점이 뷰 컨트롤러 자체인 경우를 메뉴얼세그, 컨트롤이 출발점인 경우를 액션세그 또는 트리거세그라고 나누어 부르기도 함.
  • 메뉴얼세그를 실행하려면 UIKit프레임워크에 정의된 performSegue(withIdentifier:sender:)메소드를 사용

15.5.1 액션 세그웨이

  • 액션세그는 트리거와 세그웨이가 직접 연결된 것을 의미. 터치 또는 클릭 이벤트를 발생시켜 세그웨이를 실행할 수 있는 요소를 말함
  • 액션세그는 화면 전환을 위해 프로그래밍 코드가 필요없고, 스토리보드에 구현된 객체를 트리거로 지정만 하면 되므로 전체적인 구성이 굉장히 단순해짐
  • PresentModally 는 화면전환 메소드 중 present(_:animated:)메소드를 이용한 화면 전환과 같은 기능을 함
  • 즉, 뷰 컨트롤러 자신이 새로운 화면을 불러들이도록 처리하는것
  • 내비게이션 컨트롤러가 추가되어 있는 상태에서 Show 타입의 세그웨이를 생성하면 내비게이션 컨트롤러를 통한 화면 이동이 발생. 즉, 모든 화면 이동의 결과는 내비게이션 컨트롤러의 통제하에 있게됨. 따라서 모든 뷰 컨트롤러에는 내비게이션 바가 추가됨
  • 내비게이션 컨트롤러가 없을 때는 세그웨이를 Show타입으로 생성하였더라도 PresentModally방식으로 실행됨
  • 뷰컨트롤러에 임베디드 인 하여 네비게이션 컨트롤러 추가한후에 새로운 뷰 컨트롤러를 추가하고 세그웨이로 연결한 경우 내비게이션 아이템을 추가해야 타이틀 입력 또는 바버튼을 추가 할 수 있음.

15.5.2 메뉴얼 세그

  • 메뉴얼세그는 해당 이벤트만 발생하면 자동적으로 실행되는 액션세그와 달리 뷰 컨트롤러와 뷰 컨트롤러 사이에 연결되는 수동 실행 세그웨이임.
  • 액션세그는 트리거의 터치에 의해 실행되므로 별도의 처리코드가 전혀 필요없지만, 메뉴얼세그는 트리거 없이 수동으로 실행해야 하므로 소스 코드에서 세그웨이를 실행할 메소드를 호출해야함

performSegue(withIdentifier :<세그웨이 식별자>, sender :<세그웨이 실행 객체>)

  • 두개의 인자값은 세그웨이가 여러 개일 경우를 대비한 세그웨이 식별자와 세그웨이를 실행하는 객체정보임.
  • 필요한 시점에서 세그웨이 식별자를 통해 특정 세그웨이를 지정하고 위 메소드를 호출하면 세그웨이가 실행되면서 화면이 전환되는 구조

15.5.3 Unwind - 화면 복귀

  • 새로운 화면으로 전환하는 것은 Wind라고 한다면 Unwind는 wind작업을 해제 한다는 의미. 다시 말해 새로운 화면을 해제하고 본래의 화면으로 돌아간다는 해석이 됨
  • 세그웨이는 목적지가 되는 뷰 컨트롤러의 인스턴스를 자동으로 생성. 따라서 두번째 뷰 컨트롤러에서 첫번째 뷰 컨트롤러로 세그웨이를 연결하면 자동으로 첫번째 뷰 컨트롤러의 인스턴스가 만들어짐. 하지만 이미 첫번째 뷰 컨트롤러의 인스턴스가 존재하는 상황
  • 역방향 세그웨이를 다시 생성한다는 것은 이미 존재하는 뷰 컨트롤러의 인스턴스를 또다시 만들어 낸다는 의미.
  • 뷰 컨트롤러의 인스턴스는 하나 이상 존재해서는 안됨

프레젠테이션 방식으로 이동했을 때 dismiss(animated:)
내비게이션 컨트롤러를 이용하여 이동했을 때 popViewController(animated:)

  • 다른 방법은 세그웨이 레벨에서 제공하는 것으로 이른바 Unwind Segue를 이용
  • 뷰 컨트롤러 도크바 Exit는 Unwind Segue를 구현할 수 있도록 지원함
  • 버튼을 두번째 뷰 컨트롤러에 Present Modally로 연결 후 첫번째 뷰 컨트롤러 클래스에 다음과 같이 소스 코드 작성
  • @IBAction func unwindToVC(_ segue : UIStoryboardSegue){
  • 이경우 반드시 UIStoryboardSegue타입을 인자값으로 입력받도록 정의해야함

Unwind Segue를 이용하여 한꺼번에 여러 페이지 복귀하기
중간페이지로 돌아가기

  • 되돌아갈 화면의 뷰 컨트롤러 클래스에 UIStoryboardSegue 객체를 인자값으로 받는 메소드를 구현해두기만 하면 이를 시스템에서 Unwind 메소드로 인식함
  • 따라서 Exit 아이콘에서 해당 메소드를 연결하는 것으로 Unwind Segue를 손쉽게 구현
  • 이 때문에 호출될 메소드의 이름은 앱 프로젝트 영역에서 구분되는 이름이어야 하며, 각 뷰컨트롤러를 대표할 수 있는 이름으로 만드는 것이 좋음

15.5.4 커스텀 세그

  • 지금까지 사용한 세그웨이 객체는 UIKit 프레임 워크에서 제공하는 UIStoryboardSegue 클래스를 통해 구현된 객체
  • 앱 개발시 기본적인 기능의 세그웨이로는 원하는 기능을 구현하기 힘든 경우 발생
  • 이럴때 대비하여 UIKit 프레임 워크는 UIStoryboardSegue 클래스를 서브 클래싱 하여 새로운 기능을 갖춘 세그웨이 객체를 정의 할 수 있도록 지원함
  • 이를 커스텀 세그라고 부름
  • UIStoryboardSegue를 상속받음
  • 세그웨이 실행을 처리하는 메소드는 perform()이기 때문에 커스텀 세그에서 원하는 화면 전환기능을 구현하기 위해서는 이를 오버라이드해야함
class NewSegue : UIStoryboardSegue {
    
    override func perform() {
        //세그웨이 출발지 뷰 컨트롤러
        let srcUVC = self.source
        
        //세그웨이 도착지 뷰 컨트롤러
        let destUVC = self.destination
        
        UIView.transition(from: srcUVC.view, to: destUVC.view, duration: 2, options: .transitionCurlDown)
        
    }
}

15.5.5 전처리 메소드의 활용

  • 세그웨이를 이용하여 화면을 전환하는 과정에서 뭔가 특별한 처리를 해 주어야 할 때에는 어떻게?
  • 코코아 터치 프레임워크는 세그웨이가 실행되기 전에 특정한 메소드를 호출하도록 정해져 있기 때문에 이를 이용하면 화면을 전환하기 전에 필요한 처리를 해줄 수 있음
  • 이를 전처리 메소드라고 함
  • 세그웨이를 실행하기전에 값을 저장해둘 필요가 있거나 혹은 경고창을 띄워주는 등의 처리를 해야할 경우 전처리 메소드에 해당 내용을 작성해 놓으면 그 내용이 세그웨이가 실행되기 전에 자동으로 실행됨

prepare(for segue : UIStoryboardSegue, sender : Any?) {...}

  • 주의! 이 메소드의 호출주체는 우리가 아니라 시스템이 호출하는 방식
  • 우리가 호출하고 싶을때 임의로 호출하지 못함
  • 일반적으로 뷰 컨트롤러에 연결된 세그웨이는 여러개가 될 수 있는데 이들 세그웨이는 모두 실행전에 전처리 메소드를 공통적으로 호출함
  • 하나의 전처리 메소드를 여러 세그웨이가 공유하는 방식
  • 이 때문에 전처리 메소드는 어느 세그웨이가 자신을 호출할 것인지를 알고 구분해 주어야할 필요가 있는데 그에 대한 정보가 첫번째 매개변수를 통해 전달
  • 이 매개변수를 사용하여 어느 세그웨이가 실행되는 것인지 알 수 있으므로 이를 이용하여 세그웨이에 따른 조건별 작업을 처리하면 됨 (segue.identifier)
  • 전처리 메소드의 두번째 매개변수는 세그웨이를 실행하는 트리거에 대한 정보
  • 하나의 세그웨이는 여러개의 트리거를 가질수 있음. 화면 내에 있는 여러요소가 동일한 세그웨이를 실행할 수 있다는 뜻
  • 만약 액션세그라면 버튼이나 테이블셀 혹은 제스쳐등의 객체가 주 대상이 되고
  • 메뉴얼세그라면 뷰 컨트롤러 자신이 인자값으로 전달될것임
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if (segue.identifier == "custom_segue") {
            NSLog("커스텀세그 실행")
        }else if (segue.identifier == "action_segue"){
            NSLog("액션세그 실행")
        }else {
            NSLog("알수없는 세그 실행")
        }
        
    }
}


블로그 이미지

百見 이 不如一打 요 , 百打 가 不如一作 이다.

,

Ch.13 iOS앱의 구조와 코코아 터치 프레임워크

  • 앱은 기본적으로 시스템 프레임워크에 정의된 원리에 따라 동작하지만, 이 영역을 제외한 나머지 범위에서는 커스텀 코드를 통해 원하는 기능과 유저 인터페이스를 구현할수 있음

13.1앱의 기본 구조

13.1.1 엔트리 포인트와 앱의 초기화 과정

  • Objective-C 도 C를 기반으로 하고 있기 때문에 이를 이용하여 만들어진 iOS앱도 main()함수로부터 시작됨

  • main()함수가 하는일은 실행시 시스템으로부터 전달받은 두 개의 인자값과 AppDelegate클래스를 이용하여 UIApplicationMain()함수를 호출하고 그결과로 UIApplication 객체를 반환

  • 생성된 UIApplication 객체는 UIKit 프레임 워크에 속해있으므로 이후의 앱 제어권은 UIKit 프레임워크로 이관됨

  • UIApplicationMain()함수는 iOS앱에 속하는 부분의 엔트리포인트라고 할수 있음

  • 이 함수는 앱의 핵심객체를 생성하는 프로세스를 핸들링하고, 스토리보드 파일로부터 앱의 유저 인터페이스를 읽어들일뿐만 아니라 내가 작성한 커스텀 코드를 호출해줌으로써 앱 생성 초기에 필요한 설정을 구현할 수 있게 해줌, 또한 이벤트를 입력받기 위한 이벤트 루프를 실행시키키도 함

  • UIApplicationMain()함수가 생성하는 UIApplication은 앱의 본체라고 할 수 있는 객체로 사실상 앱 그 자체를 의미함.

  • UIApplication 객체는 AppDelegate라는 대리 객체를 내세우고 커스텀 코드를 처리할 수 있도록 약간의 권한을 부여함

  • AppDelegate는 UIApplication으로부터 위임받은 일부 권한을 이용하여 커스텀 코드와 상호 작용을 하는 역할을 담당하고, 이를 통해 우리가 필요한 코드를 구현할 수 있도록 도와줌

  • AppDelegate객체는 iOS 애플리케이션 내에서 오직 하나의 인스턴스만 생성되도록 시스템 적으로 보장을 받음

    • 앱이 처음 만들어 질때 객체가 생성되고 앱이 실행되는 동안 계속 유지되다가 앱이 종료되면 그때 함께 소멸하는 등 앱 전체의 생명주기와 함께 함
  • Swift에서는 직접 UIApplicationMain()을 호출하여 delegate 클래스를 인자값으로 전달할 수 없으므로 대신 앱 델리게이트 역할을 할 클래스에 @UIApplicationMain 어노테이션을 걸어 표시하는 방식으로 시스템에 델리게이트 클래스 정보를 전달함.

  • Swift에서는 Main()함수를 통해 UIApplicationMain()을 호출하는 대신 @UIApplicationMain어노테이션을 찾아 해당 클래스를 실행
  • application(_:didFiniShingLaunchingWithOptions:)메소드에 원하는 커스텀 코드를 작성해 두면 앱이 처음 시작될때 해당 코드를 실행할 수 있음
  • 이벤트 루프가 실행되면서 내가 작성한 이벤트 핸들에 의해 커스텀 코드로 연결됨(핸들은 @IBAction등등)
  • 앱이 실행목적 완료하고 더이상 사용되지 않으면 시스템은 앱을 메모리에서 제거하기 위한 준비를 함
  • 이 과정에서 앱 시스템은 델리게이트 클래스applicationWillTerminate(_:)메소드를 호출, 앱 종료시 처리해야할 내용이 있다면 이 메소드 내부에 커스텀 코드로 작성해 두기만 하면 됨

자세한 그림 참고

*Launching an app into the foreground

*Launching an app into the background

*Handling alert-based interruptions

*Transitioning from the background to the foreground

*Moving from the foreground to the background

<[애플공식문서 참고링크] (https://goo.gl/4eEd4U)>

13.1.2 MVC패턴

  • iOS 앱의 객체 관계는 MVC패턴에 기반하고 있음
  • 모델(Model) - 데이터를 담당
  • 뷰(View) - 데이터에 대한 화면 표현을 담당
  • 컨트롤러(Controller) - 모델과 뷰 사이에 위치하여 데이터를 가공하여 뷰로 전달하고 뷰에서 발생하는 이벤트를 입력받아 처리하는 역할을 담당

  • 각각의 역할로 쪼개고 나누어 놓은 이유는 많은 장점을 가지기 때문
  • 가장 큰 장점은 앱에서 사용되는 데이터와 비즈니스 로직을 데이터에 대한 시작적인 표현으로부터 분리해 줌으로써 화면을 신경쓰지 않고도 데이터나 비즈니스 로직을 작성할 수 있다는 점

13.1.3 앱의 상태 변화

  • 앱의 상태변화는 운영체제가 처리하는 영역
  • iOS에서 앱이 가질수 있는 상태값은 다음과 같음
- Not Running : 앱이 시작되지 않았거나 실행되었지만 시스템에 의해 종료된 상태를 나타냄
- Inactive : 앱이 전면에서 실행중이지만, 아무런 이벤트를 받지 않고 있는 상태를 나타냄
- Active : 앱이 전면에서 실행중이며, 이벤트를 받고 있는 상태를 나타냄
- Background : 앱이 백그라운드에 있지만 여전히 코드가 실행되고 있는 상태를 나타냄
- Suspended : 앱이 메모리에 유지되지만 실행되는 코드가 없는 상태

  • 앱의 상태가 변화할 때마다 앱 객체는 앱 델리게이트에 정의된 특정 메소드를 호출
  • 주요 메소드는 다음과 같음
application(_:willFinishLaunchingWithOptions:)
	- 앱이 구동되어 필요한 초기실행과정이 완료되기 직전에 호출되는 메소드
application(_:didFinishLaunchingWithOptions:)
	- 앱이 사용자에게 화면으로 표시되기 직전에 호출되는 메소드
applicationDidBecomeActive(_:)
	- 실행된 앱이 포그라운드, 즉 화면 전면에 표시될때 호출되는 메소드
applicationDidEnterBackground(_:)
	- 앱이 백그라운드 상태에 진입했을때 호출
applicationWillTermainate(_:)
	- 앱이 종료되기 직전에 호출되는 메소드

UIAppdelegateProtocol 공식문서 링크

13.2 iOS와 코코아 터치 프레임 워크

13.2.1 iOS

13.2.2 코코아 터치 프레임워크

13.3 앱을 구성하는 핵심 객체들

13.3.1 iOS 유저 인터페이스의 표현구조

  • 윈도우는 iOS에서 디바이스의 스크린을 빈틈없이 채우기 위한 객체로, 항상 유저 인터페이스 표현 계층의 최상위에 위치
  • 뷰는 콘텐츠를 담아 이를 스크린상에 표시하고 사용자의 입력에 반응
  • 윈도우 객체는 항상 루트 뷰 컨트롤러만을 참조
  • 컨텐츠 뷰 컨트롤러 : 씬을 담당하고 콘텐츠를 표시하는 컨트롤러
  • 컨테이너 뷰 컨트롤러 : 콘텐츠를 배치하는 대신 다른 뷰 컨트롤러를 배치하고, 이들을 서로 유기적인 관계로 엮이도록 만들어줌

13.3.2 뷰 컨트롤러

  • 뷰 컨트롤러는 앱의 근간을 이루는 객체로 모든 앱은 최소한 하나 이상의 뷰 컨트롤러로 구성
  • View Controller : iOS 에서 가장 기본이 되는 컨트롤러로서 앱의 데이터와 표시될 외형을 연결해서 하나의 동적인 화면을 만들어 내는 컨트롤러. 내부에 뷰를 포함하고 있음 (UIKit프레임워크의 클래스는 UIViewController)
  • Navigation Controller : 앱의 화면 이동에 관한 관리와 그에 연관된 처리를 담당해주는 컨트롤러, 뷰를 포함하고 있지 않으므로 다른 컨트롤러와 결합하여 부분적으로 화면을 구성 (UIKit프레임워크의 클래스는 UINavigationController)
  • Table View Controller : 내부에 리스트 형식의 테이블 뷰를 포함하고 있어 여러 항목이나 데이터를 화면에 나열하기 위한 목적으로 사용되는 컨트롤러 (UIKit프레임워크의 클래스는 UITableViewController)
  • Tab Bar Controller : 화면을 나타내는 여러개의 탭이 있고 탭을 터치하면 화면이 전환되는 형태의 앱을 만들고자 할 때 사용되는 컨트롤러 (UIKit프레임워크의 클래스는 UITabbarController)
  • Split View Controller : (UIKit프레임워크의 클래스는 UISplitViewController)

13.3.3 뷰컨트롤러의 상태변화와 생명주기

  • 뷰컨트롤러의 생명 주기는 장면(Scene)의 전환과 복귀에 밀접하게 연관되어 있음
  • 뷰 컨트롤러는 다음과 같이 네가지 상태로 나눌수 있음
	- Appearing : 뷰 컨트롤러가 스크린에 등장하기 시작한 순간부터 등장을 완료하기 직전까지의 상태
	- Appeared : 뷰 컨트롤러가 스크린 전체에 완전히 등장한 상태
	- Disappearing : 뷰 컨트롤러가 스크린에서 가려지기 시작해서 완전히 가려지기 직전까지의 상태
	- Disappeared : 뷰 컨트롤러가 스크린에서 완전히 가려졌거나 혹은 퇴장한 상태

  • 화면이 처음 실행되거나 또는 퇴장한 상태에서 다시 등장하기 시작하는 상태(Appearing)로 바뀌는 동안 뷰 컨트롤러는 앱 객체에 의해 viewWillAppear(_:)메소드가 호출
  • 화면이 등장하기 시작한 단계를 넘어서 완전히 등장하고 나면 viewDidAppear(_:) 메소드가 호출
  • 이 상태에서 다른 액션이 일어나 화면의 전환이 이루어지거나 홈 버튼을 눌러 앱이 백그라운드로 내려가는등 스크린에서 화면이 퇴장하는 상태 변화가 발생하면 그 즉시 viewWillDisappear(_:) 메소드가 호출
  • 상태 변화가 완료 되었을때 viewDidDisappear(_:)메소드가 호출

Life Cycle


블로그 이미지

百見 이 不如一打 요 , 百打 가 不如一作 이다.

,