diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3353a0e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + test: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v7 + - name: Install SwiftLint + run: brew install swiftlint + - name: Lint + run: | + swiftlint --strict + - name: Build + run: | + xcodebuild -scheme CellKit -destination 'generic/platform=iOS' build + xcodebuild -scheme DiffableCellKit -destination 'generic/platform=iOS' build + - name: Test example app + run: | + RUNTIME=$(xcrun simctl list runtimes available --json | jq -r '[.runtimes[] | select(.platform == "iOS")] | sort_by(.version | split(".") | map(tonumber)) | last | .identifier') + UDID=$(xcrun simctl list devices available --json | jq -r --arg RT "$RUNTIME" '.devices[$RT][]? | select(.name | test("^iPhone")) | .udid' | head -1) + xcodebuild test \ + -project Example/Example.xcodeproj \ + -scheme Example \ + -destination "platform=iOS Simulator,id=$UDID" \ + -parallel-testing-enabled NO \ + -retry-tests-on-failure \ + CODE_SIGNING_ALLOWED=NO diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 723488b..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Test - -on: - - pull_request - -jobs: - test: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v2 - - name: Lint - run: | - brew install swiftlint - swiftlint --strict - - name: Pod lib lint - run: | - gem install bundler - bundle install --jobs 4 --retry 3 - bundle exec pod lib lint - - name: Build example app - run: | - xcodebuild -project Example/Example.xcodeproj -scheme Example -destination 'platform=iOS Simulator,name=iPhone 8' -sdk iphonesimulator test diff --git a/CellKit.podspec b/CellKit.podspec deleted file mode 100644 index cf1dee0..0000000 --- a/CellKit.podspec +++ /dev/null @@ -1,30 +0,0 @@ -Pod::Spec.new do |s| - s.name = "CellKit" - s.version = "0.8.1" - s.summary = "Table View and Collection View data source wrapper" - s.description = <<-DESC - Generic abstraction over table/collection data source - with automatic cell registration of - cell models and automatic animations using diffs. - DESC - s.homepage = "https://github.com/futuredapp/CellKit" - s.license = { type: "MIT", file: "LICENSE" } - s.author = { "Matěj K. Jirásek": "matej.jirasek@futured.app", "Petr Zvoníček": "zvonicek@gmail.com" } - s.social_media_url = "https://twitter.com/Futuredapps" - s.ios.deployment_target = "9.0" - s.tvos.deployment_target = "9.0" - s.swift_versions = ["4.2", "5.0", "5.1"] - s.source = { git: "https://github.com/futuredapp/CellKit.git", tag: s.version.to_s } - s.frameworks = ["Foundation", "UIKit"] - s.default_subspec = "Core" - - s.subspec "Core" do |ss| - ss.source_files = "Sources/CellKit/*" - end - - s.subspec "Diffable" do |ss| - ss.dependency "CellKit/Core" - ss.source_files = "Sources/DiffableCellKit/*" - ss.dependency "DifferenceKit", "~> 1.0" - end -end diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index b1b2359..925c242 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ E7BB267F22D4B63800A7713E /* PhonesHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7BB267122D4B63800A7713E /* PhonesHeader.swift */; }; E7BB268022D4B63800A7713E /* PhonesHeaderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7BB267222D4B63800A7713E /* PhonesHeaderModel.swift */; }; E7BB268122D4B63800A7713E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7BB267322D4B63800A7713E /* AppDelegate.swift */; }; + E7BB268122D4B63800A7713F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7BB267322D4B63800A7713F /* SceneDelegate.swift */; }; E7BCF6BE22DF145F00E71478 /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7BCF6BD22DF145F00E71478 /* ExampleUITests.swift */; }; /* End PBXBuildFile section */ @@ -66,6 +67,7 @@ E7BB267122D4B63800A7713E /* PhonesHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhonesHeader.swift; sourceTree = ""; }; E7BB267222D4B63800A7713E /* PhonesHeaderModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhonesHeaderModel.swift; sourceTree = ""; }; E7BB267322D4B63800A7713E /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + E7BB267322D4B63800A7713F /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; E7BB267422D4B63800A7713E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E7BCF6BB22DF145F00E71478 /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; E7BCF6BD22DF145F00E71478 /* ExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITests.swift; sourceTree = ""; }; @@ -141,6 +143,7 @@ isa = PBXGroup; children = ( E7BB267322D4B63800A7713E /* AppDelegate.swift */, + E7BB267322D4B63800A7713F /* SceneDelegate.swift */, BEFF59F523743F1000B3A342 /* Launch.storyboard */, ); path = Supporting; @@ -315,6 +318,7 @@ E7BB268022D4B63800A7713E /* PhonesHeaderModel.swift in Sources */, E7BB267B22D4B63800A7713E /* DeviceiOSCellModel.swift in Sources */, E7BB268122D4B63800A7713E /* AppDelegate.swift in Sources */, + E7BB268122D4B63800A7713F /* SceneDelegate.swift in Sources */, E7BB267F22D4B63800A7713E /* PhonesHeader.swift in Sources */, E7BB267522D4B63800A7713E /* ViewController.swift in Sources */, E7BB267922D4B63800A7713E /* DeviceAndroidCellModel.swift in Sources */, @@ -409,12 +413,14 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -463,10 +469,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_VERSION = 4.0; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -490,7 +498,7 @@ DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -499,7 +507,9 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 5.0; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -520,7 +530,7 @@ DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -530,7 +540,9 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; @@ -547,7 +559,7 @@ CODE_SIGN_STYLE = Automatic; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Tests/ExampleUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -558,7 +570,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.thefuntasty.ExampleUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 5.0; + SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = Example; }; @@ -576,7 +589,7 @@ CODE_SIGN_STYLE = Automatic; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Tests/ExampleUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -587,7 +600,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; + SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = Example; }; diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme new file mode 100644 index 0000000..ce41de4 --- /dev/null +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Sources/Info.plist b/Example/Sources/Info.plist index 39efe35..0f2f25e 100644 --- a/Example/Sources/Info.plist +++ b/Example/Sources/Info.plist @@ -22,8 +22,25 @@ UILaunchStoryboardName Storyboard - UIMainStoryboardFile - Main + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UIRequiredDeviceCapabilities armv7 diff --git a/Example/Sources/Supporting/AppDelegate.swift b/Example/Sources/Supporting/AppDelegate.swift index cea377f..0473aa8 100644 --- a/Example/Sources/Supporting/AppDelegate.swift +++ b/Example/Sources/Supporting/AppDelegate.swift @@ -1,10 +1,7 @@ import UIKit -@UIApplicationMain +@main final class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { true } diff --git a/Example/Sources/Supporting/SceneDelegate.swift b/Example/Sources/Supporting/SceneDelegate.swift new file mode 100644 index 0000000..1202cac --- /dev/null +++ b/Example/Sources/Supporting/SceneDelegate.swift @@ -0,0 +1,5 @@ +import UIKit + +final class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? +} diff --git a/Example/Sources/Table Cells/AndroidCell/DeviceAndroidCellModel.swift b/Example/Sources/Table Cells/AndroidCell/DeviceAndroidCellModel.swift index b831374..e0ac16d 100644 --- a/Example/Sources/Table Cells/AndroidCell/DeviceAndroidCellModel.swift +++ b/Example/Sources/Table Cells/AndroidCell/DeviceAndroidCellModel.swift @@ -2,7 +2,7 @@ import CellKit import DiffableCellKit import UIKit -struct DeviceAndroidCellModel: CellConvertible, DifferentiableCellModel, DeletableCellModel { +struct DeviceAndroidCellModel: @MainActor CellConvertible, @MainActor DifferentiableCellModel, DeletableCellModel { var domainIdentifier: Int { name.hashValue @@ -19,7 +19,7 @@ struct DeviceAndroidCellModel: CellConvertible, DifferentiableCellModel, Deletab var numberOfTaps: Int let name: String - let cellHeight: CGFloat = 60.0 + let cellHeight: Double = 60.0 let registersLazily: Bool = false let allowDelete: Bool = true } diff --git a/Example/Sources/Table Cells/IOSCell/DeviceiOSCellModel.swift b/Example/Sources/Table Cells/IOSCell/DeviceiOSCellModel.swift index b83324c..15cfa06 100644 --- a/Example/Sources/Table Cells/IOSCell/DeviceiOSCellModel.swift +++ b/Example/Sources/Table Cells/IOSCell/DeviceiOSCellModel.swift @@ -2,7 +2,7 @@ import CellKit import DiffableCellKit import UIKit -struct DeviceiOSCellModel: CellConvertible, DifferentiableCellModel, DeletableCellModel { +struct DeviceiOSCellModel: @MainActor CellConvertible, @MainActor DifferentiableCellModel, DeletableCellModel { var domainIdentifier: Int { name.hashValue @@ -19,7 +19,7 @@ struct DeviceiOSCellModel: CellConvertible, DifferentiableCellModel, DeletableCe var numberOfTaps: Int let name: String - let cellHeight: CGFloat = 60.0 + let cellHeight: Double = 60.0 let registersLazily: Bool = false let allowDelete: Bool = true } diff --git a/Example/Sources/Table Cells/NibTableViewCell.swift b/Example/Sources/Table Cells/NibTableViewCell.swift index 041f8ea..9b0e341 100644 --- a/Example/Sources/Table Cells/NibTableViewCell.swift +++ b/Example/Sources/Table Cells/NibTableViewCell.swift @@ -2,7 +2,7 @@ import CellKit import DiffableCellKit import UIKit -struct NibTableViewCellModel: ReusableCellConvertible, DifferentiableCellModel { +struct NibTableViewCellModel: @MainActor ReusableCellConvertible, @MainActor DifferentiableCellModel { var domainIdentifier: Int { text.hashValue @@ -19,7 +19,7 @@ struct NibTableViewCellModel: ReusableCellConvertible, DifferentiableCellModel { typealias Cell = NibTableViewCell let text: String - let cellHeight: CGFloat = 170.0 + let cellHeight: Double = 170.0 } final class NibTableViewCell: UITableViewCell, CellConfigurable { diff --git a/Example/Sources/Table Headers/PhonesHeaderModel.swift b/Example/Sources/Table Headers/PhonesHeaderModel.swift index 5cb32d2..e2093c8 100644 --- a/Example/Sources/Table Headers/PhonesHeaderModel.swift +++ b/Example/Sources/Table Headers/PhonesHeaderModel.swift @@ -6,7 +6,7 @@ struct PhonesHeaderModel { let title: String } -extension PhonesHeaderModel: SupplementaryViewModel, CellConvertible { +extension PhonesHeaderModel: @MainActor SupplementaryViewModel, @MainActor CellConvertible { typealias Cell = PhonesHeader var height: Double { diff --git a/Example/Sources/ViewController.swift b/Example/Sources/ViewController.swift index c29c242..4182ced 100644 --- a/Example/Sources/ViewController.swift +++ b/Example/Sources/ViewController.swift @@ -4,8 +4,8 @@ import UIKit enum Constants { - static var iPhones = ["iPhone", "iPhone 3G", "iPhone 3GS", "iPhone 4", "iPhone 4S", "iPhone 5", "iPhone 5s", "iPhone 6", "iPhone 6s", "iPhone SE", "iPhone 7", "iPhone 8", "iPhone X"] - static var androids = ["Samsung Galaxy Note", "OnePlus 2", "Nexus 6", "Huawei P9", "Kindle Fire", "Samsung Galaxy S6", "Nexus 5"] + static let iPhones = ["iPhone", "iPhone 3G", "iPhone 3GS", "iPhone 4", "iPhone 4S", "iPhone 5", "iPhone 5s", "iPhone 6", "iPhone 6s", "iPhone SE", "iPhone 7", "iPhone 8", "iPhone X"] + static let androids = ["Samsung Galaxy Note", "OnePlus 2", "Nexus 6", "Huawei P9", "Kindle Fire", "Samsung Galaxy S6", "Nexus 5"] } final class ViewController: UITableViewController { diff --git a/Example/Tests/ExampleUITests/ExampleUITests.swift b/Example/Tests/ExampleUITests/ExampleUITests.swift index f4951c7..010bfcf 100644 --- a/Example/Tests/ExampleUITests/ExampleUITests.swift +++ b/Example/Tests/ExampleUITests/ExampleUITests.swift @@ -1,14 +1,58 @@ import XCTest final class ExampleUITests: XCTestCase { + private var app: XCUIApplication! + private var table: XCUIElement! + override func setUp() { super.setUp() continueAfterFailure = false - XCUIApplication().launch() + app = XCUIApplication() + app.launch() + table = app.tables.firstMatch + XCTAssertTrue(table.waitForExistence(timeout: 5)) + } + + func testLaunchShowsSections() { + XCTAssertTrue(table.staticTexts["Welcome!"].exists) + XCTAssertTrue(table.staticTexts["iPhone tapped: 0"].exists) + XCTAssertGreaterThan(table.cells.count, 1) + } + + func testTapUpdatesCellThroughDiff() { + let cell = table.staticTexts["iPhone tapped: 0"] + XCTAssertTrue(cell.exists) + cell.tap() + + XCTAssertTrue(table.staticTexts["iPhone tapped: 1"].waitForExistence(timeout: 5)) + XCTAssertFalse(table.staticTexts["iPhone tapped: 0"].exists) + } + + func testInsertAndResetThroughDiff() { + // The name label of whatever device cell is currently first in the "Cell Phones" section. + let firstDeviceName = table.cells.element(boundBy: 1).staticTexts + .matching(NSPredicate(format: "label CONTAINS 'tapped:'")) + .firstMatch + XCTAssertEqual(firstDeviceName.label, "iPhone tapped: 0") + + app.navigationBars.buttons["+iPhone"].tap() + wait(until: firstDeviceName, "label != %@", "iPhone tapped: 0") + XCTAssertTrue(firstDeviceName.label.hasPrefix("iPhone")) + XCTAssertTrue(firstDeviceName.label.hasSuffix(" tapped: 0")) + XCTAssertTrue(table.staticTexts["iPhone tapped: 0"].exists) + + app.navigationBars.buttons["+Android"].tap() + wait(until: firstDeviceName, "NOT label BEGINSWITH 'iPhone'") + XCTAssertTrue(firstDeviceName.label.hasSuffix(" tapped: 0")) + + app.navigationBars.buttons["Reset"].tap() + wait(until: firstDeviceName, "label == %@", "iPhone tapped: 0") } - func testLaunch() { - XCTAssert(true) + private func wait(until element: XCUIElement, _ format: String, _ arguments: CVarArg..., timeout: TimeInterval = 5) { + let predicate = NSPredicate(format: format, argumentArray: arguments) + let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element) + XCTAssertEqual(XCTWaiter.wait(for: [expectation], timeout: timeout), .completed, "Timed out waiting for: \(format)") } } diff --git a/Gemfile b/Gemfile deleted file mode 100644 index ef3f1b5..0000000 --- a/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -source "https://rubygems.org" - -gem "cocoapods", "~> 1.9" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 6730098..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,90 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.2) - activesupport (4.2.11.3) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - algoliasearch (1.27.3) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) - atomos (0.1.3) - claide (1.0.3) - cocoapods (1.9.3) - activesupport (>= 4.0.2, < 5) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.9.3) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.2.2, < 2.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.6.6) - nap (~> 1.0) - ruby-macho (~> 1.4) - xcodeproj (>= 1.14.0, < 2.0) - cocoapods-core (1.9.3) - activesupport (>= 4.0.2, < 6) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.4) - cocoapods-downloader (1.3.0) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.0) - cocoapods-stats (1.1.0) - cocoapods-trunk (1.5.0) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.2.0) - colored2 (3.1.2) - concurrent-ruby (1.1.6) - escape (0.0.4) - ethon (0.12.0) - ffi (>= 1.3.0) - ffi (1.13.1) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - httpclient (2.8.3) - i18n (0.9.5) - concurrent-ruby (~> 1.0) - json (2.3.1) - minitest (5.14.1) - molinillo (0.6.6) - nanaimo (0.2.6) - nap (1.1.0) - netrc (0.11.0) - ruby-macho (1.4.0) - thread_safe (0.3.6) - typhoeus (1.4.0) - ethon (>= 0.9.0) - tzinfo (1.2.7) - thread_safe (~> 0.1) - xcodeproj (1.17.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.2.6) - -PLATFORMS - ruby - -DEPENDENCIES - cocoapods (~> 1.9) - -BUNDLED WITH - 2.1.4 diff --git a/Package.swift b/Package.swift index bc0094b..6239ef4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,17 @@ -// swift-tools-version:5.1 +// swift-tools-version:6.2 import PackageDescription +let swiftSettings: [SwiftSetting] = [ + .swiftLanguageMode(.v6), + .defaultIsolation(MainActor.self), + .enableUpcomingFeature("NonisolatedNonsendingByDefault"), + .enableUpcomingFeature("InferIsolatedConformances") +] + let package = Package( name: "CellKit", - platforms: [.iOS(.v9), .tvOS(.v9)], + platforms: [.iOS(.v15), .tvOS(.v15)], products: [ .library( name: "CellKit", @@ -14,14 +21,16 @@ let package = Package( targets: ["DiffableCellKit"]) ], dependencies: [ - .package(url: "https://github.com/ra1028/DifferenceKit.git", from: "1.0.0") + .package(url: "https://github.com/ra1028/DifferenceKit.git", from: "1.3.0") ], targets: [ .target( name: "CellKit", - dependencies: []), + dependencies: [], + swiftSettings: swiftSettings), .target( name: "DiffableCellKit", - dependencies: ["CellKit", "DifferenceKit"]) + dependencies: ["CellKit", "DifferenceKit"], + swiftSettings: swiftSettings) ] ) diff --git a/README.md b/README.md index a42615b..fdbee7f 100644 --- a/README.md +++ b/README.md @@ -2,37 +2,59 @@ # CellKit -![Cocoapods](https://img.shields.io/cocoapods/v/CellKit.svg) -![Cocoapods platforms](https://img.shields.io/cocoapods/p/CellKit.svg) -![License](https://img.shields.io/cocoapods/l/CellKit.svg) +![Swift 6](https://img.shields.io/badge/Swift-6-orange.svg) +![Platforms](https://img.shields.io/badge/platform-iOS%20%7C%20tvOS-lightgrey.svg) +![License](https://img.shields.io/github/license/futuredapp/CellKit.svg) CellKit is a Swift package that streamlines the workflow with cells in UITableView and UICollectionView. No more registering cell classes or XIBs, no more dequeueing cells and setting its view models. CellKit handles this all. +## Requirements + +- iOS 15+ / tvOS 15+ +- Xcode 26+ (Swift 6.2 toolchain) + ## Installation ### Swift Package -Add following line to your swift package dependencies, or in Xcode, go to `File -> Swift Packages -> Add Package Dependency` and type in URL address of this repository. +Add following line to your swift package dependencies, or in Xcode, go to `File -> Add Package Dependencies` and type in URL address of this repository. ```swift -.package(url: "https://github.com/futuredapp/CellKit", from: "0.8.1") +.package(url: "https://github.com/futuredapp/CellKit", from: "1.0.0") ``` Optionally you can add `DiffableCellKit`. -### CocoaPods +## Concurrency + +CellKit is compiled in the Swift 6 language mode with `defaultIsolation` set to `MainActor`, so its entire API is main actor-isolated. Because CellKit is a `UITableView`/`UICollectionView` data source layer, every one of its protocols is only ever exercised on the main thread anyway. -Add following line to your `Podfile` and then run `pod install`: +**If your module uses the default `nonisolated` isolation**, conformances need no annotations at all: -```ruby -pod 'CellKit', '~> 0.8' +```swift +struct DeviceCellModel: ReusableCellConvertible, DifferentiableCellModel { + typealias Cell = DeviceCell + // ... +} ``` -Optionally you can add `DiffableCellKit` subspec: -```ruby -pod 'CellKit', '~> 0.8', subspecs: ['Diffable'] +**If your module is itself main actor by default** (`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`), isolate each conformance explicitly: + +```swift +struct DeviceCellModel: @MainActor ReusableCellConvertible, @MainActor DifferentiableCellModel { + typealias Cell = DeviceCell + // ... +} ``` +Conformance isolation is not inferred for conformances to protocols that are themselves main actor-isolated, and `InferIsolatedConformances` — which Xcode's *Approachable Concurrency* setting turns on — does not cover that case either. See [SE-0470](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0470-isolated-conformances.md). + +A main actor-isolated module already needs one such annotation per model type against CellKit today; isolating CellKit's own API makes it one per CellKit protocol you conform to. The `Example` app is configured this way and shows what it looks like. + +Swift 6.4 (Xcode 27) infers these conformances correctly and no longer needs the annotations; they are harmless to keep while you still build with Xcode 26. + +Subclasses of `CellModelDataSource`, `AbstractDataSource` and `DifferentiableCellModelDataSource` are main actor-isolated, and so are your subclasses of them. + ## Usage CellKit provides a data source and a section model which you fill with your cells, headers and footer models. All you're left to do is to define your cell view and your cell model with protocol conformance to CellConvertible and CellConfigurable and CellKit will handle the rest. diff --git a/Sources/CellKit/DataSource.swift b/Sources/CellKit/DataSource.swift index e897be7..96a810b 100644 --- a/Sources/CellKit/DataSource.swift +++ b/Sources/CellKit/DataSource.swift @@ -152,7 +152,7 @@ extension AbstractDataSource: UITableViewDataSource { public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { let height = header(in: section)?.height - return height.flatMap { CGFloat($0) } ?? CGFloat.leastNonzeroMagnitude + return height.flatMap { CGFloat($0) } ?? CGFloat.leastNonzeroMagnitude } public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { @@ -164,7 +164,7 @@ extension AbstractDataSource: UITableViewDataSource { public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { let height = footer(in: section)?.height - return height.flatMap { CGFloat($0) } ?? CGFloat.leastNonzeroMagnitude + return height.flatMap { CGFloat($0) } ?? CGFloat.leastNonzeroMagnitude } } diff --git a/Sources/CellKit/DiffableDataSources/CollectionSupplementaryViewModel.swift b/Sources/CellKit/DiffableDataSources/CollectionSupplementaryViewModel.swift index 261f90d..956d9f7 100644 --- a/Sources/CellKit/DiffableDataSources/CollectionSupplementaryViewModel.swift +++ b/Sources/CellKit/DiffableDataSources/CollectionSupplementaryViewModel.swift @@ -1,4 +1,3 @@ -@available(iOS 13.0, tvOS 13.0, *) public protocol CollectionSupplementaryViewModel: ReusableView { var kind: SupplementaryElementKind { get } diff --git a/Sources/CellKit/DiffableDataSources/LazyCellProvider.swift b/Sources/CellKit/DiffableDataSources/LazyCellProvider.swift index 0c3da50..42e91ca 100644 --- a/Sources/CellKit/DiffableDataSources/LazyCellProvider.swift +++ b/Sources/CellKit/DiffableDataSources/LazyCellProvider.swift @@ -1,6 +1,5 @@ import UIKit -@available(iOS 13.0, tvOS 13.0, *) public final class LazyCellProvider { private var registeredIdentifiers: Set diff --git a/Sources/CellKit/DiffableDataSources/LazySupplementaryViewProvider.swift b/Sources/CellKit/DiffableDataSources/LazySupplementaryViewProvider.swift index fbdc256..ee31004 100644 --- a/Sources/CellKit/DiffableDataSources/LazySupplementaryViewProvider.swift +++ b/Sources/CellKit/DiffableDataSources/LazySupplementaryViewProvider.swift @@ -1,6 +1,5 @@ import UIKit -@available(iOS 13.0, tvOS 13.0, *) public final class LazySupplementaryViewProvider { private var registeredIdentifiers: [String: Set] diff --git a/Sources/CellKit/DiffableDataSources/SupplementaryElementKind.swift b/Sources/CellKit/DiffableDataSources/SupplementaryElementKind.swift index ea06d69..96c50e5 100644 --- a/Sources/CellKit/DiffableDataSources/SupplementaryElementKind.swift +++ b/Sources/CellKit/DiffableDataSources/SupplementaryElementKind.swift @@ -1,6 +1,5 @@ import UIKit -@available(iOS 13.0, tvOS 13.0, *) public enum SupplementaryElementKind: RawRepresentable { case header case footer diff --git a/Sources/DiffableCellKit/DifferentiableCellModel.swift b/Sources/DiffableCellKit/DifferentiableCellModel.swift index fb8e836..0128973 100644 --- a/Sources/DiffableCellKit/DifferentiableCellModel.swift +++ b/Sources/DiffableCellKit/DifferentiableCellModel.swift @@ -1,7 +1,5 @@ -import DifferenceKit -#if SWIFT_PACKAGE import CellKit -#endif +import DifferenceKit /// Support for determining whether cell model belongs to a certain cell, whether cell should be inserted, removed, updated or moved. public protocol DifferentiableCellModel: CellModel { diff --git a/Sources/DiffableCellKit/DifferentiableCellModelDataSource.swift b/Sources/DiffableCellKit/DifferentiableCellModelDataSource.swift index 27d8ab5..a91ba9c 100644 --- a/Sources/DiffableCellKit/DifferentiableCellModelDataSource.swift +++ b/Sources/DiffableCellKit/DifferentiableCellModelDataSource.swift @@ -1,8 +1,6 @@ +import CellKit import DifferenceKit import UIKit -#if SWIFT_PACKAGE -import CellKit -#endif open class DifferentiableCellModelDataSource: AbstractDataSource, DataSource { diff --git a/Sources/DiffableCellKit/DifferentiableCellModelSection.swift b/Sources/DiffableCellKit/DifferentiableCellModelSection.swift index 5a5488c..a9d7ec4 100644 --- a/Sources/DiffableCellKit/DifferentiableCellModelSection.swift +++ b/Sources/DiffableCellKit/DifferentiableCellModelSection.swift @@ -1,7 +1,5 @@ -import DifferenceKit -#if SWIFT_PACKAGE import CellKit -#endif +import DifferenceKit public typealias DifferentiableCellModelSection = GenericCellModelSection