티스토리 뷰

반응형

 

1편 글에 이어서 2편 글을 이어나갈게요!

 

SwiftUI에서 UI 업데이트하기

SwiftUI의 View에서 프로퍼티 값을 변경할 경우, @State를 마킹해서 "변경이 필요한 상태 값"이라는 것을 표시해요.

 

UIKit의 경우 특정 객체의 상태를 변경해주기 위해 그 객체에 접근해서 메서드를 통하거나 직접 그 프로퍼티의 값을 변경했었어요.

이 과정에서 타이밍에 따라 의도하지 않은 결과가 나오거나 버그가 발생하는 등의 문제가 발생하기 쉬워서 고려해야 할 게 많아요.

 

SwiftUI의 경우 @State로 마킹된 프로퍼티의 값이 변경될 경우, 그 뷰를 무효화(invalidate)하여 숨겼다가 body를 다시 계산해서 보여줘요.

UIKit의 경우 어떤 값을 보여주다가 그 값을 갱신할 경우 기존의 UI에서 reload 하는 동작을 추가했어야 했고, 이 과정에서 문제가 발생할 수 있었지만 SwiftUI에서는 이 무효화를 통해 값 경신을 보다 편리하게 만들었다고 보면 될 것 같아요.

 

예를 들어, 앱 시작시 보여주고 싶은 화면에서 변경될 프로퍼티 값을 가져야 한다면 아래와 같이 표현할 수 있어요.

struct ContentView: View {
    @State var game = Game()
    @State var guess: RGB
    
    ...
}

 

코드 편집기 활용하기

SwiftUI에서 코드 편집기를 사용하면 스토리보드처럼 편하게 개발할 수 있어요.

스토리보드와 다른 점은 SwiftUI의 경우 코드 편집기를 사용하면 그 변경사항이 코드에 바로 변경된다는 거예요! 

코드 편집기(Code Editor)

 

코드 편집기는 Control + Option + 클릭으로 열 수 있어요.

 

바인딩(Binding)

바인딩이라는 용어는 RxSwift나 Combine처럼 Reactive Programming을 경험했다면 익숙한 용어예요!

SwiftUI에서는 Stream을 만들어 바인딩을 하는 것이 아니라 "$" 표시를 통해 바인딩해요.

 

프로퍼티 guess가 있을 때, guess가 가진 요소 red에 접근한 guess.red는 단순히 읽기 전용이에요.

guess.red 그 자체는 단순한 특정 값을 의미해요.

하지만 $guess.red는 읽기 및 쓰기가 가능해요.

즉, $를 붙임으로써 단순 읽기 전용 프로퍼티가 쓰기도 가능하게 만들어주고, 이를 통해 변화를 감지할 수 있는 바인딩을 해준다고 할 수 있어요!

 

예를 들어, 입력받은 값을 단순히 출력만 해주는 로직의 경우에는 $가 필요 없어요!

read만으로 충분하기 때문이죠.

...
    Text(
        "Color: \(guess.red)"
    )
...

 

하지만 guess라는 프로퍼티가 가진 red값을 변경할 수 있기 위해서는 아래와 같이 $를 표기해요.

...
    Slider(value: $guess.red)
        .accentColor(.red)
...

 

여기서 중요한 것은 위에서 guess.red를 읽기 및 쓰기로 넘겨받고 있는 객체 Slider에서 value를 어떻게 정의하는지에요.

struct Slider: View {
    @Binding var value: Double
}

 

 

Slider라는 객체가 value를 직접 소유하고 있는 개념이 아니라 외부에서 읽기 및 쓰기로 주입받고 있는 구조이기 때문에 value 앞에 @Binding을 마크해줘요.

즉, 바인딩이 표시된 프로퍼티는 이 객체가 상태로서 직접 가지고 있는 프로퍼티가 아니라 외부에서 주입받고, 이 객체 내부에서의 변화가 외부에 전달됨을 알 수 있어요.

 

Subview 추출하기

SwiftUI의 편리한 기능 중 하나는 Subview 추출이에요.

IntelliJ의 경유 특정 메서드에서 부분 메서드를 추출(Extract)하는 기능이 잘 되어있어 자주 사용했지만, Xcode의 경우 잘 안 되는 경우가 많아 직접 구현했었어요.

 

SwiftUI의 View에서 특정 부분을 편리하게 Extract 하는 기능은 Command-클릭을 통해 할 수 있어요.

 

