본문 바로가기
개발(Development)/iOS(아이폰)

[한글 번역_06] Start Developing iOS Apps (Swift) - Building the UI > Implement a Custom Control

by 카레유 2015. 10. 30.
안녕하세요 카레유입니다.

Apple에서 제공해주는 Swift iOS 앱 개발 가이드를 한글 번역해보았습니다.

영어실력도, 개발실력도, 심지어 한국어 실력도 미흡합니다.

부족한 부분이 많지만, 많은 도움 되시길 바라겠습니다.

아직 작성 중인 내용으로 시간을 갖고 개선해 나갈 생각입니다.

감사합니다.





Implement a Custom Control

이번 레슨에서는 FoodTracker앱에서 음식에 대한 평가를 할 수 있는 별점평가 컨트롤(rating control)을 만들어보겠습니다. 이번 과정을 완료하면 아래와 같은 앱을 만들 수 있습니다.

image

Learning Objectives

At the end of the lesson, you’ll be able to:

  • 커스텀 소스 코드 파일을 만들어 스토리보드의 엘리먼트와 연결하기

  • 커스텀 클래스 정의하기

  • 커스텀 클래스의 생성자(initializer) 구현하기

  • UIView를 컨테이너로 사용하기

  • 프로그램적으로(코드로) View를 노출시키는 방법

Create a Custom View

음식에 대한 평가를 하기 위해서 사용자가 음식에 별표로 점수를 부여할 수 있는 control(button 같은 것들)이 필요합니다. control을 만드는 다양한 방법이 있지만, 우리는 직접 코드를 작성하여 스토리보드에서 사용할 수 있는 우리만의 custom view를 만둘어보겠습니다

아래 보이는 별들이 우리가 구현할 rating control입니다.

image

rating control은 음식을 별을 통해서 0, 1, 2, 3, 4, 5점으로 평가할 수 있습니다. 별을 탭하면 해당 별까지 색이 칠해집니다.(세 번째 별을 탭하면, 왼쪽 세개의 별이 색칠됩니다) 채워진 별 하나당 1점으로 계산이 됩니다.

To begin designing the UI, interaction, and behavior of this control, start by creating a custom view (UIViewsubclass. UI를 디자인하고, 사용자와 상호작용하는 control을 만들기 위해 먼저 UIView를 상속받는 custom view를 만들겠습니다.

To create a subclass of UIView

  1. "File > New > File"을 클릭하세요(아니면 단축키로 Command-N 을 누르셔도 됩니다)

  2. 다이얼로그가 뜨면 왼쪽에서 iOS를 선택하세요

  3. 오른쪽에서는 Cocoa Touch Class를 선택하고 Next를 클릭하세요

  4. Class 필드에 "RatingControl"이라고 입력하세요

  5. Subclass of 필드에서는 "UIView"를 선택해주세요

  6. Language 옵션은 Swift로 설정해주세요

    image
  7. Next를 클릭해주세요

    저장경로는 project 디렉토리가 디폴드입니다. 

    Group옵션은 앱 이름(FoodTracker)이 디폴트입니다. 

    Targets 섹션에서는 우리의 앱이 선택되어 있으며 tests는 선택되어 있지 않은게 디폴트입니다.

  8. 디폴트 상태 그대로 둔 채, Create를 클릭하세요

    RatingControl 클래스 파일인 RatingControl.swift파일이 생성됩니다. 우리는 지금 UIView의 서브클래스인 RatingControl 커스텀 뷰를 만든 것입니다

  9. 아래와 같이 RatingControl.swift 파일에 자동으로 생성된 템플릿 구현부의 주석을 모두 지워주세요.

    1. import UIKit
    2. class RatingControl: UIView {
    3. }

일반적으로 view를 만드는 방식은 두가지가 있습니다.

1) frame을 통해 view를 초기화 및 생성하여 수동으로 UI에 추가하는 방법( initializer : init(frame:) ) 

2) 스토리보드를 통해 view를 로드하는 방법( initializer : init(coder:) ) 

initializer는 클래스의 인스턴스를 생성하는 메서드입니다. initializer 메서드에서는 각 프러퍼티 값을 초기화(초기값 할당)하고, 그외 기타 세팅 작업을 할 수 있습니다. (Java 의 생성자와 비슷하다고 할 수 있습니다)

우리는 스토리보드에 view를 만들어 사용할 것이므로 슈퍼클래스인 UIView의 init(coder:)메서드를 오버라이드하여 view를 추가하는 작업을 하겠습니다.

