No idea what is happening here since I've followed other answers that use this including Swift: synchronously perform code in background; queue.sync does not work as I would expect
I have an XMLParser that I need to load 2 RSS feeds. To prevent reentrant parsing, I need to load these URLs one after the other is done. Problem is I need to do this in the background/async so it doesn't freeze up the UI.
I have this:
var myParser: XMLParser = XMLParser()
let parseQueue = DispatchQueue(label: "xmlQueue", attributes: [], target: nil)
var feeds = [RSS_FEED_URL_COT, RSS_FEED_URL] //this order
self.parseQueue.async {
for f in feeds
{
self.loadRSSData(rssFeed: f)
}
}
This does work meaning no reentrant error, but for a good 30 seconds whole UI is frozen up. What am I doing wrong here?
EDIT:
func loadRSSData(rssFeed: String){
if let rssURL = URL(string: rssFeed) {
print("LOADING THE URL: ", rssURL)
// fetch rss content from url
if let contents = XMLParser(contentsOf: rssURL)
{
self.myParser = contents
}
// set parser delegate
self.myParser.delegate = self
self.myParser.shouldResolveExternalEntities = false
// start parsing
self.myParser.parse()
}
}
#Sjyguy,
Kindly use the loadRSSData like below.
func loadRSSData(rssFeed: String){
if let rssURL = URL(string: rssFeed) {
print("LOADING THE URL: ", rssURL)
// fetch rss content from url
if let contents = XMLParser(contentsOf: rssURL)
{
self.myParser = contents
}
// set parser delegate
self.myParser.delegate = self
self.myParser.shouldResolveExternalEntities = false
**DispatchQueue.main.async{
// start parsing
self.myParser.parse()
}**
}
}
Hope it worked!
Related
Good afternoon,
I have been stuck on this problem for months. I am trying to use firebase storage to save image files that a user uploaded. The program should then be able to update the queue and show the image in a horizontal table view. Kinda like netflix where its titles of movies/shows but mine would just be pictures. After trying to figure this out, this is what I came up with. Here is to receive the images
class ImageRecieve : ObservableObject {
#Published var songImageArrayURL = [URL]()
#Published var data : Data?
#Published var songImage : NSImage?
#Published var AlbumCoverArray = [NSImage]()
func GetURLS(){
//we want to get the download urls
bfRef.listAll { (result, error) in
if let error = error{ //if theres an error, print it
print(error.localizedDescription)
}
let prefixes = result.prefixes
//loop to search each song prefix
for i in prefixes.indices{
//get the song of each prefix
prefixes[i].listAll { (result, error) in
if let error = error {
print(error.localizedDescription)
}
else {
let items = result.items
//if anything contains ".mp3" dont add it to array.
for j in items.indices{
if(!items[j].name.contains("mp3")){
SongImage.append(items[j])
self.download(SongImage: items[j])
}
}
}
}
}
}
}
func download(SongImage:StorageReference){
//get download url
DispatchQueue.main.async {
SongImage.downloadURL { (url, error) in
if let error = error { //if there is an error print it
print(error.localizedDescription)
}
else {
if(url != nil){
self.songImage = NSImage(byReferencing: url!)
self.AlbumCoverArray.append(self.songImage!)
}
}
}
}
}
func load(){
if(self.songImageArrayURL.isEmpty){
GetURLS()
}
print(self.songImageArrayURL)
for i in self.songImageArrayURL.indices{
print(self.songImageArrayURL[i])
DispatchQueue.global().async{
if let data = try? Data(contentsOf: self.songImageArrayURL[i]){
if let image = NSImage(data:data){
DispatchQueue.main.async {
self.songImage = image
}
}
}
}
}
}
func cancel(){
}
}
here is to load the images :
struct LoadImages<Placeholder: View>: View {
#ObservedObject var loader : ImageRecieve
private var placeholder : Placeholder?
init(placeholder: Placeholder? = nil) {
loader = ImageRecieve()
self.placeholder = placeholder
}
var body: some View {
image
.onAppear(perform: loader.GetURLS)
.onDisappear(perform: loader.cancel)
}
private var image: some View{
ForEach(loader.AlbumCoverArray.indices,id:\.self){
i in
Group{
if(self.loader.songImage != nil){
Image(nsImage:self.loader.AlbumCoverArray[i]).resizable().frame(width:50, height:50)
}
else{
self.placeholder
}
}
}
}
}
the problem I've been stuck on is that the photos are only downloading one at a time and not listing one by one. For example, they show one image and then switch to the next. I would like an array of images. So that the images get added to the list. I've tried using an image array but it doesnt work.
photos are only downloading one at a time and not listing one by one.
in all languages an array/list is processed sequentially, you might want to use multi-Threading for parallelism. use a queue and assign few threads which download image, after each download pop the element from queue.
all the child threads append/push the data to the main thread. in that manner you will be able to display images as they load.
PS:i am != swiftie but seeing your programming i sense turmoil. try improving your code grammar and avoid too many functions and spaces.
I'm trying to play a video with AVPlayer that was loaded inside AVAssetResourceLoaderDelegate but I always get a blank screen and the video never plays.
This is the code:
let asset = AVURLAsset(url: URL(string: "fakescheme://video.mp4")!)
asset.resourceLoader.setDelegate(ResourceLoaderDelegate(), queue: DispatchQueue.main)
let item = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: item)
let playerLayer = AVPlayerLayer(player: self.player)
playerLayer!.frame = self.view.bounds;
self.view.layer.addSublayer(self.playerLayer!)
player!.play()
...
class ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
public func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource resourceLoadingRequest: AVAssetResourceLoadingRequest) -> Bool {
var newRequest = URLRequest(url: URL(string: "https://example.com/video.mp4")!)
newRequest.allHTTPHeaderFields = resourceLoadingRequest.request.allHTTPHeaderFields
let sessionTask = URLSession.shared.dataTask(with: newRequest) { data, response, error in
resourceLoadingRequest.contentInformationRequest?.contentType = "video/mp4"
resourceLoadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
resourceLoadingRequest.contentInformationRequest?.contentLength = Int64(data.count)
resourceLoadingRequest.response = response
resourceLoadingRequest.dataRequest?.respond(with: data)
resourceLoadingRequest.finishLoading()
}
sessionTask.resume()
return true
}
}
This is the basic idea:
The fakescheme:// causes the resourceLoader() delegate to be called, inside it a new HTTP request is made to download the actual video.
And then it calls resourceLoadingRequest.dataRequest?.respond(with: data) that was supposed to cause AVPlayer to play the downloaded video.
The data variable is properly populated, but yet I always get a black screen.
The video file is also fine, it plays if I feed it directly into AVURLAsset().
I tried a million things and combinations, but can't make it play using the delegate. Any help will be greatly appreciated!
EDIT: I'll add some more info.
I tried doing this with an AES encrypted video where it uses 3 files - .m3u8, the encryption key and the .ts video.
I managed to make it download the .m3u8 and the key using the delegate, but again when I try to do it with the video file, I get black screen.
This leads me to thinking it may require the download to happen in portions, but I'm not sure how to properly do that.
I also can't find any documentation about it, such as - should you set the HTTP headers as if this is coming from a web server, should you set contentInformationRequest, etc. The delegate gets called 2 times:
// first time:
resourceLoadingRequest.dataRequest!.requestedLength == 2,
resourceLoadingRequest.dataRequest!.requestsAllDataToEndOfResource == false
// second time:
resourceLoadingRequest.dataRequest!.requestedLength == MAX_INT,
resourceLoadingRequest.dataRequest!.requestsAllDataToEndOfResource == true
I'm not sure what to make of that - I give it the entire video both times, but no success.
I finally got it to work. So there are 2 problems with the above code:
contentType should be AVFileType.mp4.rawValue, which is "public.mpeg-4". Passing "video/mp4" or another value breaks it
resourceLoadingRequest.dataRequest!.requestedLength Indeed needs to be respected, so the video file needs to be sent in chunks as requested.
This is the working delegate code:
// this IF is an ugly way to catch the first request to the delegate
// in this request you should populate the contentInformationRequest struct with the size of the video, etc
if (resourceLoadingRequest.dataRequest!.requestedLength == 2) {
let bytes : [UInt8] = [0x0, 0x0] // these are the first 2 bytes of the video, as requested
let data = Data(bytes: bytes, count: bytes.count)
resourceLoadingRequest.contentInformationRequest?.contentType = AVFileType.mp4.rawValue // this is public.mpeg-4, video/mp4 does not work
resourceLoadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
resourceLoadingRequest.contentInformationRequest?.contentLength = Int64(videoSize)
resourceLoadingRequest.dataRequest!.respond(with: data)
resourceLoadingRequest.finishLoading()
return true
}
// here we are at the second request. the OS may request the entire file, or a portion of it
// here we don't need to set any headers or contentInformationRequest, just reply with the requested data
// take a look at resourceLoadingRequest.dataRequest!.requestedLength, requestedOffset, currentOffset, requestsAllDataToEndOfResource
resourceLoadingRequest.dataRequest?.respond(with: data)
resourceLoadingRequest.finishLoading()
return true
This is the code of the whole file for reference:
import Foundation
import AVKit
class ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
public func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource resourceLoadingRequest: AVAssetResourceLoadingRequest) -> Bool {
if ((resourceLoadingRequest.request.url?.absoluteString.contains(".mp4"))!) {
// replace the fakeScheme and get the original video url
var originalVideoURLComps = URLComponents(url: resourceLoadingRequest.request.url!, resolvingAgainstBaseURL: false)!
originalVideoURLComps.scheme = "file"
let originalVideoURL = originalVideoURLComps.url
var videoSize = 0
do {
let value = try originalVideoURL!.resourceValues(forKeys: [.fileSizeKey])
videoSize = value.fileSize!
} catch {
print("error getting video size")
}
if (resourceLoadingRequest.contentInformationRequest != nil) {
// this is the first request where we should tell the OS what file is to be downloaded
let bytes : [UInt8] = [0x0, 0x0] // TODO: repeat .requestedLength times?
let data = Data(bytes: bytes, count: bytes.count)
resourceLoadingRequest.contentInformationRequest?.contentType = AVFileType.mp4.rawValue // this is public.mpeg-4, video/mp4 does not work
resourceLoadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
resourceLoadingRequest.contentInformationRequest?.contentLength = Int64(videoSize)
resourceLoadingRequest.dataRequest!.respond(with: data)
resourceLoadingRequest.finishLoading()
return true
}
// this is the second request where the actual file is to be downloaded
let requestedLength = resourceLoadingRequest.dataRequest!.requestedLength
let requestedOffset = resourceLoadingRequest.dataRequest!.requestedOffset
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: requestedLength)
let inputStream = InputStream(url: originalVideoURL!) // TODO: keep the stream open until a new file is requested?
inputStream!.open()
if (requestedOffset > 0) {
// move the stream pointer to the requested position
let buffer2 = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(requestedOffset))
inputStream!.read(buffer2, maxLength: Int(requestedOffset)) // TODO: the requestedOffset may be int64, but this gets truncated to int!
buffer2.deallocate()
}
inputStream!.read(buffer, maxLength: requestedLength)
// decrypt the video
if (requestedOffset == 0) { // TODO: this == 0 may not always work?
// if you use custom encryption, you can decrypt the video here, buffer[] holds the bytes
}
let data = Data(bytes: buffer, count: requestedLength)
resourceLoadingRequest.dataRequest?.respond(with: data)
resourceLoadingRequest.finishLoading()
buffer.deallocate()
inputStream!.close()
return true
}
return false
}
}
I'm getting info from an API using the following function where I pass in a string of a word. Sometimes the word doesn't available in the API if it doesn't available I generate a new word and try that one.
The problem is because this is an asynchronous function when I launch the page where the value from the API appears it is sometimes empty because the function is still running in the background trying to generate a word that exists in the API.
How can I make sure the page launches only when the data been received from the api ?
static func wordDefin (word : String, completion: #escaping (_ def: String )->(String)) {
let wordEncoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let uri = URL(string:"https://dictapi.lexicala.com/search?source=global&language=he&morph=false&text=" + wordEncoded! )
if let unwrappedURL = uri {
var request = URLRequest(url: unwrappedURL);request.addValue("Basic bmV0YXlhbWluOk5ldGF5YW1pbjg5Kg==", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let decoder = JSONDecoder()
let empty = try decoder.decode(Empty.self, from: data)
if (empty.results?.isEmpty)!{
print("oops looks like the word :" + word)
game.wordsList.removeAll(where: { ($0) == game.word })
game.floffWords.removeAll(where: { ($0) == game.word })
helper.newGame()
} else {
let definition = empty.results?[0].senses?[0].definition
_ = completion(definition ?? "test")
return
}
}
}
catch {
print("connection")
print(error)
}
}
dataTask.resume()
}
}
You can't stop a view controller from "launching" itself (except not to push/present/show it at all). Once you push/present/show it, its lifecycle cannot—and should not—be stopped. Therefore, it's your responsibility to load the appropriate UI for the "loading state", which may be a blank view controller with a loading spinner. You can do this however you want, including loading the full UI with .isHidden = true set for all view objects. The idea is to do as much pre-loading of the UI as possible while the database is working in the background so that when the data is ready, you can display the full UI with as little work as possible.
What I'd suggest is after you've loaded the UI in its "loading" configuration, download the data as the final step in your flow and use a completion handler to finish the task:
override func viewDidLoad() {
super.viewDidLoad()
loadData { (result) in
// load full UI
}
}
Your data method may look something like this:
private func loadData(completion: #escaping (_ result: Result) -> Void) {
...
}
EDIT
Consider creating a data manager that operates along the following lines. Because the data manager is a class (a reference type), when you pass it forward to other view controllers, they all point to the same instance of the manager. Therefore, changes that any of the view controllers make to it are seen by the other view controllers. That means when you push a new view controller and it's time to update a label, access it from the data property. And if it's not ready, wait for the data manager to notify the view controller when it is ready.
class GameDataManager {
// stores game properties
// updates game properties
// does all thing game data
var score = 0
var word: String?
}
class MainViewController: UIViewController {
let data = GameDataManager()
override func viewDidLoad() {
super.viewDidLoad()
// when you push to another view controller, point it to the data manager
let someVC = SomeOtherViewController()
someVC.data = data
}
}
class SomeOtherViewController: UIViewController {
var data: GameDataManager?
override func viewDidLoad() {
super.viewDidLoad()
if let word = data?.word {
print(word)
}
}
}
class AnyViewController: UIViewController {
var data: GameDataManager?
}
I'm trying to get an example project using CoreData and QueryGenerationTokens working. The essence of the project is to be committing changes to a background context on a timer (emulating changes coming down from a server) that shouldn't be displayed until an action is taken on the UI (say, a button press).
Currently, I have changes being saved on the background context (an entity is being added every 5s and saved) and they are automatically coming into the view context (as expected, .automaticallyMergesChangesFromParent is set to true). Where things go wrong, I am pinning the view context before any of these changes happen to the current query generation token. I would expect the view to not update with the background items being added, but it is updating with them. So it seems the query generation tokens are having no effect?
Some of the possible issues I've thought of:
the only example I've found from Apple doesn't show them using it with a fetched results controller (I'm using #FetchRequest in SwiftUI, which I'm almost entirely certain is essentially the same), so that may have an effect?
.automaticallyMergeChangesFromParent shouldn't be used and I should try a merge policy, but that doesn't seem to work either and conceptually, it seems the query generation tokens should work with this and pin to the generation no matter the merging.
Code for view - handles loading data from view context
// Environment object before fetch request necessary
// Passed in wherever main view is instantiated through .environment()
#Environment(\.managedObjectContext) var managedObjectContext
// Acts as fetched results controller, loading data automatically into items upon the managedObjectContext updating
// ExampleCoreDataEntity.retrieveItemsFetchRequest() is an extension method on the entity to easily get a fetch request for the type with sorting
#FetchRequest(fetchRequest: ExampleCoreDataEntity.retrieveItemsFetchRequest()) var items: FetchedResults<ExampleCoreDataEntity>
var body: some View {
NavigationView {
// Button to refresh and bring in changes
Button(
action: {
do {
try self.managedObjectContext.setQueryGenerationFrom(.current)
self.managedObjectContext.refreshAllObjects()
} catch {
print(error.localizedDescription)
}
},
label: { Image(systemName: "arrow.clockwise") }
)
// Creates a table of items sorted by the entity itself (entities conform to Hashable)
List(self.items, id: \.self) { item in
Text(item.name ?? "")
}
}
}
Code in SceneDelegate (where a SwiftUI application starts up) where I also initialize what is needed for CoreData:
// Setup and pass in environment of managed object context to main view
// via extension on persistent container that sets up CoreData stack
let managedObjectContext = NSPersistentContainer.shared.viewContext
do {
try managedObjectContext.setQueryGenerationFrom(.current)
} catch {
print(error.localizedDescription)
}
let view = MainView().environment(\.managedObjectContext, managedObjectContext)
// Setup background adding
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(backgroundCode), userInfo: nil, repeats: true)
// Setup window and pass in main view
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: view)
Function adding data in the background:
#objc func backgroundCode() {
ExampleCoreDataEntity.create(names: ["background object"], in: backgroundContext, shouldSave: true)
}
Setup of NSPersistentContainer:
extension NSPersistentContainer {
private struct SharedContainerStorage {
static let container: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Core_Data_Exploration")
container.loadPersistentStores { (description, error) in
guard error == nil else {
assertionFailure("CoreData: Unresolved error \(error!.localizedDescription)")
return
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
return container
}()
}
static var shared: NSPersistentContainer {
return SharedContainerStorage.container
}
}
Create/Read/Update/Delete functions on the entity:
extension ExampleCoreDataEntity {
static func retrieveItemsFetchRequest() -> NSFetchRequest<ExampleCoreDataEntity> {
let request: NSFetchRequest<ExampleCoreDataEntity> = ExampleCoreDataEntity.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \ExampleCoreDataEntity.creationDate, ascending: false)]
return request
}
static func create(names: [String], in context: NSManagedObjectContext, shouldSave save: Bool = false) {
context.perform {
names.forEach { name in
let item = ExampleCoreDataEntity(context: context)
item.name = name
item.creationDate = Date()
item.identifier = UUID()
}
do {
if save {
try context.save()
}
} catch {
// print error
}
}
}
func delete(in context: NSManagedObjectContext, shouldSave save: Bool = false) {
context.perform {
let name = self.name ?? "an item"
context.delete(context.object(with: self.objectID))
do {
if save {
try context.save()
}
} catch {
// print error
}
}
}
}
The issue was container.viewContext.automaticallyMergesChangesFromParent = true
That property cannot be set to true while working with query generation tokens. I came back to this issue and found this in the header of NSManagedObjectContext documented above automaticallyMergesChangesFromParent:
Setting this property to YES when the context is pinned to a non-current query generation is not supported.
The general flow of getting it to work is the following:
setting the query generation token to .current
calling .refreshAllObjects() on the view context
calling .performFetch() on the fetched results controller
This last part goes against the code I put in the original question which used #FetchRequest - currently, I can't figure out a way that doesn't seem extremely hacky to make it manually refetch. To get around this, I made an intermediate store class containing a FetchedResultsController that adopts its delegate protocol. That store also adopts ObservableObject which allows a SwiftUI view to listen to its changes when calling objectWillChange.send() within the ObservableObject adopting store.
In the documentation you linked to in the question you will see it says:
"Calling save(), reset(), mergeChangesFromContextDidSaveNotification:, or mergeChangesFromRemoteContextSave(:intoContexts:) on any pinned context will automatically advance it to the most recent version for the operation and then reset its query generation to currentQueryGenerationToken."
The reason you are seeing the changes from the background save is automaticallyMergesChangesFromParent is just convenience for mergeChangesFromContextDidSaveNotification so your generation is advancing.
FYI here is another sample project uses query generations - Synchronizing a Local Store to the Cloud
And here is the relevant code:
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
A class to set up the Core Data stack, observe Core Data notifications, process persistent history, and deduplicate tags.
*/
import Foundation
import CoreData
// MARK: - Core Data Stack
/**
Core Data stack setup including history processing.
*/
class CoreDataStack {
/**
A persistent container that can load cloud-backed and non-cloud stores.
*/
lazy var persistentContainer: NSPersistentContainer = {
// Create a container that can load CloudKit-backed stores
let container = NSPersistentCloudKitContainer(name: "CoreDataCloudKitDemo")
// Enable history tracking and remote notifications
guard let description = container.persistentStoreDescriptions.first else {
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores(completionHandler: { (_, error) in
guard let error = error as NSError? else { return }
fatalError("###\(#function): Failed to load persistent stores:\(error)")
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
// Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
// Observe Core Data remote change notifications.
NotificationCenter.default.addObserver(
self, selector: #selector(type(of: self).storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange, object: container)
return container
}()
/**
Track the last history token processed for a store, and write its value to file.
The historyQueue reads the token when executing operations, and updates it after processing is complete.
*/
private var lastHistoryToken: NSPersistentHistoryToken? = nil {
didSet {
guard let token = lastHistoryToken,
let data = try? NSKeyedArchiver.archivedData( withRootObject: token, requiringSecureCoding: true) else { return }
do {
try data.write(to: tokenFile)
} catch {
print("###\(#function): Failed to write token data. Error = \(error)")
}
}
}
/**
The file URL for persisting the persistent history token.
*/
private lazy var tokenFile: URL = {
let url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("CoreDataCloudKitDemo", isDirectory: true)
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch {
print("###\(#function): Failed to create persistent container URL. Error = \(error)")
}
}
return url.appendingPathComponent("token.data", isDirectory: false)
}()
/**
An operation queue for handling history processing tasks: watching changes, deduplicating tags, and triggering UI updates if needed.
*/
private lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
/**
The URL of the thumbnail folder.
*/
static var attachmentFolder: URL = {
var url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("CoreDataCloudKitDemo", isDirectory: true)
url = url.appendingPathComponent("attachments", isDirectory: true)
// Create it if it doesn’t exist.
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch {
print("###\(#function): Failed to create thumbnail folder URL: \(error)")
}
}
return url
}()
init() {
// Load the last token from the token file.
if let tokenData = try? Data(contentsOf: tokenFile) {
do {
lastHistoryToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
print("###\(#function): Failed to unarchive NSPersistentHistoryToken. Error = \(error)")
}
}
}
}
// MARK: - Notifications
extension CoreDataStack {
/**
Handle remote store change notifications (.NSPersistentStoreRemoteChange).
*/
#objc
func storeRemoteChange(_ notification: Notification) {
print("###\(#function): Merging changes from the other persistent store coordinator.")
// Process persistent history to merge changes from other coordinators.
historyQueue.addOperation {
self.processPersistentHistory()
}
}
}
/**
Custom notifications in this sample.
*/
extension Notification.Name {
static let didFindRelevantTransactions = Notification.Name("didFindRelevantTransactions")
}
// MARK: - Persistent history processing
extension CoreDataStack {
/**
Process persistent history, posting any relevant transactions to the current view.
*/
func processPersistentHistory() {
let taskContext = persistentContainer.newBackgroundContext()
taskContext.performAndWait {
// Fetch history received from outside the app since the last token
let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!
historyFetchRequest.predicate = NSPredicate(format: "author != %#", appTransactionAuthorName)
let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
request.fetchRequest = historyFetchRequest
let result = (try? taskContext.execute(request)) as? NSPersistentHistoryResult
guard let transactions = result?.result as? [NSPersistentHistoryTransaction],
!transactions.isEmpty
else { return }
// Post transactions relevant to the current view.
DispatchQueue.main.async {
NotificationCenter.default.post(name: .didFindRelevantTransactions, object: self, userInfo: ["transactions": transactions])
}
// Deduplicate the new tags.
var newTagObjectIDs = [NSManagedObjectID]()
let tagEntityName = Tag.entity().name
for transaction in transactions where transaction.changes != nil {
for change in transaction.changes!
where change.changedObjectID.entity.name == tagEntityName && change.changeType == .insert {
newTagObjectIDs.append(change.changedObjectID)
}
}
if !newTagObjectIDs.isEmpty {
deduplicateAndWait(tagObjectIDs: newTagObjectIDs)
}
// Update the history token using the last transaction.
lastHistoryToken = transactions.last!.token
}
}
}
// MARK: - Deduplicate tags
extension CoreDataStack {
/**
Deduplicate tags with the same name by processing the persistent history, one tag at a time, on the historyQueue.
All peers should eventually reach the same result with no coordination or communication.
*/
private func deduplicateAndWait(tagObjectIDs: [NSManagedObjectID]) {
// Make any store changes on a background context
let taskContext = persistentContainer.backgroundContext()
// Use performAndWait because each step relies on the sequence. Since historyQueue runs in the background, waiting won’t block the main queue.
taskContext.performAndWait {
tagObjectIDs.forEach { tagObjectID in
self.deduplicate(tagObjectID: tagObjectID, performingContext: taskContext)
}
// Save the background context to trigger a notification and merge the result into the viewContext.
taskContext.save(with: .deduplicate)
}
}
/**
Deduplicate a single tag.
*/
private func deduplicate(tagObjectID: NSManagedObjectID, performingContext: NSManagedObjectContext) {
guard let tag = performingContext.object(with: tagObjectID) as? Tag,
let tagName = tag.name else {
fatalError("###\(#function): Failed to retrieve a valid tag with ID: \(tagObjectID)")
}
// Fetch all tags with the same name, sorted by uuid
let fetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: Schema.Tag.uuid.rawValue, ascending: true)]
fetchRequest.predicate = NSPredicate(format: "\(Schema.Tag.name.rawValue) == %#", tagName)
// Return if there are no duplicates.
guard var duplicatedTags = try? performingContext.fetch(fetchRequest), duplicatedTags.count > 1 else {
return
}
print("###\(#function): Deduplicating tag with name: \(tagName), count: \(duplicatedTags.count)")
// Pick the first tag as the winner.
let winner = duplicatedTags.first!
duplicatedTags.removeFirst()
remove(duplicatedTags: duplicatedTags, winner: winner, performingContext: performingContext)
}
/**
Remove duplicate tags from their respective posts, replacing them with the winner.
*/
private func remove(duplicatedTags: [Tag], winner: Tag, performingContext: NSManagedObjectContext) {
duplicatedTags.forEach { tag in
defer { performingContext.delete(tag) }
guard let posts = tag.posts else { return }
for case let post as Post in posts {
if let mutableTags: NSMutableSet = post.tags?.mutableCopy() as? NSMutableSet {
if mutableTags.contains(tag) {
mutableTags.remove(tag)
mutableTags.add(winner)
}
}
}
}
}
}
First of all, I am just a beginner who is currently developing an app with the Swift language, so please don't mind my question too much because I really need to know and I am having trouble with maintaining the code that I constructed.
It's about the async delegate pattern.
Here is my API class. Assume that there are many API classes like that which makes async calls.
protocol InitiateAPIProtocol{
func didSuccessInitiate(results:JSON)
func didFailInitiate(err:NSError)
}
class InitiateAPI{
var delegate : InitiateAPIProtocol
init(delegate: InitiateAPIProtocol){
self.delegate=delegate
}
func post(wsdlURL:String,action:String,soapMessage : String){
let request = NSMutableURLRequest(URL: NSURL(string: wsdlURL)!)
let msgLength = String(soapMessage.characters.count)
let data = soapMessage.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
request.HTTPMethod = "POST"
request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(msgLength, forHTTPHeaderField: "Content-Length")
request.addValue(action, forHTTPHeaderField: "SOAPAction")
request.HTTPBody = data
let task = session.dataTaskWithRequest(request) {
data, response, error in
if error != nil {
self.delegate.didFailInitiate(error!)
return
}
let jsonData = JSON(data: data)
self.delegate.didSuccessInitiate(jsonData)
}
task.resume()
}
func doInitiate(token : String){
let soapMessage = “”
// WSDL_URL is the main wsdl url i will request.
action = “”
post(WSDL_URL, action: action, soapMessage: soapMessage)
}
}
Here is my ViewController:
class ViewController : UIViewController,InitiateAPIProtocol{
var initiateAPI : InitiateAPI!
var token : String = “sWAFF1”
override func viewWillAppear(animated: Bool) {
// Async call start
initiateAPI = InitiateAPI(delegate:self)
initiateAPI.doInitiate(token)
}
// Here comes call back
func didSuccessInitiate(results: JSON) {
//handle results
}
func didFailInitiate(err: NSError) {
//handle errors
}
}
My problem is I said that there are many API classes like that, so if one view controller handles 4 API classes, I have to handle many protocol delegates methods as I extend the view controller. There will be many delegates method below of view controller. If other view controllers call the same API and have to handle the same delegates, I have a problem maintaining the code because every time I change some delegate parameters, I have to fix the code at all view controllers which use those API classes.
Is there any other good way to handle async call?
If my question seems a little complex, please leave a comment, I will reply and explain it clearly.
Delegates (OOP) and "completion handlers" (function like programming) just don't fit well together.
In order to increase comprehension and to make the code more concise, an alternative approach is required. One of this approach has been already proposed by #PEEJWEEJ using solely completion handlers.
Another approach is using "Futures or Promises". These greatly extend the idea of completion handlers and make your asynchronous code look more like synchronous.
Futures work basically as follows. Suppose, you have an API function that fetches users from a remote web service. This call is asynchronous.
// Given a user ID, fetch a user:
func fetchUser(id: Int) -> Future<User> {
let promise = Promise<User>()
// a) invoke the asynchronous operation.
// b) when it finished, complete the promise accordingly:
doFetchAsync(id, completion: {(user, error) in
if error == nil {
promise.fulfill(user!)
} else {
promise.reject(error!)
}
})
return.promise.future
}
First, the important fact here is, that there is no completion handler. Instead, the asynchronous function returns you a future. A future represents the eventual result of the underlying operation. When the function fetchUser returns, the result is not yet computed, and the future is in a "pending" state. That is, you cannot obtain the result immediately from the future. So, we have to wait?? - well not really, this will be accomplished similar to an async function with a completion handler, i.e. registering a "continuation":
In order to obtain the result, you register a completion handler:
fetchUser(userId).map { user in
print("User: \(user)")
}.onFailure { error in
print("Error: \(error)")
}
It also handles errors, if they occur.
The function map is the one that registered the continuation. It is also a "combinator", that is it returns another future which you can combine with other functions and compose more complex operations.
When the future gets finally completed, the code continues with the closure registered with the future.
If you have two dependent operations, say OP1 generates a result which should be used in OP2 as input, and the combined result should be returned (as a future), you can accomplish this in a comprehensive and concise manner:
let imageFuture = fetchUser(userId).flatMap { user in
return user.fetchProfileImage()
}
imageFuture.onSuccess { image in
// Update table view on main thread:
...
}
This was just a very short intro into futures. They can do much more for you.
If you want to see futures in action, you may start the Xcode playgrounds "A Motivating Example" in the third party library FutureLib (I'm the author). You should also examine other Future/Promise libraries, for example BrightFutures. Both libraries implement Scala-like futures in Swift.
Have you looked into NSNotificationCenter?
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSNotificationCenter_Class/
You'll be able to post events from your api class, then each view controller would subscribe to the events and be notified accordingly
Does that make sense? There are lots of good examples of this pattern:
https://stackoverflow.com/a/24049111/2678994
https://stackoverflow.com/a/28269217/2678994
I've updated your code below:
class InitiateAPI{
//
// var delegate : InitiateAPIProtocol
// init(delegate: InitiateAPIProtocol){
// self.delegate=delegate
// }
func post(wsdlURL:String,action:String,soapMessage : String){
let request = NSMutableURLRequest(URL: NSURL(string: wsdlURL)!)
let msgLength = String(soapMessage.characters.count)
let data = soapMessage.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
request.HTTPMethod = "POST"
request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(msgLength, forHTTPHeaderField: "Content-Length")
request.addValue(action, forHTTPHeaderField: "SOAPAction")
request.HTTPBody = data
let task = session.dataTaskWithRequest(request) {
data, response, error in
if error != nil {
// self.delegate.didFailInitiate(error!)
/* Post notification with error */
NSNotificationCenter.defaultCenter().postNotificationName("onHttpError", object: error)
return
}
let jsonData = JSON(data: data)
// self.delegate.didSuccessInitiate(jsonData)
/* Post notification with json body */
NSNotificationCenter.defaultCenter().postNotificationName("onHttpSuccess", object: jsonData)
}
task.resume()
}
func doInitiate(token : String){
let soapMessage = “”
// WSDL_URL is the main wsdl url i will request.
action = “”
post(WSDL_URL, action: action, soapMessage: soapMessage)
}
}
Your view controller class:
class ViewController : UIViewController { //,InitiateAPIProtocol{
var initiateAPI : InitiateAPI!
var token : String = “sWAFF1”
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.didSuccessInitiate(_:)), name: "onHttpSuccess", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.didFailInitiate(_:)), name: "onHttpError", object: nil)
}
override func viewWillAppear(animated: Bool) {
// Async call start
initiateAPI = InitiateAPI(delegate:self)
initiateAPI.doInitiate(token)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
/* Remove listeners when view controller disappears */
NSNotificationCenter.defaultCenter().removeObserver(self, name: "onHttpSuccess", object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: "onHttpError", object: nil)
}
// Here comes call back
func didSuccessInitiate(notification : NSNotification) { //results: JSON) {
if let payload = notification.object as? JSON {
//handle results
}
}
func didFailInitiate(notification : NSNotification) { //err: NSError) {
if let payload = notification.object as? NSError {
//handle errors
}
}
}
Instead of using a delegate, you could (should?) use closers/functions:
func post(/*any other variables*/ successCompletion: (JSON) -> (), errorCompletion: (NSError) ->()){
/* do whatever you need to*/
/*if succeeds*/
successCompletion("")
/*if fails*/
errorCompletion(error)
}
// example using closures
post({ (data) in
/* handle Success*/
}) { (error) in
/* handle error */
}
// example using functions
post(handleData, errorCompletion: handleError)
func handleData(data: JSON) {
}
func handleError(error: NSError) {
}
This would also give you the option to handle all the errors with one function.
Also, it's ideal to parse your JSON into their desired objects before returning them. This keeps your ViewControllers clean and makes it clear where the parsing will occur.