 Apple Lover Developer & Artist

영속적인 디자인에 현대의 공감을 채워넣는 공방입니다

 Apple/Stanford iOS Programming (SwiftUI)

Lecture 2 Review Part 1: Learning more about SwiftUI

singularis7 2021. 8. 7. 01:17
반응형

지난 시간 회고해보기!

지난 수업에서는 View Protocol 의 힘을 빌려 Hello world 텍스트에 padding 값을 주고 색상도 바꿔보고 combiner 를 활용하여 여러가지의 UI 컴포넌트를 합쳐보기도 하였다. Swift UI 를 통해 새로운 함수형 프로그래밍을 경험해보기도 했고 간단한 Swift 구조체 문법을 살펴보기도 했다.

지난 시간 배웠던 개념들에서 한걸음 더 들어가보자!

HStack
A view that arranges its children in a horizontal line.

ZStack 이 UI 컴포넌트를 장치의 화면쪽에서 사용자 방향으로 쌓아가는 Combiner 였다면 HStack 은 장치의 Horizental 방향으로 UI 컴포넌트를 나열해주는 역할을 한다.  위의 예시 코드를 읽다보면 한가지 의문점이 들 수 있다! 

극적으로 생각해서 카드가 수백개 존재할 때는 어떻게 처리하려고 그래?
너무 그지 같은 코드야!!!

앞에서 우린 레고 블록 비유를 통해 UI 를 조립해나가는 개념을 보았으니 마찬가지로 ZStack을 활용해서 카드 레고를 만들면 되지!!! 당장 Card view struct 를 정의해보자!

전보다 코드가 깔끔해졌다! 구조도 직관적으로 이해가 간다. 실제로 이러한 방식이 Swift UI 프로그래밍에 핵심이라고 한다. 비유하자면 레고로 탁자, 가전, 가구 를 만들어서 방을 만들고 방을 모아서 집을 만들고 ... 좀 더 추가하자면 역시 View Protocol 의 지원사격을 받고 있으니 마찬가지로 padding foregroundColor 과 같은 UI 설정값도 수정할 수 있는 방식으로 프로그래밍해간다는 의미이다!

이번에는 Text 컴포넌트에 "Hello World" 만 보여주는 것은 카드게임 같지도 않고 무척 지루해보일 수 있다. 다른 것을 띄어보자!

Swift 는 String 값으로 무려 "이모티콘" 을 받아올 수 있고 심지어 Swift UI 를 통해 화면에 이모티콘을 띄어줄 수 있다!

우리의 코드는 또 다른 문제가 있다! 최근 Apple 기기에 이른바 Dark Mode 라는 새로운 테마 기능이 추가 되었는데 우리의 앱은 다크 모드에 대비한 기능이 하나도 존재하지 않는다! 한번 우측에 보이는 Preview 창을 다크 모드로 설정하여 문제를 해결해보자!

다크 모드의 설정은 우측에 보이는 Inspector 에서 Color Scheme 설정값을 Dark 로 바꿔줌으로써 설정할 수 있지만 귀찮으면 텍스트 에디터 창에 보이는 것 처럼 선호하는 컬러 스키마 옵션을 .dark 로 직접 주어도 동작한다! 좀 더 나아가서 light 와 dark 모드를 동시에 볼 수도 있는데 ContentPreview 창을 하나 더 만들어 주면 된다! (단점은 맥이 느려진다는 것 ㅠㅠ)

이제 문제 상황이 명확히 보인다. 다크 모드에서 카드가 정상적으로 보이지 않는다는 점이다! 사실 당연한 일이다. 우리의 카드는 테두리만 보이고 카드의 배경은 투명해서 뒤에 있은 배경색을 비추고 있을 뿐이었느니,,, 해결법은 비교적 간단할 것 같다.

카드의 배경색 역할을 하는 UI 컴포넌트를 하나 더 쌓아 올리면 되겠네!

아제 카드가 잘보이고 있다. 하지만 카드 게임이 계속 내용물만 보여주고 있다면 과연 게임으로써 가치가 있을까? 카드의 뒷면을 보여주고 사용자가 알맞은 짝궁을 맞출 수 있도록 설계할 필요가 있는게 그 전에 어떻게 카드의 앞면과 뒷면을 구분할 수 있을지 고민해보자!

