Michael Stum
Michael Stum

Reputation: 181044

I want to require a password when opening/creating a NSDocument. Where to put the prompt?

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:

  1. Either "Open" or "New" a new Document
  2. Prompt the user for a password
  3. (If opening a Document, verify that the password is good, otherwise repeat step 2 until good or cancelled)
  4. Display the Document Window

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):

main.storyboard structure

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:

I'm OK with either Swift or Objective-C since I care more about the "Where" and less about the exact "How".

Upvotes: 1

Views: 223

Answers (1)

Michael Stum
Michael Stum

Reputation: 181044

Here's how I've implemented it now:

  • Create a Subclass of NSDocumentController
  • In the AppDelegate, instantiate that class - that is enough to set it as the DocumentController for the application (there can be only one)
  • In the subclass, set up handlers for makeUntitledDocumentOfType:error: and makeDocumentWithContentsOfURL:ofType:error:
  • There, I can now create the Dialog to ask for the password and either create the (decrypted) document, or return an error.
  • MyEncryptedDocument (subclass of NSDocument) requires a password in its init/constructor. This is used in the overridden readFromData:ofType:error: and dataOfType:error: to load/decrypt and save/encrypt the data

The 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>

Upvotes: 0

Related Questions