Created
April 10, 2024 18:29
-
-
Save chockenberry/7c8a32cb67340e4275d5ac4506b46dd7 to your computer and use it in GitHub Desktop.
SwiftUI Protocol Bindings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
works anywhere in the view body.