CollectionView using NSDiffableDataSource with UICollectionViewFlowLayout - uicollectionview

When I use UICollectionView with UICollectionViewFlowLayout set. And then try to apply snapshots of datasource via
// load initial data
reloadDataSource()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
self.reloadDataSource(animating: true)
}
I am getting crash on second snapshot applied after 3 seconds delay.
The crash is happening only when animating: true
If I set animating to false then there is no crash but the collection view if left empty.
Here is this mathod applying data source
extension CollectionViewController {
func reloadDataSource(animating: Bool = false) {
print("reloading data source with snapshot -> \(snapshot.numberOfItems)")
self.dataSource.apply(self.snapshot, animatingDifferences: animating) {
print("applying snapshot completed!")
}
}
}
Data source is just
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: cellProvider)
Full project you can play (may changes over time): https://github.com/michzio/SwifUICollectionView
Update
I've tried to simplify example and do something like this and it doesn't work correctly. It seems that moving .apply() to background queue, other queue causes empty data in collectionview
func reloadDataSource(animating: Bool = false) {
print("reloading data source with snapshot -> \(snapshot.numberOfItems)")
diffQueue.async {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.categories])
snapshot.appendItems(Item.categoryItems)
self.dataSource.apply(snapshot, animatingDifferences: animating) {
print("applying snapshot completed!")
}
}
}

Ok it seems that i found the reason of all my errors with updating datasource by applying new snapshots
This lazy var dataSource is causing errors:
private(set) lazy var dataSource: UICollectionViewDiffableDataSource<Section, Item> = {
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: cellProvider)
//dataSource.supplementaryViewProvider = supplementaryViewProvider
return dataSource
}()
I've changed it to
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
and after configureCollectionView in viewDidLoad()
now I am calling configureDataSource() that does what was in lazy var initializer.

Related

SwiftUI ScrollView to PDF

