import SwiftUI
struct SchoolsDetailView: View {
var data: School
var body: some View {
VStack{
Text((data.properties.name)!)
.modifier(CustomTextM(fontName: "OpenSans-Regular", fontSize: 20, fontColor: Color.black))
Spacer()
(Text("(") + Text((data.properties.areaCode)!) + Text(")-") + Text((data.properties.phone)!))
.modifier(CustomTextM(fontName: "OpenSans-Regular", fontSize: 15, fontColor: Color.black))
(Text((data.properties.address)!) + Text(", ") + Text(data.properties.city!) + Text(", ") + Text(data.properties.state!))
.modifier(CustomTextM(fontName: "OpenSans-Regular", fontSize: 15, fontColor: Color.black))
}
}
}
struct SchoolsDetailView_Previews: PreviewProvider {
static var previews: some View {
NavigationStack{
SchoolsDetailView(data: SchoolModel().schools[2])
}
It keeps printing out array out of index. And if I do "SchoolModel().schools.first!", it prints out "Unexpectedly found nil while unwrapping an Optional value"
Not sure of what to do to call it. From the List View, it segues and shows the detail fine but on the detail page it keeps printing out error.
When wanting to support previews and using immutable data its best to pass in the simple types to the View rather than the rich model type, e.g.
struct SchoolsDetailView: View {
let name: String
let area: String
let address: String
This allows you to simply do:
struct SchoolsDetailView_Previews: PreviewProvider {
static var previews: some View {
SchoolsDetailView(name: "Test Name", area: "Test Area", address: "Test Address")
}
This has the added efficiency that body is only recomputed when the data actually displayed changes. Rather than it needlessly being recomputed when another property of the school changes which is not displayed.
Watch WWDC 2020 Structure your app for SwiftUI previews for more info, in particular the first example "First, we'll look at at an example of passing immutable, simple data types into a view that doesn't need to change the values."
Related
Hope you're well! I have an issue where updates to an array in my view model aren't getting picked up until I exit and re-open the app.
Background: My App loads a view from a CSV file hosted on my website. The app will iterate through each line and display each line in a list on the view.
Originally I had a function to call the CSV and then pass the data to a String to be parsed each time a refresh was run (user requested or background refresh). This would work for the most part but it did need a user to pull down to refresh or some time to pass for the view to reload (minor issue with the context of the whole app).
I've since changed how the app loads the CSV so it loads it in documentDirectory to resolve issues when theres no internet, the app can still display the data from the last update instead of failing. After running updates to the csv and re-loading it i can see the events variable is getting updated on my view model but not in my list/view. This is a bit of a problem for when the app is first opened as it shows no data as the view has loaded before the csv is parsed. Need to force close the app to have the data load into the list.
I've made some assumptions with the code to share, the csv load & process has no issues as I can print filterviewModel.events before & after the updates and can see changes in the console but not the view. I've also stripped down as much of the shared code so it is easier to read.
Here is the relevant section of my view model:
class EventsListViewModel: Identifiable, ObservableObject {
// Loads CSV from website and processes the data into an structured list.
#Published var events = loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }
}
My View:
struct EventListView: View {
// Calls view model
#ObservedObject var filterviewModel = EventsListViewModel()
var body: some View {
NavigationView {
// Calls event list from view model and iterates through them in a list.
List(filterviewModel.events, id: \.id) { event in
//Formats each event in scope and displays in the list.
eventCell(event: event)
}
}
// Sets the navagation title text.
.navigationTitle("Upcoming events")
// When refreshing the view it will re-load the events entries in the view model and refresh the most recent data.
.refreshable{
do {
//This is the function to refresh the data
pullData()
}
}
// End of the List build
}
}
Cell formatting (Unsure if this is relevant):
struct eventCell: View {
var event: CSVEvent
#ObservedObject var filterviewModel = EventsListViewModel()
var body: some View {
HStack{
VStack(alignment: .leading, spacing: 5){
//Passes the event location as a link to event website.
let link = event.url
Link(event.location, destination: URL(string: link)!)
// Passes the event name to the view.
Text(event.eventname)
.font(.subheadline)
.foregroundColor(.secondary)
}.frame(width: 200.0, alignment: .topLeading)
// Starts new column in the view per event.
VStack {
HStack {
Spacer()
VStack (alignment: .trailing, spacing: 5){
// Passes date
Text(event.date)
.fontWeight(.semibold)
.lineLimit(2)
.minimumScaleFactor(0.5)
// If time is not temp then display the event start time.
Text(actualtime)
.frame(alignment: .trailing)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
}
This is pullData, It retrieves the latest version of the CSV before processing some notifications (notifications section removed for ease of reading, print statement is where i can see the data updating on the view model but not applying)
func pullData(){
#ObservedObject var filterviewModel = EventsListViewModel()
filterviewModel.events = loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }
}
Here is what happens under loadCSV, unsure if this is contributing to the issue as i can see the variable successfully getting updated in pullData
// Function to pass the string above into variables set in the csvevent struct
func loadCSV(from csvName: String) -> [CSVEvent] {
var csvToStruct = [CSVEvent]()
// Create destination URL
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")
//Create string for the source file
let fileURL = URL(string: "https://example.com/testcsv.csv")!
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("CSV downloaded Successfully")
}
do {
try? FileManager.default.removeItem(at: destinationFileUrl)
try FileManager.default.copyItem(at: tempLocalUrl, to: destinationFileUrl)
} catch (let writeError) {
print("Error creating a file \(destinationFileUrl) : \(writeError)")
}
} else {
print("Error" )
}
}
task.resume()
let data = readCSV(inputFile: "testcsv.csv")
//print(data)
// splits the string of events into rows by splitting lines.
var rows = data.components(separatedBy: "\n")
// Removes first row since this is a header for the csv.
rows.removeFirst()
// Iterates through each row and sets values to CSVEvent
for row in rows {
let csvColumns = row.components(separatedBy: ",")
let csveventStruct = CSVEvent.init(raw: csvColumns)
csvToStruct.append(csveventStruct)
}
print("Full file load run")
return csvToStruct
}
func readCSV(inputFile: String) -> String {
//Split file name
let fileExtension = inputFile.fileExtension()
let fileName = inputFile.fileName()
let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let inputFile = fileURL.appendingPathComponent(fileName).appendingPathExtension(fileExtension)
do {
let savedData = try String(contentsOf: inputFile)
return savedData
} catch {
return "Error, something has happened when attempting to retrive the latest file"
}
}
Is there anything obvious that I should be doing to get the list updating when the events array is getting updated in the viewmodel?
Thanks so much for reading this far!
as mentioned,
you should have only 1 EventsListViewModel that you pass around the views. Currently you re-create a new EventsListViewModel in your eventCell. Although you don't seem to use it, at least not in the code you are showing us.
The same idea applies to all other views. Similarly for pullData() you should update the filterviewModel with the new data, for example, pass the filterviewModel into it, if it is in another class.
Try this:
EDIT-1: added pullData()
struct EventListView: View {
// Calls view model
#StateObject var filterviewModel = EventsListViewModel() // <-- here
var body: some View {
NavigationView {
// Calls event list from view model and iterates through them in a list.
List(filterviewModel.events, id: \.id) { event in
//Formats each event in scope and displays in the list.
EventCell(event: event) // <-- here
}
}
.environmentObject(filterviewModel) // <-- here
// Sets the navagation title text.
.navigationTitle("Upcoming events")
// When refreshing the view it will re-load the events entries in the view model and refresh the most recent data.
.refreshable{
do {
//This is the function to refresh the data
pullData()
}
}
// End of the List build
}
func pullData() {
filterviewModel.events = loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }
}
func loadCSV(from csvName: String) -> [CSVEvent] {
//...
}
}
struct EventCell: View {
var event: CSVEvent
#EnvironmentObject var filterviewModel: EventsListViewModel // <-- here
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 5){
//Passes the event location as a link to event website.
let link = event.url
Link(event.location, destination: URL(string: link)!)
// Passes the event name to the view.
Text(event.eventname)
.font(.subheadline)
.foregroundColor(.secondary)
}.frame(width: 200.0, alignment: .topLeading)
// Starts new column in the view per event.
VStack {
HStack {
Spacer()
VStack (alignment: .trailing, spacing: 5){
// Passes date
Text(event.date)
.fontWeight(.semibold)
.lineLimit(2)
.minimumScaleFactor(0.5)
// If time is not temp then display the event start time.
Text(actualtime)
.frame(alignment: .trailing)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
}
}
In forge viewer, for a revit converted file, when making a bubble search:
viewerApp.bubble.search({ 'type': 'geometry', 'role': '3d' });
Or
viewerApp.getSelectedItem()
I get an element node like:
children: (2) [a, a]
data: {guid: "a21582db-704b-df51-dd71-dbf8c12bcc1a", type: "geometry", role: "3d", name: "{3D}", viewableID: "6104055e-60d9-4037-9adc-cd38e10fcfba-00139c8e", …}
id: 8
isLeaf: true
parent: a {parent: a, id: 7, data: {…}, isLeaf: false, children: Array(14)}
I have the guid of the node, and a viewableID.
Then, to display a model, I can call viewerApp.selectItemById(guid/viewableID), which ends displaying the same model.
If I want to point to the 3D view I currently see in the viewer, for future reference (e.g. after revit file update), what is the best attribute for it, guid or viewableID?
Thank you,
The viewable id stands for the unique id of the Revit views in Revit API as my research, but I cannot find the relationship between Revit views and guid in the viewable bubble node. I'm checking with our engineering team if they have some insights.
The viewerApp.selectItemById() is for querying bubble node via its' guid, so you cannot pass viewable id into it. Otherwise, it will return nothing as my investigation.
To archive selecting by viewable id, I would advise you to use the follow instead:
const bubbles = viewerApp.bubble.search({ 'viewableID': '6104055e-60d9-4037-9adc-cd38e10fcfba-00139c8e' });
viewerApp.selectItemById( bubbles[0].guid );
Or extend your own methods (tested with v6.2):
LMV.BubbleNode.prototype.findByViewableId = function (viewableId) {
let item = null;
this.traverse(function (node) {
if (node.data.viewableID === viewableId) {
item = node;
return true;
}
});
return item;
};
LMV.ViewingApplication.prototype.selectItemViewableId = function (viewableId, onItemSelectedCallback, onItemFailedToSelectCallback) {
let item = this.myDocument.getRoot().findByViewableId(viewableId);
if (item) {
return this.selectItem(item, onItemSelectedCallback, onItemFailedToSelectCallback);
}
return false;
};
// -- You codes where you create the ViewingApplication instance
I'm new to angular2 and am attempting to map a JSON response to an array of custom objects. The strange behavior I am seeing is when I attempt to access a variable with an underscore in it, I receive a compile error. I modeled after the Tour of Heroes code.
My Object is:
export class EventCount {
event_count: number;
name: string;
id: string;
}
To match the JSON response sample:
[{"event_count":10,"name":"dev-03","id":"0001"},
{"event_count":6,"name":"dev-02","id":"0002"}]
My http.get request looks like:
getEventCounts(): Observable<Array<any>> {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
let query = this.getEventCountURL;
return this.http.get(query, options)
.map(this.extractCounts)
.catch(this.handleError);
}
And the function called within the map looks like:
private extractCounts(response: Response) {
if (response != null) {
try {
let myCounts = response.json() as EventCount[];
for (let entry of myCounts) {
console.log("Name: " + entry.name + ", id: " + entry.id );
}
} catch (e) {
console.error(e.toString());
}
}
return myCounts;
}
The above code works fine and I receive the correct output in the console log.
Name: dev-03, id: 0001
Name: dev-02, id: 0002
However when I change the log to the following:
console.log("Name: " + entry.name + ", count: " + entry.event_count );
I get a compile error:
ERROR in .../count.service.ts: Property 'event_count' does not exist on type 'EventCount'.
In the VSCode debugger I can clearly see that the event_count variable within the entry object is getting populated when I am not using it in the log printout.
Thoughts or ideas? Thanks in advance!
I see only one problem with your code which is almost identical what that of angular.io documentation on Http. This has to do with the local definition of myCounts which cannot be seen where the return statement needs it. Other that this, when I replaced entry.id with entry.event_count everything went fine. I declared let myCounts: EventCount[] before your if statement, removed the as EventCount[] also. Since you're new to Angular you should use the latest version of the platform.
I'm attempting to make a Pebble app that reads data from a JSON URL.
However, no matter what I do I can't seem to make the data appear on the app.
When entering this code in:
console.log(data.contents.AvailableBal);
console.log(data.contents.CurrentBal);
I would expect to get a result back in the logs that would help but I only get:
[PHONE] pebble-app.js:?: None
[PHONE] pebble-app.js:?: None
And when viewing the app in the emulator, both values where the data should be, say "Undefined".
My code is as follows. Any help would be great!
var UI = require('ui');
var ajax = require('ajax');
var splashCard = new UI.Card({
title: "Please Wait",
body: "Downloading..."
});
splashCard.show();
ajax(
{
url: 'https://djkhaled.xyz/balance.json',
type: 'json'
},
function(data) {
console.log(data.contents.AvailableBal);
console.log(data.contents.CurrentBal);
var main = new UI.Card({
title: 'Balances',
body: 'Available Balance: ' + data.contents.AvailableBal +
'\nCurrent Balance: ' + data.contents.CurrentBal
});
splashCard.hide();
main.show();
}
);
This is your JSON,
{
"contents": [{
"AvailableBal": "$0.00 CR",
"CurrentBal": "$0.00 CR"
}]
}
You can not directly access data.contents.AvailableBal because AvailableBal is not directly inside contents. contents has an array
and this array's first object (object at index 0) contains the object that has AvailableBal and CurrentBal keys.
I'm not a JavaScript developer but probably answer would be something like this,
var main = new UI.Card({
title: 'Balances',
body: 'Available Balance: ' + data.contents[0].AvailableBal +
'\nCurrent Balance: ' + data.contents[0].CurrentBal
});
How to link a JSON stream to a QML model like you would do with angularJS?
In my QML, I have a Websocket object that receives data from a server:
WebSocket {
id: socket
url: "ws://3.249.251.32:8080/jitu"
onTextMessageReceived: { var jsonObject = JSON.parse(message) }
onStatusChanged:
if (socket.status == WebSocket.Error) { console.log("Error: " + socket.errorString) }
else if (socket.status == WebSocket.Open) { console.log("Socket open"); }
else if (socket.status == WebSocket.Closed) { console.log("Socket closed"); }
active: false
}
In this JSON I have something like:
{ items: [ "FOO", "BAR" ] }
Then I want to display two tabs, on titles FOO and the other, unsurprisingly, titled BAR.
This work great if I create a repeater that goes through the array in my model and create a tab for each entry:
TabView {
anchors.fill: parent
Repeater {
model: ListModel { id: tabs }
Tab {
title: Caption
Rectangle { color: "red" }
}
}
}
Up to now, this really looks like angularJs. I just need now to update my model (scope for angular) with the data received through the websocket.
For this, I have to add the tabs from my JSON to the ListModel as such:
...
onTextMessageReceived: {
var jsonObject = JSON.parse(message)
tabs.append(jsonObject.ITU.Modalities);
}
...
The problem is, each time I'll receive an update from JSON, tabs will be added. I don't want to clear the ListView each time, that would be time consuming I think. Is there a smart way to update the model from JSON smartly ? In angular, as they are both javascript structure, they are easy to merge. But here, I don't see an easy way.