카드의 상태를 의미하는 isFaceUp 변수를 CardView 구조체에 추가해준 후 위 이미지 처럼 조건문을 추가해주면 된다. 근데 이런 방식으로 코딩하면 오류가 발생하게 되는데 이게 Swift 기본 규칙과 관련되어 있다.

Type Annotations
You can provide a type annotation when you declare a constant or variable, to be clear about the kind of values the constant or variable can store.

값이 없는 변수는 존재할 수 없다. 변수는 무조건 값을 갖고 있으며 변수가 생성된 이후 계속 값을 갖고 있어야 한다! 혼동하기 쉬운 개념으로 Swift에는 optional 이라는 개념이 존재한다. 사실 optional 은 인간의 눈에 보이기에 값이 없는 것 처럼 보일 수 있으나 엄밀하게 말하면 값이 설정되지 않은 것이다 

다시 돌아와서 위 문제를 해결하려면 어떤 방식으로 해결하던, 일단 boolean 타입의 초기값을 갖을 수 있도록 설정해주면 제대로 동작할 것이다. 다양한 사례를 살펴보자

// 함수의 실행 결과값을 받아온다
var isFaceUp: Bool { return true }
// 익숙한 방식의 변수값 설정 방법
var isFaceUp: Bool = true

// struct 에서 기본으로 제공하는 생성자 사용
struct ContentView: View {
    var body: some View {
        HStack {
            CardView(isFaceUp: true)
            CardView(isFaceUp: false)
        }
    }
}

struct CardView: View {
    var isFaceUp: Bool
    
    var body: some View {
        ZStack { ... }
    }
}

이렇게 구조체 변수에 대한 문제점은 해결된 듯 보이지만 여전히 우리의 코드는 뭔가 부족한 모습이 보인다. CardView 부분을 보면 가독성을 떨어트리고 반복되어 선언되는 문구가 존재한다 이것으로 말하자면 바로 RoundedRectangle 부분이다! 반복되는 부분을 로컬 변수로 따로 빼주면 코드를 아래와 같이 간결하게 개선시킬 수 있다.

struct CardView: View {
    var isFaceUp: Bool
    
    var body: some View {
        let shape = RoundedRectangle(cornerRadius: 25.0)
        ZStack {
            if isFaceUp {
                shape.foregroundColor(.white)
                shape.stroke(lineWidth: 3)
                Text("🥇")
                    .foregroundColor(.orange)
                    .font(.largeTitle)
            } else {
                shape
            }
        }
    }
}

여기서 잠시 Swift 문법 몇가지를 집고 넘어가는 시간을 가질 것이다. var 키워드는 자주 봐왔는데 let 란 놈은 무엇이고 둘 사이에는 어떤 차이점이 있는가?

var 은 말 그대로 variable 의 약자로 변할 수 있는 수, 가변적인 친구들을 담아야 할 때 사용한다. 반대로 C나 C++ 과 같은 다른 언어를 살펴보면 일종의 상수처럼 사용하는 변수를 정의할 때가 있다. 예를들면 const 키워드를 사용하는게 예시가 될 수 있을 것이다. Swift 에서의 let 은 변하지 않는수, 수정되지 않는 수를 표현할 때 사용된다. 실제로 위에서 let 키워드를 활용하여 정의한 shape 는 변하지 않는 친구이기 때문에 let을 통해 정의한 것이며 body나 isFaceUp 과 같은 수는 매번 값을 계산하거나 변경할 일이 생기기 때문에 var 을 활용하여 정의하고 있다.

또다른 의문점이 든다. 위에서 언급된 body나 isFaceUp과 같은 경우에는 type annotation 기능을 통해 변수의 타입을 명시하고 있는데 shape 는 왜 타입을 명시하지 않고도 동작하고 있는가? 

Type Safety and Type Inference
Because of type inference, Swift requires far fewer type declarations than languages such as C or Objective-C. Constants and variables are still explicitly typed, but much of the work of specifying their type is done for you.