Currently, I am using this code to create a PDF, but it only creates a PDF of the part of the page the user is looking at, and not the entire scroll view. How can I create a multi-page PDF to get the entire scroll view?
func exportToPDF(width:CGFloat, height:CGFloat, airplane: String, completion: #escaping (String) -> Void) {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let outputFileURL = documentDirectory.appendingPathComponent("smartsheet-\(airplane.afterchar(first: "N")).pdf")
//Normal with
let width: CGFloat = width
//Estimate the height of your view
let height: CGFloat = height
let charts = SmartSheet().environmentObject(Variables())
let pdfVC = UIHostingController(rootView: charts)
pdfVC.view.frame = CGRect(x: 0, y: 0, width: width, height: height)
//Render the view behind all other views
let rootVC = UIApplication.shared.windows.first?.rootViewController
rootVC?.addChild(pdfVC)
rootVC?.view.insertSubview(pdfVC.view, at: 0)
//Render the PDF
let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: width, height: height))
DispatchQueue.main.async {
do {
try pdfRenderer.writePDF(to: outputFileURL, withActions: { (context) in
context.beginPage()
rootVC?.view.layer.render(in: context.cgContext)
})
UserDefaults.standard.set(outputFileURL, forKey: "pdf")
UserDefaults.standard.synchronize()
print("wrote file to: \(outputFileURL.path)")
completion(outputFileURL.path)
} catch {
print("Could not create PDF file: \(error.localizedDescription)")
}
pdfVC.removeFromParent()
pdfVC.view.removeFromSuperview()
}
}
I just posted this answer to another question as well.
https://stackoverflow.com/a/74033480/14076779
After attempting a number of solutions to creating a pdf from a SwiftUI ScrollView and landed on this implementation. It is by no means an implementation that is pretty, but it seems to work.
M use case is that I present a modal to capture a user's signature and when the signature is collected, the content of the current screen is captured. There is no pagination, just a capture of the content currently rendered to the width of the screen and as much length as is required by the view.
The content consists of an array of cards that are dynamically generated, so the view has no awareness of the content and cannot calculate the height. I have not tested on a list, but I imagine it would provide similar results.
struct SummaryTabView: View {
// MARK: View Model
#ObservedObject var viewModel: SummaryTabViewModel
// MARK: States and Bindings
#State private var contentSize: CGSize = .zero // Updated when the size preference key for the overlay changes
#State private var contentSizeForPDF: CGSize = .zero // Set to match the content size when the signature modal is displayed.
// MARK: Initialization
init(viewModel: SummaryTabViewModel) {
self.viewModel = viewModel
}
// MARK: View Body
var body: some View {
ScrollView {
content
.background(Color.gray)
// Placing the a geometry reader in the overlay here seems to work better than doing so in a background and updates the size value for the size preference key.
.overlay(
GeometryReader { proxy in
Color.clear.preference(key: SizePreferenceKey.self,
value: proxy.size)
}
)
}
.onPreferenceChange(SizePreferenceKey.self) {
// This keeps the content size up-to-date during changes to the content including rotating.
contentSize = $0
}
// There are issues with the sizing the content when the orientation changes and a modal view is open such as when collecting the signature. If the iPad is rotated when the modal is open, then the content size is not being set properly. To avoid this, when the user opens the modal for signing, the content size for PDF generation is set to the current value since the signature placeholder is already present so the content size will not change.
.onReceive(NotificationCenter.default.publisher(for: .openingSignatureModal)) { _ in
self.contentSizeForPDF = contentSize
}
}
// MARK: Subviews
var header: SceneHeaderView {
SceneHeaderView(viewModel: viewModel)
}
// Extracting this view into a variable allows grabbing the screenshot when the customer signature is collected.
var content: some View {
LazyVStack(spacing: 16) {
header
ForEach(viewModel.cardViewModels) { cardViewModel in
// Each card needs to be in a VStack with no spacing or it will end up having a gap between the header and the card body.
VStack(spacing: 0) {
UICardView(viewModel: cardViewModel)
}
}
}
.onReceive(viewModel.signaturePublisher) {
saveSnapshot(content: content, size: contentSizeForPDF)
}
}
// MARK: View Functions
/// Creates and saves a pdf data file to the inspection's report property. This is done asynchronously to avoid odd behavior with the graphics renering process.
private func saveSnapshot<Content: View>(content: Content, size: CGSize) {
let pdf = content
.frame(width: size.width, height: size.height)
.padding(.bottom, .standardEdgeSpacing) // Not sure why this padding needs to be added but for some reason without it the pdf is tight to the last card on the bottom. It appears that extra padding is being added above the header, but not sure why.
.toPDF(format: self.pdfFormat)
self.saveReport(data: pdf)
}
private var pdfFormat: UIGraphicsPDFRendererFormat {
let format = UIGraphicsPDFRendererFormat()
// TODO: Add Author here
let metadata = [kCGPDFContextCreator: "Mike",
kCGPDFContextAuthor: viewModel.userService.userFullName]
format.documentInfo = metadata as Dictionary<String, Any>
return format
}
private func saveReport(data: Data?) {
guard let data = data else {
log.error("No data generated for pdf.")
return
}
do {
try viewModel.saveReport(data)
print("Report generated for inspection")
} catch {
print("Failed to create pdf report due to error: \(error)")
}
// Used to verify report in simulator
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
var filename = "\(UUID().uuidString.prefix(6)).pdf"
let bodyPath = documentDirectory.appendingPathComponent(filename)
do {
let value: Data? = viewModel.savedReport
try value?.write(to: bodyPath)
print("pdf directory: \(documentDirectory)")
} catch {
print("report not generated")
}
}
// MARK: Size Preference Key
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
}
// MARK: Get image from SwiftUI View
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
func toPDF(format: UIGraphicsPDFRendererFormat) -> Data {
let controller = UIHostingController(rootView: self)
let view = controller.view
let contentSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: contentSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsPDFRenderer(bounds: controller.view.bounds, format: format)
return renderer.pdfData { context in
context.beginPage()
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
I landed on using a size preference key because a lot of other options did not perform as expected. For instance, setting the content size in .onAppear ended up yielding a pdf with a lot of space above and below it. At one point I was creating the pdf using a boolean state that when changed would use the geometry reader's current geometry. That generally worked, but would provide unexpected results when the user would rotate the device while a modal was open.
References (as best I can recall):
SizePreferenceKey: https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/
Snapshot View extension: https://www.hackingwithswift.com/quick-start/swiftui/how-to-convert-a-swiftui-view-to-an-image

CoreData app takes too long to quit

My app may create / delete thousands of managed objects while running. I have used secondary NSManagedObjectContexts(MOCs) with NSPrivateQueueConcurrencyType and NSOperations to make the app more responsive and most parts work well. But when I pressed ⌘Q and if the number of unsaved objects are large, the app hangs for quite a while before the window closes (the beach ball keeps on rotating...).
How to make the window disappear immediately, before the save of the MOC?
I tried to insert window.close() in applicationShouldTerminate in the AppDelegate, but it has no effect.
My code for deletion is nothing special, except the hierachy is really large. Something like
let items = self.items as! Set<Item>
Group.removeItems(items)
for i in items {
self.managedObjectContext?.deleteObject(i)
}
Item is a hierarchic entity. Group has a one-to-many relationship to items.
The removeItems is generated by CoreData with #NSManaged.
Many thanks.
Updates
I tried the following code, the save still blocks the UI.
#IBAction func quit(sender: AnyObject) {
NSRunningApplication.currentApplication().hide()
NSApp.terminate(sender)
}
func applicationShouldTerminate(sender: NSApplication) -> NSApplicationTerminateReply
{
let op = NSBlockOperation { () -> Void in
do {
try self.managedObjectContext.save()
} catch {
print("error")
}
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
NSApp.replyToApplicationShouldTerminate(true)
})
}
op.start()
return .TerminateLater
}
This doesn't make the window close first, when the amount of created / deleted managed objects is large.
Then I changed to the following, as suggested by #bteapot. Still has no effect. The window still won't close immediately.
#IBAction func quit(sender: AnyObject) {
NSRunningApplication.currentApplication().hide()
NSApp.terminate(sender)
}
func applicationShouldTerminate(sender: NSApplication) -> NSApplicationTerminateReply {
let op = NSBlockOperation { () -> Void in
self.managedObjectContext.performBlock({ () -> Void in
do {
try self.managedObjectContext.save()
} catch {
print("errr")
}
})
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
NSApp.replyToApplicationShouldTerminate(true)
})
}
dispatch_async ( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
{() -> Void in
op.start()
})
return .TerminateLater
}
Finally I sort of solved the problem, though the UI is still blocked sometimes, even with the same test data.
The approach used can be found here: https://blog.codecentric.de/en/2014/11/concurrency-coredata/ , Core Data background context best practice , https://www.cocoanetics.com/2012/07/multi-context-coredata/
First I made a backgroundMOC with .PrivateQueueConcurrencyType
lazy var backgroundMOC : NSManagedObjectContext = {
let coordinator = self.persistentStoreCoordinator
let moc = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
moc.persistentStoreCoordinator = coordinator
moc.undoManager = nil
return moc
}()
Then made it prent of the original moc.
lazy var managedObjectContext: NSManagedObjectContext = {
var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
// managedObjectContext.persistentStoreCoordinator = coordinator
managedObjectContext.parentContext = self.backgroundMOC
managedObjectContext.undoManager = nil
return managedObjectContext
}()
Two methods for the save.
func saveBackgroundMOC() {
self.backgroundMOC.performBlock { () -> Void in
do {
try self.backgroundMOC.save()
NSApp.replyToApplicationShouldTerminate(true)
} catch {
print("save error: bg")
}
}
}
func saveMainMOC() {
self.managedObjectContext.performBlock { () -> Void in
do {
try self.managedObjectContext.save()
self.saveBackgroundMOC()
} catch {
print("save error")
}
}
}
Change the applicationShouldTerminate() to
func applicationShouldTerminate(sender: NSApplication) -> NSApplicationTerminateReply {
if !managedObjectContext.commitEditing() {
NSLog("\(NSStringFromClass(self.dynamicType)) unable to commit editing to terminate")
return .TerminateCancel
}
if !managedObjectContext.hasChanges {
return .TerminateNow
}
saveMainMOC()
return .TerminateLater
}
The reason it was so slow was I was using NSXMLStoreType instead of NSSQLiteStoreType.
Quitting an application might take a while since it will first empty the processes in queue.
Do you want immediate quit discarding everything in the Parent or children MOCs? But this will result in data loss.
If you have multi window application then, then close the window only but not quit the app.
Also thousands of entry should not take longer than 5 seconds to get processed and saved, if you have managed it properly. There could be some loopholes in your code, try to optimize using Instruments, CoreData profiler tool that would help you to understand the amount of time it is eating up.
To hide the window you can use the below, and in background all the coredata processing will happen, and once everything is done the app will terminate.
[self.window orderOut:nil];

