 Apple Lover Developer & Artist

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

 Apple/Stanford iOS Programming (SwiftUI)

Lecture 1 Review Part 2: Getting started with SwiftUI

singularis7 2021. 8. 5. 01:32
반응형

Swift UI Tour

ContentView.swift 전체 코드를 한 번씩 살펴보면 먼저 SwiftUI 패키지를 import 하고 있음을 볼 수 있다.

SwiftUI 는 iOS, Apple Watch, mac OS와 같은 Apple 생태계의 App UI를 설계할 때 사용된다. ContentView.swift 에는 UI를 프로그래밍하고 있기 때문에 Swift UI 가 필요하며 Model과 같은 logic에서는 import 할 필요가 없다. 실제로 Swift UI 장점 중 하나는 logic과 UI를 분리한다는 점이다.

Swift 문법 중 하나인 struct 키워드가 보인다. data structure 의 약자로 많은 프로그래밍 언어에서 본질적으로 Collections of Variable의 개념으로 존재하며 Swift의 경우 struct에서 함수를 포함할 수 있다.

어떤 사람들은 위와 같은 내용이 데이터와 변수가 서로 메서드로 encapsulate 되어서 객체지향프로그래밍과 닮았다고 생각할 수 있다. 그러나 struct 구조체는 변수와 함수를 가질 수 있지만 class 가 아니어서 상속과 같은 객체지향적 개념을 갖고 있지 않으며 대신 함수형 프로그래밍으로 기법으로 이어진다.

Swift UI 에서는 함수형 프로그래밍 디자인 모델을 사용하며 UI와 Model logic을 연결할 때 객체지향 디자인 모델을 사용하게 된다.

다시 소스 코드를 자세히 살펴보면 눈에 띄는 부분이 있다. 구조체 이름과 ":" 뒤에 딸려오는 View 의 정체는 무엇인가?

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}

우리가 만들고 있는 구조체가 View처럼 동작한다는 것을 의미한다. 강의의 설명이 직관적이지 않아서 공식 문서를 찾아보니 Swift의 Protocol 개념으로 설명되고 있다. 잠시 살펴보고 가자.

Protocol
A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements.
- Swift Official Tutorial 에서 발췌함

위와 같이 View 라는 Protocol을 사용 하하여 새로운 structure를 정의하면 View에 정의된 methods, properties 등 여러 요구사항을 만족시키는 구조체를 정의할 수 있다는 것이다. View는 어떤 동작을 하는 친구인지 간단하게 살펴보자!

View
A type that represents part of your app’s user interface and provides modifiers that you use to configure views.

View는 휴대폰 화면에 보이는 직사각형 영역을 의미한다. 기본적으로 Custom View를 정의하기 위해 Body를 반드시 Implement 해야 하며 대표적으로 다음과 같은 기능을 제공한다

  • Providing Interactivity
    • Input and Events : Supply actions for a view to perform in response to user input and system events.
    • View Presentation : Define additional views for the view to present under specified conditions.
    • State : Access storage and provide child views with configuration data.

화면에 보이는 모든 UI 구성은 SwiftUI 를 이용하여 위와 같은 View를 함수형 프로그래밍을 통해 구현하기 때문에 아주 강력한 친구이다! 이제 각종 UI Element 가 마치 View처럼 동작하는 예시를 살펴보자!

대표적으로 Text 컴포넌트에 padding 값을 설정하여 여백을 줄 수 있다. 외에도 Font 크기, 색상, 정렬 등의  기능도 전부 View 의 힘을 빌려왔기 때문에 가능한 일이다. 

계속 코드를 살펴보자 Custome View 를 정의하기 위해 View type을 활용하고 있고 기본 요구 사항인 View를 implement 함으로써 화면 구성을 정의하고 있다. 그중에 body의 타입이 눈에 띈다. Custom View 구조체를 View 를 사용해서 정의했는데 내부 변수에  some View type 은 무엇일까?

강의에서는 레고를 활용한 비유를 설명한다.

레고는 작은 건물을 쌓아 올리는 득 멋진 물건을 만드는데 사용되는 작은 기초 블록이다. 레고 집에는 여러가지 방이 존재할 수 있다. 주방, 침실, 욕실, 식당 등이 존재할 수 있다. 각 방을 구성하는 여러 다른 레고들이 존재할 수 있다. 주방에는 레고 주방 캐비닛, 레고 식탁, 레고 의자, 레고 가전제품 등으로 구성될 수 있을 것이다.

