본문 바로가기

수업 정리

93일차 수업정리(Swift - UIView, Gesture, 이벤트 처리)

 

**UIView

    => iOS 출력
        - AppDelegate 또는 SceneDelegate 의 window -> UIViewController -> UIView -> Layer를 이용해서 드로윙
    => AppDelegate, UIViewController, UIView 는 UIResponder로부터 상속 - 터치 이벤트 처리 가능
        - Layer는 그림을 그리기는 하지만 이벤트를 받을 수는 없습니다.
    => iOS의 터치 이벤트 처리를 위해서는 isUserInteractionEnabled 프로퍼티가 true 여야 합니다.
        - UIImageView 나 UILabel은 기본적으로는 터치 이벤트를 처리할 수 없음
    => iOS에서는 UIView를 화면 출력의 최소 단위로 간주
1. 뷰를 이용한 작업
    => 생성과 초기화
    => 뷰의 구조 관리
    => 이벤트 처리
    => 화면 출력

2. UIView의 하위 클래스
    => 모든 뷰들은 공통적인 이벤트 처리 속성을 가지고 있음
    => 다른 뷰와 다른 이벤트 처리는 Delegate 프로토콜을 이용해서 별도로 소유, 데이터 출력뷰는 DataSource 라는 프로토콜을 소유
  1) UIWindow: 애플리케이션의 시작이 되는 뷰

    - 기본적으로 AppDelegate 나 SceneDelegate의 속성으로 제공(~iOS 12 : AppDelegate 제공, 13 : SceneDelegate에서 제공, iOS13에

    서 프로젝트 구조상 SceneDelegate가 추가, Swift UI 사용가능), 크기 변경불가 - 디바이스의 전체 크기로 만들어짐, 까만 배경색

  2) UILabel, UIProgressView, UIActivityIndicatiorView, UIImageView, UIToolbar, UITableViewCell, UIStepper 등이 존재
  3) UIPickerView(여러 개의 항목 중 선택할 때 사용하는 뷰, UIPickerViewDelegate, UIPickerViewDataSource)
  4) UITabBar(UITabBarDelegate), UINavigationBar(UINavigationBarDelegate), UISearchBar(UISearchBarDelegate)
  5) UIScrollView(UIScrollViewDelegate)

    -> UITextView(UITextViewDelegate)

    -> UITableView(UITableViewDelegate, UITableViewDataSource)

    -> UICollectionView(UICollectionViewDelegate, UICollectionViewDataSource)

    -> UIWebView(UIWebViewDelegate), UIMapView)
  6) UIControl

3. View의 생성(allocation - 메모리 할당)과 초기화(initialize - 할당받은 메모리를 초기값으로 설정)
  1) 인터페이스 빌더에서 생성하고 속성을 설정해서 초기화
    => init(coder:Coder)라는 메소드를 이용해서 초기화
    => UIView를 SubClassing 할 때는 인터페이스 빌더에서 디자인 할 거면 init(coder:Coder)를 오버라이딩
  2) 코드를 이용해서 생성하고 초기화
    => 동적으로 생성해서 기본적으로 제공되는 윈도우나 UIView의 addSubView라는 메소드를 이용해서 배치

4. Property
    => CGRect frame: 위치 와 크기
    => CGRect bounds: 위치(origin - 기준점, 회전이나 크기 변화, 위치 변화가 일어날 때의 기준점)와 크기
    => CGPoint center: 중앙점의 위치
    => CGAffineTransform transform: 2차원 행렬을 이용해서 위치, 크기, 각도등을 설정하기 위한 속성
    => UIColor backgroundColor
    => CGFloat alpha
    => Bool opaque
    => Bool clearsContextBeforDrawing: 그리기 전에 지우고 그릴 것인지 여부
    => Int tag: 뷰를 구분하기 위한 ID
    => Bool hidden
    => CALayer layer: 뷰를 세부 속성을 설정할 때 이용

 

