iOS のキーボードと文字入力のすべて

E4e01fb8b7105e61e3876224139503ab?s=47 Yoshimasa Niwa
September 21, 2020

iOS のキーボードと文字入力のすべて

iOS の UIKit は一見、文字入力のような基本的なことはとても簡単に行えるように思えます。

しかし、一度でも文字入力を扱ったことのある方は、その難しさと期待しない動作に頭を悩ましたことが一度はあると思います。

例えば、iOS のキーボードが文字入力に被ってしまう、キーボードの位置がずれる、変更したはずの文字が反映されない、日本語が入力できない...
実のところ、一般的に文字入力はとても複雑なユーザーインタラクションであり、さらに iOS はキーボードにも様々な種類があるため、一見すると正しそうな実装であっても、ドキュメントに記載のない様々な挙動により期待しない結果に終わってしまうことが多々あります。

このセッションでは、ほぼほとんどの方が経験する iOS の文字入力に関する些細な問題からとても大きな難題について、世界中で多くのユーザーが使う iOS アプリ開発や、iOS のキーボードフレームワーク「KeyboardGuide」などの開発など、これまで長い間蓄積してきた経験に基づいて、実際におこった問題を実例を踏まえて、その原因や対策を検証していきたいと思います。

対象とする方:
- iOS アプリ開発の経験がある中・上級者
- `UITxtView` に昔年の思いがある方
- `UIKeyboardWillChangeFrameNotification` などに昔年の思いがある方

前提とする知識:
- UIKit
- Foundation
- Unicode

詳細:
https://fortee.jp/iosdc-japan-2020/proposal/32f815cc-8b16-4321-9cf4-c74f70287190

E4e01fb8b7105e61e3876224139503ab?s=128

Yoshimasa Niwa

September 21, 2020
Tweet

