SwiftUI : State에 대해 자세히 알아보기
이미 앞 게시물에서 한번 다뤘지만 SwiftUI에서 아주 중요한 부분이기 때문에 한번 더 자세히 다루겠습니다.
@State
앞전 게시물에서 수없이 나왔던 @State
와 Struct
에 대해서 알아보겠습니다.
SwiftUI
에서 가장 기본이되고 많이 사용하는 것들이니 꼭 알아두시길 바랍니다.
만약 사용자가 버튼을 누르거나 스크롤을 하거나 텍스트에 상자를 입력했다고 치면, 그 특정 행동은 State
즉, 상태를 변경합니다. 그 이후에 일어날 일은 State
가 변경되면 자동으로 변환 시켜주는일을 합니다. 사용자 인터페이스를 업데이트 하는것이죠.
그렇다면 어떻게 이렇게 할 수 있을까요? View
를 사용할때 ContentView
가 실제로 View
프로토콜을 준수한다는것을 기억해야합니다. Body
속성을 작성하죠. 이것이 View 프로토콜의 유일한 요구 사항입니다.
상태를 변경 할 때마다 body
속성이 재설정 됩니다. 뷰 자체가 다시 렌더링 되는것이죠.
따라서 State
를 변경 할 때마다 항상 새로운 View
에서 렌더링 된다는 점을 기억하시면 됩니다. 그리고 사용자가 그것을 보게 되는것이죠.
사용자가 스위치를 켜고 끄는것처럼 상태를 변경 할때 그 값을 State
안에 넣으면 뷰를 렌더링 하게 됩니다.
이것을 사용하기 위해서는 @State
와 Struct
를 필수적으로 사용해야합니다. Strcut
내부의 값이 전체 Struct
를 변경할 때마다 전체 구조체가 자동적으로 변경됩니다. 마치 이름이나 성에 대한 키를 입력 할 때마다 새롭게 적용되게 해주는것처럼 말이죠.
아래 예시를 보며 확인해보겠습니다.
struct ContentView: View {
var name = "서근"
var body: some View {
VStack {
Text(name)
Button(action: {
}) {
Text("이름 바꾸기")
}
}
}
}
name
을 변수로 지정해주고 텍스트로 이름과 버튼을 만들어줬습니다. 이상태에서 버튼을 클릭하면 아무런 변화가 없습니다.
하지만 @State
를 넣어주고 아래와 같이 코드를 변경하면 어떻게 될까요?
struct ContentView: View {
@State var name = "서근"
var body: some View {
VStack {
Text(name)
Button(action: {
self.name = "포뇨"
}) {
Text("이름 바꾸기")
}
}
}
}
버튼을 클릭하면 '서근' 에서 '포뇨'로 이름이 바뀝니다. @State
를 지정해주면서 View
가 업데이트 된것이죠.
여기서 self
는 @State name
을 의미합니다.
예제 1
만약 목록 상단에 버튼이 있고, 항목추가 버튼을 누를 때마다 자동으로 항목이 추가되고 뷰가 새로고침되게 해보겠습니다.
1. 새로운 Swift 파일을 만들어주고 이름은 Task로 정해주겠습니다. 그리고 다음과 같이 코드를 작성합니다.
여기서 중요한것은 Task의 매개변수 타입을 Identifiable 로 반드시 지정해줘야 합니다.
2. ContentView에 @State를 넣어주고 List 뷰를 넣어줍니다.
💡UUID??
유형, 인터페이스 및 기타 항목을 식별하는 데 사용할 수 있는 보편적으로 고유한 값입니다.
import Foundation
import SwiftUI
struct Task: Identifiable {
let id = UUID()
let name: String
}
import SwiftUI
struct ContentView: View {
var tasks = [Task]()
var body: some View {
List {
ForEach(tasks) { task in
Text(task.name)
}
}
}
}
이렇게 하고 런을 해주면 아무것도 없는 EmptyView
가 됩니다. 코드를 더 추가해주도록 하겠습니다.
import SwiftUI
struct ContentView: View {
//1.
@State var tasks = [Task]()
//4. addTask라는 함수 추가
private func addTask() {
self.tasks.append(Task(name: "서근 블로그 구독"))
}
var body: some View {
//2.
List {
//5.
Button(action: addTask) {
HStack {
Image(systemName: "plus")
Text("할일 추가")
}.foregroundColor(.blue)
}
//3.
ForEach(tasks) { task in
Text(task.name)
}
}
}
}
예제 2
1. New file
을 만들어 이름은 'Dish
' 라고 정해주고, 아래 이미지를 Assets
에 넣어줍니다. 그리고 Dish
라는 Extension
값을 넣어주겠습니다. 매개변수 타입을 Identifiable
로 반드시 지정해줘야 합니다.
import Foundation
import SwiftUI
struct Dish: Identifiable {
//Dish를 구별하기 위해 고유 ID를 저장해줌
let id = UUID()
let name : String
let price : Double
let imageURL : String
//음식이 맵다면
let isSpicy: Bool
}
extension Dish {
static func all() -> [Dish] {
return [
Dish(name: "김치", price: 10000, imageURL: "Kimchi", isSpicy: true),
Dish(name: "스파게티", price: 8500, imageURL: "Spagetti", isSpicy: true),
Dish(name: "토스트", price: 3000, imageURL: "Toast", isSpicy: false),
Dish(name: "초밥", price: 12000, imageURL: "SuShi", isSpicy: false),
Dish(name: "스테이크", price: 25000, imageURL: "Steak", isSpicy: false),
Dish(name: "치킨", price: 17000, imageURL: "Chicken", isSpicy: true)
]
}
}
2. ContentView
에 화면을 구성해보도록 하겠습니다. 우리는 음식 이미지와 이름을 나열하고 만약 그 음식이 매우면, 맵기를 표시하려고 합니다.
struct ContentView: View {
var model = Dish.all()
var body: some View {
//1.
List {
//2.
ForEach(model) { dish in
//3.
HStack {
//4.
Image(dish.imageURL)
.resizable()
.frame(width: 100, height: 100)
//5.
Text(dish.name)
.padding(.leading, 10)
.font(.title)
.lineLimit(nil)
//7.
Spacer()
//8.만약 음식이 맵다면 아래 이미지 표시
if(dish.isSpicy) {
//6.
Image("spicy-icon")
.resizable()
.frame(width: 30, height: 30)
}
}
}
}
}
}
음식 이미지와 이름은 정확히 불러왔지만, 매운정도에 따라 맵기 아이콘이 정상적으로 표시되지는 않습니다.
여기서 toggle
을 추가해주려고 합니다.
List {
Toggle(isOn: .constant(true)) {
Text("매운맛")
}
spicy-icon
은 정상적으로 나타나게 되었습니다. 하지만 매운맛 버튼을 토글하면 아무런 작동이 되지 않습니다.
@State
를 추가해서 작동 될 수 있도록 하겠습니다.
struct ContentView: View {
var model = Dish.all()
//9.
@State private var IsSpicy = false
var body: some View {
List {
//10 .constant(true) -> $IsSpicy
Toggle(isOn: $IsSpicy) {
Text("매운맛")
이제 토글이 작동이 됩니다. 하지만 이미지에는 아무런 변화가 없죠. 자, 만약 토글을 off
했을때 맵지않은 음식을 가리고 싶다면?
ForEach
문을 수정해야합니다. ForEach
의 model
뒤에 filter
를 추가해줍니다.
//11.
ForEach(model.filter { $0.isSpicy == self.IsSpicy}) { dish in
Filter 예문
여기에서 filter는 5글자 미만인 것만 나타낸다 라는 조건입니다.
let cast = ["Vivien", "Marlon", "Kim", "Karl"]
let shortNames = cast.filter { $0.count < 5 }
print(shortNames)
// Prints "["Kim", "Karl"]"
전체 코드
<hide/>
import SwiftUI
struct ContentView: View {
var model = Dish.all()
//9.
@State private var IsSpicy = false
var body: some View {
//1.
List {
//10 .constant(true) -> $IsSpicy
Toggle(isOn: $IsSpicy) {
Text("매운맛")
.font(.title)
}
//2.
//11. filter 추가
ForEach(model.filter { $0.isSpicy == self.IsSpicy}) { dish in
//3.
HStack {
//4.
Image(dish.imageURL)
.resizable()
.frame(width: 100, height: 100)
//5.
Text(dish.name)
.padding(.leading, 10)
.font(.title)
.lineLimit(nil)
//7.
Spacer()
//8.만약 음식이 맵다면 아래 이미지 표시
if(dish.isSpicy) {
//6.
Image("spicy-icon")
.resizable()
.frame(width: 30, height: 30)
}
}
}
}
}
}
읽어주셔서 감사합니다🤟
본 게시글의 전체코드 GitHub 👇🏻