5. 계층 관련 메소드
    => addSubView(view:UIView): 뷰를 추가 - 상위 뷰가 호출
    => removeFromSuperView: 뷰를 제거 - 자기 자신이 호출
    => 외에도 위치 변경, 중간 삽입 메소드 등이 존재 - 이런 메소드들은 Interface Builder 없이 동적으로 뷰의 출력과 제거하는 경우 사용
    => 이렇게 동적으로 뷰를 제어하는 경우 뷰에 변화가 생기면 호출되는 콜백 메소드들도 정의되어 있는데 실제 사용은 거의 안함

6. UIWindow
    => 전체 애플리케이션 출력을 위한 뷰
    => 디바이스 전체 크기 그리고 검정색 배경으로 생성되서 제공

7. UILabel
    => 텍스트 출력을 위한 뷰
    => text 속성을 이용해서 텍스트를 읽고 쓰기를 합니다.
    => Timer 클래스: 일정한 주기를 가지고 메소드를 호출하는 클래스
    => Date 클래스: 날짜 관련 클래스
    => DateFormatter 클래스: 날짜를 원하는 형식 문자열로 만들어주는 클래스
    => 위의 클래스들을 이용해서 현재 시간을 레이블에 출력 - 1초 단위로 출력
  1) Project 나 Target을 생성

  2) Main.stroyboard 파일의 ViewController에 UILabel을 추가

  3) UILabel을 코드로 제어하기 위해서 ViewController 클래스에 변수와 연결 - label
    => 인터페이스 빌더에서 뷰 선택, 마우스 오른쪽 클릭 상태에서 New Referencing Outlets 항목의 원을 선택, 소스 코드 창으로 드래그

        후 떨구고 변수 이름을 입력
    => 제대로 설정하면 소스 코드 창에 IBOutlet 이 추가된 변수가 생성
  4) ViewController.swift 파일의 viewDidLoad 메소드에 추가

        //closure를 이용한 타이머 생성
        let timer : Timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){(t:Timer) -> Void in
            
            let date:Date = Date()
            let formatter:DateFormatter = DateFormatter()
            formatter.dateFormat = "yyyy-MM-dd ccc hh:mm:ss"
            let msg = formatter.string(from: date)
            
            //클로저 안에서 외부 클래스의 인스턴스 속성을
            //참조할 때는 반드시 self를 붙여야 합니다.
            //iOS에서는 함부로 self를 생략하면 안됩니다.
            self.label.text = msg
        }
        
        //위치나 크기 또는 각도 변환 - transform
        label.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2)
        //세부적인 옵션은 layer를 이용해서 변경
        label.layer.borderWidth = 3
        //속성을 설정하다보면 UI 타입이 아닌 CG 타입으로
        //설정해야 하는 경우가 있습니다.
        //Objective-C에서는 클래스 형태의 속성을
        //사용하다가 swift에서는 값만 필요한 경우에는
        //struct로 변경해서 발생
        label.layer.borderColor = UIColor.red.cgColor

 

8. UIControl
    => UIKit이 제공하는 작은 부품
    => 하위 클래스로 UIButton, UIDatePicker, UIPageControl, UISegmentedControl, UITextField, UISlider, UISwitch, UIStepper 제공
    => 이 클래스들은 속성에 메소드를 연결해서(selector) 이벤트 처리를 할 수 있도록 만들어진 클래스
    => 인터페이스 빌더 선택, 마우스 오른쪽 클릭(이벤트에 해당하는 속성), 이벤트 선택, 뷰컨트롤러 파일로 드래그시, 메소드 생성 창 출력
    => addTarget이라는 메소드를 호출해서 직접 연결하는 것도 가능
    => 자주 사용하는 이벤트 속성 (touchUpInside - 클릭, valueChanged - 값이 변경될 때, didEndOnExit - Return 키를 눌렀을 때
  1) UIButton - 버튼
  2) UITextField - 한 줄 입력받기 위한 Control 
  3) UISlider - 일정한 범위 내의 값을 선택하기 위한 Control
  4) UISegmentedControl - 여러 개의 버튼을 하나의 묶음으로 만든 Control
  5) UISwitch - ON/OFF를 선택할 수 있는 Control
  6) UIStepper - +, -버튼을 이용해서 값을 입력
  7) UIPageControl - 페이지 번호를 출력하기 위한 Control, 실제 이벤트는 값 변경시 왼쪽을 눌렀는지 오른쪽을 눌렀는지만 확인 가능
  8) UIDatePicker: 날짜를 선택하기 위한 Control

