How to align with a certain view in a VStack? - swift

I'm trying to achieve the following layout:
As you can see, Foo is aligned with Bar horizontally, and there's a vertical line at the bottom of Foo.
I haven't managed to do it, the best I can do is the following one:
Here is the corresponding code:
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
VStack(spacing: 5) {
Text("Foo")
.padding(.bottom, 0)
getVerticalLine()
}
Text("Bar")
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow)
}
}
private func getVerticalLine() -> some View {
return Color.gray
.frame(width: 1, height: 30)
.padding(.leading, 0)
}
}
My question is: how to modify the code to achieve the expected layout?
P.S. In the end, I want to achieve something like this:

Here is the complete code that you want. You need to use multiplier for constant so that it fits in all devices.
CententView
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
FooBarView(isLastOne: false)
.padding(EdgeInsets(top: 0, leading: 10, bottom: -18, trailing: 0))
FooBarView(isLastOne: false)
.padding(EdgeInsets(top: 0, leading: 10, bottom: -18, trailing: 0))
FooBarView(isLastOne: false)
.padding(EdgeInsets(top: 0, leading: 10, bottom: -18, trailing: 0))
FooBarView(isLastOne: true)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0))
}
}
}
FooBarView
struct FooBarView: View {
var isLastOne: Bool
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
VStack(spacing: 5) {
Text("Foo")
.padding(.bottom, 0)
}
Text("Bar")
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow)
}
if !isLastOne {
getVerticalLine()
.padding(EdgeInsets(top: -5, leading: 12, bottom: 0, trailing: 0))
}
}
}
private func getVerticalLine() -> some View {
return Color.gray
.frame(width: 1, height: 40)
.padding(.leading, 0)
}
}

Each of your activities are a single HStack, with the time on the left. The line is just something that connects each row, not necessarily part of the row itself.
You can use a ZStack to show the gray line on the back and the rows on the front; a background on the first column will avoid overlapping the gray line.
The alignment is assured by giving the same frame width to the first column (time) and the vertical line.
Here's an example:
struct Example: View {
struct Activity: Hashable, Identifiable {
let id = UUID()
let time: String
let activity: String
}
let activities = [Activity(time: "10.00", activity: "Exercise"),
Activity(time: "11.00", activity: "Work"),
Activity(time: "12.00", activity: "Study"),
Activity(time: "13.00", activity: "Coding")]
let firstColumnWidth = 80.0
let spacingBetweenLines = 30.0
let internalPadding = 10.0
let lineHeight = 20.0
var body: some View {
ZStack {
HStack {
getVerticalLine
.frame(width: firstColumnWidth)
Spacer()
}
VStack(spacing: spacingBetweenLines) {
ForEach(activities) { item in
HStack(spacing: 5) {
Text(item.time)
.frame(height: lineHeight)
.padding(internalPadding)
.frame(width: firstColumnWidth)
.background(.white)
Text(item.activity)
.frame(height: lineHeight)
.padding(internalPadding)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow)
}
}
}
}
.padding()
}
private var verticalLine: some View {
let lineHeight = internalPadding * 2 + lineHeight + spacingBetweenLines
return Color.gray
.frame(width: 1, height: lineHeight * Double(activities.count - 1))
.background(.green)
}
}

Related

Having issue with DispatchQueue not working after 3-4 executions in Swift