To override the initializer

  1. RatingControl.swift파일의 클래스 선언부 바로 아래에 다음과 같은 주석을 추가하세요

    1. // MARK: Initialization
  2. 주석 바로 밑에 아래와 같이 init이라고 입력하세요

    code completion이 나타나서 자동완성 기능을 이용할 수 있습니다. 

    image
  3. 두 번째 메서드인 init(coder:) 를 선택하고 리턴키를 누르세요

    1. init(coder aDecoder: NSCoder!) {
    2. }

    XCode가 code completion기능을 통해 해당 메서드의 선언부를 추가합니다.

  4. "error fix-it" 표시를 클릭하세요(왼쪽에 빨간 동그라미 아이콘) - required 키워드를 추가하라고 안내합니다.

    image
    1. required init(coder aDecoder: NSCoder!) {
    2. }

    UIView를 상속받는 모든 서브클래스 들은 initializer 를 구현할 때 반드시 init(coder:) 메서드를 구현해야합니다. Swift 컴파일러도 이 사실을 알고 있기 때문에 코드에 이 내용을 추가하라는 "fix-it"기능을 제공합니다. "fix-it"기능은 코드 상의 에러를 해결할 수 있는 방법을 제공해주는 기능입니다.

  5. 먼저 슈퍼클래스(UIView)의 initializer를 호출하는 코드를 작성하세요(슈퍼클래스에 이미 선언되어 있는 initializer가 필요한 초기값 세팅 등을 알아서 처리합니다. 우리는 따로 필요한 작업만 하면됩니다.)

    1. super.init(coder: aDecoder)

Your init(coder:) initializer should look like this: 근데 우리는 별도로 초기화할 것이 없으므로 아래와 같이만 작성해주면 initializer 코딩을 완료한 것입니다.

  1. required init(coder aDecoder: NSCoder!) {
  2. super.init(coder: aDecoder)
  3. }

Display the Custom View

우리가 직접 제작한 custom view를 노출시키기 위해서는, UI에 view를 추가하고, 코드와 연결시켜야 합니다.

To display the view

  1. 스토리보드를 열어주세요

  2. Object library에서 "View"객체를 찾아서 stack view내부의 image view 아래에 드래그해주세요

  3. 추가한 View를 선택한 상태에서 utility area의 Size inspector( image )를 열어주세요. 

    inspector selector 바의 5번째 항목이고, 선택한 객체의 사이즈와 위치를 설정할 수 있는 inspector입니다

    image
  4. intrinsic Size 필드의 값을 "Placeholder"로 선택해주세요

  5. 바로 아래 항목인 Height필드에는 44를, Width필드에는 240을 입력하고 리턴키를 눌러주세요

    지금 까지 작업한 UI 화면은 아래와 같습니다.

    image
  6. View가 선택된 상태에서 Identity Inspector(image )를 열어주세요.

    Identity inspector은 스토리보드 상의 객체가 어느 클래스에 속하는 지와 같은 속성을 설정할 수 있습니다. 

    image
  7. Identity inspector에서 Class필드의 값을 RatingControl로 설정해주세요

    image

Add Buttons to the View

이제 커스텀 UIView의 서브클래스인 RatingControl의 기초작업은 다했습니다. 이제 할 일은 우리가 만든 View에 button들을 추가해서 사용자가 음식에 대한 평가를 할 수 있게 하는 것입니다. 커스텀 view에 빨간색 버튼을 추가하는 간단한 작업부터 시작해 보겠습니다.

To create a button in your view

  1. init(coder:) initializer 메서드 구현부에 빨간 버튼을 만들기 위해 아래의 코드를 작성하세요

    1. let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
    2. button.backgroundColor = UIColor.redColor()

    redColor()를 사용하여 view가 어디 있는지 잘 보이게 했습니다. 물론 blueColor()나 greenColor()같이 UIColor에 정의되어 있는 다른 값을 사용하셔도 됩니다.

  2. 다음 줄에 아래 코드를 추가하세요

    1. addSubview(button)

    addSubview() 메서드는 우리가 만든 버튼을 RatingControl 뷰에 추가해줍니다. (즉, 파라미터인 button객체를  RatingControl 뷰의 서브뷰로 추가하는 것입니다.)

완성된 init(coder:)메서드의 모습은 아래와 같습니다.

  1. required init(coder aDecoder: NSCoder!) {
  2. super.init(coder: aDecoder)
  3. let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
  4. button.backgroundColor = UIColor.redColor()
  5. addSubview(button)
  6. }

Checkpoint: 시뮬레이서에서 앱을 실행시켜보세요. 우리가 initializer 메서드에서 RatingControl뷰에 서브뷰로 추가한 빨간색 사각형 버튼을 볼 수 있습니다.

image

이제 view 상에 버튼을 몇개 더 추가하고, 사용자가 탭하면 음식에 대한 평가를 할 수 있도록 액션을 추가해야합니다. 

