Not able to load style in offline Mapbox Map - ios

I'm working on an iOS app that shows map in offline mode. I'm using Mapbox map v10.7.0 for the same. Style pack and tile region are downloading successfully.
When map loads street data like road name, street name, place name etc. not showing on map and then if i turn on mobile data, map shows perfectly.
I don't know what i did wrong.
Here is sample code i write for downloading tile region and style pack
let dispatchGroup = DispatchGroup()
// 1. Create style package with loadStylePack() call.
let stylePackLoadOptions = StylePackLoadOptions(glyphsRasterizationMode: .ideographsRasterizedLocally,
metadata: ["tag": "my-outdoors-style-pack"],
acceptExpired: true)!
let tileStore = TileStore.default
let accessToken = ResourceOptionsManager.default.resourceOptions.accessToken
tileStore.setOptionForKey(TileStoreOptions.mapboxAccessToken, value: accessToken)
self.tileStore = tileStore
let accessToken = ResourceOptionsManager.default.resourceOptions.accessToken
offlineManager = OfflineManager(resourceOptions: ResourceOptions(accessToken: accessToken, tileStore: tileStore))
dispatchGroup.enter()
_ = offlineManager?.loadStylePack(for: .outdoors, loadOptions: stylePackLoadOptions) { [weak self] progress in
// These closures do not get called from the main thread. In this case
DispatchQueue.main.async {
print(" progress completedResourceCount/ - requiredResourceCount \(progress.completedResourceCount) / \(progress.requiredResourceCount)")
print("StylePack = \(progress)")
}
} completion: { [weak self] result in
// getting result success
DispatchQueue.main.async {
defer {
dispatchGroup.leave()
}
switch result {
case let .success(stylePack):
print("StylePack = \(stylePack)")
case let .failure(error):
print("stylePack download Error = \(error)")
}
}
}
// 2. Create an offline region with tiles for the outdoors style
let outdoorsOptions = TilesetDescriptorOptions(styleURI: .outdoors, zoomRange: UInt8(minZoom)...UInt8(maxZoom))
guard let outdoorsDescriptor = offlineManager?.createTilesetDescriptor(for: outdoorsOptions) else {
print("outdoorsDescriptor -> missing")
return
}
// Load the tile region
let tileRegionLoadOptions = TileRegionLoadOptions(
geometry: .point(Point(coord)),
descriptors: [outdoorsDescriptor],
metadata: userInfo,
acceptExpired: true)!
// Use the the default TileStore to load this region. You can create
// custom TileStores are are unique for a particular file path, i.e.
// there is only ever one TileStore per unique path.
dispatchGroup.enter()
_ = tileStore.loadTileRegion(forId: tileRegionId, loadOptions: tileRegionLoadOptions) { progress in
// These closures do not get called from the main thread. In this case
// we're updating the UI, so it's important to dispatch to the main
// queue.
DispatchQueue.main.async {
// Update the progress bar
print("Download progress : ")
print(Float(progress.completedResourceCount) / Float(progress.requiredResourceCount))
}
} completion: { result in
//getting result success
DispatchQueue.main.async {
defer {
dispatchGroup.leave()
}
switch result {
case let .success(tileRegion):
print("tileRegion = \(tileRegion)")
print(" tileRegion progress completedResourceCount/ - requiredResourceCount \(tileRegion.completedResourceCount) / \(tileRegion.requiredResourceCount)")
case let .failure(error):
print("tileRegion download Error = \(error)")
}
}
}
// Wait for both downloads before moving to the next state
dispatchGroup.notify(queue: .main) {
print("notify download complete")
}
Code for showing mapview:
let mapView = MapView(frame: view.bounds)
mapView.mapboxMap.loadStyleURI(.outdoors)
self.view.addSubview(mapView)
result i'm getting before turning on mobile data:
after turning on mobile data

Related

Images not going straight into array

