How to put a logo in NavigationView in SwiftUI? - ios

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.

Related

SwiftUI ScrollView extra padding when go to another screen with showed keyboard

The default "Keyboard Avoidance" SwiftUI is used.
First GIF
If you put the code in VStack and add another View to it, then the container rises
Second GIF
I don't want to delete Keyboard Avoidance. I need to remove extra spacing
scrollDismissesKeyboard for ScrollView is not an option
minimal iOS version is iOS 16
struct ContentView: View {
#State var text: String = "Bu bu?"
var body: some View {
NavigationStack {
ScrollViewReader { proxy in
VStack {
ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
Spacer()
.frame(height: 500)
TextField("", text: $text)
.padding(.bottom, 70)
.frame(height: 40)
.frame(maxWidth: .infinity)
.background(Color.red)
NavigationLink("Screen 2", destination: {
Text("SwiftUI - Nice to meet you, let's never meet again")
})
}
}
Text("I'm in VSTack after scroll view")
}
}
}
}
}
I looked it up with a hierarchy view, and noticed that a UIInputSetHostView is created with a height of 216
View hierarchy 1
View hierarchy 2
disableAutocorrection not working

How to put tabs inside a scrollview in SwiftUI?

So basically I am very new to SwiftUI(started a few days ago) and am trying to put tabs within a ScrollView. The end result I am trying to achieve is that of Instagram's profile view.
I'd imagine the view's implementation would be something like this:
ScrollView {
VStack {
HStack {
// Profile Pic
// Stats
}
// Bio
// Button(s)
LazyVStack(pinnedViews: .sectionHeaders) {
Section {
TabView {
// Tab 1
// Tab 2
// Tab 3
}
} headers: {
// Tab icons
}
}
}
}
The problem is, the TabView never appears. Also I am unsure whether this is the best setup especially the LazyVStack as I am only using to for the fact that it pins the headers. As I said previously, I'm super new to SwiftUI so there are definetly some views that I have no idea exist some of which might be useful in what I am trying to achieve.
Nonetheless, how can I achieve the layout I am going for?
Thank you!
Side Question: With the way that I have the view setup, the scrollbar for the view is for the entire view however in apps like Instagram, the scrollbars are only within the tabs themselves. How could I also incorporate that aspect into the solution? Thanks again!
You are on the right track. TabView when not used as the root loses the ability to resize it self properly.
So that's why we need to specfiy its minHeight. You can use GeometryReader for that.
Then just give TabView a selection in order for the Tab-Buttons to work and most likely you want to apply .tabViewStyle(.page(indexDisplayMode: .never)) in order to be able to swipe between them.
Working Demo:
GeometryReader { proxy in
ScrollView {
VStack {
HStack {
Image(systemName: "person.circle")
Text("Some Text")
}
}
LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
Section {
TabView(selection: $tabIndex) {
VStack {
Spacer()
Text("1")
Spacer()
}
.frame(maxWidth: .infinity)
.background(.red.opacity(0.5))
.tag(0)
VStack {
Spacer()
Text("2")
Spacer()
}
.frame(maxWidth: .infinity)
.background(.green.opacity(0.5))
.tag(1)
VStack {
Spacer()
Text("3")
Spacer()
}
.frame(maxWidth: .infinity)
.background(.blue.opacity(0.5))
.tag(2)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(maxWidth: .infinity, minHeight: proxy.size.height)
} header: {
HStack {
Button {
withAnimation {
tabIndex = 0
}
} label: {
Text("Tab 1")
}
.buttonStyle(.bordered)
Button {
withAnimation {
tabIndex = 1
}
} label: {
Text("Tab 2")
}
.buttonStyle(.bordered)
Button {
withAnimation {
tabIndex = 2
}
} label: {
Text("Tab 3")
}
.buttonStyle(.bordered)
}
.padding()
.frame(maxWidth: .infinity)
.background(.regularMaterial)
}
}
}
}
As to your side question: They most likely still use a single scrollview but then move the inset of the scroll indicators. You cannot do that with SwiftUI only but you would need to introspect the underlying UIScrollView for that. SwiftUI-Introspect is good for that.
And then adjust the verticalScrollIndicatorInsets to your needs

SwiftUI onTapGuesture not working using offset inside ZStack?

I have a Mapbox map view and a search bar and activate button (HStack) inside a ZStack and using an offset modifier to position them at the top of the screen.
For some reason the offset is preventing the onTapGesture from working for the activate button, if I comment out the offset it will work but will be placed at the bottom of the screen.
I tried adding the offset to each element individually inside the HStack but that did not work...
How can I make the onTapGesture functionality work with offset?
Thank you
struct MapScreen: View {
var body: some View {
NavigationView {
ZStack(alignment: .bottom) {
HStack(spacing: 10) {
SearchBar()
.padding(.leading, 5)
.onTapGesture {
print("search pressed")
}
ActivateButton()
.onTapGesture {
print("activate pressed")
}
}.zIndex(2)
.offset(y: -770)
MapView(locations: $locations)
.edgesIgnoringSafeArea([.top, .bottom])
BottomNavBar().zIndex(1)
.edgesIgnoringSafeArea(.all).offset(y: 35)
}
.navigationViewStyle(StackNavigationViewStyle())
.fullScreenCover(isPresented: $presentSearchView, content: {
SearchView()
})
}
}
}
There's a couple problems here:
.offset(y: -770)
If you're trying to use an offset so large, you shouldn't be using offset at all. offset is usually for fine-tune adjustments and doesn't work great with big values. And also, 770 is hardcoded. What happens when you use another device with a different screen size? Don't hardcode or do calculations yourself — SwiftUI can do it for you!
Instead, use a VStack + Spacer() to push the search bar up.
ZStack(alignment: .bottom) {
VStack { /// here!
HStack(spacing: 10) {
SearchBar()
.padding(.leading, 5)
.onTapGesture {
print("search pressed")
}
ActivateButton()
.onTapGesture {
print("activate pressed")
}
}
Spacer() /// push the `HStack` to the top of the screen
}
.zIndex(2)
MapView(locations: $locations)
.edgesIgnoringSafeArea([.top, .bottom])
BottomNavBar().zIndex(1)
.edgesIgnoringSafeArea(.all).offset(y: 35)
}

