While reading CkSyncEngine demo project code, I don't find the code to remove items in syncEngine.state.pendingRecordZoneChanges explicitly. I suspect it might occur in two possible places: nextRecordZoneChangeBatch() or ``nextRecordZoneChangeBatch()`, but I can't figure out how it occurs.
nextRecordZoneChangeBatch() has the following code:
let batch = await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: changes) { recordID in
if let contact = contacts[recordID.recordName] {
let record = contact.lastKnownRecord ?? CKRecord(recordType: Contact.recordType, recordID: recordID)
contact.populateRecord(record)
return record
} else {
// We might have pending changes that no longer exist in our database. We can remove those from the state.
syncEngine.state.remove(pendingRecordZoneChanges: [ .saveRecord(recordID) ])
return nil
}
}
(I'll ignore the syncEngine.state.remove(pendingRecordZoneChanges:) in the else clause, because it's unrelated)
Could it be that CKSyncEngine.RecordZoneChangeBatch.init(pendingChanges:,recordProvider:) automatically remove a CKRecord when the recordProvider: closure returns a non-nil value? I checked its document, but it doesn't say anything about this.
Thanks for any help.
Post
Replies
Boosts
Views
Activity
I have a quesiton on .accountChange handler code in CKSyncEngine demo project. Below is the code in handleAccountChange():
if shouldDeleteLocalData {
try? self.deleteLocalData() // This error should be handled, but we'll skip that for brevity in this sample app.
}
if shouldReUploadLocalData {
let recordZoneChanges: [CKSyncEngine.PendingRecordZoneChange] = self.appData.contacts.values.map { .saveRecord($0.recordID) }
self.syncEngine.state.add(pendingDatabaseChanges: [ .saveZone(CKRecordZone(zoneName: Contact.zoneName)) ])
self.syncEngine.state.add(pendingRecordZoneChanges: recordZoneChanges)
}
IMHO, when user switches account, the most important thing is to reload data from the new account's document folder. However, I can't see this is done anywhere. In above code, if shouldDeleteLocalData is false, self.appData would still hold the previous account's local data. That seems very wrong. Am I missing something?
It would be best if iOS restarts all applications when user switches account. If that's not the case (I guess so, otherwise there is no point to handle .accountChange in the app), I think application should implement an API to re-initialize itself.
EDIT: after looking at the code again, I realize that the following code makes sure shouldDeleteLocalData is always true when user switching accounts. So the code doesn't leak the previous account's data, though I still think it has an issue - it doesn't load the new account's data.
case .switchAccounts:
shouldDeleteLocalData = true
shouldReUploadLocalData = false
I'm considering using CloudKit in my app (it doesn't use Core Data) and have read as many materials as I can find. I haven't fully grasped it yet and have a basic question on CKRecord.Reference. Does CloudKit guarantee CKRecord.Reference value is always valid? By valid I mean the target CkRecord pointed by the CKRecord.Reference exists in the database.
Let's consider an example. Suppose there are two tables: Account and Transaction:
Account Table:
AccountNumber Currency Rate
------------- -------- ----
a1 USD 0.03
Transaction Table:
TransactionNumber AccountNumber Amount
----------------- ------------- ------
t1 a1 20
Now suppose user does the following:
User first deletes account a1 and its associated transactions t1 on device A. The device saves the change to cloud.
Then user adds a new transaction t2 to account a1 on device B, before the device receives the change made in step 1 from cloud. Since a1 hasn't been deleted on device B, the operation should succeed locally. The device tries to save the change to cloud too.
My questions:
Q1) Will device B be able to save the change in step 2 to cloud?
I hope it would fail, because otherwise it would lead to inconsistent data. But I find the following in CKModifyRecordsOperation doc (emphasis mine), which implies CloudKit allows invalid reference:
During a save operation, CloudKit requires that the target record of the parent reference, if set, exists in the database or is part of the same operation; all other reference fields are exempt from this requirement.
(BTW, I think the fact that, when using CloudKit, Core Data requires all relations must be optional also indicates that CloudKit can't guarantee relation is always valid, though I think that is mainly an issue on client side caused by data transfer size. The above example, however, is different in that it's an issue on cloud side - the data on cloud is inconsistent).
I also find the following in the document. However, I don't think it helps in the above example, because IIUC CloudKit can only detect conflict when the changes on the same record but the changes in step 1 and step 2 are on different records.
Because records can change between the time you fetch them and the time you save them, the save policy determines whether new changes overwrite existing changes. By default, the operation reports an error when there’s a newer version on the server.
If the above understanding is correct, however, I don't understand why the same document has the following requirement, which implies CloudKit doesn't allow invalid reference:
When creating two new records that have a reference between them, use the same operation to save both records at the same time.
Q2) Suppose CloudKit allows invalid reference on cloud side (that is, device B successfully saves the change in step 2 to cloud) , I wonder what's the best practice to deal with it?
I think the issue is different from the optional relation requirement in Core Data when using CloudKit, because in that case the data is consistent on cloud side and eventually the client will receive complete data. In the above example, however, the data on cloud is inconsistent so the client has to remedy it somehow (although client has little information helping it).
One approach I think of is to avoid the issue in the first place. My idea is to maintain a counter in the database and requires client to increase the counter (it's not Lamport clock. BTW, is it possible to use Lamport clock in this case?) when making any change. This should help CloudKit to detect conflict (though I can't think out a good strategy on how client should deal with it. A simple one is perhaps to prompt user to select one copy). However, this approach effectively uses cloud as a centralized server, which I suspect isn't the typical way how people use CloudKit, and it requires clients to maintain local counter value in various situations. I wonder what's the typical approach? Am I missing something?
Thanks for any help.
I installed Xcode 15 without downloading iOS 17 simulator. When I opened my iOS app project in it, Xcode showed "iOS 17.0 not installed". This is as expected. My question, however, is that after I connected my phone, it's shown in "Manage run destination", but not in Xcode deployment target selection list. Does anyone know if this is as expected? Do I have to install iOS 17 simulator to get my device shown in Xcode? Thanks.
After I downloaded Xcode 15, I use the following approach to download and install iOS 17 simulator:
Download iOS 17.0 simulator runtime from Apple website
Then run xcrun command to install it:
$ xcrun simctl runtime add iOS_17_Simulator_Runtime.dmg
(BTW, I can't install iOS simulator from within Xcode directly because downloading the image always failed. I have a fast Internet connection. The issue seems to be intermittent connection failure.)
The iOS simulator was installed successfully. However, I find the behavior of xcrun command is very confusing. Below is what I observed.
First this is xcrun command output:
$ xcrun simctl runtime add iOS_17_Simulator_Runtime.dmg
D: EEF873B8-E0E6-4A0E-9A1E-C4C7E6D3BE1A iOS (17.0 - 21A328) (Ready)
I suspect the output means it mounted the img somewhere. However, I can't find that disk in Finder. I checked the manually page and found the following:
add <path> [-ma]
Add a runtime disk image to the secure storage area. The image will be staged, verified, and mounted.
When possible the image file will be cloned so no additional disk space will be used.
If stdout is a terminal and a copy is required then progress will be reported.
I wonder what's that "secure storage area"? Is that the reason why I can't see the disk?
What's more confusing is that, shortly after the xcrun command completed, a GUI message box popped up, displaying a messsage "Verifying iOS 17.0.simruntime". This took a while. Then the message box disappeared and nothing more happended. At this time I opened Xcode and verified iOS similator was installed indeed.
I monitored disk IO during the entire process. I observed a lot of read ops but few write ops, which is odd. I understand why there were many read ops, but I expected the same amount of write ops also. Is it due to COW, plus the fact that the image is mounted rather than uncompressed?
The manual says "The image will be staged, verified, and mounted." I wonder what does "staged" mean in this case? Also, the mannual says the image is mounted. So my first thought was that I shouldn't move or delete the image file I downloaded. Then I noticed the manual also says "When possible the image file will be cloned so no additional disk space will be used." So I think it's OK to delete the image file? I'd like to confirm my understanding first so I haven't tried it yet.
Thanks for any explanation.
(This is a show stopper issue for me to use VStack or LazyVStack in my app. I posted the issue here on SO but got no reply. So I'll ask in this forum.)
What I'm trying to implement is a very basic behavior: I have some items in VStack and I'd like to scroll to some item when the VStack is shown. The issue is scrollTo() scrolls too much and leads to invalid UI. See diagram below.
Setup: there are 3 items in the VStack and I'd like to scroll to the last one.
+--------------+ +--------------+
| c | | a |
| | | b |
| | | c |
| | | |
| | | |
| | | |
| | | |
| | | |
+--------------+ +--------------+
(a) What I saw (b) What I expected
The UI in diagram a is invalid and misleading because 1) it's impossible for user to get such UI interactively, and 2) when user see it he would think there is only one item.
I have been investigating how to work around the issue but couldn't find one. I found two discussion that might be relevant, but both of them were about iOS 15. I don't find any discussion on a similar issue on iOS 16.
Below is the code to reproduce the issue. I filed FB12173661 yesterday. I wonder if anyone has a workaround? Thanks.
My environment: Xcode 14.2, iOS 16.3.1. I'm installing Xcode 14.3 and will verify it on iOS 16.4 soon.
struct Item: Identifiable {
var id: UUID = UUID()
var value: String
}
struct TestView: View {
var items: [Item]
var scrollTo: UUID
var body: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(items) { item in
Text(item.value)
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
proxy.scrollTo(scrollTo, anchor: .top)
}
}
}
}
}
}
struct ContentView: View {
var items: [Item] = [Item(value: "a"), Item(value: "b"), Item(value: "c")]
var body: some View {
TestView(items: items, scrollTo: items.last!.id)
.frame(maxWidth: .infinity)
}
}
I have recently released a new app, which is free to download and has a subscription to unlock a feature. When I go to "Promo Codes" page in App Store Connect, I find both "App Promo Codes" and "In-App Purchase Promo Codes" are list. I wonder what's the use of the former? This is a freemium app, so I think it's useless?
(was: App crashed in Xcode 14.x simulators on macOS 13.2.1 (reproduced consistently) but worked well on phone)
I upgraded my system to macOS 13.2.1 and Xcode 14.2 yesterday, and found my app crashed in simulators but worked well on my phone. Because it's unlikely my code issue (the same code worked fine on macOS 13.0 + Xcode 14.0 in the past a few months.), I tried Xcode 14.1 and Xcode 14.0.1 today, unfortunately they all crashed with same symptom. I suspect there was some change (or bug) in macOS 13.2.1 that caused the issue?
I find the following report yesterday: https://developer.apple.com/forums/thread/724033?answerId=745031022, which might be related (though it was about macOS 13.0.1).
I upgraded my system before I prepare App Store review materials (what a bad idea it was!). Now I'm not sure what I should do. I'm thinking to submit the binaries that crashed in simulators because I believe it's a simulator issue on the specific OS version. Does anyone know if this is OK? My concern is that App Store automated test probably use simulators too (though I don't understand how Apple didn't catch the crash issue in the first place). Any suggestions would be appreciated.
While submitting a new app, I noticed I could choose to make the app available on Apple Silicon Mac. Since user can access the file system on the Mac, I wonder how are app's data files are protected? Are they also in sandbox and can't be accessed through file system by macOS user?
Because my laptop runs on Intel CPU, I can't experiment it myself. I googled and found this page: https://developer.apple.com/documentation/xcode/configuring-the-macos-app-sandbox/, but the app sandbox seems to be mainly about protecting macOS from the iOS app, not the vice versa?
Does anyone knows the details? Thanks.
I'm testing my app's subscription code in Xcode and run into a weird issue: my phone always receives the purchase transaction, although I have deleted it in Xcode (Debug -> Storekit -> Manage Transactions shows nothing).
It's on a real phone so I can't resort to the Erase all contents approach in Simulator. I restarted both my laptop and my phone, but the issue persisted. Uninstalling the app doesn't help either. Also, I'm pretty sure it's not my code issue because a) it used to work fine (the issue showed up out of nowhere few days ago), b) the same code worked fine on simulator, and c) I debugged the code and verified the code did received a transaction from somewhere.
Did anyone observe this issue and how do you solve it? Any suggestions would be appreciated.
I'm adding subscription support to my app. The subscription unlocks a feature of the app. From what I read, Apple's Paid Applications Agreement, Schedule 2, Section 3.8(b) has this requirement (emphasis mine):
Links to Your Privacy Policy and Terms of Use must be accessible within Your Licensed Application.
My question is about Terms of Use (I'll refere it as EULA). I'm an individual developer and my app is currently using Apple's standard EULA, which is the default setting in App Store Connect. I wonder what kind of EULA I should link in my app's subscription disclosure to meet the above requirement? Can I just link to Apple's EULA, or should I create a EULA specific to my subscription?
I have no problem in writing a EULA specific to my subscription, but my concern is that if I did it, would my app be licensed only by my custom EULA, or a combination of Apple's standard EULA and my customer EULA? I would hope the latter, but I'm not sure, because I find the following in Apple's standard EULA (emphasis mine):
Your license to each App is subject to your prior acceptance of either this Licensed Application End User License Agreement (“Standard EULA”), or a custom end user license agreement between you and the Application Provider (“Custom EULA”), if one is provided.
I checked a few apps by other developers and found they all created their own EULA. I haven't found any one linking to Apple's standard EULA. So I wonder what's the proper way to do it? Thanks for any help.
Hi, please see the example code below. The app has two views: a list view and a detail view. Clicking an item in the list view goes to the detail view. The detail view contains a "Delete It" button. Clicking on the button crashes the app.
Below is my analysis of the root cause of the crash:
When the "Delete It" button gets clicked, it removes the item from data model.
Since the detail view accesses data model through @EnvironmentObject, the data model change triggers call of detail view's body.
It turns out that the fooID binding in the detail view doesn't get updated at the time, and hence DataModel.get(id:) call crashes the app.
I didn't expect the crash, because I thought the fooID binding value in the detail view would get updated before the detail view's body gets called.
To put it in a more general way, the nature of the issue is that SwiftUI may call a view's body with a mix of up-to-date data model and stale binding value, which potentially can crash the app (unless we ignore invalid id in data model API code, but I don't think that's good idea because in my opinion this is an architecture issue that should be resolved on the caller side in the first place).
I have two questions:
Is this behavior (a view's binding value doesn't get updated when the view's body get called if the view accesses data model through @EnvironmentObject) by design, or is it just a limitation in the current implementation?
In practical apps an item A may contain its own value, as well as id of another item. As a result, to show the item A in detail view, we need to access data model to get B's value by its id. The typical way to access data model is by using @EnvironmentObject. But this issue makes it infeasible to do that. If so, what's the alternative approach?
Thanks for any suggestions.
import SwiftUI
struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. The rationale: the caller of data model API should make sure it passes a valid id.
class DataModel: ObservableObject {
@Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
func get(_ id: Int) -> Foo {
return foos.first(where: { $0.id == id })!
}
func remove(_ id: Int) {
let index = foos.firstIndex(where: { $0.id == id })!
foos.remove(at: index)
}
}
struct ListView: View {
@StateObject var dataModel = DataModel()
var body: some View {
NavigationView {
List {
ForEach($dataModel.foos) { $foo in
NavigationLink {
DetailView(fooID: $foo.id)
} label: {
Text("\(foo.value)")
}
}
}
}
.environmentObject(dataModel)
}
}
struct DetailView: View {
@EnvironmentObject var dataModel: DataModel
// Note: I know in this simple example I can pass the entire Foo's value to the detail view and the issue would be gone. I pass Foo's id just to demonstrate the issue.
@Binding var fooID: Int
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(fooID)
return VStack {
Text("\(dataModel.get(fooID).value)")
Button("Delete It") {
dataModel.remove(fooID)
}
}
}
}
struct ContentView: View {
var body: some View {
ListView()
}
}
I read the following the case number in a thread in Swift forum. I wonder how can I access it?
rdar://problem/60594597
According to the discussion in https://developer.apple.com/forums/thread/8796, the public portal of Radar has been replaced by Feedback Assistant. But when I searched the case number (60594597) in Feedback Assistant web site, it returned no results.
Am I misunderstanding something? Thanks for any help.
While verifying how binding invalidates a view (indirectly), I find an unexpected behavior.
If the view hierarchy is
list view -> detail view
it works fine (as expected) to press a button in the detail view to delete the item.
However, if the view hierarchy is
list view -> detail view -> another detail view (containing the same item)
it crashes when I press a button in the top-most detail view to delete the item. The crash occurs in the first detail view (the underlying one), because its body gets called.
To put it in another way, the behavior is:
If the detail view is the top-most view in the navigation stack, its body doesn't get called.
Otherwise, its body gets called.
I can't think out any reason for this behavior. My debugging showed below are what happened before the crash:
I pressed a button in top-most detail view to delete the item.
The ListView's body got called (as a result of ContentView body got called). It created only the detail view for the left item.
Then the first DetailView's body get called. This is what caused the crash. I can't think out why this occurred, because it certainly didn't occur for the top-most detail view.
Below is the code. Note the ListView and DetailView contains only binding and regular properties (they don't contain observable object or environment object, which I'm aware complicate the view invalidation behavior).
import SwiftUI
struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
extension Array where Element == Foo {
func get(_ id: Int) -> Foo {
return first(where: { $0.id == id })!
}
mutating func remove(_ id: Int) {
let index = firstIndex(where: { $0.id == id })!
remove(at: index)
}
}
class DataModel: ObservableObject {
@Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
}
struct ListView: View {
@Binding var foos: [Foo]
var body: some View {
NavigationView {
List {
ForEach(foos) { foo in
NavigationLink {
DetailView(foos: $foos, fooID: foo.id, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
@Binding var foos: [Foo]
var fooID: Int
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(fooID)
return VStack {
Text(label)
Divider()
Text("Value: \(foos.get(fooID).value)")
NavigationLink {
DetailView(foos: $foos, fooID: fooID, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
foos.remove(fooID)
}
}
}
}
struct ContentView: View {
@StateObject var dataModel = DataModel()
var body: some View {
ListView(foos: $dataModel.foos)
}
}
Test 1: Start the app, click on an item in the list view to go to the detail view, then click on "Delete It" button. This works fine.
The view hierarchy: list view -> detail view
Test 2: Start the app, click on an item in the list view to go to the detail view, then click on "Create another detail view" to go to another detail view. Then click on "Delete It" button. The crashes the first detail view.
The view hierarchy: list view -> detail view -> another detail view
Could it be just another bug of @Binding? Is there any robust way to work around the issue?
Hi, I'm an individual developer in China. I signed up for my Apple Developer account last year (I input my name in both Chinese character and Pinyin when filling out the form). Then I successfully submitted my app to both China Mainland App Store and US App Store, but to find that in the app's home page in US App Store, my name was shown in Chinese character.
That's quite a surprise. I think App Store should use my Pinyin name in US App Store (or perhaps better, provide an option to let me decide). The current behavior puts the China Mainland individual developers at a disadvantage because it makes it hard for them to prompt their apps in English speaking countries. Actually when one of my friends in US tried my app, his first comment was that I shouldn't show my name in Chinese character if the app is targeted for US users!
I contacted Apple Developer Program support engineers twice for this issue, once last year, once recently. At the first time the reply was just boilerplate and useless. At the second time the engineer was quite helpful, although it was still impossible to solve the issue. One of the explanation the support engineer gave was that it's the government's requirement to use developer's legal name. But I'm not convinced because:
1) Using legal name doesn't necessarily mean it has to be Chinese characters. It can be Pinyin. For example, we have Pinyin name printed on passports and credit cards.
2) Since the app is submitted to US App Store, the information in the app's home page is supposed to be read by US people. It's common convention to show Chinese people name in Pinyin in this context (e.g., English documents).
I'd think this is a deficiency in the design of the App Store. The support engineer said he would help to send my feedback. I understand how things work in big companies, so I think it may help to describe the issue in the forum also and hopefully someone can help to make the change, because it really doesn't make sense.
Thanks!