Update search results from api when value changes - swift

I have a search bar that will perform a search function based off user input. When the user searches a list is populated (ie. this is a meal app, so if they search for "eggs" a list will be populated showing results of search)
My issue is when the user completes there first search and wants to type again to find a new value (food), the list does not populate again. The API still makes the call, but I'm having trouble updating the list. I tried adding removeAll() to the array onSubmit but it didn't work as expected.
struct FoodSearchResultsView: View {
//calls API
#EnvironmentObject private var foodApi: FoodApiSearch
//textfield input
#State private var searchResultsItem = ""
//if toggled, will display, binded to search bar
#Binding var userSearch: Bool
//var holds if textfield typing is complete by user
#Binding var textComplete: Bool
//triggers select breakfast, lunch, dinner optins
//when false, api results will not display
#State private var isViewSearching = false
var body: some View {
if userSearch{
VStack{
Text(isViewSearching ? "Results" : "Searching..")
Spacer()
// delays showing api call
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
self.isViewSearching = true
}
}
//if user has completed searching for a food
if isViewSearching{
List(foodApi.userSearchResults){meal in
VStack{
HStack{
VStack(alignment: .leading){
Text(meal.mealName)
HStack{
Text(meal.calories + " cals, ")
.font(.caption)
.offset(y:8)
Text(meal.brand)
.font(.caption)
.offset(y:8)
}
}
.foregroundColor(.black)
Spacer()
Image(systemName: "plus.app")
.font(.title2)
.foregroundColor(.blue)
.offset(x: 30)
}
.frame(width:200, height:40) //width of background
.padding([.leading, .trailing], 60)
.padding([.top, .bottom], 10)
.background(RoundedRectangle(
cornerRadius:20).fill(Color("LightWhite")))
.foregroundColor(.black)
Spacer()
}
}
.frame(height:800)
}
}
}
}
}
SearchBar
struct MealSearchBar: View {
//TEXTFIELD
#State var userFoodInput = ""
#State private var didtextComplete = false
//if user seached for a meal
#State private var didUserSearch = false
//calls search API
#StateObject private var foodApi = FoodApiSearch()
var body: some View {
VStack{
ZStack{
Rectangle()
.foregroundColor(Color("LightWhite"))
HStack{
Image(systemName: "magnifyingglass")
TextField("Enter Food", text: $userFoodInput)
.onSubmit {
foodApi.searchFood(userItem: userFoodInput)
didUserSearch = true
userFoodInput = ""
}
//Text(foodApi.foodDescription)
}
.foregroundColor(.black)
.padding(.leading, 13)
}
.frame(height:40)
.cornerRadius(15)
.padding(12)
}
FoodSearchResultsView(userSearch: $didUserSearch, textComplete: $didtextComplete)
.environmentObject(foodApi)
}
}
I only attached my results view and searchbar that calls the view. I believe the issue is happening onSubmit of the textfield, if you need the api call as well, will be happy to supply it, but to confirm again for clarity, despite the list not refreshing, the API is still updating, despite the list not updating.
Update: Added API Call
class FoodApiSearch: ObservableObject{
var userSearchResults: [Meal] = Array()
#Published var foodUnit = ""
#Published var calories = ""
#Published var brand = ""
//will search for user Input
func searchFood(userItem: String){
///IMPROVE API FUNCTION LATER ON DURING LAUNCH
///
let urlEncoded = userItem.addingPercentEncoding(withAllowedCharacters: .alphanumerics)
guard
let url = URL(string: "https://api.nal.usda.gov/fdc/v1/foods/search?&api_key=****GWtDvDZVOy8cqG&query=\(urlEncoded!)") else {return}
URLSession.shared.dataTask(with: url) { (data, _,_) in
let searchResults = try! JSONDecoder().decode(APISearchResults.self, from: data!)
DispatchQueue.main.async { [self] in
var counter = 0
for item in searchResults.foods ?? []{
if (counter < 5){
self.userSearchResults.append(Meal(
id: UUID(),
brand: item.brandOwner?.lowercased().firstCapitalized ?? "Brand Unavailable",
mealName: item.lowercaseDescription?.firstCapitalized ?? "food invalid",
calories: String(Double(round(item.foodNutrients?[3].value! ?? 0.00)).removeZerosFromEnd()),
quantity: 2,
amount: "test",
protein: 2,
carbs: 2,
fat: 2)
)
counter += 1
}
else{return}
}
}
}
.resume()
}
}

