Skip to content

Instantly share code, notes, and snippets.

@swiftui-lab
Last active September 26, 2022 11:28
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save swiftui-lab/80d1b11aa0087f63577fbf257758391f to your computer and use it in GitHub Desktop.
Save swiftui-lab/80d1b11aa0087f63577fbf257758391f to your computer and use it in GitHub Desktop.
// The SwiftUI Lab: https://swiftui-lab.com
// Article: Inspecting the View Tree – Part 3
// https://swiftui-lab.com/communicating-with-the-view-tree-part-3/
import SwiftUI
enum MyViewType: Equatable {
case formContainer // main container
case fieldContainer // contains a text label + text field
case field(Int) // text field (with an associated value that indicates the character count in the field)
case title // form title
case miniMapArea // view placed behind the minimap elements
}
struct MyPreferenceData: Identifiable {
let id = UUID() // required when using ForEach later
let vtype: MyViewType
let bounds: Anchor<CGRect>
// Calculate the color to use in the minimap, for each view type
func getColor() -> Color {
switch vtype {
case .field(let length):
return length == 0 ? .red : (length < 3 ? .yellow : .green)
case .title:
return .purple
default:
return .gray
}
}
// Returns true, if this view type must be shown in the minimap.
// Only fields, field containers and the title are shown in the minimap
func show() -> Bool {
switch vtype {
case .field:
return true
case .title:
return true
case .fieldContainer:
return true
default:
return false
}
}
}
struct MyPreferenceKey: PreferenceKey {
typealias Value = [MyPreferenceData]
static var defaultValue: [MyPreferenceData] = []
static func reduce(value: inout [MyPreferenceData], nextValue: () -> [MyPreferenceData]) {
value.append(contentsOf: nextValue())
}
}
// This view draws a rounded box, with a label and a textfield
struct MyFormField: View {
@Binding var fieldValue: String
let label: String
var body: some View {
VStack(alignment: .leading) {
Text(label)
TextField("", text: $fieldValue)
.textFieldStyle(RoundedBorderTextFieldStyle())
.anchorPreference(key: MyPreferenceKey.self, value: .bounds) {
return [MyPreferenceData(vtype: .field(self.fieldValue.count), bounds: $0)]
}
}
.padding(15)
.background(RoundedRectangle(cornerRadius: 15).fill(Color(white: 0.9)))
.transformAnchorPreference(key: MyPreferenceKey.self, value: .bounds) {
$0.append(MyPreferenceData(vtype: .fieldContainer, bounds: $1))
}
}
}
struct ContentView : View {
@State private var fieldValues = Array<String>(repeating: "", count: 5)
@State private var length: Float = 360
@State private var twitterFieldPreset = false
var body: some View {
VStack {
Spacer()
HStack(alignment: .center) {
// This view puts a gray rectangle where the minimap elements will be.
// We will reference its size and position later, to make sure the mini map elements
// are overlayed right on top of it.
Color(white: 0.7)
.frame(width: 200)
.anchorPreference(key: MyPreferenceKey.self, value: .bounds) {
return [MyPreferenceData(vtype: .miniMapArea, bounds: $0)]
}
.padding(.horizontal, 30)
// Form Container
VStack(alignment: .leading) {
// Title
VStack {
Text("Hello \(fieldValues[0]) \(fieldValues[1]) \(fieldValues[2])")
.font(.title).fontWeight(.bold)
.anchorPreference(key: MyPreferenceKey.self, value: .bounds) {
return [MyPreferenceData.init(vtype: .title, bounds: $0)]
}
Divider()
}
// Switch + Slider
HStack {
Toggle(isOn: $twitterFieldPreset) { Text("") }
Slider(value: $length, in: 360...540).layoutPriority(1)
}.padding(.bottom, 5)
// First row of text fields
HStack {
MyFormField(fieldValue: $fieldValues[0], label: "First Name")
MyFormField(fieldValue: $fieldValues[1], label: "Middle Name")
MyFormField(fieldValue: $fieldValues[2], label: "Last Name")
}.frame(width: 540)
// Second row of text fields
HStack {
MyFormField(fieldValue: $fieldValues[3], label: "Email")
if twitterFieldPreset {
MyFormField(fieldValue: $fieldValues[4], label: "Twitter")
}
}.frame(width: CGFloat(length))
}.transformAnchorPreference(key: MyPreferenceKey.self, value: .bounds) {
$0.append(MyPreferenceData(vtype: .formContainer, bounds: $1))
}
Spacer()
}
.overlayPreferenceValue(MyPreferenceKey.self) { preferences in
GeometryReader { geometry in
MiniMap(geometry: geometry, preferences: preferences)
}
}
Spacer()
}.background(Color(white: 0.8)).edgesIgnoringSafeArea(.all)
}
}
struct MiniMap: View {
let geometry: GeometryProxy
let preferences: [MyPreferenceData]
var body: some View {
// Get the form container preference
guard let formContainerAnchor = preferences.first(where: { $0.vtype == .formContainer })?.bounds else { return AnyView(EmptyView()) }
// Get the minimap area container
guard let miniMapAreaAnchor = preferences.first(where: { $0.vtype == .miniMapArea })?.bounds else { return AnyView(EmptyView()) }
// Calcualte a multiplier factor to scale the views from the form, into the minimap.
let factor = geometry[formContainerAnchor].size.width / (geometry[miniMapAreaAnchor].size.width - 10.0)
// Determine the position of the form
let containerPosition = CGPoint(x: geometry[formContainerAnchor].minX, y: geometry[formContainerAnchor].minY)
// Determine the position of the mini map area
let miniMapPosition = CGPoint(x: geometry[miniMapAreaAnchor].minX, y: geometry[miniMapAreaAnchor].minY)
// -------------------------------------------------------------------------------------------------
// iOS 13 Beta 5 Release Notes. Known Issues:
// Using a ForEach view with a complex expression in its closure can may result in compiler errors.
// Workaround: Extract those expressions into their own View types. (53325810)
// -------------------------------------------------------------------------------------------------
// The following view had to be encapsulated in two separate functions (miniMapView & rectangleView),
// because beta 5 has a bug that fails to compile expressions that are "too complex".
return AnyView(miniMapView(factor, containerPosition, miniMapPosition))
}
func miniMapView(_ factor: CGFloat, _ containerPosition: CGPoint, _ miniMapPosition: CGPoint) -> some View {
ZStack(alignment: .topLeading) {
// Create a small representation of each of the form's views.
// Preferences are traversed in reverse order, otherwise the branch views
// would be covered by their ancestors
ForEach(preferences.reversed()) { pref in
if pref.show() { // some type of views, we don't want to show
self.rectangleView(pref, factor, containerPosition, miniMapPosition)
}
}
}.padding(5)
}
func rectangleView(_ pref: MyPreferenceData, _ factor: CGFloat, _ containerPosition: CGPoint, _ miniMapPosition: CGPoint) -> some View {
Rectangle()
.fill(pref.getColor())
.frame(width: self.geometry[pref.bounds].size.width / factor,
height: self.geometry[pref.bounds].size.height / factor)
.offset(x: (self.geometry[pref.bounds].minX - containerPosition.x) / factor + miniMapPosition.x,
y: (self.geometry[pref.bounds].minY - containerPosition.y) / factor + miniMapPosition.y)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment