Create new UINSWindow from Catalyst - swift

In my Catalyst app I'm creating a second window using the following code:
let userActivity = NSUserActivity(activityType: "window2")
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: userActivity, options: nil) { error in
print("error: \(error)")
}
This creates a new window, but it's of type NSPopoverWindow and not UINSWindow as I expected.
I need to change the properties of the underlying NSWindow object and I have create a MacOS Bundle to interface AppKit as described in this fine blogpost:
Mac Catalyst: Interfacing Between UIKit and AppKit without Private APIs in Swift
So, is it possible to create a new UINSWindow from Catalyst, or do I need to create it with AppKit from my MacOS Bundle?

Looks like both a new NSPopoverWindow and a new UINSWindow are created when the new scene is activated, and both are added to the NSApplication.sharedApplication.windows array. As NSPopoverWindow doesn't have an uiWindows property, the code from the blogpost crashed.
Checking the object type solved my problem:
private func nsWindow(from window: UIWindow) -> AnyObject? {
guard let nsWindows = NSClassFromString("NSApplication")?.value(forKeyPath: "sharedApplication.windows") as? [AnyObject] else { return nil }
for nsWindow in nsWindows {
if !String(describing: nsWindow).contains("UINSWindow") {
continue
}
let uiWindows = nsWindow.value(forKeyPath: "uiWindows") as? [UIWindow] ?? []
if uiWindows.contains(window) {
return nsWindow
}
}
return nil
}

Related

XCode Preview Error: "Replaced Accessor for 'keyWindow'"