Image positionning issue in SwiftUI

I am having an alignment problem while using SwiftUI.
Maybe I should say a layout issue. Anyway here is the situation:
This is the relevant part of the app interface:
One can see that while the text ("+++++") is centered, the flag is not. It is slightly shifted to the left. This left-shifting is precisely my problem. I would like the image to be centered as the text is.
Here follows the code, I would like to know what I am doing wrong for the image not to be centered:
import SwiftUI
struct TheNiceView: View {
........
var body: some View {
VStack {
HStack {
Spacer()
TheButtonView()
Spacer()
}
HStack {
Spacer()
Button(action: {})
{
Text("+++++")
.font(.largeTitle)
.foregroundColor(.gray)
.fontWeight(.heavy)
}
Spacer()
}
}
}
}
struct TheButtonView: View {
........
let imgSide:CGFloat = 72.0
var body: some View {
HStack {
Button(action: {})
{
Image(uiImage: ThaiFlagImg)
.resizable()
.frame(width: imgSide, height: imgSide)
}
}
}
}
Just in case this may be useful, this is the image used for the flag:
1
Thailand flag has five horizontal stripes in the colours red, white, blue, white and red. The image you use has 7 srtips.
2
Using the Spacers and HStack are unnecessary.
I used the following image without the Spacers and HStack, both the +++ button and the flag are aligned in the center.
https://upload.wikimedia.org/wikipedia/commons/a/a9/Flag_of_Thailand.svg
struct TheNiceView: View {
var body: some View {
VStack {
TheButtonView()
Button(action: {}) {
Text("+++++")
.font(.largeTitle)
.foregroundColor(.gray)
.fontWeight(.heavy)
}
}
}
}
struct TheButtonView: View {
let imgSide:CGFloat = 72.0
var body: some View {
Button(action: {}){
Image( "ThaiFlagImg")
.resizable()
.frame(width: imgSide, height: imgSide)
}
}
}

Place text between top and center of the screen

Is there a simple way to place text relatively between the top and center of the screen? With 'simple' I mean preferably without GeometryReader and overt calculations.
I've tried various combinations of VStack, Zstack and Spacer() but to no avail.
Code and screenshot as illustration:
struct ContentView: View {
var body: some View {
Text("Between top and center")
.offset(y: -150)
Text("Center")
}
}
A possible solution is to create two views on each side of the centred text (one hidden):
struct ContentView: View {
var body: some View {
VStack {
Spacer()
Text("Between top and center")
Spacer()
Text("Center")
Spacer()
Text("")
Spacer()
}
}
}
Alternatively, to be fool-proof (in case they change the behaviour of Text("") in the future release):
struct ContentView: View {
var body: some View {
VStack {
Spacer()
Text("Between top and center")
Spacer()
Text("Center")
Spacer()
Text("Between top and center")
.opacity(0)
Spacer()
}
}
}
(If needed, Text("Between top and center") can be extracted as a subview to avoid code duplication.)
As you said:
Spacer() [...] ensure[s] that the empty spaces between the views are equal in size
That's exactly the reason why I recommended to extract Text("Between top and center") as a subview. Then, you can change the font, size etc. of the view in one place only.
The adapted solution is:
struct ContentView: View {
var body: some View {
ZStack {
VStack {
Spacer()
subview
Spacer()
Spacer()
subview
.opacity(0)
Spacer()
}
VStack {
Spacer()
Text("Center")
Spacer()
}
}
}
var subview: some View {
Text("Between top and center")
.font(.caption)
// apply all other modifiers if you want
}
}
Here is possible solution (tested with Xcode 12.1 / iOS 14.1)
struct ContentView: View {
var body: some View {
VStack {
Color.clear.overlay(
Text("Between top and center") // << here !!
)
// Text("Between top and center")
// .frame(maxWidth: .infinity, maxHeight: .infinity) // alternate !!
Text("Center")
Color.clear
}
}
}
Update: added alternate with .frame but it is valid only if there should be the only one Text, whereas in .overlay it is possible to put anything.
Here is another solution using the method of VStack Spacers, I also used a little padding to get you closer to the center.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Top").bold()
.foregroundColor(.blue)
Spacer()
Text("Your desired area")
Spacer()
Text("Middle Text").bold()
.foregroundColor(.blue)
.padding(.bottom, 200)
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources