What is the difference between `FileDocument` and `ReferenceFileDocument` for `DocumentGroups` in SwiftUI? - swiftui

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.

Related

How to fetch Coredata List in IntentHandler class (WidgetKit-SwiftUI)?

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

In Kotlin, how can one unit test that a data class, sealed class, or class field are not changed?

Basically we might have some data like so
open class RealmCart : RealmObject() {
#PrimaryKey
var _id: String = UUID.randomUUID().toString()
var items: RealmList<RealmCartItem> = RealmList()
var discountCode: String? = null
var userId: String = ""
}
And we do not want people editing these by mistake. We have some failsafe like code owners, labels in the repo, but we also want to have a unit test that can also prevent a merge if the data is changed in any way (add, change, or remove data). Basically, we do not want any accidents, and people are not perfect.
What is the best way to go about such a thing?
This is what I ended up doing:
I created an extension function for my data models
fun RealmObject.testDeclaredFields(): List<String> {
val fields = this::class.java.fields.map { it.name }
return this::class.java.declaredFields
.map { it.name }
.filterNot { fields.contains(it) }
.sorted()
}
Basically this just gets the data model fields, excluding things like companion objects.
Then I was able to create a test simply like
class RealmMessageTest {
#Test
fun `RealmMessage fields match spec`() {
val item = RealmMessage().testDeclaredFields()
assertContentEquals(item, fieldSpec)
}
private val fieldSpec = listOf(
"_id",
"acknowledgeStatusValue",
"body",
"completed",
"createdAt",
"deliveryStatusValue",
"from",
"meta",
"organizationId",
"platforms",
"threadId",
"title"
).sorted()
}
Why do this? Sometimes when someone is making changes carelessly, they will not realize that they have added a field, changed a field, or removed an important field in a data model that is sync'd to the backend. This does not prevent the developer from changing it, but given that they need to now change it in two places, they will be more cognizant whether they need to make this change or not.
I noticed a lot of people questioned why you would need to do this. My answer is that, I work in a very large repo where newer developers edit this without a second thought. This is just to make them more cognizant of changes to these important models, before they break develop. There are code owners for the repo, but they may not always see these changes. It is just an extra precaution.
How about using a mechanism like githooks to prevent the editing of certain files from being committed?
I'm not familiar with githooks, so I can't show you exactly how to do it, but I think it would be good to prevent commits and inform the developer of the situation with an error message.

SwiftUI and Combine

I'm following a video on the Firebase YouTube channel. Starting around 27:45, the instructor is trying to set a variable based on a Boolean and ends up with the following code in init(task: Task):
$task
.map { task in
task.isCompleted ? "checkmark.circle.fill" : "circle"
}
.assign(to: \.completionStateIconName, on: self)
.store(in: &cancellables)
This seems overly convoluted to me. First, I can't find documentation on using .map on a struct object, only on arrays, etc. Second, what is with this &cancellables thing? (It's defined as private var cancellables = Set<AnyCancellable>() before the init{}.) Third, why all this code, and not simply:
task.completionStateIconName = task.isCompleted ? "checkmark.circle.fill" : "circle"
This seems to give the same result, but will there be something down the line that the first code fragment works, but the second doesn't?
$task (with the $ prefix) is a projected value of the #Published property wrapper, and it returns a variable of the type Published.Publisher. In other words, its a Combine publisher, which publishes a value whenever the property - in this case Task - changes.
If you didn't learn about the Combine framework (or other reactive frameworks), this answer is definitely not going to be enough. At a high-level, a Combine publisher emits values, which you can transform through operators like .map, and eventually subscribe to, for example with .sink or .assign.
So, line-by-line:
// a publisher of Task values
$task
// next, transform Task into a String using its isCompleted property
.map { task in
task.isCompleted ? "circle.fill" : "circle"
}
// subscribe, by assigning the String value to the completionStateIconName prop
.assign(to: \.completionStateIconName, on: self)
Now, the above returns an instance of AnyCancellable, which you need to retain while you want to receive the values. So you either need to store it directly as a property, or use .store to add it to a Set<AnyCancellable> - a common approach.
So, why is it so convoluted? This is, presumably, built so that if task property ever changes, the Combine pipeline would update the completionStateIconName property.
If you just did:
completionStateIconName = task.isCompleted ? "circle.fill" : "circle"
that would assign the value just in the beginning.
That being said, in this particular case it might actually be unnecessarily too convoluted to use Combine, whereas just using didSet:
var task: Task {
didSet {
completionStateIconName ? task.isCompleted ? "circle.fill" : "circle"
}
}

