iOS/SwiftUI

#7 Controls & User Input

HaningYa 2020. 10. 21. 14:45
728x90

이전 장에서 Text와 Image를 배워봤다면 이번장에서는 자주 사용하는 control 에 대해서 배워본다
(TextField, Button, Stepper)

A simple registration form

이름 적는 등록 화면을 만들어 볼 것이다.

A bit of refactoring

대부분의 경우 재사용성과 코드의 간결함을 위해 리팩토링 작업이 필요하다.
그래서 WelcomView 리팩토링 부터 시작한다.

배경화면 이미지 컴포넌트화

배경화면 이미지를 담을 새로운 SwiftUI view 파일을 만들고 배경 코드를 복붙한다.

그리고 다시 ContentView 에서 중복되는 코드 부분을 방금 만든 컴포넌트로 대체한다.

두가지 리팩토링을 더 진행해봐라

  • LogoImage.swift : Welcome view 의 아이콘 컴포넌트
  • WelcomeMessageView.swift : Welcome view 전체 (아이콘 + 텍스트)

Refactoring the logo image

동일하게 진행

Refactoring welcome message

Extract Subview 방법으로 진행
리팩토링 하고자 하는 container view 에 command + click 후 Extract subview

Extract 하고난 이후 코드

  • Xcode는 자동으로 view 이름을 edit mode로 해주어 바로 새로운 이름을 지정할 수 있다. WelcomeMessageView로 이름 바꿔준다.
  • 그 다음 WelcomeMessageView.swift 이름으로 SwiftUI view 파일을 컴포넌트 그룹에 추가해준다.
  • 방금 extract subview 된 코드를 body에 넣어준다.

2가지 방식으로 view 리팩토링을 진행해 보았다. 

*수정: WelcomeMessageView 에 logo 도 포함되게 수정

Creating the registration view

RegisterView.swift 파일을 새로 만든다.
그리고 방금 만든 컴포넌트인 WelcomeMessageView()와 WelcomeBackgroundImage() 를 적용한다.

SceneDelegate 에서 root view 를 바꿔준다.

Power to the user: the TextField

  • VStack으로 TextField 와 같은 control 을 추가해 나갈 것이다.
  • TextField 는 control 로써 사용자에게 (키보드를 사용해) 데이터를 입력받게 해준다. 
  • title 과 text binding 만으로 간단한 TextField 를 만들 수 있다.
  • title: placeholder 역할로 입력창이 비어있을 때 표시되는 문자열이다.
  • binding: 입력되는 텍스트와 textfield 프로퍼티 사이 양방향 커넥션을 담당하는 managed property 이다.
    (9장에서 binding 에 대해 더 배우게 된다.)

Binding 을 만드려면

  1. property 에 @State attribute를 추가한다.
  2. property 에 $ prefix를 붙여 property value 대신 binding 을 전달 하도록 한다.

그러나 TextField 가 표시되지 않는 이슈가 생긴다.

그 원인은 background image가 .fill 로 설정되있어 (이미지 원본 비율을 맞추면서) 수직상의 parent view를 꽉 차게 하기 위해 수평으로는 화면 경계를 넘어가기 때문이다.

해결하기 위해서는 ZStack을 사용하지 말고 실제 배경이 필요한 컨텐츠 뒤에 VStack을 사용해 .background modifier로 배경을 지정해야 한다.

*UIKit 에서 view 는 backgroundColor property가 있어 배경 색을 넣어줄 수 있지만 SwiftUI에서는 .background modifier가 Color, Image, Shape 과 같은 View 타입모두를 accept 한다.

배경이 작아지는 걸 볼 수 있는데 왜냐하면 배경이 지정된 VStack은 스크린 전체를 사용하지 않고 파란색 라인으로 표시된 컨텐츠를 표시하기위한 공간만을 사용하기 때문이다.

문제를 해결하기 위해서 Spacer()를 사용한다. (안드로이드 linearlayout 에서 weight 주는것과 비슷한 것 같다.)
*Spacer() 에 대한 자세한 내용은 다음장에서 배운다.

parentview 꽉차게 늘려진다.

Styling the TextField

padding 과 border 를 통해 스타일을 적용해 보자.

현재 SwiftUI 는 4가지 TextField 스타일을 제공한다.

  • No style : 따로 타입으로는 있지만 DefaultTextFieldStyle과 동일하다.
  • DefaultTextFieldStyle : 입력창만 있다.
  • PlainTextFieldStyle : 입력창만 있다.
  • RoundedBorderTextFieldStyle : 보더가 생긴다.
  • SqureBorderTextFieldStyle : 맥에서만 사용 가능