9. iOS의 이벤트 처리 패턴
  1) 상속을 받아서 재정의
    => touch, motion 등은 UIViewController 나 UIView로 부터 상속을 받아서 재정의 하는 형태로 사용
  2) 이벤트 속성에 method를 연결하는 selector 패턴 : UIControl, Notification이 이 구조를 사용
  3) delegate pattern
    => 프로토콜을 구현한 객체를 설정
    => 이벤트 처리를 다른 클래스의 객체에게 위임하는 형태
    => iOS에서는 이 부분을 클래스를 별도로 생성하지 않고 extension을 이용해서 처리

10. 실습
    => 화면에 레이블 1개와 텍스트 필드 1개 그리고 버튼을 1개 배치해서 버튼을 누르면 텍스트 필드의 내용을 레이블에 출력
  1) 기존의 ViewController에 레이블 1개와 텍스트 필드 1개 그리고 버튼을 1개 추가
  2) 레이블과 텍스트 필드를 변수 연결 : lblMsg, tfMsg
  3) 버튼의 touchUpInside 속성에 메소드를 연결 : click
    => 버튼을 선택하고 마우스 오른쪽을 클릭해서 sent actions 항목에서 이벤트를 선택

@IBAction func click(_ sender: Any) {
        //텍스트 필드의 내용을 레이블에 출력
        lblMsg.text = tfMsg.text
        //텍스트 필드의 내용 초기화
        tfMsg.text = ""
    }

 

11. Delegate Pattern
    => 프로토콜에 이벤트 처리를 위한 메소드의 모형을 선언하고 Delegate를 사용할 객체에 메소드를 구현한 인스턴스를 설정하는 패턴
    => 대부분의 GUI Programming 언어는 이 패턴의 이벤트 처리를 기본으로 하는 경우가 많음
    => iOS에서는 별도의 클래스를 만들기 보다는 대부분의 경우 extension을 이용
    => iOS에서 Delegete의 이름은 클래스명Delegate
    => DataSource라는 프로토콜이 존재하면 이 클래스는 MVC 패턴을 이용해서 데이터를 출력하기 위한 클래스

12. 키보드 문제
    => iOS에서는 키보드를 사용할 수 있는 객체가 FirstResponder(포커스를 가진 객체)가 되면 키보드가 화면에 출력
    => FirstResponder로 설정 : 객체.becomeFirstResponder() 호출
    => FirstResponder를 해제 : 객체.resignFirstResponder() 호출
    => 스마트 폰은 화면이 작아서 키보드가 계속 노출되어 있으면 화면의 일부분을 사용할 수 없게 됨
        - 입력이 끝나면 키보드를 화면에서 제거
    => 빈 화면을 터치할 때, 입력 도중 Return을 눌렀을 때 제거, 키보드가 화면에 출력될 때 하단 뷰를 상단으로 이동시키는 방법등을 사용
  1) 시작하자 마자 키보드가 화면에 출력되도록 작성
    => ViewController.swift 파일의 viewDidLoad에 작성

//tfMsg를 FirstResponder로 설정
//시작하자마자 키보드가 화면에 출력
tfMsg.becomeFirstResponder()

 

  2) ViewController의 빈 영역을 터치하면 키보드를 제거하도록 하기
    => ViewController 클래스에 터치가 발생하면 호출되는 메소드를 오버라이딩(재정의)

