SwiftUI Lecture1-2 정리
25 Jun 2021SwftUI Lecture는 Stanford 대학 Youtube 채널의 CS193p - Developing Apps for iOS 온라인 강의를 보고 정리.
swiftUI는 Xcode에서 제작해볼 수 있는 ios app 제작 도구이다.
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
기본적인 셋팅을 마치고 처음 프로젝트를 열게되면 위와 같은 코드가 등장한다. ContenView_Previews 는 Preview 영역에 어떤 View를 띄울지 정해주는 구조체이고 중요한 것은 위에 있는 ContentView 구조체이다.
기본적으로 SwiftUI는 View라는 속성을 지닌 구조체를 사용한다. 그리고 내부에 body라는 변수를 꼭 지정해줘야한다. 여기서 신기하게도 some View라는 단어를 볼 수 있는데 이는 View의 일종이 여러개 올 수 있다는 의미이다. 다음과 같이 body를 Text로 선언하면 안되는지 궁금할 수도 있는데 지금 상태에서는 충분히 가능하다.
struct ContentView: View {
var body: Text {
Text("Hello, world!")
}
}
그러나 padding이라는 modify가 추가되면 Text라는 구조체가 아니게 된다. 이처럼 여러 복합적인 View들을 섞어쓰는 가능성을 대비해서 some View라는 형태로 타입 선언을 해준다. 참고로 Text 구조체 역시 View의 일종이다.
struct ContentView: View {
var body: Text {
Text("Hello, world!")
.padding()
}
}
자 이제 간단한 텍스트가 아닌 카드의 형태로 View를 구성하고 싶다. 이 때 하나 이상의 View를 통합해서 화면에 표시하고 싶다면 View를 통합해주는 어떤 복합체를 사용해야 한다. 사용할 수 있는 View 복합체들은 여러개가 존재한다. 여기서는 5개의 복합체를 안내하도록 하겠다.
-
ZStack :
하나의 View위에 다른 View를 순차적으로 쌓는 형태이다.
-
HStack :
수평으로 View들을 나열하는 방식이다.
-
VStack :
수직으로 View들을 나열하는 방식이다.
HStack과 VStack은 사용가능한 공간을 꽉 채워서 사용한다. -
LazyVGrid :
Grid를 생성할 때 사용하는 방식이다. 만약 일정한 간격으로 View들을 배열하고 싶을 때 사용한다.
LazyVGrid(columns: [GridItem(), GridItem(), GridItem()]) {} LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))]) {}columns에 GridItem()을 원하는 만큼 배치해서 가로 갯수를 정해줄 수 있다. 혹은 .adaptive 속성을 사용하여 그리드의 가로 길이의 최솟값을 정해줄 수 있다. 이렇게 하면 주어진 영역에 대해 조건을 만족하는 한에서 최대한 많이 grid를 배치한다. 가로 모드와 세로 모드에서 배열을 다르게 하고 싶을 때 종종 사용한다.
주의할 것은 LazyVGrid는 가로로는 길이를 정해줄 수 있지만 세로로는 View가 가질 수 있는 최소의 길이를 갖는다는 것이다. 따라서 수직 길이를 변경하고 싶다면 GridItem이 되는 각각의 View에 대해서 ratio 설정으로 해결하자.
-
ScrollView :
내부에 존재하는 View들을 Scroll 형식으로 볼 수 있게 해주는 형식이다.
범위를 넘어가도 scroll을 통해 확인할 수 있다.
예시로 만든 코드는 다음과 같다.
import SwiftUI
struct ContentView: View {
@State var emojiCount = 4
var emojis = ["🚖", "🛵", "🚆", "🚍", "🛺", "🛫", "🏖", "🚔", "🛸", "🚁", "🏍", "🛳", "🚐", "🚛", "🚚"]
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))]) {
ForEach(emojis[0..<emojiCount], id: \.self){
CardView(content: $0).aspectRatio(2/3, contentMode: .fit)
//aspectRatio로 View의 가로 세로 비율 조정하기
//aspectRatio(가로/세로, contentMode: )로 사용한다.
}
}
}
.foregroundColor(.red)
Spacer()
//modify는 값을 안주면 기기별 default 설정이 입력된다.
//Spacer는 남는 공간을 모두 빈공간으로 채워주는 역할을 하는 View이다.
HStack {
remove
Spacer()
add
}
.font(.largeTitle)
.padding(.horizontal)
}
.padding(.horizontal)
}
var remove: some View {
Button {
if emojiCount > 1 {
emojiCount -= 1
}
} label: {
Image(systemName: "minus.circle")
//ios 자체 내장된 아이콘을 사용
//sf-symbols라는 문서를 통해 어떤 아이콘이 존재하는지 확인 후 사용
}
}
var add: some View {
Button {
if emojiCount < emojis.count {
emojiCount += 1
}
} label: {
Image(systemName: "plus.circle")
}
}
}
struct CardView: View {
var content: String
@State var isFaceUp: Bool = true
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 20.0)
//View를 미리 변수로 선언해둬서 후에 코드를 이쁘게 만들어주었다.
//RoundedRectangle은 둥근 모서리를 가진 직사각형을 만들어준다.
if isFaceUp {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: 5.0)
//그냥 stroke와 strokeBorder의 차이점은 경계선을 그을 때 경계선 외부-내부를 그리는지
//경계선 내부로만 그리는지의 차이이다.
//참고로 내부를 채우는 fill과 stroke는 동시에 사용할 수 없어 각각 View를 생성해줘야한다.
Text(content)
.font(.largeTitle)
} else {
shape.fill()
}
}
.onTapGesture {
isFaceUp = !isFaceUp
}
}
}
fill이나 font와 같은 속성들에 대해서는 보면 무엇인지 바로 확인이 가능할테니 설명을 넘어가고 몇가지 유의할 사항에 대해 이야기해보겠다. 먼저 HStack과 같은 상위 View에 modify를 적용하면 내부의 모든 view들에 적용이 가능하다. 다만 padding과 같이 상위 view에 적용이 가능한 속성은 내부에 전파하지 않는다. 예시로 다음을 보면 font는 HStack내부로 전파가 되지만 padding은 HStack을 감싸는 전체에 대해 적용이 된다.
HStack {
remove
Spacer()
add
}
.font(.largeTitle)
.padding(.horizontal)
CardView 구조체를 선언해놓고 ContentView에서 가져다 사용하는 것을 확인할 수 있는데 구조체로 미리 선언을 해서 반복적으로 사용하는 코드를 함축시킬 수 있다. 단, 사용횟수가 얼마 없어 단순히 코드를 깔끔하게 하고 싶은 것이라면 remove/add variable처럼 computed variable로 선언할 수도 있다.
정말 중요한 속성 중 하나로 View는 생성된 이후로 Immutable해야한다는 것이다. 그런데 자세히 보면 @State var emojiCount = 4나 ‘@State var isFaceUp: Bool = true’ 와 같은 형태로 변수를 선언해서 body내부에서 변수를 바꿔주는 것을 알 수 있다. @State는 포인터의 개념으로 해당 변수가 저장된 주소를 저장해준다. 따라서 주소값은 변하지 않기 때문에 View is Immutable의 속성을 만족할 수 있다.
아래의 2가지 방법으로 onclick 이벤트를 처리해주었다. 먼저 Button은 버튼이 눌렸을 때 실행할 action과 해당 버튼을 표시해주는 label View로 구성되어있다. .onTapGesture는 해당 View를 클릭할 시 실행할 action을 나타내준다.
Button(action: {}, label: {Text("Button")})
ZStack{}.onTapGesture {}
마지막으로 ContentView의 body 내부를 살펴보자. 놀랍게도 CardView라는 View를 반복해서 적는 것 대신 ForEach문을 사용한 것을 확인할 수 있다. SwiftUI body에서는 for문을 사용할 수 없는데 대신 ForEach 문이 사용이 가능하다. 주의할 것은 각 View마다 id라는 것을 부여해줘야 하는데 여기서는 emoji 그림 자체를 id로 부여해주었다.
유의할 것은 각각의 View에 대해 id를 다 다르게 설정해줘야한다는 것이다. 만약 동일한 emoji가 존재한다면 아래 코드에 대해서 두 View는 같은 object를 가르키게 된다. 이렇게될시 LazyVGrid 같은 것을 쓸 때 동일한 object가 2개 이상 존재할 시 에러가 나므로 꼭 모두 다른 id를 부여해주자.
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))]) {
ForEach(emojis[0..<emojiCount], id: \.self){
CardView(content: $0).aspectRatio(2/3, contentMode: .fit)
}
}