I'm trying to access IntentTimelineProvider of WidgetExtension from the main app. I'm doing this so that I can get the date property from the IntentTimelineProvider and use it in a view in the main app target as you can see from the below code.
I have set the target membership of the files(file in the main app target and the file in the widget extension target) to both App and Widget.
Widget Extension
import WidgetKit
import SwiftUI
import Intents
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)
}
//build time error here: "Reference to invalid associated type 'Entry' of type 'Provider'" here...
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries = [SimpleEntry]()
let currentDate = Date()
let midnight = Calendar.current.startOfDay(for: currentDate)
let nextMidnight = Calendar.current.date(byAdding: .day, value: 1, to: midnight)!
for offset in 0 ..< 60 * 24 {
let entryDate = Calendar.current.date(byAdding: .minute, value: offset, to: midnight)!
entries.append(SimpleEntry(date: entryDate, configuration: configuration))
}
let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
struct TimeWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
NotingView(entry: entry)
}
}
#main
struct TimeWidget: Widget {
let kind: String = "TimeWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
TimeWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct TimeWidget_Previews: PreviewProvider {
static var previews: some View {
TimeWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
Main App
import SwiftUI
struct SampleView: View {
var entry : Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
I'm currently getting the build time error :
"
Reference to invalid associated type 'Entry' of type 'Provider'
"
at getTimeline() function.
In your main app, use SimpleEntry instead of Provider.Entry. The latter is referencing the associated type of your implementation of the TimelineProvider protocol, but for whatever reason the app can’t infer the type (maybe somebody more Swift-savvy than me can explain). But you don’t need to infer it, just give it the actual type: SimpleEntry.
struct SampleView: View {
var entry : SimpleEntry
var body: some View {
Text(entry.date, style: .time)
}
}
Related
I am trying to show an array fetched from firebase with a ForEach in SwiftUI.
However, it is only showing the title of the first index of the array.
It seems like it does register how many items there are in the array, as it shows the correct number of views according to the number of items in the array but each item only has the title of the first item.
How do I make it show the title of all of the items in the array?
I fetch the project like so:
class ProjectRepository: ObservableObject
{
private var cancellables: Set<AnyCancellable> = []
private let store = Firestore.firestore()
#Published var projects: [Project] = []
init()
{
get()
}
// Retrieve projects from firebase
func get()
{
store.collection(FirestoreKeys.CollectionPath.projects)
.addSnapshotListener { querySnapshot, error in
if let error = error {
print("Error getting projects: \(error.localizedDescription)")
return
}
self.projects = querySnapshot?.documents.compactMap{ document in
try? document.data(as: Project.self)
} ?? []
}
}
// Add projects to firebase
func add(_ project: Project)
{
do {
_ = try store.collection(FirestoreKeys.CollectionPath.projects).addDocument(from: project)
} catch {
fatalError("Unable to add card: \(error.localizedDescription)")
}
}
}
This is my project model:
struct Project: Identifiable, Codable
{
#DocumentID var id: String?
var title: String
var image: String
#ServerTimestamp var startDate: Date?
#ServerTimestamp var endDate: Date?
var tasks: [Task]
}
And this is my task model:
struct Task: Identifiable, Codable
{
#DocumentID var id: String?
var title: String
var done: Bool
}
Finally this is how I am trying to show the tasks:
ScrollView {
ForEach(projectViewModel.project.tasks) { task in
HStack {
Image(task.done ? "checkmark-filled" : "checkmark-unfilled")
RoundedRectangle(cornerRadius: 20)
.foregroundColor(.white)
.frame(height: 72)
.shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 4)
.overlay(Text(task.title))
.padding(.leading)
}
}
.padding()
}
I figured it out. It was because the task needed a unique ID and it didn't have a document ID.
I replaced the
#DocumentID var id: String?
with
var id: String? = UUID().uuidString
And added an id field to the tasks in Firestore.
I then showed the tasks in the list by calling
ForEach(projectViewModel.project.tasks, id: \.id) { task in
(Insert code here)
}
Hi I want to refresh the detail view which is triggered by data change under the master view
My approach is setup a Combine listener and trigger the refresh from the detail view
It was working on ios13 but I can't make it working on ios14.2, any workaround?
Here is my master view.
import SwiftUI
struct MainView: View {
#ObservedObject var viewModel : MainViewModel
init(){
viewModel = MainViewModel.shared
}
var body: some View {
NavigationView{
List {
ForEach(viewModel.games ,id:\.self) { game in
NavigationLink(destination: self.navDest(game: game)){
Text("\(game)")
}
}
}
}
}
func navDest(game: Int) -> some View{
print("games value:",game)
return LazyView(DetailView(game: game ))
}
}
The master view model listen to the event come from detail view and update the value
import Foundation
import SwiftUI
import Combine
class MainViewModel : ObservableObject {
#Published var games : [Int] = []
static let shared = MainViewModel()
private var tickets: [AnyCancellable] = []
init(){
for i in 0...5{
games.append(i)
}
addObservor()
}
func addObservor(){
NotificationCenter.default.publisher(for: .updateGame)
.map{$0.object as! Int}
.sink { [unowned self] (game) in
self.updateGame(game: game)
}.store(in: &tickets)
}
func updateGame(game:Int){
print("updateView index:",game)
self.games[game] = 9999
print("after update",games)
}
}
import SwiftUI
struct DetailView: View {
#ObservedObject var viewModel : DetailViewModel
init(game: Int) {
print("init detail",game)
viewModel = DetailViewModel(game:game)
}
var body: some View {
VStack{
Text("\(viewModel.game)")
Button("update"){
viewModel.sendUpdate()
}
}
}
}
When I click the update from detail view, it should refresh the master and detail view at the same time.(The flow is DetailViewModlel->MainViewModlel->Refresh MainView ->Refresh DetailView (which is currently display)) But it not work on iOS 14.2
import Foundation
import SwiftUI
class DetailViewModel : ObservableObject {
var period : String = ""
var game : Int = 0
// init(period:String,game: Int){
// self.period = period
init(game: Int){
self.game = game
}
func sendUpdate(){
NotificationCenter.default.post(name: .updateGame, object: game)
}
}
extension Notification.Name {
static let updateGame = Notification.Name("updateGame")
}
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
DetailView
import SwiftUI
struct DetailView: View {
#ObservedObject var viewModel : DetailViewModel
init(game: Int) {
print("init detail",game)
viewModel = DetailViewModel(game:game)
}
var body: some View {
VStack{
Text("\(viewModel.game)")
Button("update"){
viewModel.sendUpdate()
}
}
}
}
I have logged down the debug message, it seem like the data did changed but the view didn't.
My Code fetches two JSON variables and should show them on my Widget.
The Widget stays blank. Without widget it shows me everything correct in my application.
What am I doing wrong? The API in the code is only for testing so you can also check that.
Is there something I need to change to make it show in the widget?
My Struct:
import Foundation
struct Results: Decodable {
let data: [Post]
}
struct Post: Decodable, Identifiable {
let id: String
var objectID: String {
return id
}
let home_name: String
let away_name: String
}
Fetching JSON:
import Foundation
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
#Published var test = ""
#Published var test2 = ""
func fetchData() {
if let url = URL(string: "https://livescore-api.com/api-client/teams/matches.json?number=10&team_id=19&key=I2zBIRH3S01Kf0At&secret=6kLvfRivnqeNKUzsW84F0LISMJC1KdvQ&number=7&team_id=46") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (gettingInfo, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = gettingInfo {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.data
self.test = results.data[0].away_name
self.test2 = results.data[0].home_name
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
Showing Widget:
import WidgetKit
import SwiftUI
import Intents
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)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
struct WidgetNeuEntryView : View {
#ObservedObject var networkManager = NetworkManager()
var entry: Provider.Entry
var body: some View {
Text(networkManager.test)
}
}
#main
struct WidgetNeu: Widget {
let kind: String = "WidgetNeu"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetNeuEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct WidgetNeu_Previews: PreviewProvider {
static var previews: some View {
WidgetNeuEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
networkManager.test should be shown as text but as I said it is blank.
You can't use the ObservedObject like you'd normally use in your App.
In Widgets you use a TimelineProvider which creates an Entry for your view.
Add another property to your TimelineEntry, let's call it clubName:
struct SimpleEntry: TimelineEntry {
let date: Date
let clubName: String
}
Update the NetworkManager and return results in the completion:
class NetworkManager {
func fetchData(completion: #escaping ([Post]) -> Void) {
...
URLSession(configuration: .default).dataTask(with: url) { data, _, error in
...
let result = try JSONDecoder().decode(Results.self, from: data)
completion(result.data)
...
}
.resume()
}
}
Use the NetworkManager in the TimelineProvider and create timelines entries when the fetchData completes:
struct Provider: TimelineProvider {
var networkManager = NetworkManager()
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), clubName: "Club name")
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> Void) {
let entry = SimpleEntry(date: Date(), clubName: "Club name")
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> Void) {
networkManager.fetchData { posts in
let entries = [
SimpleEntry(date: Date(), clubName: posts[0].home_name)
]
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
}
}
Use entry.clubName in the view body:
struct WidgetNeuEntryView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.date, style: .time)
Text("Club: \(entry.clubName)")
}
}
}
Note that in the above example the reload policy is set to never to only load the data once.
You can easily change it to atEnd or after(date:) if you want to reload the timeline automatically.
If you need to reload the timeline manually at any point you can just call:
WidgetCenter.shared.reloadAllTimelines()
This will work in both App and Widget.
Here is a GitHub repository with different Widget examples including the Network Widget.
I think I'm going about this SwiftUI thing all wrong. It's clear that we're just defining the layout as a structs and there can be limited conventional programming embroiled in the layout. I'm having difficulties thinking like this. What is the best way of doing this?
Take the example below. Project is an NSManagedObject. All I want to do is pass in example record so the SwiftUI will render. Nothing I try works.
struct ProjectView: View
{
#State var project: Project //NSManagedObject
var body: some View
{
TextField("", text: Binding<String>($project.projectName)!)
}
}
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
var p:Project
p = getFirstProject() //returns a Project
return ProjectView(project: p)
}
}
If I try returning the struct it says it cannot preview in the file.
If I don't return the struct I get a Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type error.
UPDATE:
var app = UIApplication.shared.delegate as! AppDelegate
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var persistentContainer: NSPersistentCloudKitContainer = {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var managedObjectContext: NSManagedObjectContext =
{
return persistentContainer.viewContext
}()
}
And the rest of the code:
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func getCount() -> String
{
let r = allRecords(Project.self)
return String(r.count)
}
struct ProjectView: View
{
// #ObservedObject var project: Project
var body: some View
{
Text(getCount())
// TextField("", text: Binding<String>($project.projectName)!)
}
}
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
ProjectView()
}
}
r.count is returning 0, but in the main application thread it is returning 8. Has app.managedObjectContext not been defined properly? I think this has just got too complicated too quickly.
Assuming getFirstProject works correctly the following should work
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
ProjectView(project: getFirstProject())
}
}
However there are concerns about the following...
struct ProjectView: View
{
#State var project: Project //NSManagedObject
because #State is designed to be internal view state-only thing, but Project in your case is a model, so the recommended scenario for this is to use ObservableObject view model either by conforming Project or as standalone clue class holding Project instance(s).
Scenario:
I'm using an Observable Class to acquire data from the network.
In this case some elementary weather data.
Problem:
I don't know how to display this data in the calling View.
For the time-being, I merely am trying to populate a Textfield (and worry about more-eleborate layout later).
I get the following:
.../StandardWeatherView.swift:22:13: Cannot invoke initializer for
type 'TextField<_>' with an argument list of type '(Text, text:
Sample?)'
Here's is my calling View which is the receiver of #ObservedObject data:
import SwiftUI
struct StandardWeatherView: View {
#EnvironmentObject var settings: Settings
#ObservedObject var standardWeatherReportLoader = StandardWeatherReportLoader()
init() {
self.standardWeatherReportLoader.doStandard()
}
var body: some View {
ZStack {
Color("FernGreen").edgesIgnoringSafeArea(.all)
TextField(Text("Weather Data"), text: standardWeatherReportLoader.weatherReport)
}
}
}
struct StandardWeatherView_Previews: PreviewProvider {
static var previews: some View {
StandardWeatherView()
}
}
Here's the publisher, acquiring data:
import Foundation
class StandardWeatherReportLoader: ObservableObject {
#Published var networkMessage: String?
#Published var hasAlert = false
#Published var weatherReport: Sample?
#Published var hasReport = false
func doStandard() {
let url = EndPoint.weather.path()
var request = URLRequest(url: EndPoint.weather.path()!)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: url!) { (data: Data?, _: URLResponse?, error: Error?) -> Void in
DispatchQueue.main.async {
guard error == nil else {
self.networkMessage = error?.localizedDescription
self.hasAlert = true
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Sample.self, from: data!)
self.weatherReport = result
self.hasReport = true
print("\n Standard Weather ----------------")
print(#function, "line: ", #line, "Result: ",result)
print("\n")
} catch let error as NSError {
print(error)
}
}
}
task.resume()
}
}
What's the simplest way of passing a string of data to the View via #Published var?
Log:
Standard Weather ---------------- doStandard() line: 38 Result:
Sample(coord: DataTaskPubTab.Coord(lon: -0.13, lat: 51.51), weather:
[DataTaskPubTab.Weather(id: 300, main: "Drizzle", description: "light
intensity drizzle")], base: "stations", main:
DataTaskPubTab.Main(temp: 280.32, pressure: 1012, humidity: 81,
tempMin: 279.15, tempMax: 281.15), visibility: 10000, wind:
DataTaskPubTab.Wind(speed: 4.1, deg: 80), clouds:
DataTaskPubTab.Clouds(all: 90), dt: 1485789600.0, id: 2643743, name:
"London")
But I'm getting nil at the TextField:
(lldb) po standardWeatherReportLoader.weatherReport nil
One option is to set a binding within your body to track whenever the TextField has updated. From within this binding, you can then edit your Published variable as you wish:
#ObservedObject var reportLoader = StandardWeatherReportLoader()
var body: some View {
// Binding to detect when TextField changes
let textBinding = Binding<String>(get: {
self.reportLoader.networkMessage
}, set: {
self.reportLoader.networkMessage = $0
})
// Return view containing the text field
return VStack {
TextField("Enter the Network Message", text: textBinding)
}
}
Edit: Also in your original post, you were passing an object of optional type Sample into the TextField which was expecting a binding String type which could cause some issues.