Appending JsonFile (UrL) in array to display on application

I am trying to add my data from my Json file into the application.I want append the Authors name from the Json file into the empty array.
I have added all of the areas that needed to be added when i run the simulation i get an empty array. I need it display the Authors first name on the simulator.
Does anyone know what i need to do to my code to make it work?
My Code :
var AuthorGlobal = [String]()
class ViewController: UIViewController, UITextFieldDelegate{
#IBOutlet weak var DisplayAuthor: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
DisplayAuthor.text="\(AuthorGlobal)"
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated);
Alamofire.request(.GET, "http://178.62.83.50/newsletters.json")
.responseJSON { response in
// print(response.request) // original URL request
// print(response.response) // URL response
// print(response.data) // server data
// print(response.result) // result of response serialization
if let _ = response.result.value {
// print("JSON: \(JSON)")
}
let json = JSON(data: response.data!)
if let Author = json["NewsLetter"][0]["Author"].string {
AuthorGlobal.append(Author)
}
if let LastName = json["NewsLetter"][0]["LastName"].string {
print(LastName)
}
if let ArticleTitle = json["NewsLetter"][0]["ArticleTitle"].string {
//Now you got your value
print(ArticleTitle)
}
if let Author = json["NewsLetter"][1]["Author"].string {
//Now you got your value
print(Author)
}
if let LastName = json["NewsLetter"][1]["LastName"].string {
//Now you got your value
print(LastName)
}
if let ArticleTitle = json["NewsLetter"][1]["ArticleTitle"].string {
//Now you got your value
print ("Article Title: " + (ArticleTitle))
}
}
}
I just tried by putting your json in my file system and loading it locally. Below is my code on Swift 2 and it all worked fine. You might want to check the JSON data coming correctly in your service call. Also, try to compare it line by line with my code to see if you missed out something. Its too late for me to call it a day so bear with me to not pointing out the exact root cause in your code!
var AuthorGlobal = [String]()
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var DisplayAuthor: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// DisplayAuthor.text="\(AuthorGlobal)"
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated);
let filePath = NSBundle.mainBundle().pathForResource("1", ofType: "json")
var fileContents : String = ""
do {
fileContents = try String(contentsOfFile: filePath!, encoding: NSUTF8StringEncoding)
} catch (_) {
}
var json : Dictionary<String,Array<Dictionary<String,String>>> = Dictionary()
do {
json = try NSJSONSerialization.JSONObjectWithData(fileContents.dataUsingEncoding(NSUTF8StringEncoding)!, options:NSJSONReadingOptions.AllowFragments) as! Dictionary<String, Array<Dictionary<String, String>>>
} catch (_) {
}
let array = json["NewsLetter"] as Array?
if let author = array?[0]["Author"] {
AuthorGlobal.append(author)
}
print(AuthorGlobal) // This prints - ["Tom"]
if let LastName = array?[0]["LastName"] {
print(LastName) // This prints - Holton
}
if let ArticleTitle = array?[0]["ArticleTitle"] {
//Now you got your value
print(ArticleTitle) // This prints - XcodeGhost: Apple suffers a major malware infection inside the iOS app store.
}
if let Author = array?[1]["Author"] {
//Now you got your value
print(Author) // This prints - Sam
}
if let LastName = array?[1]["LastName"] {
//Now you got your value
print(LastName) // This prints - Devaney
}
if let ArticleTitle = array?[1]["ArticleTitle"] {
//Now you got your value
print ("Article Title: " + (ArticleTitle)) // This prints - Article Title: Google is 2 Billion Lines of Code
}
}
}

