TextExpander / TextExpanderTouchSDK

TextExpander touch SDK
85 stars 13 forks source link

TextExpander touch SDK

(Release notes are found at the bottom of this document.)

Smile provides the TextExpander framework so that you can include TextExpander functionality in your iOS app, custom keyboard, or extension, subject to the License Agreement below.

TextExpander touch SDK home page

TextExpander touch home page

Google Group tetouch-sdk (for announcements)

The TextExpanderDemoApp project is a working example app demonstrating how to add TextExpander functionality to your app and custom keyboard.

How to Add TextExpander to your iOS App

Grab the latest TextExpander touch SDK from GitHub

  1. Launch Terminal
  2. Change the the directory into which you'd like to download the SDK
  3. Run this command:
git clone https://github.com/SmileSoftware/TextExpanderTouchSDK

Build the Sample Project

TextExpanderDemoApp demonstrates the key aspects of integrating TextExpander functionality into your iOS app:

TextExpanderDemoApp is not meant to be a model iOS app. It's meant to demonstrate TextExpander functionality so that you can see it in context and adopt it easily into your app.

  1. Download TextExpander from the App Store
  2. Open the TextExpanderTouchSDK folder from step 1
  3. Double-click TextExpanderDemoApp.xcodeproj or TextExpanderDemoAppSwift.xcodeproj to open the sample project in Xcode
  4. Choose Product -> Run to run the sample
  5. Tap Settings
  6. Turn on "Use TextExpander"
  7. Tap Fetch Snippets to get the snippets from TextExpander
  8. Tap on the views and expand snippets into them, such as "ddate" or "sig1"

Note: To dismiss the keyboard, tap the whitespace to the left or right of the text field.

Add TextExpander to Your Project

  1. Drag TextExpander.xcframework into your project
  2. Select your app's target
  3. Click on "General"
  4. Scroll down to "Frameworks, Libraries, and Embedded Content"
  5. Drag the TextExpander.xcframework from your project to that list
  6. Select "Embed & Sign" from the popup to the right of where TextExpander.xcframework appears in the list

Allow querying and opening the TextExpander touch app

As of iOS 9 you must add TextExpander URL schemes to LSApplicationQueriesSchemes so that canOpenURL can work properly.

  1. Select your app's target
  2. Click on "Info"
  3. Add a LSApplicationQueriesSchemes key (if your project does not yet have one) with an Array value
  4. Add three TextExpander URL scheme values to this array: tetouch, tetouch-xc, and tetouch-settings

Add TextExpander to Your View

TextExpander works with these views:

Objective-C:

  1. Import the TextExpander header into your view controller's header:
    #import "SMTEDelegateController.h"
  2. Add an SMTEDelegateController to your view controller:
    @property (nonatomic, strong) SMTEDelegateController *textExpander;
  3. In your view controller's viewDidLoad method, initialize SMTEDelegateController and make it the delegate of your view(s):
    self.textExpander = [[SMTEDelegateController alloc] init];
    [self.textView setDelegate:self.textExpander];
    [self.textExpander setNextDelegate:self];

Swift:

  1. Import the TextExpander module into your view controller class:
    import TextExpander
  2. Add an SMTEDelegateController to your view controller:
    var textExpander: SMTEDelegateController?
  3. In your view controller's viewDidLoad method, initialize SMTEDelegateController and make it a delegate of your view(s):
    self.textExpander = SMTEDelegateController(); self.textView!.delegate = self.textExpander

Disabling TextExpander Custom Keyboard Expansions

TextExpander 3 and 4 ship with a custom keyboard, which can expand TextExpander abbreviations when typed.

Custom keyboards do not support rich text, and their UI is limited such that they cannot support fill-ins.

If your app implements the SDK, you'll want to disable TextExpander custom keyboard expansion for the best user experience.

