This code is not specifically mine but it will show the problem I am having more clearly. It is a part of an expense calculation application. I am having a hard time manipulating data after transferring it between views. I have 2 views and I would like to take all of the expenses and add them up. Part of the problem is that I make a new instance of a class every time I add a new "expense".
import SwiftUI
struct AddView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var expenses: Expenses
#State private var name = ""
#State private var type = "Personal"
#State private var amount = ""
static let types = ["Business", "Personal"]
var body: some View {
NavigationView {
Form {
TextField("Name", text: $name)
Picker("Type", selection: $type) {
ForEach(Self.types, id: \.self) {
Text($0)
}
}
TextField("Amount", text: $amount)
.keyboardType(.numberPad)
}
.navigationBarTitle("Add new expense")
.navigationBarItems(trailing: Button("Save") {
if let actualAmount = Int(self.amount) {
let item = ExpenseItem(name: self.name, type: self.type, amount: actualAmount)
self.expenses.items.append(item)
self.presentationMode
.wrappedValue.dismiss()
}
})
}
}
}
//SecondView
import SwiftUI
struct ExpenseItem: Identifiable, Codable {
let id = UUID()
let name: String
let type: String
let amount: Int
}
class Expenses: ObservableObject {
#Published var items = [ExpenseItem]() {
didSet {
let encoder = JSONEncoder()
if let encoded = try?
encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder()
if let decoded = try?
decoder.decode([ExpenseItem].self, from: items) {
self.items = decoded
return
}
}
}
}
struct ContentView: View {
#ObservedObject var expenses = Expenses()
#State private var showingAddExpense = false
var body: some View {
NavigationView {
List {
ForEach(expenses.items) { item in
HStack {
VStack {
Text(item.name)
.font(.headline)
Text(item.type)
}
Spacer()
Text("$\(item.amount)")
}
}
.onDelete(perform: removeItems)
}
.navigationBarTitle("iExpense")
.navigationBarItems(trailing: Button(action: {
self.showingAddExpense = true
}) {
Image(systemName: "plus")
}
)
.sheet(isPresented: $showingAddExpense) {
AddView(expenses: self.expenses)
}
}
}
func removeItems(at offsets: IndexSet) {
expenses.items.remove(atOffsets: offsets)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It really depends on where you want to show the total. The easiest solution is to display the total on the ContentView, after the list of all the expenses.
But first we need to calculate the total. As the total relates to the our Expenses ObservableObject it makes sense that it calculates it is own total.
The Expenses class only has one instance and it contains multiple instances of the ExpenseItem struct. We are going to solve this by adding a computed property to the Expenses class. I have removed some code from your examples so as to highlight the changes that I have made.
class Expenses: ObservableObject {
#Published var items: [ExpenseItem]
// ...
// Computed property that calculates the total amount
var total: Int {
self.items.reduce(0) { result, item -> Int in
result + item.amount
}
}
}
The computed property total reduces all the ExpenseItem amounts into a single value. You can read more about reduce here, and you can read about computed properties here. We don't have to use a reduce, we could use a for-loop and sum the values. We don't have to use a computed property either, we could use a function. It really depends on what you want.
Next in the ContentView we can add a view to the List that shows the total.
struct ContentView: View {
#ObservedObject var expenses = Expenses()
#State private var showingAddExpense = false
var body: some View {
NavigationView {
List {
ForEach(expenses.items) { item in
// ...
}
.onDelete(perform: removeItems)
// View that shows the total amount of the expenses
HStack {
Text("Total")
Spacer()
Text("\(expenses.total)")
}
}
// ...
}
// ...
}
This will give you a result that looks like this.
Related
I'm a beginner with SwiftUI and I wanted to create a dynamic list with the birthdays of people. I also wanted to integrate a filter that facilitates to find birthdays. But in the lines:
if enableFilter == true {
return json.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return json
}
I always get these errors:
Instance member 'enableFilter' cannot be used on type 'ContentView';
did you mean to use a value of this type instead?
and
Instance member 'filterDate' cannot be used on type 'ContentView'; did
you mean to use a value of this type instead?
I think I understand why the errors are present but I don't no how to fix it. I tried:
#State static var
but then I cannot change the values with my
filterView
Thank you for your help, here is the full source code:
import SwiftUI
struct person: Codable, Hashable, Identifiable {
var id: Int
var Birthday: Date
var BirthdayString: String
}
func filter(date: Date) -> String {
let DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: date)
let DateComponentsString: String = "\(DateComponents.day)/\(DateComponents.month)/\(DateComponents.year)"
return DateComponentsString
}
struct ContentView: View {
#State var people: [person] = {
guard let data = UserDefaults.standard.data(forKey: "people") else { return [] }
if let json = try? JSONDecoder().decode([person].self, from: data) {
if enableFilter == true {
return json.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return json
}
}
return []
}()
#State var filterDate: Date = Date()
#State var enableFilter: Bool = false
#State var showFilter: Bool = false
#State var newPersonDate: Date = Date()
var body: some View {
NavigationView {
VStack {
HStack {
DatePicker(selection: $newPersonDate, label: {Text("Birthday")}).padding()
Button(action: {didTapAddTask()}, label: {Text("Add")}).padding()
}
List {
ForEach($people) { $person in
Text("\(person.Birthday)")
}
}
}
.navigationTitle(Text("People's birthday"))
}
}
var filterView: some View {
VStack {
DatePicker(selection: $filterDate, label: {Text("Date")}).padding()
Toggle(isOn: $enableFilter, label: {Text("enable filter")}).padding()
}
}
func didTapAddTask() {
let id = people.reduce(0) { max($0, $1.id) } + 1
people.insert(person(id: id, Birthday: newPersonDate, BirthdayString: filter(date: newPersonDate)), at: 0)
newPersonDate = Date()
save()
}
func save() {
guard let data = try? JSONEncoder().encode(people) else { return }
UserDefaults.standard.set(data, forKey: "people")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can use another Computed property which will get the data based on some another property's value. i.e enableFilter.
#State var people: [person] = {
guard let data = UserDefaults.standard.data(forKey: "people") else { return [] }
if let json = try? JSONDecoder().decode([person].self, from: data) {
return json
}
return []
}()
var data : [person] {
if enableFilter {
return people.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return people
}
}
And use this property to get the relevant data:
var body: some View {
NavigationView {
VStack {
HStack {
DatePicker(selection: $newPersonDate, label: {Text("Birthday")}).padding()
Button(action: {didTapAddTask()}, label: {Text("Add")}).padding()
}
List {
ForEach(data) { person in // <<--- Here `data`
Text("\(person.Birthday)")
}
}
}
.navigationTitle(Text("People's birthday"))
}
}
I have not gotten to 50 reputation so i could not comment on this question SwiftUI Issue displaying specific number of Json Data Items to ask how it was done. The idea is to have the first 10 items in a json array show when the view is loaded and then a load more button to show more items.
This is how my code looks like.
Group {
HStack {
Text("Recommended Events")
.font(.title3)
.foregroundColor(.white)
.fontWeight(.bold)
Spacer()
Button(action: {
}) {
Text("Show all")
.font(.title3)
.foregroundColor(Color.white)
.fontWeight(.bold)
}
}
.padding(15)
ForEach(recommendeds) { recommended in
NavigationLink(destination: RecommendedEventsDetailView(recommended: recommended)) {
RecommendedEventsView(recommended: recommended)
}
}
}
Edited
After https://stackoverflow.com/users/14733292/raja-kishan response i tried it and got this error Failed to produce diagnostic for expression; please file a bug report This is the stage of my code now.
struct RecommendedModel: Identifiable {
var id = UUID()
var number: Int
init(_ number: Int) {
self.number = number
}
}
struct PlacesView: View {
private var arrData: [RecommendedModel] = (0...10).map({RecommendedModel($0)})
#State private var isMore: Bool = false
//E-MARK: - Body
var body: some View {
Group {
HStack {
Text("Recommended Events")
.font(.title3)
.foregroundColor(.white)
.fontWeight(.bold)
Spacer()
Button(action: {
withAnimation {
isMore.toggle()
}
}) {
Text("Show all")
.font(.title3)
.foregroundColor(Color.white)
.fontWeight(.bold)
}
}
.padding(15)
ForEach( (isMore ? arrData : recommendeds(arrData.prefix(5)))) { recommended in
NavigationLink(destination: RecommendedEventsDetailView(recommended: \(recommended.number)) {
RecommendedEventsView(recommended: \(recommended.number))
}
}
}
}
}
Below is the Data model i had before that loads the json data saved as RecommendedModel.swift
struct Recommended: Codable, Identifiable {
let id: String
let image: String
let date: String
let month: String
let like: String
let rating: String
let heading: String
let place: String
let article: String
let more: String
}
You can do this.
You can load the first 10 or 50 by .prefix() from the array.
Demo code
struct DataModel: Identifiable {
var id = UUID()
var number: Int
init(_ number: Int) {
self.number = number
}
}
struct LoadMoreDemo: View {
private var arrData: [DataModel] = (0...100).map({DataModel($0)})
#State private var isMore: Bool = false
var body: some View {
VStack {
ScrollView {
ForEach( (isMore ? arrData : Array(arrData.prefix(15)))) { item in
Text("\(item.number)")
}
}
Button("Load More") {
withAnimation {
isMore.toggle()
}
}
}
}
}
I'm stuck on this.
I have a json that I'm parsing and that Json has an entry value and I want to do a swift list based in user input that will trigger from the external json different responses.
I have the following code so far
import SwiftUI
import Combine
import Foundation
var lokas : String = "ABCY"
struct ContentView: View {
#State var name: String = ""
#ObservedObject var fetcher = Fetcher()
var body: some View {
VStack {
TextField("Enter Loka id", text: $name)
List(fetcher.allLokas) { vb in
VStack (alignment: .leading) {
Text(movie.device)
Text(String(vb.seqNumber))
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class Fetcher: ObservableObject {
#Published var allLokas = [AllLoka_Data]()
init(){
load(devices:lokas)
}
func load(devices:String) {
let url = URL(string: "https://xxx.php?device=\(devices)&hours=6")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([AllLoka_Data].self, from: d)
DispatchQueue.main.async {
self.allLokas = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
struct AllLoka_Data: Codable {
var date: String
var time: String
var unix_time: Int
var seqNumber : Int
}
// Now conform to Identifiable
extension AllLoka_Data: Identifiable {
var id: Int { return unix_time }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
But I just added a Global variable to test it, but how do I pass the variable name to make the function work ?
Thank you
I'm new to mobile development, and in the process of learning SwiftUI.
I've been struggling to figure out what's wrong with my picker. I am successfully returning the data from my URLSession, adding it to my model. I can confirm this by adding my #ObservedObject to a List, which returns all of the items. Putting the same #ObservedObject into a picker returns an empty picker for some reason. Any help would be appreciated. Thanks.
Here's my view with the Picker(). When run, the Picker is empty. I can comment out the Picker(), leaving just the ForEach() with Text(), and the text appears.
import Foundation
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject var countries = CulturesViewModel()
#State private var selectedCountries = 0
var body: some View {
VStack {
//loop through country array and add them to picker
Picker(selection: $selectedCountries, label: Text("Select Your Country")) {
ForEach(0 ..< countries.cultures.count, id: \.self) { post in
Text(self.countries.cultures[post].Culture).tag(post)
}
}.labelsHidden()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here's my ViewModel. It set's the #Published variable to the results of the JSON request in the WebService(). If I hard-code the #Published variable to the value that's begin returned, the Picker works.
import Foundation
import Combine
import SwiftUI
class CulturesViewModel: ObservableObject {
init() {
fetchCultures()
}
#Published var cultures = [Culture](){
didSet {
didChange.send(self)
}
}
private func fetchCultures(){
WebService().GetCultures {
self.cultures = $0
}
}
let didChange = PassthroughSubject<CulturesViewModel, Never>()
}
Here's my WebService(). Unfortunately, I'm unable to share the JSON url, I've added in the json that's returned.
import Foundation
import SwiftUI
import Combine
class WebService {
func GetCultures(completion: #escaping([Culture]) ->()) {
guard let url = URL("")
[
{
"CultureId": 0,
"Culture": "Select Your Country"
},
{
"CultureId": 1078,
"Culture": "English (United States)"
},
{
"CultureId": 6071,
"Culture": "English (Canada)"
}
]
URLSession.shared.dataTask(with: url) { (data,_,_) in
do {
if let data = data {
let culturesList = try JSONDecoder().decode([Culture].self, from: data)
DispatchQueue.main.async {
completion(culturesList)
}
} else {
DispatchQueue.main.async {
completion([])
}
}
} catch {
print(error)
DispatchQueue.main.async {
completion([])
}
}
}.resume()
}
}
Lastly, here's my Model.
import Foundation
struct Culture: Codable, Identifiable {
var id = UUID()
var CultureId: Int
var Culture: String
}
The work around to make the picker refresh is to add a unique id. Refreshing (or) reloading the countries, will create a new UUID for the picker items. This will force the picker to refresh. I've modified your code to include an id.
//loop through country array and add them to picker
Picker(selection: $selectedCountries, label: Text("Select Your Country")) {
ForEach(0 ..< countries.cultures.count, id: \.self) { post in
Text(self.countries.cultures[post].Culture).tag(post)
}
}.labelsHidden()
.id(UUID())
This seems to be a known issue with the picker.
So over the last couple hours, I've been trying to create a horizontal card stack that will show the most recent headlines for a project I am working on. Where I am at now, is holding up my entire flow, because my list cannot find my id variable in my results struct. Below I've attached the code, and any help would be appreciated.
HeadlinesUI.swift - Screenshot
import SwiftUI
struct Response: Codable {
var results: [Result]
}
struct Result: Codable {
var id: Int
var title: String
var header_image: String
var summary: String
}
struct HeadlineUI: View {
#State var results = [Result]()
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 20) {
List(results, id: \.id) { items in
}
.onAppear(perform: loadData)
}
}
}
func loadData() {
}
}
struct HeadlineUI_Previews: PreviewProvider {
static var previews: some View {
HeadlineUI()
}
}
As mentioned in the comments Result is a type in Swift, so you have to use a different name.
I went for:
struct MyResult: Codable {
var id: Int
var title: String
var header_image: String
var summary: String
}
since it has got an id you can make it Identifiable:
extension MyResult: Identifiable {}
as for the body:
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 20) {
ForEach(results) { result in
Text(result.title)
}
.onAppear(perform: { self.loadData() })
}
}
}
You rather need a ForEach then a List and the builder cannot be empty. Also onAppear takes a closure as its argument. Once you make all the changes the View will behave as expected.