SwiftUI: View Model does not update the View - mvvm

I try to implement a Search Bar with Algolia, and I use the MVVM pattern.
Here's my View Model:
class AlgoliaViewModel: ObservableObject {
#Published var idList = [String]()
func searchUser(text: String){
let client = SearchClient(appID: "XXX", apiKey: "XXX")
let index = client.index(withName: "Users")
let query = Query(text)
index.search(query: query) { result in
if case .success(let response) = result {
print("Response: \(response)")
do {
let hits: Array = response.hits
var idList = [String]()
for x in hits {
idList.append(x.objectID.rawValue)
}
DispatchQueue.main.async {
self.idList = idList
print(self.idList)
}
}
catch {
print("JSONSerialization error:", error)
}
}
}
}
}
Here is my View :
struct NewChatView : View {
#State private var searchText = ""
#ObservedObject var viewModel = AlgoliaViewModel()
var body : some View{
VStack(alignment: .leading){
Text("Select To Chat").font(.title).foregroundColor(Color.black.opacity(0.5))
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 12){
HStack {
TextField("Start typing",
text: $searchText,
onCommit: { self.viewModel.searchUser(text: self.searchText) })
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.viewModel.searchUser(text: self.searchText)
}) {
Image(systemName: "magnifyingglass")
}
} .padding()
List {
ForEach(viewModel.idList, id: \.self){ i in
Text(i)
}
}
}
}
}.padding()
}
}
I often use this pattern with Firebase and everything works fine, but here with Algolia the List remains empty in the NewChatView.
The print(self.idList) statement inside the View-Model shows the right idList, but it does not update the List inside the NewChatView.

You first need to create your own custom Identifiable and Hashable model to display the searchValue in a List or ForEach.
Something like this:
struct MySearchModel: Identifiable, Hashable {
let id = UUID().uuidString
let searchValue: String
}
Then use it in your AlgoliaViewModel. Set a default value of an empty array.
You can also map the hits received and convert it to your new model. No need for the extra for loop.
class AlgoliaViewModel: ObservableObject {
#Published var idList: [MySearchModel] = []
func searchUser(text: String) {
let client = SearchClient(appID: "XXX", apiKey: "XXX")
let index = client.index(withName: "Users")
let query = Query(text)
index.search(query: query) { result in
if case .success(let response) = result {
print("Response: \(response)")
do {
let hits: Array = response.hits
DispatchQueue.main.async {
self.idList = hits.map({ MySearchModel(searchValue: $0.objectID.rawValue) })
print(self.idList)
}
}
catch {
print("JSONSerialization error:", error)
}
}
}
}
}
For the NewChatView, you can remove the ScrollView as it conflicts with the elements inside your current VStack and would hide the List with the results as well. The following changes should display all your results.
struct NewChatView : View {
#State private var searchText = ""
#ObservedObject var viewModel = AlgoliaViewModel()
var body: some View{
VStack(alignment: .leading) {
Text("Select To Chat")
.font(.title)
.foregroundColor(Color.black.opacity(0.5))
VStack {
HStack {
TextField("Start typing",
text: $searchText,
onCommit: { self.viewModel.searchUser(text: self.searchText)
})
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.viewModel.searchUser(text: self.searchText)
}) {
Image(systemName: "magnifyingglass")
}
} .padding()
List {
ForEach(viewModel.idList) { i in
Text(i.searchValue)
.foregroundColor(Color.black)
}
}
}
}.padding()
}
}

Related

SwiftUI ToDoList with checkboxes?

