Slide 1

Slide 1 text

TextKit 2 ࣌୅ͷ iOS ͷΩʔϘʔυͱςΩετೖྗ ͱදࣔͷ͢΂ͯ @niw 9/2/2023 — Tokyo, Japan iOSDC Japan 2023

Slide 2

Slide 2 text

Yoshimasa Niwa @niw

Slide 3

Slide 3 text

࣌͸ 2020 ೥…

Slide 4

Slide 4 text

iOS ͷΩʔϘʔυͱจࣈೖྗͷ͢΂ͯ @niw 09/21/2020 — Online iOSDC Japan 2020

Slide 5

Slide 5 text

iOS ͷςΩετදࣔͷ͢΂ͯ @niw 9/2/2023 — Tokyo, Japan iOSDC Japan 2023 TextKit 2 ରԠ൛ New!

Slide 6

Slide 6 text

TextKit 1

Slide 7

Slide 7 text

ಛ௃ TextKit 1 ݹ͔͘Βଓ͘ NSLayoutManager Λத৺ͱͨ͠ΫϥεͰߏ ੒͞ΕΔ API ΋ͱ΋ͱ TextKit 1 ͳͲͱ͸ݺ͹Ε͍ͯͳ͔͕ͬͨɺTextKit 2 ͷ API ͱ۠ผ͢ΔͨΊʹͦ͏ݺ͹ΕΔΑ͏ʹͳͬͨ

Slide 8

Slide 8 text

NSTextStorage NSLayoutManager NSTextContainer UITextView Ϗϡʔ ίϯτϩʔϥʔ Ϟσϧ

Slide 9

Slide 9 text

let textStorage = NSTextStorage() let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) let textContainer = NSTextContainer() layoutManager.addTextContainer(textContainer) let textView = UITextView( 
 frame: view.bounds, 
 textContainer: textContainer 
 )

Slide 10

Slide 10 text

ಛ௃ TextKit 1 ૢ࡞ͷ࠷খ୯Ґ͸จࣈͷཁૉ Ͱ͋ΔάϦϑ Ϟσϧʹ૬౰͢Δจࣈྻͷൣ ғͱରԠ͢ΔάϦϑͷૢ࡞͕ Մೳ ͋ A ಟ

Slide 11

Slide 11 text

let layoutManager = textView.layoutManager let glyphRange = layoutManager.glyphRange( forCharacterRange: NSRange( 
 location: 0, length: 1 
 ), actualCharacterRange: nil ) let rect = layoutManager.boundingRect( forGlyphRange: glyphRange, in: textView.textContainer )

Slide 12

Slide 12 text

ར༻ྫ

Slide 13

Slide 13 text

U+3042 U+3044 U+3046 U+3048 U+304A

Slide 14

Slide 14 text

U+0C85 U+0C95 U+0CCD U+0C9F U+0CCB U+0CAC U+0CB0 U+0CCD

Slide 15

Slide 15 text

Meet TextKit 2 (WWDC 21) 
 https://developer.apple.com/videos/play/wwdc2021/10061/

Slide 16

Slide 16 text

ͱ͍͏͔…

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

·ͱΊ TextKit 1 จࣈྻͷൣғͱϏϡʔͷۣܗ͕άϦϑ୯ҐͰରԠ෇ΒΕΔ จࣈྻͷൣғͱάϦϑͷൣғ͕ରԠ͚ͮΒΕͳ͍ݴޠ͕ଘࡏ ͍ͯ͠Δ ਖ਼֬ʹಈ࡞͠ͳ͍έʔε͕͋Δ

Slide 20

Slide 20 text

TextKit 2

Slide 21

Slide 21 text

ಛ௃ TextKit 2 NSTextLayoutManager Λத৺ʹ৽͘͠௥Ճ͞Εͨ API Ͱɺ TextKit 1ͱ͸ಠཱͯ͠ಈ࡞͢Δ ΑΓਖ਼֬ʹಈ࡞͢ΔΑ͏ʹͳͬͨ ෆมͷ஋ΫϥεΛ࢖͏

