My TextField resets to "" on first pass through view but acts normal thereafter - google-cloud-firestore

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

Related

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

Change variable in other class and update view

First: Iam new to swift/swiftui
Problem: trying to update a published var:String in a observableOjbect class from a function. That var is used to for a switch later in a view.
I followed some inputs i found on my google research, but it doesnt work for me and i cant figure out why.
I tried #ObservedObjects, #EnvironmentObject, #StateObject, but i cant figure out how the string can be changed - in the view it always stays "x". getBotResponse(message: messageText) is called from another view / textField. My goal is to let the bot answer text, or with prepared buttons or images. depending on the input, the string should change to a "keyword" which is used as switch in the view to show other "view" modules i prepared.
class ChatBotVariables: ObesrvableObject{
#Published var answerCase: String = "x"
#Published var showFurtherContent: Bool = false
}
func getBotResponse(message: String) -> String {
let tempMessage = message.lowercased()
#ObservedObject var tempAnswerCase = ChatBotVariables()
if tempMessage.contains("textA"){
tempAnswerCase.answerCase = "text"
return "some text for BotAnswer"
}else if tempMessage.contains("textA"){
tempAnswerCase.answerCase = "video"
tempAnswerCase.showFurtherContent.toggle()
return "some other text for BotAnswer"
}else{
tempAnswerCase.answerCase = "questionbox"
tempAnswerCase.showFurtherContent.toggle()
return "something else"
}
}
struct ChatBot: View {
//happyBar
#State var _ratio = 60.0;
#State var _start : Bool = false;
#State private var messageText = ""
#State var messages: [String] = [""]
#State public var messageProperty = "Y"
#State private var botWriting = false
#State private var textInputEnabled = false
#ObservedObject var vm = MainMessagesViewModel() //reference to MMVM
#StateObject var cbl = ChatBotVariables()
#State var _case: String = "text"
//&#EnvironmentObject var cbl: ChatBotVariables
//#StateObject var showFurtherContent: Bool = false
init() {
initChatbot()
}
var body: some View {
NavigationView {
VStack {
ScrollView{
ForEach(messages, id: \.self){ message in
if message.contains("[USER]"){
let newMessage = message.replacingOccurrences(of:"[USER]", with: "")
HStack{
Spacer()
Text(newMessage)
.padding()
.foregroundColor(Color.white)
.background(.blue.opacity(0.8))
.cornerRadius(10)
.padding(.horizontal, 16)
.padding(.bottom, 10)
}
}else{
HStack{
Text(message)
.padding()
.background(.gray.opacity(0.15))
.cornerRadius(10)
.padding(.horizontal, 16)
.padding(.bottom, 10)
Spacer()
}
}
}.rotationEffect(.degrees(180))
if cbl.showFurtherContent {
switch cbl.$answerCase{
case "emoji":
print("asdf4")
break;
case "video":
print("asdf2")
break;
case "yesno":
print("asdf")
break;
default:
()
break;
}
cbl.showFurtherContent.toggle()
}
}.rotationEffect(.degrees(180))
.background(Color.gray.opacity(0.10))
if(messages.count > 1){
let mCount = messages.count
Text("count: \(mCount) answer: \(cbl.answerCase) Property: \(messageProperty)")
}
HStack {
if botWriting{
Image(systemName: "ellipsis.bubble.fill")
.font(.system(size: 26))
.foregroundColor(Color.blue)
}
}
Spacer()
// Show / Hide Text input bar
if textInputEnabled{
VStack{
HStack{
TextField("Text eingeben", text: $messageText)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.onSubmit{
sendMessage(message: messageText)
//switchHappyBar() // Debug
}
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0))
Button{
sendMessage(message: messageText)
//switchHappyBar() // Debug
}label:{
Image(systemName: "paperplane.fill")
}
.font(.system(size: 26))
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20))
}
HStack{
Button{
// Show Options For text
}label:{
Image(systemName: "questionmark.circle")
}
.font(.system(size: 26))
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0))
Spacer()
Button{
textInputEnabled.toggle()
}label:{
Image(systemName: "keyboard.chevron.compact.down")
}
.font(.system(size: 26))
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 50))
Spacer()
}
}
}else{
HStack{
Button{
textInputEnabled.toggle()
}label:{
Image(systemName: "keyboard")
}
.font(.system(size: 26))
}
}
}
.navigationBarHidden(true)
}
}
func initChatbot(){
messages.append("Hallo \(vm.chatUser?.email ?? ""), wie geht es dir heute INIT?")
}
func sendMessage(message: String){
withAnimation{
messages.append("[USER]" + message)
self.messageText = ""
botWriting.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1){
withAnimation{
messages.append(getBotResponse(message: message))
botWriting.toggle()
}
}
}
}

