How to align text sub-views in VStack? - swiftui

I have created a package to simply layout a What's New page for an app. The core view is a BulletPointView which is defined as such:
import SwiftUI
public struct BulletPointView: View {
let title: String
let image: Image
let text : String
public init(title: String = "New feature",
image: Image = Image(systemName: "circle.fill"),
text: String = "This is a new feature for this app. And this text should wrap.") {
self.title = title
self.image = image
self.text = text
}
public var body: some View {
HStack (alignment: .center){
image
.font(.title)
.foregroundColor(Color("AccentColor"))
VStack (alignment: .leading, spacing: 4){
Text(title)
.fontWeight(.semibold)
Text(text)
.foregroundColor(.secondary)
}
.multilineTextAlignment(.leading)
.font(.subheadline)
.padding(.bottom, 6)
}
}
}
struct BulletPointView_Previews: PreviewProvider {
static var previews: some View {
VStack {
BulletPointView(image: Image(systemName: "square.and.pencil"))
BulletPointView(image: Image(systemName: "hare.fill"))
BulletPointView(image: Image(systemName: "circle.fill"))
BulletPointView(image: Image(systemName: "car.2.fill"))
BulletPointView(image: Image(systemName: "switch.2"))
BulletPointView(image: Image(systemName: "ellipsis"))
}
}
}
This allows the user to select any image to use as a bullet, or defaults to a circle. The problem is that if the images are different widths, the text to the right of the images does not align anymore as seen in the screenshot below.
How do i get all of the text views to line up regardless of the image width?
Thanks!

Here is the simplest way that is possible. You must to have a size for Image.
PS: You should not use Image as parameter for your view, just use the string of image. I corrected for you.
struct ContentView: View {
var body: some View {
VStack {
BulletPointView(string: "square.and.pencil")
BulletPointView(string: "hare.fill")
BulletPointView(string: "circle.fill")
BulletPointView(string: "car.2.fill")
BulletPointView(string: "switch.2")
BulletPointView(string: "swiftPunk")
}
.padding()
}
}
struct BulletPointView: View {
let title: String
let string: String
let text : String
init(title: String = "New feature",
string: String,
text: String = "This is a new feature for this app. And this text should wrap.") {
self.title = title
self.string = string
self.text = text
}
var body: some View {
HStack (alignment: .center){
imageFunction(string: string)
.scaledToFit()
.frame(width: 50, height: 50)
VStack (alignment: .leading, spacing: 4){
Text(title)
.fontWeight(.semibold)
Text(text)
.foregroundColor(.secondary)
}
.multilineTextAlignment(.leading)
.font(.subheadline)
.padding(.bottom, 6)
}
}
#ViewBuilder func imageFunction(string: String) -> some View {
if (UIImage(systemName: string) != nil) {
Image(systemName: string)
.font(.title)
}
else {
Image(string)
.resizable()
}
}
}

Related

Keep VStack in center of the screen in SwiftUI

I'm new on SwiftUI and I don't know how to manage my views.
I have this code:
import SwiftUI
struct ContentView: View {
#State private var email: String = ""
#State private var passord: String = ""
var body: some View {
ZStack {
Image("corner")
.resizable()
.scaledToFill()
VStack {
VStack { //VStack1
TextField("Email", text: $email)
.frame(width: 300, height: 40)
.textFieldStyle(.roundedBorder)
.bold(true)
SecureField("Password", text: $passord)
.frame(width: 300, height: 40)
.textFieldStyle(.roundedBorder)
.bold(true)
Button {
//Do something
} label: {
Text("Forgot your password ?")
.underline()
.foregroundColor(.white)
}
}
VStack { // VStack2
Text("Not registered ?")
.font(.title2)
.foregroundColor(.white)
Button("Sign up") {}
.frame(width: 200, height: 37)
.foregroundColor(.white)
.background(Color(.orange))
.cornerRadius(20)
}
}
}
}
}
I want to place the VStack2 in the bottom of the screen and keep the VStack1 on the center of the screen.
How I can do that. I've try to search but I don't find the solution on StackOverflow.
I tried to play with Spacer() and padding() but I have not a good result.
Screen
A simple way to do this would be to add VStack1 to the ZStack which will place it in the centre of the screen. Then add wrap VStack2 in another VStack with a Spacer to push it to the bottom of the screen, e.g.
ZStack {
Image("corner")
VStack { //VStack1
// etc
}
VStack {
Spacer()
VStack { // VStack2
// etc
}
}
}
Simple put
HStack {
Spacer()
VStack {
Text("Centered")
}
Spacer()
}
import SwiftUI
struct ContentView: View {
#State private var email: String = ""
#State private var passord: String = ""
var body: some View {
VStack {
Spacer()
middleView()
Spacer()
bottomView()
.paddind(.bottom, 12)
}
.ignoresSafeArea()
.background {
Image("corner")
.resizable()
.scaledToFill()
}
}
func middleView() -> some View {
VStack(spacing: 20) {
TextField("Email", text: $email)
.frame(width: 300, height: 40)
.textFieldStyle(.roundedBorder)
.bold(true)
SecureField("Password", text: $passord)
.frame(width: 300, height: 40)
.textFieldStyle(.roundedBorder)
.bold(true)
Button {
//Do something
} label: {
Text("Forgot your password ?")
.underline()
.foregroundColor(.white)
}
}
}
func bottomView() -> some View() {
VStack {
Text("Not registered ?")
.font(.title2)
.foregroundColor(.white)
Button("Sign up") {}
.frame(width: 200, height: 37)
.foregroundColor(.white)
.background(Color(.orange))
.cornerRadius(20)
}
}
}

