Skip to content

Commit bb8eb4e

Browse files
author
Apple
committed
Initial commit
0 parents  commit bb8eb4e

36 files changed

+2711
-0
lines changed

.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# See LICENSE folder for this sample’s licensing information.
2+
#
3+
# Apple sample code gitignore configuration.
4+
5+
# Finder
6+
.DS_Store
7+
8+
# Xcode - User files
9+
xcuserdata/
10+
11+
**/*.xcodeproj/project.xcworkspace/*
12+
!**/*.xcodeproj/project.xcworkspace/xcshareddata
13+
14+
**/*.xcodeproj/project.xcworkspace/xcshareddata/*
15+
!**/*.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
16+
17+
**/*.playground/playground.xcworkspace/*
18+
!**/*.playground/playground.xcworkspace/xcshareddata
19+
20+
**/*.playground/playground.xcworkspace/xcshareddata/*
21+
!**/*.playground/playground.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

Configuration/SampleCode.xcconfig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// See LICENSE folder for this sample’s licensing information.
3+
//
4+
// SampleCode.xcconfig
5+
//
6+
7+
// The `SAMPLE_CODE_DISAMBIGUATOR` configuration is to make it easier to build
8+
// and run a sample code project. Once you set your project's development team,
9+
// you'll have a unique bundle identifier. This is because the bundle identifier
10+
// is derived based on the 'SAMPLE_CODE_DISAMBIGUATOR' value. Do not use this
11+
// approach in your own projects—it's only useful for sample code projects because
12+
// they are frequently downloaded and don't have a development team set.
13+
SAMPLE_CODE_DISAMBIGUATOR=${DEVELOPMENT_TEAM}

LICENSE/LICENSE.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Copyright © 2022 Apple Inc.
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8+

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Enriching your text in text views
2+
3+
Add exclusion paths, text attachments, and text lists to your text, and render it with text views.
4+
5+
## Overview
6+
7+
- Note: This sample code project is associated with WWDC22 session [10090: What's new in TextKit and text views](https://developer.apple.com/wwdc22/10090/).

Shared/AttachmentSample.rtf

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{\rtf1\ansi\ansicpg1252\cocoartf2682
2+
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;\f1\fnil\fcharset0 Menlo-Regular;}
3+
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
4+
{\*\expandedcolortbl;;\csgray\c0\c0;}
5+
\margl1440\margr1440\vieww10920\viewh7800\viewkind0
6+
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
7+
8+
\f0\fs28 \cf0 View-based text attachments became available in iOS 15 and macOS Monterey. With view-based text attachments, you can use a
9+
\f1 UIView
10+
\f0 or an
11+
\f1 NSView
12+
\f0 as the text attachment. Events can be handled directly by the attachment view. This makes event handling with text attachments a whole lot easier, and it's only possible with TextKit 2.\
13+
\
14+
This code sample demonstrates how to use the text attachment view provider APIs with
15+
\f1 UITextView
16+
\f0 and
17+
\f1 NSTextView
18+
\f0 . The text view must be backed by TextKit 2 for this to work. \
19+
\
20+
Create a subclass of
21+
\f1 NSTextAttachmentViewProvider
22+
\f0 and overload its
23+
\f1 loadView()
24+
\f0 method to create your view-based attachment. \
25+
\
26+
In this override, provide the bounds of your view and set the property
27+
\f1 tracksTextAttachmentViewBounds
28+
\f0 to true, so that the framework consults the text attachment view provider to determine the bounds of the attachment.\
29+
\
30+
The
31+
\f1 intrinsicContentSize
32+
\f0 is used to determine the attachment bounds, so make sure your attachment view provides this value.\
33+
\
34+
Additionally, create a subclass of
35+
\f1 NSTextAttachment
36+
\f0 and override: \
37+
\
38+
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
39+
40+
\f1\fs24 \cf0 \cb2 viewProvider(for:location:textContainer:)
41+
\f0
42+
\fs28 \cb1 \
43+
\
44+
to return an instance of your custom
45+
\f1 NSTextAttachmentViewProvider
46+
\f0 . \
47+
\
48+
Also override: \
49+
\
50+
51+
\f1\fs24 image(bounds:attributes:location:textContainer:)
52+
\f0
53+
\fs28 \
54+
\
55+
to return
56+
\f1 nil
57+
\f0 . }

