536просмотров
10 декабря 2025 г.
statsScore: 590
SwiftUI Bindings 2 Не могу завершить эту тему, не рассказав про еще один лайфхак, связанный с работой Bindings. Вторая популярная ситуация, когда хочется использовать Binding(get:set:), — это случаи, когда ваш стейт нельзя мутировать напрямую. Такое ограничение может быть вызвано разными причинами, например, сложной логикой управления этим стейтом или необходимостью выполнения сайд-эффектов. Для лучшего понимания рассмотрим такой простой пример: @Observable
final class DetentsHolder { private(set) var detents: Set<PresentationDetent> = [.medium, .large] func insertDetent(_ detent: PresentationDetent) { detents.insert(detent) } func removeDetent(_ detent: PresentationDetent) { detents.remove(detent) }
} И его использование во View: struct ContentView: View { @State var holder = DetentsHolder() var body: some View { Nested( text: "Enable Large Detent", value: Binding( get: { holder.detents.contains(.large) }, set: { newValue in if newValue { holder.insertDetent(.large) } else { holder.removeDetent(.large) } })) }
} И, как мы помним, это очень неэффективно. Но альтернативой будет куча бойлерплейта (особенно если ваш минимальный таргет — iOS 16): struct ContentView: View { @State var holder = DetentsHolder() @State var isLargeDetentEnabled = false var body: some View { Nested( text: "Enable Large Detent", value: $isLargeDetentEnabled ) .onAppear { // Sync initial value from holder. isLargeDetentEnabled = holder.detents.contains(.large) } .onChange(of: holder.detents) { newValue in // Sync value changes from holder. isLargeDetentEnabled = holder.detents.contains(.large) } .onChange(of: isLargeDetentEnabled) { newValue in // Update value on holder. if newValue { holder.insertDetent(.large) } else { holder.removeDetent(.large) } } }
} Причем этот бойлерплейт решает только часть проблем. Да, теперь Nested не будет пересчитываться каждый раз при изменении ContentView. Но любое изменение биндинга в Nested будет триггерить аж два обновления для ContentView: первое — на изменение isLargeDetentEnabled, а второе — на изменение holder.detents. Это происходит из-за того, что оба этих стейта захватываются в onChange(of:). Каких-то более красивых способов решения этой проблемы у меня не было, и я очень удивился, когда в одном из комментариев к прошлому посту нашел решение через всё те же subscriptы. private extension DetentsHolder { subscript(contains element: PresentationDetent) -> Bool { get { detents.contains(element) } set { if newValue { insertDetent(element) } else { removeDetent(element) } } }
} struct ContentView: View { @State var holder = DetentsHolder() var body: some View { Nested( text: "Enable Large Detent", value: $holder[contains: .large] ) }
} Такое решение работает почти так же эффективно, как и обычные биндинги, и не вызывает лишних обновлений. Почему почти? Потому что при изменении биндинга в Nested, ContentView все равно пересчитывается. Это происходит из-за того, что внутри $holder[contains: .large] происходит обращение к subscript(contains:).get, и это обращение отслеживается неявным withObservationTracking(_:onChange:) внутри View.body. В итоге ContentView начинает реагировать на изменения DetentsHolder.detents, хотя явным образом никак их не использует. В целом, это тоже можно поправить, если лишних вычислений производить совсем не хочется. Для этого нужно обернуть Nested в какую-то другую вью: struct NestedWrapper: View { let text: String @Binding var holder: DetentsHolder var body: some View { Nested(text, value: $holder[contains: .large]) }
}