I want to write a ToDoList in swiftUI with core data. Everything works so far but I want to have a checkbox next to each item it Signify whether it is completed or not.
I have added a property isChecked:boolean in core data but I don't know how to properly read it from the database. How to use a Toggle() in my case?
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: ToDoListItem.getAllToDoListItems())
var items: FetchedResults<ToDoListItem>
#State var text: String = ""
var body: some View {
NavigationView {
List {
Section (header: Text("NewItem")){
HStack {
TextField("Enter new Item.",text: $text)
Button(action: {
if !text.isEmpty{
let newItem = ToDoListItem(context: context)
newItem.name = text
newItem.createdAt = Date()
// current date as created
newItem.isChecked = false
do {
try context.save()
} catch {
print(error)
}
// to clear the textField from the previous entry
text = ""
}
}, label: {
Text("Save")
})
}// end of vstack
}
Section {
ForEach(items){ toDoListItem in
VStack(alignment: .leading){
// to have a checkbox
Button {
toDoListItem.isChecked.toggle()
} label: {
Label(toDoListItem.name!, systemImage: toDoListItem.isChecked ? "checkbox.square" : "square")
}
if let name = toDoListItem.name {
// Toggle(isOn: toDoListItem.isChecked)
Text(name)
.font(.headline)
}
//Text(toDoListItem.name!)
//.font(.headline)
if let createdAt = toDoListItem.createdAt {
//Text("\(toDoListItem.createdAt!)")
Text("\(createdAt)")
}
}
}.onDelete(perform: { indexSet in
guard let index = indexSet.first else {
return
}
let itemToDelete = items[index]
context.delete(itemToDelete)
do {
try context.save()
}
catch {
print(error)
}
})
}
}
.navigationTitle("To Do List")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ToDoListItem.swift
class ToDoListItem: NSManagedObject,Identifiable {
#NSManaged var name:String?
#NSManaged var createdAt:Date?
#NSManaged var isChecked:Bool
// mapped to the entry properties in database
}
extension ToDoListItem {
static func getAllToDoListItems() -> NSFetchRequest<ToDoListItem>{
let request:NSFetchRequest<ToDoListItem> = ToDoListItem.fetchRequest() as!
NSFetchRequest<ToDoListItem>
// cast as todolist item
let sort = NSSortDescriptor(key: "createdAt", ascending: true)
// above order of sorting
request.sortDescriptors = [sort]
return request
}
}
Should isChecked be an optional as well?

About swift5 and alamofire5,how to use AF.request to check if login sucessed or failed?

I want to use AF.request to implement login, this is my code:
import Alamofire
var loginResult: Bool = false
func login() -> Bool {
let parameters: [String: String] = [
"password": "Adgj!4567",
"username": "admin",
]
var a = AF.request("http://192.168.64.2/logins.php", method: .post, parameters:parameters,encoder: URLEncodedFormParameterEncoder(destination: .httpBody)).response{
response in
if let data = response.data {
let result = String(data: data, encoding: .utf8)!
if result.contains("Login Success!"){
print (result)
loginResult = true
}else {
loginResult = false
}
}
}
return loginResult
}
When I called login(), I got response like this:
send request worked, but loginResult has been always false, and I know that because AF is async, my problem is that I want to check if loginResult's value is true and go to another page, but it is always false, what should i do???
ContentView.swift like this:
import SwiftUI
struct ContentView: View {
#State private var isLoginValid: Bool = false
#State private var shouldShowLoginAlert: Bool = false
var body: some View {
NavigationView{
VStack{
HStack(alignment: .center) {
VStack {
Label("username", systemImage: "")
Label("password", systemImage: "")
}
VStack {
TextField("input username", text: /*#START_MENU_TOKEN#*//*#PLACEHOLDER=Value#*/.constant("")/*#END_MENU_TOKEN#*/)
TextField("input password", text: /*#START_MENU_TOKEN#*//*#PLACEHOLDER=Value#*/.constant("")/*#END_MENU_TOKEN#*/)
}
}
.padding()
NavigationLink(destination: infoInputView(),isActive: self.$isLoginValid) {
Text("Login")
.onTapGesture {
if login(){
self.isLoginValid = true
}
else{
self.shouldShowLoginAlert = true
}
}
}.buttonStyle(BorderlessButtonStyle())
.navigationBarTitle("Login Screen")
.alert(isPresented: $shouldShowLoginAlert) {
Alert(title: Text("Email/Password incorrect"))
}
.padding(.all, 50.0)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
My mother tongue is not english, I hope you can understand me.
i have got a wrong way,the right way is this:
import SwiftUI
import Alamofire
struct ContentView: View {
#State private var isLoginValid: Bool = false
#State private var shouldShowLoginAlert: Bool = false
#State private var loginResult: Bool = false
var body: some View {
NavigationView{
VStack{
HStack(alignment: .center) {
VStack {
Label("用户名", systemImage: "")
Label("密码", systemImage: "")
}
VStack {
TextField("请输入用户名", text: /*#START_MENU_TOKEN#*//*#PLACEHOLDER=Value#*/.constant("")/*#END_MENU_TOKEN#*/)
TextField("请输入密码", text: /*#START_MENU_TOKEN#*//*#PLACEHOLDER=Value#*/.constant("")/*#END_MENU_TOKEN#*/)
}
}
.padding()
NavigationLink(destination: infoInputView(),isActive: self.$isLoginValid) {
Text("Login")
.onTapGesture {
login()
}
}.buttonStyle(BorderlessButtonStyle())
.navigationBarTitle("Login Screen")
.alert(isPresented: $shouldShowLoginAlert) {
Alert(title: Text("Email/Password incorrect"))
}
.padding(.all, 50.0)
}
}
}
func login() {
let parameters: [String: String] = [
"password": "Adgj!4567",
"username": "admin",
]
var a = AF.request(url, method: .post, parameters:parameters,encoder: URLEncodedFormParameterEncoder(destination: .httpBody))
a.response{
response in
if let data = response.data {
let result = String(data: data, encoding: .utf8)!
if result.contains("Login Success!") {
isLoginValid = true
}else {
shouldShowLoginAlert = true
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}

Listing CoreData object through Relationship

I had this working without CoreData relationships (multiple fetches), but it occurred to me that I should probably have relationships between these entities implemented, so that I can just fetch from a single entity to get all attributes.
When I fetch accountNames from the Accounts entity directly for my AccountsList.swift (to create accounts) - it works just fine, but when I try to call them through the relationship (originAccounts), it doesn't show anything in the list. Same issue for the Categories picker.
I have 3 CoreData entities, and two Pickers (for category and account)
Expenses
expenseAccount:String
expenseCategory:String
expenseCost:Double
expenseDate:Date
expenseId:UUID
expenseIsMonthly:Bool
expenseName:String
Categories
categoryName:String
Accounts
accountName:String
Expenses has a many to one relationship with both Accounts and Categories
import SwiftUI
import CoreData
struct ExpenseDetail: View {
#Environment(\.managedObjectContext) var context
#Environment(\.presentationMode) var presentationMode
#FetchRequest(fetchRequest: Expenses.expensesList)
var results: FetchedResults<Expenses>
var logToEdit: Expenses?
#State var name: String = ""
#State var amount: String = ""
#State var category: String?
#State var date: Date = Date()
#State var account: String?
#State var isMonthly: Bool = false
var currencyFormatter: NumberFormatter = {
let f = NumberFormatter()
f.numberStyle = .currency
return f
}()
var body: some View {
NavigationView {
Form{
TextField("Expense Name", text: $name)
Section{
HStack{
TextField("$\(amount)", text: $amount)
.keyboardType(.decimalPad)
.textFieldStyle(PlainTextFieldStyle())
.disableAutocorrection(true).multilineTextAlignment(.leading)
}
DatePicker(selection: $date, displayedComponents: .date) {
Text("Date")
}.onAppear{self.hideKeyboard()}
Picker(selection: $category, label: Text("Category")) {
ForEach(results) { (log: Expenses) in
Text(log.originCategories?.categoryName ?? "No Category").tag(log.originCategories?.categoryName)
}
}
Picker(selection: $account, label: Text("Account")) {
ForEach(results) { (log: Expenses) in
Text(log.originAccounts?.accountName ?? "No Account").tag(log.originAccounts?.accountName)
}
}
Toggle(isOn: $isMonthly) {
Text("Monthly Expense")
}.toggleStyle(CheckboxToggleStyle())
}
Section{
Button(action: {
onSaveTapped()
}) {
HStack {
Spacer()
Text("Save")
Spacer()
}
}
}
Section{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Spacer()
Text("Cancel").foregroundColor(.red)
Spacer()
}
}
}
}.navigationBarTitle("Add Expense")
}
}
private func onSaveTapped() {
let expenseLog: Expenses
if let logToEdit = self.logToEdit {
expenseLog = logToEdit
} else {
expenseLog = Expenses(context: self.context)
expenseLog.expenseId = UUID()
}
expenseLog.expenseName = self.name
expenseLog.originCategories?.categoryName = self.category
expenseLog.expenseCost = Double(self.amount) ?? 0
expenseLog.expenseDate = self.date
expenseLog.originAccounts?.accountName = self.account
print("\(self.account ?? "NoAccountValue")")
expenseLog.expenseIsMonthly = self.isMonthly
do {
try context.save()
} catch let error as NSError {
print(error.localizedDescription)
}
self.presentationMode.wrappedValue.dismiss()
}
}
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
struct ExpenseDetail_Previews: PreviewProvider {
static var previews: some View {
ExpenseDetail()
}
}
struct CheckboxToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
return HStack {
configuration.label
Spacer()
Image(systemName: configuration.isOn ? "checkmark.square" : "square")
.resizable()
.frame(width: 22, height: 22)
.onTapGesture { configuration.isOn.toggle() }
}
}
}
expensesList fetch details, if needed
static var expensesList: NSFetchRequest<Expenses> {
let request: NSFetchRequest<Expenses> = Expenses.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "expenseName", ascending: true)]
return request
}

2 Pickers fetching from 2 Core Data Entities

Building my first app here, so I'm curious if I'm going about this the right way.
My app allows the user to create Accounts and Categories before creating an Expense object, which will allow the user to select an account and category to apply to the expense.
How can I fetch from both entities (Accounts and Categories) in a single view?
I tried including all Attributes under the Expenses entity, but it seems that when I save, it creates a new Expense in my ExpenseList view.
I have 3 CoreData entities, and two Pickers (for category and account)
Expenses
expenseAccount:String
expenseCategory:String
expenseCost:Double
expenseDate:Date
expenseId:UUID
expenseIsMonthly:Bool
expenseName:String
Categories
categoryName:String
Accounts
accountName:String
import SwiftUI
import CoreData
struct ExpenseDetail: View {
#FetchRequest(
entity: Categories.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Categories.categoryName, ascending: true)
]
)
private var result: FetchedResults<Categories>
var logToEdit: Expenses?
#Environment(\.managedObjectContext) var context
#State var name: String = ""
#State var amount: String = ""
#State var category: String = ""
#State var date: Date = Date()
#State var account: String = ""
#State var isMonthly: Bool = false
var currencyFormatter: NumberFormatter = {
let f = NumberFormatter()
f.numberStyle = .currency
return f
}()
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form{
TextField("Expense Name", text: $name)
Section{
HStack{
TextField("$\(amount)", text: $amount)
.keyboardType(.decimalPad)
.textFieldStyle(PlainTextFieldStyle())
.disableAutocorrection(true).multilineTextAlignment(.leading)
}
DatePicker(selection: $date, displayedComponents: .date) {
Text("Date")
}.onAppear{self.hideKeyboard()}
Picker(selection: $category, label: Text("Category")) {
ForEach(result) { (log: Categories) in
Text(log.categoryName ?? "No Category").tag(log.categoryName)
}
}
Picker(selection: $account, label: Text("Account")) {
ForEach(result) { (log: Categories) in
self.Print("\(log.categoryName ?? "")")
Button(action: {
// TODO: Implement Edit
}) {
Text(log.categoryName!.capitalized).tag(self.category)
}
}
}
Toggle(isOn: $isMonthly) {
Text("Monthly Expense")
}.toggleStyle(CheckboxToggleStyle())
}
Section{
Button(action: {
onSaveTapped()
}) {
HStack {
Spacer()
Text("Save")
Spacer()
}
}
}
Section{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Spacer()
Text("Cancel").foregroundColor(.red)
Spacer()
}
}
}
}.navigationBarTitle("Add Expense")
}
}
private func onSaveTapped() {
let expenseLog: Expenses
if let logToEdit = self.logToEdit {
expenseLog = logToEdit
} else {
expenseLog = Expenses(context: self.context)
expenseLog.expenseId = UUID()
}
expenseLog.expenseName = self.name
expenseLog.expenseCategory = self.category
print("\(expenseLog.expenseName ?? "") category Picker: \(self.category)")
print("\(expenseLog.expenseName ?? "") ExpenseCategory: \(expenseLog.expenseCategory!)")
expenseLog.expenseCost = Double(self.amount) ?? 0
print("\(expenseLog.expenseName ?? "") Amount: \(self.amount)")
print("\(expenseLog.expenseName ?? "")ExpenseCost: \(expenseLog.expenseCost)")
expenseLog.expenseDate = self.date
expenseLog.expenseAccount = self.account
expenseLog.expenseIsMonthly = self.isMonthly
do {
try context.save()
} catch let error as NSError {
print(error.localizedDescription)
}
self.presentationMode.wrappedValue.dismiss()
}
}
ExpenseList code, if needed:
NavigationView{
if #available(iOS 14.0, *) {
List{
ForEach(result) { (log: Expenses) in
self.Print("\(log.expenseName ?? "") expenseCost: \(log.expenseCost)")
self.Print("\(log.expenseName ?? "") amountText: \(log.amountText)")
Button(action: {
// TODO: Implement Edit
}) {
HStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text(log.nameText).font(.headline)
Text(log.dateText).font(.caption)
}
Spacer()
VStack(alignment: .trailing, spacing: 8){
Text(log.amountText).font(.headline)
Text(log.expenseAccount?.capitalized ?? "").font(.caption)
Text(log.expenseCategory ?? "No Category")
}
}
.padding(.vertical, 2)
}
}.onDelete(perform: onDelete)
}.navigationTitle("Expense List")
.navigationBarItems(trailing:
Button(action: {
// toggles the value of our bool from false to true,
// which will present our sheet.
self.addExpense.toggle()
}, label: {
Image(systemName: "plus.circle.fill")
})
.sheet(isPresented: $addExpense) {
ExpenseDetail()
}
)
} else {
// Fallback on earlier versions
}
}
}
Code used to save Categories:
private func onSaveTapped() {
let log: Categories
if let logToEdit = self.logToEdit {
log = logToEdit
} else {
log = Categories(context: self.context)
}
log.categoryName = self.category
do {
try context.save()
} catch let error as NSError {
print(error.localizedDescription)
}
}
}

