@State, @Binding 변수에 초기값 부여하기

SteadySlower·2022년 6월 7일
0

SwiftUI

목록 보기
14/64
post-custom-banner

구현하고자 하는 화면

만들고자 하는 화면은 프로필 화면과 프로필 수정 화면입니다.

프로필 화면에서는 사용자의 이름과 프로필 메시지를 볼 수 있습니다. 그리고 프로필 수정 화면으로 들어가기 위한 NavigationLink가 하나 있습니다.

프로필 수정 화면에서는 TextField로 프로필 메시지를 수정할 수 있습니다.

🤔 첫번째 구현

상위 View인 ProfileView가 가지고 있는 User 객체를 하위 View에 전달합니다. 이 객체는 View의 Binding으로 연결됩니다. EditView에서는 TextField에 user의 message를 Binding으로 전달합니다. 따라서 TextField에서 수정하면 바로 상위 View의 user 객체에 반영됩니다.

import SwiftUI

struct User {
    let name: String
    var message: String
}

struct ProfileView: View {
    @State var user = User(name: "Kim", message: "Hello World")
    
    var body: some View {
        NavigationView {
            VStack {
                Text("name: \(user.name)")
                Text("bio: \(user.message)")
                NavigationLink {
                    EditView(user: $user)
                } label: {
                    Text("Edit Bio")
                }

            }
        }
    }
}

struct EditView: View {
    @Binding var user: User
    
    var body: some View {
        TextField("", text: $user.message)
            .frame(width: 200)
            .border(.black, width: 1)
    }
}

🚫 문제점

사실 상용앱에서 누구도 이렇게 프로필 수정 페이지를 만들지 않습니다. 이유는 아래와 같습니다.

  1. 사용자는 프로필을 수정하고 저장하지 않을 수도 있습니다.
  2. 서버와 연결되어 있다면 수정할 때마다 수정하는 API 요청을 할 수 없습니다.
  3. 수정하다가 취소하고 싶은 경우 복원을 할 수가 없습니다.

😀 해결책

EditView에서 messageText를 별도의 @State 변수로 가지고 있기

위 문제들을 해결하기 위해서 EditView에서는 TextField를 위해서 @State 변수를 따로 가지고 있겠습니다. 해당 State 변수는 ProfileView에서 넘어온 user.message를 초기값으로 가질 것입니다.

struct EditView: View {
    @State private var messageText: String 
    @Binding var user: User
}

initializer 만들기

Binding 할당하기

Binding를 받는 initializer를 만듭시다. ProfileView에서 $user를 전달할 것이기 때문에 그냥 User를 받으면 안되고 Binding으로 감싸야 합니다.

또한 property 이름 앞에 “_”를 붙여야 합니다.

struct EditView: View {
    @State private var messageText: String //👉 init 대상
    @Binding var user: User //👉 init 대상
    
    init(user: Binding<User>) {
        self._user = user
        self._messageText = State(initialValue: _user.wrappedValue.message)
    }
}

만약에 붙이지 않는다면 아래와 같은 에러가 발생합니다. 그 이유는 EditView에 선언된 user는 User 타입이기 때문에 Binding를 할당할 수 없습니다. 따라서 @Binding을 붙이는 변수의 내부값에 직접 접근해서 (”_”를 붙여서) 직접 할당하는 방법을 사용합니다.

State 할당하기

@State로 선언된 messageText 변수도 마찬가지입니다. 그냥 messageText에 할당하고자 하면 위에서 보듯이 타입이 다르기 때문에 에러가 발생합니다.

그렇다면 아래처럼 messageText에 그냥 user.message 값을 할당하면 어떨까요?

그렇다면 아래와 같이 🚫 에러 메시지가 바뀝니다. user는 Binding으로 감싸져 있습니다. 따라서 user의 property 역시 Binding으로 감싸져있게 됩니다. 따라서 다시 타입이 맞지 않습니다.

그렇다면 타입을 맞추면 되지않을까요? Binding의 wrappedValue에 접근해서 String 값을 가져와 봅시다. 사실 messageText는 EditView에서만 사용할 것이므로 binding으로 접근할 필요가 없습니다.

하지만 아래에 보듯이 결과는 역시 🚫  컴파일 에러입니다. 읽어보면 self.messageText가 initialized 되기 전에 접근할 수 없다고 합니다.

⭐️ 중요한 내용입니다. @State나 @Binding이 붙은 변수는 “_”가 붙은 내부값에 State 혹은 Binding으로 (위에서는 State, Binding) 감싸진 값으로 init되기 전에는 접근할 수 없습니다.

따라서 최종적으로 아래처럼 State의 initializer를 활용해서 State 타입의 값을 만들어서 할당해야 합니다.

init(user: Binding<User>) {
    self._user = user
    self._messageText = State(initialValue: user.wrappedValue.message)
}

기타 UI 추가하기

이제 TextField에서는 이제 user.message가 아니라 EditView 내부에서만 사용하는 변수를 바인딩으로 연결합니다.

TextField 아래에 버튼 2개를 추가합니다. 첫 번째는 변경된 메시지를 저장하는 버튼이고 두 번째 버튼은 메시지의 변경을 취소할 때 사용합니다.

struct EditView: View {
    @Environment(\.presentationMode) var mode
    @State private var messageText: String 
    @Binding var user: User
    
    init(user: Binding<User>) {
        self._user = user
        self._messageText = State(initialValue: user.wrappedValue.message)
    }
    
    var body: some View {
        VStack {
            TextField("", text: $messageText) //👉 내부에서만 사용하는 변수 연결
                .frame(width: 200)
                .border(.black, width: 1)
            Button {
                user.message = messageText //👉 binding 변수의 message에 반영
                mode.wrappedValue.dismiss()
            } label: {
                Text("Change Bio")
            }
            Button {
                mode.wrappedValue.dismiss()
            } label: {
                Text("Cancel Change")
            }
        }
    }
}

최종 결과

이제 메시지를 바꾸더라도 NavigationBar의 백버튼 혹은 Cancel 버튼을 눌러도 ProfileView의 메시지는 바뀌지 않습니다. 변경하는 버튼을 눌러야지만 메시지를 변경할 수 있습니다.

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.
post-custom-banner

0개의 댓글