공식문서 보고 SwiftUI 따라하기 (1)

Yang Si Yeon·2021년 2월 22일
0

SwiftUI

목록 보기
1/5

SwiftUI를 공부할 일이 생겼다!
애플에서 작성된 공식문서를 보고 배우는게 제일 좋을 것 같아서 보고 따라해보려고 한다.

그런데 공식 문서가 영어로 되어 있다 보니,, 머리속에 잘 집어넣고 모를 땐 다시 꺼내보기 위해 블로그에 정리 하면서 공부해봐야지 !

공식문서를 보면서 만든 프로젝트 및 코드는 Github에 올려두었다. 🐱

개요

위 링크에 걸린 공식문서에 들어보면 애플은 SwiftUI를 아래와 같이 설명하고 있다.

SwiftUI는 Swift의 성능을 바탕으로 모든 Apple 플랫폼에서 사용자 인터페이스를 구축할 수 있는 혁신적이고 간소화된 방법입니다. 단 하나의 도구 구성 및 API를 통해 모든 Apple 기기에서 사용할 수 있는 사용자 인터페이스를 구축합니다. 읽기 쉽고 작성하기 편한 선언적 Swift 구문을 통해 SwiftUI는 새로운 Xcode 디자인 도구와 매끄럽게 연동되면서 코드와 디자인이 완벽하게 동기화되도록 합니다. 또한 유동적 글자 크기 조절, 다크 모드, 현지화 및 손쉬운 사용을 자동 지원하므로 SwiftUI 코딩 첫 줄부터 가장 강력한 UI 코드를 작성할 수 있습니다.

SwiftUI의 수많은 장점은 선언적 구문데이터 주도에서 비롯된다.

선언적 구문

선언적 구문은 화면을 구성하는 컴포넌트들의 레이아웃 모양과 이에 대한 디테일 내역을 직접 설계하지 않고, 단순하면서도 직관적인 구문을 이용해 화면을 기술할 수 있게 해준다.

SwiftUI는 선언적 구문을 사용하므로 사용자 인터페이스의 기능을 명시하기만 하면 됩니다. 예를 들어, 텍스트 필드로 구성된 항목의 목록을 작성한 다음 각 필드의 정렬, 서체 및 색상을 설명하면 됩니다. 코드가 그 어느때보다 간단하고 가독성이 향상되어 시간이 절약되고 유지 관리가 용이합니다.

import SwiftUI

struct Content: View {
	@state var model = Themes.listModel
    
    var body: some View {
    	List(model.items, action: model.selectItem) { item in
        	Image(item.image)
            VStack(alignment: .leading) {
            	Text(item.title)
                Text(item.subtitle)
                	.color(.gray)
                }
            }
        }
    }
}

이렇게 선언하고 나면 레이아웃의 위치와 제약조건, 렌더링 방법에 대한 사항들은 모두 SwiftUI가 담당하게 되며, SwiftUI를 이용하면 시뮬레이터나 실제 디바이스에서 앱을 컴파일하고 실행하지 않고, 프리뷰 캔버스 내에서 앱을 실행하고 테스트할 수 있다.

데이터 주도적(Data Driven)

SwiftUI에서의 데이터 주도적은 앱 내 데이터와 앱의 UI 및 로직 사이에 대한 관계를 의미하며, 앱의 데이터 모델과 UI 컴포넌트, 로직을 바인딩(binding)하는 여러 방법으로 복잡도를 해결한다.

만약 UI 컴포넌트와 데이터 모델이 바인딩된다면, 추가적인 코드를 작성하지 않아도 모든 데이터의 변경 사항을 SwiftUI가 UI에 자동으로 반영한다. (안드로이드의 데이터 바인딩과 비슷한 개념인 것 같다.)


Creating and Combining Views

해당 과정(?)은 SwiftUI Tutorials의 첫번째 챕터인 SwiftUI Essentials의 첫번째 과정이다.

튜토리얼 링크: https://developer.apple.com/tutorials/swiftui/creating-and-combining-views

이 튜토리얼은 좋아하는 장소를 찾고 공유할 수 있는 앱인 Landmarks를 만드는 과정이다. 랜드 마크의 세부 정보를 보여주는 뷰 부터 만들기 시작한다. Landmarks는 스택을 사용해 이미지와 텍스트뷰를 배치한며, 지도를 추가 하기위해 표준 Mapkit을 include 해야한다. 또한, 뷰의 디자인을 구체화할 때 Xcode에서 실시간 피드백을 제공하므로 변경사항이 코드로 어떻게 변환되는지 확인할 수 있다.

(위처럼 번역기로 돌린듯한 해석에서 나의 부족한 영어 실력을 엿볼 수 있다..)

Section 1. 새 프로젝트를 만들고 Canvas 살펴보기

Step 1
새로운 Xcode 프로젝트를 만들어주자.

