SwiftUI: Dynamically move navigationBar items on List scroll - ios

I have custom navigationBarItems attached to my NavigationBar (the SF Symbols chevron) and when I go to scroll through my items in the ScrollView, the NavigationBar will collapse to .inline and my items stay put.
Is it possible to anchor the chevron to the right of the .navigationBarTitle or dynamically move the items upon scroll?
Here's my current code:
NavigationView {
ScrollView {
eventView()
}
.navigationBarItems(leading:
Button(action: {
// action
}) {
Image(systemName: "chevron.down.circle")
.font(.system(size: 21, weight: .regular))
.offset(x: 169, y: 47)
.foregroundColor(Color(#colorLiteral(...))
}
)
.navigationBarTitle("Upcoming")
}
(I'm new to SwiftUI, I know it's not up to scratch)
Any help is greatly appreciated :)

i am not sure what you want, but the reason for this behavior is your offset.
Try this:
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(UIFont.familyNames, id: \.self) { name in
Text(name)
}
}.navigationBarTitle("Title")
.navigationBarItems(leading:
HStack {
Button(action: {
// action
}) {
HStack {
Text("Upcoming")
Image(systemName: "chevron.down.circle")
.font(.system(size: 21, weight: .regular))
// .offset(x: 169, y: 47)
.foregroundColor(Color.orange)
}
}
})
}
}
}

Related

SwiftUI Custom Navigation Bar VStack doesn't work

I'm trying to make a custom navigation bar with back button, image, VStack (2 labels) but it didn't work. The whole view will stick to the center and not following the alignment I set. Thank you!
struct WeatherNavigation: View {
var body: some View {
HStack {
WeatherNavigation()
}
.
.
.
}
}
//
struct WeatherNavigation: View {
var body: some View {
Button(action: {
//action
}, label: {
HStack {
Image("Back")
.foregroundColor(.black)
Image("Weather")
.resizable()
.frame(width: 40, height: 40)
}
})
.frame(width: 100, height: 50, alignment: .leading)
VStack {
Text(weather.description)
.font(.appFont(size: 18))
.foregroundColor(Color(uiColor: .black))
Text(weather.location)
.font(.appFont(size: 12))
.foregroundColor(Color(uiColor: .blue))
}
.frame(width: .infinity, height: 50, alignment: .leading)
}
}
First of all, you shouldn't set frame of the whole view like that. For you problem, it can divide into: make a HStack to store all the container view and make a space between those two of them. Because using HStack you don't need to add leading.
Code will be like this
struct WeatherNavigation: View {
var body: some View {
// make a HStack to store all the attribute
HStack(alignment: .top) {
Button(action: {
//action
}, label: {
HStack {
Image("Back")
.foregroundColor(.black)
Image("Weather")
.resizable()
.frame(width: 40, height: 40)
}
})
.frame(width: 100, height: 50)
VStack {
Text("Tokyo")
.foregroundColor(Color(uiColor: .black))
Text("Japan")
.foregroundColor(Color(uiColor: .blue))
}
// make a space at the end
Spacer()
}
}
}
And the usage like this
struct ContentView: View {
var body: some View {
VStack {
HStack {
WeatherNavigation()
}
Spacer()
}
}
}
More over: adding Spacer() means that make space between view. That will solved your problem if you want to keep like your way.

How to show Profile Icon next to Large Navigation Bar Title in SwiftUI?