Shared/ExclusionPath.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
See LICENSE folder for this sample’s licensing information.
3+
4+
Abstract:
5+
A utility that hosts the exclusion paths.
6+
*/
7+
8+
import Foundation
9+
#if os(iOS)
10+
import UIKit
11+
#else
12+
import AppKit
13+
#endif
14+
15+
// Generate a bezier path for the image.
16+
struct ExclusionPath {
17+
#if os(iOS)
18+
static var uiBezierPath: UIBezierPath {
19+
let bezierPath = UIBezierPath()
20+
bezierPath.move(to: CGPoint(x: 24.5, y: 59.5))
21+
bezierPath.addCurve(to: CGPoint(x: -1.5, y: 154.5), controlPoint1: CGPoint(x: -9.24, y: 101.69), controlPoint2: CGPoint(x: -1.5, y: 154.5))
22+
bezierPath.addLine(to: CGPoint(x: 111.5, y: 154.5))
23+
bezierPath.addLine(to: CGPoint(x: 139.5, y: 183.5))
24+
bezierPath.addLine(to: CGPoint(x: 236.5, y: 88.5))
25+
bezierPath.addLine(to: CGPoint(x: 259.5, y: 110.5))
26+
bezierPath.addLine(to: CGPoint(x: 298.5, y: 75.5))
27+
bezierPath.addCurve(to: CGPoint(x: 244.5, y: 19.5), controlPoint1: CGPoint(x: 298.5, y: 75.5), controlPoint2: CGPoint(x: 255.5, y: 30.5))
28+
bezierPath.addCurve(to: CGPoint(x: 161.5, y: 14.5), controlPoint1: CGPoint(x: 237.74, y: 12.74), controlPoint2: CGPoint(x: 215.5, y: -4.5))
29+
bezierPath.addCurve(to: CGPoint(x: 111.5, y: 14.5), controlPoint1: CGPoint(x: 161.5, y: 14.5), controlPoint2: CGPoint(x: 119.21, y: 14.5))
30+
bezierPath.addCurve(to: CGPoint(x: 24.5, y: 59.5), controlPoint1: CGPoint(x: 76.19, y: 14.86), controlPoint2: CGPoint(x: 43.21, y: 36.11))
31+
bezierPath.close()
32+
var transform = CGAffineTransform.identity
33+
transform = transform.translatedBy(x: 45, y: 30)
34+
bezierPath.apply(transform)
35+
return bezierPath
36+
}
37+
#else
38+
static var nsBezierPath: NSBezierPath {
39+
let bezierPath = NSBezierPath()
40+
bezierPath.move(to: NSPoint(x: 25.5, y: 58.5))
41+
bezierPath.curve(to: NSPoint(x: -0.5, y: 153.5), controlPoint1: NSPoint(x: -8.24, y: 100.69), controlPoint2: NSPoint(x: -0.5, y: 153.5))
42+
bezierPath.line(to: NSPoint(x: 112.5, y: 153.5))
43+
bezierPath.line(to: NSPoint(x: 140.5, y: 182.5))
44+
bezierPath.line(to: NSPoint(x: 237.5, y: 87.5))
45+
bezierPath.line(to: NSPoint(x: 260.5, y: 109.5))
46+
bezierPath.line(to: NSPoint(x: 299.5, y: 74.5))
47+
bezierPath.curve(to: NSPoint(x: 245.5, y: 18.5), controlPoint1: NSPoint(x: 299.5, y: 74.5), controlPoint2: NSPoint(x: 256.5, y: 29.5))
48+
bezierPath.curve(to: NSPoint(x: 165.5, y: 23.5), controlPoint1: NSPoint(x: 234.5, y: 7.5), controlPoint2: NSPoint(x: 188.5, y: -6.5))
49+
bezierPath.curve(to: NSPoint(x: 112.5, y: 13.5), controlPoint1: NSPoint(x: 154.5, y: 15), controlPoint2: NSPoint(x: 132.5, y: 13.5))
50+
bezierPath.curve(to: NSPoint(x: 25.5, y: 58.5), controlPoint1: NSPoint(x: 77.19, y: 13.86), controlPoint2: NSPoint(x: 44.21, y: 35.11))
51+
bezierPath.close()
52+
var transform = AffineTransform.identity
53+
transform.translate(x: 45, y: 65)
54+
bezierPath.transform(using: transform)
55+
return bezierPath
56+
}
57+
#endif
58+
}