Step 2
플랫폼으로 iOS를 선택하고 App template를 선택한 뒤 Next 클릭!

Step 3
Project Name: Landmarks
Interface: SwiftUI
Life Cycle: SwiftUI App
위와 같이 설정해주자.

Step 4
LandmarksApp.swift 파일에는 아래와 같은 코드가 작성되어 있다.

import SwiftUI

@main
struct LandmarksApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

SwiftUI App 라이프사이클을 사용하는 앱은 App 프로토콜을 채택하며, body 프로퍼티는 1개 이상의 display할 내용을 차례로 제공하는 scene를 반환한다. @main 속성을 통해 entrypoint를 식별할 수 있다.

Step 5
ContenView.swift 파일에는 아래와 같은 코드가 작성되어 있다.

import SwiftUI

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

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

SwiftUI의 view 파일들은 기본적으로 2개의 structure를 선언한다. 첫번째 structure는 View 프로토콜을 채택하며 view의 내용과 레이아웃을 추가하고, 두번째 structure를 통해 view의 프리뷰를 볼 수 있다.

Step 6
캔버스의 resume 버튼을 클릭하면 프리뷰를 볼 수 있다.
캔버스가 안보인다면 Editor > Canvas를 클릭해서 볼 수 잇다.

Step 7
body 프로퍼티 안에 있는 "Hello, World!"를 다른 텍스트로 바꾸면 프리뷰도 자동으로 바뀌는 것을 볼 수 있다.

Section 2. TextView 커스터마이징하기

view의 생김새는 코드를 통해서도 바꿀 수 있고, inspector를 이용해 어떤 속성들을 바꿀 수 있는지 살펴보고 변경할 수 있다. 이번에는 inspector로 textView를 커스터마이징 해보자.

Step 1
view를 커맨드-클릭했을 때 뜨는 창에서는 view의 타입에 따라 사용자가 커스터마이징할 수 있는 속성들이 표시된다. 텍스트뷰를 커맨드-클릭한 뒤 Show SwiftUI Inspector를 클릭해보자.

Step 2
inspector를 이용해서 텍스트를 "Turtle Rock"으로 바꿔보자. 해당 이름은 우리가 앱에서 처음 보여줄 랜드마크 이름이다.

Step 3
Font를 'Title'로 바꾸자. 이렇게하면 아이폰 사용자가 선호하는 글꼴 크기 및 설정에 맞도록 텍스트에 글꼴이 적용된다.

SwiftUI view를 커스터마이징하기 위해서는 modifier 라 불리는 메소드를 호출해야한다. modifier들은 view의 display와 속성들을 변경하기 위해 해당 view를 감싼다. 각각의 modifier는 한개의 새로운 view를 반환하기 때문에 여러개의 modifier를 연속적으로 호출하는것이 일반적이다. 예시는 아래 Step 들을 통해 볼 수 있다.

Step 4
기존의 padding() modifier를 지우고 Color(.green) modifier로 변경하면 글자 색상이 green 색으로 변한다.

struct ContentView: View {
    var body: some View {
        Text("Turtle Rock")
            .font(.title)
            .foregroundColor(.green) // 연속적인 호출!
    }
}

view에 대한 정보는 모두 코드에 들어있다. 따라서 inspector로 어떤 속성을 변경하거나 삭제하더라도 Xcode가 알아서 그에 맞게 바로바로 코드를 바꿔준다.

Step 5, 6
textView 커맨드-클릭으로 inspector를 열고 글자 색상을 Inherited로 바꿔보자. 프리뷰에서 글자 색상이 검정색으로 바뀐다. 동시에 Xcode가 코드를 업데이트하면서, 작성되어 있던 foregroundColor() modifier가 지워진다!

Section 3. Stack을 이용해서 View를 combine하기

앞 section에서 만든 title 밑에 landmark의 정보들을 담은 textView들을 배치해보자.

SwiftUI에서 view를 만들 때눈 body 프로퍼티에 view의 내용, 레이아웃, 동작을 표현해야 한다. 하지만 body 프로퍼티는 1개의 view를 반환한다. 따라서 여러 개의 view를 가로, 세로, 뒤에서 앞으로 그룹화시킬 때는 Stack을 이용해 combine, embed 해야한다.

Step 1
textView 생성자를 커맨드-클릭한 후 열리는 창에서 Embed in Vstack을 클릭하자.

Step 2
Xcode 오른쪽 위에 위치한 (+) 버튼을 클릭하고, Text를 드래그해서 "Turtle Rock" textView 바로 아래로 드래그 해서 추가하자. (stack에 textView 추가)

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Turtle Rock")
                .font(.title)
            Text("PlaceHolder")
        }          
    }
}