이렇게 생각해보면 레고 하우스는 다른 여러 레고들을 활용해서 만들어졌음을 이해할 수 있다. 더 나아가 수천 개의 레고 블록 조각으로 구성되었다고 표현할 수 있을 것이며 식탁이 "레고로 만들어졌는가"라고 질문을 던지면 참값이 나올 것이다. 마찬가지로 각 레고 조각을 합쳐서 탄생한 레고 하우스 또한 "레고로 만들어졌는가" 라고 질문을 던지면 참값이 나올 것이다.

작은 레고 블록으로부터 가구, 집도 만들 수 있고 심지어 레고 우주도 만들 것이다. 이와 마찬가지로 View처럼 동작하는 Content View를 정의하려면 어느 다른 View 조각 들이 모여서 만들어질 테니 나름 직관적인 설명인 것 같다.

그렇다면 View 는 어떤 요소로 구성할 수 있는가?

화면에 보이는 텍스트, 버튼, 이모티콘, 카드 등이 모두 View의 한 종류들이다. 그중에 가장 강력한 친구가 있는데 바로 "Combiner"이다. Combiner는 화면에 있는 여러 다른 View를  가져와 가로, 세로 혹은 격자 모양 등의 형태로 결합시키는 역할을 한다. 때문에 복잡한 Custome View를 정의할 때 Combiner를 유용하게 활용할 수 있기에 가장 중요하다. 

다른 종류에 View 도 존재한다 일종의 레고 가방? 모음? 으로 비유될 수 있는데 매우 큰 레고 세트를 구입했다고 생각해보자 새 레고 상자를 열어보면 레고 부품이 모두 뒤섞여 있는 게 아니라 친절하게 만들고자 하는 물체에 따라 필요한 레고 블록이 분류되어 지퍼백에 포장되어있을 것이다. 따라서 어느 물체를 만들기 위해 필요한 레고 블록을 하나하나 찾을 필요가 없다! 위에서 설명한 View Combiner 은 UI 에 있어서 마치 물체를 만들기 위해 포장되어있는 레고 가방과 같은 역할을 한다.

다시 돌아와서 some View타입이기도 하지만 실제로는 함수이다! 함수형 프로그래밍에서 함수는 매우 중요하며 함수는 First class citizen 이기 때문에 우리가 원하는 어느 곳이던지 위치시킬 수 있다. 사실 위 코드는 이름과 입력이 없고 Text 를 return 하는 함수였으나 Swift 문법에 의해 간결하게 표현된 형태였던 것이다!

struct ContentView: View {
    var body: some View {
        return Text("Hello, world!")
            .padding(.all)
    }
}

위에서 정의된 변수 body의 값은 실제로 메모리에 저장되지 않으며 실제로 함수를 실행하여 계산되는 변수이다. 어느 누군가가 ContentView야! body의 값은 무엇이니?라고 물어볼 때마다 매번 함수를 계산하여 결괏값을 도출한다는 의미이다. 위 코드의 경우 view처럼 동작하는 text를 반환하며 text를 사용할 수 있는 이유는 Swift Ui 어딘가에 View처럼 동작하는 text 구조체가 다음과 같은 형태로 정의되어있기 때문이다.

struct Text: View {
	var body: some View { ... }
}

some View 를 좀 더 엄밀하게 본다면 그 자체로 타입은 아니다. 컴파일러에게 이 변수의 계산 결과 타입은 View의 일부가 될 텐데 네가 한번 타입을 추론해서 처리해줘!라는 의미를 지니고 있다. 잠시 Swift 문서에서 opaque 타입에 관해 살펴보도록 하자!

Opaque Types
A function or method with an opaque return type hides its return value’s type information. 

some View 를 사용한다는 의미는 body의 타입이 View 프로토콜을 준수하는 일부의 타입이지만 이것이 무엇인지 세부적으로 명시하지 않는다는 의미를 지니고 있다. 왜 Swift 가 이렇게 처리하고 있을까? 위 코드와 같은 예시에서 단순하게 Text만 return 하는 경우에 body의 타입 또한 똑같이 Text로 명시하여도 정상적으로 동작한다.

struct ContentView: View {
    var body: Text {
        return Text("Hello, world!")
    }
}

하지만 Combiner 를 활용하여 View처럼 동작하는 다른 UI 컴포넌트를 구성하여 return 할 경우 사용자가 그 타입을 일일이 명시해준다는 것은 쉬운 일이 아니기 때문에 Swift에서는 컴파일러가 자동으로 타입을 추론하도록 하는 기능을 제공하여 문제점을 해결하고 있다.

