Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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