We have an app that’s been in the store for a year, and the latest version was rejected for
Guideline 5.6 - Developer Code of Conduct. The app attempts to manipulate customers into making unwanted in-app purchases. Specifically, the app shows a one time offer for subscription when the user closes the initial subscription page launched upon opening the app.
Essentially we have a soft paywall at the end of onboarding, and if the user closes that paywall we pop a second paywall with a one-time offer with an introductory offer (2 months for $1, “Once you close your one-time offer, it’s gone!”). We’ve had this in the last few versions of the app, but not been flagged for it.
The strategy of having a one time offer after end of the onboarding paywall is not uncommon, and we are copying the approach used by many other apps in the app store. Limited time offers are a staple of marketing across all types of B2C stores.
Does anyone have any experience with this type of rejection? Should we stick to our approach and escalate to a phone call? Change the one-time offer to a “limited time offer” (e.g. reduced price for 12 hours) so there is less pressure to decide right then? It’s a strange rejection because it doesn’t seem like we are doing anythign unusual. An onboarding soft-paywall followed by a one time offer is less of a push than a hard paywall (which is approvable), and offers some people an appealing way to test the app before committing to pay for a subscription (which doesn’t offer free trials).
However, we don’t want to get flagged as a bad actor by the App Store reviewers.
Post
Replies
Boosts
Views
Activity
In iOS 18 RC, and the iOS 18 simulator shipped with Xcode 16.0 RC, there is a regression where ModelContexts on the same ModelContainer do not sync changes. A minimal example is below, but briefly: create an object in context1. Retrieve and update that object in context2, then save context2. The changes cannot be found in context1 in iOS 18 RC, but can in iOS 17 and earlier betas of iOS 18.
I've submitted this as FB15092827 but am posting here for visibility to others. I'm going to have to scramble to see if I can mitigate this in our impacted app before iOS 18 launches. It's affecting us when doing background inserts in a ModelActor to populate our app UI, but you can see below the effects are seen even on the same thread in a very simple two-context example.
@Test("updates sync between contexts") func crossContextSync() async throws {
// overview:
// create an employee in context 1
// update the employee in context 2
// check that the update is available in context 1
let context1 = ModelContext(demoAppContainer)
let context2 = ModelContext(demoAppContainer)
// create an employee in context 1
let newEmployee = Employee(salary: 0)
context1.insert(newEmployee)
try context1.save()
#expect(newEmployee.salary == 0, "Created with salary 0")
// update the employee in context 2
let employeeID = newEmployee.uuid
let predicate: Predicate<Employee> = #Predicate<Employee> { employee in
employee.uuid == employeeID
}
let fetchedEmployee = try #require(try? context2.fetch(FetchDescriptor<Employee>(predicate: predicate)).first)
#expect(fetchedEmployee.uuid == newEmployee.uuid, "We got the correct employee in the new context")
let updatedSalary = 1
fetchedEmployee.salary = updatedSalary
try context2.save()
// FAILURE IS HERE. This passes in earlier iOS betas and in iOS 17.X
#expect(newEmployee.salary == updatedSalary, "Salary was update in context 1")
// Create a new modelContext on the same container, since the container does have the changes in it.
// By creating this new context we can get updated data and the test below passes in all iOS versions tested. This may be a mitigation path but creating new contexts any time you need to access data is painful.
let context3 = ModelContext(demoAppContainer)
let fetchedEmployeeIn3 = try #require(try? context3.fetch(FetchDescriptor<Employee>(predicate: predicate)).first)
#expect(fetchedEmployeeIn3.uuid == newEmployee.uuid, "We got the correct employee in the new context3")
#expect(fetchedEmployeeIn3.salary == updatedSalary, "Salary was update in context 1")
}
Code below if you want to build a working example, but the test above is very simple
let demoAppContainer = try! ModelContainer(for: Employee.self)
@main
struct ModelContextsNotSyncedToContainerApp: App {
init() {
}
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(demoAppContainer)
}
}
}
@Model
final class Employee {
var uuid: UUID = UUID()
var salary: Int
init(salary: Int = 0) {
self.salary = salary
}
}
I've already submitted this as a bug report to Apple, but I am posting here so others can save themselves some troubleshooting. This is submitted as FB14337982 with an attached complete Xcode project to replicate.
In iOS 17 we use a ModelActor to download data which is saved as an Event, and then save it to SwiftData with a relationship to a Location. In iOS 18 22A5307d we are seeing that this code no longer persists the relationship to the Location, but still saves the Event. If we put a breakpoint in that ModelActor we see that the object graph is correct within the ModelActor stack trace at the time we call modelContext.save(). However, after saving, the relationship is missing from the default.store SQLite file, and of course from the app UI.
Here is a toy example showing how inserting an Employee into a Company using a ModelActor gives unexpected results in iOS 18 22A5307d but works as expected in iOS 17.
It appears that no relationships data survives being saved in a ModelActor.ModelContext.
Also note there seems to be a return of the old bug that saving this data in the ModelActor does not update the @Query in the UI in iOS 18 but does so in iOS 17.
Models
@Model
final class Employee {
var uuid: UUID = UUID()
@Relationship(deleteRule: .nullify) public var company: Company?
/// For a concise display
@Transient var name: String {
self.uuid.uuidString.components(separatedBy: "-").first ?? "NIL"
}
init(company: Company?) {
self.company = company
}
}
@Model
final class Company {
var uuid: UUID = UUID()
@Relationship(deleteRule: .cascade, inverse: \Employee.company)
public var employees: [Employee]? = []
/// For a concise display
@Transient var name: String {
self.uuid.uuidString.components(separatedBy: "-").first ?? "NIL"
}
init() { }
}
ModelActor
import OSLog
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "SimpleModelActor")
@ModelActor
final actor SimpleModelActor {
func addEmployeeTo(CompanyWithID companyID: PersistentIdentifier?) {
guard let companyID,
let company: Company = self[companyID, as: Company.self] else {
logger.error("Could not get a company")
return
}
let newEmployee = Employee(company: company)
modelContext.insert(newEmployee)
logger.notice("Created employee \(newEmployee.name) in Company \(newEmployee.company?.name ?? "NIL")")
try! modelContext.save()
}
}
ContentView
import OSLog
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "View")
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var companies: [Company]
@Query private var employees: [Employee]
@State private var simpleModelActor: SimpleModelActor!
var body: some View {
ScrollView {
LazyVStack {
DisclosureGroup("Instructions") {
Text("""
Instructions:
1. In iOS 17, tap Add in View. Observe that an employee is added with Company matching the shown company name.
2. In iOS 18 beta (22A5307d), tap Add in ModelActor. Note that the View does not update (bug 1). Note in the XCode console that an Employee was created with a relationship to a Company.
3. Open the default.store SQLite file and observe that the created Employee does not have a Company relationship (bug 2). The relationship was not saved.
4. Tap Add in View. The same code is now executed in a Button closure. Note in the XCode console again that an Employee was created with a relationship to a Company. The View now updates showing both the previously created Employee with NIL company, and the View-created employee with the expected company.
""")
.font(.footnote)
}
.padding()
Section("**Companies**") {
ForEach(companies) { company in
Text(company.name)
}
}
.padding(.bottom)
Section("**Employees**") {
ForEach(employees) { employee in
Text("Employee \(employee.name) in company \(employee.company?.name ?? "NIL")")
}
}
Button("Add in View") {
let newEmployee = Employee(company: companies.first)
modelContext.insert(newEmployee)
logger.notice("Created employee \(newEmployee.name) in Company \(newEmployee.company?.name ?? "NIL")")
try! modelContext.save()
}
.buttonStyle(.bordered)
Button("Add in ModelActor") {
Task {
await simpleModelActor.addEmployeeTo(CompanyWithID: companies.first?.persistentModelID)
}
}
.buttonStyle(.bordered)
}
}
.onAppear {
simpleModelActor = SimpleModelActor(modelContainer: modelContext.container)
if companies.isEmpty {
let newCompany = Company()
modelContext.insert(newCompany)
try! modelContext.save()
}
}
}
}
One of our apps has some failing unit tests that I traced back to a change in the way Calendar.date(from:) works on iOS 18 beta (22A5307d) as compared to earlier versions. The simple demo unit test below passes in iOS 17.5 and fails in 18 beta 3. I did not test this in other beta versions.
import XCTest
final class DateComponentsMath_Tests: XCTestCase {
func testAddingWeek() throws {
var components = DateComponents(
year: 2024,
weekOfYear: 1,
yearForWeekOfYear: 2024
)
let date1 = Calendar.current.date(from: components)!
// add a few weeks to the components
components.weekOfYear = components.weekOfYear! + 5
let date2 = Calendar.current.date(from: components)
XCTAssertNotEqual(date1, date2, "We added five weeks to the components so this should not result in the same date")
}
}
It appears that in iOS 18 (22A5307d), year, weekOfYear, and yearForWeekOfYear are no longer enough to uniquely specify a date. With those three values, Calendar.date(from:) always returns January 1 of the specified year. In earlier versions of iOS this was not the case.
I submitted this as FB14323984
I've got an iOS app that uses WeatherKit SDK (not the REST API) to pull weather data. I'm getting reports from users today that it is failing to update today (June 5, 2024).
When I run the app in a debugger I see:
Failed to generate jwt token for: com.apple.weatherkit.authservice with error: Error Domain=WeatherDaemon.WDSJWTAuthenticatorServiceListener.Errors Code=2 "(null)"
Received authentication failure for request: 5E11C6A7-C255-4130-B1C7-93C3D6935210
On another attempt I got a 504 response.
The Apple Weather app is also extremely slow to update (if at all) when adding a new location.
And of course, https://developer.apple.com/system-status/ shows WeatherKit with a green light.
Is anyone else seeing this?
Edit: There seems to be a broad outage with weatherkit. The apple weather app is not working for several people reported here:
https://discussions.apple.com/thread/255637780?sortBy=best
Why is the developer dashboard showing a green light?!
We need to be able to rely on these systems.
We have an app that allows users to fetch historical weather data for their selected locations. We are seeing users report failures for specific latitudes and longitudes that come back as
WeatherDaemon.WDSClient<WeatherDaemon.WeatherResource>.Errors.responseFailed
For example, this request always fails:
https://weather-data.apple.com/v3/weather/en-US/33.797/-111.775?timezone=America/Phoenix&dataSets=forecastDaily&dailyStart=2023-12-29T07:00:00Z&dailyEnd=2023-12-31T07:00:00Z&country=US&deviceLanguages=en-US&clientMetadata=<REDACTED>
Another example for another location:
https://weather-data.apple.com/v3/weather/en-AU/40.717/-74.000?timezone=America/New_York&dataSets=forecastDaily&dailyStart=2023-12-29T05:00:00Z&dailyEnd=2023-12-30T05:00:00Z&country=US&deviceLanguages=en-AU,es-AU&clientMetadata=<REDACTED>
Both example involve queries at the end of December 2023.
I have filed this as FB13608710 with example code that replicates this as it is being used in our app.
It is often the case that offline devices can add duplicate entities that needs to be merged when CloudKit syncs. Consider user-created tags. A user might create a Note, and then tag it with a newly created tag “Family.” On a separate offline device, they might create another note, and create another tag also called ”Family.” On device sync, both duplicate ”Family” tags would need to be identified as duplicates based on their name property, merged to a single entity, and their original relationships consolidated to the single merged Tag. And this needs to happen before the CloudKit sync data is presented to the UI in the main context.
With Core Data we have the mechanism to consume relevant store changes described here.
These tools allow us to listen for remote change, then process them appropriately (e.g. remove / merge duplicates) and then merge the changes into the app’s main context. This perfectly solves the problem described in the first paragraph above. Apple provides code using this mechanism for deduplicating tags in a sample app.
Is there a mechanism to solve this deduplication problem using SwiftData technology without implementing and maintaining a parallel Core Data stack?
I have added a strings catalog to my project, and it populates and updates correctly. However, after adding a few "vary as plurals" edits, now the file only renders as a JSON in the editor. So, there's no UI to add languages, for example.
The strings catalog still works, and if I manually edit a string it will be compiled into the app, and if I edit a string in my app source, it will update in the JSON file. It's just that the strings catalog UI itself is hidden.
If I delete the Localizable file and re-add it, the UI returns. But then if I make my pluralization edits again, the UI disappears and it renders only as JSON. If I drag this xcstrings file into another project it will render with the correct UI, so it seems like a project setting.
Is there some setting to re-enable this?
I'm using the new interactive widgets to trigger an app intent (tap a widget button, swift data model updates). If I tap the widget while the app is not running, then launch the app, the views (populated by an @Query) will show the correct data. However, if the app is running before tapping the widget button, the app's @Query will not refresh and so the app shows stale data. If I quit the app and relaunch it, or otherwise trigger a refresh of the query, then the view will update.
Does anyone have a way to get the app to notice when an App Intent has changed Swift Data values outside of the main app?
edit: Reported as FB13278891
In the WWDC 23 lounge, this exchange with @Dave N (Apple) indicated that using ModelActor is the right way to do background work using swift concurrency. https://developer.apple.com/forums/thread/731338
I've figured out how to use this approach to do background work using swift concurrency (inside a Task), and example code is below for those who find it useful, however I'm not seeing view updates when the background work is complete.
There seems to be no way to trigger a view update based on a @Query when inserts happen in a ModelActor’s context. However, if a delete happens on the ModelActor context, this DOES trigger a view redraw. I believe this is a bug because I expect the behavior to be the same for inserts and deletes. I've submitted this as Feedback FB12689036.
Below is a minimal project which is the default SwiftData template, where the Add and Delete buttons point to a ModelActor instead of the main view ModelContext. You will see that using the addItem function in the ModelActor does not trigger a UI update in ContentView. However if you relaunch the app the added Item will be present. In contrast, using the delete function on the ModelActor context does trigger an immediate view update in ContentView.
If this is intended behavior, we need a way to merge changes from background contexts, similar to what is described in the Core Data document “Loading and Displaying a Large Data Feed”:
https://developer.apple.com/documentation/swiftui/loading_and_displaying_a_large_data_feed
In Core Data we have automaticallyMergesChangesFromParent and mergeChanges(fromContextDidSave:) to do this manually. There seems to be no equivalent for Swift Data.
If anyone has solved this problem of merging changes from other contexts, or can confirm that this is a bug, please let me know.
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
@State private var simpleModelActor: SimpleModelActor!
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
} label: {
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
.onAppear {
simpleModelActor = SimpleModelActor(modelContainer: modelContext.container)
}
}
private func addItem() {
Task {
await simpleModelActor.addItem()
}
}
private func deleteItems(offsets: IndexSet) {
Task {
for index in offsets {
await simpleModelActor.delete(itemWithID: items[index].objectID)
}
}
}
}
import Foundation
import SwiftData
final actor SimpleModelActor: ModelActor {
let executor: any ModelExecutor
init(modelContainer: ModelContainer) {
let modelContext = ModelContext(modelContainer)
executor = DefaultModelExecutor(context: modelContext)
}
func addItem() {
let newItem = Item(timestamp: Date())
context.insert(newItem)
try! context.save() // this does not impact a re-display by the @Query in ContentView. I would have expected it to cause a view redraw.
}
func delete(itemWithID itemID: Item.ID) {
let item = context.object(with: itemID)
context.delete(object: item) // this DOES cause a view redraw in ContentView. It triggers an update by @Query.
// try! context.save() // this makes do difference to view redraw behavior.
}
}
A lost feature from Core Data seems to be the ability to enforce a uniqueness constraint based on multiple attributes. For example, with Core Data one could have a constraint for “firstName,lastName” and only the combination of the same firstName and lastName was enforced to be unique. In SwiftData there seems to be no way to achieve this with the @Attribute(.unique) macro.
It seems that the only solution would be to create a name-mangled composite attribute and automatically update that on insert, but this should be a feature of SwiftData. If I'm missing something please let me know.
The relevant entity attribute from an xcdatamodeld file is:
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="firstName"/>
<constraint value="lastName"/>
</uniquenessConstraint>
</uniquenessConstraints>
I filed this as feedback FB12385087
I've got an app live in the App Store, and a second app that I'm developing right now. In TestFlight (only!), when I display a manageSubscriptionsSheet in the new app I'm seeing the TestFlight subscription status for the other app. If I run the new app on the same device in debug mode (connected to my laptop), I get the correct manageSubscriptionsSheet displayed.
In both environments (Debug and TestFlight) I see the correct purchase sheet showing the right subscription options. It is only the manageSubscriptionsSheet that is incorrect.
Both the new app and the old app (the one showing up in manageSubscriptionsSheet) have TestFlight builds on my phone.
Of course, as I type this up I reinstall the TestFlight build (after seeing the correct manageSubscriptionsSheet in the debug build) and now I'm seeing the correct manageSubscriptionsSheet.
Is this something I'm doing wrong in my app, or is this a known issue with TestFlight apps? I know it's expected that the entitlements are different on TestFlight than in debug, but would manageSubscriptionsSheet show something different than a purchase sheet?
Is everyone else seeing that ImageRenderer is unable to render the contents of a ScrollView? When using the ImageRenderer class introduced in iOS16, the render will not contain any Views that are inside of a ScrollView. The ScrollView contents within the rendered image will be blank. This is surprising to me since I would think ScrollView use is pretty common, and so ImageRenderer would have been tested with it. Is there any documentation out there explaining what types of Views or scenarios will cause a failure of ImageRenderer?
To reproduce, run the ContentView below inside an iOS SwiftUI project. Tap the “Render this View to Image” button.
Expected results: the green-bordered Image should show a screenshot of the entire View, including the contents of the ScrollView (“Text inside a ScrollView”).
Actual results: the rendered screenshot shows the region of the ScrollView, but there are no contents shown. It is blank.
import SwiftUI
struct ContentView: View {
@State private var renderedImage: UIImage?
var body: some View {
VStack {
Button("Render this View to Image") {
renderedImage = ImageRenderer(content: self).uiImage!
}
VStack {
Text("rendered image")
Image(uiImage: renderedImage ?? UIImage(systemName: "xmark")!)
.resizable()
.aspectRatio(contentMode: .fit)
}
.frame(height: 200, alignment: .center)
.padding(5).border(.green, width: 5)
VStack {
Text("A ScrollView is shown below")
ScrollView {
VStack {
Text("Text inside a ScrollView")
Text("Text inside a ScrollView")
Text("Text inside a ScrollView")
Text("Text inside a ScrollView")
}
.fixedSize()
}
}
.padding(5).border(.red, width: 5)
.frame(height: 100)
}
.padding()
}
}
When using the new ImageRenderer introduced in iOS16, the Picker control does not render correctly. I have filed it as FB11994261.
Does anyone have a workaround for this which still uses ImageRenderer?
As an aside: the reason I need to use ImageRenderer is that in my current project I'm trying to render a large (>2730 point tall) view to an image for the user to share. Solutions using UIGraphicsImageRenderer will silently fail with a clear/black screen when trying to render views with any dimension >2730 pt. ImageRenderer is the only view capturing technique that I've found which will successfully render a View greater than this threshold.
To replicate:
add a Picker to a view. Attempt to render that view using ImageRenderer. The resulting image will not match what is displayed on screen.
Step-by-step:
Create a default SwiftUI project in Xcode. Replace the main content view with this code:
struct ContentView: View {
@State private var renderedImage: UIImage?
@State private var showSheet = false
@State private var picked: Int = 0
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Picked \(picked)")
Picker("Pick a number", selection: $picked) {
ForEach([0,1,2,3], id:\.self) { number in
Text("\(number)")
}
}
.pickerStyle(.segmented)
}
.padding()
.sheet(isPresented: $showSheet) {
if let renderedImage {
Image(uiImage: renderedImage)
}
}
.task {
Task {
renderedImage = ImageRenderer(content: self).uiImage!
showSheet = true
}
}
}
}
Expected result:
on loading the view, ImageRenderer captures the current state of ContentView, a sheet opens, and the screenshot is display, including the Picker UI element.
Actual results:
The screenshot is presented in a sheet, but the Picker element is replaced by a yellow field with a red slash-circle in the center. Attached are images of the actual ContentView, and the sheet displaying the failed render.
If an app has a subscription (or IAP) with family sharing enabled, will beta testers in TestFlight be able to test that functionality?
I’m working on an app that includes family sharing for IAP, and I highlight family sharing in the paywall. However, my testers are reporting that they do not get the entitlement when a family member purchases the IAP. I’m trying to determine if I’m making an error in my code. The family sharing tech talk indicates that no special treatment is required in the app, so I’m wondering if this is just a limitation of the TestFlight store environment.