Chapter 2: Getting Started
Version
Swift 5.3, iOS 14, Xcode 12
SwiftUI는 Apple이 2014년에 Swift를 처음 발표(announced)한 이후 가장 흥미로운 소식 중 하나이다. 이는 모든 사람이 코딩할 수 있게 하고자 하는 Apple의 목표를 향한 엄청난(enormous) 진전(step)이다. 기본 사항(basics)을 단순화(simplifies)하여, 사용자를 만족(delight)시키는 사용자 정의(custom) 기능(features)에 더 많은 시간을 할애할 수 있도록 한다.
이 책을 읽고 있다면, 이 새로운 프레임 워크(framework)를 사용한 앱을 개발에 흥분하고(excited) 있을 것이다. 이 장(chapter)에서는 SwiftUI 앱 개발의 기초(basics)와 Xcode에서 미리보기(previewing)를 실행하는 방법에 대해 설명한다. UIKit Apprentice 책의 유명한 BullsEye 앱에서 영감 받은 색상 매칭 게임(color-matching game)을 만들 것이다. 앱의 목표는 RGB 색상 공간(color space)에서 색상(color)을 선택하여 무작위로(randomly) 생성된(generated) 색상(color)과 일치(match)시키는 것이다:
이 장(chapter)에서는 다음을 수행한다:
- Xcode 캔버스(canvas)를 사용하여 코드와 함께 UI를 나란히(side-by-side) 생성하는 방법을 배우고, 한 쪽을 변경(change)하면 항상 다른 쪽도 업데이트(updates)되는 동기화(sync) 를 유지(stay)하는 방법을 확인한다.
- 이미지에 표시된 슬라이더(sliders)에 대한 재사용 가능한(reusable) 뷰(view)를 만든다.
@State
변수에 대해 알아보고, 상태 값(state value)이 변경(changes) 될 때마다 UI를 업데이트(update)하는 데 사용한다.- 사용자의 점수(score)를 보여주는 알림(alert)을 표시한다.
이제 시작할 시간이다.
Getting started
이 장(chapter)의 자료(materials)에서 UIKit/RGBullsEye 시작(starter) 프로젝트(project)를 열어 빌드하고 실행(build and run)한다:
이 앱(app)은 무작위로(randomly) 생성된(generated) 목표(target) 색상(color)의 빨간색(red), 초록색(green), 파란색(blue) 값(values)을 표시(displays)한다. 사용자는 슬라이더(sliders)를 움직여, 다른 뷰의 색상이 대상 색상과 일치(match)하도록 한다. 똑같은 작업을 하지만, 더 swiftly한 SwiftUI 앱(app)을 만드는 것이 목표이다.
Exploring the SwiftUI starter project
이 장(chapter)의 자료(materials)에서 SwiftUI/RGBullsEye 시작(starter) 프로젝트(project)를 연다.
프로젝트 네비게이터(project navigator)에서 RGBullsEye 그룹(group)을 열어, 여기에 무엇이 있는지 확인한다. 익숙한 AppDelegate.swift는 이제 RGBullsEyeApp.swift이다. 이 파일의 ContentView()
에서 앱(app)의 WindowGroup
을 생성한다:
@main
struct RGBullsEyeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@main
속성(attribute)은 이 구조체(struct)가 앱(app)의 진입점(entry point)을 포함하고 있음을 의미한다. App
프로토콜(protocol)은 실제로 실행(actually runs)되는 static main
함수(function)을 생성(generating)하는 작업을 처리한다. 앱(app)이 시작되면 ContentView.swift에 정의(defined)된 이 ContentView
인스턴스(instance)를 표시한다. 이는 View
프로토콜(protocol)을 준수(conforms)하는 struct이다:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
이것은 ContentView
의 body
에 Hello World를 표시하는 Text
뷰(view)가 포함(contains)되어 있음을 나타내는 SwiftUI 선언(declaring)이다. padding()
수정자(modifier)는 텍스트(text) 주위(around)에 10 포인트(points)의 패딩(padding)을 추가한다.
Model 그룹(group)에는 속성(properties)과 메서드(methods)가 있는 Game
구조체(struct)와 빨간색(red), 초록색(green), 파란색(blue) 값(values)을 래핑(wrap)하는 RGB
구조체(struct) 파일(files)을 포함(containing)하고 있다. Color
확장(extension)은 RGB
구조체(struct)에서 Color
뷰(view)를 만드는 사용자 정의 생성자(custom initializer)를 제공한다.
Previewing your ContentView
ContentView.swift의 ContentView
구조체(struct) 아래의 ContentView_Previews
는 ContentView
의 인스턴스(instance)를 포함(contains)하는 뷰(view)를 포함(contains)하고 있다.
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
여기에서 미리보기(preview)를 위한 샘플(sample) 데이터를 지정할 수 있으며, 다양한 화면(screen)과 글꼴(font) 크기(sizes)를 비교(compare)할 수 있다.
코드 옆의 큰 공백(blank space) 상단은 다음과 같다:
기본적(default)으로, 미리보기(preview)는 현재(currently) 활성화된(active) 구성(scheme)을 사용한다.
Resume를 클릭(click)하고 미리보기(preview)가 표시될 때까지 잠시 기다린다:
패딩 상자(padding box)를 표시하기 위해 텍스트(text)를 클릭(clicked)했다.
Note: Resume 버튼(button)이 표시되지 않는다면, 편집기 옵션(Editor Options) 버튼(button)을 클릭(click)하고 캔버스(Canvas)를 선택한다.
여전히 Resume 버튼(button)이 표시되지 않는다면, macOS Catalina(10.15) 이상에서 실행 중(running)인지 확인한다.
Note: Resume 버튼(button)을 클릭(clicking)하는 대신, 매우 유용한 키보드 단축키(shortcut)인 Option-Command-P를 사용할 수 있다. 뷰(view)에서 내용을 변경한 직후 Resume 버튼(button)이 표시되지 않는 경우에도 작동한다.
Creating your UI
SwiftUI 앱(app)에는 스토리 보드(storyboard)나 뷰 컨트롤러(view controller)가 없다. ContentView.swift가 작업을 대신한다. 코드와 객체 라이브러리에서 드래그(drag-from-object-library)를 조합(combination)하여 UI를 만들 수 있으며, 코드에서 직접(directly) 스토리 보드와 같은(storyboard-like) 작업을 수행할 수 있다. 무엇보다도(best of all) 모든 것이 항상 동기화(sync) 상태를 유지(stays)한다.
SwiftUI는 선언적(declarative)이다. 원하는 UI 모양을 선언(declare)하면, SwiftUI는 해당 선언(declarations)을 작업을 완료할 수 있는 효율적인(efficient) 코드로 변환(converts)한다. Apple은 코드를 읽기 쉽게(easy to read) 유지하기 위해 필요한 만큼 많은 뷰(views)를 생성 할 것을 권장(encourages)한다. 재사용 가능한(reusable) 매개 변수화된(parameterized) 뷰(views)가 특히 권장되며, 이는 코드를 함수(function)로 추출(extracting)하는 것과 같다. 이 장(chapter)의 뒷부분에서 확인해 볼 것이다.
이 장(chapter)에서는 인터페이스 빌더(Interface Builder, IB)에서 UI를 배치(layout)하는 방법과 유사한 캔버스(canvas)를 주로 사용한다.
Some SwiftUI vocabulary
뷰(views)를 생성하기 전에, 먼저 몇 가지 용어(vocabulary)를 알아둬야 한다.
- Canvas and Minimap: 완전한 SwiftUI를 경험(experience)하려면, 최소 Xcode 11와 macOS 10.15이 필요하다. 그래야 코드 편집기(code editor)와 함께 캔버스(canvas)에서 앱(app) 뷰(views)를 미리 볼(preview) 수 있다. 또한, 코드 미니맵(minimap)도 사용할 수 있다. 위의 스크린샷(screenshots)에서는 숨겼기 때문에 확인할 수 없다: Editor ▸ Hide Minimap
- Modifiers: UIKit 객체(objects)의 속성(properties)을 설정하는 대신, 전경색(foreground color), 글꼴(font), 패딩(padding) 등에 대한 수정자 메서드(modifier methods)를 호출(call)할 수 있다.
- Container views: 이전에(previously) 스택 뷰(stack views)를 사용한 적이 있다면,
HStack
및VStack
컨테이너 뷰(container views)를 사용하여 SwiftUI로 이 앱(app)의 UI를 만드는 것이 매우 쉬울 것이다.ZStack
과Group
등의 다른 컨테이너 뷰(container views)도 있다. 7장(chapter), "Introducing Stacks & Containers"에서 이에 대해 배우게 될 것이다.
컨테이너 뷰(container views) 외에도, Text
, Button
, Slider
와 같은 다양한 UIKit 객체(objects)에 대한 SwiftUI 뷰(views)가 있다. 툴바(toolbar)의 + 버튼(button)을 사용하여 SwiftUI 뷰(views)와 수정자(modifiers), 미디어(media) 및 코드 스니펫(code snippets) 라이브러리(Library)를 표시할 수 있다.
Creating the target color view
RGBullsEye에서 사용자가 일치(match)시키려는 색상(color)인 대상(target) 색상(color) 뷰(view)는 Text
뷰(view) 위(above)에 위치한 Color
뷰(view)이다. 그러나 body
는 단일 View
를 반환(returns)하는 computed 속성(property)이므로, 컨테이너 뷰(container view)에 포함(embed)시켜야 한다. 여기에서는 VStack
(vertical stack, 수직 스택)을 사용할 것이다.
작업 흐름(workflow)은 다음과 같다:
VStack
에Text
뷰(view)를 포함(embed)하고, 텍스트(text)를 편집(edit)한다.- 스택(stack)에
Color
뷰(view)를 추가한다.
1 단계: 캔버스(canvas)에서 Hello World Text
뷰(view)를 Command-click 한다. Xcode가 코드를 강조 표시(highlights)한다. 그러면 Embed in VStack을 선택한다.
Note: Command-click이 VStack
의 정의(definition)로 이동(jumps)하는 경우, 대신 Control-Command-click을 사용한다. Xcode 환경 설정(preferences)에 따라 설정(setting)이 다를 수 있다.
캔버스(canvas)에서는 똑같이 보이지만, 코드에서는 VStack
이 추가된다.
"Hello World"
를 "R: ??? G: ??? B: ???"
로 변경한다. 코드에서 직접(directly) 수행할 수 있지만, 다른 방법도 있다. 캔버스(canvas)에서 Text
뷰(view)를 Control-Option-click 하고 SwiftUI inspector를 선택한다:
그런 다음 관리자(inspector)에서 텍스트(text)를 편집(edit)한다:
코드가 이와 일치하도록 업데이트(updates) 된다. 이번에는 코드의 텍스트(text)를 임의로 변경하고, 캔버스(canvas)에서도 변경되는지 확인해 본다. 매우 효율적으로 동작하는 것을 알 수 있다. 확인이 끝났다면, 텍스트(text)를 이전으로 다시 변경(change)한다.
2 단계: 툴바(toolbar)에서 + 버튼(button)을 클릭(click)하여 Library를 연다. 선택한 라이브러리(library)가 Views인지 확인한 다음 color를 검색(search)한다. 이 객체(object)를 캔버스(canvas)의 Text
뷰(view)로 끌어온다(drag). 드래그하는 동안(dragging), Add Color to a new Vertical Stack...가 아닌 Insert Color in Vertical Stack가 표시될 때까지 커서(cursor)를 아래로 이동한다. 텍스트(text) 위에 삽입(insert)하려면, 커서(cursor)를 Text
뷰(view)의 상단 근처(near)에 둔다. 그런 다음 Color
객체(object)를 놓는다(release).
캔버스(canvas)와 코드(code) 모두에서 VStack
내부(inside)에 Color
뷰(view)가 위치하게 된다.
0.5 값(values)은 자리 표시자(placeholders)일 뿐이므로 강조 표시(highlighted)된다. 지금은 각각을 선택한 다음 Enter를 누르면 된다.
Note: IB에서는 여러(several) 객체(objects)를 뷰(view)로 드래그(drag)한 다음, 모두 선택하여 스택 뷰(stack view)에 포함(embed)할 수 있다. 그러나 SwiftUI의 Embed 명령(command)은 단일 객체(object)에 대해서만 작동한다.
Creating the guess color view
추측(guess) 색상(color) 뷰(view)는 대상(target) 색상(color) 뷰(view)와 매우 비슷하지만, 텍스트(text)가 다르다. 대상(target) 색상(color) 뷰(view) 아래에 위치하므로, VStack
에 추가하기만 하면 된다.
코드 편집기(code editor)에서 padding()
을 포함(including)한 Color
및 Text
코드를 복사(copy)하여, padding()
행(line) 아래에 붙여 넣는다.
두 번째 Text
뷰(view)의 문자열(string)을 "R: 204 G: 76 B: 178"
로 변경(change)한다. 이 샘플 값(sample values)은 밝은 자홍색(bright fuchsia color)을 만든다.
이제 VStack
은 다음과 같다:
VStack {
Color(red: 0.5, green: 0.5, blue: 0.5)
Text("R: ??? G: ??? B: ???")
.padding()
Color(red: 0.5, green: 0.5, blue: 0.5)
Text("R: 204 G: 76 B: 178")
.padding()
}
Creating the button and slider
색상 슬라이더(color sliders)와 Hit me! 버튼(button)이 색상 블록(color blocks) 아래에 위치하므로, 다시 VStack
에 추가하기만 하면 된다.
이전에는 Color
뷰(view)를 캔버스(canvas)로 드래그(dragged)했다. 이번에는 Slider
와 Button
뷰(views)를 코드로 드래그(drag)한다.
Note: 라이브러리(Library)를 열어 두려면, + 버튼을 Option-click 한다.
라이브러리(library)를 열고 Button
을 코드 편집기(code editor)로 드래그(drag)한다. 객체(object)를 놓을(drop) 수 있는 새 행(line)이 열릴 때까지, 두 번째 padding
행(line) 약간 아래로 마우스를 가져간다(hover).
Option-Command-P를 누르거나 Resume를 클릭(click)하여 버튼(button)을 확인한다:
이제 버튼(button)의 위치가 VStack
하단(bottom) 가장자리(edge)로 명확(clear)해 졌으므로, 캔버스(canvas)에서 Button
바로 아래에 라이브러리(Library)의 Slider를 드래그(drag)할 수 있다:
코드 편집기(code editor)에서 Slider value
를 .constant(0.5)로 설정한다. 바인딩(Bindings) 섹션(section)에서 단순히 0.5가 아닌 이유를 알게 될 것이다.
Button Content
를 Text("Hit Me!")
로 설정하고 Action
을 {}
로 설정한다.
다음과 같이 표시된다:
Note: 슬라이더(slider)의 버튼(thumb)이 중앙(centered)에 있지 않으면, 변경 사항이 적용될 때까지 미리보기(preview)를 새로 고친다(Option-Command-P).
세 개의 슬라이더(sliders)가 필요하지만 슬라이더(slider)의 값(values)은 UI를 업데이트(update)하며, 이는 다음 섹션(section)의 주제(topic)이다. 먼저 빨간색(red) 슬라이더(slider)를 설정한 다음, 매개 변수(parameters)가 있는 재사용 가능(reusable)한 하위 뷰(subview)로 추출(extract)하여 세 개의 슬라이더(sliders)를 모두 만들 수 있다.
Updating the UI
SwiftUI에서 뷰(view) 속성(property)의 값(value)이 변경(changes)될 때 UI가 업데이트(update)되어야 하는 경우, @State
속성(property)으로 지정(designate)한다. SwiftUI는 @State
속성(property)의 값(value)이 변경(changes)되면, 뷰(view)의 모습(appearance)을 무효화(invalidates)하고 body
를 다시 계산(recomputes)한다. 이를 확인하려면(to see this in action), 추측 색상(guess color)에 영향(affect)을 주는 속성(properties)이 @State
속성(properties)인지 확인(ensure)해야 한다.
Using @State properties
다음 속성(properties)을 struct ContentView
상단, body
속성(property) 위에 추가한다:
@State var game = Game()
@State var guess: RGB
RGBullsEye 게임을 표시하고 실행(run)하는 데 필요한 속성(properties) 및 메서드(methods)에 접근(access)하기 위해 Game
객체(object)를 만든다. 이러한 속성(properties) 중 하나는 target
RGB
객체(object)이다:
var target = RGB.random()
game
을 생성하면 target
의 빨강색, 초록색, 파란색 값(values)이 0에서 1 사이의 임의 값(random values)으로 초기화(initializes)된다.
슬라이더(slider) 값(values)을 저장(store)하려면 지역(local) RGB 객체(object)인 guess
도 필요하다.
guess
를 RGB()
로 초기화(initialize)하면 red
, green
, blue
가 0.5(회색)로 초기화(initializes)된다. 초기화하지 않은 경우 수행해야 하는 작업을 보여주기 위해 초기화되지 않은(uninitialized) 상태로 두었다.
미리보기(preview)에 표시할 ContentView
를 인스턴스화(instantiates)하는 ContentView_Previews
구조체(struct)로 스크롤(scroll down)한다. 생성자(initializer)는 guess
에 대한 매개 변수(parameter) 값(values)이 필요하다. ContentView()
를 다음과 같이 변경(change)한다:
ContentView(guess: RGB(red: 0.8, green: 0.3, blue: 0.7))
이러한 값(values)은 미리보기(preview)에서 자홍색(fuchsia color)으로 표시된다.
또한, RGBullsEyeApp.swift의 ContentView()
생성자(initializer)도 교체(replace)해야 한다. 이번에는 기본 생성자(default initializer)를 사용한다:
ContentView(guess: RGB())
앱(app)이 초기 장면(initial scene)을 불러오면(loads), 슬라이더(sliders)의 버튼(thumbs)이 중앙에 위치한다. 추측 색상(guess color)은 회색으로 시작한다.
Updating the Color views
ContentView.swift로 돌아와 Text("R: ??? G: ??? B: ???")
위의 Color
뷰를 수정하여 game
객체의 target
속성(property)을 사용한다.
Color(rgbStruct: game.target)
ColorExtension.swift에 정의(defined)된 RGB
구조체(struct) 생성자(initializer)를 사용해, target
색상 값(color values)으로 Color
뷰(view)를 만든다.
Option-Command-P를 눌러 임의의(random) 대상 색상(target color)을 확인한다.
Note: 미리보기(preview)는 Resume 또는 실시간 미리보기 버튼(live preview button)을 클릭(click)할 때뿐만 아니라, 주기적으로(periodically) 새로 고쳐(refreshes)지므로(자세한 내용은 곧 설명한다), 대상 색상(target color)이 자주 변경되는 것을 보고 놀랄 필요는 없다.
마찬가지로(similarly), guess
값(values)을 사용하도록 추측(guess) Color
를 수정(modify)한다:
Color(rgbStruct: guess)
미리보기(preview)를 새로 고쳐(refresh), ContentView
미리보기(preview)에서 설정한 자홍색(fuchsia color)을 확인한다:
추측(guess) Text
뷰(view)의 R, G, B 값(values)은 색상(color)과 일치(match)하지만, 곧 사용자(user)가 설정(set)한 슬라이더 값(slider values)에 대응(respond to)하게 된다.
Making reusable views
슬라이더(sliders)가 기본적으로(basically) 동일(identical)하기 때문에, 하나의 슬라이더(slider) 뷰(view)를 정의(define)한 다음, 다른 두 슬라이더(sliders)에 재사용(reuse)할 수 있다. 이것은 정확히(exactly) Apple이 권장(recommends)하는 대로이다.
Making the red slider
먼저 재사용(reuse)에 대해 생각하지 않고, 빨간색(red) 슬라이더(slider)를 만든다. Slider
의 양쪽 끝에 Text
뷰(view)를 사용하여 사용자(users)에게 최소값(minimum)과 최대값(maximum)을 알려야 한다. 이 수평(horizontal) 레이아웃(layout)을 구현하려면 HStack
이 필요하다.
Slider
뷰(view)를 Command-click하고 Embed in HStack을 선택한 다음, 위와 아래(코드에서) 또는 왼쪽과 오른쪽 (캔버스에서)에 Text
뷰(views)를 삽입한다. Placeholder
텍스트(text)를 0
과 255
로 변경(change)한 다음, 미리보기(preview)를 업데이트(update)하여 어떻게 표시되는지 확인한다:
Note: 슬라이더(slider)가 0에서 1 사이 값을 가진다는 것을 알고 있지만, 0-255 RGB 값은 16 진수(hexadecimal)로 색상(colors)을 표현하는 것과 같이 0에서 255 사이의 RGB 값을 사용자(users)가 더 수월하게(comfortable) 생각할 수 있도록 한다.
숫자가 비좁아(cramped) 보이므로 이를 수정(fix)하고, 빨간색 슬라이더(slider)처럼 외관을 바꿔(fix)준다.
먼저, HStack
을 Control-Option-click(코드 편집기에서 이 작업을 수행하는 것이 더 쉬울 수 있다)하여 속성 관리자(attributes inspector)를 연다. Padding 섹션(section)에서 왼쪽 및 오른쪽 확인란(checkboxes)을 클릭(click)한다.
왼쪽 또는 오른쪽 확인란(checkbox)을 클릭(clicking)하면, padding(.leading)
또는 padding(.trailing)
수정자(modifier)가 HStack
에 추가된다. 그런 다음(then) 다른 확인란(checkbox)을 클릭(click)하면, padding
값(value)이 .horizontal
으로 변경(changes)된다. 이제 화면 가장자리(screen edges)와 슬라이더 레이블들(slider labels) 사이에 공간(space)이 있다.
Note: 뷰(view) 전체에 패딩(padding)을 추가하는 가장 빠른 방법은 코드 편집기(code editor)에서 .padding()
을 입력하는 것이다. 속성 관리자(attributes inspector)는 일부 가장자리(some edges)에만 패딩(padding)을 설정하려는 경우에 유용(useful)하다.
다음으로 Slider
값을 편집(edit)하고 수정자(modifier)를 추가한다:
Slider(value: $guess.red)
.accentColor(.red)
수정자(modifier)는 슬라이더(slider)의 minimumTrackTintColor
를 빨간색으로 설정(sets)한다.
$guess
가 무엇을 의미하는지는 곧(real soon) 알게 된다. 먼저 작동하는지 확인(check)한다.
미리보기(preview) 코드에서 red
값(value)을 0.3과 같이 0.8이 아닌 다른 값으로 변경(change)한 다음, Option-Command-P를 누른다:
guess.red
는 0.3이고 슬라이더(slider)의 버튼(thumb)은 예상한(expect) 위치에 있다. 그리고 선행 경로(leading track)는 빨간색이며, 숫자 레이블(number labels)은 가장자리(edges)가 뭉개지지(squashed) 않는다.
Bindings
$
로 돌아온다. 간단한 표현(symbol)이지만 꽤 멋지고(pretty cool) 강력(ultra-powerful)하다. guess.red
자체는 단지 읽기 전용(read-only)인 값(value)이다. $guess.red
는 읽기-쓰기(read-write) 바인딩(binding)이다. 사용자(user)가 슬라이더(slider) 값(value)을 변경(changing)하는 동안, 추측 색상(guess color)을 업데이트(update)하려면 이것이 필요하다.
차이를 확인하려면, 추측(guess) Color
뷰(view) 아래의 Text
뷰(view)에서 값(values)을 설정한다. Text("R: 204 G: 76 B: 178")
를 다음과 같이 변경(change)한다:
Text(
"R: \(Int(guess.red * 255.0))"
+ " G: \(Int(guess.green * 255.0))"
+ " B: \(Int(guess.blue * 255.0))")
여기에서는 추측 값(guess value)을 변경하지 않고(read-only), 사용하기 때문에 $
접두사(prefix)가 필요하지 않다.
이 문자열(string)은 RGB
객체(object)의 색상 값(color values)을 0에서 255 사이의 정수(integers)로 표시한다. RGB
구조체(struct)에는 이에 대한 메서드(method)가 포함(includes)되어 있다. 다중 (multi-line)행 Text
코드를 다음으로 바꾼다(replace):
Text(guess.intString())
Option-Command-P를 누른다(press):
그리고 이제 R 값(value)은 76이다. 이는 255 * 0.3
이다.
Extracting subviews
다음으로이 섹션(section)의 목적(purpose)은 빨간색 슬라이더(slider) HStack
에서 재사용 가능한(reusable) 뷰(view)를 만드는 것이다. 재사용(reusable)할 수 있으려면, 뷰(view)에 몇 가지 매개 변수(parameters)가 필요하다. 복사-붙여 넣기-편집(Copy-Paste-Edit)으로 이 HStack
를 초록색 슬라이더(slider)로 만들려면, $guess.red
를 $guess.green
으로, .red
를 .green
로 변경한다. 이것이 여기서의 매개 변수(parameters)이다.
빨간색 슬라이더(slider) HStack
을 Command-click하고 Extract Subview를 선택(select)한다:
이것은 Refactor ▸ Extract to Function과 동일하지만, SwiftUI 뷰(views)에 대해 작동한다.
추출된(extracted) 뷰(view)의 이름을 ColorSlider로 지정한다.
Note: 메뉴(menu)에서 Extract Subview를 선택한 직후, ExtractedSubview가 강조 표시(highlighted)된다. 강조 표시된(highlighted) 상태에서 이름을 바꾸면(rename), 새 이름이 추출한(extracted) 위치와 파일 하단의 추출된(extracted) 하위 뷰(subview) 두 곳에서 나타난다(appears). 이때 이름을 바꾸지 않으면, 이 두 위치에서 추출된(extracted) 하위 뷰(subview)의 이름을 수동으로(manually) 변경해야 한다.
나타나는 모든 오류(error) 메시지(messages)에 대해 걱정할 필요 없다. 새 하위 뷰(subview) 편집(editing)을 마치면 사라진다.
이제 struct ColorSlider
상단의 body
속성(property) 앞에 다음 속성(properties)을 추가한다:
@Binding var value: Double
var trackColor: Color
value
속성(property)의 경우, ColorSlider
뷰(view)가 이 데이터를 소유하지 않기 때문에, @State
대신 @Binding
을 사용한다. ColorSlider
는 부모 뷰(parent view)에서 초기 값(initial value)을 받아 변경(mutates)하기 때문이다.
이제 $guess.red
를 $value
로, .red
를 trackColor
로 바꾼다(replace):
Slider(value: $value)
.accentColor(trackColor)
그런 다음 VStack
의 ColorSlider()
호출(call)로 돌아간다. 누락된 인수 오류(Missing arguments error) 아이콘(icon)을 클릭(click)하여 열고 Fix 버튼(button)를 눌러 누락된 인수(missing arguments)를 추가한다. 다음 매개 변수(parameter) 값(values)을 입력한다:
ColorSlider(value: $guess.red, trackColor: .red)
미리보기(preview)에서 빨간색 슬라이더(slider)가 여전히 올바르게(correctly) 표시되는지 확인한 다음, 이 행(line)을 Copy-Paste-Edit 하여 다른 두 슬라이더(sliders)를 만든다:
ColorSlider(value: $gGuess, textColor: .green)
ColorSlider(value: $bGuess, textColor: .blue)
미리보기(preview)를 새로 고쳐(refresh), 세 개의 슬라이더(sliders)를 확인한다:
모든 것이 제대로 작동한다. 게임을 바로 실행해 보고 싶을 것이다.
먼저, previews
의 guess
매개변수(parameter)를 RGB()
로 설정(set)한다:
ContentView(guess: RGB())
Live Preview
게임을 실행(play)하기 위해, 시뮬레이터(Simulator)를 실행(fire up)할 필요는 없다. Preview 도구 모음(toolbar)에서 Live Preview 버튼(button)을 클릭(click)한다:
Preview spinner가 중지(stop)될 때까지 기다린다. 필요한 경우 Try Again을 클릭(click)한다.
이제 색상(color)과 일치(match)하도록 슬라이더(sliders)를 움직인다.
UIKit 앱(app)의 작동 방식과 비교하여(compared with), 여기서 무슨 일이 일어나고 있는지 생각해 본다. SwiftUI 뷰(views)는 슬라이더(slider) 값(values)이 변경될 때마다(whenever) 자동으로 업데이트(update)된다. UIKit 앱(app)은 이 모든 코드를 슬라이더(slider)의 action에 넣는다. 모든 State
변수(variable)는 단일 진실 공급원(source of truth)이며, 뷰(views)는 일련(sequence)의 이벤트(events)가 아닌 상태(state)에 의존(depend on)한다.
Presenting an alert
슬라이더(sliders)를 사용하여 색상을 잘 맞춘(match) 후, 사용자는 원래(original) UIKit 게임과 마찬가지로 Hit Me! 버튼(button)을 탭(taps)한다. 그리고 원본(original)과 마찬가지로, Alert
이 나타나 점수(score)를 표시해야 한다.
RGB
구조체(struct)에는 guess
과 target
RGB
객체(objects)의 차이(difference)를 계산(compute)하는 difference(target:)
메서드(method)가 있고, Game
구조체(struct)에는 점수(score)를 계산(compute)하기 위해 difference(target:)
을 사용하는 check(guess:)
메서드(method)가 있다.
Button
뷰(view)의 action
에서 check(guess:)
를 호출(call)한다:
Button(action: {}) {
Text("Hit Me!")
}
Button
은 UIButton
과 마찬가지로 action과 label이 있다. 수행하려는 작업(action)은 Alert
뷰(view)의 표시(presentation)이다. 그러나 Button
의 action에서 Alert
를 생성하면 아무 것도 수행되지 않는다.
대신 Alert
을 ContentView
의 하위 뷰(subviews) 중 하나로 생성하고, Bool 유형(type)의 State
속성(property)을 추가한다. 그런 다음 Alert
을 표시(appear)하려할 때, 이 속성(property)의 값을 true
로 설정한다. 이 경우에는 Button
의 action이 된다. 사용자(user)가 Alert
을 해제(dismisses)하면, 값(value)이 false
로 변경(changes)되므로 Alert
이 사라진다(disappears).
따라서 State
속성(property)을 추가하고, false
로 초기화(initialized)한다:
@State var showScore = false
그런 다음 Button
을 다시 작성하여 action
코드를 추가한다:
Button("Hit Me!") {
showScore = true
game.check(guess: guess)
}
Button
을 구성(configure)하는 방법에는 여러 가지가 있다(turns out). 레이블(label)은 일반적으로 Image
뷰(view)와 Text
뷰(view)를 포함(containing)하는 단일 객체(object) 또는 클로저(closure)일 수 있다. action은 함수(function) 호출(call) 또는 클로저(closure)일 수 있다. 레이블(label) 또는 action이 단일 명령문(single statement)인 경우, 괄호(parentheses) 안에 넣을 수 있다. 다른 매개 변수(parameter)는 후행 클로저(trailing closure)일 수 있다.
이 경우(in this case), 레이블(label)은 String
일 뿐이므로, 레이블(label)과 action의 위치(positions)를 바꾸어(swap) action을 후행 클로저(trailing closure)로 만든다.
마지막으로(finally), 닫는 중괄호(closing curly brace) 뒤에 다음 alert
수정자(modifier)를 Button
에 추가한다:
.alert(isPresented: $showScore) {
Alert(
title: Text("Your Score"),
message: Text(String(game.scoreRound)),
dismissButton: .default(Text("OK")) {
game.startNewRound()
guess = RGB()
})
}
사용자(user)가 알림(alert)을 해제(dismisses)할 때, 값(value)이 변경되고, 이 변경된(changed) 값(value)이 UI를 변경(change)하므로 $showScore
바인딩(binding)을 전달(pass)한다. 그러면 알림(alert) 표시(presenting)가 중지된다.
Button
action이 game.check(guess:)
를 호출(calls)하면, 이 메서드(method)는 이번 라운드(round)의 점수(score)를 계산(computes)한다. 이 숫자로 String
을 만들어 알림(alert) message
에 표시(display)한다.
가장 간단한 Alert
생성자(initializer)에는 레이블(label)이 "OK"인 기본 닫기 버튼(default dismiss button)이 있으므로, action을 구성(configure)하려는 경우에만 dismissButton
매개 변수(parameter)를 포함(include)하면 된다. 이 경우(in this case) 새 라운드(round)를 시작하여 새로운 target
색상(color)을 설정(sets)한다. 그런 다음 guess
색상(color)을 회색으로 재설정(reset)한다.
Showing conditional views
구현(implement)해야 할 마지막 기능(functionality)이 있다. showScore
가 true
이면 대상 색상(target color) 레이블(label)이 올바른 색상 값(correct color values)을 표시(display)해야 사용자(user)가 이를 슬라이더 값(slider values)과 비교(compare)할 수 있다.
다음 행(line)의 Text
를 Command-click한다.
Text("R: ??? G: ??? B: ???")
Make Conditional을 선택(select)한다:
Note: SwiftUI에는 중첩된 클로저(nested closures)가 많고, Xcode는 중괄호(braces)를 순서대로(in order) 유지하는 데 도움을 준다. 둘 이상의 코드 행을 클로저(closure)로 묶어야(enclose) 하는 경우, 다른 행(lines)을 선택하고 Option-Command-[ 또는 Option-Command-]를 눌러 위나 아래로 이동한다. 이러한 키보드 단축키(keyboard shortcuts)는 SwiftUI에서 매우(tremendously) 유용(useful)히다. 해당 목록은 Xcode 메뉴(menu)의 Editor ▸ Structure에서 확인할 수 있다.
이제 if-else
를 다음과 같이 편집(edit)한다:
if !showScore {
Text("R: ??? G: ??? B: ???")
.padding()
} else {
Text(game.target.intString())
.padding()
}
사용자(user)가 버튼(button)을 눌러(taps) 알림(alert)을 표시하면, 대상 색상 레이블(target color label)에 실제 색상 값(actual color values)이 표시된다.
실시간 미리보기(live preview)를 새로 고침(refresh)한다. 실시간 미리보기(live preview)를 끄고(turn off) Resume을 클릭(click)한 다음 실시간 미리보기(live preview)를 켜야(turn on) 할 수 있다. 얼마나 높은 점수(score)를 얻을 수 있는지 확인한다:
실시간 미리보기(live preview)를 사용하여, 시뮬레이터(Simulator)를 대체할 수 있다.
Note: 자체 앱(apps)을 개발(develop)할 때, 미리보기(preview)가 항상 이렇게 잘 작동하지 않는다는 것을 알게 된다. 이상(odd)하게 보이거나, 충돌(crashes)이 발생하면 시뮬레이터(simulator)에서 실행(running)해 본다. 그래도 작동하지 않는다면, 기기(device)에서 실행(run)해 본다.
Making it prettier
앱(app)이 모든 기능(functionality)을 갖추고 있으므로, 이제 모습(looks)을 개선(improving)할 수 있다. 직사각형(rectangles) 대신(instead of) 원(circles)을 사용해 본다.
대상(target) Color
뷰(view)를 다음과 같이 색이 있는(colored) Circle
로 바꾼다(replace):
Circle()
.fill(Color(rgbStruct: game.target))
그리고 추측(guess) Color
뷰(view)도 마찬가지로 수정한다:
Circle()
.fill(Color(rgbStruct: guess))
원(circles)을 확인하려면, 미리보기(preview)를 새로 고친다(refresh):
다음 장(chapter)에서는 이러한 원(circles)을 더 다양하게 사용자 정의(customize)하므로, 다른 하위 뷰(subview)로 추출(extract)하는 것이 좋다.
Challenge
Challenge: Create a ColorCircle subview
Circle().fill...
행(lines)을 다음과 같이 바꿀 수 있도록 ColorCircle
하위 뷰(subview)를 생성한다:
ColorCircle(rgb: game.target)
ColorCircle(rgb: guess)
ColorCircle 구조체(struct)에는 바인딩(bindings)이 필요하지 않다.
해답(solution)은 이 장(chapter)의 challenge/final 폴더(folder)에 있다.
Key points
- Xcode 캔버스(canvas)는 코드(code)와 함께 UI를 나란히(side-by-side) 생성하여 동기화(sync) 상태를 유지(stay)한다. 한 쪽(one side)을 변경(change)하면, 항상 다른 쪽(other side)도 업데이트(updates)된다.
- 코드(code) 또는 캔버스(canvas)에서 도구(tools)를 조합(combination)하여 UI를 만들 수 있다.
- 스토리 보드(storyboards)에서 스택 뷰(stack views)를 사용하는 것처럼, 수평(horizontal) 및 수직(vertical) 스택(stacks)으로 뷰(view) 객체(objects)를 구성(organize)한다.
- 미리보기(Preview)를 사용하면 서로 다른 초기(initial) 데이터로 앱(app)이 어떻게 보이고(looks) 작동(behaves)하는지 확인할 수 있으며, 실시간 미리보기(Live Preview)를 사용하면 시뮬레이터(Simulator)를 실행(firing up)하지 않고도 앱(app)과 상호 작용(interact)할 수 있다.
- 재사용 가능한(reusable) 뷰(views)를 만드는 것을 목표로(aim) 해야한다. Xcode의 Extract Subview 도구(tool)를 사용하면 쉽게 구현할 수 있다.
- SwiftUI는
State
속성(property)의 값(value)이 변경(changes)될 때마다(whenever), UI를 업데이트(updates)한다. 하위 뷰(subview)에 대한 참조(reference)를Binding
으로 전달(pass)하여State
속성(property)에 대한 읽기-쓰기(read-write) 접근(access)을 허용(allowing)한다.