I am struggling to extract single values (strings and Int) from the data I fetched from an API. I fetch the data in the form of a list:
class apiCall {
func getRockets(completion:#escaping ([Rockets]) -> ()) {
guard let url = URL(string: "https://api.spacexdata.com/v4/rockets") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let rockets = try! JSONDecoder().decode([Rockets].self, from: data!)
print(rockets)
DispatchQueue.main.async {
completion(rockets)
}
}
.resume()
}
}
Then when I try using it in a View I succeed when using a List to view the values, like it shows the list of the names from the API data. But then when I want to view a single value like this:
import SwiftUI
struct RocketStatistics: View {
#State var rockets: [Rockets] = []
var body: some View {
VStack{
Text(rockets[1].name)
}
.onAppear {
apiCall().getRockets { (rockets) in
self.rockets = rockets
}
}
}
}
struct RocketStatistics_Previews: PreviewProvider {
static var previews: some View {
RocketStatistics()
}
}
It does not even give me an error, but my preview just won't update and keeps crashing.
So my question is how can I extract single values from this API data in List form and use these single values in my whole project?
To keep it simple and make it work first I started just with fetching the "name" from the API:
import Foundation
import SwiftUI
struct Rockets: Codable, Identifiable {
let id = UUID()
let name : String
}
When it all works I would also want to use Integer values from the API in my project, so tips on how to that are also welcome!
Never ever get items by a hard-coded index in a SwiftUI view without any check. When the view is rendered the first time the array is empty and any index subscription will crash.
Always check if the array contains the required number of items. In this case the array must contain at least two items
VStack{
Text(rockets.count > 1 ? rockets[1].name : "")
}
Related
I'm creating a document-based application where data is represented by TextFields in a TableView (it could also be a List, the same issue occurs). When the app SwiftUI app on an Intel MacBook Air, I get a lot of keyboard lag whenever there are more than a dozen rows in my table. It's present on the Apple Studio too, but less noticeable. I've tried changing the table into a List and LazyVStack, but it doesn't seem to make much difference. Using the Swift UI instrument, it looks to me like every TextField on the page is being redrawn on every keystroke, even though their values haven't changed.
I also tried using a custom TextField with a debounce added in (with this as a starting point). This works well for reducing the lag, but I don't think this is how debouncing is intended to be used and I ended up with some strange behaviour.
I suspect that it is rather the case that I've misunderstood how to using #Binding variables in a Document Based application, or possibly I have misconfigured the Struct where I store the data. So here are the essential parts of my code, which will hopefully make it clear where I have gone wrong without having to run anything.
struct ClaraApp: App {
#StateObject var globalViewModel = GlobalViewModel()
var body: some Scene {
DocumentGroup(newDocument: ClaraDocument(claraDoc:GroupVocab())) { file in
MainContentView(data: file.$document)
.environmentObject(self.globalViewModel)
}
}
struct MainContentView: View {
#Binding var data: ClaraDocument // Binding to the document
#EnvironmentObject var globalViewModel : GlobalViewModel
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
HostingWindowFinder { window in
if let window = window {
self.globalViewModel.addWindow(window: window)
print("New Window", window.windowNumber)
self.globalViewModel.addViewModel(self.viewModel, forWindowNumber: window.windowNumber)
window.becomeKey()
}
}
.frame(width:0, height: 0)
VStack{
TabView(selection: $viewModel.activeTab){
VocabView(vocabs: $data.claraDoc.vocabs, selectedVocab: $viewModel.selectedVocab)
.tabItem{
Label("Vocabulary", systemImage: "tablecells")
}
.tag(TabType.vocab)
//more tabs here
}
}
}
}
struct VocabView: View{
#Binding var vocabs: [Vocab] // Binding to just the part of the document concerned by this view
#Binding var selectedVocab: Vocab.ID?
var body: some View{
VStack(){
VocabTable(vocabs: $vocabs, selection: $selectedVocab)
.padding([.top, .leading, .trailing])
HStack{
Button("-"){
if selectedVocab != nil{
let oldSelectionIndex = vocabs.firstIndex(where: {$0.id == selectedVocab!})
if oldSelectionIndex != nil{
if oldSelectionIndex! > 0{
selectedVocab = vocabs[oldSelectionIndex! - 1].id
} else {
selectedVocab = nil
}
vocabs.remove(at: oldSelectionIndex!)
}
}
}
.disabled(selectedVocab == nil)
Text("\(String(vocabs.count)) entries")
Button("+"){
let newVocab = Vocab(id: UUID(), word: "", def: "", trans: "", visNote: "", hidNote: "", link: Link(linked: false), date: Date())
vocabs.append(newVocab)
selectedVocab = newVocab.id
}
}
}
}
}
struct VocabTable: View{
#Binding var vocabs: [Vocab]
#Binding var selection: Vocab.ID?
var body: some View{
Table($vocabs, selection: $selection){
TableColumn("Word") {vocab in
TextField("Word", text: vocab.word)
}
TableColumn("Definition") {vocab in
TextField("Definition", text: vocab.def)
}
TableColumn("Translation") {vocab in
TextField("Translation", text: vocab.trans)
}
TableColumn("Visible note") {vocab in
TextField("Visible note", text: vocab.visNote)
}
TableColumn("Hidden note") {vocab in
TextField("Hidden note", text: vocab.hidNote)
}
TableColumn("Created") {vocab in
HStack{
Text(vocab.date.wrappedValue, style: .date)
Text(vocab.date.wrappedValue, style: .time)
}
}
}
.onDeleteCommand{
if selection != nil{
let oldSelectionIndex = vocabs.firstIndex(where: {$0.id == selection!})
if oldSelectionIndex != nil{
if oldSelectionIndex! > 0{
selection = vocabs[oldSelectionIndex! - 1].id
} else {
selection = nil
}
vocabs.remove(at: oldSelectionIndex!)
}
}
}
}
}
// vocab struct which is contained as an array [Vocab] inside the GroupVocab struct
struct Vocab: Identifiable, Codable, Equatable, Hashable {
let id: UUID
var word: String
var def: String
var trans: String
var visNote: String
var hidNote: String
var date: Date
init(id: UUID = UUID(), word: String? = "", def: String? = "", trans: String? = "", visNote: String? = "", hidNote: String? = "", date: Date? = Date()){
self.id = id
self.word = word ?? ""
self.def = def ?? ""
self.trans = trans ?? ""
self.visNote = visNote ?? ""
self.hidNote = hidNote ?? ""
self.date = date ?? Date()
}
static func == (lhs: Vocab, rhs: Vocab) -> Bool {
return lhs.id == rhs.id && lhs.word == rhs.word && lhs.def == rhs.def && lhs.trans == rhs.trans && lhs.visNote == rhs.visNote && lhs.date == rhs.date
}
}
struct GroupVocab: Codable{
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var groupName: String = ""
var vocabs = [Vocab]()
var learners = [Learner]()
var lessons = [Lesson]()
var startDate = Date()
var endDate = Date()
}
If that doesn't shed any light, here's my attempt at making a minimal example. It isn't nearly as laggy as the actual app, but from what I can tell it exhibits the same problems. Of course, it could be something in my actual app, which is not present in this minimal example, that I have overlooked. For example, I know that the menu bar is redrawn when editing the document, but removing the menu bar doesn't improve performance. So, I'm assuming that the reduced (albeit still present) lag is due to the general baggage of the program and not one specific element that I haven't taken into account. Obviously if there are no obvious problems in the above, I will need to go back and check everything again, but to my knowledge I have already tried removing and readding each part of the application individually to no avail.
Finally, this is what the actual app looks like in use:
As mentioned in the comments on original post.
TL;DR; For those encountering a similar lag issue, the solution here was to replace the declaration of Vocab as a struct with the use of an ObservableObject class, i.e. Vocab's definition becomes class Vocab: ObservableObject, Identifiable, Codable, Equatable.
Might also want to have a look at https://www.hackingwithswift.com/books/ios-swiftui/adding-codable-conformance-for-published-properties if #Published properties in the class have to be Codable
In a bit more detail
When struct Vocab is used each keystroke (because it is a value type) creates a new (original data + change) instance of the Vocab struct.
The problem [0] with this struct new instance is that SwiftUI cannot detect the singular property change and trigger just updating its corresponding TextField [1].
Instead SwiftUI handles each new keystroke driven struct instance as if it is a completely unrelated Vocab object; for which it has to update every TextField in the Table's entry row.
It's the updating of all of the TextFields in the entry row that causes the perceived lag.
By contrast the solution - using an ObservableObject class - enables binding the TextFields to a property on an object where the instance does not change on each keystroke. Consequently SwiftUI is able to detect and update just the individual entry changes.
The final piece in the puzzle is that when using an ObservableObject. The #Published properties that update Views nicely take some extra effort to enable them to conform with the Codeable protocol. For how to add that conformance there is a nice explanation over [here[( https://www.hackingwithswift.com/books/ios-swiftui/adding-codable-conformance-for-published-properties)
Other bits
If running on higher spec machines - or with fewer properties - issues like these can be difficult to spot.
Other approaches might be possible. For instance, if it's practicable within the context of the rest of the app, relaxing Vocab's Equatable compliance [1] might be enough to enable SwiftUI to do something more clever when determining what TextFields need recomputing.
[0] In this context; generally though, preferring value types such as structs is seen as good practice because it reduces the risk of unexpected side-effects.
[1] Possibly this might also be addressable by relaxing the implementation of Equatable conformance on the struct to just being based on id equivalence.
I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.
I have this model where I have a list of boat, and a list of works (which are linked to a boat). All are linked to the user. My data is stored in Firestore by my repository.
struct boat: Codable, Identifiable {
#DocumentID var id : String?
var name: String
var description: String
#ServerTimestamp var createdtime: Timestamp?
var userId: String?
}
struct Work: Identifiable, Codable {
#DocumentID var id : String?
var title: String
var descriptionpb: String
var urgent: Bool
#ServerTimestamp var createdtime: Timestamp?
var userId: String?
var boatId: String // (boatId = id of struct boat just above)
}
I have a view in which I want to display (and let the user edit) the details of the work (such as the title and descriptionpb), I manage to display the boatId (see below), however I want to display the boat name. How should I go about it?
import SwiftUI
struct WorkDetails: View {
#ObservedObject var wcvm: WorkCellVM
#ObservedObject var wlvm = WorklistVM()
#State var presentaddwork = false
var onCommit: (work) -> (Void) = { _ in }
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text(wcvm.work.boatId) // <-- THIS IS WHAT I WANT TO CHANGE INTO boat name instead of boatId
.padding()
TextField("Enter work title", text: $wcvm.work.title, onCommit: {
self.onCommit(self.wcvm.work)
})
.font(.title)
.padding()
HStack {
TextField("Enter problem description", text: $wcvm.work.descriptionpb, onCommit: {
self.onCommit(self.wcvm.work)
})
}
.font(.subheadline)
.foregroundColor(.secondary)
.padding()
}
}
}
}
Essentially you have a Data Model problem, not a SwiftUI problem. I would be keeping all of this in Core Data and linking the various models(Entities in Core Data) with relationships. So your Work(Essentially a work order) would link to the boat that the work was being performed on.
Otherwise, you need to add a Boat as a parameter to Work. Either way would give you the desired syntax, but Core Data is much more efficient. It is also your data persistence model so you would kill two birds with one stone.
Solution found: when creating a work order, I was assigning the boat id, I am now assigning the boat name as well (and calling it in the work order display). Essentially keeping the same code as above, tweaking it a little bit so that it does what I want to do.
I have a List in SwiftUI App on MacOS with e.g. 10.000 entries.
Trying like the example below is horribly slow.
Adding .id(UUID()) to the List, which was advised in a prior post, makes it a bit quicker but still not fluid.
Even worst, adding .id(UUID()) to the list, the list then cannot be navigates by the arrow-key (up/down).
Is there a better way to achieve this?
struct TestViews_MacOS_BigList: View {
#State var selectedItem: String?
var items: [String]
var body: some View {
List(items,id: \.self, selection: $selectedItem ) { item in
Text("\(item)").tag("\(item)")
}
//.id(UUID())
}
}
func testnames()->[String]{
var list: [String] = []
for i in 1...10000 {
list.append("Sting Nr \(i)")
}
return list
}
Those are too many Views to have sitting around. You need to use CoreData or some other manual way to Batch load items and a way to only have a certain number of Views/items fetched/loaded at a time.
An NSFetchedResultsController that specifies a batch size can help with that
let fetchRequest: NSFetchRequest<Item> = NSFetchRequest<Item>(entityName: "Item")
fetchRequest.includesPendingChanges = false
fetchRequest.fetchBatchSize = 20
When the fetch is executed, the entire request is evaluated and the identities of all matching objects recorded, but only data for objects up to the batchSize will be fetched from the persistent store at a time.
#FetchRequest might do it as well, it was made for SwiftUI so it should compensate but the documentation does not specify.
Try using LazyVStack since it uses memory efficiently as below:
struct TestViews_MacOS_BigList: View {
#State var selectedItem: String?
var items: [String]
var body: some View {
ScrollView {
LazyVStack{
ForEach(items, id: \.self, content: { item in
HStack{
Button(action: {
selectedItem = item
}, label: {
Text("Select ")
})
Text("\(item)").tag("\(item)")
}
})
}
}
}
}
I have a struct model called Questions stored in DataRep which is also stored in EnvironmentObject. There are 100+ questions in 12 categories.
struct Question: Hashable, Codable, Identifiable {
var id: Int
var questionText: String
var questionComment: String
var category: String
}
class DataRep: ObservableObject {
#Published var QuestionList : [Question] = QuestionListData
#Published var selectedCategory = "all"
}
On the user interface, I placed 12 buttons at top and list view down to list the questions in that category.
When user clicks on a new category, I update the selectedCategory parameter and filter the main questions list object to select the relevant questions.
struct QuestionList: View {
#EnvironmentObject var datarep: DataRep
var body: some View {
NavigationView{
Form {
ForEach(self.filterQuestions(datarep.QuestionList)) { question in
HStack{
QuestionView (question: question)
}
}
}//List
.navigationBarTitle(self.datarep.selectedCategory )
.labelsHidden()
.listStyle(GroupedListStyle())
}
}
func filterQuestions(_ activeList : [Question]) -> [Question]
{
if self.datarep.selectedCategory != "all" {
return activeList.filter{ $0.category.contains(self.datarep.selectedCategory) }
}
return activeList
}
}
However I am running into issues with filter method as it is generating a new array each time category is changed. There is no way I know to create a binding.
any suggestions?
Regards,
M
Assuming the QuestionView will take binding (to change the question), as
struct QuestionView: View {
#Binding var question: Question
...
it can be bound (even after filtering) via main container like following
ForEach(self.filterQuestions(datarep.QuestionList)) { question in
HStack{
QuestionView (question:
self.$datarep.QuestionList[self.datarep.QuestionList.firstIndex(of: question)!])
}
}