From 3b79cd438e56660fb5e09b4c292e67bd087d56b6 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Wed, 21 Sep 2022 11:51:28 -0500 Subject: [PATCH] Feature/structured concurrency (#3) * WIP feature/structured-concurrency * Bump minimum swift version to 5.5 for concurrency. Remove unsafe swift flag. feature/structured-concurrency * Add MainActor notations to example views. Add complete concurrency checks to example projects. feature/structured-concurrency * Finish adding MainActor notations and Sendable conformances to library types. feature/structured-concurrency * Update workflow config. feature/structured-concurrency * Remove MainActor from configuration and style types feature/structured-concurrency * Remove MainActor from BackPortedButtonToggleStyle feature/structured-concurrency --- .github/workflows/test.yml | 19 +++--- .../xcschemes/swiftui-pick-better.xcscheme | 67 +++++++++++++++++++ Example/Example.xcodeproj/project.pbxproj | 8 +++ .../Shared/Sources/Shared/ContentView.swift | 1 + Example/Shared/Sources/Shared/Item.swift | 2 +- Example/Shared/Sources/Shared/ItemLabel.swift | 1 + Example/Shared/Sources/Shared/LazyView.swift | 1 + .../Sources/Shared/MultiValueSelection.swift | 1 + Example/Shared/Sources/Shared/RouteView.swift | 2 + .../Shared/SingleOptionalValueSelection.swift | 1 + .../Sources/Shared/SingleValueSelection.swift | 1 + Example/Shared/Sources/Shared/TabOption.swift | 2 +- Package.swift | 8 +-- Sources/PickBetter/BetterPicker.swift | 1 + .../PickBetter/BetterPickerSelection.swift | 2 + Sources/PickBetter/Internal/CellWrapper.swift | 1 + .../PlainInlineBetterPickerStyle.swift | 7 +- 17 files changed, 108 insertions(+), 17 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/swiftui-pick-better.xcscheme diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9761268..ca9f0cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,17 +11,20 @@ on: jobs: example: - runs-on: ${{ matrix.runsOn }} - env: - DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}/Contents/Developer" + runs-on: macos-12 strategy: matrix: - include: - - xcode: "Xcode_12.5.1.app" - runsOn: macOS-11 - - xcode: "Xcode_13.2.1.app" - runsOn: macOS-12 + xcode: + - "13.2.1" + - "13.4.1" + - "14.0" steps: - uses: actions/checkout@v2 + - name: Select Xcode ${{ matrix.xcode }} + run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app + - name: Format lint + run: swiftformat --lint . + - name: Lint + run: swiftlint . - name: Run tests run: xcodebuild -project ./Example/Example.xcodeproj -scheme Example test -destination platform='iOS Simulator',name='iPhone 11' -quiet -enableCodeCoverage YES -derivedDataPath "./output" \ No newline at end of file diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/swiftui-pick-better.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/swiftui-pick-better.xcscheme new file mode 100644 index 0000000..148134b --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/swiftui-pick-better.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 4ef6cc4..b63e98b 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -444,6 +444,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Debug; @@ -474,6 +475,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Release; @@ -500,6 +502,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 14.0; @@ -528,6 +531,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 14.0; @@ -679,6 +683,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 14.0; @@ -715,6 +720,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 14.0; @@ -734,6 +740,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.pick-better.ExampleUITests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = Example; @@ -752,6 +759,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.pick-better.ExampleUITests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = Example; diff --git a/Example/Shared/Sources/Shared/ContentView.swift b/Example/Shared/Sources/Shared/ContentView.swift index 36d6731..789ee73 100644 --- a/Example/Shared/Sources/Shared/ContentView.swift +++ b/Example/Shared/Sources/Shared/ContentView.swift @@ -9,6 +9,7 @@ import PickBetter import SwiftUI +@MainActor public struct ContentView: View { @State private var tab: TabOption = .singleValue @State private var isGridStyle: Bool = true diff --git a/Example/Shared/Sources/Shared/Item.swift b/Example/Shared/Sources/Shared/Item.swift index 4632980..e3af3e3 100644 --- a/Example/Shared/Sources/Shared/Item.swift +++ b/Example/Shared/Sources/Shared/Item.swift @@ -8,7 +8,7 @@ import Foundation -public struct Item: Identifiable { +public struct Item: Identifiable, Sendable { public let id: Int public init(id: Int) { diff --git a/Example/Shared/Sources/Shared/ItemLabel.swift b/Example/Shared/Sources/Shared/ItemLabel.swift index 443d2bf..8600cc5 100644 --- a/Example/Shared/Sources/Shared/ItemLabel.swift +++ b/Example/Shared/Sources/Shared/ItemLabel.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI +@MainActor public struct ItemLabel: View { private let itemId: String diff --git a/Example/Shared/Sources/Shared/LazyView.swift b/Example/Shared/Sources/Shared/LazyView.swift index 1d46192..42a6774 100644 --- a/Example/Shared/Sources/Shared/LazyView.swift +++ b/Example/Shared/Sources/Shared/LazyView.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI +@MainActor public struct LazyView: View where Content: View { private let content: () -> Content diff --git a/Example/Shared/Sources/Shared/MultiValueSelection.swift b/Example/Shared/Sources/Shared/MultiValueSelection.swift index b31e0eb..6423a93 100644 --- a/Example/Shared/Sources/Shared/MultiValueSelection.swift +++ b/Example/Shared/Sources/Shared/MultiValueSelection.swift @@ -10,6 +10,7 @@ import Foundation import PickBetter import SwiftUI +@MainActor public struct MultiValueSelection: View { private let items: [Item] private let isGridStyle: Bool diff --git a/Example/Shared/Sources/Shared/RouteView.swift b/Example/Shared/Sources/Shared/RouteView.swift index 4ad24b6..649237c 100644 --- a/Example/Shared/Sources/Shared/RouteView.swift +++ b/Example/Shared/Sources/Shared/RouteView.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI +@MainActor public struct RouterView: View where Links: View, Content: View { @Binding private var selection: AnyHashable private let links: (Binding) -> Links @@ -39,6 +40,7 @@ public struct RouterView: View where Links: View, Content: View } } +@MainActor public struct RouteView: View where Route: Hashable, Label: View, Destination: View { private let route: AnyHashable @Binding private var selection: AnyHashable diff --git a/Example/Shared/Sources/Shared/SingleOptionalValueSelection.swift b/Example/Shared/Sources/Shared/SingleOptionalValueSelection.swift index 79427af..b0f5b09 100644 --- a/Example/Shared/Sources/Shared/SingleOptionalValueSelection.swift +++ b/Example/Shared/Sources/Shared/SingleOptionalValueSelection.swift @@ -10,6 +10,7 @@ import Foundation import PickBetter import SwiftUI +@MainActor public struct SingleOptionalValueSelection: View { private let items: [Item] private let isGridStyle: Bool diff --git a/Example/Shared/Sources/Shared/SingleValueSelection.swift b/Example/Shared/Sources/Shared/SingleValueSelection.swift index dc982de..c368a84 100644 --- a/Example/Shared/Sources/Shared/SingleValueSelection.swift +++ b/Example/Shared/Sources/Shared/SingleValueSelection.swift @@ -10,6 +10,7 @@ import Foundation import PickBetter import SwiftUI +@MainActor public struct SingleValueSelection: View { private let items: [Item] private let isGridStyle: Bool diff --git a/Example/Shared/Sources/Shared/TabOption.swift b/Example/Shared/Sources/Shared/TabOption.swift index cd4bb73..ebfeecd 100644 --- a/Example/Shared/Sources/Shared/TabOption.swift +++ b/Example/Shared/Sources/Shared/TabOption.swift @@ -8,7 +8,7 @@ import Foundation -public enum TabOption { +public enum TabOption: Hashable, Sendable { case singleValue case singleOptionalValue case multiValue diff --git a/Package.swift b/Package.swift index 063132a..258b49a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,4 @@ -// swift-tools-version:5.4 -// The swift-tools-version declares the minimum version of Swift required to build this package. +// swift-tools-version:5.5 import PackageDescription @@ -19,9 +18,6 @@ let package = Package( ], dependencies: [], targets: [ - .target( - name: "PickBetter", - dependencies: [] - ), + .target(name: "PickBetter"), ] ) diff --git a/Sources/PickBetter/BetterPicker.swift b/Sources/PickBetter/BetterPicker.swift index 6978854..2fe8c0c 100644 --- a/Sources/PickBetter/BetterPicker.swift +++ b/Sources/PickBetter/BetterPicker.swift @@ -10,6 +10,7 @@ import Foundation import SwiftUI /// A custom implementation of a 'Picker' UI element with less magic than SwiftUI's provided `Picker` +@MainActor public struct BetterPicker: View where SelectionBox: BetterPickerSelection, ItemContent: View { diff --git a/Sources/PickBetter/BetterPickerSelection.swift b/Sources/PickBetter/BetterPickerSelection.swift index d03a36c..c463486 100644 --- a/Sources/PickBetter/BetterPickerSelection.swift +++ b/Sources/PickBetter/BetterPickerSelection.swift @@ -104,3 +104,5 @@ public struct SingleSelectionWrapper: BetterPickerSele // Required by protocol conformance } } + +extension SingleSelectionWrapper: Sendable where SelectionValue: Sendable {} diff --git a/Sources/PickBetter/Internal/CellWrapper.swift b/Sources/PickBetter/Internal/CellWrapper.swift index c9dcbad..053fe0a 100644 --- a/Sources/PickBetter/Internal/CellWrapper.swift +++ b/Sources/PickBetter/Internal/CellWrapper.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI +@MainActor struct CellWrapper: View where Content: View { let isSelected: Bool let content: Content diff --git a/Sources/PickBetter/PlainInlineBetterPickerStyle.swift b/Sources/PickBetter/PlainInlineBetterPickerStyle.swift index 25348fe..ecb12ff 100644 --- a/Sources/PickBetter/PlainInlineBetterPickerStyle.swift +++ b/Sources/PickBetter/PlainInlineBetterPickerStyle.swift @@ -36,16 +36,18 @@ public struct PlainInlineBetterPickerStyle: BetterPickerStyle { } #if DEBUG - private struct PreviewItem: Identifiable { + private struct PreviewItem: Identifiable, Sendable { let id: String } + @MainActor private var items: [PreviewItem] = ["A", "B", "C"].map { PreviewItem(id: $0) } private func itemContent(_ item: PreviewItem) -> some View { Text(item.id) } + @MainActor struct PlainInlineBetterPickerStyle_Previews: PreviewProvider { static var previews: some View { Group { @@ -56,6 +58,7 @@ public struct PlainInlineBetterPickerStyle: BetterPickerStyle { } } + @MainActor private struct OptionalSelectionPreview: View { @State private var selection: PreviewItem.ID? = nil @@ -68,6 +71,7 @@ public struct PlainInlineBetterPickerStyle: BetterPickerStyle { } } + @MainActor private struct SingleSelectionPreview: View { @State private var selection: PreviewItem.ID = items.first!.id @@ -80,6 +84,7 @@ public struct PlainInlineBetterPickerStyle: BetterPickerStyle { } } + @MainActor private struct MultiSelectionPreview: View { @State private var selection: Set = []