How to initialize data in Preview provider in SwiftUI

I am trying to fetch data from localhost, make a list of posts with List View and pass data to CustomDetailView. Here is my code for NetworkManager:
My ListView:
And StoryDetails View:
So what I have to pass to StoryDeatils_Preview?
Here is the StoryDetails code
import SwiftUI
struct StoryDetails: View {
var story: Story
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("story #123456")
.font(.callout)
.foregroundColor(Color.gray)
Spacer()
Text("5 days ago")
.font(.callout)
.foregroundColor(Color.gray)
Button(action:{
print("Hello there")
}){
Image(systemName:"info.circle").resizable()
.frame(width:22.0, height:22.0)
.accentColor(Color.gray)
}
}
Text(story.body)
.foregroundColor(.black)
.kerning(1)
.lineLimit(nil)
HStack {
Button(action: {
print("Hello World")
}){
HStack {
Image(systemName:"heart")
.accentColor(.black)
Text("233")
.foregroundColor(.black)
}
.padding(.trailing)
HStack {
Image(systemName:"bubble.left")
.accentColor(.black)
Text("45")
.foregroundColor(.black)
}
}
}
}
}
}
struct StoryDetails_Previews: PreviewProvider {
static var previews: some View {
StoryDetails(
story: Story(
id: 1,
author: 1,
body: "Testing",
edited_time: "September 2019",
pub_date: "October 2018",
comments: [Comment](),
story_likes: [StoryLike]()
)
)
}
}
Error:
Hi there first I need to see the StoryDetails() but if StoryDetails a Story it should be declared inside as var story: Story let me explain more in example code:
Here you can see my network manager class:
class NetworkManager: ObservableObject {
let url: String = "https://jsonplaceholder.typicode.com/todos/1"
var objectWillChange = PassthroughSubject<NetworkManager, Never>()
init() {
fetchData()
}
#Published var user: User? {
didSet {
objectWillChange.send(self)
print("set user")
}
}
func fetchData() {
guard let url = URL(string: url) else {return}
print("fetch data")
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {return}
print("no error")
guard let data = data else {return}
print("data is valid")
let user = try! JSONDecoder().decode(User.self, from: data)
DispatchQueue.main.async {
self.user = user
}
}.resume()
}
}
that's my content view where network manager is initialized inside:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
VStack {
DetailsView(user: networkManager.user)
}
}
}
Details view struct holds user variable:
struct DetailsView: View {
var user: User?
var body: some View {
VStack {
Text("id: \(user?.id ?? 0)")
Text("UserID: \(user?.userId ?? 0 )")
Text("title: \(user?.title ?? "Empty")")
}
}
}
and that's the DetailsView as you can see inside of this struct I declared a user object of type User need to be pass it so if I want to show it in PreviewProvider it would be like the code below
struct DetailsView_Previews: PreviewProvider {
static var previews: some View {
DetailsView(user: User(id: 0, userId: 0, title: "hello", completed: false)
}
}
model:
struct User: Decodable {
var userId: Int = 0
var id: Int = 0
var title: String = ""
var completed: Bool = false
}
PS: For sure you can unwrap better than this way to provide
any nil exception it's just POC