tl;dr - Connect the devices and use the Network framework as usual.
So I was figuring out another problem when I stumbled upon this thread: https://forums.developer.apple.com/forums/thread/658393
In it, Quinn demonstrates how to use the Network framework over pure USB (no ethernet dongle) to connect to a Mac. It turns out this also applies to two iOS devices (and maybe beyond). So far I've only tested it with two devices with Lightning ports connected by a Lightning-to-USB adaptor (no power attached) plus a USB-to-Lightning cable.
Code to reproduce:
class NetworkManager: ObservableObject {
static var shared = NetworkManager()
@Published private(set) var text = ""
@Published private(set) var listenerQ: NWListener?
@Published private(set) var browserQ: NWBrowser?
var isListenerStarted: Bool { listenerQ != nil }
var isBrowserStarted: Bool { browserQ != nil }
func log(_ string: String) {
print(string)
text.append(string + "\n")
}
func startBrowser() -> NWBrowser {
let browser = NWBrowser(for: .bonjour(type: "_customService._tcp", domain: nil), using: .tcp)
browser.stateUpdateHandler = { self.log("Browser did change state, new: \($0)") }
browser.browseResultsChangedHandler = { self.log("Browser did change results, new: \($0) \($1)") }
browser.start(queue: .main)
return browser
}
func stopBrowser(_ browser: NWBrowser) {
log("Browser will stop")
browser.stateUpdateHandler = nil
browser.cancel()
}
func startStopBrowser() {
if let browser = browserQ {
browserQ = nil
stopBrowser(browser)
} else {
browserQ = startBrowser()
}
}
func startListener() -> NWListener {
print("Listener will start")
let listener = try! NWListener(using: .tcp)
listener.service = .init(type: "_customService._tcp")
listener.stateUpdateHandler = { print("Listener did change state, new: \(($0))") }
listener.newConnectionHandler = { connection in
let remotePeer = connection.currentPath?.remoteEndpoint
self.log("Listener did receive connection, from: \(remotePeer)")
connection.cancel()
}
listener.start(queue: .main)
return listener
}
func stopListener(_ listener: NWListener) {
log("Listener will stop")
listener.stateUpdateHandler = nil
listener.cancel()
}
func startStopListener() {
if let listener = self.listenerQ {
self.listenerQ = nil
self.stopListener(listener)
} else {
self.listenerQ = self.startListener()
}
}
}
struct ContentView: View {
@ObservedObject var netMan = networkManager
var body: some View {
VStack {
HStack {
Button("\(netMan.isListenerStarted ? "Stop" : "Start") listener") {
netMan.startStopListener()
}
Button("\(netMan.isBrowserStarted ? "Stop" : "Start") browser") {
netMan.startStopBrowser()
}
}
.padding()
Text("Log")
.font(.title.bold())
ScrollView {
Text(netMan.text)
}
}
.padding()
}
}
Post
Replies
Boosts
Views
Activity
Thanks; I would've gone on a (really long) wild goose chase without that
Reposting as reply since Quinn said that comments are lower-visibility to him
Hi Quinn, I get what you're saying, but that would mean my users would have to get two USB dongles and an ethernet cable as opposed to just using the USB cable they already own. This is complicated by the fact that devices with a Lightning port need to attach the USB dongle to a Lightning-to-USB adaptor. At this point I'm not looking for ways to implement a USB-to-USB connection using just a single USB cable--I just want to know if the system(s) involved permit this sort of direct connection
I was away but I finally filed a report: FB13589481
Since it seems the code that was touted as secure is questionable at best, are there any alternatives? To get around some issues I'm attaching passwords to the Bonjour context and using a single, shared PSK (not stored as a string) to secure connections, but this is just a stopgap measure.
Also, I reposted my comment to your reply to my other thread on wired data transfer between devices.. I'd be grateful if you would be kind enough to take a second look at my thread.
One solution is to stick everything in a TabView (with the .page style)
struct Playground: View {
@State private var detent = PresentationDetent.fraction(1/3)
@State private var isSheetPresented = true
var body: some View {
Rectangle()
.fill(Color(.systemGray5))
.sheet(isPresented: $isSheetPresented) {
VStack {
Text("ScrollView-in-Sheet Experiment")
.padding()
TabView {
ScrollView {
VStack(spacing: 0) {
ForEach(0...10, id: \.self) { e in
Rectangle()
.fill(.white)
.frame(height: 50)
.overlay { Text("\(e)") }
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.background { Color(.systemGray6) }
.presentationDetents([.large, .fraction(1/3)], selection: $detent)
}
}
}
One solution is to stick everything in a SwiftUI TabView (with the .page style). You should be able to use either MSStickerBrowserView or MSStickerViews wrapped in a UIViewRepresentable
struct Playground: View {
@State private var isSheetPresented = true
@State private var detent = PresentationDetent.fraction(1/3)
var body: some View {
Rectangle()
.fill(Color(.systemGray5))
.sheet(isPresented: $isSheetPresented) {
VStack {
Text("ScrollView-in-Sheet Experiment")
.padding()
TabView {
ScrollView {
VStack(spacing: 0) {
ForEach(0...100, id: \.self) { e in
Rectangle()
.fill(.white)
.border(.gray, width: 0.3)
.frame(width: 50)
.overlay { Text(e.description) }
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.background { Color(.systemGray6) }
.presentationDetents([.large, .fraction(1/3)], selection: $detent)
}
}
}
I'm glad I added the SwiftUI tag, because I replicated the exact same issue using pure SwiftUI
Unfortunately there doesn't seem to be anything published on how to fix the issue, even in pure SwiftUI--the ScrollView behaves as though the sheet is constantly expanded and transfers the drag gesture to the sheet when scrolled to the top (i.e. when first displayed), causing the user to move the sheet when intending to scroll in any direction.
I can slap a band-aid on this by having ScrollViewReader scroll down a tiny bit upon appearing, but this is far from ideal since the issue re-emerges after the user has scrolled to the top. This is complicated by the fact that you can't disable detents in your Messages App.
A workaround would be to abandon ScrollView (and MSStickerBrowserView) in favor of TabView with pages.
struct Playground: View {
@State private var detent = PresentationDetent.fraction(1/3)
@State private var isSheetPresented = true
var body: some View {
Rectangle()
.fill(Color(.systemGray5))
.sheet(isPresented: $isSheetPresented) {
VStack {
Text("ScrollView-in-Sheet Experiment")
.padding()
ScrollView {
ScrollViewReader { scrollProxy in
VStack(spacing: 0) {
ForEach(0...10, id: \.self) { i in
Rectangle()
.fill(.white)
.frame(height: 50)
.id(i)
.overlay { Text(i.description) }
}
}
}
}
.frame(height: 200)
.padding()
}
.background { Color(.systemGray6) }
.presentationDetents([.large, .fraction(1/3)], selection: $detent)
}
}
}
I was hoping to find a Swift-based solution but there seems to be none. For whatever reason, both the AFathi's GIF generator and my APNG adaptation of it simply doesn't work. I suspect it has something to do with the UTType mess after kUTType was deprecated in iOS 15.
I went ahead and adapted the Obj-C solution by Radif Sharafullin at https://github.com/radif/MSSticker-Images. I've included the updated code that has been confirmed to be working with iOS 17.2 below.
The code is, as far as I can tell, basically the same as the Swift code, so I'm baffled as to why the Swift version doesn't work. If anyone figures out why, please let us know here! I won't mark this thread as having an answer since my stop-gap solution isn't Swift-based:
#import "mcbAnimatedImagePersister.h"
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
@implementation mcbAnimatedImagePersister
+(instancetype)shared{
static mcbAnimatedImagePersister * instance=nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance=mcbAnimatedImagePersister.new;
});
return instance;
}
-(void)persistAnimatedImageSequenceGIF:(NSArray<UIImage *> *)images frameDelay:(CGFloat)frameDelay numberOfLoops:(NSInteger)numberOfLoops toURL:(NSURL *)toURL{
NSDictionary *fileProperties = @{
(__bridge id)kCGImagePropertyGIFDictionary: @{
(__bridge id) kCGImagePropertyGIFLoopCount: @(numberOfLoops),
}
};
NSDictionary *frameProperties = @{
(__bridge id)kCGImagePropertyGIFDictionary: @{
(__bridge id)kCGImagePropertyGIFDelayTime: @(frameDelay),
}
};
CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)toURL, (__bridge CFStringRef)UTTypeGIF.identifier, images.count, NULL);
CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)fileProperties);
for (UIImage * image in images) {
CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef)frameProperties);
}
if (!CGImageDestinationFinalize(destination)) {
NSLog(@"failed to finalize image destination");
}
CFRelease(destination);
}
-(void)persistAnimatedImageSequenceAPNG:(NSArray<UIImage *> *)images frameDelay:(CGFloat)frameDelay numberOfLoops:(NSInteger)numberOfLoops toURL:(NSURL *)toURL{
NSDictionary *fileProperties = @{
(__bridge id)kCGImagePropertyPNGDictionary: @{
(__bridge id)kCGImagePropertyAPNGLoopCount: @(numberOfLoops),
}
};
NSDictionary *frameProperties = @{
(__bridge id)kCGImagePropertyPNGDictionary: @{
(__bridge id)kCGImagePropertyAPNGDelayTime: @(frameDelay),
}
};
CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)toURL, (__bridge CFStringRef)UTTypePNG.identifier, images.count, NULL);
CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)fileProperties);
for (UIImage * image in images) {
CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef)frameProperties);
}
if (!CGImageDestinationFinalize(destination)) {
NSLog(@"failed to finalize image destination");
}
CFRelease(destination);
}
@end
The following sample code shows how to implement SpriteKit within RealityKit:
Implementing Special Rendering Effects with RealityKit Postprocessing
RealityKit will still be running at 60 FPS, but the SpriteKit View's framerate should be adjustable.
A partial answer can be found in the following sample code:
Implementing Special Rendering Effects with RealityKit Postprocessing
This sort of solves my problem, but doesn't change the fact that I'm still depending on SpriteKit to render my 2D content.
Edit:
@Eskimo
Err come to think about it I should ask for one clarification regarding APs not forwarding STA-to-STA traffic before I burn myself:
In your previous response you were ambiguous about the device brand, probably due to all the Peer-to-Peer Wi-Fi specifications out there.
My use case is only between Apple products. Period.
Does the phenomenon of some APs not forwarding STA-to-STA traffic apply to iPhone Hotspots talking with iPhones or iPads or Macs? E.g. Can an iPhone hosting a Hotspot Connection (being both AP and STA) and the iPhone being hosted (the pure STA) communicate with each other (e.g. cross streaming videos)?
I did a double take and came back to ask because this is critical to me.