To add an action to the button

  1. RatingControl.swift 파일의 맨 마지막에( 맨 마지막 괄호( } ) 바로 앞) 다음 주석을 추가하세요

    1. // MARK: Button Action
  2. rabs주석 다음에 아래와 같은 액션메서드를 작성해주세요

    1. func ratingButtonTapped(button: UIButton) {
    2. print("Button pressed 👍")
    3. }

    메서드가 버튼에 연결되어있는지를 체크해볼 수 있습니다. print() 함수(function)은 standard output(Xcode에서는 editor area의 아래에 있는 debug console)에 메시지를 출력하며, 디버깅에 유용하게 사용할 수 있습니다. 

    나중에 실제로 앱상에서 동작이 구현되게 수정하겠습니다.

  3. init(coder:) initializer를 찾으세요

    1. required init(coder aDecoder: NSCoder!) {
    2. super.init(coder: aDecoder)
    3. let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
    4. button.backgroundColor = UIColor.redColor()
    5. addSubview(button)
    6. }
  4. addSubView(button) 바로 위에 다음의 코드를 작성하세요

    1. button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)

    이전에 스토리보드에 있는 엘리먼트를 코드의 액션메서드로 연결하는 작업을 해봤기 때문에, "target-action"패턴에 대해서는 익숙하실겁니다. 이번에도 같은 작업을 한것입니다. 다만 코드 상에서 "엘리먼트와 액션메서드의 연결"을 구현한 점만 다를 뿐입니다. 위의 코드에서 button객체에 ratingButtonTapped: 액션메서드를 삽입하였습니다. button에 .TouchDown 이벤트가 발생하면 이 액션메서드가 호출될 것입니다. 이벤트가 발생했을 때 메시지를 받는 대상인 target은 당연히 액션메서드가 정의되어 있는 RatingControl클래스이므로 self로 지정하였습니다. 

    Interface Builder를 사용하지 않으므로 IBAction속성이 붙은 액션메서드를 정의할 필요가 없습니다. 그냥 다른 일반적인 메서드와 똑같이 선언하고 구현하면 됩니다.

완성된 init(_:coder) initializer의 모습은 아래와 같습니다.

  1. required init(coder aDecoder: NSCoder!) {
  2. super.init(coder: aDecoder)
  3. let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
  4. button.backgroundColor = UIColor.redColor()
  5. button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
  6. addSubview(button)
  7. }

Checkpoint: 시뮬레이터에서 앱을 실행시켜 테스트 해보세요. 빨간 사각형 버튼을 클릭하면, Xcode의 콘솔창에 "Button pressed" 메시지가 출력되는걸 확인할 수 있습니다.

image

Now it’s time to think about what pieces of information the RatingControl class needs to have in order to represent a rating. You’ll need to keep track of a rating value—0, 1, 2, 3, 4, or 5—as well as the buttons that a user taps to set that rating. You can represent the rating value with an Int, and the buttons as an array of UIButton objects. RatingControl클래스가 사용자들의 평가를 표시하기 위해 필요한 작업을 해야합니다. 사용자가 누른 버튼들을 확인하고, 그에 따라 부여할 실제 점수(0, 1, ,2, 3, 4, 5)를 파악해야합니다. 실제 점수는 Int타입의 값으로 정의하고, Button들은 UIButton객체 타입의 배열(array)로 정의하겠습니다.