Thank you everyone for the advice and tips. So the actual issue was actually one of relative simplicity. I was just missing emptying the array upon ending the search.
if didtextComplete{
print("complete")
foodApi.userSearchResults = [] //emptys list
userFoodInput = ""
}
I also marked a var didtextComplete = true when the search was finished. Not the answer I was looking for, but overall it works so I'll remain satisfied for the time being

Related

How to clear variable when triggered in another view

I am attempting to clear variable values when a button is pressed in another view. Code below:
Root view
struct RecipeEditorHomeMenu: View {
#Environment(\.dismiss) var dismiss
#State var recipeAddedSuccess = false
#State private var showSaveButton = false
#State var showSuccessMessage = false
#State private var sheetMode: SheetMode = .none
#ObservedObject var recipeClass = Recipe()
var onDismiss: (() -> Void)?
var body: some View {
GeometryReader{ geo in
VStack{
if showSuccessMessage {
RecipeSuccessPopUp(shown: $showSuccessMessage, onDismiss: onDismiss)
}
RecipeEditorView(showSuccessMessage: $showSuccessMessage)
.blur(radius: showSuccessMessage ? 15 : 0)
.padding(.top, 80)
// Spacer()
//display save button
FlexibleSheet(sheetMode: $sheetMode) {
SaveRecipeButton(showSuccessMessage: $showSuccessMessage)
//sets coordinates of view on dash
.offset(y:-200)
}
}
.onAppear{
recipeAddedSuccess = false
}
//center view
.alignmentGuide(VerticalAlignment.center, computeValue: { $0[.bottom] })
.position(x: geo.size.width / 2, y: geo.size.height / 2)
.environmentObject(recipeClass)
}
}
}
EditorView (where I would like to clear variables and refresh the view so now it appears new and no variables have values anymore:
struct RecipeEditorView: View {
#EnvironmentObject var recipeClass: Recipe
#State var recipeTitle = ""
#State private var recipeTime = "Cook Time"
#State private var pickerTime: String = ""
//calls macro pickers#State var proteinPicker: Int = 0
#Binding var showSuccessMessage: Bool
var cookingTime = ["5 Mins", "10 Mins","15 Mins","20 Mins","25 Mins","30 Mins ","45 Mins ","1 Hour","2 Hours", "A Long Time", "A Very Long Time"]
var resetPickerTime: (() -> Void)?
var body: some View {
VStack{
TextField("Recipe Title", text: $recipeTitle)
.onChange(of: recipeTitle, perform: { _ in
recipeClass.recipeTitle = recipeTitle
})
.foregroundColor(Color.black)
.font(.title3)
.multilineTextAlignment(.center)
//macro selectors
HStack{
Picker(selection: $carbPicker, label: Text("")) {
ForEach(pickerGramCounter(), id: \.self) {
Text(String($0) + "g Carbs")
}
.onChange(of: carbPicker, perform: { _ in
recipeClass.recipeCarbMacro = carbPicker
})
}
.accentColor(.gray)
}
.accentColor(.gray)
}
HStack(spacing: 0){
ZStack{
Image(systemName:("clock"))
.padding(.leading, 150)
.foregroundColor(Color("completeGreen"))
Picker(selection: $pickerTime, label: Text("Gender")) {
ForEach(cookingTime, id: \.self) {
Text($0)
}
}
.onChange(of: pickerTime, perform: { _ in
recipeClass.recipePrepTime = pickerTime
})
.accentColor(.gray)
}
.multilineTextAlignment(.center)
}
}
}
}
Button where I save the recipes (and would like to clear the variables here:
struct SaveRecipeButton: View {
#Binding var showSuccessMessage: Bool
//stores recipe
#EnvironmentObject var recipeClass: Recipe
#State var isNewRecipeValid = false
static var newRecipeCreated = false
var body: some View {
Button(action: {
//action
}){
HStack{
Image(systemName: "pencil").resizable()
.frame(width:40, height:40)
.foregroundColor(.white)
Button(action: {
if (recipeClass.recipeTitle != "" &&
recipeClass.recipePrepTime != "" &&
){
isNewRecipeValid = true
showSuccessMessage = true
saveRecipe()
}
else{
showSuccessMessage = false
isNewRecipeValid = false
}
}){
Text("Save Recipe")
.font(.title)
.frame(width:200)
.foregroundColor(.white)
}
}
.padding(EdgeInsets(top: 12, leading: 100, bottom: 12, trailing: 100))
.background(
RoundedRectangle(cornerRadius: 10)
.fill(
Color("completeGreen")))
}
.transition(.sideSlide)
.animation(.easeInOut(duration: 0.25))
}
}
My recipe class holds each variable as #Published variables. I tried clearing the variables in this class as well, but the values don't refresh in the view (example: If I have a recipeTitle as "Eggs" and save that recipe down, recipeTitle does not update in the view)
**update recipe class:
class Recipe: ObservableObject, Identifiable {
let id = UUID().uuidString
#Published var recipeTitle: String = ""
#Published var isCompleted = false
#Published var recipeCaloriesMacro: Int = 0
#Published var recipeFatMacro: Int = 0
#Published var recipeProteinMacro: Int = 0
}
I asume you want to reset the values in Recipe. The reason because this doesn´t reflect into RecipeEditorView are the #State values in your RecipeEditorView.
You are neglecting a basic principle of SwiftUI: "Single source of truth".
RecipeEditorView has several #State values that you then bind to your Pickers and TextFields. Then you observe their changes and synchronize with your ViewModel Recipe.
Don´t do that, use the ViewModel itself as the (I know I´m repeating myself here) "Single source of truth".
Another issue. If a view manages the lifecycle of an ObservableObject like instantiating it and holding on to it, declare it as #StateObject. #ObservedObject is only needed it the class is injected and managed up the hierarchy.
In RecipeEditorHomeMenu:
#StateObject var recipeClass = Recipe()
in RecipeEditorView remove all that #State vars that are used to define the recipe e.g.: recipeTitle, carbPicker and pickerTime and bind to the values in the ViewModel.
Possible implementation for recipeTitle:
// this needs to be changed
TextField("Recipe Title", text: $recipeClass.recipeTitle)
Do this for the others too.
Now in SaveRecipeButton you can change these vars to whatever value you want. E.g.: resetting the title would look like:
recipeClass.recipeTitle = ""

Unwrapped optional keeps refreshing / Working with optionals, arrays and randomElement()

I was wondering how you would approach making the following application work:
You have an array where you take a random element.
You make it multiply with a random number.
You have to input the answer and then whether or not you were right you get a score point.
Trying to make it work I stumbled upon the following problems:
When actually running the program every time the view was updated from typing something into the TextField, it caused the optional to keep getting unwrapped and therefor kept updating the random number which is not the idea behind the program.
How do I check whether or not the final result is correct? I can't use "myNum2" since it only exists inside the closure
Here's my code:
struct Calculate: View {
#State var answerVar = ""
#State var myNum = Int.random(in: 0...12)
let numArray = [2,3,4,5] // this array will be dynamically filled in real implementation
var body: some View {
VStack {
List {
Text("Calculate")
.font(.largeTitle)
HStack(alignment: .center) {
if let myNum2 = numArray.randomElement() {
Text("\(myNum) * \(myNum2) = ") // how can i make it so it doesn't reload every time i type an answer?
}
TextField("Answer", text: $answerVar)
.multilineTextAlignment(.trailing)
}
HStack {
if Int(answerVar) == myNum * myNum2 { //how can i reuse the randomly picked element from array without unwrapping again?
Text("Correct")
} else {
Text("Wrong")
}
Spacer()
Button(action: {
answerVar = ""
myNum = Int.random(in: 0...12)
}, label: {
Text("Submit")
.foregroundColor(.white)
.shadow(radius: 1)
.frame(width: 70, height: 40)
.background(.blue)
.cornerRadius(15)
.bold()
})
}
}
}
}
}
Move your logic to the button action and to a function to setupNewProblem(). Have the logic code change #State vars to represent the state of your problem. Use .onAppear() to set up the first problem. Have the button change between Submit and Next to control the submittal of an answer and to start a new problem.
struct Calculate: View {
#State var myNum = 0
#State var myNum2 = 0
#State var answerStr = ""
#State var answer = 0
#State var displayingProblem = false
#State var result = ""
let numArray = [2,3,4,5] // this array will be dynamically filled in real implementation
var body: some View {
VStack {
List {
Text("Calculate")
.font(.largeTitle)
HStack(alignment: .center) {
Text("\(myNum) * \(myNum2) = ")
TextField("Answer", text: $answerStr)
.multilineTextAlignment(.trailing)
}
HStack {
Text(result)
Spacer()
Button(action: {
if displayingProblem {
if let answer = Int(answerStr) {
if answer == myNum * myNum2 {
result = "Correct"
} else {
result = "Wrong"
}
displayingProblem.toggle()
}
else {
result = "Please input an integer"
}
}
else {
setupNewProblem()
}
}, label: {
Text(displayingProblem ? "Submit" : "Next")
.foregroundColor(.white)
.shadow(radius: 1)
.frame(width: 70, height: 40)
.background(displayingProblem ? .green : .blue)
.cornerRadius(15)
.bold()
})
}
}
}
.onAppear {
setupNewProblem()
}
}
func setupNewProblem() {
myNum = Int.random(in: 0...12)
myNum2 = numArray.randomElement()!
result = ""
answerStr = ""
displayingProblem = true
}
}

My TextField resets to "" on first pass through view but acts normal thereafter

I have created a simple swiftui search View that I use to dynamically search documents in firestore. Essentially, the user begins typing the name he wants to search for and once he get to 3 characters of the name, I execute a search against firebase for every character typed thereafter.
Here is my issue.
Initially, the user types his first 3 characters and the following occurs:
immediately the TextField he is typing in is reset to "".
the search does execute perfectly as i can tell from 'print()' statements, however, no search results are presented to the user.
So when the user begins typing his search value again, the results appear immediately.
After this odd sequence, all is well. The TextField shows the search string and resulting values perfectly.
Can you help me understand why the textfield goes blank after the first 3 characters are entered?
import SwiftUI
import Firebase
import FirebaseFirestore
import FirebaseAuth
struct PlayGroupSearchCoursesView: View {
#StateObject var playGroupViewModel = PlayGroupViewModel()
#StateObject var courseListViewModel = CourseListViewModel()
#State var masterPlayerList = [Player]()
#State var searchText = ""
#State var sval = ""
#State var isSearching = false
#State var activeSearchAttribute: Int = 0
#State var presentCourseAddedAlertSheet = false
#State var selectedCourseIdx: Int = 0
var body: some View {
NavigationView {
ScrollView {
HStack {
HStack {
TextField("Search", text: $searchText)
.textCase(.lowercase)
.autocapitalization(.none)
.padding(.leading, 24)
.textFieldStyle(.roundedBorder)
.onChange(of: searchText) { newValue in
print("search text changed to <\(newValue)>")
// we will only return 7 findings and not until they enter at least 3 chars
if newValue.count > 2 {
Task {
do {
try await self.courseListViewModel.reloadSearchCourses(searchText: newValue, nameOrCity: activeSearchAttribute)
} catch {
print(error)
}
}
}
}
}
.padding()
.background(Color(.systemGray5))
.cornerRadius(8)
.padding(.horizontal)
.onTapGesture{
isSearching = true
}
// overlay (could have used zstack) the icons into the search bar itself
.overlay() {
HStack{
Image(systemName: "magnifyingglass")
Spacer()
if isSearching{
Button(action: {
searchText = ""
isSearching = false
}, label: {
Image(systemName: "xmark.circle.fill")
.padding(.vertical)
})
}
}.padding(.horizontal, 32)
.foregroundColor(.gray)
}
//
if isSearching{
Button(action: {
isSearching = false
searchText = ""
}, label: {
Text("Cancel")
.padding(.trailing)
.padding(.leading, 0)
})
.transition(.move(edge: .trailing))
}
}
// these are the search attributes - allow the user to choose the search criteria
HStack{
// add buttons to change the search attribute (course, fullname, nickname)
Button(action: {
print ("set search attribute to fullname and here is searchtext: \(searchText)")
activeSearchAttribute = 0
searchText = ""
selectedCourseIdx = 0
}) {
Text("Name")
.padding(.all, 8)
.background((activeSearchAttribute == 0 ? .blue: .gray))
.foregroundColor(.white)
.padding(EdgeInsets(top: 0, leading: 20, bottom: 15, trailing: 0))
}
.cornerRadius(6)
.disabled(false)
Button(action: {
print ("set search attribute to city")
activeSearchAttribute = 1
searchText = ""
selectedCourseIdx = 0
}) {
Text("City")
.padding(.all, 8)
.background(activeSearchAttribute == 1 ? .blue: .gray)
.foregroundColor(.white)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 15, trailing: 0))
}
.cornerRadius(6)
.padding(.leading, 15)
.disabled(false)
Spacer()
}
Here is my dynamic search function...
func reloadSearchCourses(searchText: String, nameOrCity: Int ) async throws {
var searchField: String = String()
if nameOrCity == 0 {
searchField = "name"
}
if nameOrCity == 1 {
searchField = "city"
}
self.searchCourses.removeAll()
let snapshot = try! await Firestore.firestore()
.collection("courses")
.whereField(searchField,isGreaterThanOrEqualTo: searchText)
.whereField(searchField,isLessThanOrEqualTo: searchText + "z")
.order(by: searchField) // alpha order - ascending is default
.limit(to: 7)
.getDocuments()
snapshot.documents.forEach { docsnap in
let docdata = try! docsnap.data(as: Course.self)
self.searchCourses.append(docdata!)
}
}

SWIFTUI/ How to delay .task and let it work only if the data from textFields are passed

I just tried to make an API app in SwiftUI with loveCalculator from rapidapi.com
The problem is that API first needs names from me before it gives me the results.
My program works but fetching data form API earlier that I want (when I click to show my data in next view, first show default data, then show the data that should be displayed when I click).
Also Is it possible to initialize #Published var loveData (in LoveViewModel) without passing any default data or empty String?
Something like make the data from LoveData optional ?
Can You tell me when I make mistake?
MY CODE IS :
LoveData (for api)
struct LoveData: Codable {
let percentage: String
let result: String
}
LoveViewModel
class LoveViewModel: ObservableObject {
#Published var loveData = LoveData(percentage: "50", result: "aaa")
let baseURL = "https://love-calculator.p.rapidapi.com/getPercentage?"
let myApi = "c6c134a7f0msh980729b528fe273p1f337fjsnd17137cb2f24"
func loveCal (first: String, second: String) async {
let completedurl = "\(baseURL)&rapidapi-key=\(myApi)&sname=\(first)&fname=\(second)"
guard let url = URL(string: completedurl) else {return }
do {
let decoder = JSONDecoder()
let (data, _) = try await URLSession.shared.data(from: url)
if let safeData = try? decoder.decode(LoveData.self, from: data) {
print("succesfully saved data")
self.loveData = safeData
}
} catch {
print(error)
}
}
}
LoveCalculator View
struct LoveCalculator: View {
#State var firstName = ""
#State var secondName = ""
#ObservedObject var loveViewModel = LoveViewModel()
var body: some View {
NavigationView {
ZStack{
Color(.systemTeal).ignoresSafeArea()
VStack(alignment: .center) {
Text("Love Calculator")
.padding(.top, 100)
Text("Enter first name:")
.padding()
TextField("first name", text: $firstName)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
Text("Enter second name:")
.padding()
TextField("second name", text: $secondName, onEditingChanged: { isBegin in
if isBegin == false {
print("finish get names")
}
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
NavigationLink {
LoveResults(percentage: loveViewModel.loveData.percentage, description: loveViewModel.loveData.result)
} label: {
Text("CHECK IT!")
}
Spacer()
}
}
.task {
await loveViewModel.loveCal(first: firstName, second: secondName)
}
}
}
}
LoveResults View
struct LoveResults: View {
var percentage: String
var description: String
var body: some View {
ZStack{
Color(.green).ignoresSafeArea()
VStack{
Text("RESULTS :")
.padding()
Text(percentage)
.font(.system(size: 80, weight: .heavy))
.foregroundColor(.red)
.padding()
Text(description)
}
}
}
}
Thanks for help!
Regards,
Michal
.task is the same modifier as .onAppear in terms of view lifecycle, meaning it will fire immediately when the view appears. You have to move your API call to a more controlled place, where you can call it once you have all the required data.
If you want to fire the API request only after both names are entered, you can create a computed variable, that checks for desired state of TextFields and when that variable turns to true, then you call the API.
Like in this example:
struct SomeView: View {
#State var firstName: String = ""
#State var secondName: String = ""
var namesAreValid: Bool {
!firstName.isEmpty && !secondName.isEmpty // << validation logic here
}
var body: some View {
Form {
TextField("Enter first name", text: $firstName)
TextField("Enter second name", text: $secondName)
}
.onChange(of: namesAreValid) { isValid in
if isValid {
// Call API
}
}
}
}
You can also set your loveData to optional using #Published var loveData: LoveData? and disable/hide the navigation link, until your data is not nil. In that case, you might need to provide default values with ?? to handle optional string errors in LoveResults view initializer
It was very helpful but still not enough for me, .onChange work even if I was type 1 letter in my Form.
I find out how to use Task { } and .onCommit on my TextField, now everything working well !
My code now looks like that :
LoveCalculator View :
struct LoveCalculator: View {
#State var firstName = ""
#State var secondName = ""
var namesAreValid: Bool {
!firstName.isEmpty && !secondName.isEmpty
}
func makeApiCall() {
if namesAreValid {
DispatchQueue.main.async {
Task {
await self.loveViewModel.loveCal(first: firstName, second: secondName)
}
}
}
}
#ObservedObject var loveViewModel = LoveViewModel()
var body: some View {
NavigationView {
ZStack{
Color(.systemTeal).ignoresSafeArea()
VStack(alignment: .center) {
Text("Love Calculator")
.padding(.top, 100)
Text("Enter first name:")
.padding()
TextField("first name", text: $firstName, onCommit: {makeApiCall()})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
Text("Enter second name:")
.padding()
TextField("second name", text: $secondName, onCommit: {
makeApiCall()
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
NavigationLink {
LoveResults(percentage: loveViewModel.loveData.percentage ?? "", description: loveViewModel.loveData.result ?? "")
} label: {
Text("CHECK IT!")
}
Spacer()
}
}
}
}
}
Thank You one more time for very helpful tip!
Peace!
Michał ;)
Editing :
Just look at Apple documentation and I see that they say that .onCommit is deprecated whatever it means.
So instead of this I use .onSubmit and works the same !
TextField("second name", text: $secondName)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
.onSubmit {
makeApiCall()
}
Peace! :)

SwiftUI - How to add selection to TabView

I have a JSON API, and I want to show the content inside the PageTabView. It is successful, but the view becomes laggy every time I swipe the page and I got the solution here https://stackoverflow.com/a/66591530/15132219. But the problem is now the page dots doesn't want to change every time I swipe the page. I have tried to solve it for days but still can't find the solution, Here is my Code (I can't show the API link as it's not shareable)
struct HomeBanner: View{
#State var banners: [BannerJSON] = []
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
#State var index = 0
var body: some View{
let bannerPic = "API_LINK"
GeometryReader{ geometry in
VStack(spacing: 2){
if banners.isEmpty{
Spacer()
ProgressView()
Spacer()
}else{
VStack{
ZStack{
TabView(selection: $index){
ForEach(banners, id: \.id){banner in
VStack{
//MARK: Display Banner
NavigationLink(destination: DetailBanner(banner: banner
)){
WebImage(url: URL(string: bannerPic + banner.IMAGE))
.resizable()
.scaledToFit()
}
}
}
}.tabViewStyle(PageTabViewStyle())
.frame(width: geometry.size.width, height: geometry.size.height / 3.5)
}
}
}
}.onAppear{
getBannerData(url: "API_LINK"){
(banners) in
self.banners = banners
}
}
}
}
}
struct BannerJSON{
var RECORD_STATUS: String
var IMAGE: String
var URL_LINK: String
var TITLE: String
var DESCRIPTION: String
}
extension BannerJSON: Identifiable, Decodable{
var id: String {return TITLE}
}
struct SampleTabView: View {
#State var selectedIndex: Int = 0
#State var banners: [Int] = [0,1,2,3,4,5]
var body: some View {
TabView(selection: $selectedIndex){
ForEach(0..<banners.count, id: \.self){ idx in
Text(banners[idx].description)
.tag(idx)//This line is crutial for selection to work put in on the VStack
//You can also make the tag the banner in your loop but your selection variable must be a Banner too.
}
}
}
}