[SwiftUI] Bindable

2025. 4. 21. 11:50·iOS/SwiftUI
728x90
반응형

안녕하세요, iOS 개발하는 루피입니다.

 

오늘은 공식문서를 바탕으로 Bindable에 대해 정리해보는 시간을 갖겠습니다.

 

바로 시작합니다.


Bindable이란?

@Bindable은 Observable 객체의 변경 가능한 속성들에 바인딩을 생성할 수 있게 해주는 프로퍼티 래퍼입니다.

이 프로퍼티 래퍼를 사용하면 Observable 프로토콜을 준수하는 데이터 모델 객체의 변경 가능한 속성에 바인딩을 만들 수 있습니다.

 

예를 들어, 아래 코드에서는 @Bindable로 book 입력을 감싸고 있습니다. 그런 다음 TextField를 사용해 책의 title 속성을 변경하고, Toggle을 사용해 isAvailable 속성을 변경합니다. 이때 $ 문법을 사용하여 각 속성에 대한 바인딩을 컨트롤에 전달합니다.

@Observable
class Book: Identifiable {
    var title = "Sample Book Title"
    var isAvailable = true
}

struct BookEditView: View {
    @Bindable var book: Book
    @Environment(\\\\.dismiss) private var dismiss

    var body: some View {
        Form {
            TextField("Title", text: $book.title)
            Toggle("Book is available", isOn: $book.isAvailable)
            Button("Close") {
                dismiss()
            }
        }
    }
}

Bindable 프로퍼티 래퍼는 Observable 객체에 대한 프로퍼티와 변수에 사용할 수 있습니다.

전역 변수, SwiftUI 타입 외부에 존재하는 프로퍼티, 심지어 지역 변수에도 사용 가능합니다.

 

예를 들어, 뷰의 body 내에서 @Bindable 변수를 만들 수 있습니다.

struct LibraryView: View {
    @State private var books = [Book(), Book(), Book()]

    var body: some View {
        List(books) { book in
            @Bindable var book = book
            TextField("Title", text: $book.title)
        }
    }
}

이 @Bindable 변수 book은 TextField와 book의 title 속성을 연결하는 바인딩을 제공하여 사용자가 모델 데이터를 직접 변경할 수 있게 합니다.

 

뷰의 Environment에 저장된 Observable 객체의 속성에 바인딩이 필요할 때도 같은 방식을 사용할 수 있습니다.

예를 들어, 다음 코드는 Environment 프로퍼티 래퍼를 사용해 Observable 타입인 Book의 인스턴스를 가져옵니다.

 

그런 다음 @Bindable 변수 book을 만들고 $ 문법을 사용해 title 속성에 대한 바인딩을 TextField에 전달합니다.

struct TitleEditView: View {
    @Environment(Book.self) private var book

    var body: some View {
        @Bindable var book = book
        TextField("Title", text: $book.title)
    }
}

 


코드로 보는 Bindable 필요성

Bindable에 대해 잘 이해가 가시나요??

저는 한번에 알아보기 힘들더라고요 그래서 한번 코드로 알아보겠습니다.


State, Binding

아래 코드는 간단한 텍스트 필드를 입력받는 코드입니다.

struct ContentView: View {
    @State private var query = ""
    var body: some View {
        TextField("Enter text", text: $query)
        Text(query)
    }
}

이 코드에서는 @State 프로퍼티 래퍼를 사용하여 문자열 상태를 관리하고, $ 접두사를 사용해 TextField에 바인딩을 전달합니다. 이는 기본적인 SwiftUI의 데이터 흐름 방식입니다.


StateObject, ObservableObject

struct ContentView: View {
    @StateObject private var viewModel = SearchViewModel()
    var body: some View {
        TextField("Enter text", text: $viewModel.query)
        Text(viewModel.query)
    }
}

class SearchViewModel : ObservableObject {
    @Published var query: String = ""
}

iOS 17 이전에는 클래스 기반 데이터 모델을 사용할 때 ObservableObject 프로토콜과 @Published 프로퍼티 래퍼를 사용했습니다.

이 방식은 여전히 유효하지만, iOS 17에서는 더 간단한 @Observable 매크로가 도입되었습니다.


@Observable

import SwiftUI

struct ContentView: View {
    @State var viewModel = SearchViewModel()
    var body: some View {
        VStack {
            SearchField(query: $viewModel.query)
            Text(viewModel.query)
        }
    }
}

