Skip to content

Instantly share code, notes, and snippets.

@chockenberry
Created April 10, 2024 18:29
Show Gist options
  • Save chockenberry/7c8a32cb67340e4275d5ac4506b46dd7 to your computer and use it in GitHub Desktop.
Save chockenberry/7c8a32cb67340e4275d5ac4506b46dd7 to your computer and use it in GitHub Desktop.
SwiftUI Protocol Bindings
//
// ContentView.swift
// ProtocolBinding
//
// Created by Craig Hockenberry on 4/10/24.
//
import SwiftUI
protocol ShapeProtocol {
func draw()
}
@Observable
class Shape {
var point = CGPoint.zero
var title: String = "Untitled"
}
@Observable
class Box: Shape, ShapeProtocol {
var size = CGSize.zero
func draw() {}
}
@Observable
class Circle: Shape, ShapeProtocol {
var radius: CGFloat = 0
func draw() {}
}
struct ContentView: View {
@State var shapes: [ShapeProtocol]
@State var selectedShape: Shape
@State var selectedIndex = 0
init() {
let shapes: [ShapeProtocol] = [ Circle(), Box(), Circle() ]
_shapes = State(initialValue: shapes)
_selectedShape = State(initialValue: shapes[0] as! Shape)
}
var body: some View {
VStack {
Text("Shape \(selectedIndex):")
TextField("Title:", text: $selectedShape.title).border(.secondary)
Slider(value: $selectedShape.point.x, in: 0.0...100.0)
Text("x: \(selectedShape.point.x)")
Slider(value: $selectedShape.point.y, in: 0.0...100.0)
Text("y: \(selectedShape.point.y)")
if let box = selectedShape as? Box {
Text("Box:")
/*
BEGIN: Binding code here is repetitive:
*/
let boxBinding = Binding {
selectedShape as! Box
} set: { newBox in
selectedShape = newBox
}
/*
END: Binding code here is repetitive:
*/
/*
This works, but feels wrong (selectedShape and Type are side effects)
let boxBinding = selectedBinding(box)
*/
VStack {
Slider(value: boxBinding.size.width, in: 0.0...100.0)
Text("width: \(box.size.width)")
}
VStack {
Slider(value: boxBinding.size.height, in: 0.0...100.0)
Text("height: \(box.size.height)")
}
}
else if let circle = selectedShape as? Circle {
Text("Circle:")
/*
BEGIN: Binding code here is repetitive:
*/
let circleBinding = Binding {
selectedShape as! Circle
} set: { newCircle in
selectedShape = newCircle
}
/*
END: Binding code here is repetitive:
*/
VStack {
Slider(value: circleBinding.radius, in: 0.0...100.0)
Text("radius: \(circle.radius)")
}
}
Button("Change Shape") {
selectedIndex += 1
if selectedIndex >= shapes.count {
selectedIndex = 0
}
selectedShape = shapes[selectedIndex] as! Shape
}
.buttonStyle(.borderedProminent)
}
.padding()
}
func selectedBinding<T: Shape>(_ value: T) -> Binding<T> {
return Binding<T> {
selectedShape as! T
} set: { newValue in
selectedShape = newValue
}
}
}
#Preview {
ContentView()
}
@chockenberry
Copy link
Author

chockenberry commented Apr 10, 2024

The trick is to use @Bindable to create the bindings as needed (instead of doing it manually). For example, do this at line 72:

		@Bindable var boxBinding = box
		VStack {
			Slider(value: $boxBinding.size.width, in: 0.0...100.0)
			Text("width: \(box.size.width)")
		}

@Bindable works anywhere in the view body.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment