";
312 | };
313 | /* End PBXVariantGroup section */
314 |
315 | /* Begin XCBuildConfiguration section */
316 | 3C2E434D201E7E6C00E4254A /* Debug */ = {
317 | isa = XCBuildConfiguration;
318 | buildSettings = {
319 | ALWAYS_SEARCH_USER_PATHS = NO;
320 | CLANG_ANALYZER_NONNULL = YES;
321 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
322 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
323 | CLANG_CXX_LIBRARY = "libc++";
324 | CLANG_ENABLE_MODULES = YES;
325 | CLANG_ENABLE_OBJC_ARC = YES;
326 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
327 | CLANG_WARN_BOOL_CONVERSION = YES;
328 | CLANG_WARN_COMMA = YES;
329 | CLANG_WARN_CONSTANT_CONVERSION = YES;
330 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
331 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
332 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
333 | CLANG_WARN_EMPTY_BODY = YES;
334 | CLANG_WARN_ENUM_CONVERSION = YES;
335 | CLANG_WARN_INFINITE_RECURSION = YES;
336 | CLANG_WARN_INT_CONVERSION = YES;
337 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
338 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
339 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
340 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
341 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
342 | CLANG_WARN_STRICT_PROTOTYPES = YES;
343 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
344 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
345 | CLANG_WARN_UNREACHABLE_CODE = YES;
346 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
347 | CODE_SIGN_IDENTITY = "";
348 | COPY_PHASE_STRIP = NO;
349 | CURRENT_PROJECT_VERSION = 1;
350 | DEBUG_INFORMATION_FORMAT = dwarf;
351 | ENABLE_STRICT_OBJC_MSGSEND = YES;
352 | ENABLE_TESTABILITY = YES;
353 | GCC_C_LANGUAGE_STANDARD = gnu11;
354 | GCC_DYNAMIC_NO_PIC = NO;
355 | GCC_NO_COMMON_BLOCKS = YES;
356 | GCC_OPTIMIZATION_LEVEL = 0;
357 | GCC_PREPROCESSOR_DEFINITIONS = (
358 | "DEBUG=1",
359 | "$(inherited)",
360 | );
361 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
362 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
363 | GCC_WARN_UNDECLARED_SELECTOR = YES;
364 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
365 | GCC_WARN_UNUSED_FUNCTION = YES;
366 | GCC_WARN_UNUSED_VARIABLE = YES;
367 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
368 | MTL_ENABLE_DEBUG_INFO = YES;
369 | ONLY_ACTIVE_ARCH = YES;
370 | SDKROOT = iphoneos;
371 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
372 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
373 | SWIFT_VERSION = 5.0;
374 | TARGETED_DEVICE_FAMILY = "1,2";
375 | VERSIONING_SYSTEM = "apple-generic";
376 | VERSION_INFO_PREFIX = "";
377 | };
378 | name = Debug;
379 | };
380 | 3C2E434E201E7E6C00E4254A /* Release */ = {
381 | isa = XCBuildConfiguration;
382 | buildSettings = {
383 | ALWAYS_SEARCH_USER_PATHS = NO;
384 | CLANG_ANALYZER_NONNULL = YES;
385 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
386 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
387 | CLANG_CXX_LIBRARY = "libc++";
388 | CLANG_ENABLE_MODULES = YES;
389 | CLANG_ENABLE_OBJC_ARC = YES;
390 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
391 | CLANG_WARN_BOOL_CONVERSION = YES;
392 | CLANG_WARN_COMMA = YES;
393 | CLANG_WARN_CONSTANT_CONVERSION = YES;
394 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
395 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
396 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
397 | CLANG_WARN_EMPTY_BODY = YES;
398 | CLANG_WARN_ENUM_CONVERSION = YES;
399 | CLANG_WARN_INFINITE_RECURSION = YES;
400 | CLANG_WARN_INT_CONVERSION = YES;
401 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
402 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
403 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
404 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
405 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
406 | CLANG_WARN_STRICT_PROTOTYPES = YES;
407 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
408 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
409 | CLANG_WARN_UNREACHABLE_CODE = YES;
410 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
411 | CODE_SIGN_IDENTITY = "";
412 | COPY_PHASE_STRIP = NO;
413 | CURRENT_PROJECT_VERSION = 1;
414 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
415 | ENABLE_NS_ASSERTIONS = NO;
416 | ENABLE_STRICT_OBJC_MSGSEND = YES;
417 | GCC_C_LANGUAGE_STANDARD = gnu11;
418 | GCC_NO_COMMON_BLOCKS = YES;
419 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
420 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
421 | GCC_WARN_UNDECLARED_SELECTOR = YES;
422 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
423 | GCC_WARN_UNUSED_FUNCTION = YES;
424 | GCC_WARN_UNUSED_VARIABLE = YES;
425 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
426 | MTL_ENABLE_DEBUG_INFO = NO;
427 | SDKROOT = iphoneos;
428 | SWIFT_COMPILATION_MODE = wholemodule;
429 | SWIFT_OPTIMIZATION_LEVEL = "-O";
430 | SWIFT_VERSION = 5.0;
431 | TARGETED_DEVICE_FAMILY = "1,2";
432 | VALIDATE_PRODUCT = YES;
433 | VERSIONING_SYSTEM = "apple-generic";
434 | VERSION_INFO_PREFIX = "";
435 | };
436 | name = Release;
437 | };
438 | 3C2E4350201E7E6C00E4254A /* Debug */ = {
439 | isa = XCBuildConfiguration;
440 | buildSettings = {
441 | APPLICATION_EXTENSION_API_ONLY = YES;
442 | DEFINES_MODULE = YES;
443 | DYLIB_COMPATIBILITY_VERSION = 1;
444 | DYLIB_CURRENT_VERSION = 1;
445 | DYLIB_INSTALL_NAME_BASE = "@rpath";
446 | INFOPLIST_FILE = Framework/Info.plist;
447 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
448 | LD_RUNPATH_SEARCH_PATHS = (
449 | "$(inherited)",
450 | "@executable_path/Frameworks",
451 | "@loader_path/Frameworks",
452 | );
453 | PRODUCT_BUNDLE_IDENTIFIER = net.ianmcdowell.InputAssistant;
454 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
455 | SKIP_INSTALL = YES;
456 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
457 | };
458 | name = Debug;
459 | };
460 | 3C2E4351201E7E6C00E4254A /* Release */ = {
461 | isa = XCBuildConfiguration;
462 | buildSettings = {
463 | APPLICATION_EXTENSION_API_ONLY = YES;
464 | DEFINES_MODULE = YES;
465 | DYLIB_COMPATIBILITY_VERSION = 1;
466 | DYLIB_CURRENT_VERSION = 1;
467 | DYLIB_INSTALL_NAME_BASE = "@rpath";
468 | INFOPLIST_FILE = Framework/Info.plist;
469 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
470 | LD_RUNPATH_SEARCH_PATHS = (
471 | "$(inherited)",
472 | "@executable_path/Frameworks",
473 | "@loader_path/Frameworks",
474 | );
475 | PRODUCT_BUNDLE_IDENTIFIER = net.ianmcdowell.InputAssistant;
476 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
477 | SKIP_INSTALL = YES;
478 | };
479 | name = Release;
480 | };
481 | 3C2E436B201E883E00E4254A /* Debug */ = {
482 | isa = XCBuildConfiguration;
483 | buildSettings = {
484 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
485 | CODE_SIGN_IDENTITY = "iPhone Developer";
486 | INFOPLIST_FILE = Sample/Info.plist;
487 | LD_RUNPATH_SEARCH_PATHS = (
488 | "$(inherited)",
489 | "@executable_path/Frameworks",
490 | );
491 | PRODUCT_BUNDLE_IDENTIFIER = "net.ianmcdowell.InputAssistant-Sample";
492 | PRODUCT_NAME = "$(TARGET_NAME)";
493 | };
494 | name = Debug;
495 | };
496 | 3C2E436C201E883E00E4254A /* Release */ = {
497 | isa = XCBuildConfiguration;
498 | buildSettings = {
499 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
500 | CODE_SIGN_IDENTITY = "iPhone Developer";
501 | INFOPLIST_FILE = Sample/Info.plist;
502 | LD_RUNPATH_SEARCH_PATHS = (
503 | "$(inherited)",
504 | "@executable_path/Frameworks",
505 | );
506 | PRODUCT_BUNDLE_IDENTIFIER = "net.ianmcdowell.InputAssistant-Sample";
507 | PRODUCT_NAME = "$(TARGET_NAME)";
508 | };
509 | name = Release;
510 | };
511 | 3C2E4380201EFACD00E4254A /* Debug */ = {
512 | isa = XCBuildConfiguration;
513 | buildSettings = {
514 | OTHER_LDFLAGS = "-ObjC";
515 | PRODUCT_NAME = "$(TARGET_NAME)";
516 | SKIP_INSTALL = YES;
517 | };
518 | name = Debug;
519 | };
520 | 3C2E4381201EFACD00E4254A /* Release */ = {
521 | isa = XCBuildConfiguration;
522 | buildSettings = {
523 | OTHER_LDFLAGS = "-ObjC";
524 | PRODUCT_NAME = "$(TARGET_NAME)";
525 | SKIP_INSTALL = YES;
526 | };
527 | name = Release;
528 | };
529 | /* End XCBuildConfiguration section */
530 |
531 | /* Begin XCConfigurationList section */
532 | 3C2E4341201E7E6C00E4254A /* Build configuration list for PBXProject "InputAssistant" */ = {
533 | isa = XCConfigurationList;
534 | buildConfigurations = (
535 | 3C2E434D201E7E6C00E4254A /* Debug */,
536 | 3C2E434E201E7E6C00E4254A /* Release */,
537 | );
538 | defaultConfigurationIsVisible = 0;
539 | defaultConfigurationName = Release;
540 | };
541 | 3C2E434F201E7E6C00E4254A /* Build configuration list for PBXNativeTarget "InputAssistant" */ = {
542 | isa = XCConfigurationList;
543 | buildConfigurations = (
544 | 3C2E4350201E7E6C00E4254A /* Debug */,
545 | 3C2E4351201E7E6C00E4254A /* Release */,
546 | );
547 | defaultConfigurationIsVisible = 0;
548 | defaultConfigurationName = Release;
549 | };
550 | 3C2E436A201E883E00E4254A /* Build configuration list for PBXNativeTarget "InputAssistant Sample" */ = {
551 | isa = XCConfigurationList;
552 | buildConfigurations = (
553 | 3C2E436B201E883E00E4254A /* Debug */,
554 | 3C2E436C201E883E00E4254A /* Release */,
555 | );
556 | defaultConfigurationIsVisible = 0;
557 | defaultConfigurationName = Release;
558 | };
559 | 3C2E437F201EFACD00E4254A /* Build configuration list for PBXNativeTarget "libInputAssistant" */ = {
560 | isa = XCConfigurationList;
561 | buildConfigurations = (
562 | 3C2E4380201EFACD00E4254A /* Debug */,
563 | 3C2E4381201EFACD00E4254A /* Release */,
564 | );
565 | defaultConfigurationIsVisible = 0;
566 | defaultConfigurationName = Release;
567 | };
568 | /* End XCConfigurationList section */
569 | };
570 | rootObject = 3C2E433E201E7E6C00E4254A /* Project object */;
571 | }
572 |
--------------------------------------------------------------------------------
/InputAssistant.xcodeproj/xcshareddata/xcschemes/InputAssistant Sample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/InputAssistant.xcodeproj/xcshareddata/xcschemes/InputAssistant.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Ian McDowell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Input Assistant
2 |
3 |
4 | TiltedTabView •
5 | TabView •
6 | InputAssistant •
7 | Git
8 |
9 |
10 | --------
11 |
12 | This library is a view that shows custom auto-complete suggestions for your UITextField / UITextView.
13 |
14 | [](https://travis-ci.org/IMcD23/InputAssistant)
15 | [](https://github.com/IMcD23/InputAssistant/releases/latest)
16 | 
17 | [](https://twitter.com/ian_mcdowell)
18 |
19 |
20 |
21 | # Requirements
22 |
23 | * Xcode 9 or later
24 | * iOS 10.0 or later
25 |
26 | # Usage
27 |
28 | This library provides an `InputAssistantView` class, that is designed to be set as the `inputAccessoryView` of a UITextView or UITextField.
29 |
30 | It provides three areas that you can customize.
31 | - Suggestions - A scrollable set of text suggestions.
32 | - Leading/Trailing actions - tappable buttons on either side of the suggestions.
33 | - Empty text - Optional text that can be displayed when there are no suggestions.
34 |
35 | Use the `InputAssistantViewDataSource` protocol that allows you to do this customization.
36 |
37 | To react to a suggestion being tapped, conform to the `InputAssistantViewDelegate` protocol.
38 |
39 | # Installation
40 |
41 | ## Carthage
42 | To install InputAssistant using [Carthage](https://github.com/Carthage/Carthage), add the following line to your Cartfile:
43 |
44 | ```
45 | github "IMcD23/InputAssistant" "master"
46 | ```
47 |
48 | ## Submodule
49 | To install InputAssistant as a submodule into your git repository, run the following command:
50 |
51 | ```
52 | git submodule add -b master https://github.com/IMcD23/InputAssistant.git Path/To/InputAssistant
53 | git submodule update --init --recursive
54 | ```
55 |
56 | Then, add the `.xcodeproj` in the root of the repository into your Xcode project, and add it as a build dependency.
57 |
58 | ## ibuild
59 | A Swift static library of this project is also available for the ibuild build system. Learn more about ibuild [here](https://github.com/IMcD23/ibuild)
60 |
61 | # Author
62 | Created by [Ian McDowell](https://ianmcdowell.net)
63 |
64 | # License
65 | All code in this project is available under the license specified in the LICENSE file. However, since this project also bundles code from other projects, you are subject to those projects' licenses as well.
66 |
--------------------------------------------------------------------------------
/Resources/Keyboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ian-mcdowell/InputAssistant/db3ec1c49a8c3d612899956d124854205bf3369a/Resources/Keyboard.png
--------------------------------------------------------------------------------
/Resources/Keyboard_iPad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ian-mcdowell/InputAssistant/db3ec1c49a8c3d612899956d124854205bf3369a/Resources/Keyboard_iPad.png
--------------------------------------------------------------------------------
/Sample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // InputAssistant Sample
4 | //
5 | // Created by Ian McDowell on 1/28/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
17 |
18 | window = UIWindow(frame: UIScreen.main.bounds)
19 | window?.rootViewController = ViewController()
20 | window?.makeKeyAndVisible()
21 |
22 | return true
23 | }
24 |
25 | func applicationWillResignActive(_ application: UIApplication) { }
26 | func applicationDidEnterBackground(_ application: UIApplication) { }
27 | func applicationWillEnterForeground(_ application: UIApplication) { }
28 | func applicationDidBecomeActive(_ application: UIApplication) { }
29 | func applicationWillTerminate(_ application: UIApplication) { }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/Sample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Sample/Assets.xcassets/Down.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "down.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
--------------------------------------------------------------------------------
/Sample/Assets.xcassets/Down.imageset/down.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ian-mcdowell/InputAssistant/db3ec1c49a8c3d612899956d124854205bf3369a/Sample/Assets.xcassets/Down.imageset/down.pdf
--------------------------------------------------------------------------------
/Sample/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Sample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIRequiredDeviceCapabilities
26 |
27 | armv7
28 |
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Sample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // InputAssistant Sample
4 | //
5 | // Created by Ian McDowell on 1/28/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import InputAssistant
11 |
12 | class ViewController: UIViewController {
13 |
14 | let textView: UITextView
15 | let inputAssistantView: InputAssistantView
16 | let allSuggestions = ["Suggestion", "Test", "Hello", "World", "More", "Suggestions"]
17 |
18 | init() {
19 | textView = UITextView()
20 | inputAssistantView = InputAssistantView()
21 | super.init(nibName: nil, bundle: nil)
22 |
23 | inputAssistantView.delegate = self
24 | inputAssistantView.dataSource = self
25 | inputAssistantView.leadingActions = []
26 | inputAssistantView.trailingActions = [
27 | InputAssistantAction(image: #imageLiteral(resourceName: "Down"), target: self, action: #selector(downTapped))
28 | ]
29 | }
30 |
31 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
32 |
33 | override func viewDidLoad() {
34 | super.viewDidLoad()
35 |
36 | view.backgroundColor = .groupTableViewBackground
37 |
38 | inputAssistantView.attach(to: textView)
39 |
40 | view.addSubview(textView)
41 | textView.translatesAutoresizingMaskIntoConstraints = false
42 | NSLayoutConstraint.activate([
43 | textView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
44 | textView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
45 | textView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
46 | textView.bottomAnchor.constraint(lessThanOrEqualTo: view.layoutMarginsGuide.bottomAnchor),
47 | textView.heightAnchor.constraint(equalToConstant: 120)
48 | ])
49 | }
50 |
51 | @objc private func addTapped() {
52 | print("Add")
53 | }
54 |
55 | @objc private func downTapped() {
56 | textView.resignFirstResponder()
57 | }
58 | }
59 |
60 | extension ViewController: InputAssistantViewDataSource {
61 |
62 | func textForEmptySuggestionsInInputAssistantView() -> String? {
63 | return "No suggestions"
64 | }
65 |
66 | func numberOfSuggestionsInInputAssistantView() -> Int {
67 | return allSuggestions.count
68 | }
69 |
70 | func inputAssistantView(_ inputAssistantView: InputAssistantView, nameForSuggestionAtIndex index: Int) -> String {
71 | return allSuggestions[index]
72 | }
73 | }
74 |
75 | extension ViewController: InputAssistantViewDelegate {
76 |
77 | func inputAssistantView(_ inputAssistantView: InputAssistantView, didSelectSuggestionAtIndex index: Int) {
78 |
79 | self.textView.insertText(allSuggestions[index])
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/InputAssistantCollectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InputAssistantCollectionView.swift
3 | // InputAssistant
4 | //
5 | // Created by Ian McDowell on 1/28/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class InputAssistantCollectionView: UICollectionView {
12 |
13 | /// Reference to the containing input assistant view
14 | weak var inputAssistantView: InputAssistantView?
15 |
16 | /// Width constraint that equals the contentSize.width of the collection view. Low priority.
17 | var widthConstraint: NSLayoutConstraint?
18 |
19 | /// Label to display when there are no suggestions
20 | private let noSuggestionsLabel = UILabel()
21 |
22 | init() {
23 | let layout = UICollectionViewFlowLayout()
24 | layout.estimatedItemSize = CGSize(width: 100, height: 41)
25 | layout.itemSize = UICollectionViewFlowLayout.automaticSize
26 | layout.scrollDirection = .horizontal
27 | layout.minimumInteritemSpacing = 10
28 | layout.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
29 | super.init(frame: .zero, collectionViewLayout: layout)
30 |
31 | register(InputAssistantCollectionViewCell.self, forCellWithReuseIdentifier: "Suggestion")
32 | backgroundColor = .clear
33 | showsHorizontalScrollIndicator = false
34 | showsVerticalScrollIndicator = false
35 | delaysContentTouches = false
36 | dataSource = self
37 |
38 | noSuggestionsLabel.textAlignment = .center
39 | noSuggestionsLabel.autoresizingMask = [.flexibleWidth, .flexibleHeight]
40 | noSuggestionsLabel.textColor = UIColor.darkGray
41 | addSubview(noSuggestionsLabel)
42 |
43 | widthConstraint = self.widthAnchor.constraint(equalToConstant: 0)
44 | widthConstraint?.priority = .defaultLow
45 | widthConstraint?.isActive = true
46 | }
47 |
48 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
49 |
50 | override func reloadData() {
51 | super.reloadData()
52 |
53 | // Need to reset scrolling position,
54 | // since the self sizing cells can cause a crash when scrolling back after a reloadData.
55 | contentOffset = .zero
56 |
57 | noSuggestionsLabel.text = self.inputAssistantView?.dataSource?.textForEmptySuggestionsInInputAssistantView()
58 | noSuggestionsLabel.isHidden = self.numberOfItems(inSection: 0) > 0
59 | }
60 |
61 | override var contentSize: CGSize {
62 | didSet {
63 | // If there is no data, make the width the width of the no suggestions label.
64 | // Otherwise, it should be equal to the content size of the collectionView
65 | if !noSuggestionsLabel.isHidden {
66 | let targetLabelSize = noSuggestionsLabel.systemLayoutSizeFitting(CGSize(width: 9999, height: 55))
67 | widthConstraint?.constant = targetLabelSize.width
68 | } else {
69 | widthConstraint?.constant = contentSize.width
70 | }
71 | }
72 | }
73 | }
74 |
75 | extension InputAssistantCollectionView: UICollectionViewDataSource {
76 |
77 | public func numberOfSections(in collectionView: UICollectionView) -> Int {
78 | return 1
79 | }
80 |
81 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
82 | return inputAssistantView?.dataSource?.numberOfSuggestionsInInputAssistantView() ?? 0
83 | }
84 |
85 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
86 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Suggestion", for: indexPath) as! InputAssistantCollectionViewCell
87 |
88 | guard let inputAssistantView = inputAssistantView, let name = inputAssistantView.dataSource?.inputAssistantView(inputAssistantView, nameForSuggestionAtIndex: indexPath.row) else {
89 | fatalError("No suggestion name found at index.")
90 | }
91 |
92 | cell.label.text = name
93 | cell.keyboardAppearance = inputAssistantView.keyboardAppearance
94 |
95 | return cell
96 | }
97 | }
98 |
99 | private class InputAssistantCollectionViewCell: UICollectionViewCell {
100 |
101 | let label: UILabel
102 | let highlightedBackgroundColor = UIColor(red: 235/255, green: 237/255, blue: 239/255, alpha: 1)
103 | let regularBackgroundColor = UIColor(red: 174/255, green: 180/255, blue: 186/255, alpha: 1)
104 | let darkBackgroundColor = UIColor(white: 200/255, alpha: 0.4)
105 |
106 | var keyboardAppearance: UIKeyboardAppearance = .default {
107 | didSet { updateSelectionState() }
108 | }
109 |
110 | private var keyboardAppearanceBackgroundColor: UIColor {
111 | switch keyboardAppearance {
112 | case .dark: return self.darkBackgroundColor
113 | default: return self.regularBackgroundColor
114 | }
115 | }
116 |
117 | override init(frame: CGRect) {
118 | label = UILabel()
119 |
120 | super.init(frame: frame)
121 |
122 | label.textAlignment = .center
123 |
124 | self.contentView.addSubview(label)
125 | label.translatesAutoresizingMaskIntoConstraints = false
126 | NSLayoutConstraint.activate([
127 | label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
128 | label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
129 | label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
130 | label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
131 | label.widthAnchor.constraint(greaterThanOrEqualToConstant: 50)
132 | ])
133 |
134 | self.layer.cornerRadius = 4
135 | self.layer.masksToBounds = true
136 | updateSelectionState()
137 | }
138 |
139 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
140 |
141 | override var isHighlighted: Bool {
142 | didSet { updateSelectionState() }
143 | }
144 | override var isSelected: Bool {
145 | didSet { updateSelectionState() }
146 | }
147 |
148 | private func updateSelectionState() {
149 | let isHighlighted = self.isHighlighted || self.isSelected
150 | self.backgroundColor = isHighlighted ? self.highlightedBackgroundColor : self.keyboardAppearanceBackgroundColor
151 | self.label.textColor = isHighlighted ? .black : .white
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/Sources/InputAssistantView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InputAssistantView.swift
3 | // InputAssistant
4 | //
5 | // Created by Ian McDowell on 1/28/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// A button to be displayed in on the leading or trailing side of an input assistant.
12 | public struct InputAssistantAction {
13 |
14 | /// Image to display to the user. Will be resized to fit the height of the input assistant.
15 | public let image: UIImage
16 |
17 | public weak var target: AnyObject?
18 | public let action: Selector?
19 |
20 | public init(image: UIImage, target: AnyObject? = nil, action: Selector? = nil) {
21 | self.image = image; self.target = target; self.action = action
22 | }
23 | }
24 |
25 | public protocol InputAssistantViewDataSource: class {
26 |
27 | /// Text to display when there are no suggestions.
28 | func textForEmptySuggestionsInInputAssistantView() -> String?
29 |
30 | /// Number of suggestions to display
31 | func numberOfSuggestionsInInputAssistantView() -> Int
32 |
33 | /// Return information about the suggestion at the given index
34 | func inputAssistantView(_ inputAssistantView: InputAssistantView, nameForSuggestionAtIndex index: Int) -> String
35 | }
36 |
37 | /// Delegate to receive notifications about user actions in the input assistant view.
38 | public protocol InputAssistantViewDelegate: class {
39 |
40 | /// When the user taps on a suggestion
41 | func inputAssistantView(_ inputAssistantView: InputAssistantView, didSelectSuggestionAtIndex index: Int)
42 | }
43 |
44 | /// UIInputView that displays custom suggestions, as well as leading and trailing actions.
45 | open class InputAssistantView: UIInputView {
46 |
47 | /// Actions to display on the leading side of the suggestions.
48 | public var leadingActions: [InputAssistantAction] = [] {
49 | didSet { self.updateActions(leadingActions, leadingStackView) }
50 | }
51 |
52 | /// Actions to display on the trailing side of the suggestions
53 | public var trailingActions: [InputAssistantAction] = [] {
54 | didSet { self.updateActions(trailingActions, trailingStackView) }
55 | }
56 |
57 | /// Set this to receive notifications when things happen in the assistant view.
58 | public weak var delegate: InputAssistantViewDelegate?
59 |
60 | /// Set this to provide data to the input assistant view
61 | public weak var dataSource: InputAssistantViewDataSource? {
62 | didSet { suggestionsCollectionView.reloadData() }
63 | }
64 |
65 | /// Stack view on the leading side of the collection view. Contains actions.
66 | private let leadingStackView: UIStackView
67 |
68 | /// Stack view on the trailing side of the collection view. Contains actions.
69 | private let trailingStackView: UIStackView
70 |
71 | /// Collection view, with a horizontally scrolling set of suggestions.
72 | private let suggestionsCollectionView: InputAssistantCollectionView
73 |
74 | public init() {
75 | self.leadingStackView = UIStackView()
76 | self.trailingStackView = UIStackView()
77 |
78 | self.suggestionsCollectionView = InputAssistantCollectionView()
79 |
80 | super.init(frame: .init(origin: .zero, size: .init(width: 0, height: 55)), inputViewStyle: .default)
81 |
82 | self.suggestionsCollectionView.inputAssistantView = self
83 | self.suggestionsCollectionView.delegate = self
84 |
85 | for stackView in [leadingStackView, trailingStackView] {
86 | stackView.spacing = 10
87 | stackView.alignment = .center
88 | stackView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
89 | updateActions([], stackView)
90 | }
91 |
92 | // suggestions stretch to fill
93 | suggestionsCollectionView.setContentHuggingPriority(.defaultLow, for: .horizontal)
94 |
95 | // The stack views are embedded into a container, which lays them out horizontally
96 | let containerStackView = UIStackView(arrangedSubviews: [leadingStackView, suggestionsCollectionView, trailingStackView])
97 | containerStackView.alignment = .fill
98 | containerStackView.axis = .horizontal
99 | containerStackView.distribution = .equalCentering
100 |
101 | // Stretch to fill bounds
102 | containerStackView.frame = self.bounds
103 | self.addSubview(containerStackView)
104 | containerStackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
105 | }
106 |
107 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
108 |
109 | public func reloadData() {
110 | suggestionsCollectionView.reloadData()
111 | }
112 |
113 | /// The keyboard appearance of the attached text input
114 | internal var keyboardAppearance: UIKeyboardAppearance = .default {
115 | didSet {
116 | switch keyboardAppearance {
117 | case .dark: self.tintColor = .white
118 | default: self.tintColor = .black
119 | }
120 | }
121 | }
122 | private var keyboardAppearanceObserver: NSKeyValueObservation?
123 |
124 | /// Attach the inputAssistant to the given UITextView.
125 | public func attach(to textInput: UITextView) {
126 | self.keyboardAppearance = textInput.keyboardAppearance
127 |
128 | // Hide default undo/redo/etc buttons
129 | textInput.inputAssistantItem.leadingBarButtonGroups = []
130 | textInput.inputAssistantItem.trailingBarButtonGroups = []
131 |
132 | // Disable built-in autocomplete
133 | textInput.autocorrectionType = .no
134 |
135 | // Add the input assistant view as an accessory view
136 | textInput.inputAccessoryView = self
137 |
138 | keyboardAppearanceObserver = textInput.observe(\UITextView.keyboardAppearance) { [weak self] textInput, _ in
139 | self?.keyboardAppearance = textInput.keyboardAppearance
140 | }
141 | }
142 | /// Attach the inputAssistant to the given UITextView.
143 | public func attach(to textInput: UITextField) {
144 | self.keyboardAppearance = textInput.keyboardAppearance
145 |
146 | // Hide default undo/redo/etc buttons
147 | textInput.inputAssistantItem.leadingBarButtonGroups = []
148 | textInput.inputAssistantItem.trailingBarButtonGroups = []
149 |
150 | // Disable built-in autocomplete
151 | textInput.autocorrectionType = .no
152 |
153 | // Add the input assistant view as an accessory view
154 | textInput.inputAccessoryView = self
155 |
156 | keyboardAppearanceObserver = textInput.observe(\UITextField.keyboardAppearance) { [weak self] textInput, _ in
157 | self?.keyboardAppearance = textInput.keyboardAppearance
158 | }
159 | }
160 |
161 | open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
162 | super.traitCollectionDidChange(previousTraitCollection)
163 | updateActions(leadingActions, leadingStackView)
164 | updateActions(trailingActions, trailingStackView)
165 | }
166 |
167 | /// Remove existing actions, and add new ones to the given leading/trailing stack view.
168 | private func updateActions(_ actions: [InputAssistantAction], _ stackView: UIStackView) {
169 | for view in stackView.arrangedSubviews {
170 | view.removeFromSuperview()
171 | }
172 | if actions.isEmpty {
173 | let emptyView = UIView()
174 | emptyView.widthAnchor.constraint(equalToConstant: 0).isActive = true
175 | stackView.addArrangedSubview(emptyView)
176 | } else {
177 | let itemWidth: CGFloat = self.traitCollection.horizontalSizeClass == .regular ? 60 : 40
178 | for action in actions {
179 | let button = UIButton.init(type: .system)
180 | button.setImage(action.image.scaled(toSize: CGSize(width: 25, height: 25)), for: .normal)
181 | if let target = action.target, let action = action.action {
182 | button.addTarget(target, action: action, for: .touchUpInside)
183 | }
184 |
185 | // If possible, the button should be at least 40px wide for a good sized tap target
186 |
187 | let widthConstraint = button.widthAnchor.constraint(equalToConstant: itemWidth)
188 | widthConstraint.priority = .defaultHigh
189 | widthConstraint.isActive = true
190 |
191 | stackView.addArrangedSubview(button)
192 | }
193 | }
194 | }
195 | }
196 |
197 | extension UIImage {
198 |
199 | /// Scales the image to the given CGSize
200 | func scaled(toSize size: CGSize) -> UIImage {
201 | if self.size == size {
202 | return self
203 | }
204 |
205 | let newImage = UIGraphicsImageRenderer(size: size).image { context in
206 | self.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
207 | }
208 | return newImage.withRenderingMode(self.renderingMode)
209 | }
210 | }
211 |
212 | extension InputAssistantView: UICollectionViewDelegate {
213 |
214 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
215 | UIDevice.current.playInputClick()
216 | collectionView.deselectItem(at: indexPath, animated: true)
217 |
218 | self.delegate?.inputAssistantView(self, didSelectSuggestionAtIndex: indexPath.row)
219 | }
220 | }
221 |
222 | extension InputAssistantView: UIInputViewAudioFeedback {
223 |
224 | public var enableInputClicksWhenVisible: Bool { return true }
225 | }
226 |
227 |
--------------------------------------------------------------------------------
/build.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | name
6 | InputAssistant
7 | build
8 |
9 | buildSystem
10 | xcode
11 | buildArgs
12 |
13 | -project
14 | InputAssistant.xcodeproj
15 | -target
16 | libInputAssistant
17 |
18 | outputs
19 |
20 | libInputAssistant.a
21 |
22 |
23 | dependencies
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------