I'm really unfamiliar with macOS development and are trying to figure out the right way to do this. Scenario: My application works with encrypted documents. Those are cross-platform, so I can't change the encryption mechanism (to e.g., use something provided by the OS directly). I also want to create an iOS app later, and share as much code as possible.
The flow is intended to be this:
So I have these classes:
This is all contained in a single main.storyboard (thinking about splitting, but first want to figure out correct architecture):
I have implemented read(from data: Data, ofType typeName: String)
in MyEncryptedDocument
, to just read the content as a byte array. Now, here's where I would display the password prompt, but it seems that the NSDocument class isn't the right place for that - for starters, I don't have a WindowController, and windowControllers
is empty (I assume that makeWindowControllers gets called afterwards).
I've been thinking of subclassing either NSWindowController or NSWindow, but then I wonder where the proper place would be for the password prompt? awakeFromNib
in the WindowController doesn't have the Document yet, though I could assign it via makeWindowControllers
.
This leaves me with these questions:
MyEncryptedDocument
actually deal with just the binary, encrypted data? Or should it handle the password and decrypted business object?I'm OK with either Swift or Objective-C since I care more about the "Where" and less about the exact "How".
Here's how I've implemented it now:
makeUntitledDocumentOfType:error:
and makeDocumentWithContentsOfURL:ofType:error:
readFromData:ofType:error:
and dataOfType:error:
to load/decrypt and save/encrypt the dataThe DocumentController really seems to be the place that should deal with this in my opinion, since the password/encryption is more a pipeline-concern than a concern of the actual document or any of the UI. Overall, this "feels" right to me as an inexperienced macOS dev. I'm not sure if NSAlert is the correct class for the dialog; looking at Apple's Guidelines I think I should create my own NSPanel or NSWindow. But that is a concern for later.
In Xamarin C# code, the class looks like this:
public class MyEncryptedDocumentController : NSDocumentController
{
public MyEncryptedDocumentController()
{
}
// makeUntitledDocumentOfType:error:
public override NSObject MakeUntitledDocument(string typeName, out NSError error)
{
return LoadOrCreateDocument(typeName, null, out error);
}
// makeDocumentWithContentsOfURL:ofType:error:
public override NSObject MakeDocument(NSUrl url, string typeName, out NSError outError)
{
return LoadOrCreateDocument(typeName, url, out outError);
}
private MyEncryptedDocument LoadOrCreateDocument(string typeName, NSUrl url, out NSError error)
{
error = null;
using (var sb = NSStoryboard.FromName("PasswordView", null))
using (var ctrl = sb.InstantiateControllerWithIdentifier("Password View Controller") as PasswordViewController)
using (var win = new NSAlert())
{
win.MessageText = "Please enter the Password:";
//win.InformativeText = "Error message goes here.";
win.AlertStyle = NSAlertStyle.Informational;
win.AccessoryView = ctrl.View;
var btnOK = win.AddButton("OK");
var btnCancel = win.AddButton("Cancel");
var res = win.RunModal();
var pw = ctrl.Password;
if (res == (int)NSAlertButtonReturn.First)
{
var doc = new MyEncryptedDocument(pw);
if (url != null)
{
if (!doc.ReadFromUrl(url, typeName, out error))
{
// Could check if error is a custom "Wrong Password"
// and then re-open the Alert, setting the Informational Text
// to something like "wrong password"
return null;
}
}
return doc;
}
// MyEncryptedDocument.Domain is a NSString("com.mycompany.myapplication");
// MyErrorCodes is just a custom c# enum
error = new NSError(MyEncryptedDocument.Domain, (int)MyErrorCodes.PasswordDialogCancel);
return null;
}
}
}
PasswordViewController
is a pretty simple subclass of NSViewController:
public partial class PasswordViewController : NSViewController
{
public string Password { get => tbPassphrase?.StringValue ?? ""; }
public PasswordViewController(IntPtr handle) : base(handle)
{
}
}
tbPassphrase
is the outlet for the text box in the view (@synthesize tbPassphrase = _tbPassphrase;
in the .h file). The storyboard is simple scene with a viewController:
<viewController storyboardIdentifier="Password View Controller" id="5LL-3u-LyJ" customClass="PasswordViewController" sceneMemberID="viewController">
<view key="view" id="yoi-7p-9v6">
<rect key="frame" x="0.0" y="0.0" width="315" height="22"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<secureTextField identifier="tfPassphrase" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YmM-nK-9Hb">
<rect key="frame" x="0.0" y="0.0" width="315" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="ChX-i5-luo">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<allowedInputSourceLocales>
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
</allowedInputSourceLocales>
</secureTextFieldCell>
</secureTextField>
</subviews>
</view>
<connections>
<outlet property="tbPassphrase" destination="YmM-nK-9Hb" id="sCC-Ve-8FO"/>
</connections>
</viewController>