SwiftUI cannot update CoreData Entity Object - ios

I am facing a very weird bug, I have a edit function which will accept the object and new form data from the frontend, the function can be executed without errors but the issue is the data is not updated at all. I tried reopen the simulator and refetch also no use to this. May I ask how to solve this?
class RoomDataController: ObservableObject {
#Published var rooms: [Room] = []
let container = NSPersistentContainer(name: "RentalAppContainer")
init() {
container.loadPersistentStores { description, error in
let current = FileManager.default.currentDirectoryPath
print(current)
if let error = error {
print("Failed to load data in DataController \(error.localizedDescription)")
}
}
fetchRooms()
}
func save(context: NSManagedObjectContext) {
do {
try context.save()
print("Data created or updated successfully!")
} catch {
// Handle errors in our database
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
func editRoom(room: Room, roomType: String, roomDescription: String, contactPeriod: String, roomPrice: Float, userID: UUID, isShort: Bool, isLong: Bool, images: String, email: String, phoneNum:String, context: NSManagedObjectContext) {
room.roomType = roomType
room.roomPrice = roomPrice
room.roomDescription = roomDescription
room.contactPeriod = contactPeriod
room.contactEmail = email
room.contactNum = phoneNum
room.updated_at = Date()
print("room to be edited: ", room)
save(context: container.viewContext)
fetchRooms()
}
}
The Frontend View that call the edit function
struct EditRoomView: View {
#EnvironmentObject var userAuth: UserAuth
#State var type: String = ""
#State var description: String = ""
#State var price: String = ""
#State var contactPeriod: String = ""
#State var isShort: Bool = false
#State var isLong: Bool = false
#State var email: String = ""
#State var phoneNum: String = ""
#ObservedObject var room: Room
private func editItem() {
RoomDataController().editRoom(
room: room,
roomType: self.type,
roomDescription: self.description,
contactPeriod: self.contactPeriod,
roomPrice: Float(self.price)!,
userID: userAuth.user.id!,
isShort: self.isShort,
isLong: self.isLong,
images: "room-1,room-2,room-3",
email: email,
phoneNum: phoneNum
)
}
init(room: Room){
self.room = room
_type = State(initialValue: room.roomType!)
_description = State(initialValue: room.roomDescription!)
_price = State(initialValue: String( room.roomPrice))
_contactPeriod = State(initialValue: room.contactPeriod!)
_email = State(initialValue: room.contactEmail!)
_phoneNum = State(initialValue: room.contactNum!)
if room.contactPeriod == "Short"{
_isShort = State(initialValue:true)
_isLong = State(initialValue: false)
} else {
_isShort = State(initialValue: false)
_isLong = State(initialValue:true)
}
}
var body: some View {
VStack{
HStack {
Spacer()
VStack {
Menu {
Button {
// self.selectedTab = .AccountViewTab
} label: {
NavigationLink(destination: AccountView().environmentObject(userAuth), label: {Text("My Profile")})
Image(systemName: "arrow.down.right.circle")
}
Button {
userAuth.isLoggedin = false
} label: {
Text("Log Out")
Image(systemName: "arrow.up.and.down.circle")
}
} label: {
Text("Menu").foregroundColor(Color.black)
}
}
Spacer()
VStack {
NavigationLink(destination: OwnerNavbarView().environmentObject(userAuth), label: {Text("Room Rent").foregroundColor(Color.black)})
}
.onTapGesture {
}
Spacer()
VStack {
NavigationLink(destination: OwnerNavbarView(nextView: "PostRoomHistoryViewTab").environmentObject(userAuth), label: {Text("Post History").foregroundColor(Color.black)})
}
.onTapGesture {
}
Spacer()
VStack {
NavigationLink(destination: OwnerNavbarView(nextView: "CustomerOrderViewTab").environmentObject(userAuth), label: {Text("Customer Orders").foregroundColor(Color.black)})
}
.onTapGesture {
}
Spacer()
}
.padding(.bottom)
.background(Color.gray.edgesIgnoringSafeArea(.all))
Text("Edit Room Details")
Form {
Section(header: Text("Room Type") ) {
TextField("", text: self.$type)
}
Section(header: Text("Room Description") ) {
TextField("", text: self.$description)
}
MultipleImagePickerView()
Section(header: Text("Room Price") ) {
TextField("", text: self.$price)
}
Section(header: Text("Contact Period") ) {
HStack{
Toggle( "Short", isOn: $isShort )
Toggle("Long", isOn: $isLong )
}
}
Section(header: Text("Contact Email Address * :") ) {
TextField("", text: self.$email)
}
Section(header: Text("Contact Mobile No * :") ) {
TextField("", text: self.$phoneNum)
}
}
Button(action: {
self.editItem()
}) {
HStack {
Text("Update")
}
.padding()
.scaledToFit()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}
}
}
}
Ok..Managed to solve this in this way, buy why?
do {
fetchRequest.predicate = NSPredicate(format: "id == %#", room.id?.uuidString as! CVarArg)
let result = try container.viewContext.fetch(fetchRequest)
result[0].roomType = roomType
result[0].roomPrice = roomPrice
result[0].roomDescription = roomDescription
result[0].contactPeriod = contactPeriod
result[0].contactEmail = email
result[0].contactNum = phoneNum
result[0].updated_at = Date()
} catch {
print ("fetch task failed", error)
}
if room.ownerID == userID.uuidString {
save(context: container.viewContext)
fetchRooms()
}

Related

Issue with Binding passed to TextField, it is not allowing state change

Hi all thanks in advance.
I am having an issue when running app (not in preview), a textfield is not updating the state. I've not continued to expand the MVVM yet as I am getting caught up in this UI/Binding issue.
Not sure what have I missed here? I am passing a StateObject (view model instance) into the EnvironmentObject list, which is then accessed from an EnvironmentObject and the models array of elements in a view is iterated over, then further passing the iterated elements of the array to a Binding in another view which is then bound to a textfield to be edited by the user?
Specifically, the issue is:
When swipe action > edit on an expense in the ContentView to navigate to EditExpenseView, the textfields don't allow editing.
Note:
If I move the textfield up to the ExpenseList View, the binding to edit works. I thought that maybe the List(items) was the issue because it's iterating over an immutable collection.
I am using the index and passing the array binding via $expenses[index] which is avoiding accessing the immutable collection as its only being used to get the index of the list item the user will edit.
If your still reading, thanks for being awesome!
Let me know if I can add any further information or provide clarity.
Expense Model:
struct Expense: Equatable, Identifiable, Codable {
init(date: Date, description: String, amount: Decimal, type: ExpenseType, status: ExpenseStatus, budgetId: UUID?) {
self.date = date
self.description = description
self.amount = amount
self.type = type
self.status = status
self.budgetId = budgetId
}
static func == (lhs: Expense, rhs: Expense) -> Bool {
lhs.id == rhs.id
}
var id: UUID = UUID()
var date: Date
var description: String
var amount: Decimal
var type: ExpenseType
var status: ExpenseStatus
var budgetId: UUID?
}
ExpenseViewModel:
class ExpenseViewModel: ObservableObject, Identifiable {
#Published var expenses: [Expense] = []
func insertExpense(date: Date, description: String, amount: Decimal, type: ExpenseType, status: ExpenseStatus) -> Void {
expenses.insert(Expense(date: date, description: description, amount: amount, type: type, status: status, budgetId: nil), at:0)
}
func remove(_ expense: Expense) {
expenses.removeAll(where: {$0.id == expense.id})
}
}
App Entry:
import SwiftUI
#main
struct iBudgeteerApp: App {
#StateObject private var expenses = ExpenseViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(expenses)
}
}
}
Initial View:
struct ContentView: View {
#EnvironmentObject private var model: ExpenseViewModel
private static let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
NavigationStack {
VStack {
Button("Add Row") {
model.insertExpense(date: Date(), description: "Groceries", amount: 29.94, type: .Expense, status: .Cleared)
}
ExpenseList(expenses: $model.expenses)
}
}
}
}
Expense List View:
struct ExpenseList: View {
#Binding var expenses: [Expense]
var formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
List (expenses.sorted(by: {$0.date > $1.date}).indices, id: \.self) {
index in
HStack {
Text("\(index + 1).").padding(.trailing)
VStack(alignment: .leading) {
HStack {
Text(expenses[index].date.formatted(date:.numeric, time: .omitted))
Spacer()
Text(expenses[index].description)
}
HStack {
Text(expenses[index].description)
Spacer()
Text("\(expenses[index].amount as NSNumber, formatter: formatter)")
.foregroundColor( expenses[index].type == .Expense ? .red : .green)
Image(systemName: expenses[index].type == .Expense ? "arrow.down" : "arrow.up").foregroundColor( expenses[index].type == .Expense ? .red : .green)
}.padding(.top, 1)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: { expenses.remove(at: index) } ) {
Label("Delete", systemImage: "trash")
}
.tint(.gray)
}
.swipeActions() {
NavigationLink {
EditExpenseView(expense: self.$expenses[index])
} label: {
Label("Edit", systemImage: "slider.horizontal.3")
}
.tint(.yellow)
}
}
}
}
}
Edit Expense View:
struct EditExpenseView: View {
#Binding var expense: Expense
var formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
Form {
Section(header: Text("Editing: \(expense.description)")) {
VStack {
DatePicker(
"Date",
selection: $expense.date,
displayedComponents: [.date]
)
HStack {
Text("Name")
Spacer()
TextField("description",text: $expense.description)
.fixedSize().multilineTextAlignment(.trailing)
}
HStack {
Text("Amount")
Spacer()
TextField("0.00", value: $expense.amount, formatter: formatter).fixedSize()
}
Picker("Status", selection: $expense.status) {
ForEach(ExpenseStatus.allCases, id: \.self) {
status in
Text("\(status.rawValue)")
}
}
Picker("Type", selection: $expense.type) {
ForEach(ExpenseType.allCases, id: \.self) {
type in
Text("\(type.rawValue)")
}
}
}
}
}
}
}
UPDATE
It works in:
List ($expenses) { $expense in
NavigationLink(expense.description) {
EditExpenseView(expense: $expense)
}
}
ForEach($expenses) { $expense in
NavigationLink(expense.description) {
EditExpenseView(expense: $expense)
}
}
But not in:
List($expenses) {
$expense in
VStack(alignment: .leading) {
HStack {
Text(expense.date.formatted(date:.numeric, time: .omitted))
Spacer() }
HStack {
Text(expense.description)
Spacer()
Text("\(expense.amount as NSNumber, formatter: formatter)")
.foregroundColor( expense.type == .Expense ? .red : .green)
Image(systemName: expense.type == .Expense ? "arrow.down" : "arrow.up").foregroundColor(expense.type == .Expense ? .red : .green)
}.padding(.top, 1)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: { //expenses.remove(expense)
} ) {
Label("Delete", systemImage: "trash")
}
.tint(.gray)
}
.swipeActions() {
NavigationLink {
EditExpenseView(expense: $expense)
} label: {
Label("Edit", systemImage: "slider.horizontal.3")
}
.tint(.yellow)
}
}
Disclaimer:
I couldnĀ“t test this answer properly as your example is missing information and is not reproducible. Please consider posting a minimal reproducible example.
The issue is in these lines:
List (expenses.sorted(by: {$0.date > $1.date}).indices, id: \.self) {
and then doing:
EditExpenseView(expense: self.$expenses[index])
You are not passing a binding reference of Expense on to your EditExpenseView but a binding to a copy of it. You are breaking the binding chain.
The following aproach should yield the desired result:
List ($expenses) { $expense in
HStack {
Text("\(expenses.firstIndex(of: expense) + 1).").padding(.trailing)
VStack(alignment: .leading) {
HStack {
Text(expense.date.formatted(date:.numeric, time: .omitted))
Spacer()
Text(expense.description)
}
.....
and passing your Expense on to your subview:
EditExpenseView(expense: $expense)

#Published Bool never updates to true

I am working on a SwiftUI App. Below is my code for an observalbleObject view model
class ProductViewModel: ObservableObject {
#EnvironmentObject var viewModel: LoginViewModel
#Published var productList = ProductApiData(results: [])
#Published var success = false
func getProducts(_ params: String...) {
ProductApi().getProducts(sessionId: params[0], status: params[1], searchText: params[2], completion: { result, data in
if result == .success {
guard let data = data else { return }
let decodedResponse = try! JSONDecoder().decode(ProductApiData.self, from: data as! Data)
DispatchQueue.main.sync {
self.productList = decodedResponse
}
}
})
}
func updateProduct (_ params: String...) {
ProductApi().updateProduct(sessionId: params[0], parentProductAsin: params[1], productTitle: params[2], parentCategory: params[3], productCateogry: params[4], productTeam: params[5], productSubCategory: params[6], carouselProduct: params[7], featuredProduct: params[8], isHot: params[9], isNew: params[10], numReviews: params[11], rating: params[12], productPrice: params[13], productImagePath: params[14], completion: { result, data in
if result == .success {
guard let data = data else { return }
let decodedResponse = try! JSONDecoder().decode(ApiCallData.self, from: data as! Data)
DispatchQueue.main.sync {
if decodedResponse.success{
self.success = true
print("!")
}
}
}
})
}
}
Below is the code for my view
import Foundation
import SwiftUI
struct ProductInformationView: View {
#EnvironmentObject var quantityViewModel: QuantityViewModel
#EnvironmentObject var orderItemViewModel: OrderItemViewModel
#EnvironmentObject var viewModel: LoginViewModel
#EnvironmentObject var productViewModel: ProductViewModel
#EnvironmentObject var orderItemViewModlel: OrderItemViewModel
#EnvironmentObject var orderViewModel: OrderViewModel
#StateObject var localNotification = LocalNotification()
#State var product: Product
#State private var initalQuantity = "0"
#State private var datePickerSelection = Date()
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
var body: some View {
Form {
Group {
HStack {
Text("Parent Asin")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", text: $quantityViewModel.quantityList.results[0].parentProductAsin).multilineTextAlignment(.trailing)
}
}
HStack {
Text("Product Asin")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", text: $quantityViewModel.quantityList.results[0].productAsin).multilineTextAlignment(.trailing)
}
}
TextEditor(text: $product.productTitle)
Picker(selection: $product.parentCategory, label: Text("Parent Category")) {
ForEach(["Maryland", "Terps", "MLB", "NFL"], id: \.self) {
Text($0).tag($0)
}
}
Picker(selection: $product.productTeam, label: Text("Product Team")) {
ForEach(["Maryland", "Orioles", "Yankees", "Tampa Bay", "NA"], id: \.self) {
Text($0).tag($0)
}
}
Picker(selection: $product.productCategory, label: Text("Product Category")) {
ForEach(["Clothing", "HeadWear", "Footwear", "Socks", "Accessories", "Automobile","ShotGlass","Decals and Magnets"], id: \.self) {
Text($0).tag($0)
}
}
Picker(selection: $product.productSubCategory, label: Text("Product Sub Category")) {
ForEach(["Kids", "Mens", "Women", "Unisex", "Youth", "Toddler", "Infant", "License Plate","Decal"], id: \.self) {
Text($0).tag($0)
}
}
HStack {
Text("Number of Reviews")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", value: $product.numReviews, formatter: NumberFormatter()).multilineTextAlignment(.trailing)
}
}
HStack {
Text("Rating")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", value: $product.rating, formatter: NumberFormatter()).multilineTextAlignment(.trailing)
}
}
}
Group {
Toggle(isOn: $product.isHotProduct) {
Text("Is Hot")
}
Toggle(isOn: $product.featuredProduct) {
Text("Is Featured")
}
Toggle(isOn: $product.carouselProduct) {
Text("Is Carousel")
}
Toggle(isOn: $product.isNewProduct) {
Text("Is New")
}
HStack {
Text("Box Number")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", text: $quantityViewModel.quantityList.results[0].productBoxNumber).multilineTextAlignment(.trailing)
}
}
HStack {
Text("Product Image Path")
Spacer()
if (product.productHasVariations == true) {
} else {
TextField("", text: $product.productImagePath).multilineTextAlignment(.trailing)
}
/*if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", text: .bindOptional($quantityViewModel.quantityList.results[0].productImagePath, "")).multilineTextAlignment(.trailing)
}*/
}
HStack {
Text("Product Price")
Spacer()
if (product.productHasVariations == true) {
} else {
TextField("", value: $product.productPrice, formatter: formatter).multilineTextAlignment(.trailing)
}
/*if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", text: .bindOptional($quantityViewModel.quantityList.results[0].productPrice, "") ).multilineTextAlignment(.trailing)
}*/
}
HStack {
Text("Total Quantity")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", value: $quantityViewModel.quantityList.results[0].totalQuantity, formatter: NumberFormatter()).multilineTextAlignment(.trailing)
}
}
HStack {
Text("Sold Quantity")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", value: $quantityViewModel.quantityList.results[0].soldQuantity, formatter: NumberFormatter()).multilineTextAlignment(.trailing)
}
}
HStack {
Text("In-Stock Quantity")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", value: $quantityViewModel.quantityList.results[0].inStockQuantity, formatter: NumberFormatter()).multilineTextAlignment(.trailing)
}
}
}
Button("Update") {
updateProductInformation {
}
/*updateQuantityInformation()
updateOrderItemInformation()
updateOrderInformation()
localNotification.setLocalNotification(title: product.productTitle, subtitle: product.productAsin, body: "asd", when: 5)
*/ }
}.onAppear(perform: setQuantityInformation)
}
func setQuantityInformation() {
self.quantityViewModel.getQuantities(self.viewModel.user.token!, "PARENT", product.productAsin)
}
func updateProductInformation(finished: () ->Void ) {
print("#1")
self.productViewModel.updateProduct(self.viewModel.user.token!,
$product.productAsin.wrappedValue,
$product.productTitle.wrappedValue,
$product.parentCategory.wrappedValue,
$product.productCategory.wrappedValue,
$product.productTeam.wrappedValue,
$product.productSubCategory.wrappedValue,
String($product.carouselProduct.wrappedValue),
String($product.featuredProduct.wrappedValue),
String($product.isHotProduct.wrappedValue),
String($product.isNewProduct.wrappedValue),
String($product.numReviews.wrappedValue),
String($product.rating.wrappedValue),
String($product.productPrice.wrappedValue),
$product.productImagePath.wrappedValue)
print($productViewModel.success.wrappedValue)
if($productViewModel.success.wrappedValue) {
print("#2")
updateQuantityInformation()
print("#3")
}
print($productViewModel.success.wrappedValue)
}
func updateQuantityInformation() {
self.quantityViewModel.updateQuantities(self.viewModel.user.token!,
$quantityViewModel.quantityList.results[0].productAsin.wrappedValue,
String($quantityViewModel.quantityList.results[0].totalQuantity.wrappedValue),
String($quantityViewModel.quantityList.results[0].inStockQuantity.wrappedValue),
$quantityViewModel.quantityList.results[0].productBoxNumber.wrappedValue,
"", "", "", $product.productTitle.wrappedValue)
}
func updateOrderItemInformation() {
self.orderItemViewModel.updateOrderItem(self.viewModel.user.token!,
$quantityViewModel.quantityList.results[0].productAsin.wrappedValue,
$quantityViewModel.quantityList.results[0].productBoxNumber.wrappedValue,
$product.productImagePath.wrappedValue,
String($product.productPrice.wrappedValue))
}
func updateOrderInformation() {
self.orderViewModel.updateOrder(sessionId: self.viewModel.user.token!,
orderItems: $orderItemViewModel.orderItemsListIds.wrappedValue)
}
}
Clicking the update button on my view calls a function updateProductInformation which in return calls an API to update the information. As part of the above function I have
if($productViewModel.success.wrappedValue) {
print("#2")
updateQuantityInformation()
print("#3")
}
Everytime I run this the observedvariable is always false but i am sure I am updating it as part of the observable object. Any idea why is this happening