커스텀 스타일을 적용하기 위한 3가지 방법

  • TextField 에 필요한 Modifier 를 적용한다.
  • 커스텀 text field style을 만들어 TextFieldStyle 프로토콜을 준수하는 Type으로 정의한다.
  • custom modifier를 만들어 ViewModifier 프로토콜을 준수하는 Type으로 정의한다.

3가지 방법 모두 직/간접 적으로 modifier들을 순서대로 적용한다. 그래서 시작은 첫번째 방법으로 하는 것이 좋다.

  1. TextFIeld 를 만든다.
  2. 수직 수평 padding 값을 준다.
  3. 불투명한 흰색 배경을 적용한다.
  4. 경계선을 위한 overlay를 만들고 둥근 모서리를 적용한다.
  5. Stroke 를 통해 경계선만 보여지고 뒤에 컨텐츠가 보일 수 있게 만든다.
  6. 경계선 색을 정한다.
  7. 그림자를 추가한다.

여백을 위한 padding 을 추가한다.

Creating a custom text style

custom text style은 TextFieldStyle 프로토콜을 준수해야 한다.

프로토콜이 필요하는 함수는 이것 뿐이다.

아까 Textfield 에 적용한 style 을 configuration 에 modifier으로 달아준다.

그리고 TextField 에 직접 달았던 modifier 를 삭제하고 .textFieldStyle을 modifier 만을 적용한다.

그럼 커스텀 TextField 스타일을 만들어 적용이 끝났다.

스타일은 동일하다.

Creating Custom modifier 

이전에는 TextFieldStyle을 준수하는 custom text field 스타일을 만들었다면 이번엔 custom modifier를 만들어 보겠다.
custom modifier 가 custom textfield 보다 선호되는 이유는 custom modifier는 버튼에도 적용될 수 있기 때문이다.

Component group 에 BorderedViewModifier.swift 파일을 만들자.

  1. 파일의 preview section 을 지운다.
  2. View 대신 ViewModifier를 준수하도록 바꾼다.
  3. ViewModifier 가 필요로 하는 body함수를 구현해준다.

그리고 이전에 스타일로 적용했던 modifier들을 가져와 content의 modifier로 붙인다.

custom modifier 완성이다. 이 것을 적용하기 위해서는 ModifiedContent 라는 구조체를 사용하는데 2개의 생성 파라미터가 필요하다.

  1. The Content view
  2. The modifier

위와 같이 적용한다.

그런데 custom modifier를 적용할 때 마다 저렇게 사용하는건 멋지지 않다.
View 의 extension 에 custom modifier를 적용하는 함수를 만들어서 사용하면 편리하다.

extension을 CustomModifier에다 작성한다.

A peek at TextField's initializer

textfield 는 두쌍의 initializer를 가지고 있고 각각은 title 파라미터에 대한 localized과 non-localized 된 버전을 가지고 있다. 

이번 챕터에서 사용할 버전은 non-localized버전으로 title 과 editable text 에 대한 binding 두개를 필요로 한다.

    public init<S>(_ title: S, 
    text: Binding<String>, 
    onEditingChanged: @escaping (Bool) -> Void = { _ in }, 
    onCommit: @escaping () -> Void = {}) 
    where S : StringProtocol

 

나머지 2개의 파라미터는 따로 명시하지 않으면 빈채로 구현된다.

  • onEditingChanged: focus가 와서 Bool 파라미터가 true 가 될때나 focus 를 읽어서 Bool 파라미터가 false가 될 때 콜됨
  • onCommit: 사용자가 commit action을 취했을때 콜됨. (pressing return key) - 입력을 완료한 후 다음 textfield 로 자동으로 focus를 줄때 유용하게 쓰임

다른 initializer 를 보게되면 formatter가 있는 걸 볼 수 있다.

public init<S, T>(
_ title: S,
value: Binding<T>,
formatter: Formatter,
onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}
) where S : StringProtocol

이전 initializer 과의 차이점은

  • formatter 파라미터는 Foundation 클래스에 있는 Formatter 의 인스턴스이다. String을 제외한 나머지 타입 (숫자나 날짜) 에 대해서 custom formatter를 만들어 적용할 수 있다.
  • T 제네릭 파라미터는 String 이 아닌 타입에 대해서 처리할 수 있게 해준다.

Showing the keyboard

기본적으로 textfield 에 focus 가 가면 자동으로 software keyboard가 올라오게 된다.
하지만 키보드가 textfield 를 가리는지 않게 따로 처리해줘야 한다.

이를 위해서 가장 간단한 방법으로 구현된 코드가 Utils/KeyboardFollowers.swift 에 구현되어 있다. 
Notification Center를 이용해 keyboardWillChangeFrameNotification 이벤트를 subscribe 한다.
해당 이벤트는 keyboardHeight 프로퍼티, 키보드 높이값 를 가지고 있다.
이 높이값을 이용해 Textfield 를 가지고 있는 container view 의 아래쪽 padding 을 주어 항상 textfield 가 보여지도록 처리할 수 있다.