Turn Button into Picker

I have this code, where there are three buttons which have values stored in each of them.
When I press the Add button, the values add up and appear on the list. However, I am not able to check which values are being selected.
How can I make the button look like a picker, so that when the user clicks the button A, B, or C, it will show with a checkmark that it has been selected and when the user taps the Add button, the value of the selected button and "gp" should show up on the list? Also, the checkmark should disappear once the Add button is selected so that the user can select another list.
Such as:
If A and B are selected, the list should look like:
A = 2.0, B = 5.0, gp = 7.0.
If A and C are selected, the list should look like:
A= 2.0, C = 7.0, gp = 9.0.
I have tried using Picker and other methods, however, I couldn't get through. I have found this as the best solution. However, I am not able to put a checkmark on the buttons and not able to show the selected values on the list.
import SwiftUI
struct MainView: View {
#State var br = Double()
#State var loadpay = Double()
#State var gp : Double = 0
#State var count: Int = 1
#State var listcheck = Bool()
#StateObject var taskStore = TaskStore()
#State var a = 2.0
#State var b = 5.0
#State var c = 7.0
//var userCasual = UserDefaults.standard.value(forKey: "userCasual") as? String ?? ""
#State var name = String()
func addNewToDo() {
taskStore.tasks.append(Task(id: String(taskStore.tasks.count + 1), toDoItem: "load \(gp)", amount: Double(gp)))
self.gp = 0.0
}
func stepcount() {
count += 1
}
var body: some View {
HStack {
Button(action: { gp += a }) {
Text("A =").frame(width: 70, height: 15)
.foregroundColor(.yellow)
}
.background(RoundedRectangle(cornerRadius: 5))
.foregroundColor(.black)
Button(action: { gp += b }) {
Text("B =") .frame(width: 70, height: 15)
.foregroundColor(.yellow)
}
.background(RoundedRectangle(cornerRadius: 5))
.foregroundColor(.black)
Button(action: { gp += c }) {
Text("C =").frame(width: 70, height: 15)
.foregroundColor(.yellow)
}
.background(RoundedRectangle(cornerRadius: 5))
.foregroundColor(.black)
}
HStack(spacing: 15) {
Button(
String(format: ""),
action: {
print("pay for the shift is ")
gp += loadpay
}
)
Button(
action: {
addNewToDo()
stepcount()
},
label: { Text("Add")}
)
}
Form {
ForEach(self.taskStore.tasks) { task in
Text(task.toDoItem)
}
}
}
}
struct Task : Codable, Identifiable {
var id = ""
var toDoItem = ""
var amount = 0.0
}
class TaskStore : ObservableObject {
#Published var tasks = [Task]()
}
The issue is you are trying to turn a Button into something it is not. You can create your own view that responds to a tap and keeps its state so it knows whether it is currently selected or not. An example is this:
struct MultiPickerView: View {
#State private var selectA = false
#State private var selectB = false
#State private var selectC = false
let A = 2.0
let B = 5.0
let C = 7.0
var gp: Double {
(selectA ? A : 0) + (selectB ? B : 0) + (selectC ? C : 0)
}
var body: some View {
VStack {
HStack {
SelectionButton(title: "A", selection: $selectA)
SelectionButton(title: "B", selection: $selectB)
SelectionButton(title: "C", selection: $selectC)
}
.foregroundColor(.blue)
Text(gp.description)
.padding()
}
}
}
struct SelectionButton: View {
let title: String
#Binding var selection: Bool
var body: some View {
HStack {
Image(systemName: selection ? "checkmark.circle" : "circle")
Text(title)
}
.padding()
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(Color.blue, lineWidth: 4)
)
.padding()
.onTapGesture {
selection.toggle()
}
}
}
You could make it more adaptable by using an Array that has a struct that keeps the value and selected state, and run it though a ForEach, but this is the basics of the logic.

Update search results from api when value changes

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

