Skip to content

Commit

Permalink
Sendable (#19)
Browse files Browse the repository at this point in the history
SectionDataController is now sendable but updating from only main-actor.
  • Loading branch information
muukii authored Nov 20, 2024
1 parent 89112a1 commit 70d9e82
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 86 deletions.
6 changes: 4 additions & 2 deletions Development/DataSources.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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;
Expand All @@ -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",
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion Development/DataSourcesDemo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import UIKit

@UIApplicationMain
@main
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
Expand Down
5 changes: 3 additions & 2 deletions Development/DataSourcesDemo/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ struct ModelB: Model, Differentiable, Equatable {
}
}

@MainActor
final class ViewModel {

let section0 = BehaviorRelay<[ModelA]>(value: [])
Expand Down Expand Up @@ -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()))
}
Expand All @@ -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))
}
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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"]),
],
Expand Down
113 changes: 65 additions & 48 deletions Sources/DataSources/SectionDataController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<ItemType, AdapterType>.UpdateMode, immediately: Bool, completion: @escaping () -> Void)

func asSectionDataController() -> SectionDataController<ItemType, AdapterType>
Expand Down Expand Up @@ -56,12 +57,12 @@ final class AnySectionDataController<A: Updating> {
}

/// DataSource for a section
public final class SectionDataController<T: Differentiable, A: Updating>: SectionDataControllerType where A.Element == T {
public final class SectionDataController<T: Differentiable & Sendable, A: Updating & Sendable>: 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
}
Expand All @@ -72,18 +73,26 @@ public final class SectionDataController<T: Differentiable, A: Updating>: 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<State> = .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

Expand Down Expand Up @@ -113,8 +122,11 @@ public final class SectionDataController<T: Differentiable, A: Updating>: 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.
Expand All @@ -137,9 +149,12 @@ public final class SectionDataController<T: Differentiable, A: Updating>: 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
Expand All @@ -152,40 +167,41 @@ public final class SectionDataController<T: Differentiable, A: Updating>: 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<ItemType, AdapterType> {
return self
Expand All @@ -208,26 +224,27 @@ public final class SectionDataController<T: Differentiable, A: Updating>: 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()
}

Expand Down Expand Up @@ -258,7 +275,7 @@ public final class SectionDataController<T: Differentiable, A: Updating>: Sectio

group.enter()

self.snapshot = changeset.data
self._snapshot.withLock { $0 = changeset.data }

let updateContext = UpdateContext.init(
diff: .init(diff: changeset, targetSection: targetSection),
Expand Down Expand Up @@ -299,7 +316,7 @@ public final class SectionDataController<T: Differentiable, A: Updating>: Sectio
}

group.notify(queue: .main) {
self.state = .idle
self.state.withLock { $0 = .idle }
completion()
}

Expand Down
35 changes: 14 additions & 21 deletions Sources/DataSources/Throttle.swift
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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()
Expand All @@ -52,7 +45,7 @@ final class Throttle {

timerReference = timer
}

func cancel() {
timerReference = nil
}
Expand Down
18 changes: 8 additions & 10 deletions Sources/DataSources/Updating.swift
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down Expand Up @@ -53,7 +46,12 @@ public struct UpdateContext<Element> {
}
}

public protocol Updating : class {
extension UpdateContext: Sendable where Element: Sendable {
}


@MainActor
public protocol Updating: AnyObject {

associatedtype Target
associatedtype Element
Expand Down

0 comments on commit 70d9e82

Please sign in to comment.