La solution donnée par Shaunti Fondrisi est presque parfaite. Mais un tel morceau de code ou des codes comme mettre en file d'attente l'exécution de UICollectionView
's reloadData()
à NSOperationQueue
' mainQueue
met en effet la synchronisation d'exécution au début de la prochaine boucle d'événement dans la boucle d'exécution, ce qui pourrait rendre leUICollectionView
mise à jour d'un simple coup.
Pour résoudre ce problème. Nous devons mettre le timing d'exécution du même morceau de code à la fin de la boucle d'événement en cours mais pas au début de la suivante. Et nous pouvons y parvenir en utilisant CFRunLoopObserver
.
CFRunLoopObserver
observe toutes les activités d'attente de la source d'entrée et l'activité d'entrée et de sortie de la boucle d'exécution.
public struct CFRunLoopActivity : OptionSetType {
public init(rawValue: CFOptionFlags)
public static var Entry: CFRunLoopActivity { get }
public static var BeforeTimers: CFRunLoopActivity { get }
public static var BeforeSources: CFRunLoopActivity { get }
public static var BeforeWaiting: CFRunLoopActivity { get }
public static var AfterWaiting: CFRunLoopActivity { get }
public static var Exit: CFRunLoopActivity { get }
public static var AllActivities: CFRunLoopActivity { get }
}
Parmi ces activités, .AfterWaiting
peut être observée lorsque la boucle d'événements en cours est sur le point de se terminer, et.BeforeWaiting
peut être observée lorsque la prochaine boucle d'événements vient de commencer.
Comme il n'y a qu'une seule NSRunLoop
instance par NSThread
et NSRunLoop
pilote exactement le NSThread
, on peut considérer que les accès proviennent du mêmeNSRunLoop
instance ne traversent toujours jamais les threads.
Sur la base des points mentionnés précédemment, nous pouvons maintenant écrire le code: un répartiteur de tâches basé sur NSRunLoop:
import Foundation
import ObjectiveC
public struct Weak<T: AnyObject>: Hashable {
private weak var _value: T?
public weak var value: T? { return _value }
public init(_ aValue: T) { _value = aValue }
public var hashValue: Int {
guard let value = self.value else { return 0 }
return ObjectIdentifier(value).hashValue
}
}
public func ==<T: AnyObject where T: Equatable>(lhs: Weak<T>, rhs: Weak<T>)
-> Bool
{
return lhs.value == rhs.value
}
public func ==<T: AnyObject>(lhs: Weak<T>, rhs: Weak<T>) -> Bool {
return lhs.value === rhs.value
}
public func ===<T: AnyObject>(lhs: Weak<T>, rhs: Weak<T>) -> Bool {
return lhs.value === rhs.value
}
private var dispatchObserverKey =
"com.WeZZard.Nest.NSRunLoop.TaskDispatcher.DispatchObserver"
private var taskQueueKey =
"com.WeZZard.Nest.NSRunLoop.TaskDispatcher.TaskQueue"
private var taskAmendQueueKey =
"com.WeZZard.Nest.NSRunLoop.TaskDispatcher.TaskAmendQueue"
private typealias DeallocFunctionPointer =
@convention(c) (Unmanaged<NSRunLoop>, Selector) -> Void
private var original_dealloc_imp: IMP?
private let swizzled_dealloc_imp: DeallocFunctionPointer = {
(aSelf: Unmanaged<NSRunLoop>,
aSelector: Selector)
-> Void in
let unretainedSelf = aSelf.takeUnretainedValue()
if unretainedSelf.isDispatchObserverLoaded {
let observer = unretainedSelf.dispatchObserver
CFRunLoopObserverInvalidate(observer)
}
if let original_dealloc_imp = original_dealloc_imp {
let originalDealloc = unsafeBitCast(original_dealloc_imp,
DeallocFunctionPointer.self)
originalDealloc(aSelf, aSelector)
} else {
fatalError("The original implementation of dealloc for NSRunLoop cannot be found!")
}
}
public enum NSRunLoopTaskInvokeTiming: Int {
case NextLoopBegan
case CurrentLoopEnded
case Idle
}
extension NSRunLoop {
public func perform(closure: ()->Void) -> Task {
objc_sync_enter(self)
loadDispatchObserverIfNeeded()
let task = Task(self, closure)
taskQueue.append(task)
objc_sync_exit(self)
return task
}
public override class func initialize() {
super.initialize()
struct Static {
static var token: dispatch_once_t = 0
}
// make sure this isn't a subclass
if self !== NSRunLoop.self {
return
}
dispatch_once(&Static.token) {
let selectorDealloc: Selector = "dealloc"
original_dealloc_imp =
class_getMethodImplementation(self, selectorDealloc)
let swizzled_dealloc = unsafeBitCast(swizzled_dealloc_imp, IMP.self)
class_replaceMethod(self, selectorDealloc, swizzled_dealloc, "@:")
}
}
public final class Task {
private let weakRunLoop: Weak<NSRunLoop>
private var _invokeTiming: NSRunLoopTaskInvokeTiming
private var invokeTiming: NSRunLoopTaskInvokeTiming {
var theInvokeTiming: NSRunLoopTaskInvokeTiming = .NextLoopBegan
guard let amendQueue = weakRunLoop.value?.taskAmendQueue else {
fatalError("Accessing a dealloced run loop")
}
dispatch_sync(amendQueue) { () -> Void in
theInvokeTiming = self._invokeTiming
}
return theInvokeTiming
}
private var _modes: NSRunLoopMode
private var modes: NSRunLoopMode {
var theModes: NSRunLoopMode = []
guard let amendQueue = weakRunLoop.value?.taskAmendQueue else {
fatalError("Accessing a dealloced run loop")
}
dispatch_sync(amendQueue) { () -> Void in
theModes = self._modes
}
return theModes
}
private let closure: () -> Void
private init(_ runLoop: NSRunLoop, _ aClosure: () -> Void) {
weakRunLoop = Weak<NSRunLoop>(runLoop)
_invokeTiming = .NextLoopBegan
_modes = .defaultMode
closure = aClosure
}
public func forModes(modes: NSRunLoopMode) -> Task {
if let amendQueue = weakRunLoop.value?.taskAmendQueue {
dispatch_async(amendQueue) { [weak self] () -> Void in
self?._modes = modes
}
}
return self
}
public func when(invokeTiming: NSRunLoopTaskInvokeTiming) -> Task {
if let amendQueue = weakRunLoop.value?.taskAmendQueue {
dispatch_async(amendQueue) { [weak self] () -> Void in
self?._invokeTiming = invokeTiming
}
}
return self
}
}
private var isDispatchObserverLoaded: Bool {
return objc_getAssociatedObject(self, &dispatchObserverKey) !== nil
}
private func loadDispatchObserverIfNeeded() {
if !isDispatchObserverLoaded {
let invokeTimings: [NSRunLoopTaskInvokeTiming] =
[.CurrentLoopEnded, .NextLoopBegan, .Idle]
let activities =
CFRunLoopActivity(invokeTimings.map{ CFRunLoopActivity($0) })
let observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
activities.rawValue,
true, 0,
handleRunLoopActivityWithObserver)
CFRunLoopAddObserver(getCFRunLoop(),
observer,
kCFRunLoopCommonModes)
let wrappedObserver = NSAssociated<CFRunLoopObserver>(observer)
objc_setAssociatedObject(self,
&dispatchObserverKey,
wrappedObserver,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
private var dispatchObserver: CFRunLoopObserver {
loadDispatchObserverIfNeeded()
return (objc_getAssociatedObject(self, &dispatchObserverKey)
as! NSAssociated<CFRunLoopObserver>)
.value
}
private var taskQueue: [Task] {
get {
if let taskQueue = objc_getAssociatedObject(self,
&taskQueueKey)
as? [Task]
{
return taskQueue
} else {
let initialValue = [Task]()
objc_setAssociatedObject(self,
&taskQueueKey,
initialValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return initialValue
}
}
set {
objc_setAssociatedObject(self,
&taskQueueKey,
newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
private var taskAmendQueue: dispatch_queue_t {
if let taskQueue = objc_getAssociatedObject(self,
&taskAmendQueueKey)
as? dispatch_queue_t
{
return taskQueue
} else {
let initialValue =
dispatch_queue_create(
"com.WeZZard.Nest.NSRunLoop.TaskDispatcher.TaskAmendQueue",
DISPATCH_QUEUE_SERIAL)
objc_setAssociatedObject(self,
&taskAmendQueueKey,
initialValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return initialValue
}
}
private func handleRunLoopActivityWithObserver(observer: CFRunLoopObserver!,
activity: CFRunLoopActivity)
-> Void
{
var removedIndices = [Int]()
let runLoopMode: NSRunLoopMode = currentRunLoopMode
for (index, eachTask) in taskQueue.enumerate() {
let expectedRunLoopModes = eachTask.modes
let expectedRunLoopActivitiy =
CFRunLoopActivity(eachTask.invokeTiming)
let runLoopModesMatches = expectedRunLoopModes.contains(runLoopMode)
|| expectedRunLoopModes.contains(.commonModes)
let runLoopActivityMatches =
activity.contains(expectedRunLoopActivitiy)
if runLoopModesMatches && runLoopActivityMatches {
eachTask.closure()
removedIndices.append(index)
}
}
taskQueue.removeIndicesInPlace(removedIndices)
}
}
extension CFRunLoopActivity {
private init(_ invokeTiming: NSRunLoopTaskInvokeTiming) {
switch invokeTiming {
case .NextLoopBegan: self = .AfterWaiting
case .CurrentLoopEnded: self = .BeforeWaiting
case .Idle: self = .Exit
}
}
}
Avec le code précédent, nous pouvons maintenant envoyer l'exécution de UICollectionView
's reloadData()
à la fin de la boucle d'événement actuelle par un tel morceau de code:
NSRunLoop.currentRunLoop().perform({ () -> Void in
collectionView.reloadData()
}).when(.CurrentLoopEnded)
En fait, un tel répartiteur de tâches basé sur NSRunLoop a déjà été dans l'un de mes frameworks personnels: Nest. Et voici son référentiel sur GitHub: https://github.com/WeZZard/Nest
reloadData
après viewDidAppear semble résoudre le problème, son horrible solution de contournement et doit être corrigé. J'espère que quelqu'un aide ici.