To disable TextExpander custom keyboard expansion, you'll add a listener for the Darwin notification "com.smileonmymac.tetouch.keyboard.viewWillAppear" and in that listener you'll call [SMTEDelegateController setCustomKeyboardExpansionEnabled:NO]. Here's an example:

    int status = notify_register_dispatch("com.smileonmymac.tetouch.keyboard.viewWillAppear",
                                          &SMAppDelegateCustomKeyboardWillAppearToken,
                                          dispatch_get_main_queue(), ^(int t) {
                                              [SMTEDelegateController setCustomKeyboardExpansionEnabled:NO];
                                          });

There is also a corresponding "com.smileonmymac.tetouch.keyboard.viewWillDisappear" notification. It is not necessary to register for that to re-enable expansion.

If you're writing in Swift, you'll need to do this in Objective-C. Please see SMSwiftWorkarounds.h, SMSwiftWorkarounds.m, and TextExpanderDemoAppSwift-Bridging-Header.h in TextExpanderDemoAppSwift.xcodeproj for an example of how to do this.

Add TextExpander to Your Custom Keyboard or Other Extension

Your app which contains your extension ("containing app") will have to acquire snippet data from TextExpander (see Acquiring / Updating Snippet Data below).

Both your containing app and your extension will have to turn on the App Group capability in the Info section of their Xcode targets, and they'll have to share an identically named app group. You can see an example of this in the TextExpanderDemoApp project and its custom keyboard.

  1. Import the TextExpander header into your view controller's header:
    #import "SMTEDelegateController.h"
  2. Add an SMTEDelegateController to your view controller's subclass:
    @property (nonatomic, strong) SMTEDelegateController *textExpander;
  3. In your view controller's viewDidLoad method, initialize SMTEDelegateController and set its appGroupIdentifier:
    self.textExpander = [[SMTEDelegateController alloc] init];
    self.textExpander.appGroupIdentifier = @"";
  4. Implement Acquiring / Updating Snippet Data in your containing app as described below

The TextExpander SDK will call [NSFileManager containerURLForSecurityApplicationGroupIdentifier:appGroupIdentifier] to obtain your app group container, and it will store and retrieve its snippet data from an appended path component of: Library/Application Support/TextExpander, creating the folders if necessary.

A custom keyboard won't use views and delegate methods. It will interact with TextExpander using:

[SMTEDelegateController stringByExpandingAbbreviations:stringToExpand cursorPosition:&cursorPosition options:expansionOptions];

This method extends the previous stringByExpandingAbbreviations: method by returning the index of the cursor in the expanded text when the user expands a snippet with cursor positioning.

The TextExpanderDemoApp includes a custom keyboard target, which serves as an example of how to support TextExpander in a custom keyboard. To add the custom keyboard to the demo app:

  1. Select the TextExpanderDemoApp target, and in its Build Phases tab, add the DemoAppKeyboard as a Target dependency
  2. In the TextExpanderDemoApp target, set the Code Signing Entitlements to TextExpanderDemoApp/TextExpanderDemoApp.Entitlements
  3. On developer.apple.com, you'll need to do the following, but with your own IDs in place of our examples:
    • Create an App Group (e.g. group.com.smileonmymac.textexpander.demoapp)
    • Create an App ID (e.g. com.smileonmymac.TextExpanderDemoApp)
    • Edit the App ID to add the App Group
    • Create a Provisioning Profile for development, download it, and drag it to Xcode
    • Create another App ID for the keyboard (e.g. com.smileonmymac.TextExpanderDemoApp.DemoAppKeyboard)
    • Edit the App ID to add the App Group
    • Create a Provisioning Profile for development, download it, and drag it to Xcode
  4. Select the TextExpanderDemoApp target, and in its Capabilities tab, turn your App Group on, and check the appropriate Group
  5. Select the DemoAppKeyboard target, and in its Capabilities tab, turn your App Group on, and check the appropriate Group
  6. Select the TextExpanderDemoApp target, and in its Build Phases tab, add the DemoAppKeyboard to Embed App Extensions
  7. Change the appGroupIdentifier setting in SMFirstViewController, SMSecondViewController, SMThirdViewController, and KeyboardViewController to match yours (search and replace @"group.com.smileonmymac.textexpander.demoapp")
  8. Run the demo app, and update its snippets (so that they get written to the app group container)
  9. Add your keyboard and do the test expansion in any app

