I am working on an "uninstaller" for an macOS app we've had for several years now. The purpose for the uninstaller is to allow us to put a given system into a nascent state as if the original app had never been installed so that we can more reliably test the install process.
The original app has an extensive array of preferences stored in UserDefaults. In the original app there is a resetToDefaults() method which works just fine resetting all the defaults however for the uninstaller we'd wanted to remove the values completely. It looks to be straight-forward and this is what I came up with...
func flushPreferences() {
let defaults = getDefaultPreferences()
for preferenceName in defaults.keys.sorted() {
UserDefaults.standard.removeObject(forKey: preferenceName)
}
UserDefaults.standard.synchronize()
}
... which does not work at all.
I read in the documentation
Removing a default has no effect on the value returned by the objectForKey: method if the same key exists in a domain that precedes the standard application domain in the search list.
and I don't really understand what "domain" relates to and thought it might be app so tried the code as a test in the original app and that does nothing either.
Someone else suggested this, which also does nothing
let appDomain = Bundle.main.bundleIdentifier!
UserDefaults.standard.removePersistentDomain(forName: appDomain)
I also found some test code which works absolutely fine... which looks to be nearly identical to what I'm doing. I even tried using it with hard-coding one of our pref keys and that fails as well.
func testRemoveObject() {
let myKey:String = "myKey"
UserDefaults.standard.set(true, forKey: myKey)
let beforeVal = UserDefaults.standard.value(forKey: myKey)
print("before: \(beforeVal ?? "nil")")
UserDefaults.standard.removeObject(forKey: myKey) // Note: This is the only line needed, others are debugging
let afterVal = UserDefaults.standard.value(forKey: myKey)
print("after: \(afterVal ?? "nil")")
}
What am I missing? It looks like this one (based on what I've been able to find on the web) can be somewhat mysterious but I'm thinking it must be something obvious that I'm not seeing.
Well, thanks to red_menace's suggestion I found one article that led to another that suggested that the following command will reset the user preferences cache:
killall -u #USER cfprefsd
which seemed to work (yay) but upon further investigation it appears that simply closing the app is what updates the actual preference in the .plist and that changing it in the app will not show up until you exit.
This makes sense as it explains why you can create a preference on the fly save it, confirm it saved, delete it and confirm it deleted but cannot delete a previously saved preference — as similar to the persistent prefs perhaps the new preference is not added to the cache until the application exits.
This could also explain the various odd behaviors that other posters were finding (only worked every other time, had to do it asynchronously, etc.). As for NSUserDefaults.synchronize() has been depreciated and developer.apple.com indicates that it is unneeded and does nothing.
So one problem solved...
As it turns out my initial instinct was accurate as well and you cannot access prefs from one app in another using the removeObject(forKey: preferenceName)
// Will not work cross-application, though will work locally (inter-app)
UserDefaults.standard.removeObject(forKey: preferenceName)
To get it to work cross applications you have to use CFPreferencesSetAppValue(_ key:, value:, applicationID:) which is part of the "Preferences Utilities" section of the Core Library which requires that you know the appDomain of the initial app. So, the final solution is:
In the source app:
let appDomain = Bundle.main.bundleIdentifier! // Note, needed by uninstaller
will give you the domain for the stored preference in the source app.
And in the app doing the changing — the final working code:
func flushPreferences() {
let defaults = getDefaultPreferences()
let sourceAppDomain = "{THE_BUNDLE_ID_FROM_SOURCE_APP}"
for preferenceName in defaults.keys {
print("Preference name: \(preferenceName)")
CFPreferencesSetAppValue(preferenceName as CFString,
nil,
sourceAppDomain as CFString)
}
}
Hope this helps someone save some time at some point - thanks to everyone who contributed. This one was a BEAR!
Related
I have been browsing the web, even this website... bu cannot find a good option to implement ffmpeg functionality in an iOS application made in swift.
Options looked at and reasons why they are not solutions:
SwiftFFmpeg - I am not sure it can run on iOS, plus I don't see an option to run my desired Ffmpeg command.
MobileFFmpeg - Not maintained anymore, and a new project by the same founders created a "better altenrative" called ffmpeg-kit.
ffmpeg-kit - looks amazing, but their API only allows for interaction in the Objective-C language.
Any solutions anyone can give me?
First...
Make sure you understand what "packages" are available. You can build it yourself, but to be honest, unless you have a very specific reason, I'd just use the pre-build packages. See "8. Packages" of the ffmpeg-kit README.MD to see what's on offer
Second
Go to the Releases and find the version you're interested (I used FFmpegKit Native v4.5.1), scroll down and expand the "Assets" list for the release you're interested in.
For this experiment I used ffmpeg-kit-full-4.5.1-macos-xcframework.zip. If you're doing this in iOS, the workflow is basically the same, you just need to take into account that your file access is sandboxed (and yes, I've done this, and yes, it takes a long time to transcode video, compared to a desktop)
Third
Create a new Xcode project. Again, for this experiment, I created a "MacOS App" using "Storyboards" as the UI (you could try using SwiftUI, but that's another layer of complexity this example didn't need right now)
Unzip the *xcframework.zip file you download from the last step.
In Xcode, select the "project" node, select the MacOS target (🤞 there's only one target).
Select "General", drag the *.xcframework folders from Finder to the "Frameworks, Libraries and Embedded Content" section of your project
Forth
For this experiment, I opened the ViewController class (which was automatically created by Xcode) and simply added...
func syncCommand() {
guard let session = FFmpegKit.execute("-i file1.mp4 -c:v file2.mp4") else {
print("!! Failed to create session")
return
}
let returnCode = session.getReturnCode()
if ReturnCode.isSuccess(returnCode) {
} else if ReturnCode.isCancel(returnCode) {
} else {
print("Command failed with state \(FFmpegKitConfig.sessionState(toString: session.getState()) ?? "Unknown") and rc \(returnCode?.description ?? "Unknown").\(session.getFailStackTrace() ?? "Unknown")")
}
}
func asyncCommand() {
FFmpegKit.executeAsync("-i file1.mp4 -c:v file2.mp4") { session in
guard let session = session else {
print("!! Invalid session")
return
}
guard let returnCode = session.getReturnCode() else {
print("!! Invalid return code")
return
}
print("FFmpeg process exited with state \(FFmpegKitConfig.sessionState(toString: session.getState()) ?? "Unknown") and rc \(returnCode).\(session.getFailStackTrace() ?? "Unknown")")
} withLogCallback: { logs in
guard let logs = logs else { return }
// CALLED WHEN SESSION PRINTS LOGS
} withStatisticsCallback: { stats in
guard let stats = stats else { return }
// CALLED WHEN SESSION GENERATES STATISTICS
}
}
The code above is basically the "2. Execute synchronous FFmpeg commands." and "4. Execute asynchronous FFmpeg commands by providing session specific execute/log/session callbacks." examples from the ffmpeg-kit/apple documentation
!! Important !! - don't forget to add import ffmpegkit to the start of the file!
At this point, this should now compile (you'll get a couple of warnings about logs and stats not been used, you can ignore those).
After thoughts...
You should realise by now that the code I've provided won't actually run, for two reasons.
I've not actually called either func from anywhere (I tested it by placing it in the viewDidLoad func of the ViewController class)
The input file, used in the execute command, doesn't exist. You will need to provide an actual reference to an actual file, preferably with an absolute path. This, how ever, may require you to change the "App Sandbox" settings under the targets "Signing and Capabilities"
Xcodes auto code suggestions aren't bad and I mostly filled out the above using it, and the Obj-c code as a starting point.
Also, beware, SO is not a "tutorial" site, Xcode is a complex beast and you may need to spend some time exploring other resources to overcome issues you encounter
I am writing an application which stores data about other applications. I can find the current front most application and its information easily enough:
let frontApp = NSWorkspace.shared.frontmostApplication!
and from there I can get:
let currentAppName = currentApp.localizedName ?? "Dunno"
let currentAppIcon = currentApp.icon
If the application is not in the foreground, I know I can iterate through other running applications:
let apps = NSWorkspace.shared.runningApplications
for app in apps as [NSRunningApplication] {
print(app.localizedName ?? "whatever")
print(app.bundleIdentifier ?? "bundle")
}
I would like to get the same information about a application which is not currently running, but for which I do have the bundle identifier. How can I get this?
First, get the application's bundle using
let bundleURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: someBundleID)!
let bundle = Bundle(url: bundleURL)!
Once you have the bundle, you can get most of the things that you can get from a NSRunningApplication:
bundleIdentifier you already know.
bundleURL you just got with the code above.
executableArchitecture is now bundle.executableArchitectures, with a plural. Because it's not running, you get all the architectures that it can be run on, rather than the specific architecture that it is running on.
executableURL you can get with bundle.executableURL, no problems there.
launchDate, isFinishedLaunching, processIdentifier, ownsMenuBar are nonsensical to get, since the application is not running.
Now we are left with icon and localizedName. The methods I propose for getting these are less reliable. To get icon, do
NSWorkspace.shared.icon(forFile: bundleURL.path)
I'm sure this produces the same result as NSRunningApplication.icon at least 99% of the time, but I'm not sure if there are edge cases...
To get localizedName is more unreliable. There are 2 ways, neither of which are good. Choose the one that fits your purpose the best
FileManager.default.displayName(atPath: bundleURL.path) This gives you the localised name, but if the user renames the application, it will return the new name, rather than the bundle name
Getting CFBundleDisplayName from the bundle's localizedInfoDictionary, falling back on CFBundleName and infoDictionary. This for some reason doesn't give the localised name, even though I said localizedInfoDictionary.
let infoDict = (bundle.localizedInfoDictionary ?? bundle.infoDictionary)
let localizedName = infoDict?["CFBundleDisplayName"] ?? infoDict?["CFBundleName"]
In my application I allow the user to drag and drop items from Finder (or any other source of a file based URL) into my application. What I want to do is to add a mechanism that will allow me to test this in the Xcode UI testing.
I can see how to use XCUIElement.press(forDuration:thenDragTo:) to test the drag and drop of a source and destination within the application, but I have been unable to find a way to test when the source of the drag is outside of the application.
In a somewhat related test, I test the copy and paste portion of the application by setting the string I want to paste into NSPasteboard.general, then using XCUIElement.typeKey("v", modifierFlags: .command) to paste it into the desired element. That is a little less than ideal as it depends on Command-v actually being implemented as the paste command, but that is unlikely to change so it is acceptable for my needs. (In fact I've written an XCUIElement.paste(_ s: String) extension that makes it easy for me to add this in a test.)
I believe that drag and drop is also using an NSPasteboard for its communications, so with a little investigation into the underlying mechanism, I should be able to set my object into the correct pasteboard just like I do for the cut and paste. I'm reasonably certain I can figure that part out. But I haven't figured out how to perform the actual drop.
My goal would be to create an XCUIElement.drop(_ url) that would setup the proper "public.file-url" object into the correct pasteboard, and then simulate/perform the drop into the element.
Any ideas?
I should note that I have already tried the following two items:
First, I did use the Xcode record feature to attempt to record the drag and drop operation and see what events it would give me. Unfortunately, it records absolutely nothing.
Second, I do have a menu based alternative where the user selects a file via the file selector. So if I could simulate the file selection, that would be a suitable testing alternative for my purposes. Unfortunately, I didn't make any progress along that path either. When I used Xcode to record the events, it recorded the menu selection, nothing that was actually done in the dialog.
Based on your comments I would recommend you to read this article documentation piece
https://developer.apple.com/documentation/xctest/xcuiapplication
Notice the init(bundleIdentifier: String) and init(url: URL) methods. These allow you to interact with apps apart from the target application.
Then you can use XCUIElement.press(forDuration:thenDragTo:)
import XCTest
import XCTApps
import ScreenObject
let notes = XCTApps.notes.app
let photos = XCTApps.photos.app
class Tests: XCTestCase {
func testDragAndDrop() {
photos.launch()
notes.launch()
photos.images.lastMatch.press(forDuration: 1, thenDragTo: notes.textViews["Note Body Text View"])
}
}
P.S. In this example I use XCTApps because I don't want to remember or google bundle identifiers :D
https://github.com/rzakhar/XCTApps
Ok, so I haven't yet figured out the answer to my question (how to test a drag and drop), but I have come up with an acceptable workaround for my test.
Specifically, as I thought more about the pasteboard I realized that if I'm allowing the user to drag and drop a file into my application, then I should also be allowing them to cut and paste a file into the application.
Once I had that realization, then it was a reasonably simple process to test the necessary feature of my application by pasting a URL instead of dragging and dropping the URL. This has the added advantage that I can add the necessary test file to my testing package, keeping everything nicely self contained.
To this end I've added the following function to my XCUIElement extension:
extension XCUIElement {
func paste(url: URL) {
precondition(url.isFileURL, "This must be a file URL to match the pasteboard type.")
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(url.absoluteString, forType: .fileURL)
click()
typeKey("v", modifierFlags: .command)
}
}
Then in my test code I add the following to trigger the event:
let mainWindow = app.windows[/*...my main window name goes here...*/]
let testBundle = Bundle(for: type(of: self))
let fileURL = testBundle.url(forResource: "Resources/simple", withExtension: "json")
mainWindow.paste(url: fileURL!)
Granted, this doesn't actually test the drag and drop, but it does test the same portion of my code, since in my AppDelegate I have my onPaste action method calling the same underlying method as my performDrop method.
I will wait a couple of days to see if anyone comes up with an answer to the actual question (since I would still find that useful), but if no one does, I'll accept my own answer.
I have an app which is inconsistently returning 'nil' when using FileHandle to open a file for Read. I'm on OSX (10.13.4), XCode 9.4, Swift 4.1
This OSX app uses the NSOpenPanel() to get a list of files selected by the user. My 'model' class code opens these files to build a collection of data structures The code which does this starts out like this and successfully gets a FileHandle EVERY TIME for any file and is able to read data from the file.
private func getFITHeader(filename: String) {
let file: FileHandle? = FileHandle(forReadingAtPath: filename)
if file == nil {
print("FITFile >>> File open failed for file \(filename)")
}
else {
var databuffer: Data
databuffer = (file?.readData(ofLength: 80))!
:
:
}
The files also contain a block of binary data which I process in another part of the app. While I develop the code for this I'm temporarily hard coding one of the same filenames as works above for test purposes. BUT this code (below) ALWAYS throws an exception 'Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value' when it gets to fileHandle?.seek() - for some reason the attempt to create a FileHandle is always returning 'nil' despite the code being functionally identical to tha above.
#IBAction func btnProcFile(_ sender: Any) {
var data: Data
let filename = "/Users/johncneal/Dropbox/JN Astronomy/Astronomy/Spectroscopy/RSpec_Analyses/Gamma_Cas/20161203/Gamma Cas_065.fit"
let fileHandle: FileHandle? = FileHandle(forReadingAtPath: filename)
fileHandle?.seek(toFileOffset: 2880) //skip past the headers
let dataLenToRead = 1391 * 1039 * 2
data = (fileHandle?.readData(ofLength: dataLenToRead))!
:
:
}
The code in the second function works fine in a Playground (not attaching too much meaning to that) and, wierdly, has also worked when temporarily added to a different project. Probably also worth mentioning the length of the file path doesn't seem to matter - it behaves the same on short paths.
So the question is - why is this behaviour of FileHandle reliably inconsistent?
print()'ing the filenames presented to FileHandle() showed they were identical in each case (see below). So I'm stumped and frustrated by this - any perspectives or workarounds would be appreciated.
/Users/johncneal/Dropbox/JN Astronomy/Astronomy/Spectroscopy/RSpec_Analyses/Gamma_Cas/20161203/Gamma Cas_065.fit
/Users/johncneal/Dropbox/JN Astronomy/Astronomy/Spectroscopy/RSpec_Analyses/Gamma_Cas/20161203/Gamma Cas_065.fit
Found the answer - Sandboxing !!
Darren - coincidentally I did look at the URL based route and discovering it 'throws' put some proper error reporting in the catches. Low and behold they reported I didn't have permissions on the file (which initially surprised me since I'm obviously admin on my Mac's and all the files ar local and under my username.
I bit more research turned up. this article - https://forums.developer.apple.com/thread/96062 which quickly revealed its a sandboxing problem :-) Looks like recent versions of XCode have it turned on in 'Entitlements'. The post also points out that the NSOpenPanel FileOpen dialog returns 'Security scoped urls'. At first I thought this explained why the code in the first function worked but I'm not totally convinced because I was only feeding the url.path property to FileHandle.
However, turning off Sandbox in Entitlements makes everything work just fine. Yes, I know thats not the right thing to do longer term (or if I want this to go to the App Store) so I'll be checking out the right way to do this. At least I can get on now - thanks for the input.
The FileHandle initializers are not well named.
You should use FileHandle(forReadingFrom:URL) instead of FileHandle(forReadingAtPath:String). The former is newer API that throws an error instead of returning nil. You can use the thrown error to see why it is failing, and your variables are guaranteed to be non-nil.
For example:
#IBAction func btnProcFile(_ sender: Any) {
do {
let fileUrl = URL(fileURLWithPath:"/Users/johncneal/Dropbox/JN Astronomy/Astronomy/Spectroscopy/RSpec_Analyses/Gamma_Cas/20161203/Gamma Cas_065.fit")
let fileHandle = try FileHandle(forReadingFrom: fileUrl)
fileHandle.seek(toFileOffset: 2880) //skip past the headers
let dataLenToRead = 1391 * 1039 * 2
let data: Data = fileHandle.readData(ofLength: dataLenToRead)
// etc...
} catch let error as NSError {
print("FITFile >>> File open failed: \(error)")
NSApp.presentError(error)
}
}
So I have a simple goal, just to get this working. So theoretically if you ruined this.
Triggered save players
waited a bit
Deleted the app
Rebuilt (downloaded)
Triggered restore then the word "MEDO" would print to the console
But instead it is null, making me pretty sure that it is not saving to iC cloud some reason. I followed this tutorial. Perhaps it is too outdated to work?
func c_restoreCharecters()
{
let icloud = NSUbiquitousKeyValueStore.defaultStore()
println(icloud.objectForKey("username"))
}
func c_savePlayers()
{
let icloud = NSUbiquitousKeyValueStore.defaultStore()
icloud.setObject("MEDO", forKey: "username")
println(icloud.synchronize())
}
Obviously I am going to use this concept for something completely different in the future, but I have to get the basics down first!
Some other stuff:
iCloud set up in my settings:
My Entitlements file: