Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Yoshimasa Niwa @niw

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

͙͢ʹ࡞Εͦ͏ ؆୯ͦ͏

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

MacWorld 2007 Keynote

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

ΩʔϘʔυ͕͔ͿΔ໰୊

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

github.com/niw/KeyboardGuide KeyboardGuide

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

จࣈೖྗΛ੍ݶ͍ͨ͠

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

iOS ͷจࣈೖྗͷํ๏

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

จࣈೖྗΛ੍ݶ͍ͨ͠

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

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 }

Slide 37

Slide 37 text

μϝઈର

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

จࣈೖྗΛ੍ݶ͍ͨ͠

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

͜Ε͸ӕ

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

͓·͚

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

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