To add rating properties

  1. RatingControl.swift 파일에서 클래스 선언부를 찾으세요

    1. class RatingControl: UIView {
  2. 바로 아래에 다음 코드를 추가하세요

    1. // MARK: Properties
    2. var rating = 0
    3. var ratingButtons = [UIButton]()

현재 우리가 만든 커스텀 뷰인 RatingControl에는 button이 한 개밖에 없지만, 실제로는 5개가 필요합니다. 5개의 버튼을 만들기 위해서는 for-in 반복문을 사용해보겠습니다. 

To create a total of five buttons

  1. RatingControl.swift파일에서 init(coder: ) initializer 를 찾으세요

    1. required init(coder aDecoder: NSCoder!) {
    2. super.init(coder: aDecoder)
    3. let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
    4. button.backgroundColor = UIColor.redColor()
    5. button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
    6. addSubview(button)
    7. }
  2. 마지막 4개의 라인을 for-in 반복구문으로 돌려주세요

    1. for _ in 0..<5 {
    2. let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
    3. button.backgroundColor = UIColor.redColor()
    4. button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
    5. addSubview(button)
    6. }

    for-in구문안의 내용이 적절하게 들여쓰기(indent)되었는지 확인하세요. 코드들을 모두 드래그하여 선택하고 control-I를 누르면 indent를 알맞게 재설정할 수 있습니다.

    "..<" (half-open range operator) 연산자는 우측에 위치한 값을 포함하지 않습니다. 따라서 이 구문에서는 0에서 4의 범위까지 총 5번 반복 수행됩니다. 결과적으로 5개의 버튼을 만들게 됩니다. for와 in 사이의 "_" 는 와일드카드를 나타냅니다. 별도로 값을 받는 변수가 필요 없을 때 사용하면 유용합니다.

  3. addSubview(button) 위에 아래의 코드를 추가하세요

    1. ratingButtons += [button]

    이렇게 하면 버튼이 하나 만들어질 때마다 ratingButtons배열에 순서대로 들어가게 됩니다. (*역자 : 위의 연산자가 작동하지 않는 경우, ratingButtons.append(button) 메서드를 이용하셔도 됩니다)

완성된 init(coder:) 메서드는 아래와 같습니다.

  1. required init(coder aDecoder: NSCoder!) {
  2. super.init(coder: aDecoder)
  3. for _ in 0..<5 {
  4. let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
  5. button.backgroundColor = UIColor.redColor()
  6. button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
  7. ratingButtons += [button]
  8. addSubview(button)
  9. }
  10. }

Checkpoint: 앱을 실행시켜보세요! 버튼이 하나밖에 보이지 않습니다. 사실 우리는 버튼들을 같은 자리에 차곡차곡 쌓아올린(stack) 것입니다. 버튼들이 view 상에 잘 배치되도록 조정을 해야합니다.

image

이런 종류의 레이아웃을 설계하는 코드들이 UIView클래스에 정의된 layoutSubviews라는 메서드에 내장되어 있습니다. 이 메서드는 시스템에 의해 특정 시점에 호출되며, UIView의 서브클래스들이 자신의 서브뷰들의 레이아웃을 정확하게 설계할 수 있는 기능을 제공합니다. 따라서 버튼들을 적절히 배치하기 위해서는 이 메서드를 오버라이드 해야합니다. 

To lay out the buttons

  1. init(coder:) 아래에 layoutSubviews() 메서드를 오버라디딩 하기 위한 코드를 작성해주세요

    1. override func layoutSubviews() {
    2. }

    메서드 선언부를 빠르게 작성할 수 있는 code completion 기능을 사용하시기 바랍니다.

  2. 메서드 구현부에는 아래 내용을 작성해주세요

    1. var buttonFrame = CGRect(x: 0, y: 0, width: 44, height: 44)
    2. // Offset each button's origin by the length of the button plus spacing.
    3. for (index, button) in ratingButtons.enumerate() {
    4. buttonFrame.origin.x = CGFloat(index * (44 + 5))
    5. button.frame = buttonFrame
    6. }

    이 코드는 for-in반복문을 통해 각 버튼에 대한 frame을 생성하여 설정합니다.

    enumerate()메서드는 ratingButtons 배열의 index와 value를 tuple형식의 collection으로로 반환합니다. 여기서는 배열 인덱스 값(0~4)과 그에 해당하는 button객체를 tuple 형식으로 반환합니다. 따라서 for-in 반복문에서 사용되는 지역변수 index와 button에는 배열 인덱스값과 button객체가 바인딩되어 사용됩니다. 

    반복문 구현부에서는 index 변수 값을 사용해 계산한 button frame에 대한 위치 값을 각 button 객체의 frame 값에 할당합니다. frame의 위치는 버튼 사이즈(44포인트)와 버튼 사이의 간격(5포인트 패딩)의 합에 index값을 곱한 값입니다. (*역자 : Swift2.0 아래 버전에서는 array.enumerate() 형태는 사용이 불가하다. enumerate()가 global함수로 정의도어 있는 관계로 enumerate(array)형태로 써주어야 작동한다. 따라서 for (index, button) in enumerate(ratingButtons)라고 해주어야 위의 코드는 작동된다)

Your layoutSubviews() method should look like this:

  1. override func layoutSubviews() {
  2. var buttonFrame = CGRect(x: 0, y: 0, width: 44, height: 44)
  3. // Offset each button's origin by the length of the button plus spacing.
  4. for (index, button) in ratingButtons.enumerate() {
  5. buttonFrame.origin.x = CGFloat(index * (44 + 5))
  6. button.frame = buttonFrame
  7. }
  8. }

Checkpoint: 다시 앱을 실행시켜보세요. 이번에는 버튼들이 일렬로 잘 표시 될 것입니다. 버튼을 클릭하면 마찬가지로 ratingButtonTapped(_:)메서드가 호출되고, 콘솔에 메시지가 출력될 것입니다.

image

console 창을 숨기려면, 툴바 우측에서 Debug 토글을 꺼주세요

image

Declare a Constant for the Button Size

코드상에 44라는 숫자를 여러 부분에 하드코딩하는 것은 좋지 않은 코딩 방식입니다. 수정이 필요할 때 코드의 여기저기에 흩어져있는 44를 모두 찾아서 일일이 수정해야하기 때문이지요. 이럴 때 "상수"(constant)를 사용하면 유용합니다. 수정이 필요할 때 상수 값만 수정해주면 상수가 사용된 모든 부분에 반영이 됩니다.

우리가 만든 컨테이너 뷰 역할을 하는 view의 height값에 button의 사이즈를 맞추겠습니다.

To declare a constant for the size of the buttons

  1. layoutSubview() 메서드 구현부의 맨 처음에 아래의 코드를 작성해주세요

    1. // Set the button's width and height to a square the size of the frame's height.
    2. let buttonSize = Int(frame.size.height)

    이렇게 buttonSize 상수를 만들어 사용하면 레이아웃을 더욱 유연하게 관리할 수 있습니다.

  2. 나머지 메서드에서 44로 하드코딩된 것을 모두 buttonSize상수로 변경해주세요

    1. var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
    2. // Offset each button's origin by the length of the button plus spacing.
    3. for (index, button) in ratingButtons.enumerate() {
    4. buttonFrame.origin.x = CGFloat(index * (buttonSize + 5))
    5. button.frame = buttonFrame
    6. }
  3. init(coder:) 이니셜라이저에서 for-in구문의 첫번째 라인을 다음과 같이 수정하세요

    1. let button = UIButton()

    layoutSubview()메서드에서 이미 button의 프레임을 설정했기 때문에, 더이상 button을 처음 만들 때 재설정할 필요가 없습니다.

현재까지 완성된 layoutSubviews()메서드의 모습은 아래와 같습니다.

  1. override func layoutSubviews() {
  2. // Set the button's width and height to a square the size of the frame's height.
  3. let buttonSize = Int(frame.size.height)
  4. var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
  5. // Offset each button's origin by the length of the button plus some spacing.
  6. for (index, button) in ratingButtons.enumerate() {
  7. buttonFrame.origin.x = CGFloat(index * (buttonSize + 5))
  8. button.frame = buttonFrame
  9. }
  10. }

init(coder:) 이니셜라이져의 모습도 아래와 같이 변경됩니다.

  1. required init(coder aDecoder: NSCoder!) {
  2. super.init(coder: aDecoder)
  3. for _ in 0..<5 {
  4. let button = UIButton()
  5. button.backgroundColor = UIColor.redColor()
  6. button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
  7. ratingButtons += [button]
  8. addSubview(button)
  9. }
  10. }

Checkpoint: 앱을 실행시켜보세요. 모든게 전처럼 잘 작동하는지 테스트해보세요. 버튼들은 일렬로 나열되어있어야 하고, 클릭할 때마다 ratingButtonTapped(_:)액션 메서드가 호출되어 콘솔에 메시지가 출력되어야 합니다.

image

Add Star Images to the Buttons

이제 색칠된 별과, 비어있는 별의 이미지를 버튼에 추가해보겠습니다.

image
image

레슨의 맨 밑에서 받을 수 있는 파일의 Image/폴더 내부에서 위의 이미지들을 찾을 수 있습니다. 다른 이미지를 사용하셔도 되지만, 이미지 파일 이름은 동일하게 변경해서 사용하는 것이 좋습니다.

To add images to your project

  1. project navigator에서 Images.xcassets를 선택해서 asset catalog를 열어주세요

    asset catalog는 앱에서 사용할 이미지들이 저장되어 관리되는 곳입니다.

  2. 왼쪽 하단에서 플러스(+)을 클릭하고, 팝업 메뉴에서 New Folder를 선택해주세요

    image
  3. 폴더 이름을 더블클릭해서 "Rating Images"라고 수정해주세요

  4. 해당 폴더가 선택된 상태에서 왼쪽 하단의 플러스(+)버튼을 눌러주세요. 팝업 메뉴가 나오면 New Image Set을 선택해주세요

    image set은 하나의 image asset만을 표시하고 있지만, 실제로 다양한 해상도의 화면에서 사용될 다양한 버전의 이미지를 포함할 수 있습니다.

  5. image set의 이름을 더블클릭하고, emtpy star로 변경해주세요

  6. 컴퓨터의 파인더에서 emptyStar.png파일을 선택해주세요

  7. 이미지를 드래그해서 image set의 2x 슬롯에 넣어주세요

    image

     2x는 iPhone6 시뮬레이션에 최적화된 해상도로 이미지를 노출합니다.

  8. 왼쪽 하단에서 또 플러스(+) 버튼을 누르고, 팝업메뉴에서 New Image Set을 선택해주세요

  9. image set의 이름을 더블클릭하여, filledStar로 변경해주세요

  10. 컴퓨터의 파인더에서 filledStar.png파일을 선택해주세요

  11. 드래그해서 image set의 2x 슬롯에 넣어주세요

    image

완성된 asset catalog의 모습은 아래와 같습니다.

image

이제 button에 상황에 맞게 이미지를 세팅하는 코드를 작성해봅시다.

To set star images for the buttons

  1. RatingControl.swift 파일을 열어주세요

  2. sinit(coder:) 이니셜라이저에서 for-in구문 바로 위에 아래의 코드를 작성해주세요

    1. let filledStarImage = UIImage(named: "filledStar")
    2. let emptyStarImage = UIImage(named: "emptyStar")
  3. In the for-in loop, after the line where the button is initialized, add this code:  for-in 반복문 안의 button초기화 구문 아래에 다음의 코드를 작성해주세요

    1. button.setImage(emptyStarImage, forState: .Normal)
    2. button.setImage(filledStarImage, forState: .Selected)
    3. button.setImage(filledStarImage, forState: [.Highlighted, .Selected])

    버튼의 각 상태에 따라 다른 이미지를 세팅하여, 버튼이 선택된 상태인지를 파악할 수 있게 하였습니다. emptyStar 이미지는 button이 선택되지 않은 상태(.Normal) 일때 표시됩니다. 반면 filledStar 이미지는 button이 선택된 상태(.Selected)이거나, 선택되어 하이라이트 된 상태(.Selected and .Highlighted)일때 표시됩니다.

  4. button의 backgroundColor를 빨간색으로 설정하는 코드를 삭제해주세요

    1. button.backgroundColor = UIColor.redColor()

    별 이미지를 버튼으로 사용하기 때문에 더이상 backgroundColor를 설정할 필요가 없습니다.

  5. Add this line of code: 대신 이 코드를 추가하세요

    1. button.adjustsImageWhenHighlighted = false

    이 코드는 버튼의 상태가 변화하면서 이미지에 별도의 하이라이트 효과가 나타나는 것을 막아줍니다.

완성된 init(coder:) 이니셜라이저의 모습은 아래와 같습니다.

  1. required init(coder aDecoder: NSCoder!) {
  2. super.init(coder: aDecoder)
  3. let emptyStarImage = UIImage(named: "emptyStar")
  4. let filledStarImage = UIImage(named: "filledStar")
  5. for _ in 0..<5 {
  6. let button = UIButton()
  7. button.setImage(emptyStarImage, forState: .Normal)
  8. button.setImage(filledStarImage, forState: .Selected)
  9. button.setImage(filledStarImage, forState: [.Highlighted, .Selected])
  10. button.adjustsImageWhenHighlighted = false
  11. button.addTarget(self, action: "ratingButtonTapped:", forControlEvents: .TouchDown)
  12. ratingButtons += [button]
  13. addSubview(button)
  14. }
  15. }

Checkpoint: 앱을 실행해보세요. 빨간 버튼 대신 별이 노출되는 것을 확인할 수 있습니다. 버튼을 누를때마다 ratingButtonTapped(_:) 액션 메서드가 호출되므로, 콘솔창에 메시지가 출력됩니다. 하지만 아직 버튼의 이미지가 변경되지는 않습니다. 이제 이 작업을 시작해봅시다.

image

Implement the Button Action

The user needs to be able to select a rating by tapping a star, so you’ll replace the debugging implementation with a real implementation of the ratingButtonTapped(_:) method. 별을 눌러서 평가를 할 수 있어야 합니다. 따라서 콘솔창에 메시지를 출력하는 대신, ratingButtonTapped(_:)메서드가 실제로 작동을 하도록 구현을 해야합니다.

To implement the rating action

  1. RatingControl.swift파일에서 ratingButtonTapped(_:)메서드를 찾으세요

    1. func ratingButtonTapped(button: UIButton) {
    2. print("Button pressed 👍")
    3. }
  2. 위의 print 구문을 아래의 코드로 변경하세요(*역자 : swift 2.0에서만 지원되며, 이하 버전이면 rating = find(ratingButtons, button) + 1 로 사용하셔야합니다.)

    1. rating = ratingButtons.indexOf(button)! + 1

    indexOf(_:)메서드는 파라미터로 들어온 button객체가 ratingButtons 배열의 몇 번째 index인지를 반환합니다. indexOf(_:) 메서드는 파라미터로 들어온 객체가 존재하지 않을 수도 있으므로 Optional 타입의 Int값을 반환합니다. 하지만 이 메서드 자체가 배열상에 할당되어있는 버튼이 눌려서 호출되는 것이므로, 항상 유효한 index를 반환한다고 확신할 수 있습니다. 이런 경우에 force unwrap operator( ! ) 를 붙여서 아직 알 수 없는 value에 접근이 가능합니다. index는 [0~4] 사이의 값을 갖으므로, 1을 더하여 [1~5]로 만들어 1점에서 5점까지 평가 점수를 만들어 rating변수에 할당합니다. 한마디로 버튼이 눌리면 rating 변수에 그 값을 할당하는 메서드입니다.

  3. RatingControl.swift 파일의 맨 마지막 괄호( } ) 바로 앞에 다음 메서드를 선언하세요

    1. func updateButtonSelectionStates() {
    2. }

    이 메서드는 버튼의 상태(눌렸는지 여부 등)을 업데이트(현행화)하는 역할을 하는 일종의 helper method입니다.

  4. updateButtonsSelectionStates() 메소드 구현부에 아래의 for-in 반복문을 작성해주세요

    1. for (index, button) in ratingButtons.enumerate() {
    2. // If the index of a button is less than the rating, that button should be selected.
    3. button.selected = index < rating
    4. }

    이 코드는 button 배열에 저장된 button들을 하나씩 꺼내서 index값이 rating 변수의 값보다 작은지 체크합니다. rating에는 눌려진 버튼의 index+1 값이 저장되어 있습니다. 만약 버튼의 index 값이 rating값보다 작다면 버튼의 selected 프로퍼티 값을 true로 설정합니다. 버튼의 상태가 selected가 되면, 위에서 설정한 것처럼 filledStar 이미지로 변경됩니다. 예를 들어 세 번째 버튼(인덱스2)을 누르면 rating 변수의 값은 3이 됩니다. 위의 for in 구문을 돌고나면 인덱스값이 3보다 작은 0, 1, 2 버튼의 Selected 값이 true로 설정되고, filledStar 이미지로 교체됩니다. 하지만 나머지(인덱스 3, 4) 버튼은 emptyStar 이미지로 유지 됩니다.

  5. ratingButtonTapped(_:) 메서드의 마지막 라인에 updateButtonSelectionStates() 메서드를 호출하는 코드를 추가하세요

    1. func ratingButtonTapped(button: UIButton) {
    2. rating = ratingButtons.indexOf(button)! + 1
    3. updateButtonSelectionStates()
    4. }
  6. layoutSubView() 메서드 구현부의 마지막 부분에도 updateButtonSelectionStates() 를 호출하는 코드를 추가해주세요

    1. override func layoutSubviews() {
    2. // Set the button's width and height to a square the size of the frame's height.
    3. let buttonSize = Int(frame.size.height)
    4. var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
    5. // Offset each button's origin by the length of the button plus some spacing.
    6. for (index, button) in ratingButtons.enumerate() {
    7. buttonFrame.origin.x = CGFloat(index * (buttonSize + 5))
    8. button.frame = buttonFrame
    9. }
    10. updateButtonSelectionStates()
    11. }

    rating변수의 값이 변할때 뿐만 아니라, View가 로드될때마다  button의 selected값을 반드시 update 해주어야 합니다.

  7. "// MARK: Properties" 섹션에서 rating 프로퍼티를 선언한 부분을 찾아 주세요

    1. var rating = 0
  8. rating 프러퍼티가 property observer를 갖도록 수정하세요

    1. var rating = 0 {
    2. didSet {
    3. setNeedsLayout()
    4. }
    5. }

    property observer는 자신의 값이 변화하는지를 계속해서 관찰하고 반응합니다. property observer는 자신의 값이 새로 설정될때마다 호출되기 때문에, 값이 바뀌기 직전이나 직후에 필요한 작업을 수행하기에 적합합니다. 구체적으로 didset 프로퍼티 옵저버는 프로퍼티의 값이 설정된 직후에 호출됩니다. 여기서는 rating의 값이 바뀔 때마다 setNeedsLayout()이 호출됩니다. 이 메서드는 프로퍼티의 값이 바뀔때마다 UI에 정확한 rating 프로퍼티의 값이 반영될 수 있도록 레이아웃을 업데이트합니다.