Slide 22

Slide 22 text

NSTextStorage NSLayoutManager NSTextContainer UITextView Ϗϡʔ Ϟσϧ ίϯτϩʔϥʔ

Slide 23

Slide 23 text

NSTextContentStorage NSTextLayoutManager NSTextContainer UITextView Ϗϡʔ Ϟσϧ ίϯτϩʔϥʔ

Slide 24

Slide 24 text

let textContentStorage = NSTextContentStorage() let textLayoutManager = NSTextLayoutManager() textContentStorage 
 .addTextLayoutManager(textLayoutManager) let textContainer = NSTextContainer() textLayoutManager.textContainer = textContainer let textView = UITextView( 
 frame: view.bounds, 
 textContainer: textContainer )

Slide 25

Slide 25 text

จࣈྻͱϨΠΞ΢τ

Slide 26

Slide 26 text

ಛ௃ TextKit 2 ߴ౓ʹந৅Խ͞ΕͨΠϯλʔϑΣΠε TextKit 1 ͷૢ࡞ͷ࠷খ୯ҐͰ͋ͬͨάϦϑ͸Ӆṭ͞ΕɺάϦ ϑͷ৘ใ΍ͦͷۣܗͳͲ͸Ұ੾खʹೖΒͳ͘ͳͬͨ TextKit 2 ͷૢ࡞ͷ࠷খ୯Ґ͸ߦ

Slide 27

Slide 27 text

NSTextStorage
 NSAttributedString NSTextContentStorage NSTextLayoutFragment NSTextLineFragment

Slide 28

Slide 28 text

TextKit 2 จࣈྻͱϨΠΞ΢τ ೚ҙͷจࣈྻͷൣғ͔ΒɺߦΑΓࡉ͔͍ը໘্ͷۣܗΛٻΊ Δ͜ͱ͸Ͱ͖ͳ͍ ը໘্ͷ೚ҙͷҐஔ͔Βจࣈྻ্ͷൣғʹରԠ͢Δ͔Ͳ͏͔ ͱ͍ͬͨώοτςετ͸ಠࣗͷΞτϦϏϡʔτΛ࢖͏͜ͱͰ ͋Δఔ౓Մೳ

Slide 29

Slide 29 text

จࣈྻ͔ΒϨΠΞ΢τ

Slide 30

Slide 30 text

TextKit 2 จࣈྻ͔ΒϨΠΞ΢τ NSTextLayoutManager ͷ enumerateTextLayoutFragments(from:options:u sing) ͰจࣈྻͷҐஔ͔Β NSTextLayoutFragment Λ Ұཡ textLineFragments Ͱ NSTextLineFragment ΛҰཡ NSTextLineFragment ͷ typographicBounds Λ࢖͏

Slide 31

Slide 31 text

όά͕͋Γ·͢ typographicBounds typographicBounds ͸ layoutFragmentFrame ͷ origin ͔Βͷ࠲ඪͷ͸ͣ iOS 17 Ͱ͸ͦͷ௨Γʹͳ͍ͬͯΔ iOS 16 Ͱ͸ x ํ޲͚ͩ 0.0 ͔Βͷ࠲ඪʹͳ͍ͬͯΔ

Slide 32

Slide 32 text

ޗഐ͸ೣͰ͋Δɻ໊લ͸·ͩແ ͍ɻ 
 Ͳ͜ͰੜΕ͔ͨͱΜͱݟ౰͕ͭ ͔͵ɻԿͰ΋ബ҉͍͡Ί͡Ί͠ ͨॴͰχϟʔχϟʔٽ͍͍ͯͨ ࣄ͚ͩ͸هԱ͍ͯ͠Δɻ Text Container layoutFragmentFrame typographicBounds

Slide 33

Slide 33 text