Note: Snippet changes made in TextExpander touch are not automatically available to your custom keyboard. It gets its snippets from its container app, which uses the x-callback-url method described below to acquire and update snippet data.

Acquiring / Updating Snippet Data

To acquire / update snippet data, your app needs to:

  1. Provide a URL scheme for getting snippets via x-callback-url:

    • Set the getSnippetsScheme property of the SMTEDelegateController
    • Add the scheme to your app's Info in Xcode under "URL Types"
    • Implement application:openURL:sourceApplication:annotation: or application:handleOpenURL: in your app delegate, and call [SMTEDelegateController handleGetSnippetsURL:error:cancelFlag:] with any URL's that have that scheme (or which meet the criteria as described in the note below)
    • If cancelFlag is returned as true, the user has Share Snippets turned off in TextExpander and did not permit sharing temporarily when prompted. If error is not nil, an error occurred, and you should probably inform the user.
  2. Add a user interface element to your app which, when touched, initiates acquisition / updating of snippet data by calling - [SMTEDelegateController getSnippets]

  3. Set the clientAppName property of the SMTEDelegateController, which is used to display the name of your app in the TextExpander app, as might be the case when Share Snippets is turned off to identify which app is requesting snippet data and offering the user a choice to turn Share Snippets on or to cancel.

Example setup:

self.textExpander = [[SMTEDelegateController alloc] init];
[self.textView setDelegate:self.textExpander]; // required for expansion
[self.textExpander setNextDelegate:self]; // if you still want to receive iOS delegate messages self.textExpander.getSnippetsScheme = @"supertyper-xc"; // your URL scheme self.textExpander.clientAppName = @"SuperTyper"; // your app's name
Note that you can use an existing URL scheme as your getSnippetsScheme if you want to. The callback URLs will look like these:

To differentiate from other URL calls your app receives, examine the URL for the presence of x-callback-url in the URL host and /TextExpander as the prefix of the URL path to determine whether or not a given URL is a snippet data callback to your URL scheme.

To provide users information about the current status of TextExpander data, you can use expansionStatusForceLoad:snippetCount:loadDate:error: to obtain the last-fetched snippet settings' modification date, or find that no snippet settings have yet been fetched.

Please note that it is possible, though unlikely, that your app will be unloaded when TextExpander touch is launched. You may find that you will need code before or after you call [SMTEDelegateController handleGetSnippetsURL:] to check your app's state and to restore it if necessary.

Usage Notes

Handling Attributed Text

As of iOS 6, UITextView and UITextField both implement an attributedText property. Even if your app leaves all the text in the view/field formatted the same way, TextExpander has no way to know that, so in order to retain any formatting of the existing text when it performs expansions, it inserts the snippet text into an NSMutableAttributedString copied from attributedText, then calls setAttributedText:

However, setAttributedText: has some undesirable Undo/Redo side-effects:

If your UITextView does not offer allowsEditingTextAttributes then TextExpander will expand only the "plain text" versions of snippets but, as mentioned above, it uses setAttributedText: to retain existing formatting. To avoid calls to setAttributedText: you can subclass UITextView or UITextField and implement these two methods:

- (NSAttributedString\*)textExpanderAttributedString;
- (void)textExpanderSetAttributedString: (NSAttributedString\*)newText;

As of SDK version 2.0.1, TextExpander will prefer those methods over attributedText/setAttributedText: if your UITextView/Field implements them.

In the simplest case, where your view is all formatted the same way, these methods can be as simple as:

-(NSAttributedString\*)textExpanderAttributedString {
    return self.attributedText;
}
-(void)textExpanderSetAttributedString: (NSAttributedString\*)newText {
    self.text = [newText string];
}

(Note: To avoid locking up when performing a snippet expansion Undo using setAttributedText:, TextExpander uses dispatch_async(dispatch_get_main_queue(), ^{ blah setAttributedText: changedText });)

Supporting Fill-in Snippets

The above instructions support normal snippets, where the abbreviation characters that a user types are immediately expanded to snippet content. TEtouch 2.0 and above also supports fill-in snippets. Fill-ins allow the user to set up a longer snippet with a one or more variable fields embedded, which can be text fields, conditionally included sections, and popup menus to choose among selected options.

