HStack direction is incorrect for RTL Languages - swiftui

HStack has layoutDirection set to .rightToLeft show items aligned to the left side instead of the right.
import SwiftUI
struct TestView: View {
var data = ["Test 1", "Test 2"]
var body: some View {
ScrollView(.horizontal) {
HStack{
ForEach(data, id: \.self) { name in
Text(name)
}
}
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
.environment(\.layoutDirection, .rightToLeft)
}
}

You can use the my below modifier.
https://gist.github.com/metin-atalay/bdf28a604fe9678403730d14f02946ca

This is not an error or 'incorrect'. You should use a Spacer() in your HStack.
HStack{
ForEach(data, id: \.self) { name in Text(name) }
Spacer()
}

Related

How to add the Navigation Bar space in SwiftUI NavigationView

I hide the navigation bar and provide a customized navigation bar.
But my list appears below the navigation bar.
I want the list to appear below the navigation bar when scrolling.
My desired effect:
Actual effect: Blocked 0
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
FirstView()
.tabItem {
Image(systemName: "folder.fill")
Text("Home")
}
SecondView()
.tabItem {
Image(systemName: "folder.fill")
Text("SecondView")
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct FirstView: View {
var body: some View {
ZStack(alignment: .top) {
List {
ForEach(0..<40, id: \.self) { index in
Text("cell view \(index)")
}
}
.listStyle(.inset)
HStack {
Text("首页")
}
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(.bar)
}
.navigationBarTitleDisplayMode(.inline)
//.ignoresSafeArea(edges: .)
}
}
struct SecondView: View {
var body: some View {
List {
ForEach(0..<40, id: \.self) { index in
Text("FirstView2")
}
}
.listStyle(.inset)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Please do not add padding to the List. This is a security zone problem.
Edit: scrolling under the header
For a glass effect under the header:
You can add a "dummy" element that will push the list one row down. So the the header will cover the dummy element and the whole list will be visible.
struct FirstView: View {
var body: some View {
ZStack(alignment: .top) {
List {
// Dummy Text
Text("")
ForEach(0..<40, id: \.self) { index in
Text("cell view \(index)")
}
}
.listStyle(.inset)
HStack {
Text("首页")
}
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(.bar)
.opacity(0.7)
}
.navigationBarTitleDisplayMode(.inline)
//.ignoresSafeArea(edges: .)
}
}
Original answer
Your FirstView is using a ZStack, which tells exactly the compiler to show one view on top of (covering) the other. Use a VStack, that should solve your issue.
struct FirstView: View {
var body: some View {
VStack {
HStack {
Text("首页")
}
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(.bar)
List {
ForEach(0..<40, id: \.self) { index in
Text("cell view \(index)")
}
}
.listStyle(.inset)
}
.navigationBarTitleDisplayMode(.inline)
//.ignoresSafeArea(edges: .)
}
}

how can i make a conditional navigation in swiftui [duplicate]

I am trying to push from login view to detail view but not able to make it.even navigation bar is not showing in login view. How to push on button click in SwiftUI? How to use NavigationLink on button click?
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Let's get you signed in.")
.bold()
.font(.system(size: 40))
.multilineTextAlignment(.leading)
.frame(width: 300, height: 100, alignment: .topLeading)
.padding(Edge.Set.bottom, 50)
Text("Email address:")
.font(.headline)
TextField("Email", text: $email)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Text("Password:")
.font(.headline)
SecureField("Password", text: $password)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Button(action: {
print("login tapped")
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
.padding(.horizontal,30)
}
.navigationBarTitle(Text("Login"))
}
To fix your issue you need to bind and manage tag with NavigationLink, So create one state inside you view as follow, just add above body.
#State var selection: Int? = nil
Then update your button code as follow to add NavigationLink
NavigationLink(destination: Text("Test"), tag: 1, selection: $selection) {
Button(action: {
print("login tapped")
self.selection = 1
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
Meaning is, when selection and NavigationLink tag value will match then navigation will be occurs.
I hope this will help you.
iOS 16+
Note: Below is a simplified example of how to present a new view. For a more advanced generic example please see this answer.
In iOS 16 we can access the NavigationStack and NavigationPath.
Usage #1
A new view is activated by a simple NavigationLink:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(value: "NewView") {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #2
A new view is activated by a standard Button:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button {
path.append("NewView")
} label: {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #3
A new view is activated programmatically:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Text("Content View")
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
.onAppear {
path.append("NewView")
}
}
}
iOS 13+
The accepted answer uses NavigationLink(destination:tag:selection:) which is correct.
However, for a simple view with just one NavigationLink you can use a simpler variant: NavigationLink(destination:isActive:)
Usage #1
NavigationLink is activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
}
.navigationBarTitle(Text("Login"))
}
}
}
Usage #2
NavigationLink is hidden and activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
}
}
Usage #3
NavigationLink is hidden and activated programmatically:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
.onAppear {
self.isLinkActive = true
}
}
}
Here is a GitHub repository with different SwiftUI extensions that makes navigation easier.
Another approach:
SceneDelegate
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: BaseView().environmentObject(ViewRouter()))
self.window = window
window.makeKeyAndVisible()
}
BaseView
import SwiftUI
struct BaseView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "view1" {
FirstView()
} else if viewRouter.currentPage == "view2" {
SecondView()
.transition(.scale)
}
}
}
}
#if DEBUG
struct MotherView_Previews : PreviewProvider {
static var previews: some View {
BaseView().environmentObject(ViewRouter())
}
}
#endif
ViewRouter
import Foundation
import Combine
import SwiftUI
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "view1" {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
FirstView
import SwiftUI
struct FirstView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Button(action: {self.viewRouter.currentPage = "view2"}) {
NextButtonContent()
}
}
}
}
#if DEBUG
struct FirstView_Previews : PreviewProvider {
static var previews: some View {
FirstView().environmentObject(ViewRouter())
}
}
#endif
struct NextButtonContent : View {
var body: some View {
return Text("Next")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
SecondView
import SwiftUI
struct SecondView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Spacer(minLength: 50.0)
Button(action: {self.viewRouter.currentPage = "view1"}) {
BackButtonContent()
}
}
}
}
#if DEBUG
struct SecondView_Previews : PreviewProvider {
static var previews: some View {
SecondView().environmentObject(ViewRouter())
}
}
#endif
struct BackButtonContent : View {
var body: some View {
return Text("Back")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
Hope this helps!
Simplest and most effective solution is :
NavigationLink(destination:ScoresTableView()) {
Text("Scores")
}.navigationBarHidden(true)
.frame(width: 90, height: 45, alignment: .center)
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.contentShape(Rectangle())
.padding(EdgeInsets(top: 16, leading: UIScreen.main.bounds.size.width - 110 , bottom: 16, trailing: 20))
ScoresTableView is the destination view.
In my opinion a cleaner way for iOS 16+ is using a state bool to present the view.
struct ButtonNavigationView: View {
#State private var isShowingSecondView : Bool = false
var body: some View {
NavigationStack {
VStack{
Button(action:{isShowingSecondView = true} ){
Text("Show second view")
}
}.navigationDestination(isPresented: $isShowingSecondView) {
Text("SecondView")
}
}
}
}
I think above answers are nice, but simpler way should be:
NavigationLink {
TargetView()
} label: {
Text("Click to go")
}

how to integrate functions in SwiftUI in order to create re-usable code with input dictionaries

I want to re-use the VStack code in SwiftUI:
var primary:[String:String] = ["Price":"price", "Grade":"Two", "Recovery":"Three"]
var secondary:[String:String] = ["Price":"price", "Grade":"Two", "Recovery":"Three"]
struct ContentView: View {
var body: some View {
VStack {
List{
ForEach(Array(primary), id: \.key) { key, value in
HStack {
Text(key)
.fontWeight(.light)
.padding()
Spacer()
Text(value)
.fontWeight(.light)
.padding()
}
}
}
}
The VStack works fine , but I now want to create a function or a ViewModifier or some sort in order to run the VStack twice for both arrays. I could just put the code within the loop into a ViewModeifier in this simple example, but this is not the point. I want the whole VStack to be repeatable with input variables.
Separate mentioned VStack into dedicated view and use it with input data, like below (tested with Xcode 12 / iOS 14)
struct ReuseContentView: View {
var body: some View {
VStack {
DictionaryView(data: primary)
DictionaryView(data: secondary)
}
}
}
struct DictionaryView: View {
let data: [String: String]
var body: some View {
VStack {
List{
ForEach(Array(primary), id: \.key) { key, value in
HStack {
Text(key)
.fontWeight(.light)
.padding()
Spacer()
Text(value)
.fontWeight(.light)
.padding()
}
}
}
}
}
}

swiftui list not working when put into ZStack

I use ZStack to combine a list and Color, after doing it, List will not scroll and there's no output when clicking the text.
Does anyone know how to fix it?
Thanks
struct ContentView: View {
var body: some View {
ZStack{
List{
ForEach(1...30, id: \.self){ i in
Text("ROW \(i)")
.font(.system(size: 40))
.onTapGesture {
print("clicked \(i)")
}
}
}
Color.black.opacity(0.2)
}
}
}
Move Color before List and it will work. See the altered code below.
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack{
Color.black.opacity(0.2)
List{
ForEach(1...30, id: \.self) { i in
Text("ROW \(i)")
.font(.system(size: 40))
.onTapGesture {
print("clicked \(i)")
}
}
}
}
}
}
I don't know why it isn't working — probably a bug — but you can (and probably should) do this instead.
struct ContentView: View {
var body: some View {
List{
ForEach(1...30, id: \.self){ i in
Text("ROW \(i)")
.font(.system(size: 40))
.onTapGesture {
print("clicked \(i)")
}
}
}.background(Color.black.opacity(0.2))
}
}

SwiftUI, setting title to child views of TabView inside of NavigationView does not work

Why I am putting TabView into a NavigationView is because I need to hide the bottom tab bar when user goes into 2nd level 'detail' views which have their own bottom action bar.
But doing this leads to another issue: all the 1st level 'list' views hosted by TabView no longer display their titles. Below is a sample code:
import SwiftUI
enum Gender: String {
case female, male
}
let members: [Gender: [String]] = [
Gender.female: ["Emma", "Olivia", "Ava"], Gender.male: ["Liam", "Noah", "William"]
]
struct TabItem: View {
let image: String
let label: String
var body: some View {
VStack {
Image(systemName: image).imageScale(.large)
Text(label)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
ListView(gender: .female).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
ListView(gender: .male).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
}
}
}
struct ListView: View {
let gender: Gender
var body: some View {
let names = members[gender]!
return List {
ForEach(0..<names.count, id: \.self) { index in
NavigationLink(destination: DetailView(name: names[index])) {
Text(names[index])
}
}
}.navigationBarTitle(Text(gender.rawValue), displayMode: .inline)
}
}
struct DetailView: View {
let name: String
var body: some View {
ZStack {
VStack {
Text("profile views")
}
VStack {
Spacer()
HStack {
Spacer()
TabItem(image: "pencil.circle", label: "Edit")
Spacer()
TabItem(image: "minus.circle", label: "Delete")
Spacer()
}
}
}
.navigationBarTitle(Text(name), displayMode: .inline)
}
}
What I could do is to have a #State var title in the root view and pass the binding to all the list views, then have those list views to set their title back to root view on appear. But I just don't feel so right about it, is there any better way of doing this? Thanks for any help.
The idea is to join TabView selection with NavigationView content dynamically.
Demo:
Here is simplified code depicting approach (with using your views). The NavigationView and TabView just position independently in ZStack, but content of NavigationView depends on the selection of TabView (which content is just stub), thus they don't bother each other. Also in such case it becomes possible to hide/unhide TabView depending on some condition - in this case, for simplicity, presence of root list view.
struct TestTabsOverNavigation: View {
#State private var tabVisible = true
#State private var selectedTab: Int = 0
var body: some View {
ZStack(alignment: .bottom) {
contentView
tabBar
}
}
var contentView: some View {
NavigationView {
ListView(gender: selectedTab == 0 ? .female : .male)
.onAppear {
withAnimation {
self.tabVisible = true
}
}
.onDisappear {
withAnimation {
self.tabVisible = false
}
}
}
}
var tabBar: some View {
TabView(selection: $selectedTab) {
Rectangle().fill(Color.clear).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
Rectangle().fill(Color.clear).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
.frame(height: 50) // << !! might be platform dependent
.opacity(tabVisible ? 1.0 : 0.0)
}
}
This maybe a late answer, but the TabView items need to be assigned tag number else binding selection parameter won't happen. Here is how I do the same thing on my project:
#State private var selectedTab:Int = 0
private var pageTitles = ["Home", "Customers","Sales", "More"]
var body: some View {
NavigationView{
TabView(selection: $selectedTab, content:{
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text(pageTitles[0])
}.tag(0)
CustomerListView()
.tabItem {
Image(systemName: "rectangle.stack.person.crop.fill")
Text(pageTitles[1])
}.tag(1)
SaleView()
.tabItem {
Image(systemName: "tag.fill")
Text(pageTitles[2])
}.tag(2)
MoreView()
.tabItem {
Image(systemName: "ellipsis.circle.fill")
Text(pageTitles[3])
}.tag(3)
})
.navigationBarTitle(Text(pageTitles[selectedTab]),displayMode:.inline)
.font(.headline)
}
}