완성된 updateButtonSelectionStates() 메서드는 아래와 같습니다.

  1. func updateButtonSelectionStates() {
  2. for (index, button) in ratingButtons.enumerate() {
  3. // If the index of a button is less than the rating, that button shouldn't be selected.
  4. button.selected = index < rating
  5. }
  6. }

Checkpoint: 앱을 실행시켜보세요. 5개의 별 이미지가 보일 것입니다. 별을 누를 때마다 그 별까지 색이 채워집니다.

image

Add Properties for Spacing and Number of Stars

하드코딩한 값들을 모두 제거해서 별의 갯수와 별간의 간격도 모두 한곳에서 관리할 수 있도록 하겠습니다.

To make the rating control’s properties inspectable in Interface Builder

  1. RatingControl.swift 파일에서 "// MARK: Properties" 섹션을 찾으세요

    1. // MARK: Properties
    2. var rating = 0
    3. var ratingButtons = [UIButton]()

    editor area의 function menu를 이용하면 한번에 찾을 수 있습니다.

  2. 이미 선언된 프로퍼티 밑에 아래 코드를 추가하세요요

    1. var spacing = 5

    이 프로퍼티는 버튼간의 간격입니다.

  3. layoutSubview메서드에서 버튼 간의 간격을 의미하는 수치인 5를 spacing 프로퍼티로 교체하세요

    1. buttonFrame.origin.x = CGFloat(index * (buttonSize + spacing))
  4. spacing 프로퍼티 밑에 아래의 프로퍼티도 추가하세요

    1. var stars = 5

    이 프로퍼티는 control에 노출할 별의 갯수입니다.

  5. init(coder:)에서 별의 갯수인 5를 stars프로퍼티로 교체하세요

    1. for _ in 0..<stars {

Checkpoint: 앱을 실행해서 테스트해보세요. 모든것이 이전과 동일하게 작동합니다.

Connect the Rating Control to the View Controller

이제 마지막으로 할 일은 ViewController클래스가 RatingControl을 참조할 수 있게 하는 것입니다.

To connect a rating control outlet to ViewController.swift

  1. 스토리보드를 열어주세요

  2. 툴바의 우측에서 Assistant editor를 열어주세요

    image
  3. 작업공간이 부족하다면 툴바를 통해 project navigator와 utitlity area를 숨겨주세요

    image

    물론 outline view도 숨길 수 있습니다

  4. rating control을 선택해주세요

    ViewController.swift 파일이 오른쪽 에디터에 보일 것입니다.( 안 보인다면, editor selector bar에서 Automatic > ViewController.swift 를 선택해주세요 )

  5. rating control을 control-drag 하여 ViewController.swift 파일의 photoImageView 프로퍼티 아래로 연결해주세요

    image
  6. 다이얼로그가 나타나면 Name은 ratingControl로 설정해주세요. 

    나머지 옵션들을 아래 화면과 같이 그대로 두세요

    image
  7. Connect를 클릭하세요

이제 ViewController클래스는 스토리보드에 있는 rating control에 대한 참조체를 갖게 되었습니다.

Clean Up the Project

이제 meal scemne UI작업을 거의 다 완료했습니다. 다음 레슨 부터는 FoodTracker앱에 더 복잡한 기능과 UI를 구현할 것이기 때문에, 필요 없는 것들을 정리하는 작업이 필요합니다. UI밸런스를 맞추기 위해 stack view를 중앙으로 정렬하는 작업도 해보겠습니다.

To clean up the UI

  1. 툴바를 통해 Standard Editor로 돌아오세요

    image

    project navigator와 utility area를 열어주세요

  2. 스토리 보드를 열어주세요

  3. "Set Default Label Text" 버튼을 선택하고 "Delete"키를 눌러 삭제해주세요

    stack view는 버튼의 빈자리를 매우기 위해 UI엘리먼트들을 재배치할 것입니다.

    image
  4. outline view를 열고 Stack View를 선택하세요

    image
  5. Open the Attributes inspector. Attributes inspector를 열어주세요

  6. Attributes inspector에서 Alignment 필드에서 Center를 선택해주세요. stackView의 엘리먼트들이 수평으로 중앙정렬 될것입니다.

    image

 button에 연결되어있던 액션메서드도 삭제하겠습니다.

To clean up the code

  1. ViewController.swift 파일을 열어주세요

  2. ViewController.swift 파일에서 setDefaultLabelText(_:) 액션 메서드를 삭제하세요

    1. @IBAction func setDefaultLabelText(sender: UIButton) {
    2. mealNameLabel.text = "Default Text"
    3. }

    일단은 여기까지만 지우면 됩니다. 다음 레슨에서 label outlet의 내용을 바꾸는 작업도 진행하겠습니다.

Checkpoint: 앱을 실행해보세요. 버튼이 사라진걸 빼곤 모든게 동일할 것입니다. 아 엘리먼트들이 중앙정렬된 것도 다르겠군요. 별은 일렬로 배치되어 있을 것이고, 별을 누르면 ratingButtonTapped(_:) 액션 메서드가 호출될 것입니다.

image


댓글