How to prevent SwiftUI to rerender the entire tabview when using a Custom Alert view on top of it? - swift

I have a tab view with 5 tabs. An alert view will be shown at the top when my view model's property gets true? But every time the alert is shown, the entire tab view is redrawn causing high cpu usage.
I want the alert to be shown regardless of which tab the user is in so I did a ZStack on the tab view to show the alert. But how do I prevent SwiftUI to redraw the entire tab view?
struct ContentView: View {
var body: some View {
ZStack {
TabView() {
MiniPlayer(content:
BrowseView()
)
.tabItem {
Group {
Image(systemName: "square.grid.2x2")
Text("Browse")
}
.onTapGesture {
selectedTab = 0
}
}
.tag(0)
MiniPlayer(content:
AllSongsListView()
)
.tabItem {
Group {
Image(systemName: "music.note")
Text("Songs")
}
.onTapGesture {
selectedTab = 1
}
}
.tag(1)
MiniPlayer(content:
PlaylistsView()
)
.tabItem {
Group {
Image(systemName: "music.note.list")
Text("Playlists")
}
.onTapGesture {
selectedTab = 2
}
}
.tag(2)
MiniPlayer(content:
MoreView()
)
.tabItem {
Group {
Image(systemName: "ellipsis")
Text("More")
}
.onTapGesture {
selectedTab = 3
}
}
.tag(3)
}
Alert()
}
.ignoresSafeArea(.keyboard)
}
}
struct Alert: View {
#EnvironmentObject var manager: Manager
var body: some View {
if manager.showSuccessAlert {
VStack {
VStack {
Text("Successful.")
.font(.system(size: 20, weight: .bold, design: .rounded))
.multilineTextAlignment(.center)
}
.frame(height: 50)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.blue)
.clipShape(Capsule())
Spacer()
}
.zIndex(1)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
withAnimation {
manager.showSuccessAlert = false
}
}
}
.transition(.asymmetric(insertion: .move(edge: .top), removal: .move(edge: .top)))
}
}
}

Try to move entire TabView into separated view, so that ZStack looks like
ZStack {
TabsContentView() // << move everything inside !!
Alert()
}

Related

.ignoresSafeArea(.keyboard) does not work on SwiftUI view