Edited: How we can alignment Image with Images and Text withTexts in SwiftUI, Multi-alignment?

I have a VStack which has some HStack as you can see in my codes, inside my each Hstack there is an Image and Text, after running my codes the Alignmet of all codes is ugly, I want the Image alignment center together and Text alignment leading. How I can solve the problem?
I can make all Image .center Alignment, and also all Text .leading Alignment. But I can not make both happen at same time.
struct CustomAlignment: AlignmentID
{
static func defaultValue(in context: ViewDimensions) -> CGFloat
{
return context[HorizontalAlignment.center]
}
}
struct CustomAlignment2: AlignmentID
{
static func defaultValue(in context: ViewDimensions) -> CGFloat
{
return context[HorizontalAlignment.leading]
}
}
extension HorizontalAlignment
{
static let custom: HorizontalAlignment = HorizontalAlignment(CustomAlignment.self)
static let custom2: HorizontalAlignment = HorizontalAlignment(CustomAlignment2.self)
}
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(alignment: .custom)
{
HStack()
{
Image(systemName: "folder")
.alignmentGuide(.custom) { $0[HorizontalAlignment.center] }
Text("Some texts here.")
.alignmentGuide(.custom2) { $0[HorizontalAlignment.leading] }
Spacer()
}
HStack()
{
Image(systemName: "music.note")
.alignmentGuide(.custom) { $0[HorizontalAlignment.center] }
Text("Some texts here.")
.alignmentGuide(.custom2) { $0[HorizontalAlignment.leading] }
Spacer()
}
HStack()
{
Image(systemName: "person.fill.questionmark")
.alignmentGuide(.custom) { $0[HorizontalAlignment.center] }
Text("Some texts here.")
.alignmentGuide(.custom2) { $0[HorizontalAlignment.leading] }
Spacer()
}
}
.padding()
Spacer()
}
}
Use custom alignment guide if you want precise control.
About your comment on using fixed frame, here is an article which explains how frame works in SwiftUI.
Basically, frame modifier just adds a fixed size frame around the SF in this case, but it won't alter the intrinsic size.
struct ContentView: View {
var body: some View {
VStack(alignment: .sfView) {
SFView(title: "This is some text", image: "folder")
SFView(title: "SwiftUI is cool. Combine is cooler.", image: "snow")
SFView(title: "This is a music note. This has a different length.", image: "music.note")
}
}
}
private struct SFView: View {
let title: String
let image: String
var body: some View {
HStack(spacing: 8) {
Image(systemName: image)
.font(.system(size: 20))
.frame(width: 32, height: 32)
.alignmentGuide(.sfView) { d in d[HorizontalAlignment.center] }
Text(title)
.alignmentGuide(.sfView) { d in d[HorizontalAlignment.leading] }
}
}
}
private extension HorizontalAlignment {
struct SFViewAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.leading]
}
}
static let sfView = HorizontalAlignment(SFViewAlignment.self)
}
You have to give a frame to the image as some SF Symbols are larger than others, also try to create reusable views.
try something like this:
struct ContentView: View {
var body: some View {
VStack {
RowView(title: "Some texts here.", image: "folder")
RowView(title: "Some texts here.", image: "person.fill.questionmark")
RowView(title: "Some texts here.", image: "snow")
RowView(title: "Some texts here.", image: "forward.end.alt.fill")
}
.padding()
Spacer()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct RowView: View {
let title: String
let image: String
var body: some View {
// Option 1 with Label
// Label(
// title: {
// Text(title)
// },
// icon: {
// Image(systemName: image)
// .frame(width: 30, height: 30)
// }
// )
// Option 2 with HStack
HStack {
Image(systemName: image)
.frame(width: 30, height: 30)
Text(title)
Spacer()
}
}
}
You are WAY overcomplicating it. You don't need to use all these custom alignments and constraints for something this simple.
Go back to basics and use a regular VStack / HStack and just set the icon to be an exact frame. The issue was arising because the icons had slightly different widths.
struct TestView: View {
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(systemName: "folder")
.frame(width: 30, height: 30, alignment: .center)
Text("Some text here")
}
HStack {
Image(systemName: "person.fill.questionmark")
.frame(width: 30, height: 30, alignment: .center)
Text("Some text here")
}
HStack {
Image(systemName: "snow")
.frame(width: 30, height: 30, alignment: .center)
Text("Some text here")
}
HStack {
Image(systemName: "music.note")
.frame(width: 30, height: 30, alignment: .center)
Text("Some text here")
}
}
}
}

