SWIFTUI/Grammar

SwiftUI : State에 대해 자세히 알아보기

서근 2021. 2. 10. 20:09
반응형

이미 앞 게시물에서 한번 다뤘지만 SwiftUI에서 아주 중요한 부분이기 때문에 한번 더 자세히 다루겠습니다.

@State

앞전 게시물에서 수없이 나왔던 @State 와 Struct에 대해서 알아보겠습니다.

 

SwiftUI에서 가장 기본이되고 많이 사용하는 것들이니 꼭 알아두시길 바랍니다. 

 

만약 사용자가 버튼을 누르거나 스크롤을 하거나 텍스트에 상자를 입력했다고 치면, 그 특정 행동은 State 즉, 상태를 변경합니다. 그 이후에 일어날 일은 State가 변경되면 자동으로 변환 시켜주는일을 합니다. 사용자 인터페이스를 업데이트 하는것이죠.

 

그렇다면 어떻게 이렇게 할 수 있을까요? View를 사용할때 ContentView가 실제로 View 프로토콜을 준수한다는것을 기억해야합니다. Body속성을 작성하죠. 이것이 View 프로토콜의 유일한 요구 사항입니다. 

 

상태를 변경 할 때마다 body속성이 재설정 됩니다. 뷰 자체가 다시 렌더링 되는것이죠.

따라서 State를 변경 할 때마다 항상 새로운 View에서 렌더링 된다는 점을 기억하시면 됩니다. 그리고 사용자가 그것을 보게 되는것이죠. 

 

사용자가 스위치를 켜고 끄는것처럼 상태를 변경 할때 그 값을 State안에 넣으면 뷰를 렌더링 하게 됩니다.

 

이것을 사용하기 위해서는 @StateStruct를 필수적으로 사용해야합니다. 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 뷰를 넣어줍니다.

TIP
 
 

💡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 로 반드시 지정해줘야 합니다.

Dish.zip
7.13MB

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 문을 수정해야합니다. ForEachmodel 뒤에 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 👇🏻

 

Seogun95/SwiftUI_State_TUT

State에 대해 알아봅시다. Contribute to Seogun95/SwiftUI_State_TUT development by creating an account on GitHub.

github.com