I have the following view which is presented as a sheet:
var body: some View {
NavigationView {
GeometryReader { reader in
VStack {
TransactionAmount(amountString: $amountString)
.padding(.top, Constants.amountPadding)
Spacer()
SelectedCategory(selectedCategory: $selectedCategory)
.padding()
.onTapGesture {
isCategoryPickerPresented = true
UIImpactFeedbackGenerator.feedback(style: .medium)
}
AddTransactionToolbar(noteText: $noteText)
.padding(.horizontal)
NumberPad(numberString: $amountString, selectedCategory: $selectedCategory)
}
.sheet(isPresented: $isCategoryPickerPresented) {
CategoryPicker(selection: $selectedCategory)
.presentationDetents([.medium])
}
.onAppear {
selectedCategory = storageManager.categories.first
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.ignoresSafeArea(.keyboard)
.navigationTitle("Add Transaction")
.navigationBarTitleDisplayMode(.inline)
}
}
}
For some reason ignoresSafeArea(.keyboard) is not working and my view is being pushed up when the keyboard is presented. Wrapping the view in GeometryReader hasn't made any difference either. Not really sure where to go from here. How do I fix this?

Disable to Change Page On Swipe of Tab View in SwiftUI

I am using Tab View in my SwiftUI app. I want the changing of page disabled, while swiping left or right. And I had achieved it from this. This works fine but the issue I am facing is I have a button on the bottom of every view, and when I try to swipe from the button, it is swiping left right. I want to disable it too but don't know how to do it. Here is my code:
struct TabViewTesting: View {
#State var tabSelection = 0
var body: some View {
TabView(selection: $tabSelection) {
firstView().tag(0).contentShape(Rectangle()).gesture(DragGesture())
secondView().tag(1).contentShape(Rectangle()).gesture(DragGesture())
thirdView().tag(2).contentShape(Rectangle()).gesture(DragGesture())
}.tabViewStyle(.page(indexDisplayMode: .never))
}
}
And this is the code for the Views:
extension TabViewTesting {
func firstView() -> some View {
VStack {
Text("First screen")
Spacer()
Button {
self.tabSelection = 1
} label: {
ZStack {
RoundedRectangle(cornerRadius: 20)
.frame(height: 50)
Text("move to 2nd view")
.foregroundColor(.white)
}
}.padding()
}.background(.green)
}
func secondView() -> some View {
VStack {
Text("second screen")
Spacer()
Button {
self.tabSelection = 2
} label: {
ZStack {
RoundedRectangle(cornerRadius: 20)
.frame(height: 50)
Text("move to 3rd view")
.foregroundColor(.white)
}
}.padding()
}.background(.red)
}
func thirdView() -> some View {
VStack {
Text("Third screen")
Spacer()
Button {
self.tabSelection = 0
} label: {
ZStack {
RoundedRectangle(cornerRadius: 20)
.frame(height: 50)
Text("move to first view")
.foregroundColor(.white)
}
}.padding()
}.background(.yellow)
}
}
And this is what happening:
I actually found an answer in the comments of this question.
The issue is: any view that has an "onTapGesture" will ignore ".gesture(DragGesture())".
The solution is to use ".simulataneousGesture(DragGesture())" instead to ensure the gesture is capture and handled by both view/modifier.
It worked perfectly in my case after changing it. The only exception is for 2 finger drag gesture.

SwiftUI Set Image Boundary

i want to set my right image frame boundaries like the left one without moving image or text. How can I do that. I tried a couple way but I couldn't find the solution.
You should use the built-in TabView. It provides all the functionality for you, and already made with accessibility in mind.
Here is an example (you can change it for your text and images):
struct ContentView: View {
#State private var selection: Int = 1
var body: some View {
TabView(selection: $selection) {
Text("Tab Content 1")
.tabItem {
Label("1", systemImage: "1.square")
}
.tag(1)
Text("Tab Content 2")
.tabItem {
Label("2", systemImage: "2.square")
}
.tag(2)
Text("Tab Content 3")
.tabItem {
Label("3", systemImage: "3.square")
}
.tag(3)
}
}
}
Result:
Custom version (highly unrecommended):
struct ContentView: View {
var body: some View {
VStack {
Spacer()
Text("Main Content")
Spacer()
HStack {
VStack {
Button {
//
} label: {
Label("1", systemImage: "1.square")
}
}.frame(maxWidth: .infinity)
VStack {
Button {
//
} label: {
Label("2", systemImage: "2.square")
}
}.frame(maxWidth: .infinity)
VStack {
Button {
//
} label: {
Label("3", systemImage: "3.square")
}
}.frame(maxWidth: .infinity)
}.frame(height: 50)
}
}
}

SwiftUI - custom TabBar height based on device

I am trying to set up the height of my custom TabBar based on Device, my code:
struct MyTabBar: View {
#Binding var index: Int
var body: some View {
HStack {
Button(action: {
self.index = 0
}) {
Image(ImageText.iconHome.image)
}
Spacer(minLength: 0)
Button(action: {
self.index = 1
}) {
Image(ImageText.iconBell.image)
}
Spacer(minLength: 0)
Button(action: {
self.index = 2
}) {
Image(ImageText.iconAdd.image)
}
Spacer(minLength: 0)
Button(action: {
self.index = 3
}) {
Image(ImageText.iconSearch.image).foregroundColor(Color.red)
}
Spacer(minLength: 0)
Button(action: {
self.index = 4
}) {
Image(ImageText.iconHamburger.image)
}
}.padding(.horizontal, 26).frame(height: 56)
}
}
If the devices have notch, how can I set my custom TabBar to have higher height? Does SwiftUI have something that can be useful here?
You can use the GeometryReader element to access the safe area insets, and add it as a padding using .safeAreaInsets.bottom (note that the Xcode autocompletion is almost never working on GeometryProxy properties):
var body: some View {
GeometryReader { proxy in
VStack {
// Content goes here
Spacer()
// Custom tab bar
HStack {
Spacer()
Button(action: {}) {
Image(systemName: "house.fill")
.padding()
}
Spacer()
Button(action: {}) {
Image(systemName: "house.fill")
.padding()
}
Spacer()
Button(action: {}) {
Image(systemName: "house.fill")
.padding()
}
Spacer()
Button(action: {}) {
Image(systemName: "house.fill")
.padding()
}
Spacer()
}
.padding(.bottom, proxy.safeAreaInsets.bottom)
.background(Color.red)
}
}.edgesIgnoringSafeArea(.bottom)
}

SwiftUI content is not displayed on top of the screen

I have a NavigationView and then a VStack. The issue is that everything inside VStack is being displayed in the middle of the screen and not on top. Why is that?
var body: some View {
VStack {
VStack(alignment: .leading) {
if (request?.receiverID ?? "") == userDataViewModel.userID {
Text("\(sendertFirstName) wants to ship your package").font(.title)
} else {
Text("You want to ship \(recieverFirstName)'s package").font(.title)
}
HStack{
Image(systemName: "clock.fill").font(.subheadline)
Text("\(createdAt, formatter: DateService().shortTime)").font(.subheadline).foregroundColor(.secondary)
}
HStack {
VStack(alignment: .leading) {
HStack {
Image(systemName: "person.fill").font(.subheadline)
Text(sendertFirstName).font(.subheadline).foregroundColor(.secondary)
}
}
Spacer()
VStack(alignment: .leading) {
HStack {
Image(systemName: "star.fill")
Text(rating).font(.subheadline).foregroundColor(.secondary)
}
}
}
}
VStack(alignment: .center) {
HStack {
Button("Accept") {
//print(self.request.createdAt)
//FirestoreService().respondToRequest(request: self.request.documentReference, status: "accepted")
}
.padding()
Button("Decline") {
//FirestoreService().respondToRequest(request: self.request.documentReference, status: "declined")
}
.foregroundColor(.red)
.padding()
}
}
ScrollView {
ForEach(messages) { (message: Message) in
if message.senderId == self.userDataViewModel.userID {
HStack {
Spacer()
Text(message.text).font(.subheadline).padding(7.5).background(self.blue).cornerRadius(30).foregroundColor(.white)
}.padding(.bottom, 2)
} else {
HStack {
Text(message.text).font(.subheadline).padding(7.5).background(self.gray).cornerRadius(30).foregroundColor(.white)
Spacer()
}.padding(.bottom, 2)
}
}
}
HStack {
TextField("Message", text: $inputMessage).padding(5).background(self.gray).cornerRadius(30).foregroundColor(.white)
Button(action: {
let msg: [String: Any] = [
"text": self.inputMessage,
"createdAt": Timestamp(),
"senderId": self.userDataViewModel.userID
]
self.reference.updateData([
"messages": FieldValue.arrayUnion([msg])
])
self.messages.append(Message(dictionary: msg))
self.inputMessage = ""
}) {
Image(systemName: "arrow.up.circle.fill").foregroundColor(self.blue).font(.title)
}
}
Spacer()
}
.padding()
.border(Color.red)
}
I want the content to start on top not on the middle. What am I doing wrong here?
You don't need the NavigationView - the way you have everything set up now you are wrapping the VStack in a NavigationView twice and the massive white space is the second empty navigation bar.
By default most Views take only the amount of space they need and they are placed in the center of their parent view. So if you want your content to be placed at the top, you need to add Spacer() as the last element in your VStack:
var body: some View {
VStack {
/* ... */
Spacer()
}
}
Spacer is one of the rare Views that takes all space offered to it by the parent view and will push all of your content upwards.
Maybe you can try this way:
NavigationView {
VStack {
...
}.frame(maxHeight:.infinity, alignment: .top)
.padding()
.border(Color.red).navigationBarTitle("", displayMode: .inline)
}
Add a .navigationBarHidden(true) to your VStack