Step 3, 4, 5
textView의 placeholder 텍스트를 "Joshua Tree National Park"로 바꾸고, font를 subheadline으로 바꾸자. VStack 생성자를 edit하여 leading을 기준으로 뷰를 정렬하자.

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Turtle Rock")
                .font(.title)
            Text("Joshua Tree National Park")
                .font(.subheadline)
        }
    }
}

Step 6
자, 이번에는 위치를 나타내는 textView의 오른쪽에 공원의 state를 나타내는 textView를 추가해보자. 캔버스에서 Joshua Tree National Park textView를 커맨드-클릭하고 Embed in HStack을 클릭하자.

Step 7
위치를 나타내는 textView 뒤에 새 Text를 추가하고 placeholder를 공원의 state로 바꾼 다음, font를 subheadline으로 바꾸자.

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Turtle Rock")
                .font(.title)
            HStack {
                Text("Joshua Tree National Park")
                    .font(.subheadline)
                Text("California")
                    .font(.subheadline)
            }
        }
    }
}

Step 8
레이아웃이 디바이스 전체 너비를 사용하게 하려면, 위치와 state를 포함하는 HStack에 Spacer를 추가하면 된다.

Spacer는 view의 크기를 content에 의해 정의되게 하지않고, parent view에서 사용 가능한 모든 공간을 사용할 수 있게 확장시킨다. (안드로이드의 match parent와 같다고 생각하면 좋을 것 같다.)

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Turtle Rock")
                .font(.title)
            HStack {
                Text("Joshua Tree National Park")
                    .font(.subheadline)
                Spacer()
                Text("California")
                    .font(.subheadline)
            }
        }
    }
}

Step 9
마지막으로 landmarkd의 이름과 디테일에 약간의 공간을 생기게 하기 위해 padding() modifier를 사용해준다. landmark의 이름과 디테일을 포함하는 VStack에 달아준다.

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Turtle Rock")
                .font(.title)
            HStack {
                Text("Joshua Tree National Park")
                    .font(.subheadline)
                Spacer()
                Text("California")
                    .font(.subheadline)
            }
        }
        .padding()
    }
}

Section 4. 커스텀 Image View 만들기

landmark의 이미지를 추가해보자. 파일에 더 많은 코드를 추가하는 대신 마스크, 테두리 및 그림자를 이미지에 적용하는 custom view를 만들어보자.

Step 1
애플에서 제공하는 아래 그림을 저장해서 project의 asset 카탈로그 안에 넣어주자.

Step 2
커스텀 image view를 위한 새로운 SwiftUI view를 만들어보자. File > New > File 에서 SwiftUI View를 클릭하고, 이름을 CircleImage.swift로 지정한 뒤 생성해주자.

Step 3
기본으로 만들어진 textView를 Turtle Rock의 이미지로 바꿔주자. Image(_:) 생성자를 이용해서 보여줄 이미지의 이름을 넘겨주면된다.

import SwiftUI

struct CircleImage: View {
    var body: some View {
        Image("turtlerock")
    }
}

struct CircleImage_Previews: PreviewProvider {
    static var previews: some View {
        CircleImage()
    }
}

Step 4, 5, 6
clipShape(Circle()) 메서드를 호출하여 원형 clip 모양을 이미지에 적용하자. 그 다음, 흰색 선을 가진 원을 하나 더 생성한 뒤 overlay로 추가하여 이미지에 테두리를 적용하자. 이후 radius 값이 7인 그림자를 추가하면 custom image view 생성 완료!

struct CircleImage: View {
    var body: some View {
        Image("turtlerock")
            .clipShape(Circle())
            .overlay(Circle().stroke(Color.white, lineWidth: 4))
            .shadow(radius: 7)
    }
}

쨘! ✌️ ( 애플은 이런 샘플도 예쁘게 만들어,, 맘에 든다,, )

Section 5. 다른 프레임워크에서 SwiftUI View 사용하기

이번엔 주어진 좌표를 중심으로하는 지도를 만들어보자. MapKit의 Map view를 사용해서 지도를 렌더링할 수 있다.

Step 1
지도를 관리하기 위해 새로운 custom view를 만들자. File > New > File에서 SwfitUI View를 선택하고, MapView.swift 파일을 생성하자.

Step 2
MapKit을 import !
같은 파일에서 SwiftUI와 다른 프레임워크를 함께 import하면, 해당 프레임워크에서 제공하는 SwiftUI 관련 기능에 접근할 수 있다.

Step 3
지도에 대한 지역 정보를 가지는 비공개 State 변수를 만들자.

@State 속성을 사용해서 2개 이상의 view에서 수정할 수 있는, 앱의 데이터에 대한 a source of truth를 설정할 수 있다. SwiftUI는 기본 스토리지를 관리하고 값에 따라 뷰를 자동으로 업데이트 한다.