I have the following code below to show a Message to the user and then disappear. Everything works fine for the first 1 to 3 tries, but anything above that the Message window does not disappear as it did on the previous attempts/required.
struct ContentView: View {
#State var showingNotice = false
var body: some View {
ZStack {
Button(action: {
self.showingNotice = true
}, label: {
Text("Show Notice")
})
if showingNotice {
FloatingNotice(showingNotice: $showingNotice)
}
}
.animation(.easeInOut(duration: 1))
}
}
struct FloatingNotice: View {
#Binding var showingNotice: Bool
var body: some View {
VStack (alignment: .center, spacing: 8) {
Image(systemName: "checkmark")
.foregroundColor(.white)
.font(.system(size: 48, weight: .regular))
.padding(EdgeInsets(top: 20, leading: 5, bottom: 5, trailing: 5))
Text("Review added")
.foregroundColor(.white)
.font(.callout)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 5, trailing: 10))
}
.background(Color.snackbar.opacity(0.75))
.cornerRadius(5)
.transition(.scale)
.onAppear(perform: {
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.showingNotice = false
})
})
}
}
Can anyone see what I am missing or help figure why it "stops" working after multiple executions?
Sometimes onAppear will not called so your self.showingNotice = false is not getting called. So you can do one thing move your delay block inside the button click it self as below.
struct ContentView: View {
#State var showingNotice = false
var body: some View {
ZStack {
Button(action: {
self.showingNotice = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.showingNotice = false
})
}, label: {
Text("Show Notice")
})
if showingNotice {
FloatingNotice(showingNotice: $showingNotice)
}
}
.animation(.easeInOut(duration: 1))
}
}
struct FloatingNotice: View {
#Binding var showingNotice: Bool
var body: some View {
VStack (alignment: .center, spacing: 8) {
Image(systemName: "checkmark")
.foregroundColor(.white)
.font(.system(size: 48, weight: .regular))
.padding(EdgeInsets(top: 20, leading: 5, bottom: 5, trailing: 5))
Text("Review added")
.foregroundColor(.white)
.font(.callout)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 5, trailing: 10))
}
.background(Color(.red))
.cornerRadius(5)
.transition(.scale)
}
}

SwiftUI keyboard navigation in lists on MacOS

I'm trying to implement a list which can be navigated with arrow keys - up/down. I've created layout, but now I don't totally understand how(and where) to make up/down keys intercepted so I could add my custom logic. I already tried onMoveCommand with focusable but that did not work(wasn't firing at all)
Code I have - below
public var body: some View {
VStack(spacing: 0.0) {
VStack {
HStack(alignment: .center, spacing: 0) {
Image(systemName: "command")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.leading, 20)
.offset(x: 0, y: 1)
TextField("Search Commands", text: $state.commandQuery)
.font(.system(size: 20, weight: .light, design: .default))
.textFieldStyle(.plain)
.onReceive(
state.$commandQuery
.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
) { val in
state.fetchMatchingCommands(val: val)
}
.padding(16)
.foregroundColor(Color(.systemGray).opacity(0.85))
.background(EffectView(.sidebar, blendingMode: .behindWindow))
}
}
Divider()
VStack(spacing: 0) {
List(state.filteredCommands.isEmpty && state.commandQuery.isEmpty ?
commandManager.commands : state.filteredCommands, selection: $selectedItem) { command in
VStack(alignment: .leading, spacing: 0) {
Text(command.title).foregroundColor(Color.white)
.padding(EdgeInsets.init(top: 0, leading: 10, bottom: 0, trailing: 0))
.frame(height: 10)
}.frame(maxWidth: .infinity, maxHeight: 15, alignment: .leading)
.listRowBackground(self.selectedItem == command ?
RoundedRectangle(cornerRadius: 5, style: .continuous)
.fill(Color(.systemBlue)) :
RoundedRectangle(cornerRadius: 5, style: .continuous)
.fill(Color.clear) )
.onTapGesture {
self.selectedItem = command
callHandler(command: command)
}.onHover(perform: { _ in self.selectedItem = command })
}.listStyle(SidebarListStyle())
}
}
.background(EffectView(.sidebar, blendingMode: .behindWindow))
.foregroundColor(.gray)
.edgesIgnoringSafeArea(.vertical)
.frame(minWidth: 600,
minHeight: self.state.isShowingCommandsList ? 400 : 28,
maxHeight: self.state.isShowingCommandsList ? .infinity : 28)
}
This is how it looks - and I want to make focus move between found list items
If I understand your question correctly, you want to use the arrow keys to "move" from the search TextField, to the list of items, and then navigate the list with the up/down arrow keys.
Try something simple like this example code, to monitor the up/down arrow keys, and take the appropriate action.
Adjust/tweak the logic to suit your needs.
import Foundation
import SwiftUI
import AppKit
struct ContentView: View {
let fruits = ["apples", "pears", "bananas", "apricot", "oranges"]
#State var selection: Int?
#State var search = ""
var body: some View {
VStack {
VStack {
HStack(alignment: .center, spacing: 0) {
Image(systemName: "command")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.leading, 20)
.offset(x: 0, y: 1)
TextField("Search", text: $search)
.font(.system(size: 20, weight: .light, design: .default))
.textFieldStyle(.plain)
}
}
Divider()
List(selection: $selection) {
ForEach(fruits.indices, id: \.self) { index in
Text(fruits[index]).tag(index)
}
}
.listStyle(.bordered(alternatesRowBackgrounds: true))
}
.onAppear {
NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { nsevent in
if selection != nil {
if nsevent.keyCode == 125 { // arrow down
selection = selection! < fruits.count ? selection! + 1 : 0
} else {
if nsevent.keyCode == 126 { // arrow up
selection = selection! > 1 ? selection! - 1 : 0
}
}
} else {
selection = 0
}
return nsevent
}
}
}
}

SwiftUI Animate Custom Progress Bar

I created this custom progress view with bars for the progress, how can I animate the the green color in the next bar when it's time to go to the next element? I am looking for it to fill the bar slowly from left to right
struct ContentView: View {
var body: some View {
ProgressBar(lastIndex: 8, currentIndex: 4)
.padding(.horizontal, 23)
.padding(.top, 10)
Spacer()
}
}
struct ProgressBar: View {
var lastIndex: Int
var currentIndex: Int
var body: some View {
HStack(alignment: .center, spacing: 4) {
ForEach(0...lastIndex, id: \.self) { i in
BarElement(selected: i <= currentIndex)
}
}
}
}
struct BarElement: View {
var selected: Bool
var body: some View {
Capsule()
.fill(selected ? .green : Color(UIColor.lightGray))
.frame(width: 33, height: 5, alignment: .center)
}
}
It's hard to answer without code but if you have all separated rectangles, it should be enough using .animation(.linear) on all the rectagles, so when the values changes it should animated as you asked.
This will do what you wanted.
struct BarElement: View {
var value : CGFloat = 0.0
var selected: Bool
var body: some View {
ZStack (alignment: .leading){
Capsule()
.fill(Color(UIColor.lightGray))
.frame(width: 33, height: 5, alignment: .leading)
.animation(.linear, value: selected)
Capsule()
.fill(.green)
.frame(width: selected ? 33 : 0, height: 5, alignment: .leading)
.animation(.linear, value: selected)
}
}
}

DatePicker, Having A Range or Selecting Date Causes View To Shift