I have a small problem with some code here. I am trying to populate a collection view with Five Names, descriptions and Images.
I am able to successfully to download all of the above into their respected arrays.
The problem is that the first time I perform the segue the image array has zero values in it. Then I go back a page and re-enter the page to find that all of the arrays have been populated successfully....
This is really annoying. Here is my code:
//arrays of names, descriptions and images
var names:[String] = []
var descriptions: [String] = []
var imagesArray: [UIImage] = []
Heres where I get the images:
func downloadImages(){
for x in 1...5{
let url = URL(string: "https://www.imagesLocation.com/(x).png")
let task = URLSession.shared.dataTask(with: url!){(data, response, error) in
guard
let data = data,
let newImage = UIImage(data: data)
else{
print("Could not load image from URL: ",url!)
return
}
DispatchQueue.main.async {
self.imagesArray.append(newImage)
}
}
task.resume()
}
loadDataFromFirebase()
}
Heres where I download the Names and Descriptions from:
func loadDataFromFirebase() {
// Fetch and convert data
let db = Firestore.firestore()
db.collection(self.shopName).getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
return
} else {
for document in snapshot!.documents {
let name = document.get("Name") as! String
let description = document.get("Description") as! String
self.names.append(name)
self.descriptions.append(description)
}
self.setupImages() //safe to do this here as the firebase data is valid
}
}
}
Heres where I setup the collection view with the Names, Description and Images array contents:
func setupImages(){
do {
if imagesArray.count < 5 || names.count < 5 || descriptions.count < 5 {
throw MyError.FoundNil("Something hasnt loaded")
}
self.pages = [
Page(imageName: imagesArray[0], headerText: names[0], bodyText: descriptions[0]),
Page(imageName: imagesArray[1], headerText: names[1], bodyText: descriptions[1]),
Page(imageName: imagesArray[2], headerText: names[2], bodyText: descriptions[2]),
Page(imageName: imagesArray[3], headerText: names[3], bodyText: descriptions[3]),
Page(imageName: imagesArray[4], headerText: names[4], bodyText: descriptions[4]),
]
}
catch {
print("Unexpected error: \(error).")
}
}
As you can see from the image below, every array is populating successfully apart from the images array:
Here is the segue from the previous page's code:
DispatchQueue.main.async(){
self.performSegue(withIdentifier: "goToNext", sender: self)
}
Any help is welcome :)
Your question is just a variant of the classic, "Why is my asynchronous function returning empty data?" I've answered a couple of these questions, and I'll include an analogy that explains the issue. You might understand the issue already, but I'll include it anyway for future readers:
Your mom is cooking dinner and asks you to go buy a lemon.
She starts cooking, but she has no lemon!
Why? Because you haven't yet returned from the supermarket, and your
mom didn't wait.
Source
The main issue here is that you are calling loadDataFromFirebase way too early. You assume that it will execute only after your URL requests have completed, but that is not the case. Why? Because the URL requests are executed asynchronously. That is, they run on another thread instead of blocking the thread that calls dataTask.resume. This is why, as Shashank Mishra suggests, you should use a DispatchGroup. Additionally, there is no guarantee that your images will load in the order that you begin the data tasks. I have included a fix below.
Generally, I would recommend defining variables strictly in the scopes in which you need them. Keeping names, descriptions, and images at such a high scope makes it too easy to make mistakes. I suggest refactoring your functions and deleting those three class-level arrays. Instead:
func loadDataFromFirebase(images: [UIImage]) {
// same function as you posted, except make names and descriptions local variables and
// replace self.setupImages() with:
DispatchQueue.main.async {
self.setupImages(images: images, names: names, descriptions: descriptions)
}
}
func setupImages(images: [UIImage], names: [String], descriptions: [String]) {
guard images.count == 5, names.count == 5, descriptions.count == 5 else {
print("Missing data.")
return
}
self.pages = (0..<5).map({ Page(image: images[$0], header: names[$0], body: descriptions[$0]) })
// super important!!!
tableView.reloadData()
}
Finally, here is my suggestion for a thread-safe downloadImages function:
func downloadImages() {
var images = [UIImage?](repeating: nil, count: 5)
let dispatchGroup = DispatchGroup()
for i in 1...5 {
dispatchGroup.enter()
let url = URL(string: "https://www.imagesLocation.com/\(i).png")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data, let image = UIImage(data: data) else {
print("Could not load image from", url)
dispatchGroup.leave()
return
}
images[i] = image
dispatchGroup.leave()
}.resume()
}
dispatchGroup.notify(queue: .main) {
guard images.allSatisfy({$0 != nil}) else {
print("Failed to fetch all images.")
return
}
self.loadDataFromFirebase(images: images.compactMap({$0}))
}
}
As Fattie pointed out, you should use addSnapshotListener rather than getDocuments. Also, you should add the listener/get documents while downloading the images instead of after, which will be faster. However, I am not adding either to my answer because this is already quite long, and if you have trouble with it you can post another question.
You can use DispatchGroup to achieve asynchronous calls -
func downloadImages() {
let dispatchGroup = DispatchGroup()
for x in 1...5 {
dispatchGroup.enter()
let url = URL(string: "https://www.imagesLocation.com/(x).png")
let task = URLSession.shared.dataTask(with: url!){(data, response, error) in
guard
let data = data,
let newImage = UIImage(data: data)
else{
print("Could not load image from URL: ",url!)
dispatchGroup.leave()
return
}
self.imagesArray.append(newImage)
dispatchGroup.leave()
}
task.resume()
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.loadDataFromFirebase()
}
}
Call "loadDataFromFirebase()" method on getting all 5 responses as above. It will always have all images before loading it on view.
You're misunderstanding how Firebase works.
Essentially.
Don't use getDocuments. Use .addSnapshotListener
and
Basically each time the snapshot arrives, simply call .reloadData() on the table.
A full tutorial is beyond the scope of an answer here but there are many, many, tutorials around.
Just a typical fragment ...
let db = Firestore.firestore().db.collection("yourCollection")
.whereField("user", isEqualTo: uid)
.addSnapshotListener { [weak self] documentSnapshot, error in
guard let self = self else { return }
guard let ds = documentSnapshot else {
return print("error: \(error!)")
}
self.displayItems = .. that data
self.tableView.reloadData()
}
Note the .reloadData()
Also ..
It's true that you can store an image (binary data) right in Firestore.
But really never, ever, do that - it's completely useless.
Simply use the dead-easy Firebase/Storage system where you can host images for free. Then they have completely normal URLs and so on.
Full tutorial: https://stackoverflow.com/a/62626214/294884