index out of bound from TabViewStyle of SwiftUI

I am trying to implement a tabView that takes a list of items into pages that I could swipe back and forth. However, it keeps bugging out with an "index out of bound" error. It's confusing to me because I never set index at all, and I don't know how to force an index either...
Below are my code. Apologize for any naive code, I am new to SwiftUI. Any helps are appreciated, thank you!
import SwiftUI
//#Published private var list = QuestionList.self
struct QuestionList: Codable {
var list:[QuestionItem]
}
class QuestionItem: Codable, Identifiable {
var id: Int
var text: String
var type: Int
var answer: String
}
struct ContentView: View {
#State private var qlist = [QuestionItem]()
#State private var isShowForm = false
#State private var q1 = true
#State private var answer = ""
#State private var isOn = [Bool]()
#State private var selectedTab = 0
func showForm() {
isShowForm = !isShowForm
let url = URL(string: "http://127.0.0.1:3000/question")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
if let error = error {
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print(response)
return
}
guard let data = data else {
return
}
do {
let list = try JSONDecoder().decode([QuestionItem].self, from:data)
qlist = list
print(qlist[1])
for i in 0..<qlist.count {
isOn.append(qlist[i].type == 0)
}
print(isOn)
// print(isOn)
print(type(of: qlist[1]))
} catch {
print("error: ", error)
}
}
task.resume()
}
var body: some View {
Text("Hello, world!")
.padding()
Button("Open Form") {
self.showForm()
}
if (isShowForm) {
TabView(selection: $selectedTab) {
ForEach(qlist.indices, id: \.self) { index in
if qlist[index].type == 0 {
HStack {
Text("\(self.qlist[index].text)")
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text("\(self.qlist[index].text)")
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qlist[index].answer) {
}
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It is not recommended to use indices in ForEach loops, instead use something like this updated code
that allows you to use your TextField :
ForEach($qlist) { $qItem in // <-- here $
if qItem.type == 0 {
HStack {
Text(qItem.text)
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text(qItem.text)
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qItem.answer) {
}
}
}
}
and as I mentioned in my comment, remove the print(qlist[1]) and print(type(of: qlist[1])) in showForm,
because if qlist is empty or only has one element, you will get the index out of bound error .
Remember one element is qlist[0].
EDIT-1: full test code:
this is the code I used in my test. It does not give any index errors.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var qlist = [QuestionItem]()
#State private var isShowForm = false
#State private var q1 = true
#State private var answer = ""
#State private var isOn = [Bool]()
#State private var selectedTab = 0
func showForm() {
let url = URL(string: "http://127.0.0.1:3000/question")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
if let error = error {
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
// print(response)
return
}
guard let data = data else {
return
}
do {
let list = try JSONDecoder().decode([QuestionItem].self, from:data)
qlist = list
// print(qlist[1])
for i in 0..<qlist.count {
isOn.append(qlist[i].type == 0)
}
print(isOn)
// print(isOn)
// print(type(of: qlist[1]))
isShowForm.toggle() // <--- here important
} catch {
print("error: ", error)
}
}
task.resume()
}
var body: some View {
Text("Hello, world!")
.padding()
Button("Open Form") {
// self.showForm()
// simulate showForm()
qlist = [
QuestionItem(id: 0, text: "text0", type: 0, answer: "0"),
QuestionItem(id: 1, text: "text1", type: 1, answer: "1"),
QuestionItem(id: 2, text: "text2", type: 2, answer: "2"),
QuestionItem(id: 3, text: "text3", type: 3, answer: "3")
]
isShowForm.toggle() // <--- here after qlist is set
}
if (isShowForm) {
TabView(selection: $selectedTab) {
ForEach($qlist) { $qItem in
if qItem.type == 0 {
HStack {
Text(qItem.text)
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text(qItem.text)
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qItem.answer)
.border(.red)
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
}
struct QuestionItem: Codable, Identifiable { // <-- here note the struct
var id: Int
var text: String
var type: Int
var answer: String
}

How to Stop a user from passing the login page if their info is not correct IOS

In my IOS app, I'd like to send a message to a user who tries to login with bad credentials and notify them of this with a pop up. Currently, My database can recognize a login error but my swift code doesn't see the error condition until after it dismisses the login page and enters the app.
The flask/python code that accesses the database looks like this:
#app.route('/login', methods=['GET', 'POST'])
def login():
mydb = mysql.connector.connect(host="localhost", user="root", passwd="Pass", database = "events")
if request.method == 'POST':
mycursor = mydb.cursor()
username = request.form['username']
password = request.form['password']
mycursor.execute('SELECT* FROM accounts WHERE username = %s AND password = %s', (username, password,))
account = mycursor.fetchone()
if account:
try:
mydb.commit()
mydb.close()
except e:
# Rollback in case there is any error
print("Error: ", e)
mydb.rollback()
return make_response("Success!", 200)
else:
return make_response("username/password combination dne", 500)
The swiftui code that contacts the data base inside my app looks like this:
struct LogInView: View {
#State var username: String = ""
#State var password: String = ""
#State var email: String = "test#gmail.com"
#Binding var didLogin: Bool
#Binding var needsAccount: Bool
#State var errorString: String = ""
func send(_ sender: Any, completion: #escaping (String) -> Void) {
let request = NSMutableURLRequest(url: NSURL(string: "http://localhost/login")! as URL)
request.httpMethod = "POST"
self.username = "\(self.username)"
self.password = "\(self.password)"
self.email = "\(self.email)"
let postString = "username=\(self.username)&password=\(self.password)&c=\(self.email)"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in
if error != nil {
print("error=\(String(describing: error))")
//put variable that triggers error try again view here
self.didLogin = false
self.errorString = String(describing: error)
completion(self.errorString)
return
}else{
self.didLogin = true
completion(String(describing: error))
}
print("response = \(String(describing: response))")
let responseString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("responseString = \(String(describing: responseString))")
if let httpResponse = response as? HTTPURLResponse {
self.errorString = String(httpResponse.statusCode)
}
}
task.resume()
}
var body: some View {
VStack{
Spacer()
WelcomeText()
UserImage()
TextField("Username", text: $username)
.padding()
.background(Color(.lightGray))
.cornerRadius(5.0)
.padding(.bottom, 20)
SecureField("Password", text: $password)
.padding()
.background(Color(.lightGray))
.cornerRadius(5.0)
.padding(.bottom, 20)
Button(action: {
self.send((Any).self){ array in
self.errorString = array
}/*
if self.errorString == "500"{
self.didLogin = false
}
else{
self.didLogin = true
}
}*/
},
label: {Text("LOGIN")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.orange)
.cornerRadius(15.0)})
.shadow(radius: 5)
.padding(.bottom, 10)
Button(action: {
self.needsAccount = true
}, label: {Text("Not a member yet? Sign up here")})
Spacer()
}.padding().background(Color.white).edgesIgnoringSafeArea(.all)
}
}
ContentView:
import SwiftUI
import Mapbox
import CoreLocation
struct ContentView: View {
#ObservedObject var annotationsVM: AnnotationsVM //= AnnotationsVM()
#ObservedObject var VModel: ViewModel //= ViewModel()
#ObservedObject var locationManager: LocationManager //= LocationManager()
#ObservedObject var data: DataFetcher
// #ObservedObject var mapViewCoordinator = MapViewCoordinator()
init() {
let vm = ViewModel()
VModel = vm
annotationsVM = AnnotationsVM(VModel: vm)
locationManager = LocationManager()
data = DataFetcher()
}
var userLatitude: CLLocationDegrees {
return (locationManager.lastLocation?.latitude ?? 0)
}
var userLongitude: CLLocationDegrees {
return (locationManager.lastLocation?.longitude ?? 0)
}
var lat: Double {
return (VModel.lat ?? 0)
}
var long: Double {
return (VModel.lon ?? 0)
}
var Userlat: Double {
return (VModel.userLatitude)
}
var Userlon: Double {
return (VModel.userLongitude)
}
//#State var searchedLocation: String = ""
#State private var annotationSelected: Bool = false
#State private var renderingMap: Bool = true
#State private var searchedText: String = ""
#State private var showResults: Bool = false
#State private var events: [eventdata] = []
#State private var showMoreDetails: Bool = false
#State private var didLogin: Bool = false
#State private var needsAccount: Bool = false
#State private var selectedAnnotation: MGLAnnotation? = nil
var body: some View {
VStack{
ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)){
MapView(annotationSelected: $annotationSelected, renderingMap: $renderingMap, visited: $annotationsVM.visited, showMoreDetails: $showMoreDetails, selectedAnnotation: $selectedAnnotation, VModel: VModel, locationManager: locationManager, aVM: annotationsVM, data: data, annos: $annotationsVM.annos)
.edgesIgnoringSafeArea(.all)
if showResults == true && searchedText.count >= 1 {
Text("").frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity).background(Color.white).edgesIgnoringSafeArea(.all)
//this is pretty ghetto but whatever
}
VStack{
HStack(alignment: .top){
if showResults == false {
SettingsButton()
}
Spacer()
SearchBar(annotation: annotationsVM, VModel: VModel, searchedText: $searchedText, showResults: $showResults, showMoreDetails: $showMoreDetails)
// SearchBar(annotation: annotationsVM) { sender in
// self.searchedLocation = sender.searchText.text
// }
Spacer()
if showResults == false {
MessageButton()
}
}.padding()
//Update Annotation Button
// Button (action: {
// let delayInSeconds = 1.5
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayInSeconds) {
// self.annotationsVM.addNextAnnotation(address: "22 Sunset Ave, East Quogue, NY")
//
// print("\(self.annotationsVM.annos)")
// print("User Coords: \(self.VModel.userLatitude), \(self.VModel.userLongitude)")
// }
// }, label: {Text("Press to update annotation")})
if showResults == true && searchedText.count >= 1 {
SearchResults(VModel: VModel, annotation: annotationsVM, showResults: $showResults, searchedText: $searchedText)
}
Spacer()
HStack(alignment: .bottom) {
if renderingMap {
Text("The Map is Rendering...")
}
}
//Side Note: If the Create Event Button is pressed, the currently selected annotation is unselected, so that'll need to be fixed
HStack(alignment: .bottom) {
if annotationSelected {
CreateEventButton(annotation: annotationsVM, annotationSelected: $annotationSelected)
}
}.padding()
}
VStack {
Spacer()
HStack {
Spacer()
if annotationsVM.annotationPlacementFailed == true {
AnnotationPlacementErrorView(annotationPlacementFailed: $annotationsVM.annotationPlacementFailed, annotation: annotationsVM, searchedText: $searchedText)
}
Spacer()
}
Spacer()
}
VStack {
Spacer()
HStack{
Spacer()
if self.showMoreDetails == true {
MoreDetailsView(searchedText: $searchedText, showMoreDetails: $showMoreDetails, selectedAnnotation: $selectedAnnotation)
//Instead of passing in searchedText, we need to pass in the mapView...idk how though
}
Spacer()
}
Spacer()
}
if self.didLogin == false {
LogInView(didLogin: $didLogin, needsAccount: $needsAccount)
}
if self.needsAccount == true {
SignUpView(didLogin: $didLogin, needsAccount: $needsAccount)
}
}
}
}
}
I'm not sure if this is a database//server issue or swiftui/httpresponse issue. Any insight is greatly appreciated
how does it dismiss the login page? assuming you use didLogin to check, you should default didLogin to false. so the user is not logged in until didLogin = true, meaning it will wait till you get a response from your http request. something like this:
#State private var didLogin: Bool = false
if didLogin {
ShowSomeViewAfterLogin()
} else {
ShowLogin()
}

SwiftUI/Combine no updates to #ObservedObject from #Published

I have a logon screen followed by an overview screen. When the user is successful at logon, the logon response sends back a list of items, which I want to display on the subsequent overview screen.
I can see the response being successfully mapped but the overview view is not receiving any update to the #ObservedObject. I could be missing something obvious but I've been through a bunch of articles and haven't managed to get anything working. Any help appreciated!
Logon view
import SwiftUI
struct LogonView: View {
#State private var username: String = ""
#State private var password: String = ""
#State private var inputError: Bool = false
#State private var errorMessage: String = ""
#State private var loading: Bool? = false
#State private var helpShown: Bool = false
#State private var successful: Bool = false
//MARK:- UIView
var body: some View {
NavigationView {
VStack {
VStack {
TextField("Username", text: $username)
.padding(.horizontal)
.disabled(loading! ? true : false)
Rectangle()
.frame(height: 2.0)
.padding(.horizontal)
.foregroundColor(!inputError ? Color("SharesaveLightGrey") : Color("SharesaveError"))
.animation(.easeInOut)
}.padding(.top, 80)
VStack {
SecureField("Password", text: $password)
.padding(.horizontal)
.disabled(loading! ? true : false)
Rectangle()
.frame(height: 2.0)
.padding(.horizontal)
.foregroundColor(!inputError ? Color("SharesaveLightGrey") : Color("SharesaveError"))
.animation(.easeInOut)
}.padding(.top, 40)
if (inputError) {
HStack {
Text(errorMessage)
.padding(.top)
.padding(.horizontal)
.foregroundColor(Color("SharesaveError"))
.animation(.easeInOut)
.lineLimit(nil)
.font(.footnote)
Spacer()
}
}
SharesaveButton(action: {self.submit(user: self.username, pass: self.password)},
label: "Log on",
loading: $loading,
style: .primary)
.padding(.top, 40)
.animation(.interactiveSpring())
NavigationLink(destination: OverviewView(), isActive: $successful) {
Text("")
}
Spacer()
}
.navigationBarTitle("Hello.")
.navigationBarItems(
trailing: Button(action: { self.helpShown = true }) {
Text("Need help?").foregroundColor(.gray)
})
.sheet(isPresented: $helpShown) {
SafariView( url: URL(string: "http://google.com")! )
}
}
}
//MARK:- Functions
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = ResultsViewModel()
resultsVM.getGrants(user: user, pass: pass,
successful: { response in
self.loading = false
if ((response) != nil) { self.successful = true }
},
error: { error in
self.inputError = true
self.loading = false
self.successful = false
switch error {
case 401:
self.errorMessage = "Your credentials were incorrect"
default:
self.errorMessage = "Something went wrong, please try again"
}
},
failure: { fail in
self.inputError = true
self.loading = false
self.successful = false
self.errorMessage = "Check your internet connection"
})
}
}
Results View Model
import Foundation
import Moya
import Combine
import SwiftUI
class ResultsViewModel: ObservableObject {
#Published var results: Results = Results()
func getGrants(
user: String,
pass: String,
successful successCallback: #escaping (Results?) -> Void,
error errorCallback: #escaping (Int) -> Void,
failure failureCallback: #escaping (MoyaError?) -> Void
)
{
let provider = MoyaProvider<sharesaveAPI>()
provider.request(.getSharesave(username: user, password: pass)) { response in
switch response.result {
case .success(let value):
do {
let data = try JSONDecoder().decode(Results.self, from: value.data)
self.results = data
successCallback(data)
} catch {
let errorCode = value.statusCode
errorCallback(errorCode)
}
case .failure(let error):
failureCallback(error)
}
}
}
}
Overview View
import SwiftUI
import Combine
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel = ResultsViewModel()
var body: some View {
let text = "\(self.viewModel.results.market?.sharePrice ?? 0.00)"
return List {
Text(text)
}
}
}
You submitted request to one instance of ResultsViewModel
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = ResultsViewModel() // << here
by try to read data from another instance of ResultsViewModel
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel = ResultsViewModel() // << here
but it must be the one instance, so modify as follows
1) In OverviewView
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel // expects injection
2) In LogonView
struct LogonView: View {
#ObservedObject var resultsViewModel = ResultsViewModel() // created once
and inject same instance for OverviewView
NavigationLink(destination: OverviewView(viewModel: self.resultsViewModel), isActive: $successful) {
Text("")
}
and in submit
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = self.resultsViewModel // use created
Please try after change in initialise OverviewView like below in NavigationLink
NavigationLink(destination: OverviewView(viewModel: self.resultsVM),
isActive: $successful) {
Text("")
}
OR
pass results in OverviewView as argument like below
NavigationLink(destination: OverviewView(results: self.resultsVM.results),
isActive: $successful) {
Text("")
}
.....
struct OverviewView: View {
let results: Results
var body: some View {
let text = "\(self.results.market?.sharePrice ?? 0.00)"
return List {
Text(text)
}
}
}

Resources