I am developing an App that supports multiple Profiles. I really like the way Apple displays the Profile Icon next to the Large Navigation Bar Title in all their Apps. See the Screenshot below:
My Question is the following:
Is it possible to achieve this in SwiftUI? And if so, how?
If it's not possible in pure SwiftUI, how can I achieve it including UIKit Code?
Thanks for your help.
I solved this by using SwiftUI-Introspect, to "Introspect underlying UIKit components from SwiftUI".
Here is an example of a view:
struct ContentView: View {
#State private var lastHostingView: UIView!
var body: some View {
NavigationView {
ScrollView {
ForEach(1 ... 50, id: \.self) { index in
Text("Index: \(index)")
}
.frame(maxWidth: .infinity)
}
.navigationTitle("Large title")
.introspectNavigationController { navController in
let bar = navController.navigationBar
let hosting = UIHostingController(rootView: BarContent())
guard let hostingView = hosting.view else { return }
// bar.addSubview(hostingView) // <--- OPTION 1
// bar.subviews.first(where: \.clipsToBounds)?.addSubview(hostingView) // <--- OPTION 2
hostingView.backgroundColor = .clear
lastHostingView?.removeFromSuperview()
lastHostingView = hostingView
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.trailingAnchor.constraint(equalTo: bar.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: bar.bottomAnchor, constant: -8)
])
}
}
}
}
Bar content & profile picture views:
struct BarContent: View {
var body: some View {
Button {
print("Profile tapped")
} label: {
ProfilePicture()
}
}
}
struct ProfilePicture: View {
var body: some View {
Circle()
.fill(
LinearGradient(
gradient: Gradient(colors: [.red, .blue]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 40, height: 40)
.padding(.horizontal)
}
}
The .frame(width: 40, height: 40) & hostingView.bottomAnchor constant will need to be adjusted to your needs.
And the results for each option (commented in the code):
Option 1
Option 2
View sticks when scrolled
View disappearing underneath on scroll
Without NavigationView
I done this with pure SwiftUI. You have to replace the Image("Profile") line with your own image (maybe from Assets or from base64 data with UIImage).
HStack {
Text("Apps")
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
.padding(.all, 30)
This products following result:
With NavigationView
Let's assume that you have NavigationView and inside that there's only ScrollView and .navigationTitle. You can add that profile image there by using overlay.
NavigationView {
ScrollView {
//your content here
}
.overlay(
ProfileView()
.padding(.trailing, 20)
.offset(x: 0, y: -50)
, alignment: .topTrailing)
.navigationTitle(Text("Apps"))
}
Where ProfileView could be something like this:
struct ProfileView: View {
var body: some View {
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
}
The result will be like this...
...which is pretty close to the App Store:

Unable to change background color of view in SwiftUI

I am trying to change background color main this view but unable to do it. I tried to put background(Color.green) at HStack, VSTack and even on ZStack but it did not work, not sure if i am putting at right place. By default it is taking phone or simulator color which is white but i want to apply custom background color
My Xcode version is 11.5
struct HomePageView: View {
#State var size = UIScreen.main.bounds.width / 1.6
var body: some View {
GeometryReader{_ in
VStack {
HStack {
ZStack{
// main home page components here....
NavigationView{
VStack {
AssignmentDaysView()
}.background(Color.lairBackgroundGray)
.frame(width: 100, height: 100, alignment: .top)
.navigationBarItems(leading: Button(action: {
self.size = 10
}, label: {
Image("menu")
.resizable()
.frame(width: 30, height: 30)
}).foregroundColor(.appHeadingColor), trailing:
Button(action: {
print("profile is pressed")
}) {
HStack {
NavigationLink(destination: ProfileView()) {
LinearGradient.lairHorizontalDark
.frame(width: 30, height: 30)
.mask(
Image(systemName: "person.crop.circle")
.resizable()
.scaledToFit()
)
}
}
}
).navigationBarTitle("Home", displayMode: .inline)
}
HStack{
menu(size: self.$size)
.cornerRadius(20)
.padding(.leading, -self.size)
.offset(x: -self.size)
Spacer()
}
Spacer()
}.animation(.spring()).background(Color.lairBackgroundGray)
Spacer()
}
}
}
}
}
struct HomePageView_Previews: PreviewProvider {
static var previews: some View {
HomePageView()
}
}
In your NavigationView you have a VStack. Instead you can use a ZStack and add a background below your VStack.
Try the following:
NavigationView {
ZStack {
Color.green // <- or any other Color/Gradient/View you want
.edgesIgnoringSafeArea(.all) // <- optionally, if you want to cover the whole screen
VStack {
Text("assignments")
}
.background(Color.gray)
.frame(width: 100, height: 100, alignment: .top)
}
}
Note: you use many stacks wrapped in a GeometryReader which you don't use. Consider simplifying your View by removing unnecessary stacks. Also you may not need a GeometryReader if you use UIScreen.main.bounds (however, GeometryReader is preferred in SwiftUI).
Try removing some layers: you can start with removing the top ones: GeometryReader, VStack, HStack...
Try the following:
Change the view background color especially safe area also
struct SignUpView: View {
var body: some View {
ZStack {
Color.blue //background color
}.edgesIgnoringSafeArea(.all)

How to increase tappable area of navigationBarItem in SwiftUI?

I have this code for my view
struct ContentView: View {
var body: some View {
NavigationView{
List{
ForEach(0...5, id: \.self) { note in
VStack(alignment: .leading) {
Text("title")
Text("subtitle")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
.navigationBarItems(trailing: resetButton)
.navigationBarTitle(Text("Notes"))
}
}
var resetButton: some View {
Button(action: {
print("reset")
}) {
Image(systemName: "arrow.clockwise")
}
.background(Color.yellow)
}
}
resetButton looks like this:
When I am tapping the resetButton, it seems like only the yellow area responds to touches.
How do I make tappable area of this button bigger? (Make it behave like a normal UIBarButtonItem)
You can change the frame of the view inside the button:
var resetButton: some View {
Button(action: {
print("reset")
}) {
Image(systemName: "arrow.clockwise")
.frame(width: 44, height: 44) // Or any other size you like
}
.background(Color.yellow)
}
This blog post pointed me in the right direction, I needed to add some padding directly to the Image.
Button(action: {
// Triggered code
}) {
Image(systemName: "plus")
.font(.system(size: 22, weight: .regular))
.padding(.vertical)
.padding(.leading, 60)
}
.background(Color.red) // Not needed

Add contextMenu in the items inside of a List in SwiftUI

I have created a photo library, there is list with many cells and in each cells. there are a few UIImages.
I added contextMenu the images, but when I long press on each image, the entire cell will be called instead of each image.
could anyone help me to how to add contextMenu to each the items inside of a List
struct PhotoList : View {
var photoLibrary = PhotoLibrary
var body : some View {
GeometryReader { geometry in
List(self.photoLibrary, id: \.self) { imageSet in
HStack (alignment: .center) {
ForEach(imageSet, id: \.self) { image in
Image(uiImage: image)
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
.contextMenu {
VStack {
Button(action: {}) {
HStack {
Text("Add to Today")
Image("plus.circle")
}
}
}
}
}
}
}
}
}
}
The problem with this is that the list view will force touch events to be registered to the first child view. For example it is not possible to create a list with multiple buttons in each row as in the following code.
// Individual buttons can't be pressed
List(someList, id: \.self) { _ in
HStack {
ForEach(1..<4) { k in
Button("Button") {
//what ever
}
}
}
}
HStack will get all the touch events....
Though it is possible to solve your problem by using a combination of a scrollview and a vstack. If scrolling isn't necessary just remove that piece of code.
var body : some View {
GeometryReader() { geometry in
ScrollView {
VStack {
List(self.photoLibrary, id: \.self) { imageSet in
HStack (alignment: .center) {
ForEach(imageSet, id: \.self) { image in
Image(uiImage: image)
.resizable()
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
.contextMenu {
VStack {
Button(action: {}) {
HStack {
Text("Add to Today")
Image("plus.circle")
}
}
}
}
}
}
}
}
}.position(x: geometry.size.width/2, y: geometry.size.height/2)
}
}
Code needs to be corrected for your design though. Added .resizable to the image as well. Hope this can be a start for your code.
Replace your List with a ForEach and wrap it in a VStack or LazyVStack and you're good to go. If you need scrolling wrap it in a ScrollView. The List makes it behave like table rows.
So instead of
List(self.photoLibrary, id: \.self) { imageSet in
// ...
}
do this:
ScrollView { // If scrolling is needed
VStack {
ForEach(self.photoLibrary, id: \.self) { imageSet in
// ...
}
}
}
Pretty much as NoosaMaan explained in more detail:
"[…] the list view will force touch events to be registered to the first child view […]"
I'm not sure what your struct for photoLibrary is like, so I created my own. This code will need to be changed slightly depending on your struct.
This ContentView can be called passing a list of photoLibrary to display on the screen.
struct photoLibrary {
var imageSet: [ImageItem]
init(){
self.imageSet = [ImageItem()]
}
}
struct ImageItem{
var image1: Image
var image2: Image
var image3: Image
init(){
self.image1 = Image(systemName: "xmark")
self.image2 = Image(systemName: "xmark")
self.image3 = Image(systemName: "xmark")
}
}
struct ContentView: View {
#State var photoLib: [photoLibrary]
var body: some View {
VStack {
ScrollView{
ForEach(self.photoLib.indices, id: \.self) { item in
VStack{
ForEach(photoLib[item].imageSet.indices, id: \.self) { image in
HStack{
ImageView(image: photoLib[item].imageSet[image].image1)
ImageView(image: photoLib[item].imageSet[image].image2)
ImageView(image: photoLib[item].imageSet[image].image3)
}
}
}
}
}
}
}
struct ImageView: View{
#State var image: Image
var body: some View{
image
.resizable()
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
Text("Add to Today").onTapGesture {
//do something
}
Image(systemName: "plus.circle").onTapGesture {
//do something
}
}
}
}
Display example with xmarks as the image:
As I know a context menu has to have at least two buttons but the following way I think it would be enough:
Image(uiImage: image)
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
.contextMenu {
Button(action: {}) {
Text("Add to Today")
Image("plus.circle")
}
Button(action: {}) {
Text("Some Other Button")
Image("globe")
}
}

Resources