Adding the some code to one of my files in XCode causes the following error to show when trying to run previews for that file:
Compiling failed: replaced accessor for 'keyWindow' occurs in multiple places.
The error only happens when the following code is used in the file:
extension UIApplication {
var keyWindow: UIWindow? {
return UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first(where: { $0 is UIWindowScene })
.flatMap({ $0 as? UIWindowScene })?.windows
.first(where: \.isKeyWindow)
}
var keyWindowPresentedController: UIViewController? {
var viewController = self.keyWindow?.rootViewController
if let presentedController = viewController as? UITabBarController {
viewController = presentedController.selectedViewController
}
while let presentedController = viewController?.presentedViewController {
if let presentedController = presentedController as? UITabBarController {
viewController = presentedController.selectedViewController
} else {
viewController = presentedController
}
}
return viewController
}
}
I know very little about UIKit, and this code was copy/pasted from online. Why is this code crashing my preview, and how can I fix it?
I'm running XCode 13.4.1 on macOS Monterey
UIApplication already has keyWindow property and it seems there is already other extension with same property in workspace, so just use different name for your, like
extension UIApplication {
var currentWindow: UIWindow? {
*or find which one another is and see if you can reuse it.

implementation of NSMetadataQuery along with UIDocuments in swiftUI

I am trying to make a document based app in swiftUI with a custom UI. I want iCloud capabilities in my app. I am trying to use iCloud Document (No cloudKit) way for storing data on iCloud container. I am using UIDocument and it's working. It's storing data to iCloud and I am able to retrieve it back.
Now the thing is when I run the app on two devices (iphone and iPad) and make changes to a file on one device, the changes are not reflecting on the other device while the file or say app is open. I have to close the app and relaunch it to see the changes.
I know I have to implement NSMetadataQuery to achieve this but I am struggling with it. I don't know any objective-C. I have been searching on the internet for a good article but could not find any. Can you please tell how do I implement this feature in my app. I have attach the working code of UIDocument and my Model class.
Thank you in advance !
UIDocument
class NoteDocument: UIDocument {
var notes = [Note]()
override func load(fromContents contents: Any, ofType typeName: String?) throws {
if let contents = contents as? Data {
if let arr = try? PropertyListDecoder().decode([Note].self, from: contents) {
self.notes = arr
return
}
}
//if we get here, there was some kind of problem
throw NSError(domain: "NoDataDomain", code: -1, userInfo: nil)
}
override func contents(forType typeName: String) throws -> Any {
if let data = try? PropertyListEncoder().encode(self.notes) {
return data
}
//if we get here, there was some kind of problem
throw NSError(domain: "NoDataDomain", code: -2, userInfo: nil)
}
}
Model
class Model: ObservableObject {
var document: NoteDocument?
var documentURL: URL?
init() {
let fm = FileManager.default
let driveURL = fm.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
documentURL = driveURL?.appendingPathComponent("savefile.txt")
document = NoteDocument(fileURL: documentURL!)
}
func loadData(viewModel: ViewModel) {
let fm = FileManager.default
if fm.fileExists(atPath: (documentURL?.path)!) {
document?.open(completionHandler: { (success: Bool) -> Void in
if success {
viewModel.notes = self.document?.notes ?? [Note]()
print("File load successfull")
} else {
print("File load failed")
}
})
} else {
document?.save(to: documentURL!, for: .forCreating, completionHandler: { (success: Bool) -> Void in
if success {
print("File create successfull")
} else {
print("File create failed")
}
})
}
}
func saveData(_ notes: [Note]) {
document!.notes = notes
document?.save(to: documentURL!, for: .forOverwriting, completionHandler: { (success: Bool) -> Void in
if success {
print("File save successfull")
} else {
print("File save failed")
}
})
}
func autoSave(_ notes: [Note]) {
document!.notes = notes
document?.updateChangeCount(.done)
}
}
Note
class Note: Identifiable, Codable {
var id = UUID()
var title = ""
var text = ""
}
This is a complex topic. Apple do provide some sample swift code, the Document-Based App Programming Guide for iOS and iCloud Design Guide.
There is also some good third party guidance: Mastering the iCloud Document Store.
I would recommend reading the above, and then return to the NSMetaDataQuery API. NSMetaDataQuery has an initial gathering phase and a live-update phase. The later phase can remain in operation for the lifetime of your app, allowing you to be notified of new documents in your app's iCloud container.

Document based app using one single window

I have a document-based macOS application, which is a basic text editor. The default behavior is to create a new window for every opened document. But I want to only have one window displayed at a time and when opening a document or creating a new one, this should happen in the same window and thus replace the old document.
I've tried to do some hacking in the makeWindowControllers method from NSDocument by not instantiating a new window controller but reusing an old one. But after some problems with this approach, I figured this is not the right way to go. I was wondering if there is a common approach to this problem.
This is the code I've tried
class Document: NSDocument {
static var sharedWindow: NSWindowController?
override func makeWindowControllers() {
// Instaniate window controller there is none
if Document.sharedWindow == nil {
// Returns the Storyboard that contains your Document window.
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
Document.sharedWindow = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as? NSWindowController
}
guard let sharedWindow = Document.sharedWindow else { return }
if let previousDocument = sharedWindow.document as? NSDocument {
previousDocument.close()
}
self.addWindowController(sharedWindow)
sharedWindow.contentViewController?.representedObject = content
(sharedWindow.contentViewController as? ViewController)?.handleOpenDocumentOperation()
}
...
}

Load a spritekit scene from another bundle?

I am making a SpriteKit framework using swift 4.2 and want to include some .sks files for scenes and actions. I have tried to load the scene from the bundle using the code below:
class func newGameScene() -> GameScene {
guard let gameScenePath = Bundle(for: self).path(forResource: "GameScene", ofType: "sks") else { assert(false) }
guard let gameSceneData = FileManager.default.contents(atPath: gameScenePath) else { assert(false) }
let gameSceneCoder = NSKeyedUnarchiver(forReadingWith: gameSceneData)
guard let scene = GameScene(coder: gameSceneCoder) else { assert(false) }
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
return scene
}
I load the scene and present it. (This code is mostly from Apple's template for SpriteKit as Im testing this issue.)
guard let view = view else {
return nil
}
let scene = GameScene.newGameScene()
view.presentScene(scene)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
return nil
The GameScene.sks and the code is unchanged from Apples template in this case. This code and the .sks assets are in the dynamic framework and imported into another project.
When having the framework load the scene into a view I pass it, it shows the fps and node count but not the "Hello, World!" text.
In the code below, also copied from the template, a break point shows that these are not called when mousing down.
#if os(OSX)
// Mouse-based event handling
extension GameScene {
override func mouseDown(with event: NSEvent) {
if let label = self.label {
label.run(SKAction.init(named: "Pulse")!, withKey: "fadeInOut")
}
self.makeSpinny(at: event.location(in: self), color: SKColor.green)
}
override func mouseDragged(with event: NSEvent) {
self.makeSpinny(at: event.location(in: self), color: SKColor.blue)
}
override func mouseUp(with event: NSEvent) {
self.makeSpinny(at: event.location(in: self), color: SKColor.red)
}
}
#endif
I know it must have to do with how SpritKit loads the scene but cannot find a solution. I have to use an NSKeyedUnarchiver becuase SpritKit's built in file initializer:
GameScene(fileNamed: "GameScene")
Only loads from the Main Bundle.
Now in the above I assumed that the file can be loaded by using a coder but Tomato made the point that sks most likely was not saved using a coder. In that case, It may be impossible to load an sks file from another bundle in sprite-kit using the provided api from apple. The answer may not include coders.
I have compiled the above discussion/solution into a single extension function on SKScene. Hope this helps someone!
import SpriteKit
extension SKScene {
static func fromBundle(fileName: String, bundle: Bundle?) -> SKScene? {
guard let bundle = bundle else { return nil }
guard let path = bundle.path(forResource: fileName, ofType: "sks") else { return nil }
if let data = FileManager.default.contents(atPath: path) {
return NSKeyedUnarchiver.unarchiveObject(with: data) as? SKScene
}
return nil
}
}
Just as I thought let gameSceneCoder = NSKeyedUnarchiver(forReadingWith: gameSceneData) was not creating a proper coder for you.
Just do
guard let scene = NSKeyedUnarchiver.unarchiveObject(with: gameSceneData) as? SKScene
else{
assert(false)
}
This will unarchive the file properly for you.
Note, if you want to use GameScene, make sure GameScene is set in the custom class of the SKS file

When saving to CoreData from Today Extension, Data only accesable from Widget - NOT from Main Application

I'm writing an Application for iOS with Swift 3.
In my Main iOS App I'm using an NSFetchedResultsController to show saved items as an TableView.
It's (of course) possible to add new items from another ViewController.
-> Thats all working really awesome.
So I thought it would be great if I could add an new item really fast from an TodayWidget.
What I did:
Created an SharedCode Framework and added AppGroup to my Main App and the Today Widget.
Moved my CoreDataStack.swift Class, the .xcdatamodeled and my Item+CoreDataClass.swift and Item+CoreDataProperties.swift files to my SharedCode Framework.
Sublcassed NSPersistentContainer to addforSecurityApplicationGroupIdentifier for my appGroupID
Rewrote my CoreData code in my ViewController to use the created CoreDataStack.shared.managedContext
Testing. AWESOME. My NSFetchedResultsController is working, and adding new Items works as expected. Nice. -> moveing on.
In my Today Widget I'm using an simple NSFetchRequest to get the last entered Item from CoreData. Works perfectly!
Added Buttons to modify the data and saving it to CoreData. Here I'm also using CoreDataStack.shared.managedContext and CoreDataStack.shared.save()
Automatically reloading my Data AND?! AWESOME. All working very nice. Data is saved and new data is shown in the Today Extension. Also when I count the results from my NSFetchRequest the number of Items is increased.
NOW TO MY PROBLEM:
All the data that I'm adding through the Extension is not showing in my main iOS App.
There when I'm fetching the Items from CoreData there are only the ones showing that I created in the main app. -.-
I have no Idea whats going wrong.
It's like I have two different CoreDataStores.
Also after I once added an Item trough the Widget - the Widget does not fetch an Item from the main App. Only the last one entered from the Widget.
- Here is my Code:
CoreDataStack.swift
public class CoreDataStack {
public static let shared = CoreDataStack()
public var errorHandler: (Error) -> Void = {_ in }
//#1
lazy var persistentContainer: PersistentContainer = {
let container = PersistentContainer(name: ServiceConsts.modelName)
var persistentStoreDescriptions: NSPersistentStoreDescription
let storeUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: ServiceConsts.appGroupID)!.appendingPathComponent("\(ServiceConsts.modelName).sqlite")
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
description.url = storeUrl
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: ServiceConsts.appGroupID)!.appendingPathComponent("\(ServiceConsts.modelName).sqlite"))]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
//#2
public lazy var managedContext: NSManagedObjectContext = {
return self.persistentContainer.viewContext
}()
//#3
// Optional
public lazy var backgroundContext: NSManagedObjectContext = {
return self.persistentContainer.newBackgroundContext()
}()
//#4
public func performForegroundTask(_ block: #escaping (NSManagedObjectContext) -> Void) {
self.managedContext.perform {
block(self.managedContext)
}
}
//#5
public func performBackgroundTask(_ block: #escaping (NSManagedObjectContext) -> Void) {
self.persistentContainer.performBackgroundTask(block)
}
//#6
public func saveContext () {
guard managedContext.hasChanges else { return }
do {
try managedContext.save()
} catch let error as NSError {
print("Unresolved error \(error), \(error.userInfo)")
}
}
}
PersistentContainer.swift
class PersistentContainer: NSPersistentContainer {
internal override class func defaultDirectoryURL() -> URL {
var url = super.defaultDirectoryURL()
if let newURL =
FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: ServiceConsts.appGroupID) {
url = newURL
}
return url
}
}
Can anyone help?
I have really no idea what I'm doing wrong.
Would be so awesome if anyone can give me a tip :)
Thanks <3
I finally fixed my issue <3.
It was so simple.
After hours and hours of testing testing crying and putting an axed through my MacBook ;) I found the thing thats what close killing me.
I testet if a simple Fetch Request in the Main App would get the added Items from the Today Extension. And that was working.
And than I saw it. In my Main App.
The NSFetchedResultController. I used an cache.
So now I notify the Main App when a new Weight was added via the widget - and than I call my refresh function.
func handleRefresh(_ refreshControl: UIRefreshControl) {
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: "weightCache")
do {
try fetchedResultsController.performFetch()
setupView()
self.tableView.reloadData()
} catch let error as NSError {
print("Fetching error: \(error), \(error.userInfo)")
}
refreshControl.endRefreshing()
}
So simple.
Just delete the cache.