Transcript

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

  2. Yoshimasa Niwa @niw

  3. σϞΞϓϦͷ֓ཁ จࣈ਺੍ݶͷ͋ΔςΩετฤूը໘

  4. None
  5. ͙͢ʹ࡞Εͦ͏ ؆୯ͦ͏

  6. MacWorld 2007 Ωʔϊʔτ iOS ͷ UI ͷࠜຊΛ;Γ͔͑Δ

  7. MacWorld 2007 Keynote

  8. None
  9. ΞϓϦέʔγϣϯͱಉ͡ը໘ʹදࣔ͞ΕΔ iOS ͷΩʔϘʔυ

  10. None
  11. ΩʔϘʔυ͕͔ͿΔ໰୊

  12. UIKit ͸ආ͚ͯ͘Εͳ͍ ࣗ෼Ͱආ͚Δඞཁ͕͋Δ

  13. ΩʔϘʔυͷආ͚ํ 1. ΩʔϘʔυͷઈରҐஔΛ஌Δ 2. ආ͚ΔϏϡʔͷઈରҐஔΛ஌Δ 3. ૬ରҐஔؔ܎ΛׂΓग़ͯ͠ආ͚Δ

  14. ଟ਺ͷ UIResponder.keyboard…Notification ΩʔϘʔυͷઈରҐஔΛ஌Δ ΩʔϘʔυ͕දࣔ͞Ε͍ͯΔ͔Ͳ͏͔஌Δ keyboardWillShowNotification keyboardWillHideNotification ΩʔϘʔυͷҐஔΛ஌Δ keyboardWillChangeFrameNotification

  15. keyboardFrameEndUserInfoKey ͷ࠲ඪܥ iOS ͷόʔδϣϯͰ࠲ඪܥ͕ҧ͏ iOS 12 Ҏલ UIApplication.shared.keyWindow.coordinateSpace ͳ͓ɺorigin.x ͸ৗʹؒҧͬͨ஋͕ೖ͍ͬͯΔ

    iOS 13 Ҏ߱ UIScreen.main.coordinateSpace
  16. UIView ͷઈରҐஔΛ஌Δ UIView ͷϨ΢Ξ΢τ͸جຊతʹ͸਌ View ͕ࢠ View ͷϨΠ Ξ΢τʹ੹೚Λ࣋ͭͷͰɺ౒ྗͳ͠ʹઈରҐஔͷมԽΛࢠ͕ ஌Δ͜ͱ͸Ͱ͖ͳ͍

    Auto Layout Ͱ Window ͱͷ੍໿Λ௥Ճ͢Ε͹ layoutSubviews ͷ௨஌Λݺͼग़͠Λड͚Δ͜ͱ͕Ͱ͖Δ
  17. private func updateKeyboardState(with notification: Notification) { guard let isLocal =

    notification.userInfo?[UIApplication.keyboardIsLocalUserInfoKey] as? Bool, let frame = notification.userInfo?[UIApplication.keyboardFrameEndUserInfoKey] as? CGRect else { return } // `UIResponder.keyboardWillChangeFrameNotification` _MAY BE_ posted with `CGRect.zero` frame. // Ignore it, which is useless. if frame == CGRect.zero { return } let keyboardScreen: UIScreen let coordinateSpace: UICoordinateSpace let keyboardFrame: CGRect if #available(iOS 13.0, *) { keyboardScreen = UIScreen.main coordinateSpace = keyboardScreen.coordinateSpace keyboardFrame = frame } else if let keyWindow = UIApplication.shared.keyWindow { keyboardScreen = keyWindow.screen coordinateSpace = keyWindow // Prior to iOS 13.0, keyboard frame in keyboard notifications is positioned wrongly and its X origin is always `0.0`. // Assign real X origin instead. let realKeyboardOriginX = coordinateSpace.convert(CGPoint.zero, from: keyboardScreen.coordinateSpace).x keyboardFrame = CGRect(x: realKeyboardOriginX, y: frame.origin.y, width: frame.size.width, height: frame.size.height) } else { return } // While the main screen bound is being changed, notifications _MAY BE_ posted with wrong frame. // Ignore it, because it will be eventual consistent with the following notifications. if keyboardScreen.bounds.width != keyboardFrame.width { return } dockedKeyboardState = KeyboardState(isLocal: isLocal, frame: keyboardFrame, coordinateSpace: coordinateSpace) }
  18. github.com/niw/KeyboardGuide KeyboardGuide

  19. ࣗ෼Ͱආ͚Δඞཁ͕͋Γ ΩʔϘʔυ͕͔ͿΔ໰୊ UIKit ͸खॿ͚ͯ͘͠Εͳ͍ ਖ਼͘͠ͳ͍࣮૷͕Πϯλʔωοτʹ͋;Ε͍ͯΔɻ৴༻͠ͳ ͍͜ͱ iPad ʹରԠ͢ΔͳΒϚϧνλεΫͷঢ়ଶΛ࣮֬ʹςετ͢Δ ͜ͱ

  20. σϞΞϓϦͷ֓ཁ จࣈ਺੍ݶͷ͋ΔςΩετฤूը໘

  21. จࣈೖྗΛ੍ݶ͍ͨ͠

  22. None
  23. iOS ͷจࣈೖྗͷํ๏

  24. ༷ʑͳछྨ͕͋Δ iOS ͷจࣈೖྗͷํ๏ ιϑτ΢ΣΞΩʔϘʔυ ϋʔυ΢ΣΞΩʔϘʔυ खॻ͖ೖྗ…

  25. None
  26. Voice Over ͷػೳ ΞΫηγϏϦςΟ Voice Over ͸ಡΈ্͚͛ͩͰ͸ͳ͍ ༷ʑͳจࣈೖྗํ๏͕ఏڙ͞Ε͍ͯΔ Bluetooth Ͱ઀ଓͰ͖Δ༷ʑͳೖྗσόΠε͕͋Δ

    Ұจࣈͮͭೖྗ͞ΕΔͱ͸ݶΒͳ͍
  27. None
  28. Ի੠ೖྗத͸ஞ࣍ೖྗ͞ΕΔ Ի੠ೖྗ ೔ຊޠೖྗʹࣅ͍ͯΔ Ի੠ೖྗதʹςΩετͷมߋΛ͢Δͱೖྗ͕தஅ͞ΕΔ ೔ຊޠม׵தʹউखʹ֬ఆ͞ΕΔΑ͏ͳײ͡

  29. iOS ʹ͸༷ʑͳจࣈೖྗͷํ๏͕͋Δ ͟·͟·ͳจࣈೖྗͰͰ͖ΔݶΓςετ͢Δ ೔ຊޠ͚ͩͰͳ͘ɺଞͷݴޠͷΩʔϘʔυ΋ςετ͢Δ ΞΫηγϏϦΟςΠ (Voice Over) ͷςετ͸ඞਢ

  30. จࣈೖྗΛ੍ݶ͍ͨ͠

  31. None
  32. None
  33. None
  34. None
  35. None
  36. func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String)

    -> Bool { let resultText = (textView.text! as NSString).replacingCharacters(in: range, with: text) if resultText.count <= 10 { return true } return false }
  37. μϝઈର

  38. textView(_:
 shouldChangeTextIn:
 replacementText:)
 ͷ໰୊

  39. ్தͰਖ਼͘͠ೖྗͰ͖ͳ͘ͳΔ ઈରʹ true Λฦ͢͜ͱ textView(_:shouldChangeTextIn:replacementText) ͸࣮͸Ϣʔ βʔͷΩʔϘʔυͳͲͷೖྗํ๏ʹ௚઀ີ઀ʹؔ܎͍ͯ͠Δ false Λฦ͢ͱೖྗͷ్தͰதஅͯ͠͠·͏ ೔ຊޠೖྗ΋ͦͷҰྫ

    ւ֎ͷΞϓϦͰ೔ຊޠೖྗ͕͓͔͍͠ͷ͸΄΅͜Ε͕ݪҼ
  40. େจࣈখจࣈΛԡ͢ͱ୙఺(ʃ)ʹͳΔ replacementText ͸ೖྗ͞ΕΔจࣈͰ͸ͳ͍ textView(_:shouldChangeTextIn:replacementText) ͸࣮͸Ϣʔ βʔͷΩʔϘʔυͳͲͷೖྗํ๏ʹ௚઀ີ઀ʹؔ܎͍ͯ͠Δ େจࣈখจࣈͰ୙఺͕ೖΔཧ༝͸͜Ε νΣίޠͳͲͷΞΫηϯτه߸͕͋Δ ΩʔϘʔυͰ͸ΞΫηϯτه߸͚͕ͩ͘Δ

  41. จࣈೖྗΛ੍ݶ͍ͨ͠

  42. ͦ΋ͦ΋੍ݶͯ͠͸͍͚ͳ͍

  43. iOS ʹ͸༷ʑͳจࣈೖྗ͕͋Δ จࣈೖྗͷํ๏ΛબͿͷ͸Ϣʔβʔ จࣈೖྗΛ੍ݶ͢Δͷ͸ͦ΋ͦ΋UXͱͯ͠΋ؒҧͬͯΔ จࣈೖྗ͸͢΂ͯड͚ೖΕΔɻೖྗ͕׬ྃͨ͠ΒͦΕΛड͚ ෇͚Δ͔ܾఆ͢Δ

  44. None
  45. จࣈೖྗ͕׬͔ྃͨ͠஌Γ͍ͨ

  46. None
  47. ͜Ε͸ӕ

  48. textViewDidChange(_:) 
 ͕ݺ͹Εͳ͍໰୊

  49. Ϣʔβʔͷૢ࡞Ͱ΋ݺ͹Εͳ͍έʔε͕͋Δ textViewDidChange(_:) ͕ݺ͹Εͳ͍έʔε textViewDidEndEditing ͕ݺ͹ΕΔΑ͏ͳૢ࡞Λͨ࣌͠ ಛఆͷঢ়گͰमਖ਼ީิΛΩʔϘʔυ͔ΒબΜͩ࣌

  50. None
  51. ճආࡦ͸͋Δͷ͔? ͜Ε͸όάͬΆ͍ గਖ਼߲໨͸ΩʔϘʔυͷิ׬ػೳ ΩʔϘʔυͱີ઀ʹؔ܎͍ͯ͠Δ API ͕͋ͬͨ͸ͣ

  52. ͨͩ͠Ϣʔβʔͷૢ࡞͕࢝·ͬͨλΠϛϯάͰݺ͹ΕΔ textView(_:shouldChangeTextIn:replacemen tText:) Λ࢖͏

  53. RunLoop Λ஌Δ ΠϕϯυϋϯυϦϯάͷλΠϛϯά UIKit ͸ΠϕϯτυϦϒϯ Ϣʔβʔͷૢ࡞͸શͯΠϕϯτϧʔϓͰॲཧ͞ΕΔ Ϣʔβʔͷૢ࡞͕׬ྃ͢Δͷ͸ɺҰճͷϧʔϓͷதͰॲཧ͕ ऴΘͬͨλΠϛϯά Πϕϯτͷॲཧ͸ CFRunLoopRun

    Ͱ࣮ߦ͞Ε͍ͯΔ
  54. /* rl, rlm are locked on entrance and exit */

    static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { int64_t startTSR = (int64_t)mach_absolute_time(); if (__CFRunLoopIsStopped(rl)) { __CFRunLoopUnsetStopped(rl); return kCFRunLoopRunStopped; } else if (rlm->_stopped) { rlm->_stopped = false; return kCFRunLoopRunStopped; } mach_port_name_t dispatchPort = MACH_PORT_NULL; Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF(); dispatch_source_t timeout_timer = NULL; struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context)); if (seconds <= 0.0) { // instant timeout seconds = 0.0; timeout_context->termTSR = 0LL; } else if (seconds <= TIMER_INTERVAL_LIMIT) { dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT); timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_retain(timeout_timer); timeout_context->ds = timeout_timer; timeout_context->rl = (CFRunLoopRef)CFRetain(rl); timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds); dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout); dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel); uint64_t nanos = (uint64_t)(seconds * 1000 * 1000 + 1) * 1000; dispatch_source_set_timer(timeout_timer, dispatch_time(DISPATCH_TIME_NOW, nanos), DISPATCH_TIME_FOREVER, 0); dispatch_resume(timeout_timer); } else { // infinite timeout seconds = 9999999999.0; timeout_context->termTSR = INT64_MAX; } Boolean didDispatchPortLastTime = true; int32_t retVal = 0; do { uint8_t msg_buffer[3 * 1024]; #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED mach_msg_header_t *msg = NULL; #elif DEPLOYMENT_TARGET_WINDOWS HANDLE livePort = NULL; Boolean windowsMessageReceived = false; #endif __CFPortSet waitSet = rlm->_portSet; rl->_ignoreWakeUps = false; if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); __CFRunLoopDoBlocks(rl, rlm); Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); if (sourceHandledThisLoop) { __CFRunLoopDoBlocks(rl, rlm); } Boolean poll = sourceHandledThisLoop || (0LL == timeout_context->termTSR); if (MACH_PORT_NULL != dispatchPort && ! didDispatchPortLastTime) { #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED msg = (mach_msg_header_t *)msg_buffer; if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), 0)) { goto handle_msg; } #elif DEPLOYMENT_TARGET_WINDOWS if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) { goto handle_msg; } #endif } didDispatchPortLastTime = false; if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl); // do not do any user callouts after this point (after notifying of sleeping) // Must push the local-to-this-activation ports in on every loop // iteration, as this mode could be run re- entrantly and we don't // want these ports to get serviced. __CFPortSetInsert(dispatchPort, waitSet); __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl); #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED if (kCFUseCollectableAllocator) { objc_clear_stack(0); memset(msg_buffer, 0, sizeof(msg_buffer)); } msg = (mach_msg_header_t *)msg_buffer; __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), poll ? 0 : TIMEOUT_INFINITY); #elif DEPLOYMENT_TARGET_WINDOWS // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages. // Note: don't pass 0 for polling, or this thread will never yield the CPU. __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived); #endif __CFRunLoopLock(rl); __CFRunLoopModeLock(rlm); // Must remove the local-to-this-activation ports in on every loop // iteration, as this mode could be run re- entrantly and we don't // want these ports to get serviced. Also, we don't want them left // in there if this function returns. __CFPortSetRemove(dispatchPort, waitSet); rl->_ignoreWakeUps = true; // user callouts now OK again __CFRunLoopUnsetSleeping(rl); if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); handle_msg:; rl->_ignoreWakeUps = true; #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED mach_port_t livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL; #endif #if DEPLOYMENT_TARGET_WINDOWS if (windowsMessageReceived) { // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl); if (rlm->_msgPump) { rlm->_msgPump(); } else { MSG msg; if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) { TranslateMessage(&msg); DispatchMessage(&msg); } } __CFRunLoopLock(rl); __CFRunLoopModeLock(rlm); sourceHandledThisLoop = true; } else #endif if (MACH_PORT_NULL == livePort) { // handle nothing } else if (livePort == rl->_wakeUpPort) { // do nothing on Mac OS #if DEPLOYMENT_TARGET_WINDOWS // Always reset the wake up port, or risk spinning forever ResetEvent(rl->_wakeUpPort); #endif } else if (livePort == rlm->_timerPort) { #if DEPLOYMENT_TARGET_WINDOWS // We use a manual reset timer to ensure that we don't miss timers firing because the run loop did the wakeUpPort this time // The only way to reset a timer is to reset the timer using SetWaitableTimer. We don't want it to fire again though, so we set the timeout to a large negative value. The timer may be reset again inside the timer handling code. LARGE_INTEGER dueTime; dueTime.QuadPart = LONG_MIN; SetWaitableTimer(rlm->_timerPort, &dueTime, 0, NULL, NULL, FALSE); #endif __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()); } else if (livePort == dispatchPort) { __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl); _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL); #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED _dispatch_main_queue_callback_4CF(msg); #elif DEPLOYMENT_TARGET_WINDOWS _dispatch_main_queue_callback_4CF(); #endif _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL); __CFRunLoopLock(rl); __CFRunLoopModeLock(rlm); sourceHandledThisLoop = true; didDispatchPortLastTime = true; } else { // Despite the name, this works for windows handles as well CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort); if (rls) { #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED mach_msg_header_t *reply = NULL; sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; if (NULL != reply) { (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply); } #elif DEPLOYMENT_TARGET_WINDOWS sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop; #endif } } #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); #endif __CFRunLoopDoBlocks(rl, rlm); if (sourceHandledThisLoop && stopAfterHandle) { retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < (int64_t)mach_absolute_time()) { retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (rlm->_stopped) { rlm->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { retVal = kCFRunLoopRunFinished; } } while (0 == retVal); if (timeout_timer) { dispatch_source_cancel(timeout_timer); dispatch_release(timeout_timer); } else { free(timeout_context); } return retVal; }
  55. __CFRunLoopRun() { while (true) { Call kCFRunLoopBeforeTimers observer callbacks; Call

    kCFRunLoopBeforeSources observer callbacks; Perform blocks queued by CFRunLoopPerformBlock; Call the callback of each version 0 CFRunLoopSource that has been signaled; if (any version 0 source callbacks were called) { Perform blocks newly queued by CFRunLoopPerformBlock; } if (I didn’t drain the main queue on the last iteration AND the main queue has any blocks waiting) { while (main queue has blocks) { perform the next block on the main queue } } else { Call kCFRunLoopBeforeWaiting observer callbacks; Wait for a CFRunLoopSource to be signaled, or wait for a timer to fire, or wait for a block to be added to the main queue; Call kCFRunLoopAfterWaiting observer callbacks; if (the event was a timer) { Call CFRunLoopTimer callbacks for timers that should have fired by now } else if (event was a block arriving on the main queue) { while (main queue has blocks) { Perform the next block on the main queue } } else { Look up the version 1 CFRunLoopSource for the event; if (I found a version 1 source) { Call the source’s callback; } } } Perform blocks queued by CFRunLoopPerformBlock; } }
  56. Πϕϯτ్தͰݱࡏͷॲཧ͕ऴΘͬͨ࣌ʹ࣮ߦͰ͖Δ RunLoop.perform(_:) textView(_:shouldChangeTextIn:replacementText) ͕ݺ͹Εͨ࣌ ʹ RunLoop.main.perform(_:) ॲཧΛ༧໿ textViewDidChange(_:) Ͱ΋ॲཧΛ༧໿ ஗Ԇͤͨ͞λΠϛϯάͰ׬ྃΛ஌Δ

  57. จࣈೖྗͷ׬ྃΛ஌Δ UITextViewDelegate ͷݺͼग़͠͸ͦͷ··࢖͑ͳ͍ RunLoop.main.perform(_:) Ͱݺͼग़͠Λ஗Ԇͤͯ͞ UIKit ͷ ॲཧ͕׬ྃ͢ΔͷΛ଴ͭ textViewDidEndEditing ͷݺͼग़͠΋๨Εͣʹ

  58. None
  59. ͓·͚

  60. ௒աͨ͠Օॴʹ৭Λ͚ͭΔ

  61. γϯλοΫεϋΠϥΠτ Λ࣮૷͢Δ

  62. TextKit ͕࢖ΘΕ͍ͯΔ UITextView ͷςΩετදࣔ TextKit ͸จࣈͷϨΠΞ΢τ΍දࣔͷػೳΛఏڙ͢Δ UITextView ࣗମ͸ओʹจࣈೖྗͷॲཧΛ୲౰ɺจࣈͷϨΠΞ ΢τ΍දࣔ͸ TextKit

    ͕࢖ΘΕ͍ͯΔ UITextView ͕ϏϡʔͳΒɺTextKit ͸Ϟσϧͱίϯτϩʔϥɺ ͷΑ͏ͳײ͡
  63. TextKit NSTextStorage NSLayoutManager NSTextContainer UITextView σϦήʔτ ϋΠϥΠτॲཧ Ϣʔβʔͷૢ࡞ όοΫάϥ΢ϯυॲཧ Ϗϡʔ

    Ϟσϧ ࠷৽ͷ݁Ռ͚ͩΛ൓ө ίϯτϩʔϥʔ
  64. NSMutableAttributedString ͷαϒΫϥε NSTextStorage ςΩετͱൣғ͝ͱͷଐੑΛอ࣋ UITextView ͷจࣈೖྗͷॲཧͱ͸ผͳͷͰςΩετͷมߋ͸ ͠ͳ͍΄͏͕ྑ͍ ςΩετͱଐੑͷมߋͷ྆ํΛ NSTextStorageDelegate Ͱ஌

    Δ͜ͱ͕Ͱ͖Δ
  65. ϋΠϥΠτॲཧΛ͓͜ͳ͏λΠϛϯά NSTextStorageDelegate ϋΠϥΠτॲཧ͸ςΩετͷมߋΛτϦΨʔʹ͢Δ NSTextStorageDelegate ͸ͨͱ͑͹೔ຊޠม׵தͰ΋ஞ࣍ݺ ͹ΕΔ NSTextStorageDelegate.textStorage(_:didProcessEditing:range:c hangeInLength:) ͰɺeditedMask ͕

    editedCharacters ͷ࣌ ͷΈϋΠϥΠτॲཧΛ࣮ߦ͢Δ
  66. ϋΠϥΠτॲཧ جຊతʹϋΠϥΠτॲཧ͸ॏ͍ॲཧ ଟগ஗Εͯ΋໰୊ͳ͍ͷͰɺόοΫάϥ΢ϯυͰ࣮ߦ͢Δ ૬౰සൟʹݺ͹ΕΔͷͰΩϟογϡͯ͠΋ྑ͍ ׬ྃ͠ͳ͔ͬͨલͷॲཧ͸Ωϟϯηϧ͢Δ ݁Ռ͸ NSAttributedString Ͱදݱ͢Δͱྑ͍

  67. ଐੑ͚ͩΛมߋ͢Δ NSTextStorageͷมߋ ϋΠϥΠτͨ݁͠ՌΛ setAttributes(_:range:) ͱɺ addAttribute(_:value:range) ͔ addAttributes(:range:) Ͱߋ৽͢Δ beginEditing(),

    endEditing() ͷதͰߦ͏ setAttributedString(_:) ͸ݺΜͰ͸͍͚ͳ͍ɻͨͱ͑ϓϨʔ ϯςΩετ͕ಉ͡Ͱ΋ editedMask ͕ editedCharacters ʹͳΔ ͷͰແݶϧʔϓ͢Δ
  68. func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange,

    changeInLength delta: Int) { // Only attributes are edited guard editedMask.contains(.editedCharacters) else { return } // Capture plain string let string = textStorage.string DispatchQueue.global(qos: .utility).async { let overflowedCount = min(self.maximumLength - string.unicodeScalars.count, 0) let overflowedBeginIndex = string.unicodeScalars.index(string.unicodeScalars.endIndex, offsetBy: overflowedCount) let overflowedBeginOffset = overflowedBeginIndex.utf16Offset(in: string) let overflowedEndOffset = string.unicodeScalars.endIndex.utf16Offset(in: string) let overflowedRange = NSRange(location: overflowedBeginOffset, length: overflowedEndOffset - overflowedBeginOffset) DispatchQueue.main.async { // Plain string is changed already guard textStorage.string == string else { return } textStorage.beginEditing() // Reset foreground color textStorage.addAttribute(.foregroundColor, value: UIColor.darkText, range: NSRange(location: 0, length: textStorage.length)) // Add foreground color for overflowed characters textStorage.addAttribute(.foregroundColor, value: UIColor.systemRed, range: overflowedRange) textStorage.endEditing() } } }
  69. NSTextStorage Λ࢖͏ γϯλοΫεϋΠϥΠτΛ࣮૷͢Δ UITextView ΍ͦͷσϦήʔτͰ͸ߦΘͳ͍ NSTextStorage ͱͦͷσϦήʔτͰ࣮૷͢Δ ॏ͍ॲཧͳͷͰόοΫάϥϯυͰߦ͏ɻΩϟογϡ΍࠷৽ͷ ݁ՌͷΈΛ൓өͤ͞ΔΑ͏ʹ͢Δ

  70. None
  71. iOS ͷΩʔϘʔυͱจࣈೖྗͷ͢΂ͯ ·ͱΊ ΩʔϘʔυ͸ͦͷҐஔΛਖ਼͘͠஌Δ ଟ༷ͳจࣈೖྗͷํ๏͕͋Δ͜ͱΛ஌Δ Πϯλʔωοτͷ৘ใ͸ͦͷ··৴͡ͳ͍ υΩϡϝϯτͷ৘ใ͸ͦͷ··৴͡ͳ͍