The fill-in process involves the use of x-callback URLs (thanks Greg Pierce of Agile Tortoise!) to switch to the TextExpander touch app, where the user fills out field values, then a switch back to your app, where the completed text is inserted.

To support fill-in snippets, your app needs to:

  1. Provide a URL scheme for fill-in snippet completion via x-callback-url. (Leaving this nil will avoid the fill-in process, %fill% macros in the snippet will be replaced with (field name).)
    1. Set the fillCompletionScheme property of the SMTEDelegateController (probably the same as the getSnippetsScheme)
    2. Add the scheme to your app's Info in Xcode under "URL Types" (if not using an existing URL scheme -- see note below)
    3. Implement application:openURL:sourceApplication:annotation: or application:handleOpenURL: in your app delegate, and call the SMTEDelegateController's handleFillCompletionURL: with any URL's that have that scheme (or which meet the criteria as described in the note below)
  2. Implement the SMTEFillDelegate protocol to allow the SDK to return first responder status to the correct text item to insert a completed fill-in snippet by setting the fillDelegate property of the SMTEDelegateController with your SMTEFillDelegate implementing object

Continuing the example setup:

self.textExpander = [[SMTEDelegateController alloc] init];
[self.textView setDelegate:self.textExpander];  // required for expansion
[self.textExpander setNextDelegate:self]; // if you still want to receive iOS delegate messages
self.textExpander.getSnippetsScheme = @"supertyper-xc"; // required to fetch snippets
self.textExpander.clientAppName = @"SuperTyper"; // required to fetch snippets
self.textExpander.fillCompletionScheme = @"supertyper-xc"; // to handle fill-in callback

Note that you can use an existing URL scheme as your fillCompletionScheme if you want to. The callback URLs will look like these:

So you can easily examine the URL for the presence of x-callback-url in the URL host and /SMTE as the prefix of the URL path to determine whether or not a given URL is a fill-in callback to your URL scheme.

The main point of the SMTEFillDelegate protocol is that, during the fill-in process, the TEtouch app gets activated. Since your app loses focus temporarily, iOS might unload your app, so to insert the filled-in snippet text at the correct location in your app, you must re-activate the text area and tell TE's SDK . The SDK will insert the fill text at the offset where the snippet was activated.

In most cases, iOS will not unload your app in the short time it takes the user to fill the fields, so your SMTEFillDelegate's implementation of makeIdentifiedTextObjectFirstResponder:fillWasCanceled:cursorPosition: generally may not need to do any work at all (other than returning the appropriate value) in cases where your app has not unloaded.

In case your app does get unloaded, you should save whatever text the user was typing when the fill-in snippet was triggered. Your app probably already does this, but the optional prepareForFillSwitch: method gives your app a chance to save state just before the URL which opens TEtouch is opened, or to return NO to abort the process at the last moment.

The SMTEFillDelegate methods are straightforward for UITextViews, UITextFields, and UISearchBars. You make up some kind of name to identify the object, like "mainTextArea". When your app is re-focused and the filled-in snippet text is ready to be inserted, you only need to provide the appropriate UIKit object and make it become first responder in your implementation of makeIdentifiedTextObjectFirstResponder:fillWasCanceled:cursorPosition.

However, for UIWebViews, things are a bit more complicated, since the UIWebView object alone is not enough to specify where the insertion should occur. In that case, an NSDictionary is used so that it can contain both the UIWebView and an ID or name of the HTML element where the fill-in snippet was triggered.

The example app includes two different implementations of SMTEFillDelegate, and demonstrates how to figure out which of two SMTEDelegateController instances to pass a fill completion callback URL to.

Testing Notes

Support

Smile offers no promise of support for the TextExpander framework. If you have questions, please address them via email to textexpander-touch@smilesoftware.com. As time and resources permit, we will attempt to answer such questions. Of course, if you are willing to add TextExpander support to your app, it is in our best interest to endeavor to support you.