//터치가 발생할 때 호출되는 메소드
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //상위 클래스의 메소드를 호출
        super.touchesBegan(touches, with: event)
        //FirstResponder 해제 - 키보드가 사라집니다.
        tfMsg.resignFirstResponder()
    }

 

  3) Delegate 이용 - 클래스 내부에 구현하고 코드로 연결
    => 텍스트 필드에서 Return 키를 누르면 키보드를 화면에서 제거
    => UITextFieldDelegate 프로토콜의 textFiledShoudReturn 이라는 메소드가 Return 키를 누르면 호출되는 메소드
    => 프로토콜을 class에 conform

class ViewController: UIViewController, UITextFieldDelegate


    => 필요한 메소드를 구현

//텍스트 필드에서 return 키를 누르면 호출되는 메소드
    //swift에서는 프로토콜의 메소드를 만들 때는
    //override를 붙이지 않음
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        tfMsg.resignFirstResponder()
    }

 

    => viewDidLoad 메소드에 텍스트 필드의 delegate 속성에 객체를 설정

//tfMsg의 delegate를 설정
tfMsg.delegate = self 

 

  4) 클래스 외부에 구현하고 인터페이스 빌더에서 연결
    => viewDidLoad 메소드에서 delegate 설정 부분을 제거

//tfMsg.delegate = self 

 

    => 이전에 만든 메소드를 제거

    => extension 생성 - 기존 클래스 기능 확장, C#에서도 기능 확장가능, C#, Swift는 프레임워크가 제공하는 클래스의 기능도 확장 가능

extension ViewController : UITextViewDelegate{
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        tfMsg.resignFirstResponder()
    }
}

 

    => 인터페이스 빌더에서 텍스트 필드 선택, 마우스 오른쪽 클릭 후 delegate 속성을 선택, ViewController를 드래그 해주면 연결

13. Notification
    => 서버나 시스템이 어떤 상태 변화를 클라이언트 또는 애플리케이션에게 알려주는 것
    => Push Notification: 클라이언트의 요청이 없는데도 서버가 알림을 주는 것
        - 예전에는 스마트 폰 애플리케이션들만 가능했는데 지금은 Web에서도 가능
    =>iOS에서 Notification이 발생했을 때 메소드 호출 
        - NotificationCenter.default.addObserver(메소드소유객체, selector:#selector(메소드명), name:notificatino명, object:전달객체)
    => 키보드가 보여질 때 발생하는 노티피케이션: UIResponder.keyboardWillShowNotification
    => 키보드가 보여질 때 발생하는 노티피케이션: UIResponder.keyboardWillHideNotification
    => iOS에서는 클래스 안에서 사용될 옵션을 클래스 내부의 enum으로 생성
        - iOS 12 부터 enum의 상수 이름을 변경
        - 이전에는 모두 대문자로 이름을 만들었으나 지금은 일반적인 변수 명명법과 동일한 형태로 변경
        - swift를 사용하는 이전 소스코드가 에러 발생시 상수 부분을 확인

  1) ViewController 클래스에 키보드가 화면에 출력될 때 호출되는 메소드를 작성

    //키보드가 화면에 출력될 때 호출될 메소드
    @objc func keyboardWillShow(notification:Notification){
        //레이블을 위로 이동
        let frame = lblMsg.frame
        let moveFrame = CGRect(x: frame.origin.x, y: frame.origin.y - 50, width: frame.size.width, height: frame.size.height)
        lblMsg.frame = moveFrame
    }
    //키보드가 화면에서 없어질 때 호출될 메소드
    @objc func keyboardWillHide(notification:Notification){
        //레이블을 위로 이동
        let frame = lblMsg.frame
        let moveFrame = CGRect(x: frame.origin.x, y: frame.origin.y + 50, width: frame.size.width, height: frame.size.height)
        lblMsg.frame = moveFrame
    }

 

  2) viewDidLoad 메소드에 노티피케이션을 등록

