UIKit 을 사용했을 때는 어떠한 변수에 변화가 생기면 해당 변화를 직접 관찰하고 반영 해 주어야 했는데 SwiftUI 에서는 property wrapper 를 활용해 이러한 작업을 자동화 할 수 있도록 했다.
@State
속성으로 어떤 프로퍼티의 초기값을 지정했다면, 다른 값으로 재할당 불가, @Binding
변수를 통해서만 가능// @state 값의 변화를 감지
@State private var isActivated: Bool = false
var body: some View {
NavigationView {
VStack {
HStack {
MyVstackView(isActivated: $isActivated)
MyVstackView(isActivated: $isActivated)
MyVstackView(isActivated: $isActivated)
}
.padding(isActivated ? 50.0 : 10.0)
.background(isActivated ? Color.yellow : Color.black)
.onTapGesture {
print("Hstack 이 클릭")
withAnimation {
self.isActivated.toggle()
}
}
NavigationLink {
MyTextView(isActivated: $isActivated)
} label: {
Text("네비게이션 ")
.fontWeight(.bold)
.font(.system(size: 40))
.padding()
.background(.orange)
.foregroundColor(.white)
.cornerRadius(30)
}
.padding(.top , 50)
}
}
}
}
앞전 게시물에서 수없이 나왔던 @State
와 Struct
에 대해서 알아보겠습니다.
SwiftUI
에서 가장 기본이되고 많이 사용하는 것들이니 꼭 알아두시길 바랍니다.
만약 사용자가 버튼을 누르거나 스크롤을 하거나 텍스트에 상자를 입력했다고 치면, 그 특정 행동은 State
즉, 상태를 변경합니다. 그 이후에 일어날 일은 State
가 변경되면 자동으로 변환 시켜주는일을 합니다. 사용자 인터페이스를 업데이트 하는것이죠.
그렇다면 어떻게 이렇게 할 수 있을까요? View
를 사용할때 ContentView
가 실제로 View
프로토콜을 준수한다는것을 기억해야합니다. Body
속성을 작성하죠. 이것이 View 프로토콜의 유일한 요구 사항입니다.
상태를 변경 할 때마다 body
속성이 재설정 됩니다. 뷰 자체가 다시 렌더링 되는것이죠.
따라서 State
를 변경 할 때마다 항상 새로운 View
에서 렌더링 된다는 점을 기억하시면 됩니다. 그리고 사용자가 그것을 보게 되는것이죠.
사용자가 스위치를 켜고 끄는것처럼 상태를 변경 할때 그 값을 State
안에 넣으면 뷰를 렌더링 하게 됩니다.
이것을 사용하기 위해서는 @State
와 Struct
를 필수적으로 사용해야합니다. Strcut
내부의 값이 전체 Struct
를 변경할 때마다 전체 구조체가 자동적으로 변경됩니다. 마치 이름이나 성에 대한 키를 입력 할 때마다 새롭게 적용되게 해주는것처럼 말이죠.
아래 예시를 보며 확인해보겠습니다.
SWIFT
struct ContentView: View {
var name = "로이"
var body: some View {
VStack {
Text(name)
Button(action: {
}) {
Text("이름 바꾸기")
}
}
}
}
name
을 변수로 지정해주고 텍스트로 이름과 버튼을 만들어줬습니다. 이상태에서 버튼을 클릭하면 아무런 변화가 없습니다.
하지만 @State
를 넣어주고 아래와 같이 코드를 변경하면 어떻게 될까요?
SWIFT
struct ContentView: View {
@State var name = "로이"
var body: some View {
VStack {
Text(name)
Button(action: {
self.name = "리오"
}) {
Text("이름 바꾸기")
}
}
}
}
버튼을 클릭하면 '서근' 에서 '포뇨'로 이름이 바뀝니다. @State
를 지정해주면서 View
가 업데이트 된것이죠.
여기서 self
는 @State name
을 의미합니다.
만약 목록 상단에 버튼이 있고, 항목추가 버튼을 누를 때마다 자동으로 항목이 추가되고 뷰가 새로고침되게 해보겠습니다.
1. 새로운 Swift 파일을 만들어주고 이름은 Task로 정해주겠습니다. 그리고 다음과 같이 코드를 작성합니다.
TIP
💡UUID??유형, 인터페이스 및 기타 항목을 식별하는 데 사용할 수 있는 보편적으로 고유한 값입니다.
SWIFT
import Foundation
import SwiftUI
struct Task: Identifiable {
let id = UUID()
let name: String
}
SWIFT
import SwiftUI
struct ContentView: View {
var tasks = [Task]()
var body: some View {
List {
ForEach(tasks) { task in
Text(task.name)
}
}
}
}
이렇게 하고 런을 해주면 아무것도 없는 EmptyView
가 됩니다. 코드를 더 추가해주도록 하겠습니다.
SWIFT
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)
}
}
}
}
1. New file
을 만들어 이름은 'Dish
' 라고 정해주고, 아래 이미지를 Assets
에 넣어줍니다. 그리고 Dish
라는 Extension
값을 넣어주겠습니다. 매개변수 타입을 Identifiable
로 반드시 지정해줘야 합니다.
SWIFT
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
에 화면을 구성해보도록 하겠습니다. 우리는 음식 이미지와 이름을 나열하고 만약 그 음식이 매우면, 맵기를 표시하려고 합니다.
SWIFT
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)
}
}
}
}
}
}
음식 이미지와 이름은 정확히 불러왔지만, 매운정도에 따라 맵기 아이콘이 정상적으로 표시되지는 않습니다.
SWIFT
List {
Toggle(isOn: .constant(true)) {
Text("매운맛")
}
spicy-icon
은 정상적으로 나타나게 되었습니다. 하지만 매운맛 버튼을 토글하면 아무런 작동이 되지 않습니다.
@State
를 추가해서 작동 될 수 있도록 하겠습니다.
SWIFT
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
를 추가해줍니다.
SWIFT
//11.
ForEach(model.filter { $0.isSpicy == self.IsSpicy}) { dish in
여기에서 filter는 5글자 미만인 것만 나타낸다 라는 조건입니다.
SWIFT
let cast = ["Vivien", "Marlon", "Kim", "Karl"]
let shortNames = cast.filter { $0.count < 5 }
print(shortNames)
// Prints "["Kim", "Karl"]"