Swift가 Type inference 기능을 제공해주고 있기 때문이다. 대부분의 경우에는 타입을 명시적으로 적는 경우가 많지만 특정 상황에서 이 기능이 유용하게 활용될 수 있을 것 같다는 생각이 들었다. 

let shape: RoundedRectangle = RoundedRectangle(cornerRadius: 25.0)
let shape = RoundedRectangle(cornerRadius: 25.0)

타입을 명시하지 않아도 프로그래머와 Swift 모두가 쉽게 변수의 타입을 이해할 수 있는 경우가 있는 것이다. 더 나아가 첫번째 코드는 중복되는 정보를 코딩하고 있기 때문에 가독성이 떨어지는 느낌이 들기도 하다.

다시 카드 게임을 만드는 본론으로 돌아와본다. 우리의 프로그램은 카드의 앞면과 뒷면을 보여주는 UI 까지는 완성되었지만 사용자의 이벤트에 응답하는 부분이 존재하지 않는다. 생각해보자 컴퓨터를 사용할 때에는 표준 입출력 을 활용하여 프로그램을 사용하듯이 스마트폰에도 입출력 부분에 해당하는 내용이 있지 않을까?

지난 강의에서도 언급했지만 이름이 SwiftUI라서 이 프레임워크가 UI 구성 기능만 제공하는 것은 아니었다. 사용자로부터 제스처 등과 같은 입력 처리도 할 수 있는 친구이다! 우리의 목표는 카드를 터치하면 카드가 뒤집어 지는 것을 구현하는 것이니 방법을 찾아보자!

SwiftUI 에는 UI 컴포넌트에 터치 이벤트를 감지했을때 실행할 기능을 명시하는 이벤트 핸들러가 존재한다.

onTapGesture(count:perform:)
Adds an action to perform when this view recognizes a tap gesture.

터치 이벤트가 발생할 때 마다 카드뷰 구조체가 갖고 있는 isFaceUp 변수에 있는 값을 toggle 시키도록 코드를 작성하였다. 하지만 위와같이 오류가 발생한다! 무슨 의미일까?

Cannot use mutating member on immutable value: 'self' is immutable

struct 의 value 타입 특성에 의해 한번 인스턴스의 값이 결정되고난 후에는 수정할 수 없다. 추후 게임 로직을 구성하면서 이와 관련한 자세한 차이점을 이해해 보도록 한다.

그럼 단순히 UI 테스트를 할 때에는 toggle 기능을 시연해볼 수 없는 것인가? 아니다! 테스트 해볼 수 있다. var 앞에 @State 키워드를 붙이면 당장의 문제는 해결되기는 한다. 다만 혼동하면 안될 내용이 하나 있다. 저 키워드를 붙인다고 내 UI 구조체가 mutable 해진 것이 아니라는 점이다. 내 구조체는 한번 생성된 후에 여전히 수정할 수 없다는 특성을 지늬고 있다. 수정되는 순간 전혀 다른 값이 되어버린다. 하지만   isFaceUp 앞에 @State 를 붙여줌으로써 이 변수를 포인터로 사용할 수 있게된다. 

다시 정리해보면 isFaceUp 변수는 포인터인데, 포인터는 변하지 않기 때문에 UI 구조체 또한 변하지 않으며 포인터가 가리키는 메모리에 저장되어있는 값을 바꾸면 마치 게임 로직 class 를 구현한 후에 UI를 테스트 하는 것과 비슷한 상황이 된다는 것이다. 어쨋든 결과는 정상적으로 돌아가게 될 것이다. 

오늘 강좌를 30:20 까지 들었는데, C++ 만 자주 사용하다보니 복사를 방지하려고 포인터를 다루는 등의 형태에 아주 익숙해져있었는데 Swift 를 보면 이런 내용을 어떻게 통제할 수 있을까 궁금했었다. 어찌보면 class 와 struct 의 차이점을 이해해 보면 내가 고민하던 문제점을 해결하는 열쇠가 될 수 있지 않을까? 

Part 2 에서 봅시다!

반응형