SwiftUI unlimited TabView item - foreach

I got 50 questions on Firebase. I return this data with ForEach in the TabView I prepared as a custom. But from question 23, I can't see other questions. I can't advance to the next page. What is the reason of this ? Can't I return items in TabView unlimitedly?
Gif:
TabView PagingView:
struct PagingQuestionView: View {
var tests: [Test] = []
#State var imageUrl: String = ""
var storage = Storage.storage().reference()
#ObservedObject var pagingQuestionOptionConfigure: PagingQuestionOptionConfigure = PagingQuestionOptionConfigure()
var body: some View {
GeometryReader { proxy in
TabView(selection: $pagingQuestionOptionConfigure.pageIndex) {
ForEach(tests, id: \.self) { item in
VStack(spacing: 20) {
EmptyView()
.frame(width: 350, height: 250)
.padding()
VStack {
Text("\(item.id))")
Text("\(item.question)")
.padding()
}
PagingOptionView(options: item.sections)
}
.tag(item.id)
}
.rotationEffect(.degrees(-90))
.frame(width: proxy.size.width, height: proxy.size.height)
}
.frame(width: proxy.size.height, height: proxy.size.width)
.rotationEffect(.degrees(90), anchor: .topLeading)
.offset(x: proxy.size.width)
.modifier(TabViewModifier())
}
}
}
TabViewModifier:
struct TabViewModifier: ViewModifier {
#ViewBuilder
func body(content: Content) -> some View {
if #available(iOS 14.0, *) {
content
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
} else {
content
}
}
}

I solved my problem.
I added this in ForEach
ForEach(tests.indices, id: \.self){ ... }

Related

How can i animate data fetched from API in my ScrollView/LazyVGrid?

This is my main view where i'm using AsyncAwait to fetch the data. I wanna have the effect of nice animation when i receive the data. Tried a lot of things, but none worked. Some animations were successful but only after opening the specific view for the 3-rd + time. On first opening i could never achieve that effect. Would really appreciate if someone could tell me what should i do. Thanks in advance :)
struct PresentationRootView: View {
// setting up the default value for segmented control
#State private var selectedSegmentedOption: SegmentedSelection = .myPresentations
#ObservedObject var viewModel: PresentationsViewModel
var body: some View {
let columns = [GridItem(.flexible())]
VStack(spacing: 0) {
Picker(selection: $selectedSegmentedOption) {
Text("My Presentations").tag(SegmentedSelection.myPresentations)
Text("Templates").tag(SegmentedSelection.templates)
} label: {}
.pickerStyle(.segmented)
}
ScrollView {
if selectedSegmentedOption == .myPresentations {
if viewModel.shouldShowEmptyScreen {
Text("No data")
}
LazyVGrid(columns: columns, spacing: 4, content: {
ForEach($viewModel.myPresentations, id: \.ID) { element in
PresentationSingleView(url: element.ThumbURLWithSas.wrappedValue, title: element.Name.wrappedValue, numberOfResources: 4)
.padding([.leading, .trailing], 13)
}
}).task {
await viewModel.getAllPresentations(page: 1, pageSize: 10)
}
}
}
.background(CustomColor.background.swiftUIColor)
Spacer()
.navigationTitle("Presentations")
}
}
struct PresentationSingleView: View {
// replace this with the real data
var url: URL
var title: String
var numberOfResources: Int
var isIphone = DefaultAppManager.shared.isIphone
#StateObject var image = FetchImage()
var body: some View {
HStack(alignment: .top) {
image.view?
.resizable()
.scaledToFill()
.frame(maxWidth: isIphone ? 90 : 140, maxHeight: isIphone ? 64 : 140)
.cornerRadius(4)
.padding(isIphone ? 10 : 20)
VStack(alignment: .leading) {
Text(title)
.font(Font.custom("Roboto-Regular", size: 12))
.padding(.top, 20)
Spacer()
HStack(alignment: .center) {
Image("presentationResourceIcon")
Text("\(numberOfResources) resources")
.font(Font.custom("Roboto-Medium", size: 12))
Spacer()
Image("more_options")
.onTapGesture {
print("tapped on right image!!!")
}
}
.padding(.bottom, 20)
}
Spacer()
}
.onAppear {
image.priority = .normal
image.load(url)
}
.frame(height: isIphone ? 90 : 150)
.background(Color.white)
.cornerRadius(8)
}
}

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()
}

Swift How do I hide a view in response to an action?

