Figured it out. I used a modified version of this NSImage+TintColor extension to create a swatch dictionary:
let namedColors:[String] = [
"aquamarine",
"black",
"blue",
...
}
let myColorSwatches:[String:NSImage] = { (colorNames:[String]) -> [String:NSImage] in
var toReturn:[String:NSImage] = [:]
for colorName in colorNames {
let image = NSImage(systemSymbolName: "rectangle.fill", accessibilityDescription: nil)!
image.isTemplate = false
image.lockFocus()
NSColor(named: colorName)!.set()
let imageRect = NSRect(origin: .zero, size: image.size)
imageRect.fill(using: .sourceIn)
image.unlockFocus()
toReturn[colorName] = image
}
return toReturn
}(namedColors)
... which I can then reference in my SwiftUI Picker:
Picker("Color:", selection: pickerColor) {
ForEach(namedColors, id: \.self) { colorName in
HStack {
Image(nsImage: myColorSwatches[colorName]!)
Text(colorName)
}
}
}
And just as a reminder in case anybody else wants to do this, I have a color set defined in my Assets.xcassets for each named color.
Post
Replies
Boosts
Views
Activity
Figured it out. My supercategories were defined as a struct with two properties: name and frc. While reading over some forum hits for the umpteenth time, this one - https://developer.apple.com/forums/thread/68477 finally clicked. Switched them over to use a class instead, and now everything is happy. From:
Swift
struct sidebarCategoryItem {
let name:String
let frc:NSFetchedResultsControllerCPMObject_CD
}
to this instead:
Swift
class sidebarCategoryItem {
let name:String
let frc:NSFetchedResultsControllerCPMObject_CD
init(name initName:String, frc initFRC:NSFetchedResultsControllerCPMObject_CD) {
self.name = initName
self.frc = initFRC
}
}
+5 lines of code, and -a million lines added for troubleshooting.
Okay, this is definitely something weird about the parent object. I tried this:
Swift
let parent = myOutlineView.child(topLevelSectionIndex!, ofItem: nil)!
print("parent: \(String(describing: parent))")
print("parent row: \(myOutlineView.row(forItem: parent))")
let row2Parent = myOutlineView.item(atRow: 2)!
print("row2Parent: \(String(describing:row2Parent))")
print("row2Parent row: \(myOutlineView.row(forItem: row2Parent))")
and I get this in my log:
text
parent: supercategory
parent row: -1
row2Parent: supercategory
row2Parent row: 2
The String(describing:) of both objects is identical, though the objects are clearly different internally. And if I tell insertItems to insert something into row2Parent, it works! The problem is I only know the row indexes when my application has no data downloaded. As I fetch stuff and as the user expands and collapses sections, the rows for the supercategories will change. I clearly can't get it just by the row number, and it isn't telling me the row number for the object I got from its child(_:ofItem:) method.
I've tried getting the object directly from the NSOutlineViewDataSource method outlineView(_:child:ofItem:). No change. Debug descriptions of the objects are identical, but it still shows row -1.
Tried getting the object directly from the array I use for the supercategories. No change.
Does anybody know what sources insertItems(_:at:inParent:) accepts for the object in the inParent?
Realized a bit ago I forgot to include the code which sets topLevelSectionIndex. It was in a block of debug prints which I trimmed from the code above:
Swift
let topLevelSectionIndex:Int? = {
for (index, section) in objectSections.enumerated() {
if controller == section.frc { return index }
}
return nil
}()
let myOutlineView:NSOutlineView = myScrollView.documentView! as! NSOutlineView
switch type {
...
I tried adding this code to my case .insert: just above the insertItems:
Swift
print("Parent is expandable? \(myOutlineView.isExpandable(parent))")
print("Parent is expanded? \(myOutlineView.isItemExpanded(parent))")
myOutlineView.expandItem(parent)
print("Parent is expanded? \(myOutlineView.isItemExpanded(parent))")
and it gave me this in my logs:
text
Parent is expandable? true
Parent is expanded? false
Parent is expanded? false
I get the same results whether the supercategory is expanded or collapsed before I download my data and my code is called. This makes me think the parent object I'm getting isn't the parent object the NSOutlineView wants me to use.
Further update. I tried simply ignoring the NSItemProvider objects and instead finding the drag pasteboard and reading the objects from it directly. This feels really awkward, but it works. My drag source is the same as my original post. Here is the working drop target:
.onDrop(of: ["public.text"], isTargeted: nil) { _ in
mainViewLogger.trace("Entered drop handler block.")
let dragBoard = NSPasteboard(name: .drag)
guard let draggedItems = dragBoard.readObjects(forClasses: [NSString.self], options: nil) else { return false }
print("Pulled \(draggedItems.count) items from the drag pasteboard: \(String(describing: draggedItems))")
return false
}
This code obviously doesn't do anything with the dropped data, and it always reports an unsuccessful drop, but the draggedItems list does actually contain the dragged objects.
I realized this morning I had not tried dragging from TextEdit into the SwiftUI view. Again, the green orb with the + shows up, and again when I drop the text, I get a provider with no registered types.
If I change the NSOutlineDataSource method to this:
func outlineView(
_ outlineView: NSOutlineView,
pasteboardWriterForItem item: Any)> NSPasteboardWriting?
{
sidebarControllerLogger.trace("Drag operation started.")
guard let workingItem = item as? MyEntity
else {
sidebarControllerLogger.debug("Dragging something which isn't an object! Description: \(String(describing: item), privacy: .public)")
return nil
}
let provider = NSItemProvider(object: workingItem.name! as NSString)
print("Provider created for types \(String(describing:provider.registeredTypeIdentifiers))")
return workingItem.name! as NSString
}
then I get this in my logs when I start a drag:
Provider created for types ["public.utf8-plain-text"]
This seems to me to reinforce the idea that "public.text" is an appropriate UTType for my test drop target code. The provider still doesn't offer it, though. If I try to return the provider I made above, I get a build-time error: "Type of expression is ambiguous without more context".
I get the same warnings, and I'm building my toolbar items programatically. I don't set the maxSize or minSize values in my code at all, and it still complains at me. I think it's probably an issue with default arguments when building an NSToolbarItem.
I accidentally marked my reply as the solution, when I meant to mark Quinn's. Now I don't see a way to undo that. Weird.
That got me where I needed to be. Thank you!
To save others time, here's what I ended up writing:
func addTrustForCertificate(_ serverCertificate:SecCertificate, host:String) {
let serverCertDictionary:CFDictionary = [
kSecClass:kSecClassCertificate,
kSecValueRef:serverCertificate
] as [CFString:Any] as CFDictionary
let secItemAddError:OSStatus = SecItemAdd(serverCertDictionary, nil)
switch secItemAddError {
case noErr:
break
default:
let errorMessage:String = SecCopyErrorMessageString(secItemAddError,nil)
print("addTrustForCertificate SecItemAdd error: \(String(describing: errorMessage))")
}
let secPolicyToSet:SecPolicy = SecPolicyCreateSSL(true, nil)
let SecTrustDict1:CFDictionary = [
"kSecTrustSettingsAllowedError":CSSMERR_TP_CERT_EXPIRED,
"kSecTrustSettingsPolicy":secPolicyToSet,
"kSecTrustSettingsPolicyName":"sslServer",
"kSecTrustSettingsPolicyString":host,
"kSecTrustSettingsResult":1
] as CFDictionary
let SecTrustDict2:CFDictionary = [
"kSecTrustSettingsAllowedError":-2147408896,
"kSecTrustSettingsPolicy":secPolicyToSet,
"kSecTrustSettingsPolicyName":"sslServer",
"kSecTrustSettingsPolicyString":host,
"kSecTrustSettingsResult":1
] as CFDictionary
let trustSettings:CFArray = [SecTrustDict1,SecTrustDict2] as CFArray
let trustSettingsError:OSStatus = SecTrustSettingsSetTrustSettings(serverCertificate, .user, trustSettings)
switch trustSettingsError {
case noErr:
break
default:
let errorMessage:String = SecCopyErrorMessageString(trustSettingsError,nil)
print("addTrustForCertificate SecTrustSettingsSetTrustSettings error: \(String(describing: errorMessage))")
}
}
Lines 2 through 13 import the certificate into the Keychain. Lines 15 through 38 set the trust overrides. Explicit types because I don't mess around with type inference in even vaguely-security-related code.
CSSMERR\_TP\_CERT\_EXPIRED is error -2147409654 (cssmerr.h). I'm not sure what error -2147408896 is. Haven't found it in an error table yet. I got the two values by examining some existing self-signed certificates I trusted in Safari.
While adding some tests, I realized I've been overcomplicating this. Specifically, urlSession(session: didReceive challenge: completionHandler:) gets called for all certificate validation, even for certificates which are trusted (I hadn't tried with trusted certs before). Writing my own is not the right way to handle certificate validation for my application. If you don't set a custom delegate, the application will trust all the certificates Safari trusts.
If the certificate isn't trusted yet, you will get a series of errors like this:
Connection 1: default TLS Trust evaluation failed(-9807)
Connection 1: TLS Trust encountered error 3:-9807
Connection 1: encountered error(3:-9807)
Connection 1: unable to determine interface type without an established connection
Task <F4695366-AE0C-41D9-A3AA-CEA0682E6413>.<1> HTTP load failed, 0/0 bytes (error code: -1202 [3:-9807])
Task <F4695366-AE0C-41D9-A3AA-CEA0682E6413>.<1> finished with error [-1202] Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “<destination>” which could put your confidential information at risk." UserInfo={NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, \_kCFStreamErrorDomainKey=3, NSErrorPeerCertificateChainKey=(
"<cert(0x7f91cd0a5600) s: <cert name> i: <cert name>>"
), NSErrorClientCertificateStateKey=0, NSErrorFailingURLKey=https://<destination>/<path>, NSErrorFailingURLStringKey=https://<destination>/<path>, NSUnderlyingError=0x600003d7eb50 {Error Domain=kCFErrorDomainCFNetwork Code=-1202 "(null)" UserInfo={\_kCFStreamPropertySSLClientCertificateState=0, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x6000001e4510>, \_kCFNetworkCFStreamSSLErrorOriginalValue=-9807, \_kCFStreamErrorDomainKey=3, \_kCFStreamErrorCodeKey=-9807, kCFStreamPropertySSLPeerCertificates=(
"<cert(0x7f91cd0a5600) s: <cert name> i: <cert name>>"
)}}, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <F4695366-AE0C-41D9-A3AA-CEA0682E6413>.<1>"
), \_kCFStreamErrorCodeKey=-9807, \_NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <F4695366-AE0C-41D9-A3AA-CEA0682E6413>.<1>, NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x6000001e4510>, NSLocalizedDescription=The certificate for this server is invalid. You might be connecting to a server that is pretending to be “<destination>” which could put your confidential information at risk.} The error can then be handled separately. In my case, I'm initializing an object to represent the connection to this remote server, so if the connection doesn't work, I just throw the error back to the code trying to create this object. I use the error to create an NSAlert, which I show as a sheet with a button to cancel, and a button to trust the certificate. If the user picks the button to trust the certificate, the sheet completion handler then adds it to the keychain:
case .failure(let error as NSError):
switch (error.domain,error.code) {
case (NSURLErrorDomain,-1202):
let errorSheet = NSAlert(error: error)
errorSheet.addButton(withTitle: "Cancel Connection")
errorSheet.addButton(withTitle: "Trust this Certificate")
errorSheet.beginSheetModal(for: self.myWindow!)
{ (response:NSApplication.ModalResponse) -> Void in
switch response{
case .alertFirstButtonReturn:
break
case .alertSecondButtonReturn:
let serverCertificate = (error.userInfo["NSErrorPeerCertificateChainKey"] as! [SecCertificate])[0]
let serverCertItemDictionary = [
kSecClass:kSecClassCertificate,
kSecValueRef:serverCertificate,
kSecReturnRef:true,
kSecReturnAttributes:true
] as [CFString : Any]
let serverCertDictionaryCF = serverCertItemDictionary as CFDictionary
var secItemAddReturn:CFTypeRef?
let secItemAddError = SecItemAdd(serverCertDictionaryCF, &secItemAddReturn)
switch secItemAddError {
case noErr:
break
case errSecDuplicateItem:
print("This server's certificate is already in the Keychain.")
default:
let errorString = SecCopyErrorMessageString(secItemAddError,nil)
print("SecItemAdd error: \(String(describing: errorString))")
}
default:
break
}
}
default:
let errorSheet = NSAlert(error: error)
errorSheet.beginSheetModal(for: self.myWindow!)
}
Extending the switch statements to handle other errors is relatively easy.
Thought of another possible option. If I use NSErrorMergePolicy, my call to save throws an error which contains a list of NSConstraintConflict objects. The NSConstraintConflict contains a list of the conflicting objects. I'm not sure what to do with these NSManagedObject instances, though. Do I pick one per NSConstraintConflict, delete the others, and call save again? That seems like an awkward thing to do while handling exceptions thrown by a call to save.
There is no API to display the contents of a certificate to a user, this could be something that you build yourself once the contents of the certificate is parsed. That's what I figured. Seems like a relatively uncommon need.
Keep in mind though that this delegate is operating during a time sensitive context. If the user takes too long to make a decision the request does run the risk of timing out. Makes sense. I'll have to experiment to find the best way to handle the timing.
Thanks for the help and the example code!
I knew about multi-column constraints, but I was thinking in terms of codable structs containing only the properties handed to me by the server. Now that you've mentioned namespaces, it seems obvious the solution is to just add my own property in the initializer which specifies which server the object is from, and use that in combination with the UUID to ensure uniqueness.
Thanks for the help!
Forgot to add: I don't think NSPersistentDocument is directly relevant, as the data doesn't have any significance without the server. For example, the server does not let you build a new object with a UUID you specify, it assigns a UUID to new objects. There is no reason for anything to touch the document except when connected to the server. No reason to copy them from workstation to workstation, or to back them up.
Right now, I'm only interested in the ability to cache data so I don't have to download everything on every launch. It seems to me the document stuff is just unnecessary overhead, but maybe I'm missing something. Maybe the best way to do this is persistent documents automatically named and saved in ~/Library? But that seems like what multiple persistent containers would do.
That is what led me to ask here if multiple separate persistent containers might work.
That's a shame.So it sounds like my best bet at this point is toSet my Core Data entities to manual/none and create the subclass filesCombine the class and extension (as 'init' doesn't seem to like being in extensions, and Xcode won't let a method in the class refer to the attributes in the extension)Add a CodingKeys enum and 'required convenience public init(from decoder:)' methodUse JSONDecoder to decode the JSON to an array of entitiesIssue a context.save() after each array is decodedAlternatively, I could make some kind of translation layer for the JSON to spot dashes in keys and replace the dash and the next letter with the letter uppercased. Could even be reversible, as long as the server's schema doesn't allow uppercase letters in keys (this one doesn't).Both options sound interesting, and like good things to learn.