Shared/ExclusionPathSample.rtf

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{\rtf1\ansi\ansicpg1252\cocoartf2682
2+
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 Futura-Medium;\f1\fnil\fcharset0 Apple-Chancery;\f2\fnil\fcharset0 Georgia;
3+
\f3\fnil\fcharset0 HelveticaNeue;}
4+
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
5+
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;}
6+
\margl1440\margr1440\vieww14160\viewh11760\viewkind0
7+
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0
8+
9+
\f0\fs40 \cf2 Choosing the Right Extension Approach
10+
\f1 \
11+
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qj\partightenfactor0
12+
13+
\f2\fs24 \cf2 \
14+
\pard\pardeftab560\slleading20\qj\partightenfactor0
15+
16+
\f3\fs26 \cf0 We\'92ve looked at the built-in text controls, the components in the TextKit stack, and how to configure those components to achieve different effects. There\'92s a lot you can do with that knowledge already, but if you need even more, you\'92ll need to extend parts of TextKit. So now we\'92ll talk a little bit about choosing the right approach for that. And choosing the right approach is like building up your text toolbox. It\'92s like going to the hardware store because you need a hammer, and then when you get there, there\'92s this giant wall of hammers to choose from. You want to pick the hammer that can do the job, and the least expensive one that will do what you need. So these are the hammers that are available to us. Delegation is like your standard hammer with the claw on the end, it\'92s useful for multiple tasks. The delegates have a lot of commonly desired customization hooks and most of the time it\'92ll get the job done. Notifications is like a ball-peen hammer, which has a ball on the end instead of a claw. It\'92s more specialized and better suited for certain tasks than the standard hammer of delegation, but it\'92s not quite as versatile. And finally, subclassing is your sledgehammer. You can use the sledgehammer for just about anything you\'92d need a hammer for, but it\'92s probably overkill for a lot of things.}

Shared/ListSample.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Egg Sandwich
2+
Egg Sandwich No. 2
3+
Fried Egg Sandwich
4+
Ribbon Sandwich
5+
Egg and Lettuce Sandwich
6+
Egg and Olive Sandwich
7+
Egg and Cucumber Sandwich
8+
Egg and Brown Bread Sandwich
9+
Puritan Sandwich
10+
Cold Sandwich
11+
Montpelier Sandwich
12+
Japanese Egg Sandwich
13+
Brown Egg Sandwich
14+
Easter Sandwich
15+
Outing Sandwich
16+
Traveller's Sandwich
17+
Curried Egg and Oyster Sandwiches
18+
Watercress and Egg Sandwich
19+
Watercress and Egg Sandwich No. 2
20+
Green Pepper and Egg Sandwich

Shared/NSTextStorage+SampleText.swift

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
See LICENSE folder for this sample’s licensing information.
3+
4+
Abstract:
5+
The extension that loads the sample text.
6+
*/
7+
8+
#if os(iOS)
9+
import UIKit
10+
private let textColor = UIColor.label
11+
#else
12+
import AppKit
13+
private let textColor = NSColor.textColor
14+
#endif
15+
16+
extension NSTextStorage {
17+
// Load the surrounding text for the exclusion path.
18+
func loadExclusionPathSampleText() {
19+
guard let fileURL = Bundle.main.url(forResource: "ExclusionPathSample", withExtension: "rtf") else {
20+
fatalError("Failed to find ExclusionPathSample.rtf in the app bundle.")
21+
}
22+
let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf]
23+
guard let sampleText = try? NSAttributedString(url: fileURL, options: options, documentAttributes: nil) else {
24+
fatalError("Failed to load sample text.")
25+
}
26+
replaceCharacters(in: NSRange(location: 0, length: length), with: sampleText)
27+
addAttribute(.foregroundColor, value: textColor, range: NSRange(location: 0, length: length))
28+
}
29+
30+
// Set up a view-based text attachment.
31+
func loadAttachmentSampleText(textLayoutManager: NSTextLayoutManager) {
32+
33+
guard let fileURL = Bundle.main.url(forResource: "AttachmentSample", withExtension: "rtf") else {
34+
fatalError("Failed to find AttachmentSample.rtf in the app bundle.")
35+
}
36+
let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf]
37+
guard let sampleText = try? NSAttributedString(url: fileURL, options: options, documentAttributes: nil) else {
38+
fatalError("Failed to load sample text.")
39+
}
40+
replaceCharacters(in: NSRange(location: 0, length: length), with: sampleText)
41+
addAttribute(.foregroundColor, value: textColor, range: NSRange(location: 0, length: length))
42+
43+
let attachment = Attachment()
44+
attachment.allowsTextAttachmentView = true
45+
attachment.textLayoutManager = textLayoutManager
46+
let attachmentAttributedString = NSAttributedString(attachment: attachment)
47+
append(attachmentAttributedString)
48+
}
49+
50+
// Set up a sample list.
51+
func loadListSampleText() {
52+
guard let fileURL = Bundle.main.url(forResource: "ListSample", withExtension: "txt") else {
53+
fatalError("Failed to find ListSample.txt in the app bundle.")
54+
}
55+
guard let sampleText = try? String(String(contentsOf: fileURL)) else {
56+
fatalError("Failed to load sample text.")
57+
}
58+
let sampleContents = NSAttributedString(string: sampleText)
59+
replaceCharacters(in: NSRange(location: 0, length: length), with: sampleContents)
60+
61+
guard let paragraphStyle = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle else {
62+
return
63+
}
64+
let textList = NSTextList(markerFormat: .decimal, options: 0)
65+
paragraphStyle.textLists = [textList]
66+
67+
let attributes: [NSAttributedString.Key: Any] = [
68+
.paragraphStyle: paragraphStyle,
69+
.foregroundColor: textColor
70+
]
71+
72+
let listAttributedString = NSAttributedString(string: string, attributes: attributes)
73+
replaceCharacters(in: NSRange(location: 0, length: length), with: listAttributedString)
74+
}
75+
76+
// Change the list marker style for the text lists, if any.
77+
func setListMarkerStyle(styleIndex: Int) {
78+
guard styleIndex >= 0 && styleIndex < 3 else {
79+
return
80+
}
81+
let markerFormats: [NSTextList.MarkerFormat] = [.decimal, .disc, .square]
82+
let newMarkerFormat = markerFormats[styleIndex]
83+
84+
var pos = 0
85+
while pos < length {
86+
var pstyleRange = NSRange(location: NSNotFound, length: 0)
87+
let searchRange = NSRange(location: pos, length: length - pos)
88+
let value = attribute(.paragraphStyle, at: pos, longestEffectiveRange: &pstyleRange, in: searchRange)
89+
if let pstyle = value as? NSParagraphStyle {
90+
if !pstyle.textLists.isEmpty, let mutablePstyle = pstyle.mutableCopy() as? NSMutableParagraphStyle {
91+
mutablePstyle.textLists = mutablePstyle.textLists.map { textList in
92+
NSTextList(markerFormat: newMarkerFormat, options: 0)
93+
}
94+
addAttribute(.paragraphStyle, value: mutablePstyle, range: pstyleRange)
95+
}
96+
}
97+
pos = NSMaxRange(pstyleRange)
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)