*observedObject 는 9장에서 공부하게 된다. 지금은 @State랑 비슷한 역할인데 custom class 에 적용된다고 생각하면 된다. 

프로퍼티 초기화를 통해 의존성을 주입한다. 
*의존성 주입이란 간단하게 initializer 를 통해 KeyboardFollower instance를 전달하는 것 이다.

 

[DI] Dependency Injection 이란?

디펜던시 인젝션, 의존성 주입에 대해 간단하게 작성해 봅니다.

medium.com

그리고 preview 와 sceneDelegate 의 RegisterView 에 파라미터에 추가한다.

거의다 끝났다. 이제 키보드 height 에 맞게 textfield container view 에 bottom padding 만 주면 된다.

*padding() 이 두번 들거간 걸 볼 수 있는데 잘못된 것이 아니다. 이런 방식으로 원래 한다. 결과적으로 padding 은 둘다 계산되서 합으로 보여지게 된다.

이렇게 SwiftUI 코드 한줄로 다음 규칙을 따르는 동적 padding 을 구현해보았다.

  • 키보드가 보여지지 않을때 keyboardHandler.keyboardHeight 는 0 이니 추가적인 padding 이 적용되지 않는다.
  • 키보드가 보여지는 경우 keyboardHandler.keyboardHeight 값이 padding으로 적용되어 textfield 가 키보드에 의해 절대 가려지지 않는다.

그런데 한가지 더 작업이 필요하다. 

keyboard의 경우 safe area를 포함하지 않고 bottom edge 에서 올라오는 반면에 padding 은 safe area로 부터 시작된다. 
그래서 edgesIgnoringSafeArea modifier를 통해 해결해야 한다.

결과

iOS14부터 keyboard avoidence 가 default 로 들어가있다.

 

Disabling Keyboard Avoidance in SwiftUI’s UIHostingController

UIHostingController has logic to avoid the keyboard, which is often unwanted. We explore a hack to disable this feature.

steipete.com

Taps and buttons

SwiftUI 버튼은 UIKit의 버튼보다 더 유연하다.
text label 만 사용하는 것 부터 이미지를 추가하는 것까지 다양하게 커스텀 할 수 있다.

선언을 보면 generaic type을 사용하는 걸 볼 수 있다.

위 generic type은 button의 visual content 이다. (View 타입이여야함)
그 말은 즉슨 Text나 Image와 같은 기본 컴포턴트부터 여러 Stack 과 text, image control 등을 합친 커스텀 View도 적용할 수 있다는 뜻이다.

    public init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)

생성자를 보면 두개의 파라미터를 요구한다. (사실 두개의 클로져)

  • action: trigger handler
  • label: button content

 label 파라미터에 적용된 @ViewBuilder attribute는 closure가 multiple child views를 리턴할 수 있게 해준다.

*tap handler parameter는 tap, tapAction 대신 action으로 불려지고 문서에는 trigger handler라고 정의되있다. (플랫폼별 부르는 명칭이 달라서)
*button initializer는 tap handler를 첫번째 파라미터로 받는다. (Swift 에서는 대부분 action closure 를 뒤에 위치시킨다.)
이 뜻은 trailing clousre 문법을 사용할 수 없다는 것 이다. 왜냐하면 SwiftUI 에서 항상 제일 뒷 파라미터는 view declaration 이기 때문이다. (view declartion 은 후행 클로져 문법을 사용한다.)

Submitting the form

inline closure를 사용할 수 있지만 view declaration code를 산개해서 작성하는 것은 피해야 하기 떄문에 trigger event 를 처리하기 위해 instance method를 사용해볼 것 이다.

이제 버튼클릭을 하면 유저 정보를 저장하는 기능을 만들어 볼 것 이다.

프로젝트 내에 있는 UserManager class 를 사용할 것인데 userdefaults를 사용하여 유저 정보를 저장하고 유저 와 유저 세팅을 복원하는 역할을 담당한다.

UserManager는 ObservableObject를 준수하기 때문에 view 에게 class 의 내용을 저달할 수 있다. instance state가 변할 때 view update를 trigger 한다.

이 class 는  @Published attribute로 profile, setting 두개의 외부로 노출된 프로퍼티가 있다. (view reload 를 trigger 하는 state를 나타냄)

그래서 RegisterView 에 있는 Name property 를 지우고 userManager instance 를 대신 사용한다.

@EnvironmentObject로 한 이유는 해당 instance 를 전체 앱에게 한번만 주입할 것이기 때문에 어디서든지 값을 받아오기 위해 environment로 선언한 것 이다. 
(ObservableObject와 EnvironmentObject는 추후 9장에서 더 자세하게 배운다.)

