diff --git a/Development/DataSources.xcodeproj/project.pbxproj b/Development/DataSources.xcodeproj/project.pbxproj index d0cb84f..927bd1a 100644 --- a/Development/DataSources.xcodeproj/project.pbxproj +++ b/Development/DataSources.xcodeproj/project.pbxproj @@ -378,7 +378,7 @@ ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -387,6 +387,7 @@ MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = me.muukii.DataSourcesDemo; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -403,7 +404,7 @@ ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -412,6 +413,7 @@ MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = me.muukii.DataSourcesDemo; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; diff --git a/Development/DataSourcesDemo/AppDelegate.swift b/Development/DataSourcesDemo/AppDelegate.swift index 3201009..b673ca5 100644 --- a/Development/DataSourcesDemo/AppDelegate.swift +++ b/Development/DataSourcesDemo/AppDelegate.swift @@ -8,7 +8,7 @@ import UIKit -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? diff --git a/Development/DataSourcesDemo/Components.swift b/Development/DataSourcesDemo/Components.swift index c459946..cae97b0 100644 --- a/Development/DataSourcesDemo/Components.swift +++ b/Development/DataSourcesDemo/Components.swift @@ -44,6 +44,7 @@ struct ModelB: Model, Differentiable, Equatable { } } +@MainActor final class ViewModel { let section0 = BehaviorRelay<[ModelA]>(value: []) @@ -73,7 +74,7 @@ final class ViewModel { section0.modify { $0.removeFirst() } - DispatchQueue.global().async { + Task { @MainActor [self] in self.section0.modify { $0.append(ModelA(identity: UUID().uuidString, title: String.randomEmoji())) } @@ -82,7 +83,7 @@ final class ViewModel { section1.modify { $0.removeFirst() } - DispatchQueue.global().async { + Task { @MainActor [self] in self.section1.modify { $0.append(ModelB(identity: UUID().uuidString, title: arc4random_uniform(30).description)) } diff --git a/Package.swift b/Package.swift index c9fa795..eb58603 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,9 @@ -// swift-tools-version:5.5 +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "DataSources", - platforms: [.iOS(.v12)], + platforms: [.iOS(.v16)], products: [ .library(name: "DataSources", targets: ["DataSources"]), ], diff --git a/Sources/DataSources/SectionDataController.swift b/Sources/DataSources/SectionDataController.swift index a756c9d..b3faa6d 100644 --- a/Sources/DataSources/SectionDataController.swift +++ b/Sources/DataSources/SectionDataController.swift @@ -7,14 +7,15 @@ // import Foundation +import os.atomic +@preconcurrency import DifferenceKit -import DifferenceKit - -public protocol SectionDataControllerType where AdapterType.Element == ItemType { +public protocol SectionDataControllerType: Sendable where AdapterType.Element == ItemType { associatedtype ItemType : Differentiable associatedtype AdapterType : Updating + @MainActor func update(items: [ItemType], updateMode: SectionDataController.UpdateMode, immediately: Bool, completion: @escaping () -> Void) func asSectionDataController() -> SectionDataController @@ -56,12 +57,12 @@ final class AnySectionDataController { } /// DataSource for a section -public final class SectionDataController: SectionDataControllerType where A.Element == T { +public final class SectionDataController: Sendable, SectionDataControllerType where A.Element == T { public typealias ItemType = T public typealias AdapterType = A - public enum State { + public enum State: Sendable { case idle case updating } @@ -72,18 +73,26 @@ public final class SectionDataController: Sectio } // MARK: - Properties + + public var items: [T] { + return _items.withLock { $0 } + } + + public var snapshot: [T] { + return _snapshot.withLock { $0 } + } - private(set) public var items: [T] = [] + private let _items: OSAllocatedUnfairLock<[T]> = .init(initialState: []) - private(set) public var snapshot: [T] = [] + private let _snapshot: OSAllocatedUnfairLock<[T]> = .init(initialState: []) - private var state: State = .idle + private let state: OSAllocatedUnfairLock = .init(initialState: .idle) private let throttle = Throttle(interval: 0.1) private let adapter: AdapterType - public internal(set) var displayingSection: Int + public let displayingSection: Int // MARK: - Initializers @@ -113,8 +122,11 @@ public final class SectionDataController: Sectio /// - Returns: public func item(at indexPath: IndexPath) -> T? { guard let index = toIndex(from: indexPath) else { return nil } - guard snapshot.indices.contains(index) else { return nil } - return snapshot[index] + + return _snapshot.withLock { + guard $0.indices.contains(index) else { return nil } + return $0[index] + } } /// Reserves that a move occurred in DataSource by View operation. @@ -137,9 +149,12 @@ public final class SectionDataController: Sectio destinationIndexPath.section == displayingSection, "destinationIndexPath.section \(sourceIndexPath.section) must be equal to \(displayingSection)" ) + + _snapshot.withLock { + let o = $0.remove(at: sourceIndexPath.item) + $0.insert(o, at: destinationIndexPath.item) + } - let o = snapshot.remove(at: sourceIndexPath.item) - snapshot.insert(o, at: destinationIndexPath.item) } /// Update @@ -152,40 +167,41 @@ public final class SectionDataController: Sectio /// - updateMode: /// - immediately: False : indicate to throttled updating /// - completion: + @MainActor public func update( items: [T], updateMode: UpdateMode, immediately: Bool = false, completion: @escaping () -> Void ) { - - self.items = items - - let task = { [weak self] in - guard let `self` = self else { return } - - let old = self.snapshot - let new = self.items - - self.__update( - targetSection: self.displayingSection, - currentDisplayingItems: old, - newItems: new, - updateMode: updateMode, - completion: { - completion() - }) - } - - if immediately { - throttle.cancel() - task() - } else { - throttle.on { + + self._items.withLock { $0 = items } + + let task = { [weak self] in + guard let `self` = self else { return } + + let old = self.snapshot + let new = self.items + + self.__update( + targetSection: self.displayingSection, + currentDisplayingItems: old, + newItems: new, + updateMode: updateMode, + completion: { + completion() + }) + } + + if immediately { + throttle.cancel() task() + } else { + throttle.on { + task() + } } - } - } + } public func asSectionDataController() -> SectionDataController { return self @@ -208,26 +224,27 @@ public final class SectionDataController: Sectio return indexPath.item } + @MainActor private func __update( targetSection: Int, currentDisplayingItems: [T], newItems: [T], updateMode: UpdateMode, completion: @escaping () -> Void - ) { - + ) { + assertMainThread() - - self.state = .updating - + + self.state.withLock { $0 = .updating } + switch updateMode { case .everything: - self.snapshot = newItems + self._snapshot.withLock { $0 = newItems } adapter.reload { assertMainThread() - self.state = .idle + self.state.withLock { $0 = .idle } completion() } @@ -258,7 +275,7 @@ public final class SectionDataController: Sectio group.enter() - self.snapshot = changeset.data + self._snapshot.withLock { $0 = changeset.data } let updateContext = UpdateContext.init( diff: .init(diff: changeset, targetSection: targetSection), @@ -299,7 +316,7 @@ public final class SectionDataController: Sectio } group.notify(queue: .main) { - self.state = .idle + self.state.withLock { $0 = .idle } completion() } diff --git a/Sources/DataSources/Throttle.swift b/Sources/DataSources/Throttle.swift index 258c9eb..32c8b54 100644 --- a/Sources/DataSources/Throttle.swift +++ b/Sources/DataSources/Throttle.swift @@ -1,31 +1,24 @@ -// -// Throttle.swift -// DataSources -// -// Created by muukii on 8/9/17. -// Copyright © 2017 muukii. All rights reserved. -// - import Foundation +@MainActor final class Throttle { - + private var timerReference: DispatchSourceTimer? - + let interval: TimeInterval let queue: DispatchQueue - + private var lastSendTime: Date? - - init(interval: TimeInterval, queue: DispatchQueue = .main) { + + nonisolated init(interval: TimeInterval, queue: DispatchQueue = .main) { self.interval = interval self.queue = queue } - + func on(handler: @escaping () -> Void) { - + let now = Date() - + if let _lastSendTime = lastSendTime { if (now.timeIntervalSinceReferenceDate - _lastSendTime.timeIntervalSinceReferenceDate) >= interval { handler() @@ -34,14 +27,14 @@ final class Throttle { } else { lastSendTime = now } - + let deadline = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(interval * 1000.0)) - + timerReference?.cancel() - + let timer = DispatchSource.makeTimerSource(queue: queue) timer.schedule(deadline: deadline) - + timer.setEventHandler(handler: { [weak timer, weak self] in self?.lastSendTime = nil handler() @@ -52,7 +45,7 @@ final class Throttle { timerReference = timer } - + func cancel() { timerReference = nil } diff --git a/Sources/DataSources/Updating.swift b/Sources/DataSources/Updating.swift index 9a95b3b..206db3f 100644 --- a/Sources/DataSources/Updating.swift +++ b/Sources/DataSources/Updating.swift @@ -1,18 +1,11 @@ -// -// Updating.swift -// DataSources -// -// Created by muukii on 8/7/17. -// Copyright © 2017 muukii. All rights reserved. -// import Foundation import DifferenceKit -public struct IndexPathDiff { +public struct IndexPathDiff: Sendable { - public struct Move { + public struct Move : Sendable { let from: IndexPath let to: IndexPath } @@ -53,7 +46,12 @@ public struct UpdateContext { } } -public protocol Updating : class { +extension UpdateContext: Sendable where Element: Sendable { +} + + +@MainActor +public protocol Updating: AnyObject { associatedtype Target associatedtype Element