SwiftUI: add Text below a Button - ios

So I'm building my first App and trying to get some Text below the Button, but it should still be a part of the button. So thats what i got:
Button{
print("tapped")
} label: {
Image("Wand")
.resizable()
.frame(width: 100, height: 100)
}
.font(.title3)
.background(Color.gray)
.foregroundColor(.black)
.cornerRadius(20)
It looks like this:
But I want is this:
I've tried putting a VStack into the Label, but the Text stays in the gray button

I think it may help you
public struct VerticalLabelStyle: LabelStyle {
public let spacing: CGFloat
public init(spacing: CGFloat = 8) {
self.spacing = spacing
}
public func makeBody(configuration: Configuration) -> some View {
VStack(alignment: .center, spacing: spacing) {
configuration.icon
configuration.title
}
}
}
public struct ContentView: View {
#State private var text: String = ""
public var body: some View {
Label {
Text(text)
} icon: {
Image(systemName: "gamecontroller.fill")
.resizable()
.frame(width: 44, height: 44, alignment: .center)
}
.frame(width: 100, height: 100)
.labelStyle(VerticalLabelStyle())
.onTapGesture(perform: onTap)
}
func onTap() {
text = "tapped"
}
}
If you have some questions please ask

Your idea with a VStack is the correct way. You just have to keep in mind where to place your modifiers. In your case you placed them on the button level. Meaning they are applied to the entire label, which is the reason why your Text is inside the gray background. Just update also the place of your modifiers inside your VStack.
Here is are short example:
Button {
// Your button action
} label: {
VStack {
RoundedRectangle(cornerRadius: 25)
.frame(width: 100, height: 100)
.padding()
.background(Color.black) // new position of background modifier that is only applied on your Image (here a rectangle)
Text("Foo")
.font(.title3)
.foregroundColor(.black)
// These modifiers could stay on button level, but I moved them here to explicitly show where they applied
}
}
Keep in mind you can put any view inside the label of your button and that in this case the Text is part of the button and will trigger the action if a user taps on it. If you only want that the Image is the tap action wrap your button inside a VStack containing only the Image and a Text as the second part of the Stack like this:
VStack {
Button {
// Your button action
} label: {
VStack {
RoundedRectangle(cornerRadius: 25)
.frame(width: 100, height: 100)
.padding()
.background(Color.black)
}
}
Text("Foo")
.font(.title3)
.foregroundColor(.black)
}

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.

SwiftUI TextEditor Divider doesn't change Y position based on text-line count?

I am trying to make a SwiftUI TextEditor with a Divider that adapts its position to stay under the bottom-most line of text inside of a edit-bio section of the app.
Note: I have a frame on my TextEditor so that it doesn't take up the whole-screen
Right now the Divider is static and stays in one place. Is there a built-in way to make the divider stay under the bottom most line of text?
I would think the Spacer would have given me this behavior?
Thank you!
struct EditBio: View {
#ObservedObject var editProfileVM: EditProfileViewModel
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM.bio)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
Divider().frame(height: 1).background(.white)
Spacer()
}
}
}
It is doing exactly what you told it to do. But a background color on your TextEditor. You will see that it has a height of 200 + a spacing of 10 from the VStack.
I changed your code to make it obvious:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.background(Color.gray)
Divider().frame(height: 1).background(.red)
Spacer()
}
}
}
to produce this:
You can see the TextEditor naturally wants to be taller than 200, but that is limiting it. Therefore, the Spacer() is not going to cause the TextEditor to be any smaller.
The other problem that setting a fixed frame causes will be that your text will end up off screen at some point. I am presuming what you really want is a self sizing TextEditor that is no larger than it's contents.
That can be simply done with the following code:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
SelfSizingTextEditor(text: $editProfileVM)
// Frame removed for the image below.
// .frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.foregroundColor(.white)
// made the .top padding to be .vertical
.padding(.vertical, 70)
.padding([.leading, .trailing], 50)
.background(Color.gray)
Divider().frame(height: 5).background(.red)
Spacer()
}
}
}
struct SelfSizingTextEditor: View {
#Binding var text: String
#State var textEditorSize = CGSize.zero
var body: some View {
ZStack {
Text(text)
.foregroundColor(.clear)
.copySize(to: $textEditorSize)
TextEditor(text: $text)
.frame(height: textEditorSize.height)
}
}
}
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
func copySize(to binding: Binding<CGSize>) -> some View {
self.readSize { size in
binding.wrappedValue = size
}
}
}
producing this view:

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)

NavigationLink on SwiftUI pushes view twice

