-
Chapter 6: Controls & User InputRaywenderlich/SwiftUI by Tutorials 2021. 4. 7. 15:51
Version
Swift 5.3, iOS 14, Xcode 12
5장(chapter), "Intro to Controls: Text & Image"에서 가장 일반적으로(commonly) 사용되는 두 가지 컨트롤(controls)인
Text
와Image
를 사용하는 방법을 배웠으며, 두 컨트롤을 하나로 결합(combines)하는 Label도 간략하게 살펴 보았다.이 장(chapter)에서는
TextField
,Button
,Stepper
등과 같이 일반적으로 사용되는(commonly-used) 다른 컨트롤(controls)과 리팩토링(refactoring)의 중요성(power)에 대해 자세히 알아본다.A simple registration form
5장(chapter)에서 구현(implemented)한 Welcome to Kuchi 화면(screen)은
Text
와Image
를 시작하고, 수정자(modifiers)에 발을 담그는 데(get your feet wet) 유용했다. 이제 사용자에게 이름을 입력하도록 요청(ask)하는 간단한 양식(form)을 구현(implementing)하여 앱(app)에 몇 가지 상호 작용(interactivity)을 추가 할 것이다.이 장(chapter)의 시작(starter) 프로젝트(project)는 5장(chapter)의 마지막 프로젝트(project)와 거의(nearly) 동일(identical)하다. 이전에 중단한(left off) 부분에서 이어 시작한다. 유일한 차이점(difference)은 이 장(chapter)에서 작업을 완료(done)하는 데 필요한 몇 가지 새 파일(files)이 포함(included)되어 있다는 것이다.
이전 장(chapter)에서 사용(borrowed)한 프로젝트(project)의 사본(copy)을 계속 작업하고 싶다면 자유롭게(feel free) 작업하되, 이 경우(case)에는 시작(starter) 프로젝트(project)에서 이 장(chapter)에 필요한 추가(additional) 파일(files)을 iOS와 macOS 대상(targets) 모두에 복사(copy)하고 수동으로(manually) 추가(add)해야 한다:
- Shared/Profile/Profile.swift
- Shared/Profile/Settings.swift
- Shared/Profile/UserManager.swift
iOS 대상(target)에만 추가해야 하는 항목도 있다:
- iOS/Utils/KeyboardFollower.swift
이 파일들(files)을 자유롭게 살펴본다(take a look).
A bit of refactoring
재사용할(reusable) 수 있도록 하고, 각 뷰(view)에 대해 작성하는 코드의 양을 최소화(minimize)하기 위해 작업을 리팩토링(refactor)해야하는 경우가 많이 있다. 자주(frequently) 사용되며, Apple에서 권장(recommended)하는 패턴(pattern)이다.
작성(building)할 새로운 registration view에는 6장(chapter)에서 만든 welcome view와 동일한 배경 이미지(background image)가 있다. 리팩토링(refactoring)이 유용하게(handy) 쓰일 첫 번째 사례(case)가 여기에 있다. 간단하게 welcome view에서 코드를 복사(copy)하여 붙여넣을(paste) 수 있지만, 재사용(reusable) 및 유지 관리(maintainable)가 어려워진다.
WelcomeView.swift를 열고, 배경 이미지(background image)를 정의(define)하는 다음 코드 행(lines)을 선택(select)하고 복사(copy)한다:
Image("welcome-background") .resizable() .aspectRatio(1 / 1, contentMode: .fill) .edgesIgnoringSafeArea(.all) .saturation(0.5) .blur(radius: 5) .opacity(0.08)
그런 다음 Components 그룹(group)에서 우클릭(right-clicking)하여, WelcomeBackgroundImage.swift라는 새 SwiftUI 뷰(View)를 만들어 새 구성 요소(component) 뷰(view)를 생성(create)한다. 다시 말하지만 iOS와 macOS의 두 대상(targets)에 모두 추가(add)해야 한다. 그런 다음 위에서 복사(copied)한 코드를
body
구현(implementation)에 붙여 넣어 포함(contains)된 기본Text
를 대체(replacing)한다. 이제body
은 다음과 같아야 한다:var body: some View { Image("welcome-background") .resizable() .aspectRatio(1 / 1, contentMode: .fill) .edgesIgnoringSafeArea(.all) .saturation(0.5) .blur(radius: 5) .opacity(0.08) }
이제 WelcomeView.swift로 돌아가 이전에(previously) 복사한(copied) 코드 행(lines)을 새로 만든 뷰(view)로 대체(replace)하여 다음과 같이 보이게 한다:
var body: some View { ZStack { WelcomeBackgroundImage() HStack { ...
자동(automatic) 미리보기(preview)가 활성화(enabled)되어 있는지 확인하고, 필요한(necessary) 경우 재개(resume)한다. 기능적(functional) 변경(changes)없이 코드를 리팩토링(refactored)했기 때문에, 예상대로(expect) 변경된(changed) 사항이 없음을 알 수 있다.
Refactored welcome view 이 섹션(section)의 주제(topic)는 리팩토링(refactoring)이므로, 한 단계 더 나아가(go a step further) 다음을 리팩토링(refactor)한다.
- welcome view에 표시되는(displayed) 아이콘(icon) 이미지(image)
- 아이콘(icon)과 “Welcome to Kuchi” 텍스트(text)로 구성된(composed) 온전한(entire) Welcome view
Exercise: 이제 SwiftUI 리팩토링(refactoring) 업적(achievement)을 잠금 해제(unlocked) 했으므로, 두 가지 리팩토링(refactoring)을 직접 수행한 다음 아래에서 진행하는 작업과 비교(compare)해 본다. 두 개의 새로운 뷰(views) 이름을
LogoImage
와WelcomeMessageView
으로 지정할 수 있다.Refactoring the logo image
WelcomeView.swift에서 Image 코드를 선택(select)한다:
Image(systemName: "table") .resizable() .frame(width: 30, height: 30) .overlay(Circle().stroke(Color.gray, lineWidth: 1)) .background(Color(white: 0.9)) .clipShape(Circle()) .foregroundColor(.red)
그리고:
- 코드를 클립 보드(clipboard)에 복사(copy)한다.
- 코드를
LogoImage()
로 바꾼다(replace). - SwiftUI 템플릿(template)을 사용하여 Components 그룹(group)에 새 LogoImage.swift 파일(file)을 만든다.
- LogoImage.swift의
body
구현(implementation)을 welcome view에서 복사한(copied) 코드로 바꾼다.
WelcomeView.swift를 열고 미리보기(preview)를 재개(resume)하면, 다시 한 번 차이점(differences)을 느끼지 못할 것이다. 이는 리팩토링(refactoring)이 잘 되었다는 것을 의미한다.
Refactoring the welcome message
WelcomeView.swift에서 이 작업을 약간 다르게(differently) 수행한다:
Label
을 Command-Click한다. 팝업(popup) 메뉴(menu)가 나타난다(appear):
Refactored subview - Extract Subview를 선택(choose)한다. Xcode는 선택된 구성 요소(component)를
ExtractedView()
로 대체(replace)하고, 파일(file)의 끝에서 새로운ExtractedView
구조체(struct)로 구현(implementation)을 이동시킨다.
Refactored extracted subview - Xcode는 친절하게도 새 뷰(view)의 이름을 편집(edit) 모드(mode)로 넣을 수 있으므로, 새 이름을 즉시(right away) 입력(type)할 수 있다.
WelcomeMessageView
라고 입력하고 Enter를 누른다. - 이제 새 파일(file)로 이동한다. 전체(entire)
WelcomeMessageView
구조체(struct)를 선택(select)하고 잘라낸다(cut). - 다음으로(next), SwiftUI 템플릿(template)을 사용하여 Components 그룹(group)에 새로운 WelcomeMessageView.swift 파일(file)을 만든다.
WelcomeMessageView
의 구현(implementation)을 welcome view에서 잘라낸(cut) 코드로 바꾼다(replace).
다시 한 번(once again) WelcomeView.swift를 열고 미리보기(preview)를 재개(resume)하면, 아무런 차이(difference)가 없다.
welcome view를 리팩토링(refactored)하여, 이를 구성하는 요소(components)를 훨씬 더 재사용할(reusable) 수 있도록 했다.
Creating the registration view
registration view는 새 파일(file)을 만들어야 한다. 프로젝트 탐색기(Project navigator)에서 Welcome 그룹(group)을 마우스로 우클릭(right-click)하고 RegisterView.swift라는 새 SwiftUI 뷰(View)를 추가한다.
다음으로
body
구현(implementation)을 다음으로 바꾼다(replace):VStack { WelcomeMessageView() }
한 줄(single line)의 코드로 재사용 가능한(reusable) 작은 구성 요소(components)가 얼마나 쉽고(easy) 강력할(powerful) 수 있는지 증명했다(proved).
Initial Register View 또한, 이전(previous) 리팩토링(refactoring) 덕분에 몇 줄의 코드(a couple lines of code)를 추가하는 것으로 간단하게 백그라운드 뷰(background view)를 추가할 수도 있다.
body
구현(implementation)을 다음 코드로 바꾼다(replace):ZStack { WelcomeBackgroundImage() VStack { WelcomeMessageView() } }
식사가 준비되었다(lunch is served). 전자 레인지(microwave)보다 빠르다.
Microwave 앱(app)을 실행(run)하려고 하면, 여전히 welcome view가 표시(displays)된다. 두 뷰(views)가 완전히(exactly) 똑같아 보이기 때문에 아마 쉽게(easily) 눈치채지(notice) 못할 것이다. 하지만, 그게 중요한 점(point)은 그게 아니다.
어쨌든(anyway) 앱(app)은 여전히(still) 시작(launch)시 welcome view를 표시(display)하도록 구성(configured)되어 있다. 이를 변경(change)하려면, KuchiApp.swift를 열고,
WelcomeView
를RegisterView
로 교체한다(replace):var body: some Scene { WindowGroup { RegisterView() } }
Power to the user: the TextField
리팩토링(refactoring)을 완료(done)하고, 이제 앱(app)에서 사용자(user)가 이름을 입력(enter)하는 방법을 제공하는 데 집중(focus on)할 수 있다.
Registration form 이전 (previous)섹션(section)에서
VStack
컨테이너(container)를RegisterView
에 추가했으며, 이는 콘텐츠(content)를 수직으로(vertically) 쌓는데(stack) 필요하기 때문에 임의의(random) 결정(decision)이 아니었다.TextField
는 일반적으로(usually) 키보드로 사용자가 데이터를 입력(enter)할 수 있도록 하는 데 사용하는 컨트롤(control)이다. 이전에 iOS 또는 macOS 앱(app)을 만들어 본(built) 적이 있다면, 이의 손위 사촌(older cousins)인UITextField
와NSTextField
를 만났을 것이다.가장 간단한(simplest) 형식(form)으로 제목(title)과 텍스트(text) 바인딩(binding)을 받는 생성자(initializer)를 사용하여 컨트롤(control)을 추가(add)할 수 있다.
title
은 텍스트 필드(text field)가 비어있을 때(empty), 텍스트 필드(text field) 내부에 표시되는(appears) 자리 표시자(placeholder) 텍스트(text)이고,binding
은 텍스트 필드(text field)의 텍스트(text)와 속성(property) 자체 간의 양방향(2-way) 연결(connection)을 처리(takes care of)하는 관리 속성(managed property)이다.8장(chapter), "State & Data Flow — Part I"에서 바인딩(binding)에 대한 자세한 내용을 알아본다. 지금은 바인딩(binding)을 만들고 사용하려면 다음을 수행해야 한다는 것만 기억하면 된다:
- 속성(property)에
@State
특성(attribute)을 추가(add)한다. - 속성(property) 값(value) 대신 바인딩(binding)을 전달(pass)하려면, 속성(property) 앞에(prefix)
$
를 붙인다.
다음 속성(property)을
RegisterView
에 추가한다:@State var name: String = ""
그런 다음
WelcomeMessageView()
다음에 텍스트 필드(text field)를 추가한다:TextField("Type your name...", text: $name)
미리보기(preview)에 텍스트 필드(text field)가 표시(appear)될 것으로 예상(expect)했지만, 아무 일도 일어나지 않으며(nothing happens) 이전(before)과 동일하게 보인다.
자세히 살펴보면(closer inspection) 문제(problem)가 드러난다(reveals). 코드 편집기(code editor)에서
TextField
를 클릭(click)하면 미리보기(preview)에서 텍스트 필드(text field)가 선택된다. 파란색 사각형(blue rectangle)에서 볼 수 있듯이 너무 넓다(wide).Wide text field Challenge: 왜 이런 일이 발생하는지 생각해 본다. Hint: 배경 이미지(background image)와 관련되어 있다.
그 이유는 배경 이미지(background image)가
.fill
콘텐츠 모드(content mode)로 구성(configured)되어 있기 때문이다. 이는 이미지(image)가 가능한(as possible) 많은 상위 뷰(parent view) 공간(space)을 차지(occupy)하도록 확장(expands)된다. 이미지가 정사각형(square)이기 때문에 부모(parent)의 세로(vertically)에 맞추면(fits), 가로(horizontally)는 화면 경계(boundaries)를 넘어 간다(beyond)는 의미이다.이 문제를 해결(fix)하는 방법은
ZStack
사용을 피하고(avoid), 대신VStack
에.background
수정자(modifier)를 사용하여 실제 콘텐츠(content) 뒤에 배경 뷰(background view)를 배치(position)하는 것이다.register view에서
ZStack
을 제거(remove)한 다음,WelcomeBackgroundImage()
를.background
수정자(modifier)로VStack
에 추가한다:var body: some View { VStack { WelcomeMessageView() TextField("Type your name...", text: $name) } .background(WelcomeBackgroundImage()) }
Note: UIKit에서 뷰(views)에는 균일한(uniform) 배경색(background color)을 지정(specify)하는 데 사용할 수있는
backgroundColor
속성(property)이 있다. SwiftUI에서는(counterpart) 더 다형적(polymorphic)이다..background
수정자(modifier)는Color
,Image
,Shape
등을 포함하여View
를 준수(conforms)하는 모든 유형(type)을 허용(accepts)한다.이 변경(change)으로 이제 텍스트 필드(text field)가 표시(visible)되지만, 배경(background)이 너무 작아 보인다.
Background too small 그 이유는
VStack
이 전체 화면(entire screen)을 사용하지 않고 콘텐츠(content)를 렌더링(render)하는 데 필요한 것만 사용하기 때문이다. 위 사진(picture)에서 파란색으로 강조된(highlighted in blue) 실제 크기(actual size)를 볼 수 있다.이 문제(problem)를 해결(fix)하려면,
VStack
의 시작 부분과 끝 부분에 하나씩, 두 개의Spacer
s를 추가한다:VStack { Spacer() // <-- 1st spacer to add WelcomeMessageView() TextField("Type your name...", text: $name) Spacer() // <-- 2nd spacer to add } .background(WelcomeBackgroundImage())
다음 장(chapter)에서
Spacer
에 대해 자세히 배우게 될 것이다. 지금 당장 알아야 할 것은 Spacer는 모든 공간(space)을 자유롭게 사용할 수 있도록 확장(expands)시킨다는 것이다. 이 변경(change)으로 이제 배경 이미지(background images)는 예상대로(expected) 확장(expand)된다.Text field visible Styling the TextField
아주 미니멀한 모양(very minimalistic look)을 원하지 않는 한, 텍스트 필드(text field)의 스타일(styling)이 만족스럽지(be satisfied with) 않을 것이다.
더 보기 좋게 만들려면, 약간의 패딩(padding)과 테두리(border)를 추가해야 한다. 테두리(border)의 경우, 텍스트 필드(text field)에 스타일(style)을 적용(applies)하는
.textFieldStyle
수정자(modifier)를 활용할 수(take advantage of) 있다.현재(currently), SwiftUI는 아래 이미지와 같은 네 가지 스타일(styles)을 제공(provides)한다:
Text field styles "no style"은 명시적으로(explicitly) 언급(mentioned)되지만, 이는
DefaultTextFieldStyle
에 해당한다(corresponds).DefaultTextFieldStyle
과PlainTextFieldStyle
간에 눈에 띄는(noticeable) 차이가 없음을 알 수 있다. 그러나(however),RoundedBorderTextFieldStyle
은 모서리가 약간 둥근(slightly rounded corners) 테두리(border)를 제공(presents)한다. 다섯 번째 스타일(style)인SquareBorderTextFieldStyle
도 있지만 이는 macOS에서만 사용할 수 있다.Kuchi에서는 다른 사용자 정의(custom) 스타일(style)을 제공(provide)할 것이다. 이를 위한 세 가지 선택 사항(options)이 있다.
- 필요에 따라
TextField
에 수정자(modifiers)를 적용한다(apply). TextFieldStyle
프로토콜(protocol)을 준수하는(conforming) 구체적인(concrete) 유형(type)을 정의하여(defining), 고유한(own) 텍스트 필드(text field) 스타일(style)을 만든다.ViewModifier
프로토콜(protocol)을 준수하는(conforming) 구체적인(concrete) 유형(type)을 정의하여(defining), 사용자 정의(custom) 수정자(modifier)를 만든다.
어떤 방법(solution)을 선택하든 수정자(modifiers) 목록(list)을 직간접적으로(directly or indirectly) 순차(sequence) 적용(applying)하는 방식으로 구성(consists)되기 때문에, 가장 논리적인(logical) 것은 첫 번째 방법(method)부터 시작하는 것이다.
텍스트 필드(text field)에 다음 수정자(modifiers)를 적용한다:
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color.white) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(lineWidth: 2) .foregroundColor(.blue) ) .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2)
아래 그림(figure)은 각 수정자(modifier)의 효과(effect)를 보여준다:
Rext field border style 각각은 다음과 같이 작동한다.:
- 수정되지 않은(unmodified) 텍스트 필드(text field)를 만든다.
- 세로(vertically) 16 포인트(points), 가로(horizontally) 8 포인트(points) 패딩(padding)을 추가(adds)한다.
- 불투명한(non-transparent) 흰색 배경(background)을 추가(adds)한다.
- 모서리 반경(corner radius)이 8인 둥근 사각형(rounded rectangle)을 사용하여 테두리(border)에 대한 오버레이(overlay)를 만든다.
- stroke 효과(effect)를 추가(adds)하여 테두리(border)만 유지(keep)하고, 내용(content)은 보이지 않게(behind visible) 한다.
- 테두리(border)를 파란색(blue)으로 만든다.
- 그림자(shadow)를 추가(adds)한다.
텍스트 필드(text field)의 왼쪽(left)과 오른쪽(right) 가장자리(edges)에 간격(spacing)이 없음을 알 수 있다(notice). 2단계(step)에서 추가한(adds) 패딩(padding)은 텍스트 필드(text field)와 이를 포함하는(contains) 뷰(views) 사이에 패딩(padding)을 추가한다. 텍스트 필드(text field)와 상위 뷰(parent view) 사이에 패딩(padding)을 추가(add)하려면, 텍스트 필드(text field)를 포함(contains)하고 있는 뷰(view)인
VStack
에 패딩(padding) 수정자(modifier)를 추가(add)해야 한다.VStack
에서.background(WelcomeBackgroundImage())
의 바로 앞(right before), 스택(stack)의 닫는 대괄호(closing bracket) 뒤에 다음을 추가한다:.padding()
Form with padding Creating a custom text style
사용자 정의(custom) 텍스트 필드(text field) 스타일(style)은 하나의 메서드(method)만 선언(declares)하는
TextFieldStyle
을 채택(adopt)해야 한다:public func _body( configuration: TextField<Self._Label>) -> some View
configuration
매개 변수(parameters)로 텍스트 필드(text field)를 받아(receives) 원하는 만큼의 수정자(modifiers)를 적용(apply)하여 resulting view를 반환(returning)한다.RegisterView.swift의
RegisterView
구조체(struct) 앞에 새로운 사용자 정의(custom) 텍스트 스타일(text style)을 만든다:struct KuchiTextStyle: TextFieldStyle { public func _body( configuration: TextField<Self._Label>) -> some View { return configuration } }
이 텍스트 스타일(text style)은 받은(receives) 것과 동일한 텍스트 필드(text field)를 반환(returns)하기 때문에, 아무런 작업도 하지 않는다. 이를 사용자 정의(customize)하려면 수정자(modifiers)를 추가해야 한다.
따라서, 이전에 적용한 4개의 수정자(modifiers)를 이 메서드(method)의 텍스트 필드(text field)로 이동한다.
RegisterView
에서 다음 행(lines)을 선택(select)하고 자른다(cut):.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color.white) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(lineWidth: 2) .foregroundColor(.blue) ) .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2)
KuchiTextStyle
에서body
구현(implementation)의return configuration
문(statement) 뒤에 붙여 넣으면(paste) 다음과 같이 된다:public func _body( configuration: TextField<Self._Label>) -> some View { return configuration .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color.white) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(lineWidth: 2) .foregroundColor(.blue) ) .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2) }
반환(returning)되는 것은 네 개의 수정자(modifiers)를 적용한(applying) 결과 텍스트 필드(resulting text field)이다.
이제이 새로운 스타일(style)을 사용할 수 있다.
RegisterView
로 돌아가textFieldStyle
수정자(modifier)를 텍스트 필드(text field)에 추가하면 다음과 같다:TextField("Type your name...", text: $name) .textFieldStyle(KuchiTextStyle())
여기에서 단순히
KuchiTextStyle
의 새 인스턴스(instance)를 생성하고, 이를textFieldStyle
에 전달(pass)한다.미리보기(preview)를 보면, 리팩토링(refactoring) 전과 동일하며 기능적(functional) 관점(standpoint)에서 변경된 사항(changed)은 없다.
Form with padding 다음 섹션(section)에서 사용자 정의(custom) 수정자(modifier)로 이동하기 때문에, 더 이상 이런 사용자 정의(custom) 스타일(style)은 필요하지 않다. 텍스트 필드(text field)에 적용된(applied) 4개의 수정자(modifiers)가 다시 표시 될 때까지 모든 변경 사항을 실행 취소(undo, Control + Z 반복 누름)하여 새로 생성된
KuchiTextStyle
이 사라지게 한다. 그리고RegisterView
의body
구현(implementation)이 다음과 같은지 확인(verify)한다:var body: some View { VStack { Spacer() WelcomeMessageView() TextField("Type your name...", text: $name) .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color.white) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(lineWidth: 2) .foregroundColor(.blue) ) .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2) Spacer() } .padding() .background(WelcomeBackgroundImage()) }
Creating a custom modifier
사용자 정의(custom) 텍스트 필드 스타일(text field style)보다 사용자 정의(custom) 수정자(modifier)를 선호하는(preferring) 이유(reason)는 버튼(buttons)에 동일한 수정자(modifiers)를 적용(apply)할 수 있기 때문이다.
SwiftUI View 템플릿(template)을 사용하여 Components 그룹(group)에 새 파일(file)을 추가하고, 이름을 BorderedViewModifier로 지정한다.
먼저, 사용자 정의(custom) 수정자(modifier)에는 필요 없기 때문에, 자동 생성된(autogenerated)
BorderedViewModifier_Previews
구조체(struct)를 삭제(delete)한다. 다음으로, 준수(conforms)하는 프로토콜(protocol)을View
에서ViewModifier
로 변경(change)한다:struct BorderedViewModifier: ViewModifier {
ViewModifier
는body
멤버(member)를 정의(defines)하지만, 속성(property)이 아닌 수정자(modifier)가 적용되는(applied to) 뷰(view)인 콘텐츠(content)를 가져와 콘텐츠(content)에 적용(applied to)된 수정자(modifier)의 결과로 다른 뷰(view)를 반환(returns)하는 함수(function)이다. 개념적으로(conceptually) 사용자 정의(custom) 텍스트 필드 스타일(text field style)과 유사하므로 반복되는(recurring) 패턴(pattern)을 볼 수 있다.속성(property)을 다음 함수(function)로 바꾼다(replace):
func body(content: Content) -> some View { content }
코드는 그대로 수정자(modifier)가 적용된(applied to) 동일한 뷰(view)를 반환(returns)한다. 아직 끝나지(done) 않았으므로, 걱정할 필요 없다.
RegisterView.swift로 돌아가서 텍스트 필드(text field)에 적용된(applied to) 모든 수정자(modifiers)를 선택(select)하고 잘라낸다(cut):
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color.white) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(lineWidth: 2) .foregroundColor(.blue) ) .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2)
다음으로 BorderedViewModifier.swift로 다시 전환(switch back)하고,
content
뒤에 다음 수정자(modifiers)를 붙여 넣는다:func body(content: Content) -> some View { content .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color.white) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(lineWidth: 2) .foregroundColor(.blue) ) .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2) }
그게 전부이다(that’s it). 이제 새로운 사용자 정의(custom) 수정자(modifier)가 생겼다. 이를 적용(apply)하려면
ModifiedContent
라는 구조체(struct)를 사용한다. 생성자(initializer)는 다음 두 개의 매개 변수(parameters)를 사용한다:- The content view
- The modifier
RegisterView
를 열고, 다음과 같이TextField
를ModifiedContent
인스턴스화(instantiation)에 포함(embed)한다:ModifiedContent( content: TextField("Type your name...", text: $name), modifier: BorderedViewModifier() )
미리보기(preview)가 업데이트(updates)된 후 파란색 테두리(blue border)가 올바르게(correctly) 적용(applied)된 것을 볼 수 있다. 하지만 솔직히(honest) 말해서 그 코드는 환상적(fantastic)이진 않다. 일반 수정 자 호출과 같이 더 간단한(simpler) 수정자(modifier) 호출(call)로 대체(replace)할 수 있다면 좋을 것이다.
필요한 것은 뷰(view) 확장(extension)에서 convenience method를 만드는 것이다.
BorderedViewModifier
를 열고, 파일(file) 끝에 다음 확장(extension)을 추가(add)한다:extension View { func bordered() -> some View { ModifiedContent( content: self, modifier: BorderedViewModifier() ) } }
이제
RegisterView
로 돌아가,ModifiedContent
구성 요소(component)를 다음으로 바꿀(replace) 수 있다:TextField("Type your name...", text: $name) .bordered()
미리보기(preview)에서 수정자(modifier)가 올바르게(correctly) 적용되었는지(applied) 확인한다. 기능(functional) 변경 사항(change)을 적용하지 않고, 코드 리팩토링(refactoring)만 적용(applied)했기 때문에 이미 짐작한대로 이전과 동일하다.
A peek at TextField’s initializer
TextField
에는 두 쌍(pairs)의 생성자(initializers)가 있으며, 각 쌍(pair)은 title 매개 변수(parameter)에 대한 지역화(localized) 및 지역화되지 않은(non-localized) 버전(version)이 있다.이 장(chapter)에서 사용되는 버전(version)은 편집 가능한(editable) 텍스트(text)에 대한 제목(title)과 바인딩(binding)을 사용하는 현지화되지 않은(non-localized) 버전(version)이다:
public init<S>( _ title: S, text: Binding<String>, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {} ) where S : StringProtocol
여기에서 사용하지 않은 매개 변수(parameters)가 두 개 있다. 이는 기본적(default)으로 빈(empty) 구현(implementation)이므로 명시적(explicitly)으로 제공(provided)하지 않는다. 이러한 매개 변수(parameters)는 사용자 입력(input) 전후에(before and after) 추가(additional) 처리(processing)를 수행하는 데 사용할 수 있는 두 개의 클로저(closures)이다.
onEditingChanged
: edit가 포커스(focus)를 얻거나(obtains, Boolean 매개 변수가true
인 경우) 포커스(focus)를 잃을 때(loses, 매개 변수가false
인 경우) 호출(called)된다.onCommit
: 사용자가 리턴(return) 키(key)를 누르는 것과 같은 실행(commit) 작업(action)을 수행할 때 호출된다(called). 자동으로(automatically) 포커스(focus)를 다음 필드(field)로 이동하려는 경우에 유용하다.
다른 초기화(initializers) 쌍(pair)은 추가(additional) 포매터(formatter)을 사용한다. 현지화되지 않은(non localized) 버전(version)에는 다음과 같은 시그니처(signature)가 있다:
public init<S, T>( _ title: S, value: Binding<T>, formatter: Formatter, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {} ) where S : StringProtocol
다른 쌍(pair)과의 차이점(differences)은 다음과 같다:
formatter
매개 변수(parameter)는Foundation
의 추상(abstract) 클래스(class)인Formatter
를 상속하는(inherited) 클래스(class)의 인스턴스(instance)이다. 편집된(edited) 값(value)이 문자열과 다른 유형(예 : 숫자 또는 날짜)인 경우 사용할 수 있지만, 사용자 정의(custom) 포매터(formatters)을 만들 수도 있다.T
제네릭(generic) 매개 변수(parameter)는 TextField에서 처리하는 실제 기본(underlying) 유형(type)을 결정(determines)한다.
포매터(formatters)에 대한 자세한 내용은 데이터 포매팅(Data Formatting) apple.co/2MNqO7q을 참조한다.
Showing the keyboard
사용자가 데이터를 입력(type)하도록 허용(letting)하면, 조만간(sooner or later) 소프트웨어(software) 키보드(keyboard)를 표시(display)해야 한다.
TextField
가 포커스(focus)를 받자마자(as soon as) 자동으로(automatically) 그런 현상이 발생하지만, 키보드(keyboard)가TextField
를 가리지(cover) 않도록 해야 한다.iOS 시뮬레이터(Simulator)를 iPhone 8이나 iPhone 11으로 변경하고 앱(app)을 실행(run)하면, 키보드(keyboard)가 표시(visible)될 때, 실제로 겹치지(overlap)는 않지만 텍스트 필드(text field)와 너무 가깝다(close)는 것을 알 수 있다.
Keyboard visible 이 프로젝트(project)와 함께 제공된 매우 기본적인(basic) 키보드(keyboard) 처리기(handler) 구현(implementation)은 Utils/KeyboardFollower.swift에서 찾을 수 있다.
알림 센터(Notification Center)를 사용하여 키보드 높이(height)가 포함(contains)된
keyboardHeight
속성(property)을 저장(stores)하는keyboardWillChangeFrameNotification
이벤트(event)를 구독(subscribe)한다. 키보드(keyboard)가 숨겨져(hidden) 있으면 0이고, 보이면(visible) 0보다 큰 값(value)이다.따라서 변경 사항(changes)을 구독(subscribe)하고 키보드(keyboard)의 높이(height)를 사용하여,
TextField
가 포함된(containing) 뷰(view)의 하단 패딩(bottom padding)을 변경(alter)할 수 있다.가장 먼저 할 일(the first thing to do)은
RegisterView
의name
바로 뒤에 새 속성(property)을 추가하는 것이다:@ObservedObject var keyboardHandler: KeyboardFollower
@ObservedObject
속성(attribute)은 8장(chapter), "State & Data Flow — Part I"에서 설명한다. 현재로서는, 이전에 접했던(encountered)@State
속성(attribute)과 유사하지만(similar to) 다른 방식으로(a different way), 사용자 정의(custom) 클래스(class)에 적용(applied to)된다는 가장 중요한 것만 알면 된다.이 속성(property)을 초기화(initialize)해야 하며, 의존성 주입(dependency injection)을 사용할 수 있다. 즉, 생성자(initializer)를 사용해
KeyboardFollower
인스턴스(instance)를 전달(passing)한다.다음을
keyboardHandler
속성(property) 아래의RegisterView
에 추가한다:init(keyboardHandler: KeyboardFollower) { self.keyboardHandler = keyboardHandler }
RegisterView
가 인스턴스화(instantiated)되는 모든 곳에 이 매개 변수(parameter)를 전달(pass)해야 한다. 미리보기 제공자(preview provider)로 스크롤(scroll down)하여previews
속성(property)의 구현(implementation)을 다음과 같이 변경한다:RegisterView(keyboardHandler: KeyboardFollower())
다음으로 KuchiApp.swift를 열어, 같은 일을 반복한다:
WindowGroup { RegisterView(keyboardHandler: KeyboardFollower()) }
그리고, 미리보기(preview)에도 똑같이 적용해 준다:
struct KuchiApp_Previews: PreviewProvider { static var previews: some View { RegisterView(keyboardHandler: KeyboardFollower()) } }
거의 다됐다. 마지막으로(lastly), RegisterView.swift로 돌아가
VStack
에 length 매개 변수(parameter)로keyboardHandler.keyboardHeight
를 사용하는 하단 패딩(bottom padding) 수정자(modifier)를 추가해야 한다. VStack의 다른 모든 수정자(modifiers) 앞에(before) 추가한다:.padding(.bottom, keyboardHandler.keyboardHeight)
Note: 이는 추가해야하는 새로운 패딩(padding) 수정자(modifier)이다. 다른 패딩(padding)이 있지만, 이를 변경(alter)하거나 교체(replace)해서는 안 된다. 여러(multiple) 패딩(padding) 수정자(modifiers)의 효과(effect)가 결합(combined)되어, 각 방향(direction)에 적용된(applied to) 패딩(padding)의 산술 합계(arithmetic sum)가 된다.
이 행(line)을 사용하면, SwiftUI에게 컨테이너 뷰(containing view)의 아래(bottom)에 다음 규칙을 따르는 동적(dynamic) 패딩(padding)을 적용(apply)하도록 한다:
- 키보드가 표시(visible)되지 않으면,
keyboardHandler.keyboardHeight
가 0이므로, 패딩(padding)이 적용(applied)되지 않는다. - 키보드가 표시(visible)되면,
keyboardHandler.keyboardHeight
의 값(value)이 0보다 크므로, 키보드(keyboard) 높이(height)와 동일(equal)한 패딩(padding)이 적용(applied)된다.
Keyboard padding 하지만 이게 끝이 아니다. safe area가 있는 휴대폰(phones)에서의 키보드(keyboard)는 화면(screen) 하단 가장자리(bottom edge)에서 시작하므로 safe area를 포함(including)하지 않는 반면, 위(above)에서 추가한 패딩(padding)은 safe area에서 시작한다.
이를 수정(fix)하려면,
EdgeIgnoringSafeArea
수정자(modifier)를 사용할 수 있다. 위(above)에서 추가한 패딩(padding) 뒤에 다음을 추가한다:.edgesIgnoringSafeArea( keyboardHandler.isVisible ? .bottom : [])
여기서는 키보드(keyboard)가 표시(visible)되는 경우에만, 뷰(view)가 하단(bottom) 안전 영역(safe area)을 무시(ignore)하도록 한다.
body
의 구현(implementation)은 다음과 같아야 한다:VStack(content: { Spacer() WelcomeMessageView() TextField("Type your name...", text: $name) .bordered() Spacer() }) .padding(.bottom, keyboardHandler.keyboardHeight) .edgesIgnoringSafeArea( keyboardHandler.isVisible ? .bottom : []) .padding() .background(WelcomeBackgroundImage())
앱(app)을 실행(run)하면 텍스트 필드(text field)가 vertically centered에 표시되지만(왼쪽 이미지), 텍스트 필드(text field)가 포커스(focus)되어 키보드(keyboard)가 표시(displayed)되면 텍스트 필드(text field)가 위쪽(top)으로 이동한다(오른쪽 이미지).
Keyboard on off Taps and buttons
이제 양식(form)이 준비되었으므로, 사용자가 가장 자연스럽게 원하는 작업은 해당 양식(form)을 제출(submit)하는 것이다. 가장 자연스럽고 오래된 방식은 제출(submit) 버튼(button)을 사용하는 것이다.
SwiftUI 버튼(button)은 UIKit/AppKit 버튼(counterpart)보다 훨씬 더 유연(flexible)하다. 콘텐츠(content)를 텍스트(text) 레이블(label)만 사용하거나 이미지(image)와 함께(combination) 사용하는 것으로 제한(limited)되지 않는다.
대신 버튼(button)에 대해
View
라면 무엇이든 사용할 수 있다. 다음과 같이 제네릭(generic) 유형(type)을 사용하는 선언(declaration)에서 이를 확인할 수 있다:struct Button<Label> where Label : View
제네릭(generic) 유형(type)은
View
를 준수(conform)하는 버튼(button)의 시각적(visual) 콘텐츠(content)이다.즉, 버튼(button)에는
Text
나Image
같은 기본 구성 요소(component)뿐만 아니라, 세로(vertical) 또는 가로(horizontal) 스택(stack)에 포함(enclosed)된Text
와Image
컨트롤(controls) 쌍(pair)과 같은 복합(composite) 구성 요소(component) 또는 더 복잡한(complex) 구성 요소(component)가 포함될 수 있다.버튼(button)을 추가(Adding)하는 것은 선언(declaring)하는 것만큼 쉽다. 레이블(label)을 지정하고, 처리기(handler)를 첨부(attach)하기만 하면된다. 시그니처(signature)는 다음과 같다:
init( action: @escaping () -> Void, @ViewBuilder label: () -> Label )
생성자(initializer)는 실제로 두 개의 클로저(closures)를 매개 변수(parameters)로 취한다:
- action: the trigger handler
- label: the button content
label
매개 변수(parameter)에 적용된(applied to)@ViewBuilder
속성(attribute)은 클로저(closure)가 여러(multiple) 하위 뷰(child views)를 반환(return)하도록 하는 데 사용된다.Note: tap handler 매개 변수(parameter)는 tap 또는 tapAction 대신 action이라고 하며, 문서(documentation)를 찾아보면 tap handler가 아니라 trigger handler라고 한다.
왜냐하면, iOS에서는 탭(tap)이고 macOS에서는 마우스 클릭(click), watchOS에서는 디지털 크라운 프레스(digital crown press) 등이 될 수 있기 때문이다.Note: 버튼(button) 생성자(initializer)는 탭(tap) 처리기(handler)를 마지막 매개 변수(parameter)가 아닌 첫 번째 매개 변수(parameter)로 사용하여, 마지막 위치에 작업(action) 클로저(closures)를 제공하는 Swift의 일반적인 관행을 깬다(breaking).
이는 후행 클로저 구문(trailing closure syntax)을 사용할 수 없다는 것을 의미한다. 그 이유는 SwiftUI에서 마지막 매개 변수(parameter)는 항상 뷰(view) 선언(declaration)이기 때문에 패턴(pattern)에 맞춰 변경되었을 가능성이 매우 높다. 참고로(by the way), 해당 선언에서는 동일한 후행 클로저 구문(trailing closure syntax)을 사용할 수 있다.Submitting the form
인라인(inline) 클로저(closure)를 추가할 수 있지만, 뷰 선언(declaration)을 코드로 복잡하게(cluttering) 만들지 않는 것이 좋다. 따라서 트리거(trigger) 이벤트(event)를 처리(handle)하는 대신, 인스턴스(instance) 메서드(method)를 사용한다.
RegisterView
에서TextField
뒤에 버튼(button)을 추가한다:Button(action: self.registerUser) { Text("OK") }
그런 다음
RegisterView
구조체(struct) 뒤에registerUser()
이벤트 처리기(event handler)를 포함(containing)하는 다음 확장(extension)을 추가한다:// MARK: - Event Handlers extension RegisterView { func registerUser() { print("Button triggered") } }
이제 시뮬레이터(Simulator) 또는 실시간 미리보기(Live Preview)를 활성화(activating)하여, 앱(app)을 실행(run)하고 OK를 누르면 메시지(message)가 Xcode 콘솔(console)에 출력된다. 실시간 미리보기(Live Preview)에서 아무 것도 표시(displayed)되지 않으면, 실시간 미리보기(Live Preview) 버튼(button)을 우클릭(right-clicking)하여 디버그 미리보기(Debug Preview)를 활성화(enable)해야 한다.
Button tap 이제 트리거(trigger) 처리기(handler)가 연결(wired up)되었으므로, 콘솔(console)에 메시지(message)를 출력하는 것보다 더 유용한 작업을 해야 한다. 프로젝트(project)에는 user defaults로 사용자 설정(user settings)을 각각 저장(saving)하고 복원(restoring)하는
UserManager
클래스(class)가 있다.UserManager
는 클래스(class)가 뷰(views)에서 사용될 수 있도록 하는 프로토콜(protocol)인ObservableObject
를 준수한다(conforms). 인스턴스(instance) 상태(state)가 변경(changes)되면 뷰(view) 업데이트(update)를 실행(triggers)한다. 이 클래스(class)는 뷰(view) 리로드(reloads)를 실행(triggers)하는 상태(state)를 식별(identifies)하는@Published
속성(attribute)으로 표시(marked with)된profile
과settings
라는 두 가지 속성(properties)을 노출(exposes)한다.즉,
RegisterView
에서name
속성(property)을 삭제하고UserManager
인스턴스(instance)로 대체(replace)할 수 있다:@EnvironmentObject var userManager: UserManager
전체 앱(app)에 대해 인스턴스(instance)를 한 번 주입(inject)하고 필요한 모든 환경(environment)에서 사용(retrieve)할 것이므로
@EnvironmentObject
속성(attribute)으로 표시된다(marked with). 9장(chapter), “State & Data Flow — Part II”에서ObservableObject
과@EnvironmentObject
에 대해 자세히 알아본다.다음으로
TextField
에서$name
참조(reference)를$userManager.profile.name
으로 변경(change)해야 한다. 다음과 같이 되도록 수정한다:TextField("Type your name...", text: $userManager.profile.name) .bordered()
마지막으로
registerUser()
에서print
문(statement)을 다음과 같은 더 유용한 구현(implementation)으로 바꾼다(replace):func registerUser() { userManager.persistProfile() }
이제 이 뷰(view)를 미리 보려(preview)고 하면 실패(fail)하게 된다. 위(above)에서 언급(mentioned)했듯이,
UserManager
인스턴스(instance)를 주입(injected)해야하기 때문이다.RegisterView_Previews
구조체(struct)에서.environmentObject
수정자(modifier)를 사용해 user manager를 뷰(view)에 전달한다.RegisterView_Previews
구현(implementation)을 다음과 같이 업데이트(update)한다:struct RegisterView_Previews: PreviewProvider { static let user = UserManager(name: "Ray") static var previews: some View { RegisterView(keyboardHandler: KeyboardFollower()) .environmentObject(user) } }
마찬가지로(likewise), 시뮬레이터(Simulator)에서 앱(app)을 실행(run)하면 충돌(crash)이 발생한다. 방금 변경한 사항은 미리보기(preview)에만 적용되며 앱(app)에는 영향(affect)을 주지 않는다. 따라서
KuchiApp
에서도 변경(changes)이 필요하다. 이를 열고, 속성(property) 및 생성자(initializer)를KuchiApp
에 추가한다.let userManager = UserManager() init() { userManager.load() }
이렇게하면
UserManager
의 인스턴스(instance)가 생성되고, 사용 가능한(available) 경우 저장된(stored) 사용자(user)를 불러온다(loaded). 다음으로RegisterView
인스턴스(instance)에environmentObject
수정자(modifier)를 사용하여 주입(inject)한다:window.rootViewController = UIHostingController( rootView: RegisterView(keyboardHandler: KeyboardFollower()) .environmentObject(userManager) )
Styling the button
이제 버튼(button)이 완전히(fully) 작동(operative)한다. 모양(looks)이 좋아보이지만 훌륭하지는 않다. 이를 개선하기 위해 레이블(label) 옆에 아이콘(icon)을 추가하고 레이블 글꼴(label font)을 변경한 후, 이전
TextField
에 대해 만든.border()
수정자(modifier)를 적용(apply)할 수 있다.RegisterView.swift에서 버튼(button)을 찾아 다음 코드로 바꾼다(replace):
Button(action: self.registerUser) { HStack { //label 매개변수는 여러 하위 view를 반환할 수 있다. //여기서는 HStack를 사용하여 view를 가로로 그룹화한다. //이를 생략하면, 두 components가 수직으로 배치된다. Image(systemName: "checkmark") //checkmark 아이콘을 추가한다. .resizable() .frame(width: 16, height: 16, alignment: .center) //아이콘을 가운데 정렬하고, 16 x 16 사이즈로 한다. Text("OK") .font(.body) //글꼴을 .body 유형으로 변경한다. .bold() //글꼴을 굵게 한다. } } .bordered() //bordered 수정자를 적용하여 모서리가 둥근 파란색 테두리를 추가한다.
모든 작업을 올바르게(correctly) 수행했다면, 다음과 같은 미리보기(preview)가 표시된다:
Styled button Reacting to input: validation
이제 전체 키보드(keyboard) 작업(affair)을 마무리(concluded)하고 양식(form)을 제출(submit)하는 버튼(button)을 추가했으므로, 반응 형 사용자 인터페이스(reactive user interface)의 다음 단계(next step)는 사용자(user)가 입력(entering)하는 동안, 사용자 입력(user input)에 반응(react)하는 것이다.
이는 다음과 같은 여러 가지 이유로 매우(quite) 유용할 수 있다:
- 데이터를 입력(entered)하는 동안 유효성 검사(validating)
- 입력한(typed) 문자(characters)의 수(counter)를 표시
그러나 목록(list)은 여기서 끝이 아니다. UIKit에서 사용자가 입력한(entered) 입력(input)을 모니터링(monitoring)하는 기존 방법은 delegate를 사용하거나 Notification Center event를 구독(subscribing)하는 방식이었다. 사용자가 키(key)를 누를(presses) 때마다 호출(called)되는 처리기(handler) 클로저(closure)를 취하는 수정자(modifier)와 같이, 입력(input) 변경(changes)에 반응(react)하는 유사한 방법을 찾고 싶을 것이다.
그러나, 입력 변경(input change)을 모니터링(monitor)하는 SwiftUI 방식은 다르다.
사용자 입력(user inpu)을 확인(validate)하고, 입력(input)이 유효(valid)할 때까지 OK 버튼(button)을 비활성화(disabled) 상태로 유지(keep)한다고 가정해 본다. 예전에는(in the old days) 값 변경(value changed) 이벤트(event)를 구독(subscribe)하고, 논리 표현식(logical expression)을 수행하여 버튼(button)을 활성화(enable)할지 비활성화(disable)할지 결정한 다음 버튼(button)의 상태(state)를 업데이트(update)했다.
SwiftUI의 차이점은 버튼(button)의 수정자(modifier)에 논리 표현식(logical expression)을 전달(pass)한다. 그게 전부이다(That’s all). 상태(status)의 변경(change)이 발생(occurs)하면, 뷰(view)가 다시 렌더링(rerendered)되고 논리 표현식(logical expression)이 재평가(re-evaluated)되며 버튼(button)의
disabled
상태(status)가 업데이트(updated)된다.RegisterView.swift에서 OK 버튼(button)에 다음 수정자(modifier)를 추가한다:
.disabled(!userManager.isUserNameValid())
이 수정자(modifier)는
disabled
상태(state)를 변경(changes)한다. 이는View
프로토콜(protocol)에 속하므로(belongs to) 모든 뷰(view)에 적용(applies to)된다. 오직 하나의 매개 변수(parameter)만 사용하는데, 뷰(view)가 상호 작용(interactable)할 수 있는지 여부를 나타내는 부울(Boolean)이다.사용자(user)가
TextField
에 입력(types)하면,userManager.profile.name
속성(property)이 변경(changes)되어 뷰(view) 업데이트(update)가 실행(triggers)된다. 따라서 버튼(button)이 다시 렌더링(rerendered)되고.disabled()
의 표현식(expression)이 재평가(re-evaluated)되므로, 입력(input)이 변경(changes)되면 버튼(button)의 상태(state)가 자동으로(automatically) 업데이트(updated)된다.이 앱(app)에서 name에 대한 요구 사항(requirement)은 최소 3자 이상이어야 한다는 것이다.
Button enabled or not Reacting to input: counting characters
사용자(user)가 입력(entered)한 문자 수(number of characters)를 표시(showing)하는 레이블(label)을 추가(add)하려는 경우, 그 과정(process)은 매우 유사(similar)하다.
TextField
뒤에 다음 코드를 추가한다:HStack { Spacer() //Spacer를 사용하여 pseudo-right-alignment 방식으로 Text를 오른쪽으로 밀어넣는다. Text("\(userManager.profile.name.count)") //Text는 name 속성의 문자 수이다. .font(.caption) .foregroundColor(userManager.isUserNameValid() ? .green : .red) //요효성 검사를 통과하면 녹색, 그렇지 않으면 빨간색이 된다. .padding(.trailing) } .padding(.bottom) //OK 버튼에서 약간의 간격을 추가한다.
이제 앱(app)을 실행(run)하거나 Xcode에서 실시간 미리보기(live preview)를 활성화(enable)하여 카운터(counter)가 작동(action)하는지 확인할 수 있다. 입력(type)할 때 녹색 숫자를 사용하여 입력 한 문자(entered characters) 수를 표시한다. 단, 문자 수(count)가 3 개 미만인 경우, 빨간색으로 바뀐다(turn).
Name counter Toggle Control
다음은 새로운 구성 요소(component)이다. 토글(toggle)은 켜짐(on) 또는 꺼짐(off) 상태(state)를 가질 수있는 부울(Boolean) 컨트롤(control)이다. 이 등록(registration) 양식(form)에서 사용자(user)가 자신 이름의 저장(save) 여부를 선택하도록 할 수 있다. 이는 많은 웹 사이트에서 볼 수있는 "Remember me" 확인란(checkbox)을 연상(reminiscent)시킨다.
Toggle
생성자(initializer)는TextField
에 사용된 것과 유사(similar to)하다. 생성자(initializer)는 바인딩(binding)과 레이블 뷰(label view)를 사용한다 :public init( isOn: Binding<Bool>, @ViewBuilder label: () -> Label )
바인딩(binding)의 경우
RegisterView
의 상태(state) 속성(property)을 사용할 수 있지만, 다른 뷰(views)에서 접근(accessed)할 수 있는 위치에 저장(store)하는 것이 좋다.UserManager
클래스(class)는 이미 해당 목적(purpose)을 위한settings
속성(property)을 정의(defines)하도 있다.이전에 name 문자 수(counter)에 대해 추가한
HStack
뒤와Button
앞에 다음 코드를 추가한다:HStack { Spacer() //왼쪽에 간격을 추가하고, toggle을 오른쪽으로 밀어 정렬하려면 Spacer가 필요하다. Toggle(isOn: $userManager.settings.rememberUser) { //$userManager.settings.rememberUser에 바인딩된 Toggle을 생성한다. Text("Remember me") //구성 요소 자체 앞에 표시되는 label .font(.subheadline) .foregroundColor(.gray) //label의 기본 스타일을 변경한다. } .fixedSize() //toggle이 이상적인 크기를 선택하도록 한다. //이것이 없으면, toggle은 사용가능한 모든 공간을 차지하면서 수평으로 확장하려 한다. }
Form toggle 이 변경(change)만으로는 토글 상태(toggle state)를 속성(property)으로 저장(storing)하는 것 외에, 실제 앱(app)에 기능(functional)을 추가하지 않는다.
registerUse()
구현(implementation)을 다음으로 바꾼다(replace):func registerUser() { if userManager.settings.rememberUser { //user가 선택한 기억 여부를 확인한다. userManager.persistProfile() //yes 인 경우, profile을 계속 유지한다. } else { userManager.clear() //no 인 경우, user defaults를 제거한다. } userManager.persistSettings() userManager.setRegistered() //설정을 저장하고, 등록된 user를 표시한다. }
적용된 것을 확인하려면, 앱(app)을 실행(run)해야 한다. 처음 실행(run)하면 사용자 프로필(user profile)이 저장(stored)되지 않는다. name을 입력(enter)하고, "Remember me" 토글(toggle)을 활성화(enable)한 다음 OK를 누른다(press). 다음 번에 앱(app)을 시작(launch)하면 입력한(entered) name으로
TextView
가 미리 채워(prefill)진다.Other controls
SwiftUI를 경험(encountered)하기 전에 iOS 또는 macOS 용으로 개발(developed)한 적이 있다면, 지금까지 설명한 컨트롤 외에 몇 가지 다른 컨트롤(controls)이 있다는 것을 알고 있을 것이다. 이 섹션(section)에서는 이에 대해 간략하게(briefly) 배우지만, 실제(practical) 적용(application)은 하지 않는다. 그렇지 않으면(otherwise), 이미 꽤 긴 이 장(chapter)이 너무 길어지게 된다.
Slider
슬라이더(slider)는 지정된(specified) 범위(range) 내에서 자유롭게 이동할 수 있는 커서(cursor)를 사용하여, 특정(specific) 증분 단위(increments)로 숫자 값(numeric value)을 선택할 수 있도록 한다.
선택할 수 있는 생성자(initializers)는 여러 가지(several)가 있지만, 가장 많이 사용되는 것은 다음과 같다:
public init<V>( value: Binding<V>, in bounds: ClosedRange<V>, step: V.Stride = 1, onEditingChanged: @escaping (Bool) -> Void = { _ in } ) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint
각각은 다음과 같다:
value
: 값 바인딩(value binding)bounds
: 범위(range)step
: 각 단계(step)의 간격(interval)onEditingChanged
: 편집(ediging)이 시작되거나 끝날 때 호출(called)되는 선택적 클로저(optional closure)
실제 사용 예제(example)는 다음과 같다:
@State var amount: Double = 0 ... VStack { HStack { Text("0") Slider( value: $amount, in: 0.0 ... 10.0, step: 0.5 ) Text("10") } Text("\(amount)") }
Slider 이 예제(example)에서 슬라이더(slider)는
amount
상태 속성(state property)에 바인딩(bound)되어 있고, 0에서 10 사이의 간격(interval)으로 구성(configured with)되며 0.5 단위(steps)로 증가(increments) 및 감소(decrements)한다.HStack
은 슬라이더(slider)의 왼쪽(left)과 오른쪽(right)에 각각(respectively) 최소값(minimum)과 최대값(maximum)을 지정(specifying)하는 두 개의 레이블(labels)을 추가하는 데 사용된다.VStack
은 슬라이더(slider) 아래 중앙에Text
컨트롤(control)을 배치하여, 현재 선택된 값(currently selected value)을 표시(displaying)하는 데 사용된다.Stepper
Stepper
는 개념적으로(conceptually)Slider
와 유사(similar to)하지만, 슬라이딩 커서(sliding cursor) 대신 두 개의 버튼(buttons)을 제공(provides)한다. 하나는 증가(increase) 버튼이고, 다른 하나는 컨트롤(control)에 바인딩(bound)된 값(value)을 감소(decrease)시키는 버튼이다.몇 가지 생성자(initializers)가 있지만, 가장 일반(common)적인 것은 다음과 같다:
public init<S, V>( _ title: S, value: Binding<V>, in bounds: ClosedRange<V>, step: V.Stride = 1, onEditingChanged: @escaping (Bool) -> Void = { _ in } ) where S : StringProtocol, V : Strideable
다음과 같은 인수(arguments)를 사용한다:
title
: 일반적으로(usually) 현재 바인딩(bound)된 값(value)을 포함(containing)하는 제목value
: 값 바인딩(value binding)bounds
: 범위(range)step
: 각 단계(step)의 간격(interval)onEditingChanged
: 편집(ediging)이 시작되거나 끝날 때 호출(called)되는 선택적 클로저(optional closure)>
사용 예제(example)는 다음과 같다:
@State var quantity = 0.0 ... Stepper( "Quantity: \(quantity)", value: $quantity, in: 0 ... 10, step: 0.5 )
Stepper SecureField
SecureField
는 기능적(functionally)으로TextField
와 동일(equivalent)하지만, 사용자 입력(user input)을 숨긴다(hides)는 점이 다르다(differing). 따라서 비밀번호(passwords)와 같은 민감한(sensitive) 입력(input)에 적합(suitable)하다.몇 가지 생성자(initializers)를 제공(offers)하며, 그 중 하나는 다음과 같다:
public init<S>( _ title: S, text: Binding<String>, onCommit: @escaping () -> Void = {} ) where S : StringProtocol
앞에서 설명한 컨트롤(controls)과 유사하게(similar to) 다음과 같은 인수(arguments)를 사용한다:
title
: 아무런 입력을 하지 않은 경우, 컨트롤(control) 내부(inside)에 표시(displayed)되는 자리 표시자(placeholder) 텍스트(text)text
: 텍스트 바인딩(text binding)onCommit
: 사용자가 Return 키(key)를 누르는(pressing) 등 커밋 작업(commit action)을 수행(performs)할 때 호출(called)되는 선택적 클로저(optional closure)
비밀번호(password)를 입력(entering)하는 데 사용하려면, 다음과 같이 작성한다:
@State var password = "" ... SecureField.init("Password", text: $password) .textFieldStyle(RoundedBorderTextFieldStyle())
Password empty Password entered Key points
긴 장(chapter)이었다. 이 장(chapter)에서는 SwiftUI에서 사용할 수 있는 많은 "basic" UI 구성 요소(components)뿐 아니라 다음과 같은 사실도 배웠다:
- 리팩토링(refactoring) 및 재사용 뷰(reusing views)는 결코 소홀히(neglected) 하거나 잊어서는(forgotten) 안 되는 두 가지 중요한(important) 측면(aspects)이다.
ViewModifier
를 사용하여 고유한(own) 수정자(modifiers)를 만들 수 있다.- 사용자 입력(user input)을 처리(handle)하려면
TextField
구성 요소(component)를, 또는 민감한(sensitive) 입력(input)의 경우SecureField
를 사용한다. - 키보드(keyboard)가 표시(displayed)될 때,
TextField
가 겹치지(overlapping) 않도록 주의해야 한다. 이를 위해 알림 센터(Notification Center)와 키보드(keyboard) 높이(height)를 사용할 수 있다. - 버튼(buttons)은 UIKit/AppKit에 비해 더 유연(flexible)하며, 모든 뷰(views) 컬렉션(collection)을 버튼(button)으로 만들 수 있다.
- 단순히 규칙(rules)을 설정하면 상태(state)가 변경(changes)될 때 SwiftUI가 해당 규칙(rules)을 적용(applying)하기 때문에, SwiftUI에서는 입력 유효성 검사(validating input)가 훨씬 간편하다.
- SwiftUI에는 토글(toggles), 슬라이더(sliders), 스테퍼(steppers)와 같은 사용자 입력(user input)을 처리(handle)하는 다른 컨트롤(controls)이 있다.
'Raywenderlich > SwiftUI by Tutorials' 카테고리의 다른 글
Chapter 8: State & Data Flow — Part I (0) 2021.04.12 Chapter 7: Introducing Stacks & Containers (0) 2021.04.08 Chapter 5: Intro to Controls: Text & Image (0) 2021.04.07 Chapter 4: Testing & Debugging (0) 2021.04.05 Chapter 3: Diving Deeper Into SwiftUI (0) 2021.03.19