Tap outside picker text not opening picker menu - swiftui

I have a Picker like this
Picker ("", selection: $itemIndex){
ForEach (0..<items.count, id: \.self){ index in
Text(items[index])
}
}
.frame(maxWidth: .infinity)
.background(.red)
The issue is that the picker extends to the screen width but the text displaying on the Picker is not that lengthy, maybe about 1/20 of the Picker length
Now when I click on the text, the Picker menu opens up, but when I click outside the text, still within the picker visibility, the menu don't open.
In the image above, when I tap on the red side, the menu opens up, but when I tap on both green sides it doesn't
Do anyone have an idea why this is happening?
Note: the picker is not in a Form or a NavigationController

Okay... So through the help of this question, I was able to customize my own Picker style. Below is the code to achieve this
struct CustomPickerStyle: ViewModifier {
#Binding var index: Int
var items: [String]
var font: SwiftUI.Font
var padding: CGFloat
func body(content: Content) -> some View {
Menu {
content
} label: {
HStack {
if let labelText = items[index] {
Text(labelText)
.font(font)
Spacer()
Image(systemName: "triangle.fill")
.resizable()
.frame(width: 12, height: 8)
.rotationEffect(.degrees(180))
}
}
.padding(padding)
.frame(alignment: .leading)
.background(Colors.white)
}
}
}
extension View {
func customPickerStyle(index: Binding<Int>, items: [String], font: SwiftUI.Font, padding: CGFloat) -> some View {
self.modifier(CustomPickerStyle(index: index, items: items, font: font, padding: padding))
}
}
And I this is how I used it in my UI
Picker("", selection: $itemIndex){
ForEach(0..<items.count, id: \.self){ index in
Text(items[index])
}
}
.customPickerStyle(index: $itemIndex, items: items, font: .system(size: 17), padding: 10)
.frame(width: UIScreen.main.bounds.width - 50)
.overlay(RoundedRectangle(cornerRadius: 10.0).stroke(.blue, lineWidth: 1.0))
And here is the result

I assume you wanted this
Picker ("", selection: $itemIndex){
ForEach (0..<items.count, id: \.self) { index in
Text(items[index])
.frame(maxWidth: .infinity) // << here !!
}
}
.background(.red)
or this if you told about Menu
Menu {
// content here
} label: {
Text("Menu")
.frame(maxWidth: .infinity) // << here !!
}
the general rule is - give space for label which is actually shown, instead of containing control, because hit testing is usually set up by intrinsic content.

Related

Change the background color of a SwiftUI List before the list gets loaded in