I have a view with a filter bar. One says "Shirts" & the other says "Trousers". When "Shirts" is selected, it displays a grid view of shirts. When "Trousers" is selected in the filter bar, it shows a grid view of trousers.
When I tap on an item (a shirt or a pair of trousers) it takes me to a different view (showing the product in more detail). However, the filter bar remains. I need to hide the filter bar when an item is tapped and it takes the user to a detail view.
I need to hide the marketplacefilterbar view when a product is tapped.
My ViewModel for the filter bar is:
enum MarketplaceFilterViewModel: Int, CaseIterable {
case shirts
case trousers
var title: String {
switch self {
case .shirts: return "Shirts"
case .trousers: return "Trousers"
}
}
}
Some code from the MarketplaceViewModel:
class MarketplaceViewModel: NSObject, ObservableObject {
#Published var search = ""
// Shirt Data...
#Published var shirts: [Shirt] = []
#Published var filteredShirt: [Shirt] = []
// Trouser Data...
#Published var trousers: [Trouser] = []
#Published var filteredTrouser: [Trouser] = []
}
Code for the marketplace view is:
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10){
marketplaceFilterBar
if selectedMarketplaceFilter == .raise {
MarketplaceShirtView()
}
if selectedMarketplaceFilter == .save {
MarketplaceTrouserView()
}
}
}
var marketplaceFilterBar: some View {
VStack {
HStack(spacing: 15){
Image("Logo")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60.0, height: 60.0)
.padding(EdgeInsets.init(top: 0, leading: 45, bottom: 0, trailing: 0))
Spacer(minLength: 0)
}
.padding([.horizontal,.top])
HStack {
ForEach(MarketplaceFilterViewModel.allCases, id: \.rawValue) { item in
VStack {
Text(item.title)
.font(.headline)
.fontWeight(selectedMarketplaceFilter == item ? .semibold: .regular)
.foregroundColor(selectedMarketplaceFilter == item ? .black: .gray)
if selectedMarketplaceFilter == item {
Capsule()
.foregroundColor(Color("LightBlue"))
.frame(height: 3)
.matchedGeometryEffect(id: "filter", in: animation)
} else {
Capsule()
.foregroundColor(Color(.clear))
.frame(height: 3)
}
}
.onTapGesture {
withAnimation(.easeInOut) {
self.selectedMarketplaceFilter = item
}
}
}
}
.overlay(Divider().offset(x: 0, y: 16))
}
}
Code for the Shirt Detail View:
struct ShirtDetailView: View {
#Binding var shirtData : Shirt!
#Binding var showDetailShirt: Bool
#Namespace var animation: Namespace.ID
// Shared Data Model...
#EnvironmentObject var sharedData: SharedDataModel
#EnvironmentObject var marketplaceData: MarketplaceViewModel
ScrollView {
VStack{
HStack {
Button(action: {
withAnimation(.easeOut){showDetailShirt.toggle()}
}) {
Image(systemName: "arrow.backward.circle.fill")
.foregroundColor(.gray)
.frame(width: 40, height: 40)
.background(Color.white,in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.shadow(color: .black.opacity(0.1), radius: 5, x: 5, y: 5)
}
Text(shirtData.shirt_name)
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.black)
}
VStack(spacing: 10){
WebImage(url: URL(string: shirtData.shirt_image))
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.cornerRadius(15)
Text("Details")
.opacity(0.7)
.frame(maxWidth: .infinity,alignment: .leading)
.padding(.bottom)
HStack(spacing: 15) {
Text(shirtData.shirt_details)
.font(.system(size: 18))
.padding()
}
}
}
}
Code for the Trouser Detail View:
struct TrouserDetailView: View {
#Binding var trouserData : Trouser!
#Binding var showDetailTrouser: Bool
#Namespace var animation: Namespace.ID
// Shared Data Model...
#EnvironmentObject var sharedData: SharedDataModel
#EnvironmentObject var marketplaceData: MarketplaceViewModel
ScrollView {
VStack{
HStack {
Button(action: {
withAnimation(.easeOut){showDetailTrouser.toggle()}
}) {
Image(systemName: "arrow.backward.circle.fill")
.foregroundColor(.gray)
.frame(width: 40, height: 40)
.background(Color.white,in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.shadow(color: .black.opacity(0.1), radius: 5, x: 5, y: 5)
}
Text(trouserData.trouser_name)
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.black)
}
VStack(spacing: 10){
WebImage(url: URL(string: trouserData.trouser_image))
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.cornerRadius(15)
Text("Details")
.opacity(0.7)
.frame(maxWidth: .infinity,alignment: .leading)
.padding(.bottom)
HStack(spacing: 15) {
Text(trouserData.trouser_details)
.font(.system(size: 18))
.padding()
}
}
}
}
Code for the SharedDataModel:
class SharedDataModel: ObservableObject {
// Detail Shirt Data...
#Published var detailShirt : Shirt?
#Published var showDetailShirt : Bool = false
// matched Geoemtry Effect from Search page...
#Published var fromSearchPage: Bool = false
}
The updated code for marketplace:
#Binding var charityData : Charity!
#Binding var showDetailCharity: Bool
#Binding var businessData : Business!
#Binding var showDetailBusiness: Bool
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10){
if !showDetailCharity && !showDetailBusiness {
//MARK: This is the filter bar - the view for this is further down the page
marketplaceFilterBar
}
if selectedMarketplaceFilter == .raise {
MarketplaceRaiseView()
}
if selectedMarketplaceFilter == .save {
MarketplaceSaveView()
}
}
The code for TabView:
#StateObject var appModel: AppViewModel = .init()
#StateObject var sharedData: SharedDataModel = SharedDataModel()
// Animation Namespace...
#Namespace var animation
// Hiding Tab Bar...
init(){
UITabBar.appearance().isHidden = true
}
var body: some View {
VStack(spacing: 0){
// Tab View...
TabView(selection: $appModel.currentTab) {
Marketplace(animation: _animation)
.environmentObject(sharedData)
.tag(Tab.Market)
.setUpTab()
Home()
.environmentObject(sharedData)
.tag(Tab.Home)
.setUpTab()
}
Since you already have showDetailShirt and showDetailTrouser, you can use them to hide the marketplacefilterbar like so:
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10){
if !showDetailShirt && !showDetailTrouser {
marketplaceFilterBar
}
if selectedMarketplaceFilter == .raise {
MarketplaceShirtView()
}
if selectedMarketplaceFilter == .save {
MarketplaceTrouserView()
}
}
}

SwiftUI: matchedGeometryEffect With Nested Views

I'm facing some dilemma. I like to separate my views for readability . so for example i'm having this kind of structure
MainView ->
--List1
----Items1
--List2
----Items2
----DetailView
------CellView
so cellView having same namespace for matchedGeometryEffect as DetailsView. to make the effect of transition to detail view from cell item in list. the problem is that this details view is limited to List2 screen /View.
Here's some code to make it more clear
First I have main View
struct StartFeedView: View {
var body: some View {
ScrollView(.vertical) {
ShortCutView()
PopularMoviesView()
}
}
}
then I have PopularMoviesView()
struct PopularMoviesView: View {
#Namespace var namespace
#ObservedObject var viewModel = ViewModel()
#State var showDetails: Bool = false
#State var selectedMovie: Movie?
var body: some View {
ZStack {
if !showDetails {
VStack {
HStack {
Text("Popular")
.font(Font.itemCaption)
.padding()
Spacer()
Image(systemName: "arrow.forward")
.font(Font.title.weight(.medium))
.padding()
}
ScrollView(.horizontal) {
if let movies = viewModel.popularMovies {
HStack {
ForEach(movies.results, id: \.id) { movie in
MovieCell(movie: movie, namespace: namespace, image: viewModel.imageDictionary["\(movie.id)"]!)
.padding(6)
.frame(width: 200, height: 300)
.onTapGesture {
self.selectedMovie = movie
withAnimation(Animation.interpolatingSpring(stiffness: 270, damping: 15)) {
showDetails.toggle()
}
}
}
}
}
}
.onAppear {
viewModel.getMovies()
}
}
}
if showDetails, let movie = selectedMovie, let details = movie.details {
MovieDetailsView(details: details, namespace: namespace, image: viewModel.imageDictionary["\(movie.id)"]!)
.onTapGesture {
withAnimation(Animation.interpolatingSpring(stiffness: 270, damping: 15)) {
showDetails.toggle()
}
}
}
}
}
}
so whenever I click on MovieCell.. it will expand to the limit of the PopularMoviesView boundaries on the main view.
Is there some kind of way to make it full screen without to have to inject the detail view into the MainView? Cause that would be really dirty
Here is an approach:
Get the size of MainView with GeometryReader and pass it down.
in DetailView use .overlay which can grow bigger than its parent view,
if you specify an explicit .frame
You need another inner GeometryReaderto get the top pos of inner view for offset.
struct ContentView: View {
var body: some View {
// get size of overall view
GeometryReader { geo in
ScrollView(.vertical) {
Text("ShortCutView()")
PopularMoviesView(geo: geo)
}
}
}
}
struct PopularMoviesView: View {
// passed in geometry from parent view
var geo: GeometryProxy
// own view's top position, will be updated by GeometryReader further down
#State var ownTop = CGFloat.zero
#Namespace var namespace
#State var showDetails: Bool = false
#State var selectedMovie: Int?
var body: some View {
if !showDetails {
VStack {
HStack {
Text("Popular")
.font(.caption)
.padding()
Spacer()
Image(systemName: "arrow.forward")
.font(Font.title.weight(.medium))
.padding()
}
ScrollView(.horizontal) {
HStack {
ForEach(0..<10, id: \.self) { movie in
Text("MovieCell \(movie)")
.padding()
.matchedGeometryEffect(id: movie, in: namespace)
.frame(width: 200, height: 300)
.background(.yellow)
.onTapGesture {
self.selectedMovie = movie
withAnimation(Animation.interpolatingSpring(stiffness: 270, damping: 15)) {
showDetails.toggle()
}
}
}
}
}
}
}
if showDetails, let movie = selectedMovie {
// to get own view top pos
GeometryReader { geo in Color.clear.onAppear {
ownTop = geo.frame(in: .global).minY
print(ownTop)
}}
// overlay can become bigger than parent
.overlay (
Text("MovieDetail \(movie)")
.font(.largeTitle)
.matchedGeometryEffect(id: movie, in: namespace)
.frame(width: geo.size.width, height: geo.size.height)
.background(.gray)
.position(x: geo.frame(in: .global).midX, y: geo.frame(in: .global).midY - ownTop)
.onTapGesture {
withAnimation(Animation.interpolatingSpring(stiffness: 270, damping: 15)) {
showDetails.toggle()
}
}
)
}
}
}

Resources