I've upgraded my Xamarin Forms app from the beta version to the latest release and everything is working well except for downloading portal items on iOS. The code below is what I am using to accomplish this download. It works like a charm on Android, but on iOS the GetDataAsync call returns an empty stream. The item I am trying to download is a zip file that we uploaded to the portal as a Code Sample if that matters.
Not sure if this is an iOS bug or if there is some obscure iOS setting I need to toggle, but any help here would be greatly appreciated. Thanks ...
var item = await PortalItem.CreateAsync(PortalUser.Portal, itemId);
var outFile = File.OpenWrite(fileName);
var stream = await item.GetDataAsync();
var reader = new BinaryReader(stream);
using (var writer = new BinaryWriter(outFile))
{
writer.Write(reader.ReadBytes((int)stream.Length));
writer.Flush();
reader.Close();
}
Hi Andy,
I wonder if this has to do with iOS's enforcement of App Transport Security (ATS). Beginning with iOS 9, apps have ATS enabled by default, which means that any non-SSL HTTP connections will fail. Practically speaking, this means that a developer either needs to replace any http://* endpoints with https://*, or declare exceptions in the app project's Info.plist file (see Apple's reference doc here). Beta users would not have encountered this limitation because the Xamarin iOS Runtime was using the Mono networking stack under the covers, which does not enforce this restriction. For final, the implementation has been modified to use iOS's native networking stack, which of course does enforce this. The easiest way to test whether this is an issue would be to either swap out http endpoints with https, or, if that's not easily done, add this to the Info.plist file:
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict>
That will essentially disable ATS in the app, allowing access to any http endpoint. While that can be useful during development, it's generally not something that ought to be done in a production app. Instead, exceptions for individual http endpoints should be declared where an https equivalent is not available.
Hope this helps.
-Rich
Rich,
It’s funny you should mention that. I ran into that ATS error when attempting to access Esri basemaps on AGOL, so I already implemented your suggestion for another reason. I originally thought this problem was related, but I got an error with the Esri basemaps and this does not throw any errors. It appears that the download works, but the output stream is empty.
I gave this a try and am able to repro what you're seeing. The PortalItem.GetDataAsync method appears to be returning a ProgressStreamContent.ProgressStream on iOS. That's an internal type, so I don't know precisely how it works, but it appears to download the file contents asynchronously after it's been initialized. So it appears that the method is returning at the time the download has begun, rather than when it's ended. I've found that one way around this is to copy the stream that's returned. So I have something like so:
DownloadStatus = "Downloading item data...";
// On iOS, we get a ProgressStream and on UWP, we get a ReadOnlyStream. Neither of
// these internal types initialize their Length properties.
var partialItemData = await portalItem.GetDataAsync();
var fullItemData = new MemoryStream();
partialItemData.CopyTo(fullItemData); // Copying causes the entire contents to be downloaded/read
DownloadStatus = $"Download complete. Data length: {fullItemData.Length}";
Here, the copy operation apparently waits for the download to complete. Note that the same approach can also be used on UWP, which likewise doesn't make its Length available on the stream that's initially returned.
Hope this helps.
-Rich
Hi Rich,
Thanks for the tip. The CopyTo was actually hanging my app, but I switched to CopyToAsync and it seems to be working like a charm. You got me out of a pickle there. That almost makes up for not having related tables in the API.
So will this be logged as an API bug and fixed for an upcoming release?
Andy
It will require a bit more looking into to confirm, but I'm inclined to think that this is not a bug. Rather, the stream returned is just what gets passed back on iOS by the underlying HTTP calls employed by the API. The fact that it changed from beta to final is likely because of the switch from the managed (Mono) to native (iOS) networking stack. Nonetheless, initial indications are that this is simply a by-product of how the platform works, much as length is likewise unavailable on the stream returned by the GetDataAsync call on UWP. The fact that length is not available is not a bug there, either, but rather platform-specific behavior.
-Rich
Ok, thanks again for your help …