Posts under App & System Services topic

Post

Replies

Boosts

Views

Activity

Silent Push background launch rejected with "pushDisallowed" and "AMNP" on TestFlight build
Hello, I am implementing background processing using Silent Push Notifications in an iOS application. The app needs to receive requests from a server and execute processing in the background. For this purpose, I am using Silent Push Notifications to wake up the application. However, the application is not being launched as expected when running the TestFlight version. I would appreciate any guidance regarding this behavior. Environment Xcode 26.3 iOS 18.6 TestFlight distribution Push Notifications capability enabled Background Modes enabled Remote notifications enabled APNs Authentication Key (.p8) APNs Production environment Background Previously, the application periodically connected to a server in the background and sent location information when requested by the server. Since that background processing no longer behaves as expected, I started implementing an alternative approach using Silent Push Notifications. When running the application directly from Xcode in Debug mode, Silent Push works correctly and the app is launched in the background. However, when using the TestFlight build on the same device, the behavior differs and the app is not launched in the background. Observed Behavior The APNs request appears to be successful: APNs response status is HTTP 200 Using the device token obtained from the TestFlight build Alert Push notifications are received successfully application:didReceiveRemoteNotification:fetchCompletionHandler: is called when the app is in the foreground The same method is not called when the app is in the background Silent Push payload: { "aps": { "content-available": 1 } } HTTP/2 headers: apns-push-type: background apns-priority: 5 Verified Items Production device token is being used APNs topic matches the application's bundle identifier Alert Push notifications work correctly Background App Refresh is enabled on the device The application has not been force-quit completionHandler is always called after processing completes Console Logs The following logs appear when the push is sent: Submitted: com.apple.pushLaunch.jp.co.comp.MyApp.ext.mdm Application Policy response: {100, 0.00, [{[pushDisallowed]: Required:0.00, Observed:1.00},]} Decision: AMNP I also see: Insufficient history window for deviceActivityLikelihood stream deviceActivityLikelihood returned a nil timeline Questions Under what conditions are pushDisallowed and Decision: AMNP generated? What factors can cause iOS to reject a background launch triggered by a Silent Push Notification? Since Alert Push notifications are received successfully, is it reasonable to conclude that APNs configuration, topic configuration, and device token usage are correct? Are there any specific settings, entitlements, or implementation details that should be reviewed to resolve the pushDisallowed condition? The behavior differs between the Debug build launched from Xcode and the TestFlight build running on the same device. Are there any restrictions, policies, or differences applied to TestFlight builds that could explain this behavior? If the observed behavior is expected by design, what would Apple recommend as the appropriate architecture or API for server-triggered background processing in this scenario? Thank you for your time and assistance.
3
0
253
2d
Requesting private watchOS Bluetooth entitlements for open-source CGM-connected AID app (FB22619409)
I'm a contributor to Trio, an open-source automated insulin delivery (AID) app for iOS/watchOS maintained by the Nightscout Foundation. I'm posting at the recommendation of the engineering team via Feedback Assistant FB22619409 (Developer Support case 102865854185). The goal We're prototyping direct BLE connectivity between the Trio watchOS extension and the Dexcom G7 CGM sensor — eliminating dependence on iPhone-to-Watch relay via WatchConnectivity. For an AID app, stale CGM data affects insulin dosing decisions; this is a patient safety concern. The entitlements needed To maintain a BLE connection to the G7 while backgrounded and with the display off/wrist lowered, the WatchKit extension requires: com.apple.developer.bluetooth-central-background com.apple.developer.bluetooth-central-screen-off-scanning What I've confirmed Both entitlements are present on Dexcom's shipping G7 WatchKit extension (com.dexcom.g7app.watchkitapp.watchkitextension), verifiable via: codesign -d --entitlements :- \ "Dexcom G7.app/Watch/G7Watch.app/PlugIns/G7Watch Extension.appex" Output includes: com.apple.developer.bluetooth-central-background = true com.apple.developer.bluetooth-central-screen-off-scanning = true These are not self-service capabilities exposed through Xcode or the developer portal for our account: Xcode → Trio Watch App target → Signing & Capabilities → + Capability → searching com.apple.developer.bluetooth-central-background returns No Matches Certificates, Identifiers & Profiles → WatchKit Extension App ID (org.nightscout.5QE6TMMEH2.trio.watchkitapp.watchkitextension) → the entitlement does not appear under Capability Requests A screen recording demonstrating both is attached to FB22619409. The May 16 Apple Feedback response noted that the entitlement was visible in an internal Xcode project — consistent with it being a restricted/managed entitlement not exposed through standard developer accounts. My questions What is the correct process to request com.apple.developer.bluetooth-central-background for a watchOS extension App ID where it does not appear in Capability Requests? Is com.apple.developer.bluetooth-central-screen-off-scanning available through a private/managed entitlement process, and how do we enter that process? Is there a formal Apple program (e.g., MFi, HealthKit entitlements, or similar) applicable to CGM-connected medical apps that covers these entitlements? Full account details, screen recording, and entitlement output are attached to FB22619409 / Developer Support case 102865854185. Happy to provide a test build, full entitlement output, or additional context if needed. Thank you
1
0
207
3d
Error when access StoreView / ProductView on iOS 27.0 simulator
Hi, Feedback: FB23494579 Using StoreView / ProductView on iOS 27.0 simulator / device hub throws the following error: Error: Accessing State<ProductViewEventConfiguration>'s value without being installed on a View. This will create a new ProductViewEventConfiguration instance each time. Environment macOS 26.5.2 (25F84) Xcode 27.0 beta 2 (27A5209h) Simulator / Device Hub - iPhone 17 Pro (iOS 27.0) Screenshot
0
0
64
3d
Live Activities Push-to-Start flows
Good morning, We are implementing Live Activities in a push-to-start flow. We wrap the listener for push to start tokens in a high priority task: if ptsListenerTask == nil || ptsListenerTask?.isCancelled == true { ptsListenerTask = Task(priority: .high) { [weak self] in for await pushToken in Activity<LiveAuctionAttributes>.pushToStartTokenUpdates { //Send token to back-end } } I've tried a few variations of this and they work well on most devices. I have seen a couple of devices that refuse to issue a push to start token. The user will have logging for the init flow and starting the PTS listener then the logs just go silent, nothing happens. One thing that seemed to work was getting the user to start a Live Activity manually (from our debugging tool) then the PTS token gets issued. This is not very reliable and working a mock live activity into the flow for obtaining a PTS token is a poor solution. Is anyone else seeing this and is there a known issue with obtaining PTS tokens? Thanks! Brad
12
2
772
3d
TimeDataSource .dateRange(endingAt:) won't update
Hello, I'm trying to add a new Live Activity to my app showing a timer to a specific date and time. I thought I could use some TimeDataSource so that the timer would be updated automatically by SwiftUI without relying on Live Activity updates. That's not the case with .dateRange(endingAt:) though. Text(.dateRange(endingAt: targetDate), format: .components(style: .narrow)) Something like this correctly shows the timer exactly how I want it, but it never updates. Other TimeDataSource like .currentDate and .durationOffset(to:) do update automatically, but are not what I'm looking for. Am I missing something? Should I use another formatter to make it work?
0
0
60
3d
HKStatisticsCollectionQuery initialResultsHandler returns nil results (error) for one specific user — read auth granted, data exists, survives reinstall
Environment: iPhone 13 Pro, iOS 26.5. Affects a single user out of many; cannot reproduce on any of our test devices. We use HKStatisticsCollectionQuery to read step counts for a statistics screen. For one specific user, the query's initialResultsHandler appears to deliver results == nil (the success branch never runs), so our completion is never called and the screen shows an infinite spinner. private let store = HKHealthStore() func fetchHourlyStepCounts(for day: Date, completion: @escaping ([Int]) -> Void) { guard let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else { return } let calendar = Calendar.current let startOfDay = calendar.startOfDay(for: day) var hourly = DateComponents() hourly.hour = 1 let query = HKStatisticsCollectionQuery( quantityType: stepType, quantitySamplePredicate: nil, options: .cumulativeSum, anchorDate: startOfDay, intervalComponents: hourly ) query.initialResultsHandler = { _, collection, error in guard let collection else { // For the affected user, execution seems to reach here (collection == nil). // Adding logging of the HKError + authorization status for the next occurrence. return } var counts: [Int] = [] let end = calendar.date(byAdding: .day, value: 1, to: startOfDay)! collection.enumerateStatistics(from: startOfDay, to: end) { stats, _ in let steps = stats.sumQuantity()?.doubleValue(for: .count()) ?? 0 counts.append(Int(steps)) } DispatchQueue.main.async { completion(counts) } } store.execute(query) } What we've confirmed / ruled out: Read authorization for stepCount is granted (the user toggled it ON in the HealthKit sheet on video). The Apple Health app shows step data for this user (so data exists). A coarser query (2-year interval) for the same user succeeds, while the hourly query appears to fail — same type / predicate / options / auth. Symptom persists across app reinstall and device reboot, and re-granting Health permission. Permission denial returns empty results (per Apple docs), not an error — so this isn't simple denial. Not errorDatabaseInaccessible as far as we can tell (foreground, device unlocked). Questions: What can cause HKStatisticsCollectionQuery.initialResultsHandler to return results == nil (with an error) persistently for one device/account, when read auth is granted and data exists? Can errorHealthDataRestricted occur without an MDM/supervised profile (i.e., on a normal consumer device)? What device/account states actually trigger it? Is it expected that a coarse-interval query succeeds while an hourly-interval query on the same type fails for the same user? We're adding logging of the actual HKError code + authorizationStatus for the next occurrence, but would appreciate any insight on what conditions produce this.
0
0
66
3d
Live Activity Stops Updating After 30 Seconds in Background During Audio Playback
Hi I developed a music app that plays offline audio and displays lyrics using Live Activities. According to ActivityKit documentation, Live Activities can be updated from the background. However, in my case, updates stop after ~30 seconds when the app goes to the background or the device is locked. Important points: The app continues running in the background (audio playback works fine using AVAudioSession with .playback) Background code execution is working as expected Only the Live Activity stops updating I am not using push updates since this is an offline app. Is there any limitation or requirement for updating Live Activities continuously in the background during audio playback? Audio Session Configuration let session = AVAudioSession.sharedInstance() try session.setCategory( .playback, mode: .default, options: [.mixWithOthers] // ✅ DO NOT interrupt other audio ) try session.setActive(true) print("✅ [AudioSession] Activated with mixWithOthers") } catch { print("❌ [AudioSession] Error: \(error)") } Live Activity Update Methods guard let activity = getLiveActivity(for: recordID) else{ print("⚠️ No Live Activity found for recordID: \(recordID)") return } guard activity.activityState == .active else { print("⚠️ Activity is not active") return } Task { let content = ActivityContent( state: state, staleDate: Date().addingTimeInterval(60 * 60 * 12), relevanceScore: 1.0 ) await activity.update(content) print("✅ Live Activity updated with ActivityContent") } }
1
0
604
3d
Enforced segregation between VM and Host
VM are quite interesting options in a BYOD environment but im not able to enforce a proper segregation between VM and Host. Examples: If the VM is MDM enrolled the Host can access the VM Disk quite easily If the Host is managed a VM can be used to bypass Network security Is a mdm enrolled VM a scenario you have on your roadmap?
2
0
200
3d
Understanding Crash Reporter Extension lifecycle and debugging behavior
Hi! I have a few questions about the lifecycle and capabilities of the Crash Reporter Extension. Besides using the corpsePort to inspect the crashed process through Mach APIs, is it safe/supported/recommended for the extension to access files in a shared App Group container? Are there any caveats or exceptions we should be aware of, for example around memory-mapped files, file coordination, or filesystem access after the host app has crashed? Shall we use some particular APIs for this kind of shared resource or not? While debugging the extension, I noticed that when I trigger a crash in the app I am debugging, LLDB does not stop inside the extension (it also ends up stopping the debugging session). However, I can observe that the extension does run, because it writes data into a shared App Group directory related to the crash. Is this expected behavior? Is there a recommended way to debug the Crash Reporter Extension reliably (with lldb, or other way)? More generally, I would like to better understand the extension lifecycle: When exactly does the extension start running? How long can it live after the app crashes? Is there a time limit for operating on the corpse process? Is the extension subject to resource limits similar to other app extensions, such as memory, disk, CPU, watchdog, or jetsam constraints? If the Crash Reporter Extension itself crashes, how can we detect that? Would those crashes appear in Xcode Organizer, or is there another recommended way to observe them? Any clarification around the supported lifecycle, debugging model, and resource limits would be very useful.
4
1
356
3d
AlertConfiguration.AlertSound.named plays system default despite CAF in main bundle (AlarmKit, iOS 26.5)
We need an official answer: wrong wiring, known bug on 26.5, or unsupported? AlertConfiguration.AlertSound.default works. AlertConfiguration.AlertSound.named("lifeloop_alarm_silent") schedules successfully (Stop/Snooze UI, no API error) but playback is always system default — never our bundled CAF. OFFICIAL API WE FOLLOW Documentation: AlertConfiguration.AlertSound.named(:) https://developer.apple.com/documentation/activitykit/alertconfiguration/alertsound/named(:) "The name of the sound file to use for the alert. Choose a file that's in your app's main bundle or the Library/Sounds folder of your app's data container." WWDC25 session 230 shows: let sound = AlertConfiguration.AlertSound.named("Chime") with the file in the main bundle. We use: AlertConfiguration.AlertSound.named("lifeloop_alarm_silent") Our Swift schedules with AlarmManager.AlarmConfiguration.alarm(..., sound: alertSound). REFERENCES WE ALREADY READ AlarmConfiguration overview: https://developer.apple.com/documentation/alarmkit/alarmmanager/alarmconfiguration Albert Pascual DTS reply (Feb 2026) on this thread pointed at nested alertConfiguration with sound AlarmKit FAQ: https://developer.apple.com/forums/thread/797158 Older forum reply (~2025): claimed .named("Glass Drum.caf") worked — not verified on 26.5 today On iOS 26.5: Bundle.main finds our file with AND without ".caf" in the lookup string; playback still system default OUR WIRING (please confirm correct or tell us what to change) We schedule fixed-time dose alarms with: AlarmManager.AlarmConfiguration.alarm( schedule: .fixed(fireDate), attributes: attributes, stopIntent: stopIntent, secondaryIntent: snoozeIntent, sound: alertSound) .default on this parameter works. .named("lifeloop_alarm_silent") does not — despite Bundle.main finding the file (proof below). Question: Is sound: on .alarm(...) correct for custom AlertSound.named, or must sound go in nested AlertConfiguration(title:body:sound:) as in the overview example? WHAT WE HEAR ON DEVICE sound = .default → loud system alarm (works) sound = .named("lifeloop_alarm_silent") → same loud system/default every time No API error — silent fallback to default PROOF 1 — FILE IN SHIPPED IPA $ find Payload -name "lifeloop_alarm_*.caf" Payload/LifeLoop.app/lifeloop_alarm_silent.caf PROOF 2 — RUNTIME ON DEVICE (2026-06-30, iOS 26.5) Bundle.main.url(forResource:withExtension:): FOUND Bundle.main.url(full filename in name): FOUND Path: .../LifeLoop.app/lifeloop_alarm_silent.caf All .caf in main bundle: lifeloop_alarm_silent.caf Library/Sounds: (none) Widget .appex .caf: (none) Native log at schedule: [alarmkit] build_config · soundModeRaw=silent alertSound=named:lifeloop_alarm_silent silentCafInBundle=true [alarmkit] schedule_ok · alarmID=FBDF2082-61F1-4C93-8B20-094A60EA8626 CAF: lifeloop_alarm_silent.caf — mono, 44100 Hz, 16-bit PCM, ~0.5 s QUESTIONS FOR DTS Main app bundle only, or must CAF also be in widget .appex? Must named(_:) include ".caf" or base name only? Bundle.main FOUND at schedule time but playback is default — known bug on 26.5? Please answer: (A) wrong wiring + correct recipe, (B) known bug + fix version, or (C) not supported — please document. Thank you. Same problem as tonny00 — custom CAF never plays, always the loud system/default alarm. App: LifeLoop (com.stujacks.lifeloop) on iOS 26.5 (physical device). https://developer.apple.com/forums/thread/814732
1
0
61
3d
APNs push never delivered — even a direct FCM test message fails
Push notifications are never delivered to my iOS app and I've exhausted the usual checks. Bundle: com.thomasmanfredini.lafrench, Team 7TWV3839F3, TestFlight (production). Architecture: Firebase Cloud Messaging (the app registers an FCM token; Firebase relays to APNs). Key symptom: a direct FCM test message (Firebase Console → "Send test message") to a valid registration token delivers nothing (app killed, lock screen). Already verified (all correct): notification permission ON (incl. Lock Screen); TestFlight build with aps-environment=production; GoogleService-Info.plist matches the Firebase iOS app (project la-french, sender 649219813428); FCM token generated and valid; APNs Auth Key in Firebase with correct Key ID + Team ID — tried two keys (one Production, one Sandbox & Production), neither delivers; bundle ID matches the APNs topic. Since even a direct FCM test fails with a valid token and correct config, what could prevent APNs from delivering to this app? What should I check next (account-level APNs, Push Notifications capability on the App ID, etc.)? Thanks!
4
0
94
3d
StoreKit 2: New offer code NOT in Transaction.currentEntitlements()
This is production issue with a user completely stuck: User entered an offer code for 1 year free --> validated under iOS > Apple Account > subscriptions: it correctly shows a 1 year free trial But the transaction is not listed in his Transaction.currentEntitlements() Even after a restore (App.sync()) The Apple Account has always been the same (no mix) This is with the new offer codes introduced in 2026 Also, user wanted to pay the subscription himself in order to unlock the situation, he cannot because of the 'active' offer. Tried giving him another code, but it's refused by the system because there is only one active. Urgent help would be greatly appreciated. func readEntitlements(fromDeferredTransaction: Bool = false) async { var purchasedNonConsumables: [Product] = [] var purchasedSubscriptions: [Product] = [] var purchasedNonRenewableSubscriptions: [Product] = [] var activeSubTransactions: [Transaction] = [] //Iterate through all of the user's purchased products. for await result in Transaction.currentEntitlements { // currentEntitlements is a StoreKit2 useful feature that only gives us the relevant transactions (not the old & expired ones) do { //Check whether the transaction is verified. If it isn’t, catch `failedVerification` error. let transaction = try checkVerified(result) //Check the `productType` of the transaction and get the corresponding product from the store. switch transaction.productType { case .nonConsumable: if let nc = availableNonConsumables.first(where: { $0.id == transaction.productID }) { purchasedNonConsumables.append(nc) } case .nonRenewable: if let nonRenewable = availableNonRenewableSubscriptions.first(where: { $0.id == transaction.productID }) { let currentDate = Date() let expirationDate = Calendar(identifier: .gregorian).date(byAdding: DateComponents(year: 1), to: transaction.purchaseDate)! if currentDate < expirationDate { purchasedNonRenewableSubscriptions.append(nonRenewable) } } case .autoRenewable: if transaction.revocationDate == nil { activeSubTransactions.append(transaction) } if let subscription = availableSubscriptions.first(where: { $0.id == transaction.productID }) { DLog("Found valid entitlement. Subscription with exp date = \(String(describing: transaction.expirationDate))") purchasedSubscriptions.append(subscription) } else { DLog("Entitled to \(transaction.productID) but its Product is not loaded (product fetch failed/incomplete). Access will be granted from the transaction.") } default: break } } catch { print() } } //Update the store information with the purchased products. self.purchasedNonConsumables = purchasedNonConsumables self.purchasedNonRenewableSubscriptions = purchasedNonRenewableSubscriptions //Update the store information with auto-renewable subscription products. self.purchasedSubscriptions = purchasedSubscriptions //Authoritative entitlement transactions (independent of the product fetch succeeding). self.activeSubTransactions = activeSubTransactions subscriptionGroupStatus = try? await availableSubscriptions.first?.subscription?.status.first?.state // Callback IAPManager.shared.updateProStatus(isSureThatUserIsOnline: fromDeferredTransaction) }
3
0
130
3d
Moving from Multipeer Connectivity to Network Framework
I see a lot of folks spend a lot of time trying to get Multipeer Connectivity to work for them. My experience is that the final result is often unsatisfactory. Instead, my medium-to-long term recommendation is to use Network framework instead. This post explains how you might move from Multipeer Connectivity to Network framework. If you have questions or comments, put them in a new thread. Place it in the App & System Services > Networking topic area and tag it with Multipeer Connectivity and Network framework. IMPORTANT Xcode 27 beta has formally deprecated Multipeer Connectivity. I plan to properly update this post soon. In the meantime, the existing text is still perfectly valid if your app needs to support older systems, where it can’t take advantage of the nice new Network framework API we added in iOS 26 and aligned releases. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Moving from Multipeer Connectivity to Network Framework Multipeer Connectivity has a number of drawbacks: It has an opinionated networking model, where every participant in a session is a symmetric peer. Many apps work better with the traditional client/server model. It offers good latency but poor throughput. It doesn’t support flow control, aka back pressure, which severely constrains its utility for general-purpose networking. It includes a number of UI components that are effectively obsolete. It hasn’t evolved in recent years. For example, it relies on NSStream, which has been scheduled for deprecation as far as networking is concerned. It always enables peer-to-peer Wi-Fi, something that’s not required for many apps and can impact the performance of the network (see Enable peer-to-peer Wi-Fi, below, for more about this). Its security model requires the use of PKI — public key infrastructure, that is, digital identities and certificates — which are tricky to deploy in a peer-to-peer environment. It has some gnarly bugs. IMPORTANT Many folks use Multipeer Connectivity because they think it’s the only way to use peer-to-peer Wi-Fi. That’s not the case. Network framework has opt-in peer-to-peer Wi-Fi support. See Enable peer-to-peer Wi-Fi, below. If Multipeer Connectivity is not working well for you, consider moving to Network framework. This post explains how to do that in 13 easy steps (-: Plan for security Select a network architecture Create a peer identifier Choose a protocol to match your send mode Discover peers Design for privacy Configure your connections Manage a listener Manage a connection Send and receive reliable messages Send and receive best effort messages Start a stream Send a resource Finally, at the end of the post you’ll find two appendices: Final notes contains some general hints and tips. Symbol cross reference maps symbols in the Multipeer Connectivity framework to sections of this post. Consult it if you’re not sure where to start with a specific Multipeer Connectivity construct. Plan for security The first thing you need to think about is security. Multipeer Connectivity offers three security models, expressed as choices in the MCEncryptionPreference enum: .none for no security .optional for optional security .required for required security For required security each peer must have a digital identity. Optional security is largely pointless. It’s more complex than no security but doesn’t yield any benefits. So, in this post we’ll focus on the no security and required security models. Your security choice affects the network protocols you can use: QUIC is always secure. WebSocket, TCP, and UDP can be used with and without TLS security. QUIC security only supports PKI. TLS security supports both TLS-PKI and pre-shared key (PSK). You might find that TLS-PSK is easier to deploy in a peer-to-peer environment. To configure the security of the QUIC protocol: func quicParameters() -> NWParameters { let quic = NWProtocolQUIC.Options(alpn: ["MyAPLN"]) let sec = quic.securityProtocolOptions … configure `sec` here … return NWParameters(quic: quic) } To enable TLS over TCP: func tlsOverTCPParameters() -> NWParameters { let tcp = NWProtocolTCP.Options() let tls = NWProtocolTLS.Options() let sec = tls.securityProtocolOptions … configure `sec` here … return NWParameters(tls: tls, tcp: tcp) } To enable TLS over UDP, also known as DTLS: func dtlsOverUDPParameters() -> NWParameters { let udp = NWProtocolUDP.Options() let dtls = NWProtocolTLS.Options() let sec = dtls.securityProtocolOptions … configure `sec` here … return NWParameters(dtls: dtls, udp: udp) } To configure TLS with a local digital identity and custom server trust evaluation: func configureTLSPKI(sec: sec_protocol_options_t, identity: SecIdentity) { let secIdentity = sec_identity_create(identity)! sec_protocol_options_set_local_identity(sec, secIdentity) if disableServerTrustEvaluation { sec_protocol_options_set_verify_block(sec, { metadata, secTrust, completionHandler in let trust = sec_trust_copy_ref(secTrust).takeRetainedValue() … evaluate `trust` here … completionHandler(true) }, .main) } } To configure TLS with a pre-shared key: func configureTLSPSK(sec: sec_protocol_options_t, identity: Data, key: Data) { let identityDD = identity.withUnsafeBytes { DispatchData(bytes: $0) } let keyDD = identity.withUnsafeBytes { DispatchData(bytes: $0) } sec_protocol_options_add_pre_shared_key( sec, keyDD as dispatch_data_t, identityDD as dispatch_data_t ) sec_protocol_options_append_tls_ciphersuite( sec, tls_ciphersuite_t(rawValue: TLS_PSK_WITH_AES_128_GCM_SHA256)! ) } Select a network architecture Multipeer Connectivity uses a fully connected network architecture. All peers are equal, and every peer is effectively connected to every peer. Many apps work better with the client/server model, where one peer acts on the server and all the others are clients. Network framework supports both models. To implement a client/server network architecture with Network framework: Designate one peer as the server and all the others as clients. On the server, use NWListener to listen for incoming connections. On each client, use NWConnection to made an outgoing connection to the server. To implement a fully connected network architecture with Network framework: On each peer, start a listener. And also start a connection to each of the other peers. This is likely to generate a lot of redundant connections, as peer A connects to peer B and vice versa. You’ll need to a way to deduplicate those connections, which is the subject of the next section. IMPORTANT While the fully connected network architecture is more likely to create redundant connections, the client/server network architecture can generate redundant connections as well. The advice in the next section applies to both architectures. Create a peer identifier Multipeer Connectivity uses MCPeerID to uniquely identify each peer. There’s nothing particularly magic about MCPeerID; it’s effectively a wrapper around a large random number. To identify each peer in Network framework, generate your own large random number. One good choice for a peer identifier is a locally generated UUID, created using the system UUID type. Some Multipeer Connectivity apps persist their local MCPeerID value, taking advantage of its NSSecureCoding support. You can do the same with a UUID, using either its string representation or its Codable support. IMPORTANT Before you decide to persist a peer identifier, think about the privacy implications. See Design for privacy below. Avoid having multiple connections between peers; that’s both wasteful and potentially confusing. Use your peer identifier to deduplicate connections. Deduplicating connections in a client/server network architecture is easy. Have each client check in with the server with its peer identifier. If the server already has a connection for that identifier, it can either close the old connection and keep the new connection, or vice versa. Deduplicating connections in a fully connected network architecture is a bit trickier. One option is to have each peer send its peer identifier to the other peer and then the peer with the ‘best’ identifier wins. For example, imagine that peer A makes an outgoing connection to peer B while peer B is simultaneously making an outgoing connection to peer A. When a peer receives a peer identifier from a connection, it checks for a duplicate. If it finds one, it compares the peer identifiers and then chooses a connection to drop based on that comparison: if local peer identifier > remote peer identifier then drop outgoing connection else drop incoming connection end if So, peer A drops its incoming connection and peer B drops its outgoing connection. Et voilà! Choose a protocol to match your send mode Multipeer Connectivity offers two send modes, expressed as choices in the MCSessionSendDataMode enum: .reliable for reliable messages .unreliable for best effort messages Best effort is useful when sending latency-sensitive data, that is, data where retransmission is pointless because, by the retransmission arrives, the data will no longer be relevant. This is common in audio and video applications. In Network framework, the send mode is set by the connection’s protocol: A specific QUIC connection is either reliable or best effort. WebSocket and TCP are reliable. UDP is best effort. Start with a reliable connection. In many cases you can stop there, because you never need a best effort connection. If you’re not sure which reliable protocol to use, choose WebSocket. It has key advantages over other protocols: It supports both security models: none and required. Moreover, its required security model supports both TLS-PKI and TLS PSK. In contrast, QUIC only supports the required security model, and within that model it only supports TLS-PKI. It allows you to send messages over the connection. In contrast, TCP works in terms of bytes, meaning that you have to add your own framing. If you need a best effort connection, get started with a reliable connection and use that connection to set up a parallel best effort connection. For example, you might have an exchange like this: Peer A uses its reliable WebSocket connection to peer B to send a request for a parallel best effort UDP connection. Peer B receives that, opens a UDP listener, and sends the UDP listener’s port number back to peer A. Peer A opens its parallel UDP connection to that port on peer B. Note For step 3, get peer B’s IP address from the currentPath property of the reliable WebSocket connection. If you’re not sure which best effort protocol to use, use UDP. While it is possible to use QUIC in datagram mode, it has the same security complexities as QUIC in reliable mode. Discover peers Multipeer Connectivity has a types for advertising a peer’s session (MCAdvertiserAssistant) and a type for browsering for peer (MCNearbyServiceBrowser). In Network framework, configure the listener to advertise its service by setting the service property of NWListener: let listener: NWListener = … listener.service = .init(type: "_example._tcp") listener.serviceRegistrationUpdateHandler = { change in switch change { case .add(let endpoint): … update UI for the added listener endpoint … break case .remove(let endpoint): … update UI for the removed listener endpoint … break @unknown default: break } } listener.stateUpdateHandler = … handle state changes … listener.newConnectionHandler = … handle the new connection … listener.start(queue: .main) This example also shows how to use the serviceRegistrationUpdateHandler to update your UI to reflect changes in the listener. Note This example uses a service type of _example._tcp. See About service types, below, for more details on that. To browse for services, use NWBrowser: let browser = NWBrowser(for: .bonjour(type: "_example._tcp", domain: nil), using: .tcp) browser.browseResultsChangedHandler = { latestResults, _ in … update UI to show the latest results … } browser.stateUpdateHandler = … handle state changes … browser.start(queue: .main) This yields NWEndpoint values for each peer that it discovers. To connect to a given peer, create an NWConnection with that endpoint. About service types The examples in this post use _example._tcp for the service type. The first part, _example, is directly analogous to the serviceType value you supply when creating MCAdvertiserAssistant and MCNearbyServiceBrowser objects. The second part is either _tcp or _udp depending on the underlying transport protocol. For TCP and WebSocket, use _tcp. For UDP and QUIC, use _udp. Service types are described in RFC 6335. If you deploy an app that uses a new service type, register that service type with IANA. Discovery UI Multipeer Connectivity also has UI components for advertising (MCNearbyServiceAdvertiser) and browsing (MCBrowserViewController). There’s no direct equivalent to this in Network framework. Instead, use your preferred UI framework to create a UI that best suits your requirements. Note If you’re targeting Apple TV, check out the DeviceDiscoveryUI framework. Discovery TXT records The Bonjour service discovery protocol used by Network framework supports TXT records. Using these, a listener can associate metadata with its service and a browser can get that metadata for each discovered service. To advertise a TXT record with your listener, include it it the service property value: let listener: NWListener = … let peerID: UUID = … var txtRecord = NWTXTRecord() txtRecord["peerID"] = peerID.uuidString listener.service = .init(type: "_example._tcp", txtRecord: txtRecord.data) To browse for services and their associated TXT records, use the .bonjourWithTXTRecord(…) descriptor: let browser = NWBrowser(for: .bonjourWithTXTRecord(type: "_example._tcp", domain: nil), using: .tcp) browser.browseResultsChangedHandler = { latestResults, _ in for result in latestResults { guard case .bonjour(let txtRecord) = result.metadata, let peerID = txtRecord["peerID"] else { continue } // … examine `result` and `peerID` … _ = peerID } } This example includes the peer identifier in the TXT record with the goal of reducing the number of duplicate connections, but that’s just one potential use for TXT records. Design for privacy This section lists some privacy topics to consider as you implement your app. Obviously this isn’t an exhaustive list. For general advice on this topic, see Protecting the User’s Privacy. There can be no privacy without security. If you didn’t opt in to security with Multipeer Connectivity because you didn’t want to deal with PKI, consider the TLS-PSK options offered by Network framework. For more on this topic, see Plan for security. When you advertise a service, the default behaviour is to use the user-assigned device name as the service name. To override that, create a service with a custom name: let listener: NWListener = … let name: String = … listener.service = .init(name: name, type: "_example._tcp") It’s not uncommon for folks to use the peer identifier as the service name. Whether that’s a good option depends on the user experience of your product: Some products present a list of remote peers and have the user choose from that list. In that case it’s best to stick with the user-assigned device name, because that’s what the user will recognise. Some products automatically connect to services as they discover them. In that case it’s fine to use the peer identifier as the service name, because the user won’t see it anyway. If you stick with the user-assigned device name, consider advertising the peer identifier in your TXT record. See Discovery TXT records. IMPORTANT Using a peer identifier in your service name or TXT record is a heuristic to reduce the number of duplicate connections. Don’t rely on it for correctness. Rather, deduplicate connections using the process described in Create a peer identifier. There are good reasons to persist your peer identifier, but doing so isn’t great for privacy. Persisting the identifier allows for tracking of your service over time and between networks. Consider whether you need a persistent peer identifier at all. If you do, consider whether it makes sense to rotate it over time. A persistent peer identifier is especially worrying if you use it as your service name or put it in your TXT record. Configure your connections Multipeer Connectivity’s symmetric architecture means that it uses a single type, MCSession, to manage the connections to all peers. In Network framework, that role is fulfilled by two types: NWListener to listen for incoming connections. NWConnection to make outgoing connections. Both types require you to supply an NWParameters value that specifies the network protocol and options to use. In addition, when creating an NWConnection you pass in an NWEndpoint to tell it the service to connect to. For example, here’s how to configure a very simple listener for TCP: let parameters = NWParameters.tcp let listener = try NWListener(using: parameters) … continue setting up the listener … And here’s how you might configure an outgoing TCP connection: let parameters = NWParameters.tcp let endpoint = NWEndpoint.hostPort(host: "example.com", port: 80) let connection = NWConnection.init(to: endpoint, using: parameters) … continue setting up the connection … NWParameters has properties to control exactly what protocol to use and what options to use with those protocols. To work with QUIC connections, use code like that shown in the quicParameters() example from the Security section earlier in this post. To work with TCP connections, use the NWParameters.tcp property as shown above. To enable TLS on your TCP connections, use code like that shown in the tlsOverTCPParameters() example from the Security section earlier in this post. To work with WebSocket connections, insert it into the application protocols array: let parameters = NWParameters.tcp let ws = NWProtocolWebSocket.Options(.version13) parameters.defaultProtocolStack.applicationProtocols.insert(ws, at: 0) To enable TLS on your WebSocket connections, use code like that shown in the tlsOverTCPParameters() example to create your base parameters and then add the WebSocket application protocol to that. To work with UDP connections, use the NWParameters.udp property: let parameters = NWParameters.udp To enable TLS on your UDP connections, use code like that shown in the dtlsOverUDPParameters() example from the Security section earlier in this post. Enable peer-to-peer Wi-Fi By default, Network framework doesn’t use peer-to-peer Wi-Fi. To enable that, set the includePeerToPeer property on the parameters used to create your listener and connection objects. parameters.includePeerToPeer = true IMPORTANT Enabling peer-to-peer Wi-Fi can impact the performance of the network. Only opt into it if it’s a significant benefit to your app. If you enable peer-to-peer Wi-Fi, it’s critical to stop network operations as soon as you’re done with them. For example, if you’re browsing for services with peer-to-peer Wi-Fi enabled and the user picks a service, stop the browse operation immediately. Otherwise, the ongoing browse operation might affect the performance of your connection. Manage a listener In Network framework, use NWListener to listen for incoming connections: let parameters: NWParameters = .tcp … configure parameters … let listener = try NWListener(using: parameters) listener.service = … service details … listener.serviceRegistrationUpdateHandler = … handle service registration changes … listener.stateUpdateHandler = { newState in … handle state changes … } listener.newConnectionHandler = { newConnection in … handle the new connection … } listener.start(queue: .main) For details on how to set up parameters, see Configure your connections. For details on how to set up up service and serviceRegistrationUpdateHandler, see Discover peers. Network framework calls your state update handler when the listener changes state: let listener: NWListener = … listener.stateUpdateHandler = { newState in switch newState { case .setup: // The listener has not yet started. … case .waiting(let error): // The listener tried to start and failed. It might recover in the // future. … case .ready: // The listener is running. … case .failed(let error): // The listener tried to start and failed irrecoverably. … case .cancelled: // The listener was cancelled by you. … @unknown default: break } } Network framework calls your new connection handler when a client connects to it: var connections: [NWConnection] = [] let listener: NWListener = listener listener.newConnectionHandler = { newConnection in … configure the new connection … newConnection.start(queue: .main) connections.append(newConnection) } IMPORTANT Don’t forget to call start(queue:) on your connections. In Multipeer Connectivity, the session (MCSession) keeps track of all the peers you’re communicating with. With Network framework, that responsibility falls on you. This example uses a simple connections array for that purpose. In your app you may or may not need a more complex data structure. For example: In the client/server network architecture, the client only needs to manage the connections to a single peer, the server. On the other hand, the server must managed the connections to all client peers. In the fully connected network architecture, every peer must maintain a listener and connections to each of the other peers. Understand UDP flows Network framework handles UDP using the same NWListener and NWConnection types as it uses for TCP. However, the underlying UDP protocol is not implemented in terms of listeners and connections. To resolve this, Network framework works in terms of UDP flows. A UDP flow is defined as a bidirectional sequence of UDP datagrams with the same 4 tuple (local IP address, local port, remote IP address, and remote port). In Network framework: Each NWConnection object manages a single UDP flow. If an NWListener receives a UDP datagram whose 4 tuple doesn’t match any known NWConnection, it creates a new NWConnection. Manage a connection In Network framework, use NWConnection to start an outgoing connection: var connections: [NWConnection] = [] let parameters: NWParameters = … let endpoint: NWEndpoint = … let connection = NWConnection(to: endpoint, using: parameters) connection.stateUpdateHandler = … handle state changes … connection.viabilityUpdateHandler = … handle viability changes … connection.pathUpdateHandler = … handle path changes … connection.betterPathUpdateHandler = … handle better path notifications … connection.start(queue: .main) connections.append(connection) As in the listener case, you’re responsible for keeping track of this connection. Each connection supports four different handlers. Of these, the state and viability update handlers are the most important. For information about the path update and better path handlers, see the NWConnection documentation. Network framework calls your state update handler when the connection changes state: let connection: NWConnection = … connection.stateUpdateHandler = { newState in switch newState { case .setup: // The connection has not yet started. … case .preparing: // The connection is starting. … case .waiting(let error): // The connection tried to start and failed. It might recover in the // future. … case .ready: // The connection is running. … case .failed(let error): // The connection tried to start and failed irrecoverably. … case .cancelled: // The connection was cancelled by you. … @unknown default: break } } If you a connection is in the .waiting(_:) state and you want to force an immediate retry, call the restart() method. Network framework calls your viability update handler when its viability changes: let connection: NWConnection = … connection.viabilityUpdateHandler = { isViable in … react to viability changes … } A connection becomes inviable when a network resource that it depends on is unavailable. A good example of this is the network interface that the connection is running over. If you have a connection running over Wi-Fi, and the user turns off Wi-Fi or moves out of range of their Wi-Fi network, any connection running over Wi-Fi becomes inviable. The inviable state is not necessarily permanent. To continue the above example, the user might re-enable Wi-Fi or move back into range of their Wi-Fi network. If the connection becomes viable again, Network framework calls your viability update handler with a true value. It’s a good idea to debounce the viability handler. If the connection becomes inviable, don’t close it down immediately. Rather, wait for a short while to see if it becomes viable again. If a connection has been inviable for a while, you get to choose as to how to respond. For example, you might close the connection down or inform the user. To close a connection, call the cancel() method. This gracefully disconnects the underlying network connection. To close a connection immediately, call the forceCancel() method. This is not something you should do as a matter of course, but it does make sense in exceptional circumstances. For example, if you’ve determined that the remote peer has gone deaf, it makes sense to cancel it in this way. Send and receive reliable messages In Multipeer Connectivity, a single session supports both reliable and best effort send modes. In Network framework, a connection is either reliable or best effort, depending on the underlying network protocol. The exact mechanism for sending a message depends on the underlying network protocol. A good protocol for reliable messages is WebSocket. To send a message on a WebSocket connection: let connection: NWConnection = … let message: Data = … let metadata = NWProtocolWebSocket.Metadata(opcode: .binary) let context = NWConnection.ContentContext(identifier: "send", metadata: [metadata]) connection.send(content: message, contentContext: context, completion: .contentProcessed({ error in // … check `error` … _ = error })) In WebSocket, the content identifier is ignored. Using an arbitrary fixed value, like the send in this example, is just fine. Multipeer Connectivity allows you to send a message to multiple peers in a single send call. In Network framework each send call targets a specific connection. To send a message to multiple peers, make a send call on the connection associated with each peer. If your app needs to transfer arbitrary amounts of data on a connection, it must implement flow control. See Start a stream, below. To receive messages on a WebSocket connection: func startWebSocketReceive(on connection: NWConnection) { connection.receiveMessage { message, _, _, error in if let error { … handle the error … return } if let message { … handle the incoming message … } startWebSocketReceive(on: connection) } } IMPORTANT WebSocket preserves message boundaries, which is one of the reasons why it’s ideal for your reliable messaging connections. If you use a streaming protocol, like TCP or QUIC streams, you must do your own framing. A good way to do that is with NWProtocolFramer. If you need the metadata associated with the message, get it from the context parameter: connection.receiveMessage { message, context, _, error in … if let message, let metadata = context?.protocolMetadata(definition: NWProtocolWebSocket.definition) as? NWProtocolWebSocket.Metadata { … handle the incoming message and its metadata … } … } Send and receive best effort messages In Multipeer Connectivity, a single session supports both reliable and best effort send modes. In Network framework, a connection is either reliable or best effort, depending on the underlying network protocol. The exact mechanism for sending a message depends on the underlying network protocol. A good protocol for best effort messages is UDP. To send a message on a UDP connection: let connection: NWConnection = … let message: Data = … connection.send(content: message, completion: .idempotent) IMPORTANT UDP datagrams have a theoretical maximum size of just under 64 KiB. However, sending a large datagram results in IP fragmentation, which is very inefficient. For this reason, Network framework prevents you from sending UDP datagrams that will be fragmented. To find the maximum supported datagram size for a connection, gets its maximumDatagramSize property. To receive messages on a UDP connection: func startUDPReceive(on connection: NWConnection) { connection.receiveMessage { message, _, _, error in if let error { … handle the error … return } if let message { … handle the incoming message … } startUDPReceive(on: connection) } } This is exactly the same code as you’d use for WebSocket. Start a stream In Multipeer Connectivity, you can ask the session to start a stream to a specific peer. There are two ways to achieve this in Network framework: If you’re using QUIC for your reliable connection, start a new QUIC stream over that connection. This is one place that QUIC shines. You can run an arbitrary number of QUIC connections over a single QUIC connection group, and QUIC manages flow control (see below) for each connection and for the group as a whole. If you’re using some other protocol for your reliable connection, like WebSocket, you must start a new connection. You might use TCP for this new connection, but it’s not unreasonable to use WebSocket or QUIC. If you need to open a new connection for your stream, you can manage that process over your reliable connection. Choose a protocol to match your send mode explains the general approach for this, although in that case it’s opening a parallel best effort UDP connection rather than a parallel stream connection. The main reason to start a new stream is that you want to send a lot of data to the remote peer. In that case you need to worry about flow control. Flow control applies to both the send and receive side. IMPORTANT Failing to implement flow control can result in unbounded memory growth in your app. This is particularly bad on iOS, where jetsam will terminate your app if it uses too much memory. On the send side, implement flow control by waiting for the connection to call your completion handler before generating and sending more data. For example, on a TCP connection or QUIC stream you might have code like this: func sendNextChunk(on connection: NWConnection) { let chunk: Data = … read next chunk from disk … connection.send(content: chunk, completion: .contentProcessed({ error in if let error { … handle error … return } sendNextChunk(on: connection) })) } This acts like an asynchronous loop. The first send call completes immediately because the connection just copies the data to its send buffer. In response, your app generates more data. This continues until the connection’s send buffer fills up, at which point it defers calling your completion handler. Eventually, the connection moves enough data across the network to free up space in its send buffer, and calls your completion handler. Your app generates another chunk of data For best performance, use a chunk size of at least 64 KiB. If you’re expecting to run on a fast device with a fast network, a chunk size of 1 MiB is reasonable. Receive-side flow control is a natural extension of the standard receive pattern. For example, on a TCP connection or QUIC stream you might have code like this: func receiveNextChunk(on connection: NWConnection) { let chunkSize = 64 * 1024 connection.receive(minimumIncompleteLength: chunkSize, maximumLength: chunkSize) { chunk, _, isComplete, error in if let chunk { … write chunk to disk … } if isComplete { … close the file … return } if let error { … handle the error … return } receiveNextChunk(on: connection) } } IMPORTANT The above is cast in terms of writing the chunk to disk. That’s important, because it prevents unbounded memory growth. If, for example, you accumulated the chunks into an in-memory buffer, that buffer could grow without bound, which risks jetsam terminating your app. The above assumes that you can read and write chunks of data synchronously and promptly, for example, reading and writing a file on a local disk. That’s not always the case. For example, you might be writing data to an accessory over a slow interface, like Bluetooth LE. In such cases you need to read and write each chunk asynchronously. This results in a structure where you read from an asynchronous input and write to an asynchronous output. For an example of how you might approach this, albeit in a very different context, see Handling Flow Copying. Send a resource In Multipeer Connectivity, you can ask the session to send a complete resource, identified by either a file or HTTP URL, to a specific peer. Network framework has no equivalent support for this, but you can implement it on top of a stream: To send, open a stream and then read chunks of data using URLSession and send them over that stream. To receive, open a stream and then receive chunks of data from that stream and write those chunks to disk. In this situation it’s critical to implement flow control, as described in the previous section. Final notes This section collects together some general hints and tips. Concurrency In Multipeer Connectivity, each MCSession has its own internal queue and calls delegate callbacks on that queue. In Network framework, you get to control the queue used by each object for its callbacks. A good pattern is to have a single serial queue for all networking, including your listener and all connections. In a simple app it’s reasonable to use the main queue for networking. If you do this, be careful not to do CPU intensive work in your networking callbacks. For example, if you receive a message that holds JPEG data, don’t decode that data on the main queue. Overriding protocol defaults Many network protocols, most notably TCP and QUIC, are intended to be deployed at vast scale across the wider Internet. For that reason they use default options that aren’t optimised for local networking. Consider changing these defaults in your app. TCP has the concept of a send timeout. If you send data on a TCP connection and TCP is unable to successfully transfer it to the remote peer within the send timeout, TCP will fail the connection. The default send timeout is infinite. TCP just keeps trying. To change this, set the connectionDropTime property. TCP has the concept of keepalives. If a connection is idle, TCP will send traffic on the connection for two reasons: If the connection is running through a NAT, the keepalives prevent the NAT mapping from timing out. If the remote peer is inaccessible, the keepalives fail, which in turn causes the connection to fail. This prevents idle but dead connections from lingering indefinitely. TCP keepalives default to disabled. To enable and configure them, set the enableKeepalive property. To configure their behaviour, set the keepaliveIdle, keepaliveCount, and keepaliveInterval properties. Symbol cross reference If you’re not sure where to start with a specific Multipeer Connectivity construct, find it in the tables below and follow the link to the relevant section. [Sorry for the poor formatting here. DevForums doesn’t support tables properly, so I’ve included the tables as preformatted text.] | For symbol | See | | ----------------------------------- | --------------------------- | | `MCAdvertiserAssistant` | *Discover peers* | | `MCAdvertiserAssistantDelegate` | *Discover peers* | | `MCBrowserViewController` | *Discover peers* | | `MCBrowserViewControllerDelegate` | *Discover peers* | | `MCNearbyServiceAdvertiser` | *Discover peers* | | `MCNearbyServiceAdvertiserDelegate` | *Discover peers* | | `MCNearbyServiceBrowser` | *Discover peers* | | `MCNearbyServiceBrowserDelegate` | *Discover peers* | | `MCPeerID` | *Create a peer identifier* | | `MCSession` | See below. | | `MCSessionDelegate` | See below. | Within MCSession: | For symbol | See | | --------------------------------------------------------- | ------------------------------------ | | `cancelConnectPeer(_:)` | *Manage a connection* | | `connectedPeers` | *Manage a listener* | | `connectPeer(_:withNearbyConnectionData:)` | *Manage a connection* | | `disconnect()` | *Manage a connection* | | `encryptionPreference` | *Plan for security* | | `myPeerID` | *Create a peer identifier* | | `nearbyConnectionData(forPeer:withCompletionHandler:)` | *Discover peers* | | `securityIdentity` | *Plan for security* | | `send(_:toPeers:with:)` | *Send and receive reliable messages* | | `sendResource(at:withName:toPeer:withCompletionHandler:)` | *Send a resource* | | `startStream(withName:toPeer:)` | *Start a stream* | Within MCSessionDelegate: | For symbol | See | | ---------------------------------------------------------------------- | ------------------------------------ | | `session(_:didFinishReceivingResourceWithName:fromPeer:at:withError:)` | *Send a resource* | | `session(_:didReceive:fromPeer:)` | *Send and receive reliable messages* | | `session(_:didReceive:withName:fromPeer:)` | *Start a stream* | | `session(_:didReceiveCertificate:fromPeer:certificateHandler:)` | *Plan for security* | | `session(_:didStartReceivingResourceWithName:fromPeer:with:)` | *Send a resource* | | `session(_:peer:didChange:)` | *Manage a connection* | Revision History 2026-06-30 Replaced star network architecture with fully connected network architecture. 2026-06-14 Updated to account for changes in Xcode 27 beta. 2025-04-11 Added some advice as to whether to use the peer identifier in your service name. Expanded the discussion of how to deduplicate connections in a fully connected network architecture. 2025-03-20 Added a link to the DeviceDiscoveryUI framework to the Discovery UI section. Made other minor editorial changes. 2025-03-11 Expanded the Enable peer-to-peer Wi-Fi section to stress the importance of stopping network operations once you’re done with them. Added a link to that section from the list of Multipeer Connectivity drawbacks. 2025-03-07 First posted.
0
0
2.3k
3d
LiveActivity push-to-start flow inconsistency
Problem: unable to retrieve push-to-start token upon app start Environment: simulator, iPhone 15 Pro, iOS 18.1 (also reproduced on physical devices) First image is a start from XCode with "Run" option (Cmd+R) where debugger is linked Second image is a "cold" start - starting app by tapping the icon Difference is: Start observing (line 2), push token changed (last line). Everything is okay Push token changed (7 line), start observing (9 line). liveactivitysd generates token before we reach pushToStartTokenUpdates loop - we losing token The question is: how is this possible (case 2)?
0
0
72
3d
iOS 26.5 breaks AppIntents with AppEnums defined in Swift Package
For some reason since iOS 26.5, a ControlWidgetButton or a Button in a widget configured with an AppIntent conforming to OpenIntent fails to receive its AppEnum parameter correctly if the intent and enum are defined inside a shared Swift Package. Tapping the control widget button opens the main application, but it doesn't pass the AppEnum parameter value in, leaving the app on its default screen instead of navigating to the intended view. This was working perfectly in iOS 26.4, where the app would launch and receive the correct enum case from the intent. No code, configuration, or scheme changes were made between the two runs, only the simulator/OS version differs. Has anyone else run into what seems to be a regression on iOS 26.5? I filed feedback with a sample project: FB22848510
1
2
447
3d
Latency critical DMA read via PCIe
Dear All, I am currently developing a high throughput audio system which operates via PCIe tunneled into a USB4 interface. This include a custom FPGA based hardware and custom Audio DriverKit driver. While performing read operation via the hw DMA (that is a Host to Device transfer), I am noticing sparse latency spikes into the read transfers. Specifically, 4KB operations (which I assume including MRd + CpID) take normally from 5us to 40us to be completed, perfectly fine for my case. However, in some rare occasions, they can end up to 400us, which causes me overruns. The measurements have been carried out from the FPGA and they include the overall request and transfer time. While trying to tackle the problem, I'm investigating the possible power saving options and performance constraint methods at my disposal. I currently use these methods to mitigate the problem. ChangePowerState(kIOServicePowerCapabilityOn); SetPowerOverride(true); RequireMaxBusStall(kIOMaxBusStall25usec); CreatePMAssertion(kIOServicePMAssertionCPUBit | kIOServicePMAssertionForceFullWakeupBit, &ivars->PMAssertionID, false); The buffers are currently about 16MB, single segment, 16KB aligned and, of course, "prepared" for DMA. The system run for 3 hours without any overrun, but I'm not still fully convinced about its reliability. May someone provide me some comments on this? Are there profiling tools that I can use? Feel free to request me any required detail. The testing system is a MacBook Pro M2 Pro. Many Thanks and Best Regards Francesco
9
0
468
4d
StoreKit 2 returns no products for auto-renewable subscriptions in TestFlight
Hi everyone, I’m having an issue where my TestFlight build cannot load auto-renewable subscription products from App Store Connect. The app is built with Flutter and uses StoreKit 2 through an in-app purchase implementation. The same subscription system works correctly on Google Play Billing and returns product details/prices there, but on iOS StoreKit returns no subscription products. I tested this on a real iPhone installed from TestFlight, not from Xcode debug and not using a local StoreKit configuration. The app requests four auto-renewable subscription product IDs. The IDs in code exactly match the product IDs in App Store Connect. The app diagnostics show: Platform: iOS StoreKit mode: StoreKit 2 Store available: true Requested IDs: 4 subscription product IDs Returned IDs: none Missing IDs: all requested product IDs Error source: app_store Error code: storekit_no_response Error message: StoreKit: Failed to get response from platform. Store note: The store accepted the query, but did not return details for these product IDs. Things I have already checked: Product IDs match exactly between Flutter code and App Store Connect Bundle ID matches the App Store Connect app record In-App Purchase capability is enabled Subscriptions are in one subscription group Subscription availability is set for all countries/regions The TestFlight build is installed on a real device Local StoreKit configuration was removed StoreKit reports store availability as true App Store Connect / In-App Purchase keys are configured for backend verification The same product-loading flow works on Google Play The current App Store Connect status for the subscriptions is: Subscriptions: Ready to Submit / Ready for Review Localizations: Prepare for Submission Also, the bank information is not submitted yet. My question is: can StoreKit in TestFlight return subscription product details while the first auto-renewable subscriptions are still only “Ready to Submit” and have not yet been submitted with an app version for review? Or will StoreKit return no products until: The first subscriptions are attached to an app version and submitted for review Subscription localizations are fully prepared/submitted Paid agreements, tax, and banking are fully completed Since StoreKit is available and accepts the product query, but returns zero products, I’m trying to understand whether this is expected App Store Connect behavior or if there is another configuration issue I should check. Thanks.
1
0
150
4d
Inquiry about Bluetooth State Restoration behavior under iOS 26
Our vehicle digital key app uses CBCentralManagerOptionRestoreIdentifierKey and bluetooth-central background mode to maintain BLE connections with cars. We want to confirm: if the app is actively connected to a BLE vehicle peripheral and then terminated by system memory reclamation (not user force-quit), will iOS automatically relaunch the app via Bluetooth State Restoration when the peripheral sends notifications or re-advertises? Also please list all mandatory conditions to trigger such Bluetooth-based background relaunch after system termination, and clarify whether this pure-BLE relaunch channel works independently apart from iBeacon location-triggered cold launch for our terminated app on iOS 26 without AccessorySetupKit. Best regards
1
0
136
4d
Non-Consumable IAP stuck in ‘Developer Action Required’ – cannot link to version page
My Non-Consumable IAP was rejected with Guideline 3.1.1 and now has status ‘Developer Action Required’. The problem: On the version page in App Store Connect, the ‘In-App Purchases and Subscriptions’ section does not appear at the bottom. I cannot link the IAP to a new version submission. Every time I submit a new binary, Apple approves the app version but rejects the IAP afterward — even though I have no way to attach it to the version in the first place. This seems to be a circular issue: the IAP doesn’t appear on the version page because of its current status, but the only way to fix the status is to submit it with a new version. Has anyone experienced this and found a solution? Any help appreciated!​​​​​​​​​​​​​​​​
3
0
279
4d
Silent Push background launch rejected with "pushDisallowed" and "AMNP" on TestFlight build
Hello, I am implementing background processing using Silent Push Notifications in an iOS application. The app needs to receive requests from a server and execute processing in the background. For this purpose, I am using Silent Push Notifications to wake up the application. However, the application is not being launched as expected when running the TestFlight version. I would appreciate any guidance regarding this behavior. Environment Xcode 26.3 iOS 18.6 TestFlight distribution Push Notifications capability enabled Background Modes enabled Remote notifications enabled APNs Authentication Key (.p8) APNs Production environment Background Previously, the application periodically connected to a server in the background and sent location information when requested by the server. Since that background processing no longer behaves as expected, I started implementing an alternative approach using Silent Push Notifications. When running the application directly from Xcode in Debug mode, Silent Push works correctly and the app is launched in the background. However, when using the TestFlight build on the same device, the behavior differs and the app is not launched in the background. Observed Behavior The APNs request appears to be successful: APNs response status is HTTP 200 Using the device token obtained from the TestFlight build Alert Push notifications are received successfully application:didReceiveRemoteNotification:fetchCompletionHandler: is called when the app is in the foreground The same method is not called when the app is in the background Silent Push payload: { "aps": { "content-available": 1 } } HTTP/2 headers: apns-push-type: background apns-priority: 5 Verified Items Production device token is being used APNs topic matches the application's bundle identifier Alert Push notifications work correctly Background App Refresh is enabled on the device The application has not been force-quit completionHandler is always called after processing completes Console Logs The following logs appear when the push is sent: Submitted: com.apple.pushLaunch.jp.co.comp.MyApp.ext.mdm Application Policy response: {100, 0.00, [{[pushDisallowed]: Required:0.00, Observed:1.00},]} Decision: AMNP I also see: Insufficient history window for deviceActivityLikelihood stream deviceActivityLikelihood returned a nil timeline Questions Under what conditions are pushDisallowed and Decision: AMNP generated? What factors can cause iOS to reject a background launch triggered by a Silent Push Notification? Since Alert Push notifications are received successfully, is it reasonable to conclude that APNs configuration, topic configuration, and device token usage are correct? Are there any specific settings, entitlements, or implementation details that should be reviewed to resolve the pushDisallowed condition? The behavior differs between the Debug build launched from Xcode and the TestFlight build running on the same device. Are there any restrictions, policies, or differences applied to TestFlight builds that could explain this behavior? If the observed behavior is expected by design, what would Apple recommend as the appropriate architecture or API for server-triggered background processing in this scenario? Thank you for your time and assistance.
Replies
3
Boosts
0
Views
253
Activity
2d
Hotspot not disabled on control center through the widget with multiple network
Hotspot not being disabled through the widget with multiple network buttons (dont remember the name of it, it has multiple connection widgets as one). The hotspot is however is shown disabled but its not the correct state as we can discover in settings that is still enabled.
Replies
1
Boosts
0
Views
53
Activity
3d
Requesting private watchOS Bluetooth entitlements for open-source CGM-connected AID app (FB22619409)
I'm a contributor to Trio, an open-source automated insulin delivery (AID) app for iOS/watchOS maintained by the Nightscout Foundation. I'm posting at the recommendation of the engineering team via Feedback Assistant FB22619409 (Developer Support case 102865854185). The goal We're prototyping direct BLE connectivity between the Trio watchOS extension and the Dexcom G7 CGM sensor — eliminating dependence on iPhone-to-Watch relay via WatchConnectivity. For an AID app, stale CGM data affects insulin dosing decisions; this is a patient safety concern. The entitlements needed To maintain a BLE connection to the G7 while backgrounded and with the display off/wrist lowered, the WatchKit extension requires: com.apple.developer.bluetooth-central-background com.apple.developer.bluetooth-central-screen-off-scanning What I've confirmed Both entitlements are present on Dexcom's shipping G7 WatchKit extension (com.dexcom.g7app.watchkitapp.watchkitextension), verifiable via: codesign -d --entitlements :- \ "Dexcom G7.app/Watch/G7Watch.app/PlugIns/G7Watch Extension.appex" Output includes: com.apple.developer.bluetooth-central-background = true com.apple.developer.bluetooth-central-screen-off-scanning = true These are not self-service capabilities exposed through Xcode or the developer portal for our account: Xcode → Trio Watch App target → Signing & Capabilities → + Capability → searching com.apple.developer.bluetooth-central-background returns No Matches Certificates, Identifiers & Profiles → WatchKit Extension App ID (org.nightscout.5QE6TMMEH2.trio.watchkitapp.watchkitextension) → the entitlement does not appear under Capability Requests A screen recording demonstrating both is attached to FB22619409. The May 16 Apple Feedback response noted that the entitlement was visible in an internal Xcode project — consistent with it being a restricted/managed entitlement not exposed through standard developer accounts. My questions What is the correct process to request com.apple.developer.bluetooth-central-background for a watchOS extension App ID where it does not appear in Capability Requests? Is com.apple.developer.bluetooth-central-screen-off-scanning available through a private/managed entitlement process, and how do we enter that process? Is there a formal Apple program (e.g., MFi, HealthKit entitlements, or similar) applicable to CGM-connected medical apps that covers these entitlements? Full account details, screen recording, and entitlement output are attached to FB22619409 / Developer Support case 102865854185. Happy to provide a test build, full entitlement output, or additional context if needed. Thank you
Replies
1
Boosts
0
Views
207
Activity
3d
Error when access StoreView / ProductView on iOS 27.0 simulator
Hi, Feedback: FB23494579 Using StoreView / ProductView on iOS 27.0 simulator / device hub throws the following error: Error: Accessing State<ProductViewEventConfiguration>'s value without being installed on a View. This will create a new ProductViewEventConfiguration instance each time. Environment macOS 26.5.2 (25F84) Xcode 27.0 beta 2 (27A5209h) Simulator / Device Hub - iPhone 17 Pro (iOS 27.0) Screenshot
Replies
0
Boosts
0
Views
64
Activity
3d
Live Activities Push-to-Start flows
Good morning, We are implementing Live Activities in a push-to-start flow. We wrap the listener for push to start tokens in a high priority task: if ptsListenerTask == nil || ptsListenerTask?.isCancelled == true { ptsListenerTask = Task(priority: .high) { [weak self] in for await pushToken in Activity<LiveAuctionAttributes>.pushToStartTokenUpdates { //Send token to back-end } } I've tried a few variations of this and they work well on most devices. I have seen a couple of devices that refuse to issue a push to start token. The user will have logging for the init flow and starting the PTS listener then the logs just go silent, nothing happens. One thing that seemed to work was getting the user to start a Live Activity manually (from our debugging tool) then the PTS token gets issued. This is not very reliable and working a mock live activity into the flow for obtaining a PTS token is a poor solution. Is anyone else seeing this and is there a known issue with obtaining PTS tokens? Thanks! Brad
Replies
12
Boosts
2
Views
772
Activity
3d
TimeDataSource .dateRange(endingAt:) won't update
Hello, I'm trying to add a new Live Activity to my app showing a timer to a specific date and time. I thought I could use some TimeDataSource so that the timer would be updated automatically by SwiftUI without relying on Live Activity updates. That's not the case with .dateRange(endingAt:) though. Text(.dateRange(endingAt: targetDate), format: .components(style: .narrow)) Something like this correctly shows the timer exactly how I want it, but it never updates. Other TimeDataSource like .currentDate and .durationOffset(to:) do update automatically, but are not what I'm looking for. Am I missing something? Should I use another formatter to make it work?
Replies
0
Boosts
0
Views
60
Activity
3d
HKStatisticsCollectionQuery initialResultsHandler returns nil results (error) for one specific user — read auth granted, data exists, survives reinstall
Environment: iPhone 13 Pro, iOS 26.5. Affects a single user out of many; cannot reproduce on any of our test devices. We use HKStatisticsCollectionQuery to read step counts for a statistics screen. For one specific user, the query's initialResultsHandler appears to deliver results == nil (the success branch never runs), so our completion is never called and the screen shows an infinite spinner. private let store = HKHealthStore() func fetchHourlyStepCounts(for day: Date, completion: @escaping ([Int]) -> Void) { guard let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else { return } let calendar = Calendar.current let startOfDay = calendar.startOfDay(for: day) var hourly = DateComponents() hourly.hour = 1 let query = HKStatisticsCollectionQuery( quantityType: stepType, quantitySamplePredicate: nil, options: .cumulativeSum, anchorDate: startOfDay, intervalComponents: hourly ) query.initialResultsHandler = { _, collection, error in guard let collection else { // For the affected user, execution seems to reach here (collection == nil). // Adding logging of the HKError + authorization status for the next occurrence. return } var counts: [Int] = [] let end = calendar.date(byAdding: .day, value: 1, to: startOfDay)! collection.enumerateStatistics(from: startOfDay, to: end) { stats, _ in let steps = stats.sumQuantity()?.doubleValue(for: .count()) ?? 0 counts.append(Int(steps)) } DispatchQueue.main.async { completion(counts) } } store.execute(query) } What we've confirmed / ruled out: Read authorization for stepCount is granted (the user toggled it ON in the HealthKit sheet on video). The Apple Health app shows step data for this user (so data exists). A coarser query (2-year interval) for the same user succeeds, while the hourly query appears to fail — same type / predicate / options / auth. Symptom persists across app reinstall and device reboot, and re-granting Health permission. Permission denial returns empty results (per Apple docs), not an error — so this isn't simple denial. Not errorDatabaseInaccessible as far as we can tell (foreground, device unlocked). Questions: What can cause HKStatisticsCollectionQuery.initialResultsHandler to return results == nil (with an error) persistently for one device/account, when read auth is granted and data exists? Can errorHealthDataRestricted occur without an MDM/supervised profile (i.e., on a normal consumer device)? What device/account states actually trigger it? Is it expected that a coarse-interval query succeeds while an hourly-interval query on the same type fails for the same user? We're adding logging of the actual HKError code + authorizationStatus for the next occurrence, but would appreciate any insight on what conditions produce this.
Replies
0
Boosts
0
Views
66
Activity
3d
Live Activity Stops Updating After 30 Seconds in Background During Audio Playback
Hi I developed a music app that plays offline audio and displays lyrics using Live Activities. According to ActivityKit documentation, Live Activities can be updated from the background. However, in my case, updates stop after ~30 seconds when the app goes to the background or the device is locked. Important points: The app continues running in the background (audio playback works fine using AVAudioSession with .playback) Background code execution is working as expected Only the Live Activity stops updating I am not using push updates since this is an offline app. Is there any limitation or requirement for updating Live Activities continuously in the background during audio playback? Audio Session Configuration let session = AVAudioSession.sharedInstance() try session.setCategory( .playback, mode: .default, options: [.mixWithOthers] // ✅ DO NOT interrupt other audio ) try session.setActive(true) print("✅ [AudioSession] Activated with mixWithOthers") } catch { print("❌ [AudioSession] Error: \(error)") } Live Activity Update Methods guard let activity = getLiveActivity(for: recordID) else{ print("⚠️ No Live Activity found for recordID: \(recordID)") return } guard activity.activityState == .active else { print("⚠️ Activity is not active") return } Task { let content = ActivityContent( state: state, staleDate: Date().addingTimeInterval(60 * 60 * 12), relevanceScore: 1.0 ) await activity.update(content) print("✅ Live Activity updated with ActivityContent") } }
Replies
1
Boosts
0
Views
604
Activity
3d
Enforced segregation between VM and Host
VM are quite interesting options in a BYOD environment but im not able to enforce a proper segregation between VM and Host. Examples: If the VM is MDM enrolled the Host can access the VM Disk quite easily If the Host is managed a VM can be used to bypass Network security Is a mdm enrolled VM a scenario you have on your roadmap?
Replies
2
Boosts
0
Views
200
Activity
3d
Understanding Crash Reporter Extension lifecycle and debugging behavior
Hi! I have a few questions about the lifecycle and capabilities of the Crash Reporter Extension. Besides using the corpsePort to inspect the crashed process through Mach APIs, is it safe/supported/recommended for the extension to access files in a shared App Group container? Are there any caveats or exceptions we should be aware of, for example around memory-mapped files, file coordination, or filesystem access after the host app has crashed? Shall we use some particular APIs for this kind of shared resource or not? While debugging the extension, I noticed that when I trigger a crash in the app I am debugging, LLDB does not stop inside the extension (it also ends up stopping the debugging session). However, I can observe that the extension does run, because it writes data into a shared App Group directory related to the crash. Is this expected behavior? Is there a recommended way to debug the Crash Reporter Extension reliably (with lldb, or other way)? More generally, I would like to better understand the extension lifecycle: When exactly does the extension start running? How long can it live after the app crashes? Is there a time limit for operating on the corpse process? Is the extension subject to resource limits similar to other app extensions, such as memory, disk, CPU, watchdog, or jetsam constraints? If the Crash Reporter Extension itself crashes, how can we detect that? Would those crashes appear in Xcode Organizer, or is there another recommended way to observe them? Any clarification around the supported lifecycle, debugging model, and resource limits would be very useful.
Replies
4
Boosts
1
Views
356
Activity
3d
AlertConfiguration.AlertSound.named plays system default despite CAF in main bundle (AlarmKit, iOS 26.5)
We need an official answer: wrong wiring, known bug on 26.5, or unsupported? AlertConfiguration.AlertSound.default works. AlertConfiguration.AlertSound.named("lifeloop_alarm_silent") schedules successfully (Stop/Snooze UI, no API error) but playback is always system default — never our bundled CAF. OFFICIAL API WE FOLLOW Documentation: AlertConfiguration.AlertSound.named(:) https://developer.apple.com/documentation/activitykit/alertconfiguration/alertsound/named(:) "The name of the sound file to use for the alert. Choose a file that's in your app's main bundle or the Library/Sounds folder of your app's data container." WWDC25 session 230 shows: let sound = AlertConfiguration.AlertSound.named("Chime") with the file in the main bundle. We use: AlertConfiguration.AlertSound.named("lifeloop_alarm_silent") Our Swift schedules with AlarmManager.AlarmConfiguration.alarm(..., sound: alertSound). REFERENCES WE ALREADY READ AlarmConfiguration overview: https://developer.apple.com/documentation/alarmkit/alarmmanager/alarmconfiguration Albert Pascual DTS reply (Feb 2026) on this thread pointed at nested alertConfiguration with sound AlarmKit FAQ: https://developer.apple.com/forums/thread/797158 Older forum reply (~2025): claimed .named("Glass Drum.caf") worked — not verified on 26.5 today On iOS 26.5: Bundle.main finds our file with AND without ".caf" in the lookup string; playback still system default OUR WIRING (please confirm correct or tell us what to change) We schedule fixed-time dose alarms with: AlarmManager.AlarmConfiguration.alarm( schedule: .fixed(fireDate), attributes: attributes, stopIntent: stopIntent, secondaryIntent: snoozeIntent, sound: alertSound) .default on this parameter works. .named("lifeloop_alarm_silent") does not — despite Bundle.main finding the file (proof below). Question: Is sound: on .alarm(...) correct for custom AlertSound.named, or must sound go in nested AlertConfiguration(title:body:sound:) as in the overview example? WHAT WE HEAR ON DEVICE sound = .default → loud system alarm (works) sound = .named("lifeloop_alarm_silent") → same loud system/default every time No API error — silent fallback to default PROOF 1 — FILE IN SHIPPED IPA $ find Payload -name "lifeloop_alarm_*.caf" Payload/LifeLoop.app/lifeloop_alarm_silent.caf PROOF 2 — RUNTIME ON DEVICE (2026-06-30, iOS 26.5) Bundle.main.url(forResource:withExtension:): FOUND Bundle.main.url(full filename in name): FOUND Path: .../LifeLoop.app/lifeloop_alarm_silent.caf All .caf in main bundle: lifeloop_alarm_silent.caf Library/Sounds: (none) Widget .appex .caf: (none) Native log at schedule: [alarmkit] build_config · soundModeRaw=silent alertSound=named:lifeloop_alarm_silent silentCafInBundle=true [alarmkit] schedule_ok · alarmID=FBDF2082-61F1-4C93-8B20-094A60EA8626 CAF: lifeloop_alarm_silent.caf — mono, 44100 Hz, 16-bit PCM, ~0.5 s QUESTIONS FOR DTS Main app bundle only, or must CAF also be in widget .appex? Must named(_:) include ".caf" or base name only? Bundle.main FOUND at schedule time but playback is default — known bug on 26.5? Please answer: (A) wrong wiring + correct recipe, (B) known bug + fix version, or (C) not supported — please document. Thank you. Same problem as tonny00 — custom CAF never plays, always the loud system/default alarm. App: LifeLoop (com.stujacks.lifeloop) on iOS 26.5 (physical device). https://developer.apple.com/forums/thread/814732
Replies
1
Boosts
0
Views
61
Activity
3d
APNs push never delivered — even a direct FCM test message fails
Push notifications are never delivered to my iOS app and I've exhausted the usual checks. Bundle: com.thomasmanfredini.lafrench, Team 7TWV3839F3, TestFlight (production). Architecture: Firebase Cloud Messaging (the app registers an FCM token; Firebase relays to APNs). Key symptom: a direct FCM test message (Firebase Console → "Send test message") to a valid registration token delivers nothing (app killed, lock screen). Already verified (all correct): notification permission ON (incl. Lock Screen); TestFlight build with aps-environment=production; GoogleService-Info.plist matches the Firebase iOS app (project la-french, sender 649219813428); FCM token generated and valid; APNs Auth Key in Firebase with correct Key ID + Team ID — tried two keys (one Production, one Sandbox & Production), neither delivers; bundle ID matches the APNs topic. Since even a direct FCM test fails with a valid token and correct config, what could prevent APNs from delivering to this app? What should I check next (account-level APNs, Push Notifications capability on the App ID, etc.)? Thanks!
Replies
4
Boosts
0
Views
94
Activity
3d
StoreKit 2: New offer code NOT in Transaction.currentEntitlements()
This is production issue with a user completely stuck: User entered an offer code for 1 year free --> validated under iOS > Apple Account > subscriptions: it correctly shows a 1 year free trial But the transaction is not listed in his Transaction.currentEntitlements() Even after a restore (App.sync()) The Apple Account has always been the same (no mix) This is with the new offer codes introduced in 2026 Also, user wanted to pay the subscription himself in order to unlock the situation, he cannot because of the 'active' offer. Tried giving him another code, but it's refused by the system because there is only one active. Urgent help would be greatly appreciated. func readEntitlements(fromDeferredTransaction: Bool = false) async { var purchasedNonConsumables: [Product] = [] var purchasedSubscriptions: [Product] = [] var purchasedNonRenewableSubscriptions: [Product] = [] var activeSubTransactions: [Transaction] = [] //Iterate through all of the user's purchased products. for await result in Transaction.currentEntitlements { // currentEntitlements is a StoreKit2 useful feature that only gives us the relevant transactions (not the old & expired ones) do { //Check whether the transaction is verified. If it isn’t, catch `failedVerification` error. let transaction = try checkVerified(result) //Check the `productType` of the transaction and get the corresponding product from the store. switch transaction.productType { case .nonConsumable: if let nc = availableNonConsumables.first(where: { $0.id == transaction.productID }) { purchasedNonConsumables.append(nc) } case .nonRenewable: if let nonRenewable = availableNonRenewableSubscriptions.first(where: { $0.id == transaction.productID }) { let currentDate = Date() let expirationDate = Calendar(identifier: .gregorian).date(byAdding: DateComponents(year: 1), to: transaction.purchaseDate)! if currentDate < expirationDate { purchasedNonRenewableSubscriptions.append(nonRenewable) } } case .autoRenewable: if transaction.revocationDate == nil { activeSubTransactions.append(transaction) } if let subscription = availableSubscriptions.first(where: { $0.id == transaction.productID }) { DLog("Found valid entitlement. Subscription with exp date = \(String(describing: transaction.expirationDate))") purchasedSubscriptions.append(subscription) } else { DLog("Entitled to \(transaction.productID) but its Product is not loaded (product fetch failed/incomplete). Access will be granted from the transaction.") } default: break } } catch { print() } } //Update the store information with the purchased products. self.purchasedNonConsumables = purchasedNonConsumables self.purchasedNonRenewableSubscriptions = purchasedNonRenewableSubscriptions //Update the store information with auto-renewable subscription products. self.purchasedSubscriptions = purchasedSubscriptions //Authoritative entitlement transactions (independent of the product fetch succeeding). self.activeSubTransactions = activeSubTransactions subscriptionGroupStatus = try? await availableSubscriptions.first?.subscription?.status.first?.state // Callback IAPManager.shared.updateProStatus(isSureThatUserIsOnline: fromDeferredTransaction) }
Replies
3
Boosts
0
Views
130
Activity
3d
Moving from Multipeer Connectivity to Network Framework
I see a lot of folks spend a lot of time trying to get Multipeer Connectivity to work for them. My experience is that the final result is often unsatisfactory. Instead, my medium-to-long term recommendation is to use Network framework instead. This post explains how you might move from Multipeer Connectivity to Network framework. If you have questions or comments, put them in a new thread. Place it in the App & System Services > Networking topic area and tag it with Multipeer Connectivity and Network framework. IMPORTANT Xcode 27 beta has formally deprecated Multipeer Connectivity. I plan to properly update this post soon. In the meantime, the existing text is still perfectly valid if your app needs to support older systems, where it can’t take advantage of the nice new Network framework API we added in iOS 26 and aligned releases. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Moving from Multipeer Connectivity to Network Framework Multipeer Connectivity has a number of drawbacks: It has an opinionated networking model, where every participant in a session is a symmetric peer. Many apps work better with the traditional client/server model. It offers good latency but poor throughput. It doesn’t support flow control, aka back pressure, which severely constrains its utility for general-purpose networking. It includes a number of UI components that are effectively obsolete. It hasn’t evolved in recent years. For example, it relies on NSStream, which has been scheduled for deprecation as far as networking is concerned. It always enables peer-to-peer Wi-Fi, something that’s not required for many apps and can impact the performance of the network (see Enable peer-to-peer Wi-Fi, below, for more about this). Its security model requires the use of PKI — public key infrastructure, that is, digital identities and certificates — which are tricky to deploy in a peer-to-peer environment. It has some gnarly bugs. IMPORTANT Many folks use Multipeer Connectivity because they think it’s the only way to use peer-to-peer Wi-Fi. That’s not the case. Network framework has opt-in peer-to-peer Wi-Fi support. See Enable peer-to-peer Wi-Fi, below. If Multipeer Connectivity is not working well for you, consider moving to Network framework. This post explains how to do that in 13 easy steps (-: Plan for security Select a network architecture Create a peer identifier Choose a protocol to match your send mode Discover peers Design for privacy Configure your connections Manage a listener Manage a connection Send and receive reliable messages Send and receive best effort messages Start a stream Send a resource Finally, at the end of the post you’ll find two appendices: Final notes contains some general hints and tips. Symbol cross reference maps symbols in the Multipeer Connectivity framework to sections of this post. Consult it if you’re not sure where to start with a specific Multipeer Connectivity construct. Plan for security The first thing you need to think about is security. Multipeer Connectivity offers three security models, expressed as choices in the MCEncryptionPreference enum: .none for no security .optional for optional security .required for required security For required security each peer must have a digital identity. Optional security is largely pointless. It’s more complex than no security but doesn’t yield any benefits. So, in this post we’ll focus on the no security and required security models. Your security choice affects the network protocols you can use: QUIC is always secure. WebSocket, TCP, and UDP can be used with and without TLS security. QUIC security only supports PKI. TLS security supports both TLS-PKI and pre-shared key (PSK). You might find that TLS-PSK is easier to deploy in a peer-to-peer environment. To configure the security of the QUIC protocol: func quicParameters() -> NWParameters { let quic = NWProtocolQUIC.Options(alpn: ["MyAPLN"]) let sec = quic.securityProtocolOptions … configure `sec` here … return NWParameters(quic: quic) } To enable TLS over TCP: func tlsOverTCPParameters() -> NWParameters { let tcp = NWProtocolTCP.Options() let tls = NWProtocolTLS.Options() let sec = tls.securityProtocolOptions … configure `sec` here … return NWParameters(tls: tls, tcp: tcp) } To enable TLS over UDP, also known as DTLS: func dtlsOverUDPParameters() -> NWParameters { let udp = NWProtocolUDP.Options() let dtls = NWProtocolTLS.Options() let sec = dtls.securityProtocolOptions … configure `sec` here … return NWParameters(dtls: dtls, udp: udp) } To configure TLS with a local digital identity and custom server trust evaluation: func configureTLSPKI(sec: sec_protocol_options_t, identity: SecIdentity) { let secIdentity = sec_identity_create(identity)! sec_protocol_options_set_local_identity(sec, secIdentity) if disableServerTrustEvaluation { sec_protocol_options_set_verify_block(sec, { metadata, secTrust, completionHandler in let trust = sec_trust_copy_ref(secTrust).takeRetainedValue() … evaluate `trust` here … completionHandler(true) }, .main) } } To configure TLS with a pre-shared key: func configureTLSPSK(sec: sec_protocol_options_t, identity: Data, key: Data) { let identityDD = identity.withUnsafeBytes { DispatchData(bytes: $0) } let keyDD = identity.withUnsafeBytes { DispatchData(bytes: $0) } sec_protocol_options_add_pre_shared_key( sec, keyDD as dispatch_data_t, identityDD as dispatch_data_t ) sec_protocol_options_append_tls_ciphersuite( sec, tls_ciphersuite_t(rawValue: TLS_PSK_WITH_AES_128_GCM_SHA256)! ) } Select a network architecture Multipeer Connectivity uses a fully connected network architecture. All peers are equal, and every peer is effectively connected to every peer. Many apps work better with the client/server model, where one peer acts on the server and all the others are clients. Network framework supports both models. To implement a client/server network architecture with Network framework: Designate one peer as the server and all the others as clients. On the server, use NWListener to listen for incoming connections. On each client, use NWConnection to made an outgoing connection to the server. To implement a fully connected network architecture with Network framework: On each peer, start a listener. And also start a connection to each of the other peers. This is likely to generate a lot of redundant connections, as peer A connects to peer B and vice versa. You’ll need to a way to deduplicate those connections, which is the subject of the next section. IMPORTANT While the fully connected network architecture is more likely to create redundant connections, the client/server network architecture can generate redundant connections as well. The advice in the next section applies to both architectures. Create a peer identifier Multipeer Connectivity uses MCPeerID to uniquely identify each peer. There’s nothing particularly magic about MCPeerID; it’s effectively a wrapper around a large random number. To identify each peer in Network framework, generate your own large random number. One good choice for a peer identifier is a locally generated UUID, created using the system UUID type. Some Multipeer Connectivity apps persist their local MCPeerID value, taking advantage of its NSSecureCoding support. You can do the same with a UUID, using either its string representation or its Codable support. IMPORTANT Before you decide to persist a peer identifier, think about the privacy implications. See Design for privacy below. Avoid having multiple connections between peers; that’s both wasteful and potentially confusing. Use your peer identifier to deduplicate connections. Deduplicating connections in a client/server network architecture is easy. Have each client check in with the server with its peer identifier. If the server already has a connection for that identifier, it can either close the old connection and keep the new connection, or vice versa. Deduplicating connections in a fully connected network architecture is a bit trickier. One option is to have each peer send its peer identifier to the other peer and then the peer with the ‘best’ identifier wins. For example, imagine that peer A makes an outgoing connection to peer B while peer B is simultaneously making an outgoing connection to peer A. When a peer receives a peer identifier from a connection, it checks for a duplicate. If it finds one, it compares the peer identifiers and then chooses a connection to drop based on that comparison: if local peer identifier > remote peer identifier then drop outgoing connection else drop incoming connection end if So, peer A drops its incoming connection and peer B drops its outgoing connection. Et voilà! Choose a protocol to match your send mode Multipeer Connectivity offers two send modes, expressed as choices in the MCSessionSendDataMode enum: .reliable for reliable messages .unreliable for best effort messages Best effort is useful when sending latency-sensitive data, that is, data where retransmission is pointless because, by the retransmission arrives, the data will no longer be relevant. This is common in audio and video applications. In Network framework, the send mode is set by the connection’s protocol: A specific QUIC connection is either reliable or best effort. WebSocket and TCP are reliable. UDP is best effort. Start with a reliable connection. In many cases you can stop there, because you never need a best effort connection. If you’re not sure which reliable protocol to use, choose WebSocket. It has key advantages over other protocols: It supports both security models: none and required. Moreover, its required security model supports both TLS-PKI and TLS PSK. In contrast, QUIC only supports the required security model, and within that model it only supports TLS-PKI. It allows you to send messages over the connection. In contrast, TCP works in terms of bytes, meaning that you have to add your own framing. If you need a best effort connection, get started with a reliable connection and use that connection to set up a parallel best effort connection. For example, you might have an exchange like this: Peer A uses its reliable WebSocket connection to peer B to send a request for a parallel best effort UDP connection. Peer B receives that, opens a UDP listener, and sends the UDP listener’s port number back to peer A. Peer A opens its parallel UDP connection to that port on peer B. Note For step 3, get peer B’s IP address from the currentPath property of the reliable WebSocket connection. If you’re not sure which best effort protocol to use, use UDP. While it is possible to use QUIC in datagram mode, it has the same security complexities as QUIC in reliable mode. Discover peers Multipeer Connectivity has a types for advertising a peer’s session (MCAdvertiserAssistant) and a type for browsering for peer (MCNearbyServiceBrowser). In Network framework, configure the listener to advertise its service by setting the service property of NWListener: let listener: NWListener = … listener.service = .init(type: "_example._tcp") listener.serviceRegistrationUpdateHandler = { change in switch change { case .add(let endpoint): … update UI for the added listener endpoint … break case .remove(let endpoint): … update UI for the removed listener endpoint … break @unknown default: break } } listener.stateUpdateHandler = … handle state changes … listener.newConnectionHandler = … handle the new connection … listener.start(queue: .main) This example also shows how to use the serviceRegistrationUpdateHandler to update your UI to reflect changes in the listener. Note This example uses a service type of _example._tcp. See About service types, below, for more details on that. To browse for services, use NWBrowser: let browser = NWBrowser(for: .bonjour(type: "_example._tcp", domain: nil), using: .tcp) browser.browseResultsChangedHandler = { latestResults, _ in … update UI to show the latest results … } browser.stateUpdateHandler = … handle state changes … browser.start(queue: .main) This yields NWEndpoint values for each peer that it discovers. To connect to a given peer, create an NWConnection with that endpoint. About service types The examples in this post use _example._tcp for the service type. The first part, _example, is directly analogous to the serviceType value you supply when creating MCAdvertiserAssistant and MCNearbyServiceBrowser objects. The second part is either _tcp or _udp depending on the underlying transport protocol. For TCP and WebSocket, use _tcp. For UDP and QUIC, use _udp. Service types are described in RFC 6335. If you deploy an app that uses a new service type, register that service type with IANA. Discovery UI Multipeer Connectivity also has UI components for advertising (MCNearbyServiceAdvertiser) and browsing (MCBrowserViewController). There’s no direct equivalent to this in Network framework. Instead, use your preferred UI framework to create a UI that best suits your requirements. Note If you’re targeting Apple TV, check out the DeviceDiscoveryUI framework. Discovery TXT records The Bonjour service discovery protocol used by Network framework supports TXT records. Using these, a listener can associate metadata with its service and a browser can get that metadata for each discovered service. To advertise a TXT record with your listener, include it it the service property value: let listener: NWListener = … let peerID: UUID = … var txtRecord = NWTXTRecord() txtRecord["peerID"] = peerID.uuidString listener.service = .init(type: "_example._tcp", txtRecord: txtRecord.data) To browse for services and their associated TXT records, use the .bonjourWithTXTRecord(…) descriptor: let browser = NWBrowser(for: .bonjourWithTXTRecord(type: "_example._tcp", domain: nil), using: .tcp) browser.browseResultsChangedHandler = { latestResults, _ in for result in latestResults { guard case .bonjour(let txtRecord) = result.metadata, let peerID = txtRecord["peerID"] else { continue } // … examine `result` and `peerID` … _ = peerID } } This example includes the peer identifier in the TXT record with the goal of reducing the number of duplicate connections, but that’s just one potential use for TXT records. Design for privacy This section lists some privacy topics to consider as you implement your app. Obviously this isn’t an exhaustive list. For general advice on this topic, see Protecting the User’s Privacy. There can be no privacy without security. If you didn’t opt in to security with Multipeer Connectivity because you didn’t want to deal with PKI, consider the TLS-PSK options offered by Network framework. For more on this topic, see Plan for security. When you advertise a service, the default behaviour is to use the user-assigned device name as the service name. To override that, create a service with a custom name: let listener: NWListener = … let name: String = … listener.service = .init(name: name, type: "_example._tcp") It’s not uncommon for folks to use the peer identifier as the service name. Whether that’s a good option depends on the user experience of your product: Some products present a list of remote peers and have the user choose from that list. In that case it’s best to stick with the user-assigned device name, because that’s what the user will recognise. Some products automatically connect to services as they discover them. In that case it’s fine to use the peer identifier as the service name, because the user won’t see it anyway. If you stick with the user-assigned device name, consider advertising the peer identifier in your TXT record. See Discovery TXT records. IMPORTANT Using a peer identifier in your service name or TXT record is a heuristic to reduce the number of duplicate connections. Don’t rely on it for correctness. Rather, deduplicate connections using the process described in Create a peer identifier. There are good reasons to persist your peer identifier, but doing so isn’t great for privacy. Persisting the identifier allows for tracking of your service over time and between networks. Consider whether you need a persistent peer identifier at all. If you do, consider whether it makes sense to rotate it over time. A persistent peer identifier is especially worrying if you use it as your service name or put it in your TXT record. Configure your connections Multipeer Connectivity’s symmetric architecture means that it uses a single type, MCSession, to manage the connections to all peers. In Network framework, that role is fulfilled by two types: NWListener to listen for incoming connections. NWConnection to make outgoing connections. Both types require you to supply an NWParameters value that specifies the network protocol and options to use. In addition, when creating an NWConnection you pass in an NWEndpoint to tell it the service to connect to. For example, here’s how to configure a very simple listener for TCP: let parameters = NWParameters.tcp let listener = try NWListener(using: parameters) … continue setting up the listener … And here’s how you might configure an outgoing TCP connection: let parameters = NWParameters.tcp let endpoint = NWEndpoint.hostPort(host: "example.com", port: 80) let connection = NWConnection.init(to: endpoint, using: parameters) … continue setting up the connection … NWParameters has properties to control exactly what protocol to use and what options to use with those protocols. To work with QUIC connections, use code like that shown in the quicParameters() example from the Security section earlier in this post. To work with TCP connections, use the NWParameters.tcp property as shown above. To enable TLS on your TCP connections, use code like that shown in the tlsOverTCPParameters() example from the Security section earlier in this post. To work with WebSocket connections, insert it into the application protocols array: let parameters = NWParameters.tcp let ws = NWProtocolWebSocket.Options(.version13) parameters.defaultProtocolStack.applicationProtocols.insert(ws, at: 0) To enable TLS on your WebSocket connections, use code like that shown in the tlsOverTCPParameters() example to create your base parameters and then add the WebSocket application protocol to that. To work with UDP connections, use the NWParameters.udp property: let parameters = NWParameters.udp To enable TLS on your UDP connections, use code like that shown in the dtlsOverUDPParameters() example from the Security section earlier in this post. Enable peer-to-peer Wi-Fi By default, Network framework doesn’t use peer-to-peer Wi-Fi. To enable that, set the includePeerToPeer property on the parameters used to create your listener and connection objects. parameters.includePeerToPeer = true IMPORTANT Enabling peer-to-peer Wi-Fi can impact the performance of the network. Only opt into it if it’s a significant benefit to your app. If you enable peer-to-peer Wi-Fi, it’s critical to stop network operations as soon as you’re done with them. For example, if you’re browsing for services with peer-to-peer Wi-Fi enabled and the user picks a service, stop the browse operation immediately. Otherwise, the ongoing browse operation might affect the performance of your connection. Manage a listener In Network framework, use NWListener to listen for incoming connections: let parameters: NWParameters = .tcp … configure parameters … let listener = try NWListener(using: parameters) listener.service = … service details … listener.serviceRegistrationUpdateHandler = … handle service registration changes … listener.stateUpdateHandler = { newState in … handle state changes … } listener.newConnectionHandler = { newConnection in … handle the new connection … } listener.start(queue: .main) For details on how to set up parameters, see Configure your connections. For details on how to set up up service and serviceRegistrationUpdateHandler, see Discover peers. Network framework calls your state update handler when the listener changes state: let listener: NWListener = … listener.stateUpdateHandler = { newState in switch newState { case .setup: // The listener has not yet started. … case .waiting(let error): // The listener tried to start and failed. It might recover in the // future. … case .ready: // The listener is running. … case .failed(let error): // The listener tried to start and failed irrecoverably. … case .cancelled: // The listener was cancelled by you. … @unknown default: break } } Network framework calls your new connection handler when a client connects to it: var connections: [NWConnection] = [] let listener: NWListener = listener listener.newConnectionHandler = { newConnection in … configure the new connection … newConnection.start(queue: .main) connections.append(newConnection) } IMPORTANT Don’t forget to call start(queue:) on your connections. In Multipeer Connectivity, the session (MCSession) keeps track of all the peers you’re communicating with. With Network framework, that responsibility falls on you. This example uses a simple connections array for that purpose. In your app you may or may not need a more complex data structure. For example: In the client/server network architecture, the client only needs to manage the connections to a single peer, the server. On the other hand, the server must managed the connections to all client peers. In the fully connected network architecture, every peer must maintain a listener and connections to each of the other peers. Understand UDP flows Network framework handles UDP using the same NWListener and NWConnection types as it uses for TCP. However, the underlying UDP protocol is not implemented in terms of listeners and connections. To resolve this, Network framework works in terms of UDP flows. A UDP flow is defined as a bidirectional sequence of UDP datagrams with the same 4 tuple (local IP address, local port, remote IP address, and remote port). In Network framework: Each NWConnection object manages a single UDP flow. If an NWListener receives a UDP datagram whose 4 tuple doesn’t match any known NWConnection, it creates a new NWConnection. Manage a connection In Network framework, use NWConnection to start an outgoing connection: var connections: [NWConnection] = [] let parameters: NWParameters = … let endpoint: NWEndpoint = … let connection = NWConnection(to: endpoint, using: parameters) connection.stateUpdateHandler = … handle state changes … connection.viabilityUpdateHandler = … handle viability changes … connection.pathUpdateHandler = … handle path changes … connection.betterPathUpdateHandler = … handle better path notifications … connection.start(queue: .main) connections.append(connection) As in the listener case, you’re responsible for keeping track of this connection. Each connection supports four different handlers. Of these, the state and viability update handlers are the most important. For information about the path update and better path handlers, see the NWConnection documentation. Network framework calls your state update handler when the connection changes state: let connection: NWConnection = … connection.stateUpdateHandler = { newState in switch newState { case .setup: // The connection has not yet started. … case .preparing: // The connection is starting. … case .waiting(let error): // The connection tried to start and failed. It might recover in the // future. … case .ready: // The connection is running. … case .failed(let error): // The connection tried to start and failed irrecoverably. … case .cancelled: // The connection was cancelled by you. … @unknown default: break } } If you a connection is in the .waiting(_:) state and you want to force an immediate retry, call the restart() method. Network framework calls your viability update handler when its viability changes: let connection: NWConnection = … connection.viabilityUpdateHandler = { isViable in … react to viability changes … } A connection becomes inviable when a network resource that it depends on is unavailable. A good example of this is the network interface that the connection is running over. If you have a connection running over Wi-Fi, and the user turns off Wi-Fi or moves out of range of their Wi-Fi network, any connection running over Wi-Fi becomes inviable. The inviable state is not necessarily permanent. To continue the above example, the user might re-enable Wi-Fi or move back into range of their Wi-Fi network. If the connection becomes viable again, Network framework calls your viability update handler with a true value. It’s a good idea to debounce the viability handler. If the connection becomes inviable, don’t close it down immediately. Rather, wait for a short while to see if it becomes viable again. If a connection has been inviable for a while, you get to choose as to how to respond. For example, you might close the connection down or inform the user. To close a connection, call the cancel() method. This gracefully disconnects the underlying network connection. To close a connection immediately, call the forceCancel() method. This is not something you should do as a matter of course, but it does make sense in exceptional circumstances. For example, if you’ve determined that the remote peer has gone deaf, it makes sense to cancel it in this way. Send and receive reliable messages In Multipeer Connectivity, a single session supports both reliable and best effort send modes. In Network framework, a connection is either reliable or best effort, depending on the underlying network protocol. The exact mechanism for sending a message depends on the underlying network protocol. A good protocol for reliable messages is WebSocket. To send a message on a WebSocket connection: let connection: NWConnection = … let message: Data = … let metadata = NWProtocolWebSocket.Metadata(opcode: .binary) let context = NWConnection.ContentContext(identifier: "send", metadata: [metadata]) connection.send(content: message, contentContext: context, completion: .contentProcessed({ error in // … check `error` … _ = error })) In WebSocket, the content identifier is ignored. Using an arbitrary fixed value, like the send in this example, is just fine. Multipeer Connectivity allows you to send a message to multiple peers in a single send call. In Network framework each send call targets a specific connection. To send a message to multiple peers, make a send call on the connection associated with each peer. If your app needs to transfer arbitrary amounts of data on a connection, it must implement flow control. See Start a stream, below. To receive messages on a WebSocket connection: func startWebSocketReceive(on connection: NWConnection) { connection.receiveMessage { message, _, _, error in if let error { … handle the error … return } if let message { … handle the incoming message … } startWebSocketReceive(on: connection) } } IMPORTANT WebSocket preserves message boundaries, which is one of the reasons why it’s ideal for your reliable messaging connections. If you use a streaming protocol, like TCP or QUIC streams, you must do your own framing. A good way to do that is with NWProtocolFramer. If you need the metadata associated with the message, get it from the context parameter: connection.receiveMessage { message, context, _, error in … if let message, let metadata = context?.protocolMetadata(definition: NWProtocolWebSocket.definition) as? NWProtocolWebSocket.Metadata { … handle the incoming message and its metadata … } … } Send and receive best effort messages In Multipeer Connectivity, a single session supports both reliable and best effort send modes. In Network framework, a connection is either reliable or best effort, depending on the underlying network protocol. The exact mechanism for sending a message depends on the underlying network protocol. A good protocol for best effort messages is UDP. To send a message on a UDP connection: let connection: NWConnection = … let message: Data = … connection.send(content: message, completion: .idempotent) IMPORTANT UDP datagrams have a theoretical maximum size of just under 64 KiB. However, sending a large datagram results in IP fragmentation, which is very inefficient. For this reason, Network framework prevents you from sending UDP datagrams that will be fragmented. To find the maximum supported datagram size for a connection, gets its maximumDatagramSize property. To receive messages on a UDP connection: func startUDPReceive(on connection: NWConnection) { connection.receiveMessage { message, _, _, error in if let error { … handle the error … return } if let message { … handle the incoming message … } startUDPReceive(on: connection) } } This is exactly the same code as you’d use for WebSocket. Start a stream In Multipeer Connectivity, you can ask the session to start a stream to a specific peer. There are two ways to achieve this in Network framework: If you’re using QUIC for your reliable connection, start a new QUIC stream over that connection. This is one place that QUIC shines. You can run an arbitrary number of QUIC connections over a single QUIC connection group, and QUIC manages flow control (see below) for each connection and for the group as a whole. If you’re using some other protocol for your reliable connection, like WebSocket, you must start a new connection. You might use TCP for this new connection, but it’s not unreasonable to use WebSocket or QUIC. If you need to open a new connection for your stream, you can manage that process over your reliable connection. Choose a protocol to match your send mode explains the general approach for this, although in that case it’s opening a parallel best effort UDP connection rather than a parallel stream connection. The main reason to start a new stream is that you want to send a lot of data to the remote peer. In that case you need to worry about flow control. Flow control applies to both the send and receive side. IMPORTANT Failing to implement flow control can result in unbounded memory growth in your app. This is particularly bad on iOS, where jetsam will terminate your app if it uses too much memory. On the send side, implement flow control by waiting for the connection to call your completion handler before generating and sending more data. For example, on a TCP connection or QUIC stream you might have code like this: func sendNextChunk(on connection: NWConnection) { let chunk: Data = … read next chunk from disk … connection.send(content: chunk, completion: .contentProcessed({ error in if let error { … handle error … return } sendNextChunk(on: connection) })) } This acts like an asynchronous loop. The first send call completes immediately because the connection just copies the data to its send buffer. In response, your app generates more data. This continues until the connection’s send buffer fills up, at which point it defers calling your completion handler. Eventually, the connection moves enough data across the network to free up space in its send buffer, and calls your completion handler. Your app generates another chunk of data For best performance, use a chunk size of at least 64 KiB. If you’re expecting to run on a fast device with a fast network, a chunk size of 1 MiB is reasonable. Receive-side flow control is a natural extension of the standard receive pattern. For example, on a TCP connection or QUIC stream you might have code like this: func receiveNextChunk(on connection: NWConnection) { let chunkSize = 64 * 1024 connection.receive(minimumIncompleteLength: chunkSize, maximumLength: chunkSize) { chunk, _, isComplete, error in if let chunk { … write chunk to disk … } if isComplete { … close the file … return } if let error { … handle the error … return } receiveNextChunk(on: connection) } } IMPORTANT The above is cast in terms of writing the chunk to disk. That’s important, because it prevents unbounded memory growth. If, for example, you accumulated the chunks into an in-memory buffer, that buffer could grow without bound, which risks jetsam terminating your app. The above assumes that you can read and write chunks of data synchronously and promptly, for example, reading and writing a file on a local disk. That’s not always the case. For example, you might be writing data to an accessory over a slow interface, like Bluetooth LE. In such cases you need to read and write each chunk asynchronously. This results in a structure where you read from an asynchronous input and write to an asynchronous output. For an example of how you might approach this, albeit in a very different context, see Handling Flow Copying. Send a resource In Multipeer Connectivity, you can ask the session to send a complete resource, identified by either a file or HTTP URL, to a specific peer. Network framework has no equivalent support for this, but you can implement it on top of a stream: To send, open a stream and then read chunks of data using URLSession and send them over that stream. To receive, open a stream and then receive chunks of data from that stream and write those chunks to disk. In this situation it’s critical to implement flow control, as described in the previous section. Final notes This section collects together some general hints and tips. Concurrency In Multipeer Connectivity, each MCSession has its own internal queue and calls delegate callbacks on that queue. In Network framework, you get to control the queue used by each object for its callbacks. A good pattern is to have a single serial queue for all networking, including your listener and all connections. In a simple app it’s reasonable to use the main queue for networking. If you do this, be careful not to do CPU intensive work in your networking callbacks. For example, if you receive a message that holds JPEG data, don’t decode that data on the main queue. Overriding protocol defaults Many network protocols, most notably TCP and QUIC, are intended to be deployed at vast scale across the wider Internet. For that reason they use default options that aren’t optimised for local networking. Consider changing these defaults in your app. TCP has the concept of a send timeout. If you send data on a TCP connection and TCP is unable to successfully transfer it to the remote peer within the send timeout, TCP will fail the connection. The default send timeout is infinite. TCP just keeps trying. To change this, set the connectionDropTime property. TCP has the concept of keepalives. If a connection is idle, TCP will send traffic on the connection for two reasons: If the connection is running through a NAT, the keepalives prevent the NAT mapping from timing out. If the remote peer is inaccessible, the keepalives fail, which in turn causes the connection to fail. This prevents idle but dead connections from lingering indefinitely. TCP keepalives default to disabled. To enable and configure them, set the enableKeepalive property. To configure their behaviour, set the keepaliveIdle, keepaliveCount, and keepaliveInterval properties. Symbol cross reference If you’re not sure where to start with a specific Multipeer Connectivity construct, find it in the tables below and follow the link to the relevant section. [Sorry for the poor formatting here. DevForums doesn’t support tables properly, so I’ve included the tables as preformatted text.] | For symbol | See | | ----------------------------------- | --------------------------- | | `MCAdvertiserAssistant` | *Discover peers* | | `MCAdvertiserAssistantDelegate` | *Discover peers* | | `MCBrowserViewController` | *Discover peers* | | `MCBrowserViewControllerDelegate` | *Discover peers* | | `MCNearbyServiceAdvertiser` | *Discover peers* | | `MCNearbyServiceAdvertiserDelegate` | *Discover peers* | | `MCNearbyServiceBrowser` | *Discover peers* | | `MCNearbyServiceBrowserDelegate` | *Discover peers* | | `MCPeerID` | *Create a peer identifier* | | `MCSession` | See below. | | `MCSessionDelegate` | See below. | Within MCSession: | For symbol | See | | --------------------------------------------------------- | ------------------------------------ | | `cancelConnectPeer(_:)` | *Manage a connection* | | `connectedPeers` | *Manage a listener* | | `connectPeer(_:withNearbyConnectionData:)` | *Manage a connection* | | `disconnect()` | *Manage a connection* | | `encryptionPreference` | *Plan for security* | | `myPeerID` | *Create a peer identifier* | | `nearbyConnectionData(forPeer:withCompletionHandler:)` | *Discover peers* | | `securityIdentity` | *Plan for security* | | `send(_:toPeers:with:)` | *Send and receive reliable messages* | | `sendResource(at:withName:toPeer:withCompletionHandler:)` | *Send a resource* | | `startStream(withName:toPeer:)` | *Start a stream* | Within MCSessionDelegate: | For symbol | See | | ---------------------------------------------------------------------- | ------------------------------------ | | `session(_:didFinishReceivingResourceWithName:fromPeer:at:withError:)` | *Send a resource* | | `session(_:didReceive:fromPeer:)` | *Send and receive reliable messages* | | `session(_:didReceive:withName:fromPeer:)` | *Start a stream* | | `session(_:didReceiveCertificate:fromPeer:certificateHandler:)` | *Plan for security* | | `session(_:didStartReceivingResourceWithName:fromPeer:with:)` | *Send a resource* | | `session(_:peer:didChange:)` | *Manage a connection* | Revision History 2026-06-30 Replaced star network architecture with fully connected network architecture. 2026-06-14 Updated to account for changes in Xcode 27 beta. 2025-04-11 Added some advice as to whether to use the peer identifier in your service name. Expanded the discussion of how to deduplicate connections in a fully connected network architecture. 2025-03-20 Added a link to the DeviceDiscoveryUI framework to the Discovery UI section. Made other minor editorial changes. 2025-03-11 Expanded the Enable peer-to-peer Wi-Fi section to stress the importance of stopping network operations once you’re done with them. Added a link to that section from the list of Multipeer Connectivity drawbacks. 2025-03-07 First posted.
Replies
0
Boosts
0
Views
2.3k
Activity
3d
LiveActivity push-to-start flow inconsistency
Problem: unable to retrieve push-to-start token upon app start Environment: simulator, iPhone 15 Pro, iOS 18.1 (also reproduced on physical devices) First image is a start from XCode with "Run" option (Cmd+R) where debugger is linked Second image is a "cold" start - starting app by tapping the icon Difference is: Start observing (line 2), push token changed (last line). Everything is okay Push token changed (7 line), start observing (9 line). liveactivitysd generates token before we reach pushToStartTokenUpdates loop - we losing token The question is: how is this possible (case 2)?
Replies
0
Boosts
0
Views
72
Activity
3d
iOS 26.5 breaks AppIntents with AppEnums defined in Swift Package
For some reason since iOS 26.5, a ControlWidgetButton or a Button in a widget configured with an AppIntent conforming to OpenIntent fails to receive its AppEnum parameter correctly if the intent and enum are defined inside a shared Swift Package. Tapping the control widget button opens the main application, but it doesn't pass the AppEnum parameter value in, leaving the app on its default screen instead of navigating to the intended view. This was working perfectly in iOS 26.4, where the app would launch and receive the correct enum case from the intent. No code, configuration, or scheme changes were made between the two runs, only the simulator/OS version differs. Has anyone else run into what seems to be a regression on iOS 26.5? I filed feedback with a sample project: FB22848510
Replies
1
Boosts
2
Views
447
Activity
3d
Latency critical DMA read via PCIe
Dear All, I am currently developing a high throughput audio system which operates via PCIe tunneled into a USB4 interface. This include a custom FPGA based hardware and custom Audio DriverKit driver. While performing read operation via the hw DMA (that is a Host to Device transfer), I am noticing sparse latency spikes into the read transfers. Specifically, 4KB operations (which I assume including MRd + CpID) take normally from 5us to 40us to be completed, perfectly fine for my case. However, in some rare occasions, they can end up to 400us, which causes me overruns. The measurements have been carried out from the FPGA and they include the overall request and transfer time. While trying to tackle the problem, I'm investigating the possible power saving options and performance constraint methods at my disposal. I currently use these methods to mitigate the problem. ChangePowerState(kIOServicePowerCapabilityOn); SetPowerOverride(true); RequireMaxBusStall(kIOMaxBusStall25usec); CreatePMAssertion(kIOServicePMAssertionCPUBit | kIOServicePMAssertionForceFullWakeupBit, &ivars->PMAssertionID, false); The buffers are currently about 16MB, single segment, 16KB aligned and, of course, "prepared" for DMA. The system run for 3 hours without any overrun, but I'm not still fully convinced about its reliability. May someone provide me some comments on this? Are there profiling tools that I can use? Feel free to request me any required detail. The testing system is a MacBook Pro M2 Pro. Many Thanks and Best Regards Francesco
Replies
9
Boosts
0
Views
468
Activity
4d
StoreKit 2 returns no products for auto-renewable subscriptions in TestFlight
Hi everyone, I’m having an issue where my TestFlight build cannot load auto-renewable subscription products from App Store Connect. The app is built with Flutter and uses StoreKit 2 through an in-app purchase implementation. The same subscription system works correctly on Google Play Billing and returns product details/prices there, but on iOS StoreKit returns no subscription products. I tested this on a real iPhone installed from TestFlight, not from Xcode debug and not using a local StoreKit configuration. The app requests four auto-renewable subscription product IDs. The IDs in code exactly match the product IDs in App Store Connect. The app diagnostics show: Platform: iOS StoreKit mode: StoreKit 2 Store available: true Requested IDs: 4 subscription product IDs Returned IDs: none Missing IDs: all requested product IDs Error source: app_store Error code: storekit_no_response Error message: StoreKit: Failed to get response from platform. Store note: The store accepted the query, but did not return details for these product IDs. Things I have already checked: Product IDs match exactly between Flutter code and App Store Connect Bundle ID matches the App Store Connect app record In-App Purchase capability is enabled Subscriptions are in one subscription group Subscription availability is set for all countries/regions The TestFlight build is installed on a real device Local StoreKit configuration was removed StoreKit reports store availability as true App Store Connect / In-App Purchase keys are configured for backend verification The same product-loading flow works on Google Play The current App Store Connect status for the subscriptions is: Subscriptions: Ready to Submit / Ready for Review Localizations: Prepare for Submission Also, the bank information is not submitted yet. My question is: can StoreKit in TestFlight return subscription product details while the first auto-renewable subscriptions are still only “Ready to Submit” and have not yet been submitted with an app version for review? Or will StoreKit return no products until: The first subscriptions are attached to an app version and submitted for review Subscription localizations are fully prepared/submitted Paid agreements, tax, and banking are fully completed Since StoreKit is available and accepts the product query, but returns zero products, I’m trying to understand whether this is expected App Store Connect behavior or if there is another configuration issue I should check. Thanks.
Replies
1
Boosts
0
Views
150
Activity
4d
Inquiry about Bluetooth State Restoration behavior under iOS 26
Our vehicle digital key app uses CBCentralManagerOptionRestoreIdentifierKey and bluetooth-central background mode to maintain BLE connections with cars. We want to confirm: if the app is actively connected to a BLE vehicle peripheral and then terminated by system memory reclamation (not user force-quit), will iOS automatically relaunch the app via Bluetooth State Restoration when the peripheral sends notifications or re-advertises? Also please list all mandatory conditions to trigger such Bluetooth-based background relaunch after system termination, and clarify whether this pure-BLE relaunch channel works independently apart from iBeacon location-triggered cold launch for our terminated app on iOS 26 without AccessorySetupKit. Best regards
Replies
1
Boosts
0
Views
136
Activity
4d
Non-Consumable IAP stuck in ‘Developer Action Required’ – cannot link to version page
My Non-Consumable IAP was rejected with Guideline 3.1.1 and now has status ‘Developer Action Required’. The problem: On the version page in App Store Connect, the ‘In-App Purchases and Subscriptions’ section does not appear at the bottom. I cannot link the IAP to a new version submission. Every time I submit a new binary, Apple approves the app version but rejects the IAP afterward — even though I have no way to attach it to the version in the first place. This seems to be a circular issue: the IAP doesn’t appear on the version page because of its current status, but the only way to fix the status is to submit it with a new version. Has anyone experienced this and found a solution? Any help appreciated!​​​​​​​​​​​​​​​​
Replies
3
Boosts
0
Views
279
Activity
4d