Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save seanwoodward/74ae15843eca463e70b83f59202134e7 to your computer and use it in GitHub Desktop.
Save seanwoodward/74ae15843eca463e70b83f59202134e7 to your computer and use it in GitHub Desktop.
A few updates to such a great example
// The SwiftUI Lab
// Website: https://swiftui-lab.com
// Article: https://swiftui-lab.com/alignment-guides
import SwiftUI
import Observation
extension EnvironmentValues {
@Entry var alignmentModel: Model = Model()
}
@Observable
class Model {
var minimumContainer = true
var extendedTouchBar = false
var twoPhases = true
var addImplicitView = false {
didSet {
guard !addImplicitView else { return }
showImplicit = false
}
}
var showImplicit = false
var algn: [AlignmentEnum] = [.center, .center, .center]
var delayedAlgn: [AlignmentEnum] = [.center, .center, .center]
var frameAlignment: Alignment = .center
var stackAlignment: HorizontalAlignment = .leading
@available(*, deprecated, message: "not used")
func nextAlignment() -> Alignment {
switch frameAlignment {
case .leading:
.center
case .center:
.trailing
default:
.leading
}
}
struct Defualts: Equatable {
static let minimumContainer = true
static let extendedTouchBar = false
static let twoPhases = true
static let addImplicitView = false
static let showImplicit = false
static let algn: [AlignmentEnum] = [.center, .center, .center]
static let delayedAlgn: [AlignmentEnum] = [.center, .center, .center]
static let frameAlignment = Alignment.center
static let stackAlignment = HorizontalAlignment.leading
static func isDefaults(_ model: Model) -> Bool {
model.minimumContainer == Defualts.minimumContainer &&
model.extendedTouchBar == Defualts.extendedTouchBar &&
model.twoPhases == Defualts.twoPhases &&
model.addImplicitView == Defualts.addImplicitView &&
model.showImplicit == Defualts.showImplicit &&
model.algn == Defualts.algn &&
model.delayedAlgn == Defualts.delayedAlgn &&
model.frameAlignment == Defualts.frameAlignment &&
model.stackAlignment == Defualts.stackAlignment
}
}
var showResetButton: Bool {
!Defualts.isDefaults(self)
}
func resetToDefaults() {
withAnimation {
minimumContainer = Defualts.minimumContainer
extendedTouchBar = Defualts.extendedTouchBar
twoPhases = Defualts.twoPhases
addImplicitView = Defualts.addImplicitView
showImplicit = Defualts.showImplicit
algn = Defualts.algn
delayedAlgn = Defualts.delayedAlgn
frameAlignment = Defualts.frameAlignment
stackAlignment = Defualts.stackAlignment
}
}
}
extension Alignment {
var asString: String {
switch self {
case .leading:
".leading"
case .center:
".center"
case .trailing:
".trailing"
default:
"unknown"
}
}
}
extension HorizontalAlignment {
var asString: String {
switch self {
case .leading:
".leading"
case .trailing:
".trailing"
case .center:
".center"
default:
"unknown"
}
}
}
extension HorizontalAlignment: @retroactive Hashable {
public func hash(into hasher: inout Hasher) {
switch self {
case .leading:
hasher.combine(0)
case .center:
hasher.combine(1)
case .trailing:
hasher.combine(2)
default:
hasher.combine(3)
}
}
}
extension Alignment: @retroactive Hashable {
public func hash(into hasher: inout Hasher) {
switch self {
case .leading:
hasher.combine(0)
case .center:
hasher.combine(1)
case .trailing:
hasher.combine(2)
default:
hasher.combine(3)
}
}
}
struct ContentView: View {
var body: some View {
Group {
if UIDevice.current.userInterfaceIdiom == .pad
{
GeometryReader { proxy in
VStack(spacing: 0) {
Color.clear
.frame(maxWidth: .infinity)
.frame(height: 1)
.background(Color(uiColor: .secondarySystemBackground))
HStack(spacing: 0) {
ControlsView()
.frame(width: 380)
.frame(maxHeight: .infinity, alignment: .center)
.layoutPriority(1)
.background(Color(uiColor: .secondarySystemBackground))
VStack {
DisplayView(width: proxy.size.width - 401)
.frame(maxWidth: proxy.size.width - 401)
}
.frame(height: proxy.size.height - 300)
.background(Color(uiColor: .systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 30))
.shadow(radius: 10)
.padding(.trailing, 20)
Color.clear
.frame(maxHeight: .infinity)
.frame(width: 1)
.background(Color(uiColor: .secondarySystemBackground))
}
.frame(height: (proxy.size.height - 300))
VStack {
CodeView()
.frame(height: 300)
.padding(.top)
}
.frame(width: proxy.size.width, alignment: .center)
}
.background(Color(uiColor: .secondarySystemBackground))
}
} else {
VStack(spacing: 30) {
Text("I need an iPad to run!")
Text("😟").scaleEffect(2)
}
.font(.largeTitle)
}
}
}
}
extension Animation {
static let alignment = Animation.easeInOut(duration: 0.5)
}
struct ControlsView: View {
@Environment(\.alignmentModel) var model
var body: some View {
@Bindable var model = model
VStack {
VStack(spacing: 10) {
ZStack {
if model.showResetButton {
Button("reset", action: model.resetToDefaults)
.frame(maxWidth: .infinity, alignment: .trailingLastTextBaseline)
.transition(.opacity.combined(with: .move(edge: .trailing)))
}
Text("Settings")
.font(.title)
.frame(maxWidth: .infinity, alignment: .centerLastTextBaseline)
}
Toggle(isOn: $model.minimumContainer, label: { Text("Narrow Container") })
Toggle(isOn: $model.extendedTouchBar, label: { Text("Extended Bar") })
Toggle(isOn: $model.twoPhases, label: { Text("Show in Two Phases") })
Toggle(isOn: $model.addImplicitView, label: { Text("Include Implicitly View") })
if self.model.addImplicitView {
Toggle(isOn: $model.showImplicit, label: { Text("Show Implicit Guides") })
//.disabled(!self.model.addImplicitView)
}
VStack {
Text("Frame Alignment")
Picker(selection: $model.frameAlignment.animation(.alignment), label: EmptyView()) {
Text(".leading").tag(Alignment.leading)
Text(".center").tag(Alignment.center)
Text(".trailing").tag(Alignment.trailing)
}
.pickerStyle(SegmentedPickerStyle())
}
VStack {
Text("Stack Alignment")
Picker(selection: $model.stackAlignment.animation(.alignment), label: EmptyView()) {
Text(".leading").tag(HorizontalAlignment.leading)
Text(".center").tag(HorizontalAlignment.center)
Text(".trailing").tag(HorizontalAlignment.trailing)
}
.pickerStyle(SegmentedPickerStyle())
}
}
.padding(20)
}
.frame(alignment: .top)
.background(Color(uiColor: .systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 20))
.shadow(radius: 10)
.padding(.horizontal, 20)
.animation(.alignment, value: model.showResetButton)
.animation(.alignment, value: model.addImplicitView)
}
}
struct CodeView: View {
@Environment(\.alignmentModel) var model
let indent: @Sendable (ViewDimensions) -> CGFloat = { _ in 40 }
var body: some View {
VStack(alignment: .leading) {
VStack(alignment: .leading) {
Text("VStack(alignment: \(model.stackAlignment.asString) {")
.padding(.top, 5)
.frame(alignment: .leading)
.alignmentGuide(.leading, computeValue: indent)
CodeFragment(idx: 0)
CodeFragment(idx: 1)
if model.addImplicitView {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) {
Text("SomeView()")
.foregroundColor(.primary)
.transition(.opacity)
if model.showImplicit {
Group {
Text(".alignmentGuide(\(model.stackAlignment.asString), computedValue: { d in ")
Text("d[\(model.stackAlignment.asString)]").padding(.horizontal, 5)
Text(" }")
}
.foregroundStyle(.secondary)
.transition(.opacity)
}
}
.padding(.vertical, 5)
}
.transition(.opacity)
.animation(.alignment, value: model.showImplicit)
}
CodeFragment(idx: 2)
HStack(spacing: 0) {
Text("}.frame(alignment: ")
Text("\(self.model.frameAlignment.asString)").padding(5)
Text(")")
}
.frame(alignment: .leading)
.alignmentGuide(.leading, computeValue: indent)
}
.padding(20)
}
.font(Font.custom("Menlo", size: 16))
.background(Color(uiColor: .systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 20))
.shadow(radius: 10)
.animation(.alignment, value: model.showImplicit)
.animation(.alignment, value: model.addImplicitView)
}
}
struct CodeFragment: View {
@Environment(\.alignmentModel) var model: Model
var idx: Int
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) {
Text("SomeView()")
Text(".alignmentGuide(\(self.model.stackAlignment.asString), computedValue: { d in ")
Text("\(self.model.algn[idx].asString)")
.padding(5)
.background(self.model.algn[idx] != self.model.delayedAlgn[idx] ? Color.yellow : Color.clear)
Text(" }")
}
}
}
}
struct DisplayView: View {
@Environment(\.alignmentModel) var model
let width: CGFloat
var body: some View {
VStack {
VStack(alignment: model.stackAlignment, spacing: 20) {
Block(algn: binding(0))
.frame(width: 250)
.alignmentGuide(model.stackAlignment, computeValue: { [delayedAlgn = model.delayedAlgn[0]] d in delayedAlgn.computedValue(d) })
Block(algn: binding(1))
.frame(width: 200)
.alignmentGuide(model.stackAlignment, computeValue: { [delayedAlgn = model.delayedAlgn[1]] d in delayedAlgn.computedValue(d) })
if model.addImplicitView {
RoundedRectangle(cornerRadius: 8)
.fill(Color.gray)
.frame(width: 250, height: 50)
.overlay(Text("Implicitly Aligned").foregroundColor(.white))
.overlay(Marker(algn: AlignmentEnum.fromHorizontalAlignment(model.stackAlignment)).opacity(0.5))
.transition(.scale.combined(with: .opacity))
}
Block(algn: binding(2)).frame(width: 300)
.alignmentGuide(model.stackAlignment, computeValue: { [delayedAlgn = model.delayedAlgn[2]] d in delayedAlgn.computedValue(d) })
}
.padding(2)
}
.frame(width: model.minimumContainer ? nil : width, alignment: model.frameAlignment)
.border(Color.red)
.animation(.alignment, value: model.addImplicitView)
.animation(.alignment, value: model.minimumContainer)
.animation(.alignment, value: model.frameAlignment)
.animation(.alignment, value: model.stackAlignment)
}
func binding(_ idx: Int) -> Binding<AlignmentEnum> {
return Binding<AlignmentEnum>(get: {
model.algn[idx]
}, set: { v in
model.algn[idx] = v
let delay = model.twoPhases ? 500 : 0
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay)) {
withAnimation(.alignment
) {
model.delayedAlgn[idx] = v
}
}
})
}
}
struct Block: View {
@Binding var algn: AlignmentEnum
let a = Animation.alignment
var body: some View {
let gesture = DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onEnded({ v in
withAnimation(a) {
algn = .rawValue(v.startLocation.x)
}
})
VStack(spacing: 0) {
HStack {
AlignButton(label: "L", action: { withAnimation(a) { algn = .leading } })
Spacer()
AlignButton(label: "C", action: { withAnimation(a) { algn = .center } })
Spacer()
AlignButton(label: "T", action: { withAnimation(a) { algn = .trailing } })
}
.padding(5)
.padding(.bottom, 20)
}
.background(RoundedRectangle(cornerRadius: 8).foregroundColor(.gray))
.overlay(
TouchBar()
.gesture(gesture)
)
.overlay(
Marker(algn: algn)
.opacity(0.5)
)
}
}
struct TouchBar: View {
@Environment(\.alignmentModel) var model
@State private var flag = false
var body: some View {
GeometryReader { proxy in
UnevenRoundedRectangle(cornerRadii: .init(topLeading: 0, bottomLeading: 8, bottomTrailing: 8, topTrailing: model.extendedTouchBar ? 8 : 0))
.foregroundColor(.yellow)
.frame(width: proxy.size.width + (model.extendedTouchBar ? 100 : 0), height: 20)
.offset(x: 0, y: proxy.size.height - 20)
.animation(.easeOut(duration: 0.5), value: model.extendedTouchBar)
}
}
}
struct AlignButton: View {
let label: String
let action: () -> ()
var body: some View {
Button(action: action) {
Text(label)
.foregroundColor(.black)
.padding(10)
.background(
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.green)
)
}
}
}
struct Marker: View {
let algn: AlignmentEnum
var body: some View {
GeometryReader { proxy in
MarkerLine().offset(x: algn.value(for: proxy.size.width))
}
}
}
struct MarkerLine: Shape {
func path(in rect: CGRect) -> Path {
var p = Path()
p.move(to: CGPoint(x: 0, y: 0))
p.addLine(to: CGPoint(x: 0, y: rect.maxY))
p = p.strokedPath(.init(lineWidth: 4, lineCap: .round, lineJoin: .bevel, miterLimit: 1, dash: [6, 6], dashPhase: 3))
return p
}
}
enum AlignmentEnum: Hashable, Sendable {
case leading
case center
case trailing
case rawValue(CGFloat)
var asString: String {
switch self {
case .leading: "d[.leading]"
case .center: "d[.center]"
case .trailing: "d[.trailing]"
case .rawValue(let v): "\(v)"
}
}
func value(for width: CGFloat) -> CGFloat {
switch self {
case .leading: 0
case .center: width / 2.0
case .trailing: width
case .rawValue(let v): v
}
}
func computedValue(_ d: ViewDimensions) -> CGFloat {
switch self {
case .leading: d[.leading]
case .center: d.width / 2.0
case .trailing: d[.trailing]
case .rawValue(let v): v
}
}
static func fromHorizontalAlignment(_ a: HorizontalAlignment) -> AlignmentEnum {
switch a {
case .leading: .leading
case .center: .center
case .trailing: .trailing
default: .rawValue(0)
}
}
}
#Preview(traits: .landscapeLeft) {
ContentView()
.environment(\.alignmentModel, Model())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment