How to select a Picker with MenuPickerStyle outside of the label text - swift

I have a simple menu picker that I have placed within a rounded rectangle border and chevron image. I'd like for users to be able to tap anywhere within this rectangle to activate the showing of the picker options but no matter what it only seems to be selectable within the actual text (see image with highlighted blue border). Is there any way to achieve this without scaling the text?
I've tried adding padding and scaling modifiers with no such luck.
var body: some View {
ZStack {
HStack {
Rectangle()
.foregroundColor(Color.black)
.frame(height: 40)
Image(uiImage: Icon.chevronDown.image())
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8))
}
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.black, lineWidth: 1)
)
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
Picker(selectedOption, selection: $selectedOption) {
ForEach(options, id: \.self) { option in
Text(option)
}
}
.pickerStyle(MenuPickerStyle())
}
}
How can I make the white area between the rounded black border and the blue text selectable to active the menu picker options the same way as tapping on the actual blue text?

Use Picker(selection:label:content:), which takes a View as the label: argument. Put everything you want as the tappable view inside the label: argument, like so:
var body: some View {
Picker(selection: $selectedOption, label:
HStack {
Rectangle()
.foregroundColor(Color(.systemBackground))
.frame(height: 40)
Image(systemName: "chevron.down")
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8))
}
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.black, lineWidth: 1)
)
.overlay(
Text("Picker Option \(selectedOption)")
)
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
) {
ForEach(options, id: \.self) { option in
Text("Picker Option \(option)")
}
}
.pickerStyle(MenuPickerStyle())
}
Update: The above code doesn’t work in Xcode 13 beta 5. Hopefully it’s a bug, but if not, here is a workaround: put the Picker in a Menu!
var body: some View {
Menu {
Picker("picker", selection: $selectedOption) {
ForEach(options, id: \.self) { option in
Text("Picker Option \(option)")
}
}
.labelsHidden()
.pickerStyle(InlinePickerStyle())
} label: {
HStack {
Rectangle()
.foregroundColor(Color(.systemBackground))
.frame(height: 40)
Image(systemName: "chevron.down")
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8))
}
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.black, lineWidth: 1)
)
.overlay(
Text("Picker Option \(selectedOption)")
)
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
}
}

Related

How to align with a certain view in a VStack?

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)
}
}

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.

SwiftUi bottom left button dont show

I have some SwiftUI function. I want to show my button in bottom left, but it shows top left.
struct NewRecordButton: View {
var body: some View {
ZStack (alignment: .bottomTrailing) {
HStack (alignment: .bottom) {
Spacer()
Button(action: { },
label: {
Text("⌂")
.font(.system(.largeTitle))
.frame(width: 35, height: 30)
.foregroundColor(Color.white)
.padding(.bottom,4)
}) .background(Color.black)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.3),
radius: 3,
x: 3,
y: 3)
}
}
}
}
Any ideas?
Here is possible variant
ZStack (alignment: .bottomTrailing) {
Rectangle().fill(Color.clear)
HStack (alignment: .bottom){
Button(action: {
}, label: {
Text("⌂")
.font(.system(.largeTitle))
.frame(width: 35, height: 30)
.foregroundColor(Color.white)
.padding(.bottom,4)
})
.background(Color.black)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.3),
radius: 3,
x: 3,
y: 3)
Spacer()
}
}
Although it's possible, but you should try NOT to use dummy views to arrange other views!
For pushing a view down, you should use a VStack and a Spacer together
VStack {
Spacer()
// bottomView
}
So it should be like:
ZStack {
VStack {
Spacer() // This will push it down, since it is in a `VStack`
HStack {
Button("⌂") {}
.font(.largeTitle)
.frame(width: 35, height: 30)
.foregroundColor(.white)
.padding(.bottom, 4)
.background(Color.black)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.3), radius: 3, x: 3, y: 3)
Spacer() // This will push it left, since it is in a `HStack`
}
}
}

Gesture blocking buttons in ZStack

I have a gesture added to a Rectangle inside a ZStack. I have a VStack which is a view or menu of buttons. When I add the gesture to the Rectangle (because I need to detect a tap on that view behind the menu buttons), then the buttons can not receive interaction from the user on device. I have the Rectangle behind the buttons and they do show above the Rectangle. On the simulator it worked just fine but on device it does not. Any help would be greatly appreciated from the community. Thanks!
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.black.opacity(0.01))
.gesture(DragGesture(minimumDistance: 0)
.onChanged({ (value) in
self.store.tapped = true
}))
VStack(alignment: .leading, spacing: 0) {
//menu buttons
HStack(alignment: .center, spacing: 18.5) {
someView()
.environmentObject(self.anotherStore)
.frame(width: 145, height: 22)
self.otherSubviews()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 37, alignment: .leading)
.padding(EdgeInsets(top: 0, leading: 6, bottom: 0, trailing: 6))
//menu detail view
self.detailView()
}
.padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 12))
}
}
I have tried changing the .zIndex for both to -1 for the Rectangle to something higher for the menu view. It does not change anything
I would like for the Rectangle to receive interaction (taps) from the user and also that the VStack of buttons will also receive interaction from the user.
Figured out that with a view in the background that has a gesture you need to make sure that your other views have .contentShape(Specify A Shape).
In the Apple documentation it states
/// Returns a new view that defines the content shape for
/// hit-testing `self` as `shape`.
I added .contentShape(Rectangle()) to the VStack and it allowed taps for the menu view of buttons as well as allowing taps to the Rectangle view in the background.
Updated code with answer
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.black.opacity(0.01))
.gesture(DragGesture(minimumDistance: 0)
.onChanged({ (value) in
self.store.tapped = true
}))
VStack(alignment: .leading, spacing: 0) {
//menu buttons
HStack(alignment: .center, spacing: 18.5) {
someView()
.environmentObject(self.anotherStore)
.frame(width: 145, height: 22)
self.otherSubviews()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 37, alignment: .leading)
.padding(EdgeInsets(top: 0, leading: 6, bottom: 0, trailing: 6))
//menu detail view
self.detailView()
}.contentShape(Rectangle()) //Define a tappable area
.padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 12))
}
}

SwiftUI - Remove space between cells

I am implementing a list view in SwiftUI. What I am trying to approach is to have cells that has no space between any other cells or parent view.
So in this screenshot as you can see there is a space between every cell and also space with the edge of the phone, which I want to remove.
struct FlickrView : View {
var flickrResponse: [FlickrResponse]
var body: some View {
List(flickrResponse) { item in
FlickrImageCell(response: item)
}
}
}
struct FlickrImageCell : View {
var response: FlickrResponse
var body: some View {
return ZStack(alignment: .topLeading) {
Image(uiImage: response.image ?? UIImage())
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: nil, height: 100.0, alignment: .center)
.clipShape(Rectangle())
.padding(0)
Text(response.title).fontWeight(.medium).multilineTextAlignment(.center)
}
}
}
I have tried this modifier:
.padding(EdgeInsets(top: 0, leading: -20, bottom: 20, trailing: -20))
But I have two problems with this approach: First, I don't think its convenient to write literal negative values. Second, the bottom padding does not work with any value.
So any suggestions?
I've had good luck with listRowInsets
struct ContentView: View {
var body: some View {
List {
Color.red
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
Color.blue
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
Color.yellow
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
}