iOS11 How to handle a custom file dropped in a custom view - ios

I've been messing around with the drop feature of iOS11. I found a lot of examples with the standard UIImage etc but nothing with custom files.
The drop part works pretty well, I use it to drop a custom file and import it into my app.
The problem is that I only get the Data of this file and I would like to get its name and extension for example.
I don't know if my way of handling the custom file dropped is how it should be to obtain this information. And if so, how do you get this information from the file?
I have a DropFile class conforming to the NSItemProviderReading protocol.
class DropFile : NSObject, NSItemProviderReading {
let fileData:Data?
required init(data:Data, typeIdentifier:String) {
fileData = data
}
static var readableTypeIdentifiersForItemProvider: [String] {
var documentTypeArray: [String] = []
for ext in FileImportProcessHelper.sharedInstance.getImportDocumentType() {
let UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil)
documentTypeArray.append((UTI?.takeRetainedValue() as String?)!)
}
return documentTypeArray
}
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self {
return self.init(data: data, typeIdentifier: typeIdentifier)
}
}
This is my ViewController conforming to the UIDropInteractionDelegate
// MARK: Handling Drop
#available(iOS 11.0, *)
// We refuse the dropped items based on their UTI
func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
var documentTypeArray: [String] = []
for ext in FileImportProcessHelper.sharedInstance.getImportDocumentType() {
let UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil)
documentTypeArray.append(UTI?.takeRetainedValue() as! String)
}
return session.hasItemsConforming(toTypeIdentifiers: documentTypeArray) && session.items.count == 1
}
#available(iOS 11.0, *)
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
// Copy file from source app
return UIDropProposal(operation: .copy)
}
#available(iOS 11.0, *)
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
// Consume drag items
session.loadObjects(ofClass: DropFile.self) { items in
if let fileItems = items as? [DropFile] {
DragnDropManager.sharedManager.createDropObject(fileItems: fileItems)
}
}
}
And then how I write my file.
func createDropObject(fileItems: [DropFile]) {
let documentsPathString = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
let inbox = URL(fileURLWithPath: documentsPathString!).appendingPathComponent("UserDropInbox/")
do {
try FileManager.default.createDirectory(atPath: inbox.path, withIntermediateDirectories: true, attributes: nil)
} catch let error as NSError {
NSLog("Unable to create directory \(error.debugDescription)")
}
for file in fileItems {
do {
let dropFilePath = inbox.appendingPathComponent("File").appendingPathExtension("pdf")
try file.fileData?.write(to:dropFilePath)
} catch {
NSLog(error as! String)
}
}
FileImportInboxManager.shared.hasReceivedFiles = true;
}

I admit, it's not straightforward :
you can list all the UIDragItems dragged by the user in the UIDropSession items member
each of these items have a NSItemProvider itemProvider member
this item provider has an optional String? suggestedName member that may not be nil
A simple loop to print all the session's items suggested names :
for item in session.items {
if let name = item.itemProvider.suggestedName {
print(name)
}
}
Practically, when files come from the File app it will provide the name of the file without its extension, but given you can easily access its UTI, you can append an extension that will be close to the original one (jpeg instead of jpg for example).

Related

iOS - CoreData CloudKit sharing - participants adding children

