I'm building an Apple Watch app that uses SwiftUI.
New data periodically comes into the app from an outside source, and I keep track of when that last happened with a #Published Date (called lastUpdatedDate) inside an ObservableObject (store).
In the app, I want to use a SwiftUI Text struct to indicate to the user how long ago the data was updated.
I'm weighing different options, and I'm wondering what the best practice would be for something like this.
Solution #1 - Text.DateStyle
A very simple method I tried was using Text.DateStyle.relative:
Text(store.lastUpdatedDate, style: .relative)
This worked the way I wanted to some degree, because it kept the text up-to-date with the relative interval since the date had happened, but the text is not customizable. I do not want it to show the number of seconds, just minutes or hours.
Solution #2 - Computed Property
Inside the View where I want to display the Text, I have access to the ObservableObject, and added a computed property that uses a function that converts the Date to a String for the relative date (for example, 5 minutes ago) using an extension on Date with a RelativeDateTimeFormatter
Computed property:
private var lastUpdatedText: String {
store.lastUpdatedDate.timeAgoDisplay()
}
Date extension:
extension Date {
func timeAgoDisplay() -> String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
return formatter.localizedString(for: self, relativeTo: Date())
}
}
This seems like the best solution I've found so far, because it automatically updates every time the app is opened. The downside, however, is that it only seems to update when the app comes to the foreground (when another app or the Clock Face was previously active). This is probably fine, but it could be better on Series 5, Series 6, and Series 7, which have alway-on displays. On watchOS 8, with the Always On State, this text remains on the screen when the wrist is dropped, and when the wrist is raised again. The text can become outdated, because these events do not cause the lastUpdatedText computed property to be updated again.
Update: This is actually not a very good solution. Through more testing, I found out that it is a fluke that it was updating every time the app opened. It was only re-computing the property on app open because other items in the view were getting refreshed, and on its own it would not have actually re-computed every time the app was opened.
Other Possible Solutions
I've considered adding a second #Published variable in the ObservableObject that's a String containing the relative time interval. The downside of this is I would have to manually initialize and update that variable. If I do that, I could manually update the text based on lifecycle functions in ExtensionDelegate, like applicationWillEnterForeground(), but that only handles when the app first comes to the foreground (which is the same update frequency as the computed property). I haven't found any way to detect when the wrist is dropped or raised again, so it seems the only way I could keep it up to date is to set up one or more published Timers, and update the #Published String every minute, disabling and setting up the timer(s) every time the app goes into the background and returns to the foreground. Does that seem like a good solution? Is there a better solution I'm overlooking?
Solution
Based on the accepted answer, I was able to use a TimelineView to update periodically.
Even though I am only using minute granularity for the text that's displayed, I wanted the text to update to the second when the data was actually refreshed. To accomplish that part, I started by adding a new extension for Date:
extension Date {
func withSameSeconds(asDate date: Date) -> Date? {
let calendar = Calendar.current
var components = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute, .second], from: self)
let secondsDateComponents = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute, .second], from: date)
components.second = secondsDateComponents.second
return calendar.date(from: components)
}
}
Then, I created my view used the TimelineView to update the text every minute after the data is refreshed:
struct LastUpdatedTextView: View {
#ObservedObject var store: DataStore
var body: some View {
if let nowWithSameSeconds = Date().withSameSeconds(asDate: store.lastUpdatedDate) {
TimelineView(PeriodicTimelineSchedule(from: nowWithSameSeconds,
by: 60))
{ context in
Text(store.lastUpdatedText(forDate: lastUpdatedDate))
}
} else {
EmptyView()
}
}
}
Apple addresses this in Build A Workout App from WWDC21
They use TimelineView and context.cadence == .live to tell their formatter to not show milliseconds when the watch is in Alway on.
You could use that code to determine what to show.
struct TimerView: View {
var date: Date
var showSubseconds: Bool
var fontWeight: Font.Weight = .bold
var body: some View {
if #available(watchOSApplicationExtension 8.0, watchOS 8.0, iOS 15.0, *) {
//The code from here is mostly from https://developer.apple.com/wwdc21/10009
TimelineView(MetricsTimelineSchedule(from: date)) { context in
ElapsedTimeView(elapsedTime: -date.timeIntervalSinceNow, showSubseconds: context.cadence == .live)
}
} else {
Text(date,style: .timer)
.fontWeight(fontWeight)
.clipped()
}
}
}
#available(watchOSApplicationExtension 8.0, watchOS 8.0, iOS 15.0,*)
private struct MetricsTimelineSchedule: TimelineSchedule {
var startDate: Date
init(from startDate: Date) {
self.startDate = startDate
}
func entries(from startDate: Date, mode: TimelineScheduleMode) -> PeriodicTimelineSchedule.Entries {
PeriodicTimelineSchedule(from: self.startDate, by: (mode == .lowFrequency ? 1.0 : 1.0 / 30.0))
.entries(from: startDate, mode: mode)
}
}
struct ElapsedTimeView: View {
var elapsedTime: TimeInterval = 0
var showSubseconds: Bool = false
var fontWeight: Font.Weight = .bold
#State private var timeFormatter = ElapsedTimeFormatter(showSubseconds: false)
var body: some View {
Text(NSNumber(value: elapsedTime), formatter: timeFormatter)
.fontWeight(fontWeight)
.onChange(of: showSubseconds) {
timeFormatter.showSubseconds = $0
}
.onAppear(perform: {
timeFormatter = ElapsedTimeFormatter(showSubseconds: showSubseconds)
})
}
}
I'm trying to show HTML text which is an event program stored in database from TinyMCE textarea in a bootstrap modal.
I need to parse it before, for that I found that Laravel 5.* uses:{!! !!} to parse but with $event->program it always shows the last one stored.
here is my js code
$('#eventDetails').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget)
var id = button.data('id')
var name = button.data('name')
var description = button.data('description')
var program = button.data('program')
var date = button.data('date')
var modal = $(this)
modal.find('.modal-body #id').text(id);
modal.find('.modal-body #name').text(name);
modal.find('.modal-body #description').text(description);
modal.find('.modal-body #program').text(program);
modal.find('.modal-body #date').text(date);
});
so what shoud I do please !!?
I am trying to use this power bi below code where powerbi object not found error is getting in my typescript code:
// Read embed application token from textbox
var txtAccessToken = $('#txtAccessToken').val();
// Read embed URL from textbox
var txtEmbedUrl = $('#txtReportEmbed').val();
// Read report Id from textbox
var txtEmbedReportId = $('#txtEmbedReportId').val();
// Read embed type from radio
var tokenType = $('input:radio[name=tokenType]:checked').val();
// Get models. models contains enums that can be used.
var models = window['powerbi-client'].models;
// We give All permissions to demonstrate switching between View and Edit mode and saving report.
var permissions = models.Permissions.All;
// Embed configuration used to describe the what and how to embed.
// This object is used when calling powerbi.embed.
// This also includes settings and options such as filters.
// You can find more information at https://github.com/Microsoft/PowerBI-JavaScript/wiki/Embed-Configuration-Details.
var config= {
type: 'report',
tokenType: tokenType == '0' ? models.TokenType.Aad : models.TokenType.Embed,
accessToken: txtAccessToken,
embedUrl: txtEmbedUrl,
id: txtEmbedReportId,
permissions: permissions,
settings: {
filterPaneEnabled: true,
navContentPaneEnabled: true
}
};
// Get a reference to the embedded report HTML element
var embedContainer = $('#embedContainer')[0];
// Embed the report and display it within the div container.
var report = powerbi.embed(embedContainer, config);
// Report.off removes a given event handler if it exists.
report.off("loaded");
// Report.on will add an event handler which prints to Log window.
report.on("loaded", function() {
Log.logText("Loaded");
});
report.on("error", function(event) {
Log.log(event.detail);
report.off("error");
});
report.off("saved");
report.on("saved", function(event) {
Log.log(event.detail);
if(event.detail.saveAs) {
Log.logText('In order to interact with the new report, create a new token and load the new report');
}
});
in the above code the powerbi object shows not found in my typescript code: powerbi.embed(embedContainer, config);
I tried to use window['powerbi'] or window.powerbi but doesn't work. What should be the solution then?
I faced a similar issue a few weeks back (probably exactly the same). For me it seems that what works is using window.powerbi.embed() for the embed action, whereas the import import * as powerbi from "powerbi-client"; is used for all other Power BI objects.
I had the same problem, found this question through a google search. I wasn't able to figure out why it wasn't on the window, but as a work around you can initialize it yourself like this:
import * as pbi from "powerbi-client";
const powerbi = new pbi.service.Service(
pbi.factories.hpmFactory,
pbi.factories.wpmpFactory,
pbi.factories.routerFactory
);
const container = document.getElementById("report-container");
powerbi.embed(container, embedConfiguration);
I have the following lines in my SAPUI5 app
var dateVal = controls.awardDate.getDateValue();
var month = dateVal.getMonth();
awardDate is a datepicker the user enters a date on and returns a javascript date object. This is a snippet of my qunit to test this element.
awardDate: {
getValue: getInvalidValue,
getValueState: getValueStateWarning,
setValue: setValue,
getDatevalue: getDateValue
}
In my qunit I get an error saying that the object doesn't support property or method 'getDateValue'. I'm not sure how I'm supposed to stub this function when it returns an object. Other tests I have do it this way
var getValue = sinon.stub().returns('');
where I get an empty string.
so my attempt at to do it with the datepicker is
var getDateValue = sinon.stub().returns(new Date());
but this doesn't work. I still get the same error. Has anyone done this before?
edit/update: I was able to fix part of the problem by doing the following
var getValueDate = sinon.stub().returns(Object, function(){ });
Now the problem I have is the same error but for getMonth() which returns a string. All other variables are global but dateVal is created on the spot when the user updates the datepicker. Any ideas on how to proceed on this one?
Try with this code:
var getValueDate = sinon.stub(controls.awardDate, 'getDateValue');
var month = {
getMonth: sinon.stub()
}
getValueDate.returns([month]);
I was able to figure out how to solve this. I had to make the Object type a specific Date object like this
var getValueDate = sinon.stub().returns(new Date()), function(){ });
I'm trying to replace instances of jsTree with Foundation's drilldown menu as it has better benefits, but in some places I'm utilizing jsTree's lazy loading however there's no option for this in Foundation. Is there any way of doing this, or does anybody know if it's been done before?
I had the initial parent-level list populating, but the drilldown functionality wasn't being applied after calling Foundation.Drilldown(), and I haven't been able to figure out the next-level items yet.
$(function(){
var tree = $("ul#tree_id");
$.getJSON("/url/",function(data){
console.log(data);
if(data){
$.each(data,function(key,value){
var node = document.createElement("li");
$(node).html(value.text);
tree.prepend($(node))
});
}
var elem = new Foundation.Drilldown(tree);
});
});
Thanks.