●GROWTH — Rork keeps growing with 743K monthly visits and an 85% growth rate●MAX — Rork Max generates native Swift apps for iPhone, iPad, Watch, TV, Vision Pro, and iMessage●MAX — It reaches AR/LiDAR scanning, Metal 3D games, Live Activities, HealthKit, and Core ML, beyond React Native's reach●STACK — Standard Rork builds iOS and Android together in React Native (Expo), so non-engineers can ship real apps●PRICE — Plans start free, paid tiers from $25/month, and Rork Max at $200/month●MARKET — Gartner projects 75% of new apps will be low-code or no-code by the end of 2026●GROWTH — Rork keeps growing with 743K monthly visits and an 85% growth rate●MAX — Rork Max generates native Swift apps for iPhone, iPad, Watch, TV, Vision Pro, and iMessage●MAX — It reaches AR/LiDAR scanning, Metal 3D games, Live Activities, HealthKit, and Core ML, beyond React Native's reach●STACK — Standard Rork builds iOS and Android together in React Native (Expo), so non-engineers can ship real apps●PRICE — Plans start free, paid tiers from $25/month, and Rork Max at $200/month●MARKET — Gartner projects 75% of new apps will be low-code or no-code by the end of 2026
Direct Device-to-Device Sharing in Rork Max Apps — The Local Network Permission Trap That Makes MultipeerConnectivity Fail Silently
How to add serverless, nearby device-to-device sharing to a native Swift app generated by Rork Max. It works in the simulator but no peers ever appear on real devices — and the culprit is almost always a silent Local Network permission failure.
I wanted to hand a piece of in-app data to someone standing right next to me — say, pushing a wallpaper preset straight to a friend's phone — without a server or an account in between. When I built this with Rork Max, the generated Swift ran perfectly in the simulator and the peer list filled with mocks. But on two real devices, one phone never appeared on the other. No error, no callback, just silence. Chasing it down, the problem was not my use of MCSession at all. It was the Local Network permission one layer earlier, rejecting me without a word.
That silent failure is the first wall you hit when building nearby communication with MultipeerConnectivity. As an indie developer who keeps shipping apps, I've learned that permission gaps are especially nasty because they only surface on hardware. This article walks through dropping a device-to-device sharing feature into a Rork Max native app with the smallest possible diff: how to isolate the permission trap, and a wrapper you can use as-is.
Why connect devices directly instead of going through a server
Routing nearby sharing through a backend means that even with the other person right in front of you, the data takes a detour: upload, round-trip through a server, download. It won't work offline, it costs you server bills, and it can force account linking just to move a piece of throwaway data.
MultipeerConnectivity is Apple's own framework that automatically bundles Wi-Fi and Bluetooth to discover nearby devices — whether on the same Wi-Fi or fully offline peer-to-peer — and stream data directly between them. For "good enough if you can just hand it over" data like wallpapers or presets, I think not owning a server is the more honest choice. On the small apps I run under Dolice Labs, deciding not to add a backend for a single feature pays off in operating cost too.
If, on the other hand, your goal is permanent cross-device sync, designing cross-device data sync with CloudKit is the better fit. The key with nearby sharing is to scope it to "here and now" use cases.
Separate MultipeerConnectivity into three roles
This framework gets far clearer once you split it into three actors.
Advertiser: announces "I'm here" to those around you. Uses MCNearbyServiceAdvertiser.
Browser: looks for nearby advertisers and, when it finds one, sends an invitation. Uses MCNearbyServiceBrowser.
Session: the pipe that actually moves data once an invitation is accepted. That's MCSession.
When the same app plays both advertiser and browser at once, either device can discover and invite the other. This "announce both ways, search both ways" setup is the easiest to work with for nearby sharing, in my experience.
serviceType is the shared keyword that ties these three together. It has strict naming rules described below, and if you break them, both advertiser and browser quietly stop working.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦If you were stuck on peers never showing up on real devices with no error at all, you can now isolate from logs whether the Local Network permission or the Bonjour service type is to blame
✦You get a thin, ready-to-drop-in wrapper that handles the full state flow: invitation, MCSession establishment, and recovery after disconnect
✦You'll know exactly where to fill in the permission, Info.plist, and background constraints by hand — the parts Rork Max's generated code cannot cover
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The first wall: Local Network permission fails silently
MultipeerConnectivity uses Bonjour (mDNS) internally to find peers. Since iOS 14, any Bonjour-based communication requires the Local Network permission, and this is the single biggest gotcha because it only kicks in on real devices.
The structure of the trap looks like this:
If you forget the usage string (NSLocalNetworkUsageDescription) in Info.plist, the permission dialog never appears at all. With no dialog, nothing is ever denied or granted, and the browser finds nothing.
If you forget to list the Bonjour service types in NSBonjourServices, you can advertise but the discovery side is filtered out — again, silently nothing.
Once the user picks "Don't Allow," your app can never re-prompt. From then on the browser returns empty forever.
None of these throw an exception or fire an error callback, so it looks like "the code is right but it won't run." First, confirm these two entries are in your Info.plist. The NSBonjourServices values register both _servicetype._tcp and _servicetype._udp, matching the serviceType you actually use.
<!-- Info.plist --><key>NSLocalNetworkUsageDescription</key><string>We look for devices on your local network to share presets directly with people nearby.</string><key>NSBonjourServices</key><array> <string>_dolicoshare._tcp</string> <string>_dolicoshare._udp</string></array>
The framework won't explicitly tell you whether permission was denied. In practice I treat it as a rule: "if nothing shows up after searching for a while, suspect the permission or Info.plist." To make isolation possible, log each of discovery start, invitation send, and state change to os.Logger so you can see at a glance what is not happening on device.
A working minimal implementation: a thin wrapper around MCSession
Because the responsibilities split three ways, the raw API tends to scatter callbacks. So we fold advertiser, browser, and session into one class and expose state to SwiftUI through @Published. Here is a minimal setup that works as-is.
import MultipeerConnectivityimport os@MainActorfinal class NearbyShareManager: NSObject, ObservableObject { // serviceType: 1-15 chars, lowercase letters/digits/hyphen only (see the traps below) private let serviceType = "dolicoshare" private let myPeerID = MCPeerID(displayName: UUID().uuidString.prefix(8).description) private lazy var session: MCSession = { let s = MCSession(peer: myPeerID, securityIdentity: nil, encryptionPreference: .required) s.delegate = self return s }() private lazy var advertiser = MCNearbyServiceAdvertiser( peer: myPeerID, discoveryInfo: nil, serviceType: serviceType) private lazy var browser = MCNearbyServiceBrowser( peer: myPeerID, serviceType: serviceType) private let log = Logger(subsystem: "net.rorklab.share", category: "multipeer") @Published private(set) var foundPeers: [MCPeerID] = [] @Published private(set) var connectedPeers: [MCPeerID] = [] func start() { advertiser.delegate = self browser.delegate = self advertiser.startAdvertisingPeer() browser.startBrowsingForPeers() log.info("start advertising and browsing") } func stop() { advertiser.stopAdvertisingPeer() browser.stopBrowsingForPeers() session.disconnect() foundPeers.removeAll() connectedPeers.removeAll() } func invite(_ peerID: MCPeerID) { // Invite the peer into our session. Too short a timeout drops connections. browser.invitePeer(peerID, to: session, withContext: nil, timeout: 20) } func send(_ data: Data) { guard !session.connectedPeers.isEmpty else { return } do { try session.send(data, toPeers: session.connectedPeers, with: .reliable) } catch { log.error("send failed: \(error.localizedDescription, privacy: .public)") } }}
Implement the delegates split across four protocols. By reflecting the state transitions into @Published, the SwiftUI side can list them plainly.
I set encryptionPreference to .required. Nearby or not, there's no reason to put plaintext on the air. Setting it to .none makes connections faster, but I recommend defaulting to .required.
Dropping it into Rork Max's generated code with a minimal diff
Because Rork Max generates native Swift, you can keep the UI and the broad data model as they are. What the generated code usually can't finish, though, are the "only bite you on device and in App Store review" areas: permissions, Info.plist, and background constraints. Splitting the integration into three steps keeps it from getting confusing.
Fill in Info.plist by hand: add NSLocalNetworkUsageDescription and NSBonjourServices. These never appear in generated code, so you always add them yourself.
Hold NearbyShareManager as a @StateObject on the screen: from the generated share button's action, just call start() and invite(), and push the networking logic into the wrapper.
Match the send/receive data format to your existing model: turn presets and images into Data via Codable and pass them to send(_:). On the receiving side, decode back to the original type in didReceive data:.
Traps that catch you in practice, and how to handle them
Here are the spots most likely to snag you on device, roughly in the order I hit them.
Break the serviceType naming rule and everything dies.serviceType must be 1-15 characters, lowercase letters, digits, and hyphens only, and cannot start or end with a hyphen. Include an uppercase letter or underscore like Dolico_Share and you get a silent no-op, not an exception. Keep it short with lowercase ASCII and hyphens only.
MCPeerID displayName is capped at 63 bytes. Pass an overly long name or a device name with emoji as-is and it gets rejected. I use a safe value like the first few characters of a UUID, and manage the display name separately inside the app.
Don't shrink the invitation timeout too far. Trim invitePeer's timeout down to a few seconds and it gives up before connecting whenever reception is slightly weak. Keeping it around 20 seconds visibly cut my dropped connections.
It isn't kept alive in the background. MultipeerConnectivity assumes the app is in the foreground. Close or lock the screen mid-share and the session drops. Even just adding a line to the UI — "keep this in front until the transfer finishes" — makes the experience steadier.
Don't trust the simulator's results. Nearby discovery can only be verified correctly on two real devices. The failure I opened with started right here. A list appearing in the simulator does not guarantee real-device Bonjour behavior. Always test on hardware.
What to check before production
Nearby sharing, when it works, has a genuine satisfaction to it: no server, and you just hand the data to the person in front of you. At the same time, most of the failures hide in the dull places — permissions and Info.plist — and they happen silently, which is what makes them tricky.
As your next step, put a build with NSLocalNetworkUsageDescription and NSBonjourServices on two real devices, and just check whether the Local Network permission dialog actually appears on first launch. Once that passes, the wrapper in this article should run nearly as-is. I hope it helps anyone stuck at the same spot narrow it down.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.