다음 TextField 에서 $name 레퍼런스를 $userManager.profile 로 바꿔주고 버튼 action 에 print 대신 userManager.persistProfile() 로 수정한다.

UserManager의 인스턴스를 RegisterView_Previews 에 주입한다. 


그럼 Preview 는 잘되는데 Simulater에서는 crash 가 난다. 해결하기 위해 SceneDelegate 에도 주입한다.

Styling the button

버튼을 꾸며본다.

  1. 여러 childview 를 가질 수 있게 HStack 으로 감싼다.
  2. 체크박스 아이콘 추가
  3. 아이콘을 중앙정렬하고 16*16 사이즈로 만든다.
  4. label 폰트와 bold 속성을 준다.
  5. .bordered custom modifier 을 통해 둥근 모서리와 경계선을 준다.

Reacting to input: validation

사용자가 입력할때 반응형 인터페이스를 제공하는 기능을 만들어 본다.

두가지 유용한 용도로 쓸 수 있다.

  • 입력하는 도중 data의 validation 을 확인
  • 현재까지 몇글자를 입력했는지 보여주는 기능

UIKit 에서 이러한 기능을 지원하려면 delegate를 사용하거나 value change event 에 대해서 subscribe 했어야 했다. 
SwiftUI는 논리적 표현식을 modifier에 직접 전달해서 status 가 바뀔 때 마다 view 가 render 될때 버튼을 disable 시킬수 있다는 점이다.

disabled modifier는 View protocol 에 속해 있어 아무 view 에나 적용 가능하다. 
view 가 상호작용 가능한지 안한지에 대한 파라미터 한개만을 사용한다.

사용자가 TextField 에 이름을 입력할때 userManager.profile.name 프로퍼티가 바뀌게 되고 view update 를 trigger 하게 된다. 
그때 button 이 다시 rendering 될 때 disabled() 표현식도 다시 평가되어 자동으로 validation에 따라 버튼을 enable, disable 시킬 수 있게 된다.

*이 게임에서는 최소 3글자 이상의 이름을 입력하라는 validation rule 이 있다.

disabled 상태
enabled 상태

Reacting to input: counting characters

몇글자나 입력되었는지 counting 을 유저에게 보여주는 기능을 만든다.

Toggle Control

Toggle initializer 는 TextField 와 비슷하게 binding 과 label view 를 인자로 받는다.

binding의 경우 RegisterView 가 가지고 있는 state property를 사용해도 되지만 다른 view 로 부터 접근 가능하게 저장하는 것이 좋기 떄문에 UserManager class 에서 이미 settings를 목적에 맞게 정의해 두었다. (ObservableObject)

그리고 registerUser 함수에 관련 된 동작을 작성한다.

이로써

  1. 사용자를 기억할지 않할지에 따라
  2. 기억한다고 하면 profile 을 유지 (persistProfile)
  3. 안한다 하면 user default 를 초기화
  4. setting 을 저장하고 user를 register 된 상태로 변경

 

Other controls

다른 control 들에 대해 간단하게 배워본다.

Slider

  1. value: binding 하는 값
  2. bounds: 범위
  3. step: 증감소 단계
  4. onEditingChanged: editing이 시작하거나 끝날때 call 되는 optional closure

Stepper(Slider 와 쓰임새 비슷함)

  1. title: 현재 bound value 를 포함하는 제목
  2. vlaue: binding 하는 값
  3. bounds : 범위
  4. step: 단계
  5. onEditingChanged: editing이 시작하거나 끝날때 call 되는 optional closure

SecureField

TextField와 기능은 같으나 입력값을 가려준다는 특징이 있음

  1. title: placeholder
  2. text: binding 되는 텍스트
  3. onCommit: optional closure, return 키 눌렀을때 call 됨

Keypoint

SwiftUI 의 기본 컴포넌트에 대해서 배워봤다,

  • refactoring 과 reusing 은 중요하다.
  • ViewModier를 통해 직접 modifier를 만들 수 있다.
  • 사용자 입력을 처리하기 위해 TextField 나 SecureField 를 사용하면 된다.
  • keyboard가 textfield 가리지 않게 해라 (iOS14 에서는 자동으로 된다.)
  • SwiftUI 의 Button 은 UIKit Button 보다 더 확장성이 좋다.
  • 입력값을 Validating하는것도 배워봤다. 
  • 그 밖에 toggles, sliders, steppers 등이 있다.

참고할만한 링크

View, COntrols Documnetation

 

Apple Developer Documentation

 

developer.apple.com

 

WWDC19 SwiftUI Essentials

 

SwiftUI Essentials - WWDC 2019 - Videos - Apple Developer

Take your first deep-dive into building an app with SwiftUI. Learn about Views and how they work. From basic controls to sophisticated...

developer.apple.com

 

728x90