How do I get the Code Review Id in a custom section of the code review page?

I am writing a visual studio extension that has a section on the code review page. I would like access to the information about the rest of the code review page, specifically what code review is current on the page being displayed. I should be able to access the workitemId but so far I have not figured out how.
Edit
Using Cole's suggestion I have accessed the PageContent but I do not know what type I should cast the content to. Nor do I know when it will be available. I would like access both when I initialize my section, and later. Here is my code when I try to initialize the section:
public override object SectionContent
{
get
{
if (base.SectionContent == null)
{
var teamExplorerPage = this.GetService<ITeamExplorerPage>();
var pageContent = teamExplorerPage.PageContent;
this.model.CodeReviewId = pageContent;
base.SectionContent = new CodePlusTeamExplorerSectionView(this.ServiceProvider, this.model);
}
return base.SectionContent;
}
}
When I debug the code I see that a DataContext is available on the PageContent, but I do not know what type to cast the pageContent (orITeamExplorerPage) to, to gain access to it. Also the DataContext has a CodeReviewId property which seems like the value I need but it is null at this point of the Lifecycle. If I want to retrieve some additional data based on the CodeReviewId when/where is it available?
I am still trying to figure out how to get it earlier in the lifecycle of the page but from an event like a button click in the page I can retrieve the value using reflection. This seems like a bit of a hack, perhaps someone else has a better way, but here it is.
private void Button_Click(object sender, RoutedEventArgs e)
{
var teamExplorer = this.GetService<ITeamExplorer>();
var currentPage = teamExplorer.CurrentPage;
var currentContent = currentPage.PageContent;
Type type = currentContent.GetType();
PropertyInfo pi = type.GetProperty("DataContext");
var dataContext = pi.GetValue(currentContent);
Type contextTypetype = dataContext.GetType();
PropertyInfo contextTypePi = contextTypetype.GetProperty("CodeReviewId");
var codeReviewId = contextTypePi.GetValue(dataContext);
var context = new Dictionary<string, object>();
context.Add("WorkItemId", codeReviewId);
teamExplorer.NavigateToPage(new Guid(TeamExplorerPageIds.ViewCodeReview), context);
}

Creating mock cloud kit objects for unit testing

