I want to make a segment tab page in SwiftUI such as bellow.
My code is as bellow:
struct ContentView: View {
#State var index = 0
#State private var offset: CGFloat = 0
var body: some View {
VStack(spacing: 0) {
HStack() {
Button("tab1") {
self.index = 0
}
Button("tab2") {
self.index = 1
}
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
VStack() {
List {
VStack {
HStack() {
Text("tab1 line1")
Spacer()
}
.frame(height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab1 line1")
}
}
VStack {
HStack() {
Text("tab1 line2")
Spacer()
}
.frame(height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab1 line2")
}
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.cornerRadius(30)
VStack() {
List {
VStack {
HStack() {
Text("tab2 line1")
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab2 line1")
}
}
VStack {
HStack() {
Text("tab2 line2")
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab2 line2")
}
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.onAppear() {
print("load")
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.cornerRadius(30)
}
}
.content
.offset(x: self.getOffset())
.animation(.spring())
.frame(width: UIScreen.main.bounds.size.width, alignment: .leading)
}
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
.background(Color.gray)
}
private func getOffset() -> CGFloat {
let offset = CGFloat(self.index) * (-UIScreen.main.bounds.size.width)
return offset
}
}
Everything works well expect the onTapGesture in tab2. When tap the line of "tab1",the onTapGesture works well to print "click tab1 line1" or "click tab1 line2".
After click button "tab2", the page will scroll to tab2 page. When click the line of list "tab2", onTapGesture not works.
I try a day, and found two methods to make it works:
After scroll to page "tab2", Scroll up or down the tab2 list, the onTapGesture will works to print "click tab2 line1" and "click tab2 line2".
remove the code .cornerRadius(30), the click in tab2 page will also works.
I just want to keep the radius of list. How to fix this incredible bug?
The problem is here:
.content
.offset(x: self.getOffset())
This duplicates the scrollview's content and offsets it to the right. As a result, what you are tapping is the duplicated content, which probably affects the tap gesture.
Instead, use ScrollViewReader to scroll the ScrollView, and remove your getOffset code.
struct ContentView: View {
#State var index = 0
#State private var offset: CGFloat = 0
var body: some View {
VStack(spacing: 0) {
HStack() {
Button("tab1") {
self.index = 0
}
Button("tab2") {
self.index = 1
}
}
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { proxy in
HStack(alignment: .center, spacing: 0) {
VStack() {
List {
VStack {
HStack() {
Text("tab1 line1")
Spacer()
}
.frame(height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab1 line1")
}
}
VStack {
HStack() {
Text("tab1 line2")
Spacer()
}
.frame(height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab1 line2")
}
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.cornerRadius(30)
.id(0) /// set id here
VStack() {
List {
VStack {
HStack() {
Text("tab2 line1")
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab2 line1")
}
}
VStack {
HStack() {
Text("tab2 line2")
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab2 line2")
}
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.onAppear() {
print("load")
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.cornerRadius(30)
.id(1) /// also set id here
}
.onChange(of: index) { _ in /// scroll the ScrollView
withAnimation(.spring()) {
proxy.scrollTo(index)
}
}
}
}
.frame(width: UIScreen.main.bounds.size.width, alignment: .leading)
}
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
.background(Color.gray)
}
}
Related
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)
}
}
}
I have a tab bar with 2 TabItems, What I want is to have one of the tab item's view beeing transparant.
ZStack {
Color.clean
TabView {
if #available(iOS 15.0, *) {
HomeView()
.tabItem {
Label("Home", systemImage: "person.crop.circle")
}
} else {
// Fallback on earlier versions
}
PatientView()
.tabItem {
Label("Patient", systemImage: "heart.text.square.fill")
}
}
}
I have tried to add an EmptyView() but i didn't get the desired result. HomeView should be transparent but also having 2 buttons displayed.
struct HomeView: View {
var body: some View {
if showLoadingSpinner {
ZStack {
Color.backgroundColor.edgesIgnoringSafeArea(.all)
ProgressView("Loading Avatar")
.progressViewStyle(CircularProgressViewStyle(tint: Color.brandPrimary))
}
.onAppear{
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
showLoadingSpinner = false
}
}
}
else {
GeometryReader { geometry in
ZStack {
VStack {
CreateZipMetadataView()
}
.frame(maxHeight: .infinity, alignment: .top)
.padding(.leading, UIScreen.screenWidth/2 - (geometry.size.width/7)/2)
EmptyView()
.frame(maxHeight: .infinity)
ZStack {
Circle()
.foregroundColor(.white)
.frame(width: geometry.size.width/7, height: geometry.size.width/7)
.padding(.leading, UIScreen.screenWidth/2 - (geometry.size.width/7)/2)
.frame(maxHeight: .infinity, alignment: .bottom)
}
}
}
I made a card style List row which work fine. The issue is that the list row background color remains. I can make it disappear by setting it to systemGray6 but it's not very adaptive for dark mode and there is very likely a better way of doing this.
Card View:
var body: some View {
ZStack {
Rectangle().fill(Color.white)
.cornerRadius(10).shadow(color: .gray, radius: 4)
.frame(width: UIScreen.main.bounds.width - 40)
HStack {
Image(uiImage: (UIImage(data: myVideo.thumbnailImage!) ?? UIImage(systemName: "photo"))!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
VStack {
Text(myVideo.title!)
.multilineTextAlignment(.center)
.font(.body)
.padding(.bottom, 10)
.frame(maxWidth: UIScreen.main.bounds.width - 40, maxHeight: .infinity, alignment: .center)
Text(myVideo.youtuber!)
.font(.subheadline)
}
}
.padding(5)
}
}
List View:
var body: some View {
NavigationView {
if myVideos.isEmpty {
Text("You have not added videos yet!")
font(.subheadline)
} else {
List() {
Section(header: Text("Not Watched")) {
ForEach(myVideos) { video in
if !video.watched {
NavigationLink(destination: MyVideoView(myVideo: video)) {
MyListRowView(myVideo: video)
}
}
}
.onDelete(perform: removeItems)
}
Section(header: Text("Watched")) {
ForEach(myVideos) { video in
if video.watched {
NavigationLink(destination: MyVideoView(myVideo: video)) {
MyListRowView(myVideo: video)
}
}
}
.onDelete(perform: removeItems)
}
.listRowBackground(Color(UIColor.systemGray6))
}
.navigationBarTitle("Videos")
.listStyle(GroupedListStyle())
}
}
}
Image: (Intended behaviour in "WATCHED" and Incorrect behaviour in "NOT WATCHED"
I set the width of a SwiftUI Button to 0 to "deactivate" it.
If the with of the button is set to 0, the button disappears as expected, but clicking in the left edge of the yellow Stack activates the Button.
Why does this happen?
How can I avoid it?
struct ContentView: View {
#State var zeroWidth = false
var body: some View {
VStack{
ButtonLine( leftButtons: [ButtonAttr( label: "LB1",
action: {print("LB1")},
iconSystemName : "person"
)],
zeroWidth: zeroWidth
)
Button("Toggle width \(zeroWidth ? "On" : "Off" ) "){ self.zeroWidth.toggle() }
}
}
}
struct ButtonLine: View {
let leftButtons : [ButtonAttr]
let zeroWidth : Bool
var body: some View {
HStack(spacing: 0) {
ForEach(leftButtons.indices, id: \.self)
{ i in
HStack(spacing: 0.0)
{
Button(action: { self.leftButtons[i].action() }) {
ButtonLabel( singleline: false,
buttonAttr: self.leftButtons[i]
)
.padding(0)
//.background(Color.green) // not visible
}
.buttonStyle(BorderlessButtonStyle())
.frame( width: self.zeroWidth ? 0 : 100, height: 50)
.background(Color.green)
.clipped()
.foregroundColor(Color.white)
.padding(0)
}
// .background(Color.blue) // not visible
}
// .background(Color.blue) // not visible
Spacer()
Text("CONTENT")
.background(Color.green)
.onTapGesture {
print("Content tapped")
}
Spacer()
}
.background(Color.yellow)
.onTapGesture {
print("HS tapped")
}
}
}
struct ButtonLabel: View {
var singleline : Bool
var buttonAttr : ButtonAttr
var body: some View {
VStack (spacing: 0.0) {
Image(systemName: buttonAttr.iconSystemName).frame(height: singleline ? 0 : 20).clipped()
.padding(0)
.background(Color.blue)
Text(buttonAttr.label)
.padding(0)
.background(Color.blue)
}
.padding(0)
.background(Color.red)
}
}
struct ButtonAttr
{ let label : String
let action: ()-> Void
let iconSystemName : String
}
Instead of tricky "deactivate", just use real remove, like below
HStack(spacing: 0.0)
{
if !self.zeroWidth {
Button(action: { self.leftButtons[i].action() }) {
ButtonLabel( singleline: false,
buttonAttr: self.leftButtons[i]
)
.padding(0)
//.background(Color.green) // not visible
}
.buttonStyle(BorderlessButtonStyle())
.frame(width: 100, height: 50)
.background(Color.green)
.clipped()
.foregroundColor(Color.white)
.padding(0)
}
}.frame(height: 50) // to keep height persistent
there is very simple explanation.
try next snippet
struct ContentView: View {
var body: some View {
Text("Hello").padding().border(Color.yellow).fixedSize().frame(width: 0)
}
}
Why?
.frame(..)
is defined as a function of View, which return another View, as any kind of View modifier. The resulting View has .zero sized frame, as expected.
It is really true? Let's check it!
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Rectangle()
.fill(Color.orange)
.frame(width: 100, height: 100)
Text("Hello")
.padding()
.border(Color.black)
.fixedSize()
.frame(width: 0, height: 0)
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)
.blendMode(.exclusion)
}
}
}
Just add .clipped modifier to your Text View
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Rectangle()
.fill(Color.orange)
.frame(width: 100, height: 100)
Text("Hello")
.padding()
.border(Color.black)
.fixedSize()
.frame(width: 0, height: 0)
.clipped()
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)
.blendMode(.exclusion)
}
}
}
and the Text "disappears" ...
It disappears from the screen, but not from View hierarchy!. Change the code again
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Rectangle()
.fill(Color.orange)
.frame(width: 100, height: 100)
Text("Hello")
.padding()
.border(Color.black)
.fixedSize().onTapGesture {
print("tap")
}
.frame(width: 0, height: 0)
.clipped()
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)
.blendMode(.exclusion)
}
}
}
and you see, that there is still some "invisible" area sensitive on tap gesture
You can disable you Button by adding a .disabled(self.zeroWidth)
Button(action: { self.leftButtons[i].action() }) {
ButtonLabel( singleline: false,
buttonAttr: self.leftButtons[i]
)
.padding(0)
//.background(Color.green) // not visible
}
.disabled(self.zeroWidth)
.buttonStyle(BorderlessButtonStyle())
.frame( width: self.zeroWidth ? 0 : 100, height: 50)
.background(Color.green)
.clipped()
.foregroundColor(Color.white)
.padding(0)
You can debug the view hierarchy by clicking that icon in xcode:
As the picture shows, I want the button to be in the bottom right corner.
https://i.imgur.com/GiVor8a.png
var body: some View {
ZStack {
HStack {
Color.black
}
Button(action: {}) {
HStack {
Image(systemName: "rectangle.grid.1x2.fill")
}
.padding()
.background(Color.yellow)
.mask(Circle())
}.frame(width: 60, height: 60)
.border(Color.red, width: 1)
}
}
You can use Spacer() for that,
Here is Your Code :
ZStack {
HStack {
Color.black
}
VStack(alignment:.trailing) {
Spacer()
HStack {
Spacer()
Button(action: {}) {
HStack {
Image(systemName: "rectangle.grid.1x2.fill")
}
.padding()
.background(Color.yellow)
.mask(Circle())
}.frame(width: 60, height: 60)
.border(Color.red, width: 1)
}
}
}