//노티피케이션 등록 - selector 대신에 closure 사용 가능
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)

 

**UIImageVIew

    => 이미지 출력을 위한 뷰
    => image 프로퍼티에 이미지를 설정
        - UIImage 객체 생성 - UIImage(named:프로젝트 안에 포함된 이미지 파일이름)
        - iOS에서는 프로젝트 내의 모든 디렉토리는 의미가 없고 디렉토리 안에 파일을 복사했더라도 파일이름만 기재
    => animationImages 속성에 UIImage 배열을 설정하면 애니메이션이 가능
    => 애니메이션 시작과 종료는 startAnimation(), stopAnimation 메소드 호출, isAnimation 프로퍼티 확인시 애니메이션 여부 확인 가능

**Gesture

    => 터치 이벤트 처리를 위해서 추가된 클래스
    => UIView 클래스의 인스턴스에 적용
    => 일종의 delegate 패턴 이나 selector 패턴과 유사
    => 터치 이벤트를 세분화해서 쉽게 터치 이벤트를 적용하기 위해서 생성
1. 종류
    => UITapGestureRecognizer: 두드림 이벤트
    => UIPinchGestureRecognizer: 두 손가락으로 벌이거나 줄이는 것 이벤트
    => UIPanGestureRecognizer: 뷰의 드래그
    => UIScreenEdgePanGestureRecognizer: 드래깅이나 패닝이 디스플레이 화면 끝에서 발생하는 경우
    => UISwipeGestureRecognizer: 빠르게 드래그
    => UIRotationGestureRecognizer: 두 손가락으로 회전
    => UILongPressGestureRecognizer: 길게 누르기

 

2. 공통된 메소드
    => location 이라는 메소드를 전부 소유해서 이벤트가 발생한 좌표를 리턴

3. 프로퍼티
    => 각 제스쳐마다 별도로 소유

4. 사용방법
  1) 제스쳐 객체를 생성
  2) 제스쳐 객체에 메소드를 연결
  3) 뷰에 연결

    => 뷰객체.addGestureRecognizer(체스쳐 객체)
        - 연결하기 전에 제스쳐의 프로퍼티를 이용해서 옵션을 설정가능
    => numberOfTapsRequired : 두드린 횟수
    => numberOfTouchesRequired : 두드리는 손가락의 개수를 개수를 설정
    => 터치의 개수는 11개까지 설정 가능 - 손가락 10개와 펜 1개

  4) 뷰에서 해제
    => add 대신에 remove를 사용