When adding a NavigationLink in SwiftUI the destination is presented twice, ex: I tap on the NavigationLink and it pushes my destination but when I dismiss the destination, via the back button or the swipe gesture it pushes the destination again without taping on the link.
Here is the part of my code that handles the link:
var body: some View {
HStack(spacing: 8.0) {
ForEach(part.getReference()) { (imageRef: ReferenceImage) in
ZStack(alignment: .topTrailing) {
Image(uiImage: imageRef.getUIImage())
.resizable()
.frame(width: 90, height: 90)
.cornerRadius(6)
.onLongPressGesture {
print("looong")
self.managedObjectContext.delete(imageRef)
do {
try self.managedObjectContext.save()
} catch {
print("error deleting: \(error.localizedDescription)")
}
}
ZStack(alignment: .center) {
Circle()
.foregroundColor(Color.appColors.lightRose)
.opacity(0.7)
.frame(width: 35, height: 35)
Image(systemName: "arkit")
.imageScale(.large)
}
NavigationLink(destination:
ZStack {
Color.appColors.rose
.edgesIgnoringSafeArea(.top)
ReferenceARSwiftUIView(currentImage: imageRef.getUIImage())
.navigationBarTitle("AR Reference")
}
) {
EmptyView()
.frame(width: 90, height: 90)
}
}
}.animation(.interpolatingSpring(stiffness: 0.5, damping: 0.5))
EDIT 01:
As suggested I removed a bit of the noise in the code:
var part: Part
var body: some View {
HStack(spacing: 8.0) {
ForEach(part.getReference()) { (imageRef: ReferenceImage) in
ZStack(alignment: .topTrailing) {
Image(uiImage: imageRef.getUIImage())
.resizable()
.frame(width: 90, height: 90)
.cornerRadius(6)
NavigationLink(destination: ReferenceARSwiftUIView(currentImage: imageRef.getUIImage())) {
EmptyView()
.frame(width: 90, height: 90)
}
}
}.animation(.interpolatingSpring(stiffness: 0.5, damping: 0.5))
EDIT 02:
I think I narrowed down, basically if I remove the ForEach the NavigationLink pushes correctly to the next View. Also depending on the number of itens I have on my array for the ForEach the number of pushes is the same.
I'm sure that Part conforms to Identifiable protocol and has id property.
The destination of the NavigationLink is presented multiple times because you have multiple instances of Part that have the exact same identity id in the forEach loop.
Just make sure that each instance of Part has a unique id and everything will work as expected.
I solved this by setting the NavigationLink's tag to the unique id of the model item.
Assuming you have an id for your imageRef, ReferenceImage class.
var part: Part
var body: some View {
HStack(spacing: 8.0) {
ForEach(part.getReference()) { (imageRef: ReferenceImage) in
ZStack(alignment: .topTrailing) {
Image(uiImage: imageRef.getUIImage())
.resizable()
.frame(width: 90, height: 90)
.cornerRadius(6)
NavigationLink(destination: ReferenceARSwiftUIView(currentImage: imageRef.getUIImage()), tag: imageRef.id) {
EmptyView()
.frame(width: 90, height: 90)
}
}
}.animation(.interpolatingSpring(stiffness: 0.5, damping: 0.5))
NavigationLink(destination: ReferenceARSwiftUIView(currentImage: imageRef.getUIImage()), tag: imageRef.id)
Yeah changing List to ScrollView helped me in a similar situation.
ScrollView {
if !usersViewModel.isLoading {
ForEach(self.usersViewModel.users, id: \.id) { user in
NavigationLink(destination: UserDashboardView(showDashboardDetail: $showDashboardDetail, user: user)) {
VStack(alignment: .leading) {
NetworkCardView(user: user)
}
.frame(width: screen.size.width - 60, height: 250)
.padding(.top)
}
}
.listRowBackground(AppColor())
}
}
I wasn't able to reproduce your issue, because even the stripped down version contains types declared by you, but not included in the question.
A couple of things:
If you want to use NavigationLink, the view needs to be embeded in a NavigationView.
If you are using ForEach inside an HStack there is a chance you are going to run out of space to display all the elements, so it would be a good idea to wrap it in a ScrollView.
Last, but not least: inside ForEach you have to make NavigationLink your top level view and make any other views its content:
var body: some View {
NavigationView {
ScrollView(.horizontal) {
HStack {
ForEach(part.getReference()) { (imageRef: ReferenceImage) in
NavigationLink(destination: ReferenceARSwiftUIView(currentImage: imageRef.getUIImage())) {
Image(uiImage: imageRef.getUIImage())
.resizable()
.frame(width: 90, height: 90)
.cornerRadius(6)
}
}
}
}
}
}

Resources