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.
Related
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 when a I need to load a json file to swiftUI when this json comes from an URL with entry variables that must be filled by the user before getting the data.
I have the following code:
// This is the structure to get the data from the API.
struct loka_result: Codable {
var date: String
var time: String
var unix_time: Int
var seqNumber : Int
var lat: Double
var lng: Double
var device: String
var accuracy: Double
var info: String
var temperature : Double?
var battery_voltage : Float?
}
// Now conform to Identifiable
extension loka_result: Identifiable {
public var id: Int { return unix_time }
}
// Model Data to get the data
final class ModelData: ObservableObject {
#Published var allthelokas : [loka_result] = loadAllData(deviceId:selected_loka, timeDelta: selected_delta)
}
// Function to get and parse the data
func loadAllData<T: Decodable>(deviceId:String,timeDelta:Int) -> T
{
let url = URL(string: "https://xxxx.php?device=\(deviceId)&hours=\(timeDelta)"),
data = try? Data(contentsOf: url!)
return
try! JSONDecoder().decode(T.self, from: data!)
}
Then in TempChartView I would like to get a list
import SwiftUI
struct TempChartNew: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
VStack {
List(modelData.allthelokas) { j in
// ForEach(modelData.allthelokas) { jj in
HStack {
Text(j.device)
Text(j.date)
Text(j.time)
Text(String(j.seqNumber))
}
}
}
}
}
struct TempChartNew_Previews: PreviewProvider {
static var previews: some View {
TempChartNew().environmentObject(ModelData())
}
}
all this its working because just to test it I have added two global variables to fill in the data for the function.
var selected_loka: String = "3JJJ30"
var selected_delta: Int = 24
But the ideia would be for the view to have two entry points.
And its here where I'm stuck ! I don't know how to pass the variables to the class.
Does anyone can please point me in the correct direction ? - Thank you
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
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.
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.