How to make increase/Decrease the UpVote/DownVote like stackOverFlow?

I have this code which allows the users to make upvote or downvote like stack overflow. My problem is that the Text("\(likes) UpVote").padding(.top, 8)
does not refresh immediately, instead the user should change the perspective to get the refreshed upvote/downvote.
How to improve that, so that the user can only click one time on one of them and it Changes them immediately?
This is how my code looks like:
import SwiftUI
import SDWebImageSwiftUI
import Firebase
struct PostCellCard : View {
var user = ""
var image = ""
var id = ""
var likes = ""
var comments = ""
var msg = ""
var body : some View{
VStack(alignment: .leading, content: {
HStack{
Image("person-icon-1675").resizable().frame(width: 35, height: 35).clipShape(Circle()).onTapGesture {
print("slide out menu ....")
}
HStack(alignment: .top){
VStack(alignment: .leading){
Text(user).fontWeight(.heavy)
Text(msg).padding(.top, 8)
}}
Spacer()
Button(action: {
}) {
Image("menu").resizable().frame(width: 15, height: 15)
}.foregroundColor(Color("darkAndWhite"))
}
if self.image != ""{
AnimatedImage(url: URL(string: image)).resizable().frame(height: 250)
}
HStack{
Button(action: {
let db = Firestore.firestore()
let like = Int.init(self.likes)!
db.collection("posts").document(self.id).updateData(["likes": "\(like - 1)"]) { (err) in
if err != nil{
print((err)!)
return
}
print("down updated....")
}
}) {
Image(systemName: "arrow.down.to.line.alt").resizable().frame(width: 26, height: 26)
}.foregroundColor(Color("darkAndWhite"))
Button(action: {
let db = Firestore.firestore()
let like = Int.init(self.likes)!
db.collection("posts").document(self.id).updateData(["likes": "\(like + 1)"]) { (err) in
if err != nil{
print((err)!)
return
}
print("up updated....")
}
}) {
Image(systemName: "arrow.up.to.line.alt").resizable().frame(width: 26, height: 26)
}.foregroundColor(Color("darkAndWhite"))
Spacer()
}.padding(.top, 8)
Text("\(likes) UpVote").padding(.top, 8)
Text("\(likes) DownVote").padding(.top, 8)
Text("View all \(comments) Comments")
}).padding(8)
}
}
unfortunately i had to change a lot so that it was running ...so here is my code and it works - changes likes - of course you can make an int out of it and increase it instead of just setting a text
struct ContentView : View {
var user = ""
var image = ""
var id = ""
#State var likes = ""
var comments = ""
var msg = ""
var body : some View{
VStack {
HStack{
Image(systemName:"circle").resizable().frame(width: 35, height: 35).clipShape(Circle()).onTapGesture {
print("slide out menu ....")
self.likes = "tapped"
}
HStack(alignment: .top){
VStack(alignment: .leading){
Text(user).fontWeight(.heavy)
Text(msg).padding(.top, 8)
}}
Spacer()
Button(action: {
self.likes = "aha"
}) {
Image(systemName:"circle").resizable().frame(width: 15, height: 15)
}.foregroundColor(Color("darkAndWhite"))
}
if self.image != ""{
Text("Animated Image")
// AnimatedImage(url: URL(string: image)).resizable().frame(height: 250)
}
HStack{
Button(action: {
self.likes = "oho"
print("button action")
}) {
Image(systemName: "circle").resizable().frame(width: 26, height: 26)
}.foregroundColor(Color("darkAndWhite"))
Button(action: {
let like = Int.init(self.likes)!
print("action")
self.likes = "uhu"
}) {
Text("aha")
// Image(systemName: "arrow.up.to.line.alt").resizable().frame(width: 26, height: 26)
}.foregroundColor(Color("darkAndWhite"))
Spacer()
}.padding(.top, 8)
Text("\(likes) UpVote").padding(.top, 8)
Text("\(likes) DownVote").padding(.top, 8)
Text("View all \(comments) Comments")
}.padding(8)
}
}
You would use a property wrappers for your likes variable, so when you change the amount of likes, it will also change it on the Text as will.
What should work in your case is the #State property wrapper, which would look like this...
#State var likes = ""
For more information about property wrappers here's a good article explains them
https://www.hackingwithswift.com/quick-start/swiftui/understanding-property-wrappers-in-swift-and-swiftui