struct SearchField: View {
    @Binding var query: String

    var body: some View {
        TextField("Search Query", text: $query)
            .textFieldStyle(.roundedBorder)
            .padding()
    }
}

@Observable
class SearchViewModel{
    var query: String = ""
}

@Observable 매크로를 사용하면 ObservableObject와 @Published를 사용하지 않고도 클래스의 속성 변경을 추적할 수 있습니다.

위 코드에서는 @State로 viewModel을 관리하고, $viewModel.query를 통해 바인딩을 전달합니다.


@Bindable이 필요한 상황

하지만 다음과 같은 상황에서 문제가 발생합니다.

import SwiftUI

struct ContentView: View {
    @State var viewModel = SearchViewModel()
    var body: some View {
        VStack {
            SearchView(viewModel: viewModel)
            Text(viewModel.query)
        }
    }
}

struct SearchView: View {
    let viewModel: SearchViewModel
    var body: some View {
        SearchField(query: $viewModel.query) // 컴파일 에러 발생!
    }
}

struct SearchField: View {
    @Binding var query: String

    var body: some View {
        TextField("Search Query", text: $query)
            .textFieldStyle(.roundedBorder)
            .padding()
    }
}

@Observable
class SearchViewModel{
    var query: String = ""
}

여기서 SearchView는 viewModel을 일반 속성으로 받고 있기 때문에 $viewModel.query와 같은 바인딩 접근이 불가능합니다.

컴파일러는 "Value of type 'SearchViewModel' has no member '$'" 같은 오류를 표시합니다.


해결 방법 1: 수동 바인딩 생성

이 문제를 해결하기 위한 한 가지 방법은 수동으로 바인딩을 생성하는 것입니다:

struct SearchView: View {
    let viewModel: SearchViewModel
    var body: some View {
        SearchField(query: Binding(
            get: { viewModel.query },
            set: { newValue in viewModel.query = newValue }
        ))
    }
}

이 방식은 작동하지만 코드가 장황해지고, 여러 속성에 대해 반복해야 한다면 매우 번거롭습니다.


해결 방법 2: @Bindable 사용

@Bindable을 사용하면 이 문제를 간단하게 해결할 수 있습니다:

struct SearchView: View {
    @Bindable var viewModel: SearchViewModel
    var body: some View {
        SearchField(query: $viewModel.query) // 정상 작동!
    }
}

또는 지역 변수로 선언할 수도 있습니다:

struct SearchView: View {
    let viewModel: SearchViewModel
    var body: some View {
        @Bindable var viewModel = viewModel
        SearchField(query: $viewModel.query) // 정상 작동!
    }
}ㅂ

@Bindable은 Observable 객체에 대한 바인딩 접근자($)를 제공하여, 프로퍼티 래퍼로 감싸지 않은 일반 속성이나 지역 변수에서도 바인딩을 생성할 수 있게 해줍니다.


@Bindable의 장점과 활용

1. 코드 간결성

@Bindable을 사용하면 수동으로 바인딩을 생성하는 번거로운 코드를 작성할 필요가 없습니다. 특히 여러 속성에 바인딩이 필요한 경우 코드가 훨씬 간결해집니다.

2. 유연한 적용 범위

@Bindable은 다양한 상황에서 사용할 수 있습니다:

  • 프로퍼티에 적용
  • 지역 변수에 적용
  • 함수 매개변수에 적용
  • 전역 변수에 적용

3. 데이터 흐름의 명확성

@Bindable을 사용하면 데이터가 어디서 오는지, 어떻게 변경되는지 더 명확하게 표현할 수 있습니다. 특히 복잡한 뷰 계층에서 데이터 흐름을 추적하기 쉬워집니다.

4. 성능 최적화

@Observable과 @Bindable의 조합은 SwiftUI의 뷰 업데이트 메커니즘을 최적화합니다. 변경된 속성에 대해서만 뷰가 업데이트되므로 불필요한 렌더링을 방지할 수 있습니다.


실제 활용 예시

다음은 @Bindable을 활용한 좀 더 복잡한 예시입니다:

@Observable
class UserProfile {
    var name: String = ""
    var email: String = ""
    var notificationsEnabled: Bool = true
    var theme: Theme = .light

    enum Theme: String, CaseIterable {
        case light, dark, system
    }
}