I have implemented sharing of items from my Core Data store, using the NSPersistentCloudKitContainer api, with limited success. The objects I share (a TripsLog) have child objects (Trip), which appear on participants' devices. I've implemented more or less a direct copy of Apple's example code, except using my own core data entities.
The problem is that any new trips I create on participants' devices don't appear in the owner's database, whereas any I create on the owner's device appear for all participants, updating their displays pretty much instantly. If I edit any of the original trips on a participant's device, the changes are successfully shared, it's just the new ones which are problematic.
I'm getting a lot of noise in the console -
"Attempt to map database failed: permission was denied. This attempt will not be retried"
"Permission Failure" (10/2007); server message = "Invalid bundle ID for container"
etc
which really aren't helpful. The bundle ID & containers are set up properly and it all works fine for syncing across a single user's devices.
Although my code is more-or-less just the same as Apple's, except for the container identifier & the Core Data entities, here's most of the relevant stuff -
// sharing protocol
protocol CloudKitSharable: NSManagedObject {
static var entityName: String { get }
var identifier: String { get }
var sharedTitle: String { get }
var sharedSubject: String? { get }
var thumbnailImage: UIImage? { get }
}
class PersistenceController: NSObject {
enum CoreDataError: Error {
case modelURLNotFound(forResourceName: String)
case modelLoadingFailed(forURL: URL)
}
static let shared = PersistenceController()
private static let containerIdentifier = "iCloud.com.containerIdentifier"
private var _privatePersistentStore: NSPersistentStore?
var privatePersistentStore: NSPersistentStore {
return _privatePersistentStore!
}
private var _sharedPersistentStore: NSPersistentStore?
var sharedPersistentStore: NSPersistentStore {
return _sharedPersistentStore!
}
lazy var cloudKitContainer: CKContainer = {
return CKContainer(identifier: Self.containerIdentifier)
}()
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container: NSPersistentCloudKitContainer = try! mainDatabaseContainer()
guard let localDatabaseURL = localDatabaseURL, let cloudDatabaseURL = cloudDatabaseURL else {
fatalError("#\(#function): Failed to get local database URLs")
}
// Set up the database which will sync over the cloud
let cloudStoreDescription = NSPersistentStoreDescription(url: cloudDatabaseURL)
cloudStoreDescription.configuration = "Cloud"
cloudStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
setupStoreDescription(cloudStoreDescription)
let cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: Self.containerIdentifier)
cloudKitContainerOptions.databaseScope = .private
cloudStoreDescription.cloudKitContainerOptions = cloudKitContainerOptions
// Setting up for sharing
let sharedStoreDescription = cloudStoreDescription.copy() as! NSPersistentStoreDescription
sharedStoreDescription.url = sharedDatabaseURL
let sharedStoreOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: Self.containerIdentifier)
sharedStoreOptions.databaseScope = .shared
sharedStoreDescription.cloudKitContainerOptions = sharedStoreOptions
// Set up the stuff which we don't want to sync to the cloud
let localStoreDescription = NSPersistentStoreDescription(url: localDatabaseURL)
localStoreDescription.configuration = "Local"
setupStoreDescription(localStoreDescription)
// finish setting up the container
container.persistentStoreDescriptions = [
cloudStoreDescription,
localStoreDescription,
sharedStoreDescription
]
loadPersistentStores(for: container)
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = TransactionAuthor.app
/**
Automatically merge the changes from other contexts.
*/
container.viewContext.automaticallyMergesChangesFromParent = true
/**
Pin the viewContext to the current generation token and set it to keep itself up-to-date with local changes.
*/
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("#\(#function): Failed to pin viewContext to the current generation:\(error)")
}
/**
Observe the following notifications:
- The remote change notifications from container.persistentStoreCoordinator.
- The .NSManagedObjectContextDidSave notifications from any context.
- The event change notifications from the container.
*/
NotificationCenter.default.addObserver(self, selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator)
NotificationCenter.default.addObserver(self, selector: #selector(containerEventChanged(_:)),
name: NSPersistentCloudKitContainer.eventChangedNotification,
object: container)
return container
}()
func mergeTransactions(_ transactions: [NSPersistentHistoryTransaction], to context: NSManagedObjectContext) {
context.perform {
for transaction in transactions {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
}
}
private var cloudDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("CloudDatabase.sqlite")
}
private var localDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("Database.sqlite")
}
private var sharedDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("Shared.sqlite")
}
private func setupStoreDescription(_ description: NSPersistentStoreDescription) {
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.shouldInferMappingModelAutomatically = true
}
private func model(name: String) throws -> NSManagedObjectModel {
return try loadModel(name: name, bundle: Bundle.main)
}
private func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
throw CoreDataError.modelURLNotFound(forResourceName: name)
}
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
throw CoreDataError.modelLoadingFailed(forURL: modelURL)
}
return model
}
private func mainDatabaseContainer() throws -> NSPersistentCloudKitContainer {
return NSPersistentCloudKitContainer(name: "Database", managedObjectModel: try model(name: "Database"))
}
private func locationsContainer() throws -> NSPersistentContainer {
return NSPersistentContainer(name: "Locations", managedObjectModel: try model(name: "Locations"))
}
private func loadPersistentStores(for container: NSPersistentContainer) {
container.loadPersistentStores { [unowned self] storeDescription, error in
if let error = error {
fatalError("#\(#function): Failed to load persistent stores:\(error)")
} else {
print("Database store ok: ", storeDescription)
if let containerOptions = storeDescription.cloudKitContainerOptions, let url = self.sharedDatabaseURL {
if containerOptions.databaseScope == .shared {
let sharedStore = container.persistentStoreCoordinator.persistentStore(for: url)
self._sharedPersistentStore = sharedStore
}
} else if let url = self.cloudDatabaseURL {
let privateStore = container.persistentStoreCoordinator.persistentStore(for: url)
self._privatePersistentStore = privateStore
}
}
}
}
/**
An operation queue for handling history-processing tasks: watching changes, deduplicating tags, and triggering UI updates, if needed.
*/
lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
}
// Sharing extensions
extension PersistenceController {
func presentCloudSharingController<T: CloudKitSharable>(for item: T) {
/**
Grab the share if the item is already shared.
*/
var itemToShare: CKShare?
if let shareSet = try? persistentContainer.fetchShares(matching: [item.objectID]), let (_, share) = shareSet.first {
itemToShare = share
}
let sharingController: UICloudSharingController
if let itemToShare = itemToShare {
sharingController = UICloudSharingController(share: itemToShare, container: cloudKitContainer)
} else {
sharingController = newSharingController(for: item)
}
sharingController.delegate = self
/**
Setting the presentation style to .formSheet so there's no need to specify sourceView, sourceItem, or sourceRect.
*/
if let viewController = rootViewController {
sharingController.modalPresentationStyle = .formSheet
viewController.present(sharingController, animated: true)
}
}
func presentCloudSharingController(share: CKShare) {
let sharingController = UICloudSharingController(share: share, container: cloudKitContainer)
sharingController.delegate = self
/**
Setting the presentation style to .formSheet so there's no need to specify sourceView, sourceItem, or sourceRect.
*/
if let viewController = rootViewController {
sharingController.modalPresentationStyle = .formSheet
viewController.present(sharingController, animated: true)
}
}
private func newSharingController<T: CloudKitSharable>(for unsharedItem: T) -> UICloudSharingController {
return UICloudSharingController { (_, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
/**
The app doesn't specify a share intentionally, so Core Data creates a new share (zone).
CloudKit has a limit on how many zones a database can have, so this app provides an option for users to use an existing share.
If the share's publicPermission is CKShareParticipantPermissionNone, only private participants can accept the share.
Private participants mean the participants an app adds to a share by calling CKShare.addParticipant.
If the share is more permissive, and is, therefore, a public share, anyone with the shareURL can accept it,
or self-add themselves to it.
The default value of publicPermission is CKShare.ParticipantPermission.none.
*/
self.persistentContainer.share([unsharedItem], to: nil) { objectIDs, share, container, error in
if let share = share {
self.configure(share: share, with: unsharedItem)
}
completion(share, container, error)
}
}
}
private var rootViewController: UIViewController? {
for scene in UIApplication.shared.connectedScenes {
if scene.activationState == .foregroundActive,
let sceneDelegate = (scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate,
let window = sceneDelegate.window {
return window?.rootViewController
}
}
print("\(#function): Failed to retrieve the window's root view controller.")
return nil
}
}
extension PersistenceController: UICloudSharingControllerDelegate {
/**
CloudKit triggers the delegate method in two cases:
- An owner stops sharing a share.
- A participant removes themselves from a share by tapping the Remove Me button in UICloudSharingController.
After stopping the sharing, purge the zone or just wait for an import to update the local store.
This sample chooses to purge the zone to avoid stale UI. That triggers a "zone not found" error because UICloudSharingController
deletes the zone, but the error doesn't really matter in this context.
Purging the zone has a caveat:
- When sharing an object from the owner side, Core Data moves the object to the shared zone.
- When calling purgeObjectsAndRecordsInZone, Core Data removes all the objects and records in the zone.
To keep the objects, deep copy the object graph you want to keep and make sure no object in the new graph is associated with any share.
The purge API posts an NSPersistentStoreRemoteChange notification after finishing its job, so observe the notification to update
the UI, if necessary.
*/
func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) {
if let share = csc.share {
purgeObjectsAndRecords(with: share)
}
}
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
if let share = csc.share, let persistentStore = share.persistentStore {
persistentContainer.persistUpdatedShare(share, in: persistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
}
}
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("\(#function): Failed to save a share: \(error)")
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return csc.share?.title ?? "Airframe Logbook"
}
}
extension PersistenceController {
func shareObject<T: CloudKitSharable>(_ unsharedObject: T, to existingShare: CKShare?, completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)? = nil) {
persistentContainer.share([unsharedObject], to: existingShare) { (objectIDs, share, container, error) in
guard error == nil, let share = share else {
print("\(#function): Failed to share an object: \(error!))")
completionHandler?(share, error)
return
}
/**
Deduplicate tags, if necessary, because adding a photo to an existing share moves the whole object graph to the associated
record zone, which can lead to duplicated tags.
*/
if existingShare != nil {
/*
if let tagObjectIDs = objectIDs?.filter({ $0.entity.name == "Tag" }), !tagObjectIDs.isEmpty {
self.deduplicateAndWait(tagObjectIDs: Array(tagObjectIDs))
}
*/
} else {
self.configure(share: share, with: unsharedObject)
}
/**
Synchronize the changes on the share to the private persistent store.
*/
self.persistentContainer.persistUpdatedShare(share, in: self.privatePersistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
/**
Delete the Core Data objects and the records in the CloudKit record zone associated with the share.
*/
func purgeObjectsAndRecords(with share: CKShare, in persistentStore: NSPersistentStore? = nil) {
guard let store = (persistentStore ?? share.persistentStore) else {
print("\(#function): Failed to find the persistent store for share. \(share))")
return
}
persistentContainer.purgeObjectsAndRecordsInZone(with: share.recordID.zoneID, in: store) { (zoneID, error) in
if let error = error {
print("\(#function): Failed to purge objects and records: \(error)")
}
}
}
func existingShare(for item: NSManagedObject) -> CKShare? {
if let shareSet = try? persistentContainer.fetchShares(matching: [item.objectID]),
let (_, share) = shareSet.first {
return share
}
return nil
}
func share(with title: String) -> CKShare? {
let stores = [privatePersistentStore, sharedPersistentStore]
let shares = try? persistentContainer.fetchShares(in: stores)
let share = shares?.first(where: { $0.title == title })
return share
}
func shareTitles() -> [String] {
let stores = [privatePersistentStore, sharedPersistentStore]
let shares = try? persistentContainer.fetchShares(in: stores)
return shares?.map { $0.title } ?? []
}
private func configure<T: CloudKitSharable>(share: CKShare, with item: T) {
share[CKShare.SystemFieldKey.title] = item.sharedTitle
}
}
extension PersistenceController {
func addParticipant(emailAddress: String, permission: CKShare.ParticipantPermission = .readWrite, share: CKShare,
completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) {
/**
Use the email address to look up the participant from the private store. Return if the participant doesn't exist.
Use privatePersistentStore directly because only the owner may add participants to a share.
*/
let lookupInfo = CKUserIdentity.LookupInfo(emailAddress: emailAddress)
let persistentStore = privatePersistentStore //share.persistentStore!
persistentContainer.fetchParticipants(matching: [lookupInfo], into: persistentStore) { (results, error) in
guard let participants = results, let participant = participants.first, error == nil else {
completionHandler?(share, error)
return
}
participant.permission = permission
participant.role = .privateUser
share.addParticipant(participant)
self.persistentContainer.persistUpdatedShare(share, in: persistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
func deleteParticipant(_ participants: [CKShare.Participant], share: CKShare,
completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) {
for participant in participants {
share.removeParticipant(participant)
}
/**
Use privatePersistentStore directly because only the owner may delete participants to a share.
*/
persistentContainer.persistUpdatedShare(share, in: privatePersistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
// Core Data models
extension TripsLog {
#NSManaged var name: String
#NSManaged var identifier: String
#NSManaged var entries: NSSet
}
extension Trip {
#NSManaged var identifier: String
#NSManaged var name: String
#NSManaged var date: Date
#NSManaged var comments: String?
#NSManaged var leaderName: String
#NSManaged var images: NSSet?
}
If anyone is able to shed any light on this I'd really appreciate it, as Apple's own documentation is somewhat lacking. Many thanks!

How to list files and folder in Google drive root folder

This code is used to show google drive contents in my iOS app. Now I could sign in and show contents by using below query "mimeType ='\(mimeType)' or mimeType = 'application/vnd.google-apps.folder'".
The problem is that it returns all the mp3 files even the ones inside sub-folders, that is not what I want. I want to show the same structure as google drive root. Then when a user enter any sub-folder, I would send another request to retrieve the mp3 files in that sub-folder.
So how could I reconstruct this query to achieve it?
// the code to filter/search google drive files.
import Foundation
import GoogleAPIClientForREST
class GoogleDriveAPI {
private let service: GTLRDriveService
init(service: GTLRDriveService) {
self.service = service
}
public func search(_ mimeType: String, onCompleted: #escaping ([GTLRDrive_File]?, Error?) -> ()) {
let query = GTLRDriveQuery_FilesList.query()
query.pageSize = 100
query.q = "mimeType ='\(mimeType)' or mimeType = 'application/vnd.google-apps.folder'"
self.service.executeQuery(query) { (ticket, results, error) in
onCompleted((results as? GTLRDrive_FileList)?.files, error)
}
}
if you do mimeType = 'application/vnd.google-apps.folder' then you are telling it that you only want folders or a specific mime type.
if you do 'root' in parents" it will return everything with a parent folder of root.
so if you do 'root' in parents" and mimeType = 'application/vnd.google-apps.folder' you will get all of the folders that have a parent folder of root.
Per DalmTo's answer, I have changed my code and have some test. Now I get what I want.
In short, I split the list file into two steps, first to search with query = 'root' in parents", which will list all contents in root folder. Then I do a filter files?.filter { $0.mimeType == "audio/mpeg" || $0.mimeType == "application/vnd.google-apps.folder"} before passing that data source to another ViewController.
To do this, I get every folders in root and every mp3 files in root, other type of files are ignored. Then if a user enter one folder in root, I would do another http request(file list) to get its content.
Google drive API function.
import Foundation
import GoogleAPIClientForREST
class GoogleDriveAPI {
private let service: GTLRDriveService
init(service: GTLRDriveService) {
self.service = service
}
public func search(onCompleted: #escaping ([GTLRDrive_File]?, Error?) -> ()) {
let query = GTLRDriveQuery_FilesList.query()
query.pageSize = 100
// query.q = "mimeType ='\(mimeType)' or mimeType = 'application/vnd.google-apps.folder'"
query.q = "'root' in parents"
self.service.executeQuery(query) { (ticket, results, error) in
onCompleted((results as? GTLRDrive_FileList)?.files, error)
}
}
call api from a ViewController, and I put a bit context here to make it clear.
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
setViews()
// add an observer on notification "userDidSignInGoogle"
NotificationCenter.default.addObserver(self, selector: #selector(userDidSignInGoogle), name: .signInGoogleCompleted, object: nil)
setUpGoogleSignIn()
}
func setUpGoogleSignIn() {
GIDSignIn.sharedInstance().delegate = self
GIDSignIn.sharedInstance().uiDelegate = self
GIDSignIn.sharedInstance().scopes = [kGTLRAuthScopeDrive]
GIDSignIn.sharedInstance().signInSilently()
}
// MARK: - Notification
#objc private func userDidSignInGoogle(_ notification: Notification) {
// Update screen after user successfully signed in
updateScreen()
print("userDidSignInGoogle")
listAudioFilesAndFolders()
}
func listAudioFilesAndFolders() {
self.googleAPIs?.search(onCompleted: { files, error in
guard error == nil, files != nil else {
print("Err: \(String(describing: error))")
return
}
self.dismiss(animated: true) {
let vc = GoogleDriveFilesViewController()
// filter the files before passing it.
vc.audioFilesAndFolders = files?.filter { $0.mimeType == "audio/mpeg" || $0.mimeType == "application/vnd.google-apps.folder"}
UIApplication.getTopMostViewController()?.present(vc, animated: true)
}
})
}

Where to store Decoded JSON array from server and how to access it globally in viewControllers?

Currently im creating application which parses JSON from my server. From server I can receive array with JSON models.
Data from this array must be populated in table View.
My question Is simple: where to store decoded array from server, if I want to access it from many viewControllers in my application?
Here is my JSON model, which coming from server.
import Foundation
struct MyModel: Codable {
var settings: Test?
var provider: [Provider]
}
extension MyModel {
struct setting: Codable {
var name: String
var time: Int
}
}
here is how I am decoding it
import Foundation
enum GetResourcesRequest<ResourceType> {
case success([ResourceType])
case failure
}
struct ResourceRequest<ResourceType> where ResourceType: Codable {
var startURL = "https://myurl/api/"
var resourceURL: URL
init(resourcePath: String) {
guard let resourceURL = URL(string: startURL) else {
fatalError()
}
self.resourceURL = resourceURL.appendingPathComponent(resourcePath)
}
func fetchData(completion: #escaping
(GetResourcesRequest<ResourceType>) -> Void ) {
URLSession.shared.dataTask(with: resourceURL) { data, _ , _ in
guard let data = data else { completion(.failure)
return }
let decoder = JSONDecoder()
if let jsonData = try? decoder.decode([ResourceType].self, from: data) {
completion(.success(jsonData))
} else {
completion(.failure)
}
}.resume()
}
}
This is an example of CategoriesProvider. It just stores categories in-memory and you can use them across the app. It is not the best way to do it and not the best architecture, but it is simple to get started.
class CategoriesProvider {
static let shared = CategoriesProvider()
private(set) var categories: [Category]?
private let categoryRequest = ResourceRequest<Category>(resourcePath: "categories")
private let dataTask: URLSessionDataTask?
private init() {}
func fetchData(completion: #escaping (([Category]?) -> Void)) {
guard categories == nil else {
completion(categories)
return
}
dataTask?.cancel()
dataTask = categoryRequest.fetchData { [weak self] categoryResult in
var fetchedCategories: [Category]?
switch categoryResult {
case .failure:
print("error")
case .success(let categories):
fetchedCategories = categories
}
DispatchQueue.main.async {
self?.categories = fetchedCategories
completion(fetchedCategories)
}
}
}
}
I suggest using URLSessionDataTask in order to cancel a previous task. It could happen when you call fetchData several times one after another. You have to modify your ResourceRequest and return value of URLSession.shared.dataTask(...)
Here more details about data task https://www.raywenderlich.com/3244963-urlsession-tutorial-getting-started#toc-anchor-004 (DataTask and DownloadTask)
Now you can fetch categories in CategoriesViewController in this way:
private func loadTableViewData() {
CategoriesProvider.shared.fetchData { [weak self] categories in
guard let self = self, let categories = categories else { return }
self.categories = categories
self.tableView.reloadData()
}
}
In the other view controllers, you can do the same but can check for the 'categories' before making a fetch.
if let categories = CategoriesProvider.shared.categories {
// do something
} else {
CategoriesProvider.shared.fetchData { [weak self] categories in
// do something
}
}
If you really want to avoid duplicate load data() calls, your simplest option would be to cache the data on disk (CoreData, Realm, File, etc.) after parsing it the first time.
Then every ViewController that needs the data, can just query your storage system.
Of course the downside of this approach is the extra code you'll have to write to manage the coherency of your data to make sure it's properly managed across your app.
make a global dictionary array outside any class to access it on every viewcontroller.

Remove the observer using the handle in Firebase in Swift

I have the following case. The root controller is UITabViewController. There is a ProfileViewController, in it I make an observer that users started to be friends (and then the screen functions change). ProfileViewController can be opened with 4 tabs out of 5, and so the current user can open the screen with the same user in four places. In previous versions, when ProfileViewController opened in one place, I deleted the observer in deinit and did the deletion just by ref.removeAllObservers(), now when the user case is such, I started using handle and delete observer in viewDidDisappear. I would like to demonstrate the code to find out whether it can be improved and whether I'm doing it right in this situation.
I call this function in viewWillAppear
fileprivate func firObserve(_ isObserve: Bool) {
guard let _user = user else { return }
FIRFriendsDatabaseManager.shared.observeSpecificUserFriendshipStart(observer: self, isObserve: isObserve, userID: _user.id, success: { [weak self] (friendModel) in
}) { (error) in
}
}
This is in the FIRFriendsDatabaseManager
fileprivate var observeSpecificUserFriendshipStartDict = [AnyHashable : UInt]()
func observeSpecificUserFriendshipStart(observer: Any, isObserve: Bool, userID: String, success: ((_ friendModel: FriendModel) -> Void)?, fail: ((_ error: Error) -> Void)?) {
let realmManager = RealmManager()
guard let currentUserID = realmManager.getCurrentUser()?.id else { return }
DispatchQueue.global(qos: .background).async {
let specificUserFriendRef = Database.database().reference().child(MainGateways.friends.description).child(currentUserID).child(SubGateways.userFriends.description).queryOrdered(byChild: "friendID").queryEqual(toValue: userID)
if !isObserve {
guard let observerHashable = observer as? AnyHashable else { return }
if let handle = self.observeSpecificUserFriendshipStartDict[observerHashable] {
self.observeSpecificUserFriendshipStartDict[observerHashable] = nil
specificUserFriendRef.removeObserver(withHandle: handle)
debugPrint("removed handle", handle)
}
return
}
var handle: UInt = 0
handle = specificUserFriendRef.observe(.childAdded, with: { (snapshot) in
if snapshot.value is NSNull {
return
}
guard let dict = snapshot.value as? [String : Any] else { return }
guard let friendModel = Mapper<FriendModel>().map(JSON: dict) else { return }
if friendModel.friendID == userID {
success?(friendModel)
}
}, withCancel: { (error) in
fail?(error)
})
guard let observerHashable = observer as? AnyHashable else { return }
self.observeSpecificUserFriendshipStartDict[observerHashable] = handle
}
}
Concerning your implementation of maintaining a reference to each viewController, I would consider moving the logic to an extension of the viewController itself.
And if you'd like to avoid calling ref.removeAllObservers() like you were previously, and assuming that there is just one of these listeners per viewController. I'd make the listener ref a variable on the view controller.
This way everything is contained to just the viewController. It also is potentially a good candidate for creating a protocol if other types of viewControllers will be doing similar types of management of listeners.

Write Generic Swift Method to load data from property list

I have a method that loads an array of dictionaries from a propertylist. Then I change those arrays of dictionaries to array of a defined custom type;
I want to write that method in generic form so I call that method with the type I expect, then the method loads it and returns an array of my custom type rather than dictionaries
func loadPropertyList(fileName: String) -> [[String:AnyObject]]?
{
if let path = NSBundle.mainBundle().pathForResource(fileName, ofType: "plist")
{
if let plistXML = NSFileManager.defaultManager().contentsAtPath(path)
{
do {
if let temp = try NSPropertyListSerialization.propertyListWithData(plistXML, options: .Immutable, format: nil) as? [[String:AnyObject]]
{
return temp
}
}catch{}
}
}
return nil
}
//
func loadList<T>(fileName: String) -> [T]?{//**Here the answer I am expecting**}
I am assuming your function to read from a Plist works and that you don't want to subclass NSObject.
Since Swift reflecting does not support setting values this is not possible without some implementation for each Type you want this to work for.
It can however be done in a pretty elegant way.
struct PlistUtils { // encapsulate everything
static func loadPropertyList(fileName: String) -> [[String:AnyObject]]? {
if let path = NSBundle.mainBundle().pathForResource(fileName, ofType: "plist") {
if let plistXML = NSFileManager.defaultManager().contentsAtPath(path) {
do {
if let temp = try NSPropertyListSerialization.propertyListWithData(plistXML, options: .Immutable, format: nil) as? [[String:AnyObject]] {
return temp
}
} catch {
return nil
}
}
}
return nil
}
}
This protocol will be used in a generic fashion to get the Type name and read the corresponding Plist.
protocol PListConstructible {
static func read() -> [Self]
}
This protocol will be used to implement Key Value setters.
protocol KeyValueSettable {
static func set(fromKeyValueStore values:[String:AnyObject]) -> Self
}
This is the combination of both to generate an array of objects. This does require that the Plist is named after the Type.
extension PListConstructible where Self : KeyValueSettable {
static func read() -> [Self] {
let name = String(reflecting: self)
var instances : [Self] = []
if let data = PlistUtils.loadPropertyList(name) {
for entry in data {
instances.append(Self.set(fromKeyValueStore: entry))
}
}
return instances
}
}
This is some Type.
struct Some : PListConstructible {
var alpha : Int = 0
var beta : String = ""
}
All you have to do is implement the Key Value setter and it will now be able to be read from a Plist.
extension Some : KeyValueSettable {
static func set(fromKeyValueStore values: [String : AnyObject]) -> Some {
var some = Some()
some.alpha = (values["alpha"] as? Int) ?? some.alpha
some.beta = (values["beta"] as? String) ?? some.beta
return some
}
}
This is how you use it.
Some.read()

Resources