if #available(iOS 16.0, *) { let textLayoutManager = textView.textLayoutManager! let textContentStorage = textLayoutManager.textContentManager textLayoutManager.enumerateTextLayoutFragments( 
 from: textContentStorage?.documentRange.location 
 ) { layoutFragment in for lineFragment in layoutFragment.textLineFragments { let bounds: CGRect if #available(iOS 17.0, *) { bounds = lineFragment.typographicBounds } else { bounds = lineFragment.typographicBounds.offsetBy( dx: -layoutFragment.layoutFragmentFrame.minX, dy: 0.0 ) } // Use `bounds`... } return true }

Slide 34

Slide 34 text

ϨΠΞ΢τ͔Βจࣈྻ

Slide 35

Slide 35 text

TextKit 2 ϨΠΞ΢τ͔Βจࣈྻ NSTextLayoutManager ͷ textLayoutFragment(for:) Λ࢖ͬͯ NSLayoutFragment ΛऔಘɺlineFragments Λ ࢖ͬͯͦΕͧΕͷ typographicBounds Λώοτςετ NSLineFragment ͷ characterIndex(for:) Λ࢖ͬͯจ ࣈྻ಺ͷҐஔΛऔಘ attributes(at:effectiveRange:)Ͱจࣈྻ্ͷӨڹൣ ғΛऔಘ

Slide 36

Slide 36 text

όά͕͋Γ·͢ typographicBounds typographicBounds ͸ layoutFragmentFrame ͷ origin ͔Βͷ࠲ඪͷ͸ͣ iOS 17 Ͱ͸ͦͷ௨Γʹͳ͍ͬͯΔ iOS 16 Ͱ͸ x ํ޲͚ͩ 0.0 ͔Βͷ࠲ඪʹͳ͍ͬͯΔ

Slide 37

Slide 37 text

let attributedText = NSMutableAttributedString( 
 "ޗഐ͸ೣͰ͋Δɻ໊લ͸·ͩແ͍" 
 ) let userNameAttributeKey = NSAttributedString.Key( 
 rawValue: "UserName" 
 ) 
 attributedText.addAttribute( 
 userNameAttributeKey, 
 value: "@neko", 
 range: NSRange(location: 0, length: 2) 
 ) 
 
 textView.attributedText = attributedText 
 
 ...

Slide 38

Slide 38 text

let pointInTextView = // ... let pointInTextContainer = CGPoint( x: pointInTextView.x - textView.textContainerInset.left, y: pointInTextView.y - textView.textContainerInset.top ) 
 if let layoutFragment = textView.textLayoutManager? 
 .textLayoutFragment( 
 for: pointInTextContainer 
 ) { let pointInLayoutFragment = CGPoint( x: pointInTextContainer.x - 
 layoutFragment.layoutFragmentFrame.minX, y: pointInTextContainer.y - 
 layoutFragment.layoutFragmentFrame.minY ) ...

Slide 39

Slide 39 text

... for lineFragment in layoutFragment.textLineFragments { let typographicBounds: CGRect if #available(iOS 17.0, *) { typographicBounds = lineFragment.typographicBounds } else { typographicBounds = 
 lineFragment.typographicBounds.offsetBy( dx: -layoutFragment.layoutFragmentFrame.minX, dy: 0.0 ) } ...

Slide 40

Slide 40 text

... 
 if typographicBounds.contains(pointInLayoutFragment) { let pointInLineFragment = CGPoint( x: pointInLayoutFragment.x - typographicBounds.minX, y: pointInLayoutFragment.y - typographicBounds.minY ) let characterIndex = lineFragment.characterIndex( 
 for: pointInLineFragment 
 ) var characterRange = 
 NSRange(location: NSNotFound, length: 0) let attributes = textView.attributedText.attributes( 
 at: characterIndex, 
 effectiveRange: &characterRange 
 ) if let userName = attributes[userNameAttributeKey] { // Use `userName` and `characterRange` ... } } } }

Slide 41

Slide 41 text

ͭΒΈ͕ଟ͍ͷͰ͕͢…

Slide 42

Slide 42 text

ศརʹͳͬͨػೳ

Slide 43

Slide 43 text

TextKit 2 ͷศརʹͳͬͨػೳ NSTextAttachmentViewProvider NSTextAttachmentViewProvider Λ࢖ͬͯ೚ҙͷϏϡʔΛ؆୯ ʹจதʹຒΊࠐΊΔΑ͏ʹͳͬͨ TextKit 1 Ͱ͸ը૾͚͚ͩͩͰɺ೚ҙͷϏϡʔ͸ؤுͬͯࣗ෼ ͰϨΠΞ΢τ͢Δඞཁ͕͋ͬͨ

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

final class PlayerView: UIView { // ... override init(frame: CGRect) { // ... let player = AVQueuePlayer(playerItem: playerItem) playerLayer = AVPlayerLayer(player: player) playerLayer.videoGravity = .resizeAspectFill super.init(frame: frame) layer.addSublayer(playerLayer) player.play() } 
 // ... }

Slide 46

Slide 46 text

final class AttachmentViewProvider: NSTextAttachmentViewProvider { override func loadView() { view = PlayerView() } override func attachmentBounds( for attributes: [NSAttributedString.Key : Any], location: NSTextLocation, textContainer: NSTextContainer?, proposedLineFragment: CGRect, position: CGPoint ) -> CGRect { CGRect( 
 x: 0.0, 
 y: 0.0, 
 width: proposedLineFragment.width, 
 height: 100.0 
 ) } }

Slide 47

Slide 47 text

final class Attachment: NSTextAttachment { override func viewProvider( for parentView: UIView?, location: NSTextLocation, textContainer: NSTextContainer? ) -> NSTextAttachmentViewProvider? { let viewProvider = AttachmentViewProvider( textAttachment: self, parentView: parentView, textLayoutManager: textContainer?.textLayoutManager, location: location ) viewProvider.tracksTextAttachmentViewBounds = true return viewProvider } }

Slide 48

Slide 48 text

let attributedText = NSMutableAttributedString() 
 attributedText.append(NSAttributedString( 
 string: "ޗഐ͸ೣͰ͋Δɻ", 
 attributes: attributes 
 )) attributedText.append(NSAttributedString( 
 attachment: Attachment() 
 )) attributedText.append(NSAttributedString( 
 string: "໊લ͸·ͩແ͍", attributes: attributes 
 ))

Slide 49

Slide 49 text

஫ҙ఺ NSTextAttachmentViewProvider NSAttributedString ʹΑͬͯɺຒΊࠐΉϏϡʔΛؚΊ ͯϦιʔε͕ॴ༗͞ΕΔ ϝϞϦϦʔΫ͕ى͜Βͳ͍Α͏ʹɺ॥؀ࢀরʹ஫ҙ

Slide 50

Slide 50 text

ޓ׵ੑ

Slide 51

Slide 51 text

ରԠ͢Δ Ϗϡʔ iOS ͷόʔδϣϯ iOS 14 iOS 15 iOS 16 Ҏ߱ UITextField 􀁣 􀁣 UITextView 􀁣

Slide 52

Slide 52 text

UITextView ࣗಈϑΥʔϧόοΫ iOS 15 ͸ TextKit 1 ͚ͩ iOS 16 Ҏ߱͸ TextKit 2 ͕σϑΥϧτ ͔͠͠ɺTextKit 1 ͷ API ʹͪΐͬͱͰ΋৮ΕΔͱ TextKit 1 ʹ੾ΓସΘͬͯ͠·͏ Ұ౓ TextKit 1 ʹͳΔͱɺTextKit 2 ʹ͸໭Βͳ͍

Slide 53

Slide 53 text

TextKit 1 ͚ͩΛ࢖͏৔߹ TextKit Λ࢖͏ ॳظԽ࣌ʹ TextKit 1 Λ࢖͏Α͏ʹ͢Δ ࣮ߦ࣌ʹίετͷ͔͔ΔࣗಈϑΥʔϧόοΫΛىͤ͜͞ͳ͍

Slide 54

Slide 54 text

TextKit 2 Λ࢖͏৔߹ TextKit Λ࢖͏ TextKit 2 ͚ͩΛ࢖͏৔߹ UITextView Λ೿ੜͤͯ͞ɺTextKit 1 ͷ API Λ௵͢ 
 _UITextViewEnablingCompatibilityMode Ͱ֬ೝ ࣮ߦ࣌ʹ൑அ͢Δ৔߹ #available(iOS 16.0, *) Ͱ iOS 16 Λ࣮ߦ࣌ʹࣝผ

Slide 55

Slide 55 text

// TextKit 1 ͚ͩΛ࢖͏৔߹ let textView = UITextView(usingTextLayoutManager: false) // TextKit 2 ͚ͩΛ࢖͏৔߹ final class TextView: UITextView { override var layoutManager: NSLayoutManager { fatalError("ͩΊͰ͢") } }

Slide 56

Slide 56 text

// ৚݅ʹΑͬͯ੾Γସ͑Δ৔߹ let textView: UITextView if #available(iOS 16.0, *) { textView = UITextView(usingTextLayoutManager: true) 
 } else { 
 textView = UITextView() } 
 final class TextView: UITextView { override var layoutManager: NSLayoutManager { if #available(iOS 16.0, *) { fatalError("ͩΊͰ͢") } else { super.layoutManager } } }

Slide 57

Slide 57 text

·ͱΊ TextKit 2 UITextView Ͱ iOS 16 Ҏ߱Ͱ TextKit 2 ͕࢖͑ΔΑ͏ʹͳͬͨ ࠷খ୯Ґ͕ߦͳͷͰɺͦΕҎԼͷૢ࡞͕ඞཁͳ৔߹͸޻෉͢ Δඞཁ͕͋Δ Ґஔܭࢉ͕ iOS 16 Ͱ͸όάͬͯΔ TextKit 1 ʹࣗಈϑΥʔϧόοΫ͢ΔͷͰڍಈΛ؅ཧ͢Δ

Slide 58

Slide 58 text

SwiftUI

Slide 59

Slide 59 text

Text, TextEditor SwiftUI ඇৗʹ൥ࡶͳ AttributedString ͕ Text ϏϡʔͰ࢖͑Δ͕ػ ೳ͕ݶΒΕΔ NSTextAttachment ͸࢖͑ͳ͍ TextKit ͷ API ΁ͷΞΫηε͸ͳ͍

Slide 60

Slide 60 text

·ͱΊ SwiftUI TextKit ʹ͸ରԠͯ͠ͳ͍ Text Ϗϡʔ΍ TextEditor Ϗϡʔ͸ػೳ͕͔ͳΓݶΒΕΔ TextKit ͷػೳ͕ඞཁͳ৔߹͸ૉ௚ʹ UITextView ΛຒΊࠐΉ

Slide 61

Slide 61 text

ͱ͍͏͜ͱͰ…

Slide 62

Slide 62 text

·ͱΊ TextKit 1 ͸จࣈྻ͔ΒάϦϑ୯ҐͰૢ࡞Մೳɻͨͩ͠ਖ਼͘͠ ಈ࡞͠ͳ͍έʔε͕͋Δ TextKit 2 ͸ศརͳػೳ͕௥Ճ͞Ε͍ͯΔ͕ɺจࣈྻ͔Βߦ୯ ҐͰͷૢ࡞ʹݶΒΕΔɻtypographicBounds ͸όάͬͯΔ SwiftUI ͷText, TextEditor Ϗϡʔ͸ػೳ͕ݶΒΕΔ

Slide 63

Slide 63 text

·ͱΊ ͲΕ΋׬ᘳͰ͸ͳ͍ ΞϓϦέʔγϣϯͷ໨త΍ඞཁͳػೳʹԠͯ͡ɺTextKit 1, TextKit 2, SwiftUI ΛબͿ