I have an odd issue where my view is being shifted down on a DatePicker that has the style GraphicalDatePickerStyle(). The things I've noticed are that if you have a minimum range added, in my case Date() it will flicker after a moment or two and shift downwards. I have also noticed if I remove that, it doesn't happen until the moment when I actually select a date on the picker itself. Here is a gif of that happening. In this example, I have removed the minimum selectable date.
The way that this view is being presented is via .offset modifier on the main view. This view is always present, though not visible and when you tap the add button it sets the offset back to 0. I stripped 99% of everything out and I still can't identify the problem.
Presentation Code (Minimal Reproducible, I Think)
struct TimeListView: View {
#ObservedObject var vm = TimerListViewModel()
#State var showsAddModal = false
var body: some View {
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach (vm.events) { dueDate in
CountdownView(dueDate: dueDate)
.shadow(color: Color.black.opacity(0.4), radius: 3, x: 3, y: 3)
}
}
.padding(.horizontal)
}
.listStyle(PlainListStyle())
.background(Color.promptlyLightNavy)
Button(action: {
withAnimation {
showsAddModal.toggle()
}
}, label: {
Text("+")
.font(.custom("system", size: 50))
.frame(width: 70, height: 70)
.foregroundColor(.promptlyNavy)
.padding(.bottom, 7)
})
.background(
Circle()
.fill(Color.promptlyTeal)
.shadow(color: Color.black.opacity(0.4), radius: 3, x: 3, y: 3))
.padding()
Rectangle()
.fill(showsAddModal ? Color.promptlyNavy.opacity(0.8) : .clear)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
withAnimation { showsAddModal.toggle() }
}
if showsAddModal {
AddCountdownView(dismissed: {
withAnimation { showsAddModal.toggle() }
}, add: { date, title in
vm.addEvent(event: Event(date: date, title: title))
}).transition(.move(edge: .bottom))
}
}
}
}
View That Is Causing The Problem (Minimal Reproducible)
struct AddCountdownView: View {
var dismissed: () -> ()
var add: ((date: Date, title: String)) -> ()
#State private var eventDate = Date.now
#State private var eventTitle = ""
var body: some View {
ZStack(alignment: .bottomLeading) {
Spacer()
VStack {
Divider()
RoundedRectangle(cornerRadius: 20)
.fill(Color.promptlyOrange)
.frame(width: 100, height: 4)
TextField("", text: $eventTitle)
.placeholder(when: eventTitle.isEmpty, placeholder: {
Text("Event Title").foregroundColor(.promptlyLightTeal)
})
.underlineTextField()
.font(.title)
DatePicker("asdf",selection: $eventDate, in: Date()...)
.applyTextColor(.white)
.datePickerStyle(GraphicalDatePickerStyle())
HStack(spacing: 50) {
Button(action: {
withAnimation { dismissed() }
}, label: {
Image(systemName: "trash")
.font(.title)
.frame(width: 100)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.promptlyTeal)
.shadow(color: Color.black.opacity(0.4), radius: 3, x: 3, y: 3)
.frame( height: 50)
)
})
Button(action: {
add((date: eventDate, title: eventTitle))
}, label: {
Image(systemName: "checkmark.seal")
.font(.title)
.frame(width: 100)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.promptlyTeal)
.shadow(color: Color.black.opacity(0.4), radius: 3, x: 3, y: 3)
.frame( height: 50)
)
})
}
.padding()
.foregroundColor(.promptlyNavy)
}
.background(Color.promptlyNavy)
.shadow(color: Color.black.opacity(0.4), radius: 3, x: 0, y: -3)
}
}
}
The problem is related directly to the way that DatePicker handles it's coloring. In my problem I was attempting to set the color by using a Hacky solution provided by a bunch of other people.
Hacky Solution (That's Bugged on iOS 15.X)
This is only bugged in this particular situation, where there's also a shadow added the the parent view.
#ViewBuilder func applyTextColor(_ color: Color) -> some View {
if UITraitCollection.current.userInterfaceStyle == .light {
self.colorInvert().colorMultiply(color)
} else {
self.colorMultiply(color)
}
}
Proper Solution (iOS 15.X)
DatePicker("asdf", selection: $eventDate, in: Date()...)
.datePickerStyle(GraphicalDatePickerStyle())
.accentColor(.promptlyOrange) // Sets Accent Color
.colorScheme(.dark) //Gets The white text.

How do I make a card in ScrollView in SwiftUI "pop" to center

I have a horizontally scrolling ScrollView in SwiftUI. I need to somehow center the element that is shown more than a half. I can't figure out how to get ScrollView position in percent, and I don't know how to optimize using GeometryReader for all possible devices.
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach (sectionData) { item in
GeometryReader { geometry in
SectionView(section: item)
.rotation3DEffect(
Angle(degrees: Double(geometry.frame(in:.global).minX - 30) / -20),
axis: (x: 0.0, y: 10.0, z: 0.0))
}
.frame(width: 275, height: 275)
}
}
.padding(30)
.padding(.bottom, 30)
}
.offset(y: -30)
This could be an aproach
public struct SectionView: View {
var section: Int
public var body: some View {
ZStack {
Rectangle()
.frame(width: 275, height: 275, alignment: .center)
Text("\(section)")
.foregroundColor(.white)
}
}
}
public struct ExampleView: View {
#State private var isShowingNumber: Int = 0
public var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { value in
ZStack (alignment: .center) {
LazyHStack(alignment: .center, spacing:275) {
ForEach (1..<20) { item in
Rectangle()
.frame(width: 1, height: 1, alignment: .center)
.foregroundColor(.clear)
.onAppear() {
print(item)
withAnimation() {
value.scrollTo(item, anchor: .center)
}
}
}
}
HStack(spacing:10) {
ForEach (1..<20) { item in
GeometryReader { geometry in
ZStack {
SectionView(section: item)
.id(item)
.rotation3DEffect(
Angle(degrees: Double(geometry.frame(in:.global).minX - 30) / -20),
axis: (x: 0.0, y: 10.0, z: 0.0))
}
}
.frame(width: 275, height: 275)
}
}
.padding(30)
.padding(.bottom, 30)
}
}
}
.offset(y: -30)
}
}