I'm working on a SwiftUI project and I'm having trouble with a list having a different background color than the rest of my screen. Specifically, the background of the list placeholder (i.e. before the list gets loaded in). Here is what I have so far:
var body: some View {
NavigationStack {
ZStack {
gradient
.opacity(0.35)
.ignoresSafeArea()
VStack {
Text("Search For Breweries")
.font(.system(.title3, design: .rounded))
.fontWeight(.bold)
TextField("Search by Name", text: $brewSearch)
.frame(width: 300, height: 50.0)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.border(.primary)
.cornerRadius(3.0)
.padding()
.onChange(of: brewSearch) { newValue in
brewSearch = newValue
Task.init(operation: {
if !newValue.isEmpty {
self.networkManager.fetchDataBySearch(name: newValue.replacingOccurrences(of: " ", with: "_"))
}
})
}
HStack(alignment: .center) {
ZStack {
Capsule()
.fill(Color("Brown"))
Capsule()
.fill(.black.opacity(0.15))
.padding(8)
HStack {
Text("Search by your location")
Image(systemName: "location")
.font(.system(size: 24, weight: .bold))
}
} //: Button ZStack
.foregroundColor(.white)
.frame(width: 300, height: 80)
.onTapGesture {
locationManager.requestAuthorization(always: true)
guard let latitude = CLLocationManager().location?.coordinate.latitude else {return}
guard let longitude = CLLocationManager().location?.coordinate.longitude else {return}
networkManager.fetchDataByLocation(latitude: latitude, longitude: longitude)
}
} //: HStack
List {
ForEach(networkManager.breweries) { brewery in
// All the list items that will get loaded in
}
}
.background(.clear)
.scrollContentBackground(.hidden)
}
}
}
}
Once the list gets loaded in, the background becomes the same as the rest of the screen, but before anything loads in, the section where the list will appear is white. I'll attach some screenshots of what I'm talking about:
[
[
How can I change it so the white area where the list will go is the same color of the rest of the screen? Right now I don't care about each list item's background color, just the background for where the list will go.
you could try this approach:
List {
ForEach(networkManager.breweries) { brewery in
// All the list items that will get loaded in
}
}.blendMode(networkManager.breweries.isEmpty ? .destinationOver : .normal)

Make SwiftUI TabView wrap its content

Is it possible to make paged TabView that wraps its content? I don't know the height of the content (it is an Image resized to fit the width of the screen) so I can't use frame modifier.
My code looks like this:
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Color.red
.frame(width: 20, height: 20)
}
The problem is that the TabView is as big as the screen and PageIndicator is placed in the top right corner of the screen instead of top right corner of the image. Tanks for any suggestions.
EDIT:
I've added code that is reproducible. PageIndicator was replaced by red rectangle.
struct Test: View {
struct Entry: Identifiable {
let image: Image
let description: String
var id: String { description }
}
let data = [
Entry(image: Image(systemName: "scribble"), description: "image 1"),
Entry(image: Image(systemName: "trash"), description: "image 2")
]
var body: some View {
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Color.red
.frame(width: 20, height: 20)
}
}
}
Your PageIndicator is not placed on the image, because you didn't place it there. You are placing it in a layer on top of a VStack that happens to contain text and an image that can be shorter than the screen. If you want the PageIndicator on the image, then you need to do that specifically. You didn't provide a Minimal, Reproducible Example, but does something like this work:
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.overlay(
PageIndicator()
)
.frame(maxWidth: .infinity)
.overlay(
PageIndicator()
)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
The .overlay() needs to go before the .frame() so it stays on the image, instead of overlaying the .frame().
Edit:
Based off of the MRE and the comment, here is alternate solution that aligns the PageIndicator to the top of the image, but does not scroll with the image. Please note that this is not a perfect MRE as the image heights are different, but this solution actually accounts for that as well. Lastly, I added a yellow background on the image to show that things are aligned properly.
struct Test: View {
struct Entry: Identifiable {
let image: Image
let description: String
var id: String { description }
}
let data = [
Entry(image: Image(systemName: "scribble"), description: "image 1"),
Entry(image: Image(systemName: "trash"), description: "image 2")
]
#State private var imageTop: CGFloat = 50
var body: some View {
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
.background(Color.yellow)
.background(
GeometryReader { imageProxy in
Color.clear.preference(
key: ImageTopInGlobal.self,
// This returns the top of the image relative to the TabView()
value: imageProxy.frame(in: .named("TabView")).minY)
}
)
Text(entry.description)
}
}
}
// This gives a reference to another container for comparison
.coordinateSpace(name: "TabView")
.tabViewStyle(.page(indexDisplayMode: .never))
VStack {
Spacer()
.frame(height: imageTop)
Color.red
.frame(width: 20, height: 20)
}
}
// You either have to ignore the safe area or account for it with regard to the TabView(). This was simpler.
.edgesIgnoringSafeArea(.top)
.onPreferenceChange(ImageTopInGlobal.self) {
imageTop = $0
}
}
}
private extension Test {
struct ImageTopInGlobal: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = nextValue()
}
}
}
Final edit:
In response to the last comment, my answer is a qualified no. I don't think there is any way to make a TabView hug its contents. (I take the original question using the term wrap to mean hug, as the TabView always "wraps" its contents.) If you try to use a preference key, the TabView collapses. There would have to be a minHeight or height set to prevent this which defeats the purpose of the hugging attempt.

SwiftUI: Custom label in navigation link is greyed out