Why do my async calls still block me from interacting with UI elements in Swift?

My scenerio:
I asynchronously call a downloader object function to try to download a file.
Within the downloader's function, it asynchronously initiates a connection for downloading.
Even so, I cannot interact with my UI elements.
For example, I cannot edit my text field while download is in progress.
I want to be able to interact with my UI elements while downloading.
Where did I miss? What should I do? I truly appreciate your help!
ViewController's snippet for async download
//relevant UI elements I am referring to
#IBOutlet var renderButton: UIButton!
#IBOutlet var urlField: UITextField!
func handleOpenURL(url: NSURL){
var downloader: aDownloader = aDownloader(url: url)
//I try to put download to background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {() -> Void in
//This function initiates a connection call in an attempt to download the file
downloader.executeConnection({
(dataURL:NSURL!,error:NSError!) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.imageView.image = UIImage(data:NSData(contentsOfURL: url)!)
})
//rest are nonsense
}, progress: { (percentage:Double) -> Void in
//nonsense
}
)
})
}
aDownloader.swift 's snippet for relevant code
class aDownloader: NSObject, allOtherProtocols {
unowned var url: NSURL
//this block reports the progress as a % number
var progressBlock : ((Double)->Void)?
//this block will return either the destinationFileURL after download finishes,
//or return error of any sort
var dataBlock: ((NSURL!,NSError!)->Void)?
init(url: NSURL) {
self.url = url
}
func executeConnection(received:(NSURL!,NSError!)->Void, progress:(Double)->Void){
var request : NSURLRequest = NSURLRequest(URL: self.url,
cachePolicy: .ReloadIgnoringLocalCacheData,
timeoutInterval: 60.0)
//I attempt to async-ly download the file here
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { () -> Void in
var connectionManager : NSURLConnection? = NSURLConnection(request:request,
delegate: self,
startImmediately: false)
connectionManager?.scheduleInRunLoop(NSRunLoop.mainRunLoop(),
forMode: NSRunLoopCommonModes)
connectionManager?.start()
})
self.dataBlock = received
self.progressBlock = progress
}
//nonsense
}
Two things.
Most importantly, you are still configuring your configuration manager to run on the main loop:
connectionManager?.scheduleInRunLoop(NSRunLoop.mainRunLoop(),
forMode: NSRunLoopCommonModes)
You will probably find it easier to just use NSURLConnection's class method
sendAsynchronousRequest:queue:completionHandler: instead of manually starting the request in a background thread.
Also, it is a bad idea to update UI elements from a background thread:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.imageView.image = UIImage(data:NSData(contentsOfURL: url)!)
})
Instead, you should load the image into a temporary variable in the background and then perform the actual assignment back on the main thread.