5. Gesture 실습
  1) Target 추가
  2) 프로젝트에서 사용할 이미지 파일을 추가 
  3) ViewController에 사용할 뷰를 인스턴스 변수로 추가

    //이미지 뷰 변수
    //!를 붙여서 변수를 선언하면
    //nil을 대입할 수 있고 사용을 할 때는 !없이 사용 가능
    var imgView:UIImageView!

 

  4) viewDidLoad 메소드에 이미지 뷰와 이미지를 생성하고 출력하는 코드를 작성

        //디바이스의 origin 과 size를 저장
        var rect:CGRect = UIScreen.main.bounds
        //인스턴스를 생성
        //전체 화면 크기의 절반으로 생성
        imgView = UIImageView(frame: CGRect(x: 50, y: 50, width: rect.size.width/2, height: rect.size.height/2))
        //중앙에 배치
        imgView.center = self.view.center
        
        //출력할 이미지 객체 생성
        let image:UIImage! =
            UIImage(named: "red0.jpg")
        //이미지 뷰에 이미지 설정
        imgView.image = image
        
        //이미지 뷰를 현재 뷰에 추가
        view.addSubview(imgView);

 

  5) 이미지 뷰 와 레이블은 터치를 사용할 수 없기 때문에 터치를 사용할려면 속성을 변경
    => viewDidLoad 메소드에 이미지 뷰가 터치를 받을 수 있도록 속성을 수정

        //이미지 뷰가 터치를 받을 수 있도록 설정
        imgView.isUserInteractionEnabled = true

 

  6) ViewController.swift 파일에 탭 제스쳐에 selector로 사용할 메소드를 생성

    //탭 제스쳐에 연결할 메소드를 생성
    @objc func tagGestureRecognizerMethod(sender:UITapGestureRecognizer){
        if imgView.contentMode == UIView.ContentMode.scaleAspectFit{
            imgView.contentMode = UIView.ContentMode.center
        }else{
            imgView.contentMode = UIView.ContentMode.scaleAspectFit
        }
    }

 

  7) viewDidLoad 메소드에 UITapGestureRecognizer 객체를 생성하고 옵션을 설정해서 이미지 뷰에 적용

      //탭 제스쳐 생성
        let tap = UITapGestureRecognizer(target: self, action: #selector(tagGestureRecognizerMethod(sender:)))
        //옵션 설정
        tap.numberOfTapsRequired = 2 //2번 두드려야 동작
        tap.numberOfTouchesRequired = 2 //2 손가락으로 두드려야 함
        //뷰에 적용
        imgView.addGestureRecognizer(tap);

 

  8) 실행을 하고 Option(ALT)를 누른 채 이미지 뷰를 2번 탭하면 이미지의 보기모드가 변경됨

6. PinchGesture
    => 2개의 손가락을 이용해서 벌리거나 좁히는 제스쳐
    => 확대 축소를 할 때 주로 이동
    => 2개의 손가락을 이용해서 ImageView를 확대 축소하기
    => UIPinchGestureRecognizer에는 scale 과 velocity라는 속성이 제공
        - scale은 처음 핀치가 발생했을 때 거리를 1로 하고 거리가 멀어지거나 가까워지면 비율에 맞추어서 변경됨
        - velocity는 핀치의 속도

  1) 이전까지의 확대 축소 배율을 저장할 변수를 ViewController에 인스턴스 변수로 추가

    //이전 확대 축소 배율을 저장할 변수
    var oldScale : CGFloat = 1.0

 

  2) UIPinchGestureRecognizer가 사용할 메소드를 선언

    //핀치 제스쳐에 연결할 메소드를 생성
    @objc func pinchMethod(sender:UIPinchGestureRecognizer){
        //현재 배율을 가져옵니다.
        let newScale = sender.scale
        //확대 축소
        if newScale > 1{
            imgView.transform = CGAffineTransform(scaleX: oldScale+(newScale-1), y: oldScale+(newScale-1))
        }else{
            imgView.transform =
            CGAffineTransform(scaleX: newScale, y: newScale)
        }
        
        //핀치가 종료되면 현재 확대 축소 배율을 저장
        if sender.state == UIGestureRecognizer.State.ended{
            if newScale > 1{
                oldScale = oldScale + (newScale-1)
            }else{
                oldScale = oldScale * newScale
            }
        }
    }

 

  3) viewDidLoad 메소드에서 imgView에 PinchGesture 적용

        //핀치 제스쳐를 생성해서 imgView에 적용
        let pinch = UIPinchGestureRecognizer(target: self, action: #selector(pinchMethod(sender:)))
        imgView.addGestureRecognizer(pinch)

 

  4) 뷰의 상태 변화
    => 상태에 변화를 주게 되면 마지막 상태를 저장
        - 마지막 상태 미저장시 변경 결과가 적용되지 않고, 초기값에서 적용 되므로 두번째 할 때 제대로 적용이 되지 않을 수 있음