How Can I Handle Tap Gesture on Widget?

I'm writing a widget with WidgetKit and I want to make the widget's content is clickable. For example, if users click to standings, I want to open the standings tab when the app becomes active.
I tried to use notification between the app and widget but the tap gesture is not working, I added print inside of the tap gesture but it did not appear in the console. Also, I added the same app group to both of them.
WidgetView:
struct LargeWidget : View {
#State var standings : [StandingsTable]
var body: some View {
VStack(alignment:.leading){
if standings.count > 0{
HStack(spacing:5){
Text("#").foregroundColor(.gray)
.frame(width: 30)
Text("Team".localized).foregroundColor(.gray)
Spacer()
Text("_D".localized).foregroundColor(.gray)
.frame(width: 30)
Text("_L".localized).foregroundColor(.gray)
.frame(width: 30)
Text("_W".localized).foregroundColor(.gray)
.frame(width: 30)
Text("_P".localized).foregroundColor(.gray)
.frame(width: 30)
}
Divider()
ForEach(0..<5, id: \.self) { i in
HStack(spacing:5){
Text(standings[i].rank)
.font(.system(size: 15))
.padding(.vertical, 3)
.frame(width: 30)
.background(Color(UIColor.systemBackground))
.cornerRadius(4)
Text(standings[i].name)
.lineLimit(1)
.padding(.leading, 5)
Spacer()
Text(standings[i].drawn)
.frame(width: 30)
Text(standings[i].lost)
.frame(width: 30)
Text(standings[i].won)
.frame(width: 30)
Text(standings[i].points)
.frame(width: 30)
}
.padding(.vertical, 5)
.background(standings[i].name == "Besiktas" ? Color(UIColor.systemGray6) : Color.clear)
.cornerRadius(8)
}
Spacer(minLength: 0)
}else{
Text("Large")
.padding()
}
}.padding()
.onTapGesture {
print("clicked to standings")
DispatchQueue.main.async(execute: {
NotificationCenter.default.post(name: NSNotification.Name("standings"), object: nil, userInfo: nil)
})
}
}
}
and here ContentView in app:
import SwiftUI
extension NSNotification {
static let openStandings = NSNotification.Name.init("standings")
}
struct ContentView: View {
#State var show: Bool = false
var body: some View {
NavigationView{
Text("Hello, world!")
.padding()
}.sheet(isPresented: self.$show) {
VStack{
Text("Notification")
.padding()
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.openStandings))
{ obj in
self.show.toggle()
}
}
}
Screenshot of Widget
Okay I created a new project with SwiftUI Environments (not old way AppDelegate and SceneDelegate).
Then I use Link for tap actions and I got it with .onOpenURL modifier in ContentView. It works :)
ContentView:
import SwiftUI
enum SelectedTab: Hashable {
case home
case standings
case options
}
struct ContentView: View {
#State var selectedTab: SelectedTab = .home
var body: some View {
TabView(selection: self.$selectedTab) {
Text("Home")
.tabItem {
Image(systemName: "house")
.renderingMode(.template)
Text("Home")
}.tag(SelectedTab.home)
Text("Standings")
.tabItem {
Image(systemName: "list.number")
.renderingMode(.template)
Text("Standings")
}.tag(SelectedTab.standings)
Text("Options")
.tabItem {
Image(systemName: "gear")
.renderingMode(.template)
Text("Options")
}.tag(SelectedTab.options)
.onOpenURL { (url) in
if url.absoluteString == "widget-deeplink://standings"{
self.selectedTab = .standings
}
}
}
}
}
Link usage example in Widget:
Link(destination: URL(string: "widget-deeplink://standings")!) {
Text("Link Test")
}