Modifier의 순서

Modifier는 이름 그대로 변경, 혹은 수정을 위한 코드예요.

어떤 요소의 값을 바꿔주는 역할을 하는 것으로 예를 들면 아래와 같은 코드가 있어요!

Text(guess.intString)
  .padding()
  .border(Color.purple)

 

어떤 Text가 있을 때, 그 텍스트에 패딩을 주고 경계 색깔을 바꿔주기 때문에 .padding.border는 Text의 Modifier라고 할 수 있어요.

그 결과물은 아래와 같아요.

출처: Raywenderlich

 

하지만 만약 순서를 변경하면 어떻게 될까요?

Text(guess.intString)
  .border(Color.purple)
  .padding()

 

당연할 수 있지만 결과가 바뀌어요.

Raywenderlich

 

위의 코드와 다르게 Padding을 주기 전 border color를 주기 때문에 Padding은 그 border 바깥에 정의되는 것이죠.

그래서 경우에 따라 순서가 중요하지 않은 케이스도 있을 수 있지만, 대부분의 경우 Modifier의 적용 순서는 중요해요.

순서에 따라 의도하지 않은 UI 결과물이 나올 수 있어요.

 

Group

HStack, VStack을 이야기하며 잠시 Group 이야기를 한 적이 있어요!

Group 역시 HStack, VStack과 마찬가지로 SwiftUI의 컨테이너예요.

요소들을 묶는다는 측면에서 H, V Stack과 같지만, Layout을 수행하지 않는다는 점이 달라요.

쉽게 말하면, HStack의 경우 Group과 마찬가지로 컨테이너 역할을 수행하지만 사용 즉시 요소들을 UI에 수평(Horizontal)이 되도록 배치시키는 반면(VStack의 경우 Vertical로 수직) Group은 요소들을 묶어주지만 그 요소들의 UI 배치에 영향을 주지는 않아요.

 

Preview 아이폰 사이즈 설정하기

1편 글에서 Preview를 통해 화면을 미리 볼 수 있다는 언급을 했었어요!

UI 작업을 할 때에 보통 여러 기기의 화면을 체크해봐야 하기 때문에 시뮬레이터를 변경하며 번거롭게 일일이 빌드를 했어야 했어요.

SwiftUI에서는 Preview의 기기 화면을 변경함으로써 쉽게 UI를 확인할 수 있어요!

struct ContentView_Previews : PreviewProvider {
  static var previews: some View {
    ContentView()
      .previewDevice("iPhone 8") // preview device 추가
  }
}

 

이름도 직관적으로 previewDevice이므로 가독성도 매우 좋은 것 같아요 :)

Device이름이 String이기 때문에 오류의 가능성이 있겠죠?

제가 사용한다면 enum으로 타입을 만들어서 사용할 것 같아요.

저 이름은 run destination menu에 있는 그대로를 사용해야 해요.

예를 들어, "iPhone SE"는 오류가 발생하기 때문에 "iPhone SE (2nd generation)"으로 설정해야 해요.

 

GeometryReader

UIKit으로 작업할 때 고민되었던 포인트 중 하나는 어떤 View의 사이즈를 파악하는 것이었어요.

명시적으로 사이즈를 지정해주는 경우도 있지만 intrinsic size처럼 코드 상에서 사이즈를 파악하기 힘든 경우도 있었어요.



SwiftUI에서는 GeometryReader를 통해 size와 frame 값들을 파악할 수 있어요.

GeometryReader에 대한 이야기는 추후에 더 자세히 다룰 예정이니 이번 글에서는 이런 게 있구나 정도로 넘어갈게요.



ContentView에서, ZStack을 Container에 넣으면 코드가 아래와 같이 돼요.

GeometryReader { proxy in
  ZStack {
    ...

 

GeometryReader는 GeometryProxy 객체를 제공하는데 이 객체는 frame 메서드들과 size, safeAreaInset 프로퍼티를 제공해요.

이 정보들을 위에서 proxy라는 이름으로 명명했어요.

일반적인 클로저의 지역 변수처럼 정의되어 있기 때문에 아래와 같이 사용할 수 있어요.

(NeuButtonStyle(
  width: proxy.size.width * buttonWidth,
  height: proxy.size.height * labelHeight))
  ...

 

3편에서 이어갈게요!

반응형
댓글