#main
struct ClockWidgetExt: Widget {
private let kind: String = "ClockWidgetExt"
public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in
HomeTestView()
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
How can I get data from my Main App to the widget?
You can add the AppGroup capability for both your Widget and App (here is a very good explanation how to add it).
UserDefaults
Instead of
UserDefaults.standard
just use the shared UserDefaults for your AppGroup:
UserDefaults(suiteName: <your_app_group>)
Then you can read/write data like explained in this answer.
File Container
With the AppGroup entitlement you get access to the shared File Container:
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: <your_app_group>)!
and access an url like this:
let someFileURL = containerURL.appendingPathComponent("SomeFile.txt")
Then you can use your shared File Container like explained in this answer:
How to read files created by the app by iOS WidgetKit?
CoreData
You can create a shared CoreData container as well:
let storeURL = containerURL.appendingPathComponent("DataModel.sqlite")
let description = NSPersistentStoreDescription(url: storeURL)
let container = NSPersistentContainer(name: "DataModel")
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { ... }
Then you can use your shared CoreData Container like explained in this answer:
Fetch data from CoreData for iOS 14 widget
Here is a GitHub repository with different Widget examples including the App Group Widget.
One simple way to do this is by adding both the app and the widget to the same App Group, and then storing and retrieving data from UserDefaults storage located in that App Group's container instead of the default UserDefaults storage for the app.
You can access this shared storage for your App Group by initializing UserDefaults using
UserDefaults(suiteName: "YOUR_APP_GROUP_NAME")
instead of accessing it using
UserDefaults.standard.
This article gives a more thorough description of the details of this process.
Related
I am not able to fetch data in my IntentHandler class. My Goal is select Todo from the CoreData list and display it in Widget.
I am trying to display a list from CoreData in Widget Intent and I am expecting to resolve this issue.
extension IntentHandler : ConfigurationIntentHandling {
func provideTodoNameOptionsCollection(for intent: ConfigurationIntent, searchTerm: String?, with completion: #escaping (INObjectCollection<TodoData>?, Error?) -> Void) {
var arrTodoData = [TodoData]()
coreDH.getAllTodos().forEach { todos in
let todoIntent = TodoData(identifier: todos.id?.uuidString, display: todos.name ?? "")
arrTodoData.append(todoIntent)
}
let collection = INObjectCollection(items: arrTodoData)
completion(collection, nil)
}
}
class IntentHandler: INExtension{
let coreDH = CoreDataHandler.shared
override func handler(for intent: INIntent) -> Any {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.
return self
}
}
If your IntentHandler doesn't get called:
Ensure your Widget uses IntentConfiguration not StaticConfiguration
Ensure your time line provider conforms to IntentTimelineProvider
Run the app scheme, then run the intent scheme, you should be able to debug and breakpoints would work.
Widgets Code-along, part 3: Advancing timelines (5:31)
Add Configuration and Intelligence to Your Widgets
If you need to share data between your app and extension:
App's data is sandboxed and is not accessible by extension
Configure App Groups and you could create core data file in the shared container to be able to access in your extension
If App and Extension use exactly the same data you could use the same sqlite file in the shared container (both app and extension would have access to it)
If App and Extension use different data and there is only a small portion that is common, then use History Tracking
Configuring App Groups
Consuming Relevant Store Changes
I have made a SwiftUI DocumentApp that reads large media files but doesn't need to write them. In my document, I just want to store the file's URL, so that I can load it using e.g. AVAudioFile. I can't work out how to do this without creating a temporary copy of the file, as the author does here, because there is no way to the file URL from the fileWrapper
I have attempted to create a pipe using the configuration.file.regularFileContents (Data), but so far I had no success reading from that pipe.
init(configuration: ReadConfiguration) throws {
let tempURL = makeTemporaryFileURL()
FileManager.default.createFile(atPath: tempURL.path, contents: configuration.file.regularFileContents, attributes: nil)
audioFileUrl = tempURL
}
You can get fileURL via document configuration, like
#main
struct MyApp: App {
var body: some Scene {
DocumentGroup(viewing: MyDocument.self) { fileConfig in
// here fileConfig has `fileURL` of opened document, so
MyViewer(content: fileConfig.document, url: fileConfig.fileURL)
}
}
}
I'm trying to setup a DocumentGroup in my app, but there's no examples out there yet ReferenceFileDocument is for. I know what a FileDocument is, but how are ReferenceFileDocuments different.
In the docs all it says is:
Conformance to ReferenceFileDocument is expected to be thread-safe,
and deserialization and serialization will be done on a background
thread.
There's a hint in the name: ReferenceFileDocument is a document that's a reference type (ie, a class). FileDocument is for a struct based document.
This has an effect on how documents are saved because SwiftUI can just make a copy of the reference type and save it without worrying about you coming along and modifying it during the save, since it's a value type or tree of value types.
With ReferenceFileDocument, there also doesn't seem to be a clear way for the SwiftUI to know when to save, so it depends on you telling it. There's no direct "doc is dirty, save it now" method, so the way you inform SwiftUI that you've done something that requires saving is through the undo manager.
You also need to provide a snapshot method to return a copy of the document that's safe for it to save.
final class QuizDocument: ReferenceFileDocument, ObservableObject {
#Published var quiz: QuizTemplate
init(quiz: QuizTemplate) {
self.quiz = quiz
}
static var readableContentTypes: [UTType] { [.exampleText] }
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let quiz = try? JSONDecoder().decode(QuizTemplate.self, from: data)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.quiz = quiz
}
// Produce a snapshot suitable for saving. Copy any nested references so they don't
// change while the save is in progress.
func snapshot(contentType: UTType) throws -> QuizTemplate {
return self.quiz
}
// Save the snapshot
func fileWrapper(snapshot: QuizTemplate, configuration: WriteConfiguration) throws -> FileWrapper {
let data = try JSONEncoder().encode(quiz)
return .init(regularFileWithContents: data)
}
}
ReferenceFileDocument is a document type that will auto-save in the background. It is notified of changes via the UndoManager, so in order to use it you must also make your document undo-able.
The only mention I see of it in the docs is here.
Here is a working example.
I have an ObservableObject with a few publishers:
private class ViewModel: ObservableObject {
#Published var top3: [SearchResult] = []
#Published var albums: [SearchResult.Album] = []
#Published var artists: [SearchResult.Artist] = []
}
The endpoint is a URLSessionDataPublisher that sends a single collection of values that can be either an album or an artist (there are actually more types but I'm reducing the problem set here.) What is the best way in Combine to separate this collection out into 3 collections: [Album], [Artist], and an array of 3 results that can be either Artist or Album?
DatabaseRequest.Search(for: searchTerm)
.publisher()
// now i want to separate the collection out into [Album] and [Artist] and assign to my 3 #Published vars
.receive(on: DispatchQueue.main)
.sink { }
.store(in: bag)
You are hitting a bit of a (common) fallacy that Combine is responsible for passing the changed data in SwiftUI. It isn't. The only thing Combine is doing here is providing the content-less message that some data has changed, and then the SwiftUI components that are using the relevant model object go and look for their data.
The data transfer in SwiftUI is entirely using Binding, which are essentially get and set closures under the covers.
So you don't really need to worry about demuxing a combine stream - and there isn't one that has "one" of these kinds of data in it. Combine would have trouble with that since it's strongly typed for both Output type and Failure type.
There's a bit more written about this in Using Combine under the chapter
SwiftUI and Combine (chapter link to the free HTML version)
Im trying to fetch entities from my coredata database of my main app and display them in my apps widget Extention. However the fetch always returns an empty results from my database. Can ayone help me out. Swift 3, ios 10.
I solved it. After creating the App Group, I created a subclass of NSPersistentContainer and override the class method defaultDirectory() to return the shared app group directory. Also override the init(name: String, managedObjectModel model: NSManagedObjectModel). Then in the coredata stack you replace the boilerplate persistentcontainer code with a new instance of the PersistentContainer class created.
import UIKit
import CoreData
class PersistentContainer: NSPersistentContainer{
override class func defaultDirectoryURL() -> URL{
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bundleId.SomeApp")!
}
override init(name: String, managedObjectModel model: NSManagedObjectModel) {
super.init(name: name, managedObjectModel: model)
}
}
Then in the CoreDataStack Code *wherever that maybe, either in the Appdelegate or or its own file. Mine was in its own file named CoredataStack
static var persistentContainer:PersistentContainer = {
let container = PersistentContainer(name: "SomeApp", managedObjectModel: CoreDataStack.managedObjectModel)
container.loadPersistentStores(completionHandler: { (storeDescription:NSPersistentStoreDescription, error:Error?) in
if let error = error as NSError?{
fatalError("UnResolved error \(error), \(error.userInfo)")
}
})
return container
}()
Hope this helps out
Today Extension runs in a different process from its containing app, thus do not share sandbox, UserDefaults, or database tabled. That is why you get an empty set when trying to fetch data saved in container, from the widget.
If you want the containing app and the widget to share data, you should add to your app the capability "App Group" via the iOS developer portal and give it a shared group identifier string.
Next, when you instantiate the FileManager object for the database (or UserDefaults), you must use initWithSuiteName method to, passing in the identifier of the shared group.