Create new row in table when button is clicked Swift UI

I am trying to insert a row in my table when I click my "Add Sets" button. TableRow is a very simple view that is just an HStack with a text field and two text input fields. I am stuck and there isn't much online for this issue. Thank You
struct ExerciseCard: View {
#State private var exercise : String = "Bench Press"
#State private var sets : Int = 1
var body: some View {
VStack{
TextField("Enter Exercise", text: $exercise).textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 300)
.multilineTextAlignment(.center)
HStack{
Group{
Text("Set")
Text("Weight")
Text("Reps")
}.padding(.horizontal, 30)
}
VStack{
ForEach(0..<sets){ number in
TableRow()
}
}.padding(.bottom, 5)
Button(action: {
self.sets += 1
}) {
Text("Add Set")
.frame(minWidth: 200)
.padding(.vertical, 5)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(20)
}
}
.padding()
.background(Color.blue.opacity(0.6))
.cornerRadius(20)
}
}
struct ExerciseCard_Previews: PreviewProvider {
static var previews: some View {
ExerciseCard()
}
}

SwiftUI how to set image for NavigationBar titleView like in UIKit?

I want to set an image in the titleView of NavigationBar in SwiftUI, as we do in UIKit
navigationItem.titleView = UIImageView(image: UIImage(named: "logo"))
this is how we do it in UIKit.
anyone know how to do it?
Here's how to do it:
Add SwiftUIX to your project.
Set your custom title view via View.navigationBarTitleView(_:displayMode:)
Example code:
struct ContentView: View {
public var body: some View {
NavigationView {
Text("Hello World")
.navigationBarTitleView(MyView())
}
}
}
Simple, Just add your root view into ZStack with top alignment and add your custom center view after root view
struct CenterNavigattionBar: View {
var body: some View {
ZStack(alignment: .top){
//Root view with empty Title
NavigationView {
Text("Test Navigation")
.navigationBarTitle("",displayMode: .inline)
.navigationBarItems(leading: Text("Cancle"), trailing: Text("Done"))
}
//Your Custom Title
VStack{
Text("add title and")
.font(.headline)
Text("subtitle here")
.font(.subheadline)
}
}
}
}
Before Image
After Image
Just use a toolbar.
You can add any views
import SwiftUI
struct HomeView: View {
// MARK: - Initializer
init() {
let appearance = UINavigationBar.appearance()
appearance.isOpaque = true
appearance.isTranslucent = false
appearance.barTintColor = UIColor(named: "background")
appearance.shadowImage = UIImage()
}
// MARK: - View
// MARK: Public
var body: some View {
NavigationView {
VStack(spacing: 20) {
Text("Hello")
Text("Navigation Bar Test")
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: leadingBarButtonItems, trailing: trailingBarButtonItems)
.toolbar {
ToolbarItem(placement: .principal) {
VStack {
Text("Title").font(.headline)
Text("Subtitle").font(.subheadline)
}
}
}
}
}
// MARK: Private
private var leadingBarButtonItems: some View {
Button(action: {
}) {
Text("Left Button")
.font(.system(size: 12, weight: .medium))
}
}
private var trailingBarButtonItems: some View {
HStack {
Button(action: {
}) {
Text("R1\nButton")
.font(.system(size: 12, weight: .medium))
.multilineTextAlignment(.center)
}
Button(action: {
}) {
Text("R2\nButton")
.font(.system(size: 12, weight: .medium))
.multilineTextAlignment(.center)
}
}
}
}
Currently, you can't.
There are two overloads for .navigationBarTitle(), taking either a Text view or a type conforming to StringProtocol. You can't even pass in a modified view like Text("Title").font(.body). This would be a great feature, I'd submit a feature request: http://feedbackassistant.apple.com
Maybe this works for you?
Basically:
Use GeometryReader to get the width of the screen
Have NavigationBarItems(leading: HStack {Spacer() Image("name").resizable().frame(width:..., height: ..., alignment: .center Spacer()}.frame(width:geometry.size.width)
Example code:
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { geometry in
Text("Hello, world!")
.padding()
.navigationTitle("test")
.navigationBarItems(leading: HStack {
Spacer()
Image("money")
.resizable()
.frame(width: 50, height: 50, alignment: .center)
Spacer()
}
.frame(width: geometry.size.width)
)
}
}
}
}
Try this...
How to put a logo in NavigationView in swiftui?
This shows how to handle adding an Image to NavigationView in SwiftUI. Hope it helps.