Implementing custom sidebar across all views - SwiftUI - ios

OUTLINE
I have made a custom slimline sidebar that I am now implementing across the whole app. The sidebar consists of a main button that is always showing and when pressed it shows or hides the rest of the sidebar that consists of buttons navigating to other views.
I am currently implementing the sidebar across the app on each view by creating a ZStack like this:
struct MainView: View {
var body: some View {
ZStack(alignment: .topLeading) {
SideBarCustom()
Text("Hello, World!")
}
}
}
PROBLEM
I am planning on adding a GeometryReader so if the side bar is shown the rest of the content moves over. With this in mind, the way I am implementing the sidebar on every view feels clunky and a long winded way to add it. Is there a more simple/better method to add this to each view?
Sidebar Code:
struct SideBarCustom: View {
#State var isToggle = false
var names = ["Home", "Products", "Compare", "AR", "Search"]
var icons = ["house.fill", "printer.fill.and.paper.fill", "list.bullet.rectangle", "arkit", "magnifyingglass"]
var imgSize = 20
var body: some View {
GeometryReader { geo in
VStack {
Button(action: {
self.isToggle.toggle()
}, label: {
Image("hexagons")
.resizable()
.frame(width: 40, height: 40)
.padding(.bottom, 20)
})
if isToggle {
ZStack{
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.red)
.frame(width: 70, height: geo.size.height)
VStack(alignment: .center, spacing: 60) {
ForEach(Array(zip(names, icons)), id: \.0) { item in
Button(action: {
// NAVIIGATE TO VIEW
}, label: {
VStack {
Image(systemName: item.1)
.resizable()
.frame(width: CGFloat(imgSize), height: CGFloat(imgSize))
Text(item.0)
}
})
}
}
}
}
}
}
}
}

I don't think there's necessarily a reason to use GeometryReader here. The following is an example that has a dynamic width sidebar (although you could set it to a fixed value) that slides in and out. The main content view resizes itself automatically, since it's in an HStack:
struct ContentView : View {
#State private var sidebarShown = false
var body: some View {
HStack {
if sidebarShown {
CustomSidebar(sidebarShown: $sidebarShown)
.frame(maxHeight: .infinity)
.border(Color.red)
.transition(sidebarShown ? .move(edge: .leading) : .move(edge: .trailing) )
}
ZStack(alignment: .topLeading) {
MainContentView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
if !sidebarShown {
Button(action: {
withAnimation {
sidebarShown.toggle()
}
}) {
Image(systemName: "info.circle")
}
}
}
}
}
}
struct CustomSidebar : View {
#Binding var sidebarShown : Bool
var body: some View {
VStack {
Button(action: {
withAnimation {
sidebarShown.toggle()
}
}) {
Image(systemName: "info.circle")
}
Spacer()
Text("Hi")
Text("There")
Text("World")
Spacer()
}
}
}
struct MainContentView: View {
var body: some View {
VStack {
Text("Main content")
}
}
}

Related

How to hide TabView in NavigationLink?