How to download A LOT of files from S3 using the transfer utility?

I have several thousand images I want to download from a S3 bucket to an iOS App.
But I'm getting memory issues I'm unable to track down.
Here is my sketchy code:
let client = HttpClient<[SomeImage]>()
client.get(fromURL: URL(string: endpoint)!) {
(result, error) in
if let error = error {
self.log(message: "\(error)", level: .error)
return
}
if let result = result {
let downloadGroup = DispatchGroup()
var count = 0
// just assembling a list of s3 keys to download here...
for item in result {
for image in (item.images ?? []) {
let prefix = "\(image.key)/"
for key in ["\(globalGetThumbnailS3Key(byImageKey: image.key))",
"\(globalGetPreviewS3Key(byImageKey: image.key))"] {
count = count + 1
let completionHandler: AWSS3TransferUtilityDownloadCompletionHandlerBlock = {
(task, URL, data, error) in
if let error = error {
self.log(message: "\(error)", level: .error)
return
}
if let data = data, let localDir = FileManager.default.applicationSupportURL {
do {
let imageURL = localDir.appendingPathComponent(key)
FileManager.default.directoryExistsOrCreate(localDir.appendingPathComponent(prefix))
try data.write(to: imageURL)
self.log(message: "downloaded \(prefix)\(key) to \(imageURL.absoluteString)", level: .verbose)
} catch let error {
self.log(message: "\(error)", level: .error)
return
}
}
}
bgSyncQueue.async(group: downloadGroup) {
self.transferUtility.downloadData(fromBucket: "\(globalDerivedImagesBucket)", key: key,
expression: nil,
completionHandler: completionHandler).continueWith {
(task) in
if let error = task.error {
// iirc, this error is caused, if the task couldnt be created due to being offline
self.log(message: "\(error)", level: .error)
return nil
}
if let result = task.result {
// do something with the task?
return nil
}
return nil
}
}
}
}
}
self.log(message: "\(count) images to download...", level: .debug)
bgSyncQueue.activate()
downloadGroup.notify(queue: DispatchQueue.main) {
self.log(message: "All items downloaded?!")
}
}
}
}
So I put all calls to the transfer utility in a serial dispatch queue, which is initially inactive. Then I activate the queue and downloading starts just fine. But after a while the app crashes with "Message from debugger: Terminated due to memory issue."
The app is only consuming about 100M of memory though. What am I overlooking?
Rob's suggestion to use the "downloadToUrl" method was the way to go, without using GCD on my part. Thanks again, Rob!
The transferUtility seems to be a fine tool, though very badly documented.
Here is the simple code used to download about 20k of images:
for key in keys {
let imageURL = localDir.appendingPathComponent(key.1)
let completionHandler: AWSS3TransferUtilityDownloadCompletionHandlerBlock = {
(task, URL, data, error) in
if let error = error {
self.log(message: "failed downloading \(key.1): \(error)", level: .error)
DispatchQueue.main.async {
countingDown()
}
return
}
DispatchQueue.main.async {
countingDown()
if let onProgress = self.onProgress {
onProgress(100.0 - ((100.0 / Double(total)) * Double(count)))
}
}
//self.log(message: "downloaded \(key.1)")
}
transferUtility.download(to: imageURL, bucket: "\(globalDerivedImagesBucket)", key: key.1, expression: nil, completionHandler: completionHandler).continueWith {
(task) in
if let error = error {
self.log(message: "\(error)", level: .error)
DispatchQueue.main.async {
countingDown()
}
return nil
}
return nil
}
}
You may need to consider using an autoreleasepool to better manage the memory used by the bridged data types as detailed here
Exert from article (in case of link changes)
Consider the code:
func run() {
guard let file = Bundle.main.path(forResource: "bigImage", ofType: "png") else {
return
}
for i in 0..<1000000 {
let url = URL(fileURLWithPath: file)
let imageData = try! Data(contentsOf: url)
}
}
Even though we’re in Swift, this will result in the same absurd memory spike shown in the Obj-C example! This is because the Data init is a bridge to the original Obj-C [NSDatadataWithContentsOfURL] -- which unfortunately still calls autorelease somewhere inside of it. Just like in Obj-C, you can solve this with the Swift version of #autoreleasepool; autoreleasepool without the #:
autoreleasepool {
let url = URL(fileURLWithPath: file)
let imageData = try! Data(contentsOf: url)
}
Disclaimer: I am no expert in Swift or Objective-C advanced memory management but I have used this in a similar scenario with good results.