single source of truth
정보 시스템의 디자인 이론 중에 하나인, 모델과 그와 연관된 스키마를 정의함에 있어서 모든 데이터 요소는 시스템에서 유일한 값으로 존재하도록 하는 것을 말합니다.
출처: 위키피디아

import SwiftUI
import MapKit

struct MapView: View {
    
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868),
        span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
    )
    
    var body: some View {
        Map(coordinateRegion: $region)
    }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}

Step 4
기본 Text를 삭제하고, region을 바인딩하고 있는 Map을 추가하자.

var body: some View {
    Map(coordinateRegion: $region)
}

상태 변수에 $ 접두사를 붙이면 기본 값에 대한 참조같은 바인딩을 전달한다. 즉, 사용자가 지도와 상호작용하면 지도는 현재 사용자 인터페이스에 표시되는 지도와 같은 값을 가지도록 region value를 업데이트 한다.

Step 5
프리뷰가 static 모드이면 native SwiftUI view들만 랜더링 하기 때문에 Map view를 보기 위해서는 live 모드로 전환해야한다. 모드를 전환하기 위해 프리뷰 디바이스 상단의 재생 버튼 클릭 !

Section 6. 상세보기 만들기

이제 이름과 장소, 원형 이미지, 지도 등 필요한 모든 것들이 준비되었다. 지금까지 사용해본 도구들을 이용해서 커스텀 뷰를 만들어보자.

Step 1, 2
Project navigator에서 ContetView.swift 파일을 선택한 뒤, 3개의 Text를 가지고 있는 VStack을 새로운 VStack 안에 넣자.

struct ContentView: View {
    var body: some View {
        VStack { // 요기 추가!
            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)
                HStack {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
            }
            .padding()
        }
    }
}

Step 3, 4
가장 상단의 VStack에 MapView를 추가한다. 이후 frame(width:height:) 메서드를 사용해 MapView의 크기를 설정해준 뒤에 live 프리뷰를 클릭해서 랜더링된 맵을 확인해보자.

struct ContentView: View {
    var body: some View {
        VStack {
            MapView()
                .frame(height: 300)
            
            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)
                HStack {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
            }
            .padding()
        }
    }
}

Step 5, 6
지도와 텍스트들 사이에 CircleImage를 추가하자. 그리고 지도 위에 이미지를 배치시키기 위해 이미지에 수직 오프섹을 -130 주고, bottom padding을 -130 주자.

struct ContentView: View {
    var body: some View {
        VStack {
            MapView()
                .frame(height: 300)
            
            CircleImage()
                .offset(y: -130)
                .padding(.bottom, -130)
            
            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)
                HStack {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
            }
            .padding()
        }
    }
}

Step 7, 8
contents를 상단에 배치 시키기 위해 가장 바깥의 VStack 밑에 spacer를 추가하자. 그리고 contents가 위로 올라갔을 때, 지도가 SafeArea를 무시하여 배치되도록 지도에 ignoreSafeArea(edges: .top) modifier를 추가해주자.

struct ContentView: View {
    var body: some View {
        VStack {
            MapView()
                .ignoresSafeArea(edges: .top) // safe area 무시
                .frame(height: 300)
            
            CircleImage()
                .offset(y: -130)
                .padding(.bottom, -130)
            
            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)
                HStack {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
            }
            .padding()
            
            Spacer()
        }
    }
}

Step 9, 10
구분선을 추가하고 추가적인 설명을 위한 text들을 추가해주자. 그리고 마지막으로 HStack 안에있는 text에 달려있던 subheadline font modifier를 밖으로 빼주고, foregroundColor를 .secondary로 적용해주면 끝!

struct ContentView: View {
    var body: some View {
        VStack {
            MapView()
                .ignoresSafeArea(edges: .top)
                .frame(height: 300)
            
            CircleImage()
                .offset(y: -130)
                .padding(.bottom, -130)
            
            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)
                HStack {
                    Text("Joshua Tree National Park")
                      
                    Spacer()
                    Text("California")
                }
                .font(.subheadline)
                .foregroundColor(.secondary)
                
                Divider()
                
                Text("About Turtle Rock")
                    .font(.title2)
                Text("Descriptive text goes here.")
            }
            .padding()
            
            Spacer()
        }
    }
}

아래는 완성된 화면이다.

참고

https://developer.apple.com/tutorials/swiftui/creating-and-combining-views

https://duwjdtn11.tistory.com/567

https://en.wikipedia.org/wiki/Single_source_of_truth

profile
가장 젊은 지금, 내가 성장하는 데에 쓰자

1개의 댓글

comment-user-thumbnail
2021년 2월 26일

안그래도, 저도 SwiftUI 공식문서 보고 따라하다가 여기까지 흘러왔네요. 잘 보았습니다. 나중에, https://www.hackingwithswift.com/100/swiftui
여기내용도 괜찮으니 시도해 보세요!!

답글 달기