Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

Yoshimasa Niwa

September 21, 2020
Tweet

More Decks by Yoshimasa Niwa

Other Decks in Technology

Transcript

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

    View Slide

  2. Yoshimasa Niwa
    @niw

    View Slide

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

    View Slide

  4. View Slide

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

    View Slide

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

    View Slide

  7. MacWorld 2007 Keynote

    View Slide

  8. View Slide

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

    View Slide

  10. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. UIView ͷઈରҐஔΛ஌Δ
    UIView ͷϨ΢Ξ΢τ͸جຊతʹ͸਌ View ͕ࢠ View ͷϨΠ
    Ξ΢τʹ੹೚Λ࣋ͭͷͰɺ౒ྗͳ͠ʹઈରҐஔͷมԽΛࢠ͕
    ஌Δ͜ͱ͸Ͱ͖ͳ͍
    Auto Layout Ͱ Window ͱͷ੍໿Λ௥Ճ͢Ε͹
    layoutSubviews ͷ௨஌Λݺͼग़͠Λड͚Δ͜ͱ͕Ͱ͖Δ

    View Slide

  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)
    }

    View Slide

  18. github.com/niw/KeyboardGuide
    KeyboardGuide

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  22. View Slide

  23. iOS ͷจࣈೖྗͷํ๏

    View Slide

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

    View Slide

  25. View Slide

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

    View Slide

  27. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. View Slide

  32. View Slide

  33. View Slide

  34. View Slide

  35. View Slide

  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
    }

    View Slide

  37. μϝઈର

    View Slide

  38. textView(_:

    shouldChangeTextIn:

    replacementText:)

    ͷ໰୊

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  44. View Slide

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

    View Slide

  46. View Slide

  47. ͜Ε͸ӕ

    View Slide

  48. textViewDidChange(_:) 

    ͕ݺ͹Εͳ͍໰୊

    View Slide

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

    View Slide

  50. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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;
    }

    View Slide

  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;
    }
    }

    View Slide

  56. Πϕϯτ్தͰݱࡏͷॲཧ͕ऴΘͬͨ࣌ʹ࣮ߦͰ͖Δ
    RunLoop.perform(_:)
    textView(_:shouldChangeTextIn:replacementText) ͕ݺ͹Εͨ࣌
    ʹ RunLoop.main.perform(_:) ॲཧΛ༧໿
    textViewDidChange(_:) Ͱ΋ॲཧΛ༧໿
    ஗Ԇͤͨ͞λΠϛϯάͰ׬ྃΛ஌Δ

    View Slide

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

    View Slide

  58. View Slide

  59. ͓·͚

    View Slide

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

    View Slide

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

    View Slide

  62. TextKit ͕࢖ΘΕ͍ͯΔ
    UITextView ͷςΩετදࣔ
    TextKit ͸จࣈͷϨΠΞ΢τ΍දࣔͷػೳΛఏڙ͢Δ
    UITextView ࣗମ͸ओʹจࣈೖྗͷॲཧΛ୲౰ɺจࣈͷϨΠΞ
    ΢τ΍දࣔ͸ TextKit ͕࢖ΘΕ͍ͯΔ
    UITextView ͕ϏϡʔͳΒɺTextKit ͸Ϟσϧͱίϯτϩʔϥɺ
    ͷΑ͏ͳײ͡

    View Slide

  63. TextKit
    NSTextStorage
    NSLayoutManager
    NSTextContainer
    UITextView
    σϦήʔτ
    ϋΠϥΠτॲཧ
    Ϣʔβʔͷૢ࡞
    όοΫάϥ΢ϯυॲཧ
    Ϗϡʔ
    Ϟσϧ
    ࠷৽ͷ݁Ռ͚ͩΛ൓ө
    ίϯτϩʔϥʔ

    View Slide

  64. NSMutableAttributedString ͷαϒΫϥε
    NSTextStorage
    ςΩετͱൣғ͝ͱͷଐੑΛอ࣋
    UITextView ͷจࣈೖྗͷॲཧͱ͸ผͳͷͰςΩετͷมߋ͸
    ͠ͳ͍΄͏͕ྑ͍
    ςΩετͱଐੑͷมߋͷ྆ํΛ NSTextStorageDelegate Ͱ஌
    Δ͜ͱ͕Ͱ͖Δ

    View Slide

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

    View Slide

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

    View Slide

  67. ଐੑ͚ͩΛมߋ͢Δ
    NSTextStorageͷมߋ
    ϋΠϥΠτͨ݁͠ՌΛ setAttributes(_:range:) ͱɺ
    addAttribute(_:value:range) ͔ addAttributes(:range:) Ͱߋ৽͢Δ
    beginEditing(), endEditing() ͷதͰߦ͏
    setAttributedString(_:) ͸ݺΜͰ͸͍͚ͳ͍ɻͨͱ͑ϓϨʔ
    ϯςΩετ͕ಉ͡Ͱ΋ editedMask ͕ editedCharacters ʹͳΔ
    ͷͰແݶϧʔϓ͢Δ

    View Slide

  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()
    }
    }
    }

    View Slide

  69. NSTextStorage Λ࢖͏
    γϯλοΫεϋΠϥΠτΛ࣮૷͢Δ
    UITextView ΍ͦͷσϦήʔτͰ͸ߦΘͳ͍
    NSTextStorage ͱͦͷσϦήʔτͰ࣮૷͢Δ
    ॏ͍ॲཧͳͷͰόοΫάϥϯυͰߦ͏ɻΩϟογϡ΍࠷৽ͷ
    ݁ՌͷΈΛ൓өͤ͞ΔΑ͏ʹ͢Δ

    View Slide

  70. View Slide

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

    View Slide