Swift iOS -DispatchGroup with URLSession is locking other parts of app that it is not located in

I have an array of up to 6 images. I use a loop to loop through all of the images, turn them into metadata, send the metadata to Storage and then when done I send the url strings to Firebase Database.
I'm using DispatchGroup to control the loop as the Url is changed to Data so I can send the data to Firebase Storage.
If this loop is happening in tabOne, if i go back and forth to tabTwo or tabThree, when the loop finishes and the alert appears, tabTwo is temporarily locked or tabThree gets temporarily locked for around 2-3 seconds. I cannot figure out where I'm going wrong?
I'm not sure if it makes a difference but I'm using a custom alert instead of the UIAlertController. It's just some UIViews and a button, it's nothing special so I didn't include the code.
var urls = [URL]()
picUUID = UUID().uuidString
dict = [String:Any]()
let myGroup = DispatchGroup()
var count = 0
for url in urls{
myGroup.enter() // enter group here
URLSession.shared.dataTask(with: url!, completionHandler: {
(data, response, error) in
guard let data = data, let _ = error else { return }
DispatchQueue.main.async{
self.sendDataToStorage("\(self.picUUID)_\(self.count).jpg", picData: data)
self.count += 1
}
}).resume()
// send dictionary data to firebase when loop is done
myGroup.notify(queue: .main) {
self.sendDataToFirebaseDatabase()
self.count = 0
}
}
func sendDataToStorage(_ picId: String, picData: Data?){
dict.updateValue(picId, forKey:"picId_\(count)")
let picRef = storageRoot.child("pics")
picRef.putData(picData!, metadata: nil, completion: { (metadata, error) in
if let picUrl = metadata?.downloadURL()?.absoluteString{
self.dict.updateValue(picUrl, forKey:"picUrl_\(count)")
self.myGroup.leave() // leave group here
}else{
self.myGroup.leave() // leave group if picUrl is nil
}
}
}
func sendDataToFirebaseDatabase(){
let ref = dbRoot.child("myRef")
ref.updateChildValues(dict, withCompletionBlock: { (error, ref) in
displaySuccessAlert()
}
}
I don't know much about Firebase, but you are dispatching your sendDataToFirebaseDatabase method to main queue which probably explains why your UI becomes unresponsive.
Dispatch sendDataToFirebaseDatabase to a background queue and only dispatch your displaySuccessAlert back to main queue.

Swift: downloading data from url causes semaphore_wait_trap freeze

In my application, button tapping downloads data from an Internet site. The site is a list of links containing binary data. Sometimes, the first link may not contain the proper data. In this case, the application takes the next link in the array and gets data from there. The links are correct.
The problem I have is that frequently (not always though) the application freezes for seconds when I tap on the button. After 5-30 seconds, it unfreezes and downloading implements normally. I understand, something is blocking the main thread. When stopping the process in xCode, I get this (semaphore_wait_trap noted):
This is how I do it:
// Button Action
#IBAction func downloadWindNoaa(_ sender: UIButton)
{
// Starts activity indicator
startActivityIndicator()
// Starts downloading and processing data
// Either use this
DispatchQueue.global(qos: .default).async
{
DispatchQueue.main.async
{
self.downloadWindsAloftData()
}
}
// Or this - no difference.
//downloadWindsAloftData()
}
}
func downloadWindsAloftData()
{
// Creates a list of website addresses to request data: CHECKED.
self.listOfLinks = makeGribWebAddress()
// Extract and save the data
saveGribFile()
}
// This downloads the data and saves it in a required format. I suspect, this is the culprit
func saveGribFile()
{
// Check if the links have been created
if (!self.listOfLinks.isEmpty)
{
/// Instance of OperationQueue
queue = OperationQueue()
// Convert array of Strings to array of URL links
let urls = self.listOfLinks.map { URL(string: $0)! }
guard self.urlIndex != urls.count else
{
NSLog("report failure")
return
}
// Current link
let url = urls[self.urlIndex]
// Increment the url index
self.urlIndex += 1
// Add operation to the queue
queue.addOperation { () -> Void in
// Variables for Request, Queue, and Error
let request = URLRequest(url: url)
let session = URLSession.shared
// Array of bytes that will hold the data
var dataReceived = [UInt8]()
// Read data
let task = session.dataTask(with: request) {(data, response, error) -> Void in
if error != nil
{
print("Request transport error")
}
else
{
let response = response as! HTTPURLResponse
let data = data!
if response.statusCode == 200
{
//Converting data to String
dataReceived = [UInt8](data)
}
else
{
print("Request server-side error")
}
}
// Main thread
OperationQueue.main.addOperation(
{
// If downloaded data is less than 2 KB in size, repeat the operation
if dataReceived.count <= 2000
{
self.saveGribFile()
}
else
{
self.setWindsAloftDataFromGrib(gribData: dataReceived)
// Reset the URL Index back to 0
self.urlIndex = 0
}
}
)
}
task.resume()
}
}
}
// Processing data further
func setWindsAloftDataFromGrib(gribData: [UInt8])
{
// Stops spinning activity indicator
stopActivityIndicator()
// Other code to process data...
}
// Makes Web Address
let GRIB_URL = "http://xxxxxxxxxx"
func makeGribWebAddress() -> [String]
{
var finalResult = [String]()
// Main address site
let address1 = "http://xxxxxxxx"
// Address part with type of data
let address2 = "file=gfs.t";
let address4 = "z.pgrb2.1p00.anl&lev_250_mb=on&lev_450_mb=on&lev_700_mb=on&var_TMP=on&var_UGRD=on&var_VGRD=on"
let leftlon = "0"
let rightlon = "359"
let toplat = "90"
let bottomlat = "-90"
// Address part with coordinates
let address5 = "&leftlon="+leftlon+"&rightlon="+rightlon+"&toplat="+toplat+"&bottomlat="+bottomlat
// Vector that includes all Grib files available for download
let listOfFiles = readWebToString()
if (!listOfFiles.isEmpty)
{
for i in 0..<listOfFiles.count
{
// Part of the link that includes the file
let address6 = "&dir=%2F"+listOfFiles[i]
// Extract time: last 2 characters
let address3 = listOfFiles[i].substring(from:listOfFiles[i].index(listOfFiles[i].endIndex, offsetBy: -2))
// Make the link
let addressFull = (address1 + address2 + address3 + address4 + address5 + address6).trimmingCharacters(in: .whitespacesAndNewlines)
finalResult.append(addressFull)
}
}
return finalResult;
}
func readWebToString() -> [String]
{
// Final array to return
var finalResult = [String]()
guard let dataURL = NSURL(string: self.GRIB_URL)
else
{
print("IGAGribReader error: No URL identified")
return []
}
do
{
// Get contents of the page
let contents = try String(contentsOf: dataURL as URL)
// Regular expression
let expression : String = ">gfs\\.\\d+<"
let range = NSRange(location: 0, length: contents.characters.count)
do
{
// Match the URL content with regex expression
let regex = try NSRegularExpression(pattern: expression, options: NSRegularExpression.Options.caseInsensitive)
let contentsNS = contents as NSString
let matches = regex.matches(in: contents, options: [], range: range)
for match in matches
{
for i in 0..<match.numberOfRanges
{
let resultingNS = contentsNS.substring(with: (match.rangeAt(i))) as String
finalResult.append(resultingNS)
}
}
// Remove "<" and ">" from the strings
if (!finalResult.isEmpty)
{
for i in 0..<finalResult.count
{
finalResult[i].remove(at: finalResult[i].startIndex)
finalResult[i].remove(at: finalResult[i].index(before: finalResult[i].endIndex))
}
}
}
catch
{
print("IGAGribReader error: No regex match")
}
}
catch
{
print("IGAGribReader error: URL content is not read")
}
return finalResult;
}
I have been trying to fix it for the past several weeks but in vain. Any help would be much appreciated!
let contents = try String(contentsOf: dataURL as URL)
You are calling String(contentsOf: url) on the main thread (main queue). This downloads the content of the URL into a string synchronously The main thread is used to drive the UI, running synchronous network code is going to freeze the UI. This is a big no-no.
You should never call readWebToString() in the main queue. Doing DispatchQueue.main.async { self.downloadWindsAloftData() } exactly put the block in the main queue which we should avoid. (async just means "execute this later", it is still executed on Dispatch.main.)
You should just run downloadWindsAloftData in the global queue instead of main queue
DispatchQueue.global(qos: .default).async {
self.downloadWindsAloftData()
}
Only run DispatchQueue.main.async when you want to update the UI.
Your stack trace is telling you that it's stopping at String(contentsOf:), called by readWebToString, called by makeGribWebAddress.
The problem is that String(contentsOf:) performs a synchronous network request. If that request takes any time, it will block that thread. And if you call this from the main thread, your app may freeze.
Theoretically, you could just dispatch that process to a background queue, but that merely hides the deeper problem, that you are doing a network request with an API that is synchronous, non-cancellable, and offers no meaningful error reporting.
You really should doing asynchronous requests with URLSession, like you have elsewhere. Avoid using String(contentsOf:) with remote URL.

Where to place a completionHandler when inside loops?

I'm using a completionHandler in this function, however it's nested within several for loops (below). The problem is the handler where it is now gets called every time the loop it's in runs, whereas I only want the handler to pass in the Set when the entire function has completed processing. If I place it outside of the loop, then it gets called too early and is empty. What should I do here?
Right now when I print to the console to test it prints:
Set item 1
Set item 1, 2
Set item 1, 2, 3 etc.
struct RekoRequest {
public func getRekos(rekoType: rekoCategory, handler: #escaping (Set<String>) -> Void) {
var urls = [NSURL]()
var IDs = Set<String>()
TwitterRequest().fetchTweets(searchType: "things") { result in
guard let tweets = result as? [TWTRTweet] else {print("Error in getRekos receiving tweet results from TwitterRequest.fetchTweets"); return}
for tweet in tweets {
let types: NSTextCheckingResult.CheckingType = .link
let detector = try? NSDataDetector(types: types.rawValue)
guard let detect = detector else { print("NSDataDetector error"); return }
let matches = detect.matches(in: text, options: .reportCompletion, range: NSMakeRange(0, (text.characters.count)))
for match in matches {
if let url = match.url {
guard let unwrappedNSURL = NSURL(string: url.absoluteString) else {print("error converting url to NSURL");return}
//Show the original URL
unwrappedNSURL.resolveWithCompletionHandler {
guard let expandedURL = URL(string: "\($0)") else {print("couldn't covert to expandedURL"); return}
guard let urlDomain = expandedURL.host else { print("no host on expandedURL"); return }
switch urlDomain {
case "www.somesite.com":
let components = expandedURL.pathComponents
for component in components {
if component == "dp" {
guard let componentIndex = components.index(of: component) else {print("component index error"); return}
let IDIndex = componentIndex + 1
let ID = components[IDIndex]
//Filter out Dups and add to Set
IDs.insert(ID)
handler(IDs)
print(ID) //this prints multiple sets of IDs, I only want one when the function is finished completely
}
}
break;
default:
break;
}
}
} else { print("error with match.url") }
} //for match in matches loop
} //for tweet in tweets loop
}
}
}
// Create an extension to NSURL that will resolve a shortened URL
extension NSURL
{
func resolveWithCompletionHandler(completion: #escaping (NSURL) -> Void)
{
let originalURL = self
let req = NSMutableURLRequest(url: originalURL as URL)
req.httpMethod = "HEAD"
URLSession.shared.dataTask(with: req as URLRequest)
{
body, response, error in completion(response?.url as NSURL? ?? originalURL)
}
.resume()
}
}
Call your completion handler after the for loop.
for component in components {
if component == "dp" {
...
}
}
handler(IDs)
Important: The handler should be called outside of the for loop, but within the TwitterRequest().fetchTweets() trailing closure.
Approaches to handling an empty set
Your IDs are being initialized to an empty set. Only after meeting certain conditions within your for loop are values being inserted into this set. If these conditions aren't met, then your IDs set will be empty.
If this is undesirable, then you will have to either make changes to your completion handler or alter your conditional logic so that you always get a non-empty set.
One approach might be to have optional set in your callback. Something like:
(Set<String>?) -> Void
If IDs are empty, then callback with a nil and have your calling code handle the possibility of a nil set.
Another approach might be to create an enum to encapsulate your result and use this in your callback. Something like:
Enum
enum Result {
case success(Set<String>)
case failure
}
Callback
handler: (Result) -> Void
Usage
handler(.success(IDs))
// or
handler(.failure)
Calling Code
getReckos(rekoType: .someType) { result in
switch result {
case .success(let IDs):
// Use IDs
case .failure:
// Handle no IDs
}
}

Resources