SwiftUI - How to make a center-aligned menu? - ios

I am trying to make a menu with SwiftUI on iOS.
If the button that opens the menu is center-aligned, I wish the menu would be center-aligned as well. Regardless of the icon or texts used in the button. How to do it?
What is strange is that menu's alignment changes depending on the button's content. If I use one icon, it is center-aligned as expected. But if I use another icon, it is left-aligned.
Here is my code:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
VStack {
Text("Some text.")
}
VStack(alignment: .center) {
Spacer()
Menu {
Button(action: {}) {
Label("Menu item", systemImage: "star")
}
Button(action: {}) {
Label("Menu item", systemImage: "star")
}
} label: {
Image(systemName: "plus") // left-aligned menu
// Image(systemName: "line.horizontal.3") // center-aligned menu
.foregroundColor(Color.white)
.padding()
.background(Color.accentColor)
.clipShape(Rectangle())
}
}
}
.navigationTitle("Content")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
So far I have tried few things: adding .frame, embedding menu and button in HStacks and VStacks.
The only thing that center-aligned menu with plus button was this:
// other code
Menu {
Button(action: {}) {
Label("Menu item", systemImage: "star")
}
Button(action: {}) {
Label("Menu item", systemImage: "star")
}
} label: {
HStack {
Spacer()
.allowsHitTesting(false)
Image(systemName: "plus")
.foregroundColor(Color.white)
.padding()
.background(Color.accentColor)
.clipShape(Rectangle())
Spacer()
.allowsHitTesting(false)
}
}
// other code
After I have embedded label Image in HStack with Spacer() before and after - menu is centered. But... these Spacers are now clickable which I don't want. I was thinking that adding .allowsHitTesting(false) to Spacer() will disable taps on them, but it didn't.
Any other ideas? :)
EDIT:
I have just tested with "plus" button on few other simulators and menu has different alignment on different simulators (all with iOS 14.5):
left-aligned:
iPhone Xs
iPhone 11 Pro
iPhone 12 mini
center-aligned:
iPhone 8
iPhone 8 Plus
iPhone 11
iPhone 11 Pro Max
iPhone 12
iPhone 12 Pro
iPhone 12 Pro Max
iPhone SE 2nd generation
Is it a bug or is there some alignment option I am missing?

Related

TextField In Tab View will show space upon keyboard