How i get back to old scene in SpriteKit?

How can I go to old scene in SpriteKit ( in Swift ) ? I am in scene1 , i paused it and i move to scene 2 and I want to get back to scene1 and i to resume it . I don't want to recreate it again .
Thanks.
EDIT :
So , I tried this :
In secondScene class i put a oldScene SKScene property and a computed setOldScene property:
class SecondScene: SKScene {
var oldScene:SKScene?
var setOldScene:SKScene? {
get { return oldScene }
set { oldScene = newValue }
}
init(size: CGSize) {
super.init(size: size)
/* Setup my scene here */
}}
And then , in FirstScene class ( where i have ONE moving ball ) , in touchBegan method i have this
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if(self.view.scene.paused) {
self.view.scene.paused = false
}
else {
self.view.scene.paused = true
}
var secondScene = SecondScene(size: self.frame.size)
secondScene.setOldScene = self.scene
if !transition { self.view.presentScene(secondScene, transition: SKTransition.doorsOpenHorizontalWithDuration(2)) ; transition = true }
}
}
But , when i call self.view.presentScene(oldScene) in SecondScene class i move yo first class and i have TWO balls that moving :-??
I can't use class variable , Xcode not suport yet .
I solved this issue by having a public property on the second scene.
And then using that to present the first scene.
So in the First Scene, when you want to show the second scene....
if let scene = SKScene(fileNamed: "SecondScene") as! SecondScene?
{
scene.parentScene = self
self.view?.presentScene(scene)
}
This following code is in the SecondScene.
When ready to return to the first scene, just call the method showPreviousScene()...
public var parentScene: SKScene? = nil
private func showPreviousScene()
{
self.view?.presentScene(parentScene!)
}
You will have to keep a reference to your old scene around, and then when you want to move back to it, simply call
skView.presentScene(firstScene)
on your SKView instance.
There are several ways to keep a reference to your class around. You could use a Singleton, or simply a class with a class type property to store the reference and access it from anywhere.