-1

Briefly explained; I am working on a project for my Tesla car. Tesla already has a widget that only can be added to the Today View tab, and that widget automatically refreshes when I swipe to to the today view. This is what it looks like: Image here

I want to accomplish the same thing in my widget. I basically have the working code, look below:

NB: This project of mine is for experimental and personal use only.

extension Date {
    func timeAgoDisplay() -> String {
        let formatter = RelativeDateTimeFormatter()
        formatter.unitsStyle = .full
        return formatter.localizedString(for: self, relativeTo: Date())
    }
}

import WidgetKit
import SwiftUI
import Intents
import TeslaSwift

struct Provider: IntentTimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent())
    }

    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: configuration)
        completion(entry)
    }

    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }

        getVehicle() // Run this function to get vehicle info

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
}

var lastUpdated = String()
var batteryLevel = Int()
var interiorTemperature = Double()

func getVehicle() {
    
    let apii = TeslaSwift()
    
    if let jsonString = UserDefaults(suiteName: "group.widget")!.string(forKey: "GlobalToken"),
       let token: AuthToken = jsonString.decodeJSON(),
       let _ = UserDefaults(suiteName: "group.widget")!.string(forKey: "GlobalToken") {
        apii.reuse(token: token, email: nil)
    }
    
    apii.useMockServer = false
    apii.debuggingEnabled = true
    
    let id = UserDefaults(suiteName: "group.widget")!.string(forKey: "GlobalSelectedID")
    
    apii.getVehicle(id!).done {
        (vehicle: Vehicle) -> Void in
        
        apii.getAllData(vehicle).done { (extendedVehicle: VehicleExtended) in
            
            batteryLevel = (extendedVehicle.chargeState?.batteryLevel)!
            interiorTemperature = (extendedVehicle.climateState?.insideTemperature!.celsius)!
            
            let formatter = DateFormatter()
            formatter.dateFormat = "dd.MM.yyyy - HH:mm:ss"
            let now = Date()
            let dateString = formatter.string(from:now)
            lastUpdated = dateString
            
        }.catch { (error) in
            
            print("error1: \(error)")
        }
        
    }.catch { error in
        print("error2: \(error)")
    }
}

struct PWidgetEntryView : View {
    
    var entry: Provider.Entry
    
    var body: some View {

        VStack {
            Text("Battery: \(batteryLevel)%")
            Text("Temparature: \(String(format: "%.0f", interiorTemperature))")
            Text("Last Updated: \(lastUpdated)")
                .environment(\.sizeCategory, .extraSmall)
        }
    }
}

@main
struct PWidget: Widget {
    let kind: String = "PWidget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            PWidgetEntryView(entry: entry)
        }
        .supportedFamilies([.systemMedium])
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

struct PWidget_Previews: PreviewProvider {
    static var previews: some View {
        PWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
            .previewContext(WidgetPreviewContext(family: .systemMedium))
    }
}

So now, when using this code. It fetch the data perfectly, but the widget does not display the data.

If I add WidgetCenter.shared.reloadAllTimelines() after the lastUpdated = dateString, the widget updates, but also keeps updating about every single five seconds. That will draw a huge amount of battery.

I have also tried by adding var didUpdateManually = false outside of the func getVehicle() { and then a if false check like this. That makes it update the widget once, but never ever again:

if (didUpdateManually == false) {
    WidgetCenter.shared.reloadAllTimelines()
    didUpdateManually = true
}

So basically there are two/three things I am trying to accomplish:

1. Display the value from API to my widget (batteryLevel, interiorTemperature and lastUpdated timestamp).

2. If either or both is possible:
2.A: When the widget is added to the Today View tab, I want to automatically update the widget by re-running the `func getVehicle()` and update the info when the user swipe to the Today View tab.
2.B: If the widget is on the home screen page, I want to widget to automatically update when the in the same way as 2A, or update once every hour or so.
Erik Auranaune
  • 1,384
  • 1
  • 12
  • 27
  • 1
    This isn’t how widgets work. Your widget is configured to update once per hour for five hours, but you pass no data to it — SimpleEntry should contain all the data the widget needs to update. If you haven’t tried Apple’s WidgetKit tutorial, it’s a great way to learn how all these components work together: https://developer.apple.com/news/?id=yv6so7ie – Adam Aug 11 '21 at 20:41
  • @Adam Yes, looks like I misunderstood a bit. Thank you for explaining. You say that `Your widget is configured to update once per hour for five hours` What does that really means? My Widget will be updated every hour for 5 hours, and then never updated again? – Erik Auranaune Aug 11 '21 at 20:45
  • Your code creates a Timeline with 5 entries, and the “.atEnd” refresh policy. So you’re widget will update for 5 hours using those 5 entries, and then “sometime” after the fifth entry, the system will call your getTimeline() method again. I say “sometime” because the system controls how often widgets update; you can only tell it what you want to happen. Lots more detail here: https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date – Adam Aug 11 '21 at 20:54
  • @Adam Ok, because my widget displays a date and time that should be different each time the widget updates. Is the approach I am using now correct in that case? – Erik Auranaune Aug 11 '21 at 20:55
  • I don’t think so, because it’s using lastUpdated and that’s a global variable… I don’t know what that will do, but I doubt it works. Try using entry.date instead: `Text("Last Updated: \(entry.date)")` – Adam Aug 11 '21 at 20:58
  • @Adam But what if I change it to `for hourOffset in 0 ..< 1 {`? Does that make the `Text` change every time it updates if I use `entry.date`? Also another quick question. I have a app in the today view that basically updates every time I swipe to the today view. How can I do something similar for my widget? – Erik Auranaune Aug 11 '21 at 21:39
  • @Adam The app I have that automatically updates when I swipe to today view is located only in here: https://i.redd.it/h0zjktsogr651.jpg (Example image from google) – Erik Auranaune Aug 11 '21 at 21:48
  • 1
    That’s an old “Today widget”, which still work but were deprecated in iOS 14. They can run code every time they appear, but the new widgets can’t. – Adam Aug 11 '21 at 22:37
  • @Adam Ok, so what would not work if the iOS is 14 or higher, I see. How would you do it then? If I want to display the exact battery percentage for my car in the widget at any time? – Erik Auranaune Aug 11 '21 at 22:43
  • @Adam Can I do a workaround on that, like this: https://stackoverflow.com/questions/64505672/ios-how-to-add-today-extension-target-in-xcode-12-1 ? Then I can be on iOS 14 like I am now, and then run code when they appear? I am going to use this app only by myself, so if it works thats good for me. – Erik Auranaune Aug 11 '21 at 22:54
  • Yes, you can still create Today widgets (the answer you linked has good instructions). They still work in iOS 15, at least for now. – Adam Aug 12 '21 at 01:13

1 Answers1

0

For update widget on every 1hour use atEnd Policy with two entries:-

  func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
             getVehicle() // Run this function to get vehicle info
             let entries = [
              SimpleEntry(date: Date(), configuration: configuration)
              SimpleEntry(date: Calendar.current.date(byAdding: .minute, value: 60, to: Date())!, configuration: configuration)
                ]
              let timeline = Timeline(entries: entries, policy: .atEnd)
              completion(timeline)
            }
    }
// If its not updating the widget just call api in getTimeline and on the success of api add timeline entries.

// On my side, I had fetch calendar events and it is worked for me, Hope its also work for you. Thanks

Sukh
  • 1,278
  • 11
  • 19