7. UIRotationGestureRecognizer
    => 2개의 손가락으로 회전을 하는 제스쳐
    => 회전 각도를 알 수 있는 rotation 과 속도를 알 수 있는 velocity 속성이 제공
    => rotation은 라디언 각도
    => 이미지 뷰를 2개의 손가락으로 회전하면 같이 회전하도록 작성

  1) 마지막에 회전한 각도를 저장할 변수를 인스턴스 변수로 선언

    //마지막 회전 각도를 저장할 변수
    var originRotation : CGFloat = 0
    var lastRotation : CGFloat = 0

 

  2) 로테이션 제스쳐에 사용할 메소드를 생성

//메소드
    @objc func rotateMethod(sender:UIRotationGestureRecognizer){
        
        if sender.state == .began{
            sender.rotation = lastRotation
            originRotation = sender.rotation
        }else if sender.state == .changed{
            let newRotation = sender.rotation + originRotation
            imgView.transform =
                CGAffineTransform(rotationAngle: newRotation)
        }else if sender.state == .ended{
            lastRotation = sender.rotation
        }
    }

 

  3) viewDidLoad 메소드에 제스쳐 연결하는 코드를 추가

        //회전 제스쳐를 생성해서 imgView에 적용
        let rotate =
            UIRotationGestureRecognizer(target: self, action: #selector(rotateMethod(sender:)))
        imgView.addGestureRecognizer(rotate)

 

8. GestureRecognizer를 이용하면 터치의 동작 자체를 판단할 필요없이 적절할 제스쳐만 찾아서 사용하면 되기 때문 편리


**이벤트 처리 방법

    => 상속받아서 재정의하는 구조
    => UIControl 이나 UIBarItem은 이벤트 속성에 메소드를 연결해서 사용
        -> 인터페이스 빌더에서 드래그해서 만들면 IBAction으로 시작하는 메소드가 생성되고 이 메소드에 내용을 작성
        -> 코드 상에서 addTarget 이라는 메소드를 호출해서 selector 나 closure를 지정하는 방법도 동일한 방법입니다.
    => Delegate 이용: Delegate를 conform 한 클래스에 메소드를 만들고 이 클래스의 객체를 delegate 속성에 설정
        - 별도의 클래스 제작, extension을 이용해서 현재 클래스의 기능을 확장해서 메소드를 만들어도 됨
        - 연결 시, delegate 속성에 인스턴스의 참조를 직접 대입, 인터페이스 빌더에서 delegate 속성을 선택하고 드래그해서 연결
    => UIView의 경우는 GestureRecognizer를 이용할 수 있음
    => Notification에 메소드를 등록해서 사용할 수 있음 - Notification은 시스템이 발생시키는 이벤트

**UIScrollView

    => GUI 시스템에서는 자신보다 큰 콘텐츠는 출력불가
    => 자신보다 큰 콘텐츠를 출력시, UIScrollView를 배치하고 그 위에 Contents를 출력할 뷰를 배치해서 스크롤이 가능하도록 제작
    => UIScrollView에는 하나의 뷰만 추가 가능
        - 여러 개의 뷰를 배치하고자 할 때는 UITableView 나 UICollectionView 또는 UIStackView를 이용

1. UIScrollView를 출력하고 delegate를 이용해서 확대 축소를 적용
  1) 새로운 target을 생성
  2) 출력할 이미지 파일을 target에 복사
  3) 이미지를 출력할 이미지 뷰 변수를 ViewController.swift 파일에 인스턴스 변수로 선언
    - var imageView:UIImageView!
  4) ViewController.swift 파일의 viewDidLoad 메소드에서 UIImage 객체를 생성해서 ImageView 에 출력하는 코드를 작성

        //UIImage 객체 생성
        let image = UIImage(named: "large.jpg")
        //이미지를 가지고 이미지 뷰를 생성
        imageView = UIImageView(image: image)
        //뷰에 출력
        self.view.addSubview(imageView)

 

  5) UIScrollView를 인스턴스 변수로 선언