struct ProfileEditView: View {
    @Bindable var profile: UserProfile

    var body: some View {
        Form {
            Section("기본 정보") {
                TextField("이름", text: $profile.name)
                TextField("이메일", text: $profile.email)
            }

            Section("설정") {
                Toggle("알림 활성화", isOn: $profile.notificationsEnabled)

                Picker("테마", selection: $profile.theme) {
                    ForEach(UserProfile.Theme.allCases, id: \\\\.self) { theme in
                        Text(theme.rawValue.capitalized)
                    }
                }
            }
        }
    }
}

struct ContentView: View {
    @State private var userProfile = UserProfile()

    var body: some View {
        NavigationStack {
            ProfileEditView(profile: userProfile)
                .navigationTitle("프로필 편집")
        }
    }
}

이 예시에서 ProfileEditView는 @Bindable을 사용하여 UserProfile 객체의 여러 속성에 바인딩을 생성합니다.

이를 통해 사용자가 폼을 통해 프로필 정보를 쉽게 편집할 수 있습니다.


결론

@Bindable은 iOS 17에서 도입된 SwiftUI의 새로운 프로퍼티 래퍼로, @Observable 매크로와 함께 사용하여 데이터 바인딩을 더 간결하고 유연하게 만들어줍니다. 특히 Observable 객체를 프로퍼티 래퍼 없이 전달받는 경우에 바인딩 접근을 가능하게 해주어 코드의 가독성과 유지보수성을 크게 향상시킵니다.

 

따라서, iOS 17 이전에는

값 타입은 @State, @Binding을 사용하고, 참조 타입은 @StateObject 와 @ObservedObject를 사용했으나

 

iOS 17 이후 Bindable이 나온 이후부터는

값타입은 @State 와 @ Binding, 참조타입은 @State 와 Bindable을 사용할 수 있게 되었습니다.

 

이로인해 우리는 StateObject와 ObservedObject에 대한 의존도를 많이 낮출수 있으나, 아직 현업에서는 iOS 17 미만을 지원하는 앱이 많기 때문에, StateObject와 ObservedObject에 대한 학습도 필요하다.


참고

https://developer.apple.com/documentation/swiftui/bindable

 

Bindable | Apple Developer Documentation

A property wrapper type that supports creating bindings to the mutable properties of observable objects.

developer.apple.com

https://www.youtube.com/watch?v=YgrnC1hIFEY


오늘도 화이팅입니다!

728x90
반응형

'iOS > SwiftUI' 카테고리의 다른 글

[SwiftUI] View & Modifiers  (0) 2025.05.13
[SwiftUI] StateObject  (0) 2025.04.21
[SwiftUI] 상태 관리 - PropertyWrapper (2)  (0) 2025.04.17
[SwiftUI] 상태 관리 - PropertyWrapper (1)  (0) 2025.04.17
[SwiftUI] UI 상태 관리 (Managing user interface state)  (0) 2025.04.15
'iOS/SwiftUI' 카테고리의 다른 글
  • [SwiftUI] View & Modifiers
  • [SwiftUI] StateObject
  • [SwiftUI] 상태 관리 - PropertyWrapper (2)
  • [SwiftUI] 상태 관리 - PropertyWrapper (1)
kimsangjunzzang
kimsangjunzzang
루피 님의 블로그 입니다.
  • kimsangjunzzang
    루피 님의 블로그
    kimsangjunzzang
  • 전체
    오늘
    어제
    • 분류 전체보기 (91) N
      • iOS (55)
        • Swift (26)
        • UIKit (9)
        • SwiftUI (8)
        • RxSwift (12)
      • FE (8)
        • 모던 자바스크립트 (3)
        • HTML (5)
      • Operating System (1)
      • 트러블 슈팅 (4) N
      • 바로 안 나오면 모르는거다 (4)
      • Algorithm (16)
      • 회고록 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    디자인 패턴
    state
    주니어 개발자
    회고록
    rxswift
    면접
    오블완
    closure
    HTML
    SwiftUI
    Delegate
    CS
    arc
    백준
    알고리즘
    swift
    ViewController
    DispatchQueue
    Algorithm
    AppleDeveloperAcademy
    life cycle
    C++
    gcd
    uikit
    JavaScript
    FP
    ios
    티스토리챌린지
    web
    boj
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
kimsangjunzzang
[SwiftUI] Bindable
상단으로

티스토리툴바