Stay informed about new versions of the TextExpander framework on this announcement-only Google Group: https://groups.google.com/forum/#!forum/tetouch-sdk

License Agreement

The TextExpander framework is Copyright © 2009-2016 SmileOnMyMac, LLC dba Smile, and is supplied "AS IS" and without warranty. SmileOnMyMac, LLC disclaims all warranties, expressed or implied, including, without limitation the warranties of merchantability and of fitness for any purpose. SmileOnMyMac assumes no liability for direct, indirect, incidental, special, exemplary, or consequential damages, which may result from the use of the TextExpander framework, even if advised of the possibility of such damage.

Permission is hereby granted to use, copy, and distribute this library, without fee, subject to the following restrictions:

  1. The origin of this library must not be misrepresented
  2. Apps which use this library must indicate "Supports TextExpander snippet expansion" in their feature set or product description
  3. Apps which use this library must indicate "Contains TextExpander framework, Copyright © 2009-2021 SmileOnMyMac, LLC dba Smile. TextExpander is a registered trademark" in their about box and the paragraph above in their license agreement. It is acceptable to link to the license agreement text posted on the app developer's website if that is more appropriate for a given app.

If your app has special needs with respect to the above restrictions, please address them, preferably with specific suggestions, via email to textexpander-touch@smilesoftware.com. Perhaps we can find a mutually agreeable solution.

Default Abbreviations & Snippets

aaddr: 123 Market Street San Francisco, CA

ddate: (day: no leading zero) (month: name) (year: 4 digits)

sig1: Cheers,

Jane Smith Senior Vice President Acme, Inc.

sig2: Yours sincerely,

Jane Smith PTA President Cupertino Elementary School

sig3: (formatted text sample) Cheers, Jane Smith Senior Vice President Acme, Inc.

sms1: I'm running late. Be there soon.

sms2: Traffic is terrible. I'll be late. Sorry.

sms3: I forgot all about our appointment. Can we reschedule?

ttel: 415-555-1212

tyvm: Thank you very much!

dnthx: (fill-in example) Dear %filltext:name=person name:width:25%,

Thank you for your generous donation of $%filltext:name=amount:width:4%. We greatly appreciate your help, and will use the funds for %fillpopup:name=popup 4:default=our educational program:our health clinic:general purposes%.

Since we are a non-profit organization, your donation of $%filltext:name=amount:width:4% should be tax-deductible.

Thank you,

Release Notes

4.6,5 (2021-02-17)

4.5 (2019-11-04)

4.0 (2016-08-12)

3.5.4 (2015-09-09)

3.5.3 (2015-08-28)

3.5 (2015-05-23)

3.0.5 (2014-12-09)

3.0.4 (2014-10-29)

3.0.3 (2014-09-25)

3.0.2 (2014-09-18)

3.0.1 (2014-09-16)

3.0 (2014-09-04)

2.3.1 (2013-12-21)

2.3 (2013-11-25)

2.2.1 (2013-09-27)

2.2 (2013-09-25)

2.1 (2013-09-10)

2.0.1 (2013-05-31)

2.0 (2013-05-15)

1.2.3 (2012-10-14)

1.2.2 (2012-09-13)

1.2.1 (2012-08-28)

1.2 (2012-06-30)

1.1.7 (2011-01-25)

1.1.6 (2010-08-07)

NOTE: The iPhone Simulator shipped with Xcode 3.2.3 has a different ABI than previous releases. It MUST link against the libteEngine.a in the Simulator4 folder. We've removed the Simulator folder from this build to avoid confusion. We have confirmed with Apple that the ABI change affects only the Simuatlor and that static libraries compiled for iOS 3 will run properly when linked with iOS 4 targets.

1.1.5 (2010-05-27)

1.1.4 (2010-03-23)

1.1.3 (2010-02-08)

1.1.2 (2009-11-06)

1.1.1.1 (2009-10-13)

1.1.1 (2009-10-05)

1.1 (2009-09-10)

1.0.2 (2009-08-31)

1.0.1 (2009-08-30)

1.0 (2009-08-24)

[^1]: The framework is unsigned. It will inherit your signature when you build an app linking it.