var scrollView:UIScrollView!

 

  6) viewDidLoad에서 이미지 뷰를 바로 출력하지 않고 UIScrollView 위에 배치해서 출력하도록 수정

        //UIImage 객체 생성
        let image = UIImage(named: "large.jpg")
        //이미지를 가지고 이미지 뷰를 생성
        imageView = UIImageView(image: image)
        //뷰에 출력
        //self.view.addSubview(imageView)
        
        //스크롤 뷰 생성 - 현재 디바이스의 크기와 동일하게
        scrollView = UIScrollView(frame: UIScreen.main.bounds)
        //스크롤 뷰는 contentSize를 설정해야 함
        scrollView.contentSize = imageView.frame.size
        //스크롤 가능하도록 설정
        scrollView.isScrollEnabled = true
        //이미지 뷰를 스크롤 뷰에 배치하고 스크롤 뷰를
        //현재 뷰 컨트롤러의 뷰에 배치
        scrollView.addSubview(imageView)
        self.view.addSubview(scrollView)
        scrollView.bounces = false

 

2. UIScrollViewDelegate
    => ScrollView 만의 이벤트 처리 메소드를 소유한 프로토콜 
  1) 줌 관련 메소드

    - optional func viewForZooming(_ ScrollView : UIScrollView) -> UIView?
    => 스크롤 뷰 안에서 실제 줌이 발생한 뷰를 설정해주는 메소드 : 여기서 리턴한 뷰가 확대 축소
        - optional func scrollViewDidEndZooming(_ ScrollView : UIScrollView, with view:UIView?, atScale scale:CGFloat) 
    => 줌 종료시 호출되는 메소드: 매개변수1 - 이벤트가 발생한 뷰, 매개변수2 - 스크롤이 발생한 내부 뷰, 매개변수3 - 확대 축소 배율
    => 확대 축소가 끝나면 그 배율을 변수에 저장하기 위해서 사용하는 메소드
        - optional func scrollViewWillBeginZooming(_ scrollView:UIScrollView)
    => 확대 축소 직전에 호출되는 메소드
  2) 스크롤 관련 메소드
    - optional func scrollViewDidScroll(_ scrollView:UIScrollView)
    => 스크롤이 끝나고 난 후 호출되는 메소드

3. ViewController.swift 파일의 viewDidLoad 메소드에서 delegate를 설정

        //최대 최소 확대 배율을 설정
        scrollView.maximumZoomScale = 2.0
        scrollView.minimumZoomScale = 0.5
        //delegate 설정
        scrollView.delegate = self

 

4. ViewController.swift 파일에 UIScrollViewDelegate 프로토콜을 conform 하는 extension을 생성하고 필요한 메소드를 구현

//클래스 기능 확장
extension ViewController:UIScrollViewDelegate{
    //실제 줌이 발생할 뷰를 설정하는 메소드
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }
}

 

5. iOS에서는 공통된 이벤트 와 자신만이 소유한 이벤트를 서로 다른 방법으로 구현
    => 공통된 이벤트는 상위 클래스에 만들어 두던가 속성으로 제공
        - 속성에 메소드를 설정하는 것: 이전에는 selector
        - selector 설정: @objc가 붙은 메소드를 만들고 #selector(메소드이름)을 이용해서 설정
        - (자료형) -> 자료형 의 형태로 대입: closure를 대입(메소드를 만들어도 되지만 {(변수명:자료형) -> 자료형 in 수행할 내용} 로 작성
        - 이 매개변수가 가장 마지막일 때는 () 외부에 작성가능
        - trailing closure 라고 합니다.
    => 이전에 selector를 이용하던 API 들이 거의 대부분 closure를 대입할 수 있는 형태로 변경
        - 이벤트 핸들링시 반드시 @objc 메소드를 만드는 방법과 closure를 만드는 방법을 숙지
    => 자신만이 소유하는 이벤트는 protocol로 만들어두고 이 프로토콜을 구현한 객체를 설정
        - 프로토콜을 구현할 때 별도의 클래스를 만들지 않고 extension을 활용하는 경우가 많음