├── .gitignore
├── Example.playground
├── Contents.swift
├── Resources
│ ├── lorem-ipsum.txt
│ └── mona.jpg
├── Sources
│ └── AttributedStringExtensions.swift
├── contents.xcplayground
├── playground.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── timeline.xctimeline
├── LICENSE
├── Package.swift
├── README.md
├── SubviewAttachingTextView.podspec
├── SubviewAttachingTextView.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ └── xcschemes
│ └── SubviewAttachingTextView.xcscheme
├── SubviewAttachingTextView.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── SubviewAttachingTextView
├── Info.plist
├── SubviewAttachingTextView.h
├── SubviewAttachingTextView.swift
├── SubviewAttachingTextViewBehavior.swift
├── SubviewTextAttachment.swift
└── TextAttachedViewProvider.swift
└── screenshot.gif
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | xcuserdata
3 | .swiftpm/xcode
4 |
--------------------------------------------------------------------------------
/Example.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | /**
2 | Available under the MIT License
3 | Copyright (c) 2017 Vlas Voloshin
4 | */
5 |
6 | import UIKit
7 | import SubviewAttachingTextView
8 |
9 | import WebKit
10 | import PlaygroundSupport
11 |
12 | // Handles tapping on the image view attachment
13 | class TapHandler: NSObject {
14 | @objc func handle(_ sender: UIGestureRecognizer!) {
15 | if let imageView = sender.view as? UIImageView {
16 | imageView.alpha = CGFloat(arc4random_uniform(1000)) / 1000.0
17 | }
18 | }
19 | }
20 |
21 |
22 | // Load lorem ipsum from a file and create a long attributed string out of it
23 | let loremIpsum = try! String(contentsOf: #fileLiteral(resourceName: "lorem-ipsum.txt"))
24 | let repeatedLorem = String(repeating: loremIpsum, count: 3)
25 | let text = NSAttributedString(string: repeatedLorem, attributes: [ .font : UIFont.systemFont(ofSize: 14) ])
26 |
27 | // Make paragraph styles for attachments
28 | let centerParagraphStyle = NSMutableParagraphStyle()
29 | centerParagraphStyle.alignment = .center
30 | centerParagraphStyle.paragraphSpacing = 10
31 | centerParagraphStyle.paragraphSpacingBefore = 10
32 |
33 | let leftParagraphStyle = NSMutableParagraphStyle()
34 | leftParagraphStyle.alignment = .left
35 | leftParagraphStyle.paragraphSpacing = 10
36 | leftParagraphStyle.paragraphSpacingBefore = 10
37 |
38 | // Create an image view with a tap recognizer
39 | let imageView = UIImageView(image: #imageLiteral(resourceName: "mona.jpg"))
40 | imageView.contentMode = .scaleAspectFit
41 | imageView.isUserInteractionEnabled = true
42 | let handler = TapHandler()
43 | let gestureRecognizer = UITapGestureRecognizer(target: handler, action: #selector(TapHandler.handle(_:)))
44 | imageView.addGestureRecognizer(gestureRecognizer)
45 |
46 | // Create an activity indicator view
47 | let spinner = UIActivityIndicatorView(style: .large)
48 | spinner.color = .black
49 | spinner.hidesWhenStopped = false
50 | spinner.startAnimating()
51 |
52 | // Create a text field
53 | let textField = UITextField()
54 | textField.borderStyle = .roundedRect
55 |
56 | // Create a web view, because why not
57 | let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
58 | webView.load(URLRequest(url: URL(string: "https://revealapp.com")!))
59 |
60 | // Create a text view to display the string and attachments
61 | let textView = SubviewAttachingTextView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
62 |
63 | // Add attachments to the string and set it on the text view
64 | // This example avoids evaluating the attachments or attributed strings with attachments in the Playground because Xcode crashes trying to decode attachment objects
65 | textView.attributedText = text
66 | .insertingAttachment(SubviewTextAttachment(view: imageView, size: CGSize(width: 256, height: 256)), at: 100, with: centerParagraphStyle)
67 | .insertingAttachment(SubviewTextAttachment(view: spinner), at: 200)
68 | .insertingAttachment(SubviewTextAttachment(view: UISwitch()), at: 300)
69 | .insertingAttachment(SubviewTextAttachment(view: textField, size: CGSize(width: 200, height: 44)), at: 400, with: leftParagraphStyle)
70 | .insertingAttachment(SubviewTextAttachment(view: UIDatePicker()), at: 500, with: centerParagraphStyle)
71 | .insertingAttachment(SubviewTextAttachment(view: webView), at: 600, with: centerParagraphStyle)
72 |
73 |
74 | // Run the playground indefinitely with the text view as the live view
75 | PlaygroundPage.current.needsIndefiniteExecution = true
76 | PlaygroundPage.current.liveView = textView
77 |
--------------------------------------------------------------------------------
/Example.playground/Resources/lorem-ipsum.txt:
--------------------------------------------------------------------------------
1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
2 |
--------------------------------------------------------------------------------
/Example.playground/Resources/mona.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vlas-voloshin/SubviewAttachingTextView/6025828b6c05fbf5529616a70c702f7425899fe5/Example.playground/Resources/mona.jpg
--------------------------------------------------------------------------------
/Example.playground/Sources/AttributedStringExtensions.swift:
--------------------------------------------------------------------------------
1 | /**
2 | Available under the MIT License
3 | Copyright (c) 2017 Vlas Voloshin
4 | */
5 |
6 | import UIKit
7 |
8 | public extension NSTextAttachment {
9 |
10 | convenience init(image: UIImage, size: CGSize? = nil) {
11 | self.init(data: nil, ofType: nil)
12 |
13 | self.image = image
14 | if let size = size {
15 | self.bounds = CGRect(origin: .zero, size: size)
16 | }
17 | }
18 |
19 | }
20 |
21 | public extension NSAttributedString {
22 |
23 | func insertingAttachment(_ attachment: NSTextAttachment, at index: Int, with paragraphStyle: NSParagraphStyle? = nil) -> NSAttributedString {
24 | let copy = self.mutableCopy() as! NSMutableAttributedString
25 | copy.insertAttachment(attachment, at: index, with: paragraphStyle)
26 |
27 | return copy.copy() as! NSAttributedString
28 | }
29 |
30 | func addingAttributes(_ attributes: [NSAttributedString.Key : Any]) -> NSAttributedString {
31 | let copy = self.mutableCopy() as! NSMutableAttributedString
32 | copy.addAttributes(attributes)
33 |
34 | return copy.copy() as! NSAttributedString
35 | }
36 |
37 | }
38 |
39 | public extension NSMutableAttributedString {
40 |
41 | func insertAttachment(_ attachment: NSTextAttachment, at index: Int, with paragraphStyle: NSParagraphStyle? = nil) {
42 | let plainAttachmentString = NSAttributedString(attachment: attachment)
43 |
44 | if let paragraphStyle = paragraphStyle {
45 | let attachmentString = plainAttachmentString
46 | .addingAttributes([ .paragraphStyle : paragraphStyle ])
47 | let separatorString = NSAttributedString(string: .paragraphSeparator)
48 |
49 | // Surround the attachment string with paragraph separators, so that the paragraph style is only applied to it
50 | let insertion = NSMutableAttributedString()
51 | insertion.append(separatorString)
52 | insertion.append(attachmentString)
53 | insertion.append(separatorString)
54 |
55 | self.insert(insertion, at: index)
56 | } else {
57 | self.insert(plainAttachmentString, at: index)
58 | }
59 | }
60 |
61 | func addAttributes(_ attributes: [NSAttributedString.Key : Any]) {
62 | self.addAttributes(attributes, range: NSRange(location: 0, length: self.length))
63 | }
64 |
65 | }
66 |
67 | public extension String {
68 |
69 | static let paragraphSeparator = "\u{2029}"
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/Example.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Example.playground/playground.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example.playground/playground.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example.playground/timeline.xctimeline:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Vlas Voloshin
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 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "SubviewAttachingTextView",
7 | platforms: [.iOS(.v8), .tvOS(.v9)],
8 | products: [
9 | .library(
10 | name: "SubviewAttachingTextView",
11 | targets: ["SubviewAttachingTextView"]
12 | ),
13 | ],
14 | dependencies: [],
15 | targets: [
16 | .target(
17 | name: "SubviewAttachingTextView",
18 | dependencies: [],
19 | path: "SubviewAttachingTextView"
20 | )
21 | ],
22 | swiftLanguageVersions: [.v5]
23 | )
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SubviewAttachingTextView
2 |
3 | [](https://github.com/Carthage/Carthage) []() []() []()
4 |
5 | `SubviewAttachingTextView` is a `UITextView` subclass that allows embedding subviews in its attributed text as text attachments. The library also provides a `SubviewAttachingTextViewBehavior` class that allows compositing the attachment behavior in existing `UITextView` subclasses.
6 |
7 | To see `SubviewAttachingTextView` in action, clone or download this repository and check out `Example.playground` inside `SubviewAttachingTextView.xcworkspace`.
8 |
9 |
10 |
11 | ## How it works
12 |
13 | `SubviewAttachingTextView` is built on top of `NSTextAttachment` and Text Kit APIs available through `UITextView`. A custom subclass of `NSTextAttachment` allows keeping a reference to the embedded views, and text view's `NSLayoutManager` and `NSTextContainer` allow querying for the attachments' bounds as they're laid out inside the text, taking into account all paragraph style properties. `NSLayoutManager`'s and `NSTextStorage`'s delegate callbacks allow implementing automatic layout and attachment updates.
14 |
15 | ## Requirements
16 |
17 | - Xcode 11.0+ (written in Swift 5.0).
18 | - For compatibility with Swift 4.2, use version 1.4.0.
19 | - For compatibility with Swift 4.0, use version 1.3.0.
20 | - For compatibility with Swift 3.x, use version 1.2.2.
21 | - iOS 8.0+
22 | - Compatible with Objective-C.
23 |
24 | ## Installation
25 |
26 | ### Carthage
27 |
28 | 1. Add the following line to your `Cartfile`:
29 |
30 | ```
31 | github "vlas-voloshin/SubviewAttachingTextView"
32 | ```
33 |
34 | 1. Follow the instructions outlined in [Carthage documentation](https://github.com/Carthage/Carthage/blob/master/README.md) to build and integrate the library into your app.
35 |
36 | ### CocoaPods
37 |
38 | 1. Add the following line to your `Podfile`:
39 |
40 | ```
41 | pod 'SubviewAttachingTextView'
42 | ```
43 |
44 | 2. Execute `pod install`.
45 |
46 | ### Manual
47 |
48 | 1. Download and copy the repository source files into your project, or add it as a submodule to your git repository.
49 | 1. Drag&drop `SubviewAttachingTextView.xcodeproj` into your project or workspace in Xcode.
50 | 1. In "General" tab of Project Settings → `Your Target`, you might find that Xcode has added a missing framework item in "Embedded Binaries". Delete it for now.
51 | 1. Still in "General" tab, add `SubviewAttachingTextView.framework` to "Embedded Binaries". This should also add it to "Linked Frameworks and Libraries".
52 |
53 | ## Usage
54 |
55 | There are two ways to integrate the subview attachment behavior in your text views; choose the one that fits your needs:
56 |
57 | ### As a drop-in `UITextView` replacement
58 |
59 | Simply change the class of `UITextView` you use to `SubviewAttachingTextView` (in Swift code) or `VVSubviewAttachingTextView` (in Objective-C code and Interface Builder).
60 |
61 | ### In custom subclasses
62 |
63 | You can easily integrate `SubviewAttachingTextViewBehavior` class into your custom `UITextView` subclass by following the implementation of `SubviewAttachingTextView`:
64 |
65 | 1. In both `init(frame:, textContainer:)` and `init?(coder:)` initializers, create a `SubviewAttachingTextViewBehavior` object (`VVSubviewAttachingTextViewBehavior` in Objective-C), store it in your text view instance.
66 | 1. Assign your text view to the behavior's `textView` property. It's a weak property, so this won't create a retain cycle.
67 | 1. Assing the behavior object to the text view's `layoutManager.delegate` and `textStorage.delegate`. This will enable the automatic attachment management.
68 | - If your custom text view already needs a different object to be its layout manager's or text storage's delegate, you can manually proxy the calls to `layoutManager(_:, didCompleteLayoutFor:, atEnd:)` and `textStorage(_:, didProcessEditing:, range:, changeInLength:)` delegate methods, but keep in mind that `SubviewAttachingTextViewBehavior` might require more methods in future.
69 | 1. Override the setter of `textContainerInset` property of your text view and make it call `layoutAttachedSubviews()` method of the attachment behavior. This ensures that the layout of attachments is updated when container insets are changed.
70 |
71 | ### Attaching subviews
72 |
73 | To embed a view in your text, do the following:
74 |
75 | - Create a `SubviewTextAttachment` instance (`VVSubviewTextAttachment` in Objective-C) and initialize it with a view object, optionally providing a size for the attachment.
76 | - If you don't provide a size explicitly, the attachment will attempt to use the view's fitting size (calculated using Auto Layout), falling back to the view's current size if a non-zero fitting size cannot be calculated.
77 | - Alternatively, initialize the attachment with a view provider: an object conforming to `TextAttachedViewProvider` protocol (`VVTextAttachedViewProvider` in Objective-C). Using a view provider allows rendering the attributed string with an attachment in multiple text views at the same time, with each text view receiving its own instance of a view representing the attachment from the view provider. `TextAttachedViewProvider` also allows customizing the attached view's size during text layout.
78 | - Create an `NSAttributedString` from the attachment using its `init(attachment:)` initializer.
79 | - Insert the resulting string into your attributed text and assign it on the text view (either `SubviewAttachingTextView` or your custom text view embedding a `SubviewAttachingTextViewBehavior`).
80 | - Adding and removing the attached subviews is managed automatically. If you want to remove an embedded subview, just remove the corresponding attachment from the text view's attributed text.
81 | - If your text view is editable, user will also be able to delete the attachment just like any other text.
82 |
83 | ## Limitations
84 |
85 | - Using pasteboard operations (copy, cut, paste) with subview attachments in editable text views is not supported at the moment.
86 | - Text layout is not automatically updated if the attached view's intrinsic size changes. At the moment, to change the size of an attached subview, the layout manager needs to be notified to invalidate layout and display for the corresponding character range, or the attachment needs to be removed from the text attributes and then added back.
87 |
88 | ## License
89 |
90 | This library is available under the MIT license. See the `LICENSE` file for more info.
91 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 |
3 | s.name = "SubviewAttachingTextView"
4 | s.version = "1.5.0"
5 | s.summary = "UITextView behavior and subclass that allow embedding subviews as attachments."
6 | s.homepage = "https://github.com/vlas-voloshin/SubviewAttachingTextView"
7 | s.license = "MIT"
8 | s.author = { "Vlas Voloshin" => "argentumko@gmail.com" }
9 | s.social_media_url = "https://twitter.com/argentumko"
10 | s.platform = :ios, "8.0"
11 | s.source = { :git => "https://github.com/vlas-voloshin/SubviewAttachingTextView.git", :tag => "#{s.version}" }
12 |
13 | s.source_files = "SubviewAttachingTextView/*.swift"
14 | s.requires_arc = true
15 | s.swift_version = '5.0'
16 |
17 | end
18 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 7D2DA4C81E3DF239004F2DAA /* SubviewAttachingTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = 7D2DA4C61E3DF239004F2DAA /* SubviewAttachingTextView.h */; settings = {ATTRIBUTES = (Public, ); }; };
11 | 7D2DA4D11E3DF28A004F2DAA /* SubviewAttachingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D2DA4CE1E3DF28A004F2DAA /* SubviewAttachingTextView.swift */; };
12 | 7D2DA4D21E3DF28A004F2DAA /* SubviewAttachingTextViewBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D2DA4CF1E3DF28A004F2DAA /* SubviewAttachingTextViewBehavior.swift */; };
13 | 7D2DA4D31E3DF28A004F2DAA /* SubviewTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D2DA4D01E3DF28A004F2DAA /* SubviewTextAttachment.swift */; };
14 | 7D44C0DC1E8574000022FFA4 /* TextAttachedViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D44C0DB1E8574000022FFA4 /* TextAttachedViewProvider.swift */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXFileReference section */
18 | 7D2DA4C31E3DF239004F2DAA /* SubviewAttachingTextView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SubviewAttachingTextView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
19 | 7D2DA4C61E3DF239004F2DAA /* SubviewAttachingTextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SubviewAttachingTextView.h; sourceTree = ""; };
20 | 7D2DA4C71E3DF239004F2DAA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
21 | 7D2DA4CE1E3DF28A004F2DAA /* SubviewAttachingTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubviewAttachingTextView.swift; sourceTree = ""; };
22 | 7D2DA4CF1E3DF28A004F2DAA /* SubviewAttachingTextViewBehavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubviewAttachingTextViewBehavior.swift; sourceTree = ""; };
23 | 7D2DA4D01E3DF28A004F2DAA /* SubviewTextAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubviewTextAttachment.swift; sourceTree = ""; };
24 | 7D44C0DB1E8574000022FFA4 /* TextAttachedViewProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAttachedViewProvider.swift; sourceTree = ""; };
25 | /* End PBXFileReference section */
26 |
27 | /* Begin PBXFrameworksBuildPhase section */
28 | 7D2DA4BF1E3DF239004F2DAA /* Frameworks */ = {
29 | isa = PBXFrameworksBuildPhase;
30 | buildActionMask = 2147483647;
31 | files = (
32 | );
33 | runOnlyForDeploymentPostprocessing = 0;
34 | };
35 | /* End PBXFrameworksBuildPhase section */
36 |
37 | /* Begin PBXGroup section */
38 | 7D2DA4B91E3DF239004F2DAA = {
39 | isa = PBXGroup;
40 | children = (
41 | 7D2DA4C51E3DF239004F2DAA /* SubviewAttachingTextView */,
42 | 7D2DA4C41E3DF239004F2DAA /* Products */,
43 | );
44 | sourceTree = "";
45 | };
46 | 7D2DA4C41E3DF239004F2DAA /* Products */ = {
47 | isa = PBXGroup;
48 | children = (
49 | 7D2DA4C31E3DF239004F2DAA /* SubviewAttachingTextView.framework */,
50 | );
51 | name = Products;
52 | sourceTree = "";
53 | };
54 | 7D2DA4C51E3DF239004F2DAA /* SubviewAttachingTextView */ = {
55 | isa = PBXGroup;
56 | children = (
57 | 7D2DA4CE1E3DF28A004F2DAA /* SubviewAttachingTextView.swift */,
58 | 7D2DA4CF1E3DF28A004F2DAA /* SubviewAttachingTextViewBehavior.swift */,
59 | 7D2DA4D01E3DF28A004F2DAA /* SubviewTextAttachment.swift */,
60 | 7D44C0DB1E8574000022FFA4 /* TextAttachedViewProvider.swift */,
61 | 7D2DA4C61E3DF239004F2DAA /* SubviewAttachingTextView.h */,
62 | 7D2DA4C71E3DF239004F2DAA /* Info.plist */,
63 | );
64 | path = SubviewAttachingTextView;
65 | sourceTree = "";
66 | };
67 | /* End PBXGroup section */
68 |
69 | /* Begin PBXHeadersBuildPhase section */
70 | 7D2DA4C01E3DF239004F2DAA /* Headers */ = {
71 | isa = PBXHeadersBuildPhase;
72 | buildActionMask = 2147483647;
73 | files = (
74 | 7D2DA4C81E3DF239004F2DAA /* SubviewAttachingTextView.h in Headers */,
75 | );
76 | runOnlyForDeploymentPostprocessing = 0;
77 | };
78 | /* End PBXHeadersBuildPhase section */
79 |
80 | /* Begin PBXNativeTarget section */
81 | 7D2DA4C21E3DF239004F2DAA /* SubviewAttachingTextView */ = {
82 | isa = PBXNativeTarget;
83 | buildConfigurationList = 7D2DA4CB1E3DF239004F2DAA /* Build configuration list for PBXNativeTarget "SubviewAttachingTextView" */;
84 | buildPhases = (
85 | 7D2DA4BE1E3DF239004F2DAA /* Sources */,
86 | 7D2DA4BF1E3DF239004F2DAA /* Frameworks */,
87 | 7D2DA4C01E3DF239004F2DAA /* Headers */,
88 | 7D2DA4C11E3DF239004F2DAA /* Resources */,
89 | );
90 | buildRules = (
91 | );
92 | dependencies = (
93 | );
94 | name = SubviewAttachingTextView;
95 | productName = SubviewAttachingTextView;
96 | productReference = 7D2DA4C31E3DF239004F2DAA /* SubviewAttachingTextView.framework */;
97 | productType = "com.apple.product-type.framework";
98 | };
99 | /* End PBXNativeTarget section */
100 |
101 | /* Begin PBXProject section */
102 | 7D2DA4BA1E3DF239004F2DAA /* Project object */ = {
103 | isa = PBXProject;
104 | attributes = {
105 | LastUpgradeCheck = 1110;
106 | ORGANIZATIONNAME = "Vlas Voloshin";
107 | TargetAttributes = {
108 | 7D2DA4C21E3DF239004F2DAA = {
109 | CreatedOnToolsVersion = 8.2.1;
110 | LastSwiftMigration = 1110;
111 | ProvisioningStyle = Automatic;
112 | };
113 | };
114 | };
115 | buildConfigurationList = 7D2DA4BD1E3DF239004F2DAA /* Build configuration list for PBXProject "SubviewAttachingTextView" */;
116 | compatibilityVersion = "Xcode 3.2";
117 | developmentRegion = en;
118 | hasScannedForEncodings = 0;
119 | knownRegions = (
120 | en,
121 | Base,
122 | );
123 | mainGroup = 7D2DA4B91E3DF239004F2DAA;
124 | productRefGroup = 7D2DA4C41E3DF239004F2DAA /* Products */;
125 | projectDirPath = "";
126 | projectRoot = "";
127 | targets = (
128 | 7D2DA4C21E3DF239004F2DAA /* SubviewAttachingTextView */,
129 | );
130 | };
131 | /* End PBXProject section */
132 |
133 | /* Begin PBXResourcesBuildPhase section */
134 | 7D2DA4C11E3DF239004F2DAA /* Resources */ = {
135 | isa = PBXResourcesBuildPhase;
136 | buildActionMask = 2147483647;
137 | files = (
138 | );
139 | runOnlyForDeploymentPostprocessing = 0;
140 | };
141 | /* End PBXResourcesBuildPhase section */
142 |
143 | /* Begin PBXSourcesBuildPhase section */
144 | 7D2DA4BE1E3DF239004F2DAA /* Sources */ = {
145 | isa = PBXSourcesBuildPhase;
146 | buildActionMask = 2147483647;
147 | files = (
148 | 7D2DA4D11E3DF28A004F2DAA /* SubviewAttachingTextView.swift in Sources */,
149 | 7D2DA4D31E3DF28A004F2DAA /* SubviewTextAttachment.swift in Sources */,
150 | 7D44C0DC1E8574000022FFA4 /* TextAttachedViewProvider.swift in Sources */,
151 | 7D2DA4D21E3DF28A004F2DAA /* SubviewAttachingTextViewBehavior.swift in Sources */,
152 | );
153 | runOnlyForDeploymentPostprocessing = 0;
154 | };
155 | /* End PBXSourcesBuildPhase section */
156 |
157 | /* Begin XCBuildConfiguration section */
158 | 7D2DA4C91E3DF239004F2DAA /* Debug */ = {
159 | isa = XCBuildConfiguration;
160 | buildSettings = {
161 | ALWAYS_SEARCH_USER_PATHS = NO;
162 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
163 | CLANG_ANALYZER_NONNULL = YES;
164 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
165 | CLANG_CXX_LIBRARY = "libc++";
166 | CLANG_ENABLE_MODULES = YES;
167 | CLANG_ENABLE_OBJC_ARC = YES;
168 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
169 | CLANG_WARN_BOOL_CONVERSION = YES;
170 | CLANG_WARN_COMMA = YES;
171 | CLANG_WARN_CONSTANT_CONVERSION = YES;
172 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
173 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
174 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
175 | CLANG_WARN_EMPTY_BODY = YES;
176 | CLANG_WARN_ENUM_CONVERSION = YES;
177 | CLANG_WARN_INFINITE_RECURSION = YES;
178 | CLANG_WARN_INT_CONVERSION = YES;
179 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
180 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
181 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
182 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
183 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
184 | CLANG_WARN_STRICT_PROTOTYPES = YES;
185 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
186 | CLANG_WARN_UNREACHABLE_CODE = YES;
187 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
188 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
189 | COPY_PHASE_STRIP = NO;
190 | CURRENT_PROJECT_VERSION = 1;
191 | DEBUG_INFORMATION_FORMAT = dwarf;
192 | ENABLE_STRICT_OBJC_MSGSEND = YES;
193 | ENABLE_TESTABILITY = YES;
194 | GCC_C_LANGUAGE_STANDARD = gnu99;
195 | GCC_DYNAMIC_NO_PIC = NO;
196 | GCC_NO_COMMON_BLOCKS = YES;
197 | GCC_OPTIMIZATION_LEVEL = 0;
198 | GCC_PREPROCESSOR_DEFINITIONS = (
199 | "DEBUG=1",
200 | "$(inherited)",
201 | );
202 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
203 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
204 | GCC_WARN_UNDECLARED_SELECTOR = YES;
205 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
206 | GCC_WARN_UNUSED_FUNCTION = YES;
207 | GCC_WARN_UNUSED_VARIABLE = YES;
208 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
209 | MTL_ENABLE_DEBUG_INFO = YES;
210 | ONLY_ACTIVE_ARCH = YES;
211 | SDKROOT = iphoneos;
212 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
213 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
214 | TARGETED_DEVICE_FAMILY = "1,2";
215 | VERSIONING_SYSTEM = "apple-generic";
216 | VERSION_INFO_PREFIX = "";
217 | };
218 | name = Debug;
219 | };
220 | 7D2DA4CA1E3DF239004F2DAA /* Release */ = {
221 | isa = XCBuildConfiguration;
222 | buildSettings = {
223 | ALWAYS_SEARCH_USER_PATHS = NO;
224 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
225 | CLANG_ANALYZER_NONNULL = YES;
226 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
227 | CLANG_CXX_LIBRARY = "libc++";
228 | CLANG_ENABLE_MODULES = YES;
229 | CLANG_ENABLE_OBJC_ARC = YES;
230 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
231 | CLANG_WARN_BOOL_CONVERSION = YES;
232 | CLANG_WARN_COMMA = YES;
233 | CLANG_WARN_CONSTANT_CONVERSION = YES;
234 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
235 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
236 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
237 | CLANG_WARN_EMPTY_BODY = YES;
238 | CLANG_WARN_ENUM_CONVERSION = YES;
239 | CLANG_WARN_INFINITE_RECURSION = YES;
240 | CLANG_WARN_INT_CONVERSION = YES;
241 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
242 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
243 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
244 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
245 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
246 | CLANG_WARN_STRICT_PROTOTYPES = YES;
247 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
248 | CLANG_WARN_UNREACHABLE_CODE = YES;
249 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
250 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
251 | COPY_PHASE_STRIP = NO;
252 | CURRENT_PROJECT_VERSION = 1;
253 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
254 | ENABLE_NS_ASSERTIONS = NO;
255 | ENABLE_STRICT_OBJC_MSGSEND = YES;
256 | GCC_C_LANGUAGE_STANDARD = gnu99;
257 | GCC_NO_COMMON_BLOCKS = YES;
258 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
259 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
260 | GCC_WARN_UNDECLARED_SELECTOR = YES;
261 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
262 | GCC_WARN_UNUSED_FUNCTION = YES;
263 | GCC_WARN_UNUSED_VARIABLE = YES;
264 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
265 | MTL_ENABLE_DEBUG_INFO = NO;
266 | SDKROOT = iphoneos;
267 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
268 | TARGETED_DEVICE_FAMILY = "1,2";
269 | VALIDATE_PRODUCT = YES;
270 | VERSIONING_SYSTEM = "apple-generic";
271 | VERSION_INFO_PREFIX = "";
272 | };
273 | name = Release;
274 | };
275 | 7D2DA4CC1E3DF239004F2DAA /* Debug */ = {
276 | isa = XCBuildConfiguration;
277 | buildSettings = {
278 | APPLICATION_EXTENSION_API_ONLY = YES;
279 | CLANG_ENABLE_MODULES = YES;
280 | CODE_SIGN_IDENTITY = "";
281 | DEFINES_MODULE = YES;
282 | DEVELOPMENT_TEAM = "";
283 | DYLIB_COMPATIBILITY_VERSION = 1;
284 | DYLIB_CURRENT_VERSION = 1;
285 | DYLIB_INSTALL_NAME_BASE = "@rpath";
286 | INFOPLIST_FILE = SubviewAttachingTextView/Info.plist;
287 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
288 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
289 | PRODUCT_BUNDLE_IDENTIFIER = com.argentumko.SubviewAttachingTextView;
290 | PRODUCT_NAME = "$(TARGET_NAME)";
291 | SKIP_INSTALL = YES;
292 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
293 | SWIFT_VERSION = 5.0;
294 | };
295 | name = Debug;
296 | };
297 | 7D2DA4CD1E3DF239004F2DAA /* Release */ = {
298 | isa = XCBuildConfiguration;
299 | buildSettings = {
300 | APPLICATION_EXTENSION_API_ONLY = YES;
301 | CLANG_ENABLE_MODULES = YES;
302 | CODE_SIGN_IDENTITY = "";
303 | DEFINES_MODULE = YES;
304 | DEVELOPMENT_TEAM = "";
305 | DYLIB_COMPATIBILITY_VERSION = 1;
306 | DYLIB_CURRENT_VERSION = 1;
307 | DYLIB_INSTALL_NAME_BASE = "@rpath";
308 | INFOPLIST_FILE = SubviewAttachingTextView/Info.plist;
309 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
310 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
311 | PRODUCT_BUNDLE_IDENTIFIER = com.argentumko.SubviewAttachingTextView;
312 | PRODUCT_NAME = "$(TARGET_NAME)";
313 | SKIP_INSTALL = YES;
314 | SWIFT_VERSION = 5.0;
315 | };
316 | name = Release;
317 | };
318 | /* End XCBuildConfiguration section */
319 |
320 | /* Begin XCConfigurationList section */
321 | 7D2DA4BD1E3DF239004F2DAA /* Build configuration list for PBXProject "SubviewAttachingTextView" */ = {
322 | isa = XCConfigurationList;
323 | buildConfigurations = (
324 | 7D2DA4C91E3DF239004F2DAA /* Debug */,
325 | 7D2DA4CA1E3DF239004F2DAA /* Release */,
326 | );
327 | defaultConfigurationIsVisible = 0;
328 | defaultConfigurationName = Release;
329 | };
330 | 7D2DA4CB1E3DF239004F2DAA /* Build configuration list for PBXNativeTarget "SubviewAttachingTextView" */ = {
331 | isa = XCConfigurationList;
332 | buildConfigurations = (
333 | 7D2DA4CC1E3DF239004F2DAA /* Debug */,
334 | 7D2DA4CD1E3DF239004F2DAA /* Release */,
335 | );
336 | defaultConfigurationIsVisible = 0;
337 | defaultConfigurationName = Release;
338 | };
339 | /* End XCConfigurationList section */
340 | };
341 | rootObject = 7D2DA4BA1E3DF239004F2DAA /* Project object */;
342 | }
343 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView.xcodeproj/xcshareddata/xcschemes/SubviewAttachingTextView.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
52 |
53 |
59 |
60 |
66 |
67 |
68 |
69 |
71 |
72 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0.1
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView/SubviewAttachingTextView.h:
--------------------------------------------------------------------------------
1 | //
2 | // SubviewAttachingTextView.h
3 | // SubviewAttachingTextView
4 | //
5 | // Created by Vlas Voloshin on 29/1/17.
6 | // Copyright © 2017 Vlas Voloshin. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for SubviewAttachingTextView.
12 | FOUNDATION_EXPORT double SubviewAttachingTextViewVersionNumber;
13 |
14 | //! Project version string for SubviewAttachingTextView.
15 | FOUNDATION_EXPORT const unsigned char SubviewAttachingTextViewVersionString[];
16 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView/SubviewAttachingTextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SubviewAttachingTextView.swift
3 | // SubviewAttachingTextView
4 | //
5 | // Created by Vlas Voloshin on 29/1/17.
6 | // Copyright © 2017 Vlas Voloshin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /**
12 | Simple subclass of UITextView that embeds a SubviewAttachingTextViewBehavior. A similar implementation can be used in custom subclasses.
13 | */
14 | @objc(VVSubviewAttachingTextView)
15 | open class SubviewAttachingTextView: UITextView {
16 |
17 | public override init(frame: CGRect, textContainer: NSTextContainer?) {
18 | super.init(frame: frame, textContainer: textContainer)
19 | self.commonInit()
20 | }
21 |
22 | public required init?(coder aDecoder: NSCoder) {
23 | super.init(coder: aDecoder)
24 | self.commonInit()
25 | }
26 |
27 | private let attachmentBehavior = SubviewAttachingTextViewBehavior()
28 |
29 | private func commonInit() {
30 | // Connect the attachment behavior
31 | self.attachmentBehavior.textView = self
32 | self.layoutManager.delegate = self.attachmentBehavior
33 | self.textStorage.delegate = self.attachmentBehavior
34 | }
35 |
36 | open override var textContainerInset: UIEdgeInsets {
37 | didSet {
38 | // Text container insets are used to convert coordinates between the text container and text view, so a change to these insets must trigger a layout update
39 | self.attachmentBehavior.layoutAttachedSubviews()
40 | }
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView/SubviewAttachingTextViewBehavior.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SubviewAttachingTextViewBehavior.swift
3 | // SubviewAttachingTextView
4 | //
5 | // Created by Vlas Voloshin on 29/1/17.
6 | // Copyright © 2017 Vlas Voloshin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /**
12 | Component class managing a text view behaviour that tracks all text attachments of SubviewTextAttachment class, automatically inserts/removes their views as text view subviews, and updates their layout according to the text view's layout manager.
13 | - Note: Follow the implementation of `SubviewAttachingTextView` for an example of adopting this behavior in your custom text view subclass.
14 | */
15 | @objc(VVSubviewAttachingTextViewBehavior)
16 | open class SubviewAttachingTextViewBehavior: NSObject, NSLayoutManagerDelegate, NSTextStorageDelegate {
17 |
18 | @objc
19 | open weak var textView: UITextView? {
20 | willSet {
21 | // Remove all managed subviews from the text view being disconnected
22 | self.removeAttachedSubviews()
23 | }
24 | didSet {
25 | // Synchronize managed subviews to the new text view
26 | self.updateAttachedSubviews()
27 | self.layoutAttachedSubviews()
28 | }
29 | }
30 |
31 | // MARK: Subview tracking
32 |
33 | private let attachedViews = NSMapTable.strongToStrongObjects()
34 | private var attachedProviders: Array {
35 | return Array(self.attachedViews.keyEnumerator()) as! Array
36 | }
37 |
38 | /**
39 | Adds attached views as subviews and removes subviews that are no longer attached. This method is called automatically when text view's text attributes change. Calling this method does not automatically perform a layout of attached subviews.
40 | */
41 | @objc
42 | open func updateAttachedSubviews() {
43 | guard let textView = self.textView else {
44 | return
45 | }
46 |
47 | // Collect all SubviewTextAttachment attachments
48 | let subviewAttachments = textView.textStorage.subviewAttachmentRanges.map { $0.attachment }
49 |
50 | // Remove views whose providers are no longer attached
51 | for provider in self.attachedProviders {
52 | if (subviewAttachments.contains { $0.viewProvider === provider } == false) {
53 | self.attachedViews.object(forKey: provider)?.removeFromSuperview()
54 | self.attachedViews.removeObject(forKey: provider)
55 | }
56 | }
57 |
58 | // Insert views that became attached
59 | let attachmentsToAdd = subviewAttachments.filter {
60 | self.attachedViews.object(forKey: $0.viewProvider) == nil
61 | }
62 | for attachment in attachmentsToAdd {
63 | let provider = attachment.viewProvider
64 | let view = provider.instantiateView(for: attachment, in: self)
65 | view.translatesAutoresizingMaskIntoConstraints = true
66 | view.autoresizingMask = [ ]
67 |
68 | textView.addSubview(view)
69 | self.attachedViews.setObject(view, forKey: provider)
70 | }
71 | }
72 |
73 | private func removeAttachedSubviews() {
74 | for provider in self.attachedProviders {
75 | self.attachedViews.object(forKey: provider)?.removeFromSuperview()
76 | }
77 | self.attachedViews.removeAllObjects()
78 | }
79 |
80 | // MARK: Layout
81 |
82 | /**
83 | Lays out all attached subviews according to the layout manager. This method is called automatically when layout manager finishes updating its layout.
84 | */
85 | @objc
86 | open func layoutAttachedSubviews() {
87 | guard let textView = self.textView else {
88 | return
89 | }
90 |
91 | let layoutManager = textView.layoutManager
92 | let scaleFactor = textView.window?.screen.scale ?? UIScreen.main.scale
93 |
94 | // For each attached subview, find its associated attachment and position it according to its text layout
95 | let attachmentRanges = textView.textStorage.subviewAttachmentRanges
96 | for (attachment, range) in attachmentRanges {
97 | guard let view = self.attachedViews.object(forKey: attachment.viewProvider) else {
98 | // A view for this provider is not attached yet??
99 | continue
100 | }
101 | guard view.superview === textView else {
102 | // Skip views which are not inside the text view for some reason
103 | continue
104 | }
105 | guard let attachmentRect = SubviewAttachingTextViewBehavior.boundingRect(forAttachmentCharacterAt: range.location, layoutManager: layoutManager) else {
106 | // Can't determine the rectangle for the attachment: just hide it
107 | view.isHidden = true
108 | continue
109 | }
110 |
111 | let convertedRect = textView.convertRectFromTextContainer(attachmentRect)
112 | let integralRect = CGRect(origin: convertedRect.origin.integral(withScaleFactor: scaleFactor),
113 | size: convertedRect.size)
114 |
115 | UIView.performWithoutAnimation {
116 | view.frame = integralRect
117 | view.isHidden = false
118 | }
119 | }
120 | }
121 |
122 | private static func boundingRect(forAttachmentCharacterAt characterIndex: Int, layoutManager: NSLayoutManager) -> CGRect? {
123 | let glyphRange = layoutManager.glyphRange(forCharacterRange: NSMakeRange(characterIndex, 1), actualCharacterRange: nil)
124 | let glyphIndex = glyphRange.location
125 | guard glyphIndex != NSNotFound && glyphRange.length == 1 else {
126 | return nil
127 | }
128 |
129 | let attachmentSize = layoutManager.attachmentSize(forGlyphAt: glyphIndex)
130 | guard attachmentSize.width > 0.0 && attachmentSize.height > 0.0 else {
131 | return nil
132 | }
133 |
134 | let lineFragmentRect = layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil)
135 | let glyphLocation = layoutManager.location(forGlyphAt: glyphIndex)
136 | guard lineFragmentRect.width > 0.0 && lineFragmentRect.height > 0.0 else {
137 | return nil
138 | }
139 |
140 | return CGRect(origin: CGPoint(x: lineFragmentRect.minX + glyphLocation.x,
141 | y: lineFragmentRect.minY + glyphLocation.y - attachmentSize.height),
142 | size: attachmentSize)
143 | }
144 |
145 | // MARK: NSLayoutManagerDelegate
146 |
147 | public func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) {
148 | if layoutFinishedFlag {
149 | self.layoutAttachedSubviews()
150 | }
151 | }
152 |
153 | // MARK: NSTextStorageDelegate
154 |
155 | public func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
156 | if editedMask.contains(.editedAttributes) {
157 | self.updateAttachedSubviews()
158 | }
159 | }
160 |
161 | }
162 |
163 | // MARK: - Extensions
164 |
165 | public extension UITextView {
166 |
167 | @objc(vv_convertPointToTextContainer:)
168 | func convertPointToTextContainer(_ point: CGPoint) -> CGPoint {
169 | let insets = self.textContainerInset
170 | return CGPoint(x: point.x - insets.left, y: point.y - insets.top)
171 | }
172 |
173 | @objc(vv_convertPointFromTextContainer:)
174 | func convertPointFromTextContainer(_ point: CGPoint) -> CGPoint {
175 | let insets = self.textContainerInset
176 | return CGPoint(x: point.x + insets.left, y: point.y + insets.top)
177 | }
178 |
179 | @objc(vv_convertRectToTextContainer:)
180 | func convertRectToTextContainer(_ rect: CGRect) -> CGRect {
181 | let insets = self.textContainerInset
182 | return rect.offsetBy(dx: -insets.left, dy: -insets.top)
183 | }
184 |
185 | @objc(vv_convertRectFromTextContainer:)
186 | func convertRectFromTextContainer(_ rect: CGRect) -> CGRect {
187 | let insets = self.textContainerInset
188 | return rect.offsetBy(dx: insets.left, dy: insets.top)
189 | }
190 |
191 | }
192 |
193 | private extension CGPoint {
194 |
195 | func integral(withScaleFactor scaleFactor: CGFloat) -> CGPoint {
196 | guard scaleFactor > 0.0 else {
197 | return self
198 | }
199 |
200 | return CGPoint(x: round(self.x * scaleFactor) / scaleFactor,
201 | y: round(self.y * scaleFactor) / scaleFactor)
202 | }
203 |
204 | }
205 |
206 | private extension NSAttributedString {
207 |
208 | var subviewAttachmentRanges: [(attachment: SubviewTextAttachment, range: NSRange)] {
209 | var ranges = [(SubviewTextAttachment, NSRange)]()
210 |
211 | let fullRange = NSRange(location: 0, length: self.length)
212 | self.enumerateAttribute(NSAttributedString.Key.attachment, in: fullRange) { value, range, _ in
213 | if let attachment = value as? SubviewTextAttachment {
214 | ranges.append((attachment, range))
215 | }
216 | }
217 |
218 | return ranges
219 | }
220 |
221 | }
222 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView/SubviewTextAttachment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SubviewTextAttachment.swift
3 | // SubviewAttachingTextView
4 | //
5 | // Created by Vlas Voloshin on 29/1/17.
6 | // Copyright © 2017 Vlas Voloshin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /**
12 | Describes a custom text attachment object containing a view. SubviewAttachingTextViewBehavior tracks attachments of this class and automatically manages adding and removing subviews in its text view.
13 | */
14 | @objc(VVSubviewTextAttachment)
15 | open class SubviewTextAttachment: NSTextAttachment {
16 |
17 | @objc
18 | public let viewProvider: TextAttachedViewProvider
19 |
20 | /**
21 | Initialize the attachment with a view provider.
22 | */
23 | @objc
24 | public init(viewProvider: TextAttachedViewProvider) {
25 | self.viewProvider = viewProvider
26 | super.init(data: nil, ofType: nil)
27 | }
28 |
29 | /**
30 | Initialize the attachment with a view and an explicit size.
31 | - Warning: If an attributed string that includes the returned attachment is used in more than one text view at a time, the behavior is not defined.
32 | */
33 | @objc
34 | public convenience init(view: UIView, size: CGSize) {
35 | let provider = DirectTextAttachedViewProvider(view: view)
36 | self.init(viewProvider: provider)
37 | self.bounds = CGRect(origin: .zero, size: size)
38 | }
39 |
40 | /**
41 | Initialize the attachment with a view and use its current fitting size as the attachment size.
42 | - Note: If the view does not define a fitting size, its current bounds size is used.
43 | - Warning: If an attributed string that includes the returned attachment is used in more than one text view at a time, the behavior is not defined.
44 | */
45 | @objc
46 | public convenience init(view: UIView) {
47 | self.init(view: view, size: view.textAttachmentFittingSize)
48 | }
49 |
50 | // MARK: - NSTextAttachmentContainer
51 |
52 | open override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {
53 | return self.viewProvider.bounds(for: self, textContainer: textContainer, proposedLineFragment: lineFrag, glyphPosition: position)
54 | }
55 |
56 | open override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage? {
57 | return nil
58 | }
59 |
60 | // MARK: NSCoding
61 |
62 | public required init?(coder aDecoder: NSCoder) {
63 | fatalError("SubviewTextAttachment cannot be decoded.")
64 | }
65 |
66 | }
67 |
68 | // MARK: - Internal view provider
69 |
70 | final internal class DirectTextAttachedViewProvider: TextAttachedViewProvider {
71 |
72 | let view: UIView
73 |
74 | init(view: UIView) {
75 | self.view = view
76 | }
77 |
78 | func instantiateView(for attachment: SubviewTextAttachment, in behavior: SubviewAttachingTextViewBehavior) -> UIView {
79 | return self.view
80 | }
81 |
82 | func bounds(for attachment: SubviewTextAttachment, textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint) -> CGRect {
83 | return attachment.bounds
84 | }
85 |
86 | }
87 |
88 | // MARK: - Extensions
89 |
90 | private extension UIView {
91 |
92 | @objc(vv_attachmentFittingSize)
93 | var textAttachmentFittingSize: CGSize {
94 | let fittingSize = self.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
95 | if fittingSize.width > 1e-3 && fittingSize.height > 1e-3 {
96 | return fittingSize
97 | } else {
98 | return self.bounds.size
99 | }
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/SubviewAttachingTextView/TextAttachedViewProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextAttachedViewProvider.swift
3 | // SubviewAttachingTextView
4 | //
5 | // Created by Vlas Voloshin on 25/3/17.
6 | // Copyright © 2017 Vlas Voloshin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /**
12 | Describes a protocol that provides views inserted as subviews into text views that render a `SubviewTextAttachment`, and customizes their layout.
13 | - Note: Implementing this protocol is encouraged over providing a single view in a `SubviewTextAttachment`, because it allows attributed strings with subview attachments to be rendered in multiple text views at the same time: each text view would get its own subview that corresponds to the attachment.
14 | */
15 | @objc(VVTextAttachedViewProvider)
16 | public protocol TextAttachedViewProvider: class {
17 |
18 | /**
19 | Returns a view that corresponds to the specified attachment.
20 | - Note: Each `SubviewAttachingTextViewBehavior` caches instantiated views until the attachment leaves the text container.
21 | */
22 | @objc(instantiateViewForAttachment:inBehavior:)
23 | func instantiateView(for attachment: SubviewTextAttachment, in behavior: SubviewAttachingTextViewBehavior) -> UIView
24 |
25 | /**
26 | Returns the layout bounds of the view that corresponds to the specified attachment.
27 | - Note: Return `attachment.bounds` for default behavior. See `NSTextAttachmentContainer.attachmentBounds(for:, proposedLineFragment:, glyphPosition:, characterIndex:)` method for more details.
28 | */
29 | @objc(boundsForAttachment:textContainer:proposedLineFragment:glyphPosition:)
30 | func bounds(for attachment: SubviewTextAttachment, textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint) -> CGRect
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vlas-voloshin/SubviewAttachingTextView/6025828b6c05fbf5529616a70c702f7425899fe5/screenshot.gif
--------------------------------------------------------------------------------