$30 off During Our Annual Pro Sale. View Details »

TextKit 2 時代の iOS のキーボードとテキスト入力と表示のすべて

Yoshimasa Niwa
September 02, 2023

TextKit 2 時代の iOS のキーボードとテキスト入力と表示のすべて

iOS は一見、テキスト入力や表示のような基本的実装はとても簡単に思えますが、実際には一度はその動作に頭を悩ましたことがあるでしょう。

iOSDC 2020 では「iOS のキーボードと文字入力のすべて」と題してこれらの問題について詳細に検討しました。
そして、この3年間で iOS は大きく変化し、SwiftUI が広く使われるようになり、全く新しいAPI、TextKit 2 が登場しました。
TextKit 2 は大きく変化しおり、互換性に関して注意すべき点が多くあります。
このセッションでは、過去の事例と比較して、多くのユーザが使う iOS アプリにおけるリッチテキストの実装などを踏まえて、
キーボードの挙動や SwiftUI、そして TextKit 2 によって変わったテキスト処理について実装例を用いて検討していきます。

対象とする方: iOS アプリ開発の経験がある中・上級者

Yoshimasa Niwa

September 02, 2023
Tweet

More Decks by Yoshimasa Niwa

Other Decks in Programming

Transcript

  1. ಛ௃ TextKit 1 ݹ͔͘Βଓ͘ NSLayoutManager Λத৺ͱͨ͠ΫϥεͰߏ ੒͞ΕΔ API ΋ͱ΋ͱ TextKit

    1 ͳͲͱ͸ݺ͹Ε͍ͯͳ͔͕ͬͨɺTextKit 2 ͷ API ͱ۠ผ͢ΔͨΊʹͦ͏ݺ͹ΕΔΑ͏ʹͳͬͨ
  2. let textStorage = NSTextStorage() let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) let

    textContainer = NSTextContainer() layoutManager.addTextContainer(textContainer) let textView = UITextView( 
 frame: view.bounds, 
 textContainer: textContainer 
 )
  3. 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 )
  4. let textContentStorage = NSTextContentStorage() let textLayoutManager = NSTextLayoutManager() textContentStorage 


    .addTextLayoutManager(textLayoutManager) let textContainer = NSTextContainer() textLayoutManager.textContainer = textContainer let textView = UITextView( 
 frame: view.bounds, 
 textContainer: textContainer )
  5. TextKit 2 จࣈྻ͔ΒϨΠΞ΢τ NSTextLayoutManager ͷ enumerateTextLayoutFragments(from:options:u sing) ͰจࣈྻͷҐஔ͔Β NSTextLayoutFragment Λ

    Ұཡ textLineFragments Ͱ NSTextLineFragment ΛҰཡ NSTextLineFragment ͷ typographicBounds Λ࢖͏
  6. όά͕͋Γ·͢ typographicBounds typographicBounds ͸ layoutFragmentFrame ͷ origin ͔Βͷ࠲ඪͷ͸ͣ iOS 17

    Ͱ͸ͦͷ௨Γʹͳ͍ͬͯΔ iOS 16 Ͱ͸ x ํ޲͚ͩ 0.0 ͔Βͷ࠲ඪʹͳ͍ͬͯΔ
  7. 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 }
  8. TextKit 2 ϨΠΞ΢τ͔Βจࣈྻ NSTextLayoutManager ͷ textLayoutFragment(for:) Λ࢖ͬͯ NSLayoutFragment ΛऔಘɺlineFragments Λ

    ࢖ͬͯͦΕͧΕͷ typographicBounds Λώοτςετ NSLineFragment ͷ characterIndex(for:) Λ࢖ͬͯจ ࣈྻ಺ͷҐஔΛऔಘ attributes(at:effectiveRange:)Ͱจࣈྻ্ͷӨڹൣ ғΛऔಘ
  9. όά͕͋Γ·͢ typographicBounds typographicBounds ͸ layoutFragmentFrame ͷ origin ͔Βͷ࠲ඪͷ͸ͣ iOS 17

    Ͱ͸ͦͷ௨Γʹͳ͍ͬͯΔ iOS 16 Ͱ͸ x ํ޲͚ͩ 0.0 ͔Βͷ࠲ඪʹͳ͍ͬͯΔ
  10. let attributedText = NSMutableAttributedString( 
 "ޗഐ͸ೣͰ͋Δɻ໊લ͸·ͩແ͍" 
 ) let userNameAttributeKey

    = NSAttributedString.Key( 
 rawValue: "UserName" 
 ) 
 attributedText.addAttribute( 
 userNameAttributeKey, 
 value: "@neko", 
 range: NSRange(location: 0, length: 2) 
 ) 
 
 textView.attributedText = attributedText 
 
 ...
  11. 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 ) ...
  12. ... 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 ) } ...
  13. ... 
 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` ... } } } }
  14. 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() } 
 // ... }
  15. 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 
 ) } }
  16. 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 } }
  17. let attributedText = NSMutableAttributedString() 
 attributedText.append(NSAttributedString( 
 string: "ޗഐ͸ೣͰ͋Δɻ", 


    attributes: attributes 
 )) attributedText.append(NSAttributedString( 
 attachment: Attachment() 
 )) attributedText.append(NSAttributedString( 
 string: "໊લ͸·ͩແ͍", attributes: attributes 
 ))
  18. ରԠ͢Δ Ϗϡʔ iOS ͷόʔδϣϯ iOS 14 iOS 15 iOS 16

    Ҏ߱ UITextField 􀁣 􀁣 UITextView 􀁣
  19. UITextView ࣗಈϑΥʔϧόοΫ iOS 15 ͸ TextKit 1 ͚ͩ iOS 16

    Ҏ߱͸ TextKit 2 ͕σϑΥϧτ ͔͠͠ɺTextKit 1 ͷ API ʹͪΐͬͱͰ΋৮ΕΔͱ TextKit 1 ʹ੾ΓସΘͬͯ͠·͏ Ұ౓ TextKit 1 ʹͳΔͱɺTextKit 2 ʹ͸໭Βͳ͍
  20. TextKit 2 Λ࢖͏৔߹ TextKit Λ࢖͏ TextKit 2 ͚ͩΛ࢖͏৔߹ UITextView Λ೿ੜͤͯ͞ɺTextKit

    1 ͷ API Λ௵͢ 
 _UITextViewEnablingCompatibilityMode Ͱ֬ೝ ࣮ߦ࣌ʹ൑அ͢Δ৔߹ #available(iOS 16.0, *) Ͱ iOS 16 Λ࣮ߦ࣌ʹࣝผ
  21. // TextKit 1 ͚ͩΛ࢖͏৔߹ let textView = UITextView(usingTextLayoutManager: false) //

    TextKit 2 ͚ͩΛ࢖͏৔߹ final class TextView: UITextView { override var layoutManager: NSLayoutManager { fatalError("ͩΊͰ͢") } }
  22. // ৚݅ʹΑͬͯ੾Γସ͑Δ৔߹ 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 } } }
  23. ·ͱΊ TextKit 2 UITextView Ͱ iOS 16 Ҏ߱Ͱ TextKit 2

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