I'm using text field in Tab view, but when keyboard shows out. There has a space upon keyboard.
enter image description here
var body: some View {
TabView {
TestView()
}
}
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(0..<100) { data in
Text("\(data)")
}
}
Spacer()
HStack {
Image(systemName: "paperplane")
TextField("test field", text: $test)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
.padding()
.ignoresSafeArea(.keyboard, edges: .bottom)
}
If I'm using without Tab view, the keyboard works totally fine.
enter image description here
I took some search and put .ignoresSafeArea(.keyboard, edges: .bottom), I don't know why it still doesn't work.
Modifier should be applied in correct place:
HStack {
Image(systemName: "paperplane")
TextField("test field", text: $test)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.ignoresSafeArea(.keyboard, edges: .bottom) // << here !!
Tested with Xcode 13.4 / iOS 15.5

SwiftUI iOS 14/15 Can't tap Button underneath Spacer in ScrollView

In SwiftUI, I have a Button that's underneath the Spacer within a ScrollView. The ScrollView steals the tap gesture, so the Button never sees the tap.
So, for example, the button in this example does not work:
struct DoesNotWork: View {
var body: some View {
ZStack {
VStack {
// Button doesn't work
Button("Tap This") {
print("Tapped")
}
Spacer()
}
VStack {
ScrollView {
Spacer()
.frame(height: 150)
Rectangle()
.foregroundColor(.blue)
.frame(height: 150)
Spacer()
}
}
}
}
}
This version -- with everything else the same except no ScrollView -- works fine:
struct ThisWorks: View {
var body: some View {
ZStack {
VStack {
// Button works normally
Button("Tap This") {
print("Tapped")
}
Spacer()
}
VStack {
Spacer()
.frame(height: 150)
Rectangle()
.foregroundColor(.blue)
.frame(height: 150)
Spacer()
}
}
}
}
So that rules out the VStack, ZStack, and the Spacer.
I've tried using .allowsHitTesting(false) on the Spacer that's within the ScrollView, and also tried .disabled(true) (and in combination). Adding .allowsHitTesting(false) to the ScrollView makes the button works but of course breaks the ScrollView.
I also tried setting .foregroundColor(.clear) on the Spacer.
For what it's worth, I'm having the same behavior with both Xcode 13.0 Beta 5 testing with iOS 15.0 beta 7, as well as with Xcode 12.5.1 with iOS 14.7.1.
I'm out of ideas. It seems like it should be the simplest thing in the world, but I can't figure out a way around this.
Any help is greatly appreciated!

How to add a label accessory to a sidebar item in SwiftUI

I'm building a SwiftUI app for iPad, using the new Sidebar UI introduced with iPadOS 14. Each sidebar item should have a label accessory. Here's how I'm building it, and the corresponding result:
struct ContentView: View {
enum NavigationItem {
case companies, aapl
}
#State private var selection: NavigationItem? = .companies
var sidebar: some View {
List(selection: $selection) {
NavigationLink(destination: Text("Companies"), tag: NavigationItem.companies, selection: $selection) {
HStack {
Label("Companies", systemImage: "list.bullet")
Spacer()
Text("6")
.foregroundColor(.secondary)
.padding(.trailing, 6)
}
}
.tag(NavigationItem.companies)
NavigationLink(destination: Text("Apple Inc"), tag: NavigationItem.aapl, selection: $selection) {
Label("Apple Inc", systemImage: "circle")
}
.tag(NavigationItem.aapl)
}
.listStyle(SidebarListStyle())
.navigationTitle("Menu")
}
var body: some View {
NavigationView {
sidebar
Text("Select an item")
.foregroundColor(.secondary)
}
}
}
It works, but the color of the accessory label doesn't change when the item is selected. The label built with Label works as expected though.
Is this the correct way to add a label accessory to a sidebar item in SwiftUI, or is there a better way?

Why is .imageScale(.large) not big like in iOS stock apps?

I am using the NavigationView with some navigationBarItems.
I have specified that the dimension (or scale) of both buttons has to be .large (like in the SwiftUI tutorials by Apple) but as you can see below they are not as big as the navigationBarItems in iOS stock apps like in Shortcuts or Wallet.
Any help is appreciated!
Thanks!
Images:
Shortcuts App by Apple
My View
struct GroupsTab: View {
var addButton: some View {
Button(action: {}) {
Image(systemName: "plus.circle.fill")
.imageScale(.large)
// TO ACHIEVE THE SAME RESULT AS IN IOS I NEED TO SCALE THE IMAGE BY ABOUT 1.2 BUT IT BECOMES BLURRY
//.scaleEffect(1.2)
}
}
var editButton: some View {
Button(action: {}) {
Text("Edit")
.imageScale(.large)
}
}
var body: some View {
NavigationView {
Text("hello world")
.navigationBarItems(leading: editButton, trailing: addButton)
.navigationBarTitle("Groups")
}
}
}
Instead of .imageScale(.large) use .font(.largeTitle)

How to put a logo in NavigationView in SwiftUI?

I am trying to use a logo image instead of a NavigationView title at the top section of the app. Couldn't find any documentation of using images inside a NavigationView.
iOS 14+
Starting from iOS 14 you can create a ToolbarItem with the principal placement:
struct ContentView: View {
var body: some View {
NavigationView {
Text("Test")
.toolbar {
ToolbarItem(placement: .principal) {
Image(systemName: "ellipsis.circle")
}
}
}
}
}
See the ToolbarItemPlacement documentation for more placements.
NavigationView.navigationBarTitle() can only take a Text() argument right now. You could instead use .navigationBarItems() to set an Image as either the trailing or leading argument, but this is the SwiftUI equivalent of UINavigationItem.leftBarButtonItem[s] and UINavigationItem.rightBarButtonItem[s], which means that you're restricted to navigation bar button dimensions. But if you're ok with that, you may want to set a blank title so that you can specify a standard-height navigation bar.
Hard-Coded Positioning
If you can stand to live with yourself, you can fake a centered nav bar item by hard-coding padding around the image, like
.padding(.trailing, 125),
(Note that I deliberately positioned it off-center so that you can see that it's hard-coded.)
Slightly Less Hard-Coded Positioning
Even better would be to wrap the whole thing in a GeometryReader { geometry in ... } block to use the screen dimensions to calculate precise positioning, if you know the exact width of the image you're using:
GeometryReader { geometry in
NavigationView {
...
}
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(trailing:
PresentationButton(
Image(systemName: "person.crop.circle")
.imageScale(.large)
.padding(.trailing, (geometry.size.width / 2.0) + -30), // image width = 60
destination: ProfileHost()
)
)
If you don't want to hack it, here's what you can do:
Standard nav bar height, left button item
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading:
PresentationButton(
Image(systemName: "person.crop.circle")
.imageScale(.large)
.padding(),
destination: ProfileHost()
)
)
Standard nav bar height, right button item
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(trailing:
PresentationButton(
Image(systemName: "person.crop.circle")
.imageScale(.large)
.padding(),
destination: ProfileHost()
)
)
Expanded nav bar height, no title, left button item
.navigationBarItems(leading:
PresentationButton(
Image(systemName: "person.crop.circle")
.imageScale(.large)
.padding(),
destination: ProfileHost()
)
)
Use this:
NavigationView {
Text("Hello, SwiftUI!")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
HStack {
Image(systemName: "sun.min.fill")
Text("Title").font(.headline)
}
}
}
}
Credit: https://sarunw.com/posts/custom-navigation-bar-title-view-in-swiftui/
With SwiftUIX, you can use navigationBarTitleView(View):
NavigationView() {
NavigationLink(destination:YourView().navigationBarTitleView(Image(systemName: "message.fill")))
}
I don't want to claim 100% accuracy whether title image positioned at center but visually it looks center to me. Do your judgment and adjust padding :)
Here is code:
.navigationBarTitle(
Text("")
, displayMode: .inline)
.navigationBarItems(leading:
HStack {
Button(action: {
}) {
Image(systemName: "arrow.left")
}.foregroundColor(Color.oceanWhite)
Image("oceanview-logo")
.resizable()
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 40, alignment: .center)
.padding(UIScreen.main.bounds.size.width/4+30)
}
,trailing:
HStack {
Button(action: {
}) {
Image(systemName: "magnifyingglass")
}.foregroundColor(Color.oceanWhite)
}
)
To extend on NRitH's answer, putting your logo in a different component (to borrow a React way of putting it) may help anyone looking to understand the concepts.
The actual Image can be wrapped in any container view such as a VStack, etc. An example of setting up a struct as a component to be used in our navigation items could be something like the following:
struct NavLogo: View {
var body: some View {
VStack {
Image("app-logo")
.resizable()
.aspectRatio(2, contentMode: .fit)
.imageScale(.large)
}
.frame(width: 200)
.background(Color.clear)
}
}
When the aspect ratio is set, only the width needs to be set on the frame on the container view. We could also set a property in the NavLogo to set width and/or height from property dependency injection. Regardless, our navigationBarItems becomes very straight forward and more readable 🙂
NavigationView {
Text("Home View")
.navigationBarItems(
leading: NavLogo()
trailing: ProfileButton()
)
}
On iOS 13, a little hacky way to achieve this:
private var logo: some View {
Image("logo-image")
}
var body: some View {
GeometryReader { g in
content()
.navigationBarTitle("")
.navigationBarItems(leading:
ZStack(alignment: .leading) {
logo.frame(width: g.size.width).padding(.trailing, 8)
HStack {
leadingItems().padding(.leading, 10)
Spacer()
trailingItems().padding(.trailing, 10)
}
.frame(width: g.size.width)
}
)
}
}
Try the following.
struct ContainerView: View {
var body: some View {
VStack {
Image(systemName: "person.crop.square")
ContentView()
}
}
}
It worked for me.
Make sure you change ContentView to ContainerView inside SceneDelegate.swift before running on simulator or device.

Resources