Trying to mock CKDatabase but it has no accessible initializer. Is there another way to create a fake CKDatabase object?
This code comes back with the "error: cannot override 'init' which has been marked unavailable"
class MockDatabase : CKDatabase
{
override func saveRecord(record: CKRecord!, completionHandler: ((CKRecord!, NSError!) -> Void)!) { }
override func deleteRecordWithID(recordID: CKRecordID!, completionHandler: ((CKRecordID!, NSError!) -> Void)!) { }
override func fetchRecordWithID(recordID: CKRecordID!, completionHandler: ((CKRecord!, NSError!) -> Void)!) { }
override func deleteSubscriptionWithID(subscriptionID: String!, completionHandler: ((String!, NSError!) -> Void)!) { }
override func saveSubscription(subscription: CKSubscription!, completionHandler: ((CKSubscription!, NSError!) -> Void)!) {}
}
The cloud kit framework seems like a perfect candidate for unit testing as there are a lot of possible errors that can come back from the server and making sure your app can handle them all consistently , with every iteration would be great. Has anyone found a way around this?
Since CKDatabase init is made unavailable, you will never be able to create an instance of it. So there is no point in creating your MockDatabase because you will never be able to create an instance of that. The only way to do some sort of Mocking is by creating your own DAO class wrapper around the CKDatabase functionality and only call CKDatabase functions using that DAO. Then you could mock that DAO. You could even add extra functionality to that DAO, but then you will not be able to test that.
All the CloudKit classes are #objc (they all derive from NSObject.) That means they use dynamic dispatching. In Objective-C you could mock CKDatabase by implementing a new Obj-C class MockCKDatabase that subclasses NSObject and implements all the public methods of CKDatabase (or at least all the ones you use.) Then you literally just instantiate one and cast the pointer from MockCKDatabase* to CKDatabase*.
I've never done this sort of thing in Swift, but I imagine that if you just did the same steps it should work.
as #Edwin pointed out, CKDatabase init is made unavailable, so you can't make instances of it. Neither can you subclass or use an extension to override CKContainer init (compiler will complain that 'init()' is unavailable). You can't even use a CKContainer with a mock of CKDatabase, since the logic that associates them is hidden within the CloudKit implementation. All that said, I built a framework called MockCloudKitFramework that is a drop-in replacement for a limited (but reasonable) subset of CloudKit capabilities. The MockCloudKitFramework (MCF) implements mock operations for CKContainer and CKDatabase, plus the main subclasses of CKDatabaseOperation.
MCF creates its own protocols (that contain the same signatures as their CloudKit counterparts) and extends both CloudKit and MCF mocks from this set of protocols. It was necessary to create these protocols since CKContainer and CKDatabase both inherit directly from NSObject as their common Protocol. So that IOC would force generic functions to be open to NSObject types - leaving functions wide open for injection of everything that inherits from NSObject.
So using MCF comes down to:
Extending CloudKit and MCF classes from these protocols.
Refactoring your code to use generics - typed on the MCF protocols.
Using IOC to inject the mocks of CKContainer and CKDatabase (MockCKContainer and MockCKDatabase, respectively).
See the MCF docs - examples are provided, along with unit and integration tests.
Using MCF
Lets say that you have a generic class that takes a type based on the CKContainerProtocol protocol (one of the MCF protocols). It contains a method that accepts either a CKFetchRecordsOperation or a MockCKFetchRecordsOperation (the MCF mock of CKFetchRecordsOperation) based on their common MCF protocol, CKFetchRecordsOperationProtocol:
import CloudKit
/// Example Class to handle iCloud related transactions.
class CloudController<T: CKContainerProtocol> {
let cloudContainer: T
let database: T.DatabaseType
init(container: T, databaseScope: CKDatabase.Scope) {
self.cloudContainer = container
self.database = container.database(with: databaseScope)
}
/// Check if a record exists in iCloud.
/// - Parameters:
/// - cKFetchRecordsOperation: the fetch operation
/// - recordId: the record id to locate
/// - completion: closure to execute on caller
/// - Returns: success(true) when record is located, success(false) when record is not found, failure if an error occurred.
func checkCloudRecordExists<O: CKFetchRecordsOperationProtocol> (
cKFetchRecordsOperation: O,
recordId: CKRecord.ID,
_ completion: #escaping (Result<Bool, Error>) -> Void) {
var dbOperation = cKFetchRecordsOperation
dbOperation.recordIDs = [recordId]
var record: CKRecord?
dbOperation.desiredKeys = ["recordID"]
// perRecordResultBlock doesn't get called if the record doesn't exist
dbOperation.perRecordResultBlock = { _, result in
// success iff no partial failure
switch result {
case .success(let r):
record = r
case .failure:
record = nil
}
}
// fetchRecordsResultBlock always gets called when finished processing.
dbOperation.fetchRecordsResultBlock = { result in
// success if no transaction error
switch result {
case .success:
if let _ = record { // record exists and no errors
completion(.success(true))
} else { // record does not exist
completion(.success(false))
}
case .failure(let error): // either transaction or partial failure occurred
completion(.failure(error))
}
}
database.add(dbOperation)
}
}
We can build a test without MCF that validates that the record does (or doesn't) exist, but first we have to:
Populate (or depopulate) the CloudKit test environment with the data to set the test up.
Make sure that we are using the same CKDatabase scope (public, private, or shared) as where the data is.
Hope that there are no network hiccups or data transfer errors that will inject unanticipated errors or interruptions.
But still, we can't test what the error conditions might be since we have no control over them.
MCF allows us to test all of this, using a local mock instance of CKDatabase (MockCKDatabase) that we can manipulate directly or through CloudKit modification operations like (Mock)CKModifyRecordsOperation. MockCKContainer is used to access the MockCKDatabase instance of the appropriate scope using the same accessors as CKContainer does. Here is an example of a test case setup:
import XCTest
import CloudKit
#testable import OurProject // required for access to CloudController
import MockCloudKitFramework // import MCF only into test environment
class MockCloudKitTestProjectIntegrationTest: XCTestCase {
var cloudContainer: MockCKContainer!
var cloudDatabase: MockCKDatabase!
// NOTE that CloudController class is a Generic and that both CKContainer and MockCKContainer extend CKContainerProtocol
var cloudController: CloudController<MockCKContainer>!
// called before each test
override func setUpWithError() throws {
try? super.setUpWithError()
// reset state for each test. Each test must set up exactly the state they require.
MockCKContainer.resetContainer()
// get the default container from MCF
cloudContainer = MockCKContainer.default()
// get the MockCKDatabase of CKDatabase.Scope of private
cloudDatabase = cloudContainer.privateCloudDatabase
// we pass in a MockCKContainer to the generic function expecting a class that extends MCF CKContainerProtocol
cloudController = CloudController(container: cloudContainer, databaseScope: .private)
}
}
Now we can test the checkCloudRecordExists() method by adding the record that we expect to fetch directly to MockCKDatabase and calling the function, passing in our mock CKFetchRecordsOperation:
func test_checkCloudRecordExists_success() {
let expect = expectation(description: "CKDatabase fetch")
let record = makeCKRecord() // function that makes a CKRecord
// add the record to MockCKDatabase
cloudDatabase.addRecords(records: [record])
let operation = MockCKFetchRecordsOperation()
// pass in our mock CKFetchRecordsOperation
cloudController.checkCloudRecordExists(cKFetchRecordsOperation: operation, recordId: record.recordID) { exists in
XCTAssertTrue(exists)
expect.fulfill()
}
waitForExpectations(timeout: 1)
}
If we wanted to test an error condition from the call, we just need to add an error using the setError property of MockCKDatabaseOperation (or any of its subclasses). Now the test will fail, even though the record exists. This simulates an internal error that was the result of a CloudKit transaction:
// call checkCloudRecordExists() when the record is present but error is set
func test_checkCloudRecordExists_error() {
let expect = expectation(description: "CKDatabase fetch")
let record = makeCKRecord()
cloudDatabase.addRecords(records: [record])
let operation = MockCKFetchRecordsOperation()
// set an error on operation
let nsErr = createNSError(with: CKError.Code.internalError)
operation.setError = nsErr
cloudController.checkCloudRecordExists(cKFetchRecordsOperation: operation, recordId: record.recordID) { result in
switch result {
case .success:
XCTFail("should have failed")
expect.fulfill()
case .failure(let error):
XCTAssertEqual(error.createCKError().code.rawValue, nsErr.code)
expect.fulfill()
}
}
waitForExpectations(timeout: 1)
}
We can even test our function logic for partial failures to make sure that we handle the scenario of when a record might be found but some CKError occurred so we cannot be sure. All we need to do is set the setRecordErrors property on the operation to the set of record ids that should fail (MCF picks a random CKError to fail with):
// test for partial failures
func test_checkCloudRecordExists_partial_failure() {
let expect = expectation(description: "CKDatabase fetch")
let record = makeCKRecord()
cloudDatabase.addRecords(records: [record])
let operation = MockCKFetchRecordsOperation()
// set an error on operation
operation.setRecordErrors = [record.recordID]
cloudController.checkCloudRecordExists(cKFetchRecordsOperation: operation, recordId: record.recordID) { result in
switch result {
case .success:
XCTFail("should have failed")
case .failure(let error):
let ckError = error.createCKError()
XCTAssertEqual(ckError.code.rawValue,
CKError.partialFailure.rawValue,
"The transaction error should always be set to CKError.partialFailure when record errors occur")
if let partialErr: NSDictionary = error.createCKError().getPartialErrors() {
let ckErr = partialErr.allValues.first as? CKError
XCTAssertEqual("CKErrorDomain", ckErr?.toNSError().domain)
expect.fulfill()
}
}
}
waitForExpectations(timeout: 1)
}