Skip to content

Instantly share code, notes, and snippets.

@swiftui-lab
Created September 16, 2019 13:14
Show Gist options
  • Save swiftui-lab/793ca53ad1f2f0d7eb07aa23b54d9cbf to your computer and use it in GitHub Desktop.
Save swiftui-lab/793ca53ad1f2f0d7eb07aa23b54d9cbf to your computer and use it in GitHub Desktop.

Revisions

  1. swiftui-lab created this gist Sep 16, 2019.
    401 changes: 401 additions & 0 deletions alignment-guides-tool.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,401 @@
    // The SwiftUI Lab
    // Website: https://swiftui-lab.com
    // Article: https://swiftui-lab.com/alignment-guides

    import SwiftUI

    class Model: ObservableObject {
    @Published var minimumContainer = true
    @Published var extendedTouchBar = false
    @Published var twoPhases = true
    @Published var addImplicitView = false
    @Published var showImplicit = false

    @Published var algn: [AlignmentEnum] = [.center, .center, .center]
    @Published var delayedAlgn: [AlignmentEnum] = [.center, .center, .center]
    @Published var frameAlignment: Alignment = .center
    @Published var stackAlignment: HorizontalAlignment = .leading

    func nextAlignment() -> Alignment {
    if self.frameAlignment == .leading {
    return .center
    } else if self.frameAlignment == .center {
    return .trailing
    } else {
    return .leading
    }
    }
    }

    extension Alignment {
    var asString: String {
    switch self {
    case .leading:
    return ".leading"
    case .center:
    return ".center"
    case .trailing:
    return ".trailing"
    default:
    return "unknown"
    }
    }
    }

    extension HorizontalAlignment {
    var asString: String {
    switch self {
    case .leading:
    return ".leading"
    case .trailing:
    return ".trailing"
    case .center:
    return ".center"
    default:
    return "unknown"
    }
    }
    }

    extension HorizontalAlignment: 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: 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) {
    HStack(spacing: 0) {
    ControlsView().frame(width: 380).layoutPriority(1).background(Color(UIColor.secondarySystemBackground))

    DisplayView(width: proxy.size.width - 380).frame(maxWidth: proxy.size.width - 380).clipShape(Rectangle())//.border(Color.green, width: 3)

    }.frame(height: (proxy.size.height - 300))

    VStack {
    CodeView().frame(height: 300)
    }.frame(width: proxy.size.width, alignment: .center).background(Color(UIColor.secondarySystemBackground))


    }.environmentObject(Model())
    }
    } else {
    VStack(spacing: 30) {
    Text("I need an iPad to run!")
    Text("😟").scaleEffect(2)
    }.font(.largeTitle)
    }
    }
    }
    }

    struct ControlsView: View {
    @EnvironmentObject var model: Model

    var body: some View {

    return Form {
    HStack { Spacer(); Text("Settings").font(.title); Spacer() }
    Toggle(isOn: self.$model.minimumContainer, label: { Text("Narrow Container") })
    Toggle(isOn: self.$model.extendedTouchBar, label: { Text("Extended Bar") })
    Toggle(isOn: self.$model.twoPhases, label: { Text("Show in Two Phases") })
    Toggle(isOn: self.$model.addImplicitView, label: { Text("Include Implicitly View") })

    if self.model.addImplicitView {
    Toggle(isOn: self.$model.showImplicit, label: { Text("Show Implicit Guides") })//.disabled(!self.model.addImplicitView)
    }

    HStack {
    Text("Frame Alignment")
    Picker(selection: self.$model.frameAlignment.animation(), label: EmptyView()) {
    Text(".leading").tag(Alignment.leading)
    Text(".center").tag(Alignment.center)
    Text(".trailing").tag(Alignment.trailing)
    }.pickerStyle(SegmentedPickerStyle())
    }

    HStack {
    Text("Stack Alignment")
    Picker(selection: self.$model.stackAlignment.animation(), label: EmptyView()) {
    Text(".leading").tag(HorizontalAlignment.leading)
    Text(".center").tag(HorizontalAlignment.center)
    Text(".trailing").tag(HorizontalAlignment.trailing)
    }.pickerStyle(SegmentedPickerStyle())
    }
    }.padding(10).background(Color(UIColor.secondarySystemBackground))
    }
    }

    struct CodeView: View {
    @EnvironmentObject var model: Model

    var body: some View {
    VStack(alignment: .leading) {

    Text("VStack(alignment: \(self.model.stackAlignment.asString) {")

    CodeFragment(idx: 0)
    CodeFragment(idx: 1)

    if model.addImplicitView {
    VStack(alignment: .leading, spacing: 0) {
    HStack(spacing: 0) {
    Text(" SomeView()").foregroundColor(.primary)
    Text(".alignmentGuide(\(self.model.stackAlignment.asString), computedValue { d in ")
    Text("d[\(self.model.stackAlignment.asString)]").padding(5)
    Text(" }")
    }.foregroundColor(self.model.showImplicit ? .secondary : .clear)//.transition(AnyTransition.opacity.animation())
    }
    }

    CodeFragment(idx: 2)

    HStack(spacing: 0) {
    Text("}.frame(alignment: ")
    Text("\(self.model.frameAlignment.asString)").padding(5)
    Text(")")
    }

    }
    .font(Font.custom("Menlo", size: 16))
    .padding(20)
    }
    }

    struct CodeFragment: View {
    @EnvironmentObject 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 {
    @EnvironmentObject var model: Model
    let width: CGFloat

    var body: some View {

    VStack(alignment: self.model.stackAlignment, spacing: 20) {

    Block(algn: binding(0)).frame(width: 250)
    .alignmentGuide(self.model.stackAlignment, computeValue: { d in self.model.delayedAlgn[0].computedValue(d) })

    Block(algn: binding(1)).frame(width: 200)
    .alignmentGuide(self.model.stackAlignment, computeValue: { d in self.model.delayedAlgn[1].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(self.model.stackAlignment)).opacity(0.5))
    }

    Block(algn: binding(2)).frame(width: 300)
    .alignmentGuide(self.model.stackAlignment, computeValue: { d in self.model.delayedAlgn[2].computedValue(d) })


    }.frame(width: self.model.minimumContainer ? nil : width, alignment: self.model.frameAlignment).border(Color.red)

    }

    func binding(_ idx: Int) -> Binding<AlignmentEnum> {
    return Binding<AlignmentEnum>(get: {
    self.model.algn[idx]
    }, set: { v in
    self.model.algn[idx] = v

    let delay = self.model.twoPhases ? 500 : 0

    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay)) {
    withAnimation(.easeInOut(duration: 0.5)
    ) {
    self.model.delayedAlgn[idx] = v
    }
    }
    })
    }

    }

    struct Block: View {
    @Binding var algn: AlignmentEnum

    let a = Animation.easeInOut(duration: 0.5)

    var body: some View {
    let gesture = DragGesture(minimumDistance: 0, coordinateSpace: .local)
    .onEnded({v in
    withAnimation(self.a) {
    self.algn = .value(v.startLocation.x)
    }
    })

    return VStack(spacing: 0) {
    HStack {
    AlignButton(label: "L", action: { withAnimation(self.a) { self.algn = .leading } })
    Spacer()
    AlignButton(label: "C", action: { withAnimation(self.a) { self.algn = .center } })
    Spacer()
    AlignButton(label: "T", action: { withAnimation(self.a) { self.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 {
    @EnvironmentObject var model: Model

    @State private var flag = false

    var body: some View {
    GeometryReader { proxy in
    RoundedRectangle(cornerRadius: 8)
    .foregroundColor(.yellow)
    .frame(width: proxy.size.width + (self.model.extendedTouchBar ? 100 : 0), height: 20)
    .offset(x: 0, y: proxy.size.height / 2.0 - 10)
    }
    }
    }


    struct AlignButton: View {
    let label: String
    let action: () -> ()

    var body: some View {
    Button(action: {
    self.action()
    }, label: {
    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: self.algn.asNumber(width: 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: Equatable {
    case leading
    case center
    case trailing
    case value(CGFloat)

    var asString: String {
    switch self {
    case .leading:
    return "d[.leading]"
    case .center:
    return "d[.center]"
    case .trailing:
    return "d[.trailing]"
    case .value(let v):
    return "\(v)"
    }
    }

    func asNumber(width: CGFloat) -> CGFloat {
    switch self {
    case .leading:
    return 0
    case .center:
    return width / 2.0
    case .trailing:
    return width
    case .value(let v):
    return v
    }
    }

    func computedValue(_ d: ViewDimensions) -> CGFloat {
    switch self {
    case .leading:
    return d[.leading]
    case .center:
    return d.width / 2.0
    case .trailing:
    return d[.trailing]
    case .value(let v):
    return v
    }
    }

    static func fromHorizontalAlignment(_ a: HorizontalAlignment) -> AlignmentEnum {
    switch a {
    case .leading:
    return .leading
    case .center:
    return .center
    case .trailing:
    return .trailing
    default:
    return .value(0)
    }
    }
    }