First of all I did a full research for my problem and NOTHING on Google helped me,
the problem is simple, it seems that the solution should also be simple, but I can't hide the tabbar in NavigationLink, and if something works out, then wierd behavior of the buttons and the transition back, etc...
TabView itself
import SwiftUI
struct Main: View {
#State var currentTab: Int = 0
var body: some View {
TabView(selection: $currentTab) {
HomeView().tag(0)
AccountInfoView().tag(1)
SettingsView().tag(2)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.edgesIgnoringSafeArea(.bottom)
.overlay(
TabBarView(currentTab: $currentTab),
alignment: .top
)
}
}
struct TabBarView: View {
#Binding var currentTab: Int
#Namespace var namespace
var tabBarOptions: [String] = ["Search", "Items", "Account"]
var body: some View {
HStack(spacing: 0) {
ForEach(Array(zip(self.tabBarOptions.indices,
self.tabBarOptions)),
id: \.0,
content: {
index, name in
TabBarItem(currentTab: self.$currentTab,
namespace: namespace.self,
tabBarItemName: name,
tab: index)
})
}
.padding(.top)
.background(Color.clear)
.frame(height: 100)
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarItem: View {
#Binding var currentTab: Int
let namespace: Namespace.ID
var tabBarItemName: String
var tab: Int
var body: some View {
Button {
self.currentTab = tab
} label: {
VStack {
Spacer()
Text(tabBarItemName)
if currentTab == tab {
CustomColor.myColor
.frame(height: 2)
.matchedGeometryEffect(id: "underline",
in: namespace,
properties: .frame)
} else {
Color.clear.frame(height: 2)
}
}
.animation(.spring(), value: self.currentTab)
}
.fontWeight(.heavy)
.buttonStyle(.plain)
}
}
NavigationLink -> this is just the part of the code that contains the NavigationLink, this VStack of course is inside the NavigationView.
struct HomeView: View {
NavigationView {
...
VStack(spacing: 15) {
ForEach(self.data.datas.filter {(self.search.isEmpty ? true : $0.title.localizedCaseInsensitiveContains(self.search))}, id: \.id) { rs in
NavigationLink(
destination: ItemDetails(data: rs)
){
RecentItemsView(data: rs)
}
.buttonStyle(PlainButtonStyle())
}
}
}
}
ItemDetails
struct ItemDetails: View {
let data: DataType
var body : some View {
NavigationView {
VStack {
AsyncImage(url: URL(string: data.pic), content: { image in
image.resizable()
}, placeholder: {
ProgressView()
})
.aspectRatio(contentMode: .fill)
.frame(width: 250, height: 250)
.clipShape(RoundedRectangle(cornerRadius: 12.5))
.padding(10)
VStack(alignment: .leading, spacing: 8, content: {
Text(data.title)
.fontWeight(.bold)
.frame(maxWidth: .infinity, alignment: .center)
Text(data.description)
.font(.caption)
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .center)
})
.padding(20)
}
.padding(.horizontal)
}
.navigationBarBackButtonHidden(true)
}
}
I apologize for the garbage in the code, it seemed to me that there is not much of it and it does not interfere with understanding the code, also during the analysis of this problem on the Google\SO, I did not need to work with other parts of the code anywhere, except for those that I provided above, but if I missed something, then please let me know, thanks.

Data loss on View Class, when keyboard appears

When ever I click on textField and key board appears, my list of data i.e coming from API is vanashing,
import SwiftUI
import ExytePopupView
struct Wallet: View {
#State private var searchText = ""
#State private var showCancelButton: Bool = false
#ObservedObject var walltetVM = ShopViewModel()
#State var showShopDetails: Bool = false
#State var openShowDetails: Bool = false
#State var selectedShopId:String = ""
#State var selectedCouponDetail: CouponModel?
var body: some View {
NavigationView {
VStack {
// Search view
VStack{
HStack {
Button {
} label: {
Image(systemName: "magnifyingglass")
.foregroundColor(AppColors.semiBlue)
.frame(width: 20, height: 20, alignment: .center)
.padding()
}
ZStack(alignment: .leading) {
if searchText.isEmpty {
Text(MaggnetLocalizedString(key: "Restaurant, Beauty shop...", comment: ""))
.foregroundColor(AppColors.blackWhite)
.font(Font(AppFont.lightFont(lightFontWithSize: 15)))
}
TextField("", text: $searchText)
.font(Font(AppFont.regularFont(regularFontWithSize: 15)))
}
.foregroundColor(AppColors.blackWhite)
.padding(.horizontal,10)
.padding(.leading,-15)
Divider()
Button {
} label: {
HStack {
Image("places")
}
.padding(.horizontal,20)
.padding(.leading,-12)
}
}
.frame(height: 45)
.background(AppColors.fadeBackground)
.clipShape(Capsule())
}
.padding(.horizontal)
.padding(.vertical,10)
ScrollView(.vertical) {
VStack{
NavigationLink(destination:ShopDetail(shopId:self.selectedShopId)
.environmentObject(walltetVM),
isActive: $openShowDetails) {
EmptyView()
}.hidden()
Points()
ForEach(0..<walltetVM.finalsCouponList.count,id: \.self){
index in
VStack{
// SHOP LIST HEADERS
HStack{
Text(walltetVM.finalsCouponList[index].name)
.multilineTextAlignment(.leading)
.font(Font(AppFont.mediumFont(mediumFontWithSize: 15)))
.foregroundColor(AppColors.blackWhite)
.padding(.horizontal,10)
Spacer()
Button {
} label: {
Text(MaggnetLocalizedString(key: "viewAll", comment: ""))
.font(Font(AppFont.regularFont(regularFontWithSize: 12)))
.foregroundColor(AppColors.blackWhite)
.padding()
.frame(height: 27, alignment: .center)
.background(AppColors.fadeBackground)
.cornerRadius(8)
}
}
.padding()
// MAIN SHOP LIST
VStack{
ScrollView(.horizontal,showsIndicators: false){
HStack{
ForEach(0..<walltetVM.finalsCouponList[index].couopons.count,id: \.self){
indeX in
Shops(coupons: walltetVM.finalsCouponList[index].couopons[indeX])
.onTapGesture {
selectedShopId = walltetVM.finalsCouponList[index].couopons[indeX].businessId?.description ?? ""
print(selectedShopId)
selectedCouponDetail = walltetVM.finalsCouponList[index].couopons[indeX]
showShopDetails = true
}
}
}
.padding(.horizontal)
}
}
.padding(.top,-5)
}
.padding(.top,-5)
}
}
}
.blur(radius: showShopDetails ? 3 : 0)
.popup(isPresented: $showShopDetails, autohideIn: 15, dismissCallback: {
showShopDetails = false
}) {
ShopDetailPopUp(couponDeatil: self.selectedCouponDetail)
.frame(width: 300, height: 400)
}
.navigationBarTitle(Text("Wallet"),displayMode: .inline)
.navigationBarItems(trailing: HStack{
Button {
} label: {
Image("wishIcon").frame(width: 20, height: 20, alignment: .center)
}
Button {
} label: {
Image("notifIcon").frame(width: 20, height: 20, alignment: .center)
}
})
.resignKeyboardOnDragGesture()
}
.onAppear(){
walltetVM.getWallets()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.openShopDetails))
{ obj in
showShopDetails = false
openShowDetails.toggle()
}
.environment(\.layoutDirection,Preferences.chooseLanguage == AppConstants.arabic ? .rightToLeft : .leftToRight)
}
}
}
struct Wallet_Previews: PreviewProvider {
static var previews: some View {
Wallet()
}
}
bannerList is marked as #Published, API call working fine, but in same View class I have one search text field , when I tap on it all the data I was rendering from API get lost and disappears from list.
You are holding a reference to your view model using ObservedObject:
#ObservedObject var walltetVM = ShopViewModel()
However, because you are creating the view model within the view, the view might tear down and recreate this at any moment, which might be why you are losing your data periodically.
If you use StateObject, this ensures that SwiftUI will retain the object for as long as the view lives, so the object won't be recreated.
#StateObject var walltetVM = ShopViewModel()
Alternatively, you could create the view model outside of the view and inject it into the view and keep using ObservedObject (but you'll still need to make sure the object lives as long as the view).
I found something working but still not optimised solution
instead of calling it on onAppear, I called API function in init method.
init(){
walltetVM.getWallets()
}

TabView animation glitch

Get that strange glitch and can't fix it.
Source of problem - TabView and changing size of child views with animation.
Changing UITabbar appearance not helped. Changing safe area options not helped.
UITabBar.appearance().isHidden = false and opaque appearance give flickering of whole tabbar.
I want to hide default UITabbar to customize my own.
Any ideas?
Flickering of bottom safe area: Demonstration GIF
Sample project:
struct ContentView: View {
#State var selected = "second"
var body: some View {
ZStack(alignment: .bottom) {
VStack(spacing: 0) {
FirstView()
TabView(selection: $selected) {
SecondView()
.tabItem({
Text("second")
})
.tag("second")
ThirdView()
.tabItem({
Text("third")
})
.tag("third")
}
}
HStack {
Image(systemName: selected == "second" ? "circle.fill" : "circle")
.onTapGesture {
selected = "second"
}
Image(systemName: selected == "third" ? "circle.fill" : "circle")
.onTapGesture {
selected = "third"
}
}
.frame(width: 70, height: 40, alignment: .center)
.background(Color.white)
.cornerRadius(10)
.padding()
}
.edgesIgnoringSafeArea(.all)
.onAppear {
UITabBar.appearance().isHidden = true
}
}
}
struct FirstView: View {
#State var height:CGFloat = 200
var body: some View {
ZStack(alignment: .bottom) {
Color.red
.frame(height: height)
Image(systemName: height == 200 ? "arrow.down" : "arrow.up")
.foregroundColor(.white)
.padding(5)
.onTapGesture {
withAnimation(.easeInOut(duration: 2)) {
height = height == 200 ? 350 : 200
}
}
}
}
}
struct SecondView: View {
var body: some View {
ZStack {
Text("hello")
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.green)
.onAppear {
UITabBar.appearance().isHidden = true
}
}
}
struct ThirdView: View {
var body: some View {
Color.blue
.edgesIgnoringSafeArea(.bottom)
}
}
Thanks for answers :)
Since you have your own index navigation anyways, I recommend to get rid of TabView altogether and switch views by if / else or switch statements:
struct ContentView: View {
#State var selected = "second"
var body: some View {
ZStack(alignment: .bottom) {
VStack(spacing: 0) {
FirstView()
if selected == "second" {
SecondView()
} else {
ThirdView()
}
}
HStack {
Image(systemName: selected == "second" ? "circle.fill" : "circle")
.onTapGesture {
selected = "second"
}
Image(systemName: selected == "third" ? "circle.fill" : "circle")
.onTapGesture {
selected = "third"
}
}
.frame(width: 70, height: 40, alignment: .center)
.background(Color.white)
.cornerRadius(10)
.padding()
}
.edgesIgnoringSafeArea(.all)
}
}

How to create sticky bottom in SwiftUI?

I am trying to create sticky footer in swiftUI where other part of screen is scrollable but in footer there is one view with buttons and other element which should be fixed.
Thank You for help.
If I understand correctly, what you want to do is stack vertically (VStack)
a Scrollview
another VStack (with the Toggle and the Button), aligned at the bottom :
VStack {
ScrollView {...} // 1
VStack { // 2
Toggle(...)
Button(...)
}
.frame(alignment: .bottom)
}
To take your example :
struct SwiftUIView: View {
#State private var checked: Bool = false
let text = String(repeating: "blabla ", count: 20)
var body: some View {
VStack {
ScrollView {
ForEach((1...100), id: \.self) {_ in
Text(text)
}
}
VStack {
Toggle(isOn: $checked, label: {
Text("I have read...")
})
Button("Enter") {
// action
}
.frame(maxWidth: .infinity)
.padding(.vertical)
.background(Color.red)
}
.padding()
.border(Color.black)
.frame(alignment: .bottom)
}
}
}

Buttons inside list item don't work properly

I have 2 views: PollCard and PollList (like list of polls)
In the PollCard view I have 2 buttons(images), that calls "answer" function:
HStack{
Button(action: {
self.answer()
print("Pressed first image")
}){
Image(poll.v1img)
.resizable()
.renderingMode(.original)
.scaledToFill()
.frame(width: 150, height: 200)
}.frame(width: 150, height: 200)
Button(action: { self.answer()}){
Image(poll.v2img )
.resizable()
.renderingMode(.original)
.frame(width: 150, height: 200)
}.frame(width: 150, height: 200).zIndex(4)
}
In the PollList view I have this simple list:
var body: some View {
HStack{
List(pollData) { poll in
PollCard(poll: poll)
}.padding()
}
}
But when I click the images in the list, it selects like all images and presses it
It is also very easy to check - terminal prints Pressed first image even if I've pressed only second image
What should I do to fix this?
As I mentioned in the comment section the workaround would be to substitute the HStack around the List with a ScrollView and the List with a ForEach:
struct ContentView: View {
struct Data: Identifiable {
var id: Int
}
#State var data = [Data(id: 0), Data(id: 1), Data(id: 2), Data(id: 3), Data(id: 4), Data(id: 5)]
var body: some View {
ScrollView {
ForEach(self.data) { data in
HStack {
Button(action: {
print("Pressed blue...")
}, label: {
Rectangle()
.foregroundColor(Color.blue)
.frame(width: 150, height: 200)
})
Button(action: {
print("Pressed red...")
}, label: {
Rectangle()
.foregroundColor(Color.red)
.frame(width: 150, height: 200)
})
}
}
}
}
}
I hope this helps!
this is an unexpected behavior of Button (or it is a bug?) in current SwiftUI (either on macOS or iOS).
The workaround in your case is simple, try to apply PlainButtonStyle for your buttons
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Text("Hello, World!")
HStack {
Button(action: {
print("button1")
}) {
Color.yellow
}.buttonStyle(PlainButtonStyle())
Button(action: {
print("button2")
}) {
Color.green
}.buttonStyle(PlainButtonStyle())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The funny thing is that it is enough to apply the style on one button only ... or apply it to parent HStack :-)
changing ContentView ...
struct ContentView: View {
var body: some View {
List {
Text("Hello, World!")
HStack {
Button(action: {
print("button1")
}) {
Color.yellow
}
Button(action: {
print("button2")
}) {
Color.green
}
}
.buttonStyle(PlainButtonStyle())
.frame(height: 100)
Text("By by, World!")
}
}
}
you get
where each of buttons works as expected

Resources