I'm trying to use my custom card view as label to navigation link. this navigation link is also a Grid item.
The result is a
My Card Code:
struct CityCard: View {
var cityData: CityData
var body: some View {
VStack {
Text("\(cityData.name)")
.padding([.top], 10)
Spacer()
Image(systemName: cityData.forecastIcon)
.resizable()
.frame(width: 45, height: 45)
Spacer()
HStack(alignment: .bottom) {
Text(String(format: "%.2f $", cityData.rate))
.padding([.leading, .bottom], 10)
Spacer()
Text(String(format: "%.2f °", cityData.degrees))
.padding([.trailing, .bottom], 10)
}
}.frame(width: 150, height: 150)
.background(Color.blue)
.cornerRadius(10)
.navigationTitle(cityData.name)
}
}
My List View:
struct CityList: View {
var cities: [CityData]
let columns = [
GridItem(.flexible(minimum: 150, maximum: 150)),
GridItem(.flexible(minimum: 150, maximum: 150))
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(self.cities) { item in
NavigationLink(destination: Text(item.name), label: {
CityCard(cityData: item)
})
}
}
}
}
}
someone has a solution why it gives me this opacity?
Update:
The main contact view is:
It's greyed out because you don't yet have a NavigationView in your view hierarchy, which makes it possible to actually navigate to a new view.
You can, for example, wrap your ScrollView in a NavigationView:
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(self.cities) { item in
NavigationLink(destination: Text(item.name), label: {
CityCard(cityData: item)
})
}
}
}
}
}
Or, if it's only happening in your preview, add a NavigationView to your preview code:
NavigationView {
CityList(...)
}
Keep in mind that if you have the default accent color (blue, in light mode) that your links will become invisible (since they will be blue) on top of the blue background. You can set a custom accent color in your project or via the accentColor modifier:
NavigationView {
//content
}.accentColor(.black)

Using a SwiftUI menu in the top-trailing corner

When placing a SwiftUI Menu button in the top-trailing corner, using ignoresSafeArea(), it works correctly in the SwiftUI preview:
But not on the actual device/in simulator:
When I use ignoresSafeArea() again on the menu itself, a strange behavior occurs (iOS 14.5): without padding the menu looses function, with padding, the button moves down when the Menu appears and the Menu will have an odd spacing between button and Menu:
Is there a way to get a Menu to appear correctly in the top-trailing corner?
Example code:
struct MenuButtonExampleView: View {
var body: some View {
Color.yellow
.ignoresSafeArea()
.overlay(self.menuButton, alignment: .topTrailing)
}
#ViewBuilder var menuButton: some View {
Menu(
content: {
Button(
action: {
debugPrint("Action")
},
label: {
Label("Action", systemImage: "xmark")
}
)
},
label: {
Button(
action: {},
label: {
ZStack {
Circle()
.foregroundColor(.green)
.frame(width: 30, height: 30)
Image(systemName: "ellipsis")
.foregroundColor(.white)
}
}
)
.padding(20)
}
)
}
}
This may be due to the navigation bar.
You can try to add this code.
.navigationBarHidden(true)
Create a let constant and get the complete screen height and assign it to that constant and after that change the body code as you can see in the code below and it will work for every device
let customHeight = UIScreen.main.bounds.height
var body: some View {
Color.yellow
.overlay(self.menuButton, alignment: .topTrailing)
.statusBar(hidden: true)
.frame(height: customHeight + (customHeight/18))
.ignoresSafeArea()
}

How to Remove Padding/Align Picker Value Left with Hidden Label in SwiftUI?

I'm trying to align my Picker hidden label values left to another TextField in my form (see attached image). What's the best way to remove the 8px padding in the displayed picker value?
import SwiftUI
struct ContactFormView: View {
var countries = ["Malaysia", "Singapore", "Japan"]
#State var name: String = ""
#State var mobile: String = ""
#State var mobileCountry: String = "Malaysia"
var body: some View {
NavigationView {
Form {
Section(header: Text("Details")) {
TextField("Name", text: $name)
.border(Color.red, width: 1)
HStack {
Picker(selection: $mobileCountry, label: EmptyView()) {
ForEach(countries, id: \.self) {
Text($0).border(Color.red)
}
}
.scaledToFit()
.labelsHidden()
.border(Color.red, width: 1)
TextField("Mobile", text: $mobile)
.border(Color.red, width: 1)
}
}
}
.navigationBarTitle("New contact")
}
}
}
Probably your best bet is to use a negative padding on your picker:
Picker(selection: $mobileCountry, label: EmptyView()) {
ForEach(countries, id: \.self) {
Text($0).border(Color.red)
}
}
.scaledToFit()
.labelsHidden()
.border(Color.red, width: 1)
.padding(.horizontal, -8)
Loading your code in Xcode 12.5, adding that negative padding aligns the text in the way you want.
Intuitively I would have chosen .padding(.leading, -8) to remove padding for the Picker's leading edge only – but in doing so, the gap between the picker and text field grows larger.
Applying the negative padding to both horizontal values keeps that gap the same for me. But I'd recommend in your code trying both, and seeing which one makes the most sense for you.