이번에는 text 가 아닌 SwiftUI 에서 제공하는 다른 UI 컴포넌트를 사용해보자! RoundedRectangle 함수를 사용하면 모서리가 둥근 사각형 UI를 생성할 수 있다.

struct ContentView: View {
    var body: some View {
        RoundedRectangle(cornerRadius: 25.0)   
    }
}

위 코드를 살펴보면 Swift 문법의 특징이 보이기도 하는데 함수의 인자에 이름을 붙여줄 수 있다는 점이다. 이를 통해 사용자가 인자의 기능을 좀 더 명확하게 구분할 수 있으며 이와 같은 원리를 함수를 정의할 때 적용할 수 있다.

RoundedRectangle 도 text View 와 마찬가지로 View처럼 동작하기 때문에 padding, font 등 View 적인 요소를 설정하고 변경할 수 있다. 아래의 내용은 각종 View 적인 요소를 변경해본 예시이다.

외에도 추가적으로 View 처럼 동작하는 요소들의 설정값을 바꾸기 위해서는 Xcode 우측에 위치한 Inspector에 위치한 "Add Modifier" 기능을 활용하면 쉽게 찾아볼 수 있다! 

이제 UI 요소를 View 의 힘을 빌려서 수정하는데 익숙해졌다! 이번에는 카드 게임을 만드는데 필요한 UI를 Combiner를 통해 만들어 보도록 하자!

struct ContentView: View {
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 25.0)
                .stroke(lineWidth: 3)
                .padding(.horizontal)
                .foregroundColor(.red)
            Text("Hello World!")
                .foregroundColor(.orange)
                .padding()
        }
        
    }
}

ZStack을 활용하면 내부에 있는 View 요소들을 장치의 화면 쪽에서 사용자 방향으로 쌓아 나간다. Combiner에는 아주 재미있는 특징이 있는데 Combiner 에 어느 속성을 지정하거나 변경하면 내부의 컴포넌트에도 설정값이 상속되어서 동시에 변경된다.

위 예시는 ZStack 함수에 foregroundColor 를 빨간색으로 지정하여 내부 요소들이 기본적으로 빨간색으로 지정되었으나 text 컴포넌트의 경우 별도로 색상값을 주황색으로 지정해 주었기 때문에 위와 같은 결과를 보여주고 있다.

마지막으로 ZStack 을 관찰해보자 ZStack 은 함수이고 공식 문서에 따르면 인자로 Alignment를 지정해주어서 내부 UI 컴포넌트를 정렬해 줄 수 있는데 왜 저렇게 기이한 형태로 쓰는지 궁금할 수 있다. 사실 원래 코드는 아래와 같다.

struct ContentView: View {
    var body: some View {
        return ZStack(alignment: .center, content: {
            RoundedRectangle(cornerRadius: 25.0)
                .stroke(lineWidth: 3)
            Text("Hello World!")
                .foregroundColor(.orange)
        })
        .padding()
        .foregroundColor(.red)
    }
}

Swift의 경우 코드의 미적 요소를 위해 마지막 인자에 함수가 들어가는 경우 파라미터에 밖으로 꺼내 올 수 있다. 

struct ContentView: View {
    var body: some View {
        return ZStack(alignment: .center) {
            RoundedRectangle(cornerRadius: 25.0)
                .stroke(lineWidth: 3)
            Text("Hello World!")
                .foregroundColor(.orange)
        }
        .padding()
        .foregroundColor(.red)
    }
}

만약에 입력값이 존재하지 않을 경우에는 마찬가지로 입력 부분의 괄호를 생략할 수 있다.

struct ContentView: View {
    var body: some View {
        return ZStack {
            RoundedRectangle(cornerRadius: 25.0)
                .stroke(lineWidth: 3)
            Text("Hello World!")
                .foregroundColor(.orange)
        }
        .padding()
        .foregroundColor(.red)
    }
}

마지막으로 return 까지 생략하면 우리가 본 원래의 코드가 완성되는 것이다. 정말 간결하고 잘 만들어진 언어인 것 같다!

struct ContentView: View {
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 25.0)
                .stroke(lineWidth: 3)
            Text("Hello World!")
                .foregroundColor(.orange)
        }
        .padding()
        .foregroundColor(.red)
    }
}

첫 강의 부터 이해하기 어려운 내용들이 정말 많다! 새롭게 경험한 함수형 프로그래밍 사고가 아직 나에게 익숙하지 않다. 계속 노력해봐야겠다.

반응형