Skip to content

Commit

Permalink
Merge pull request #10 from MFB-Technologies-Inc/feature/segmented-pi…
Browse files Browse the repository at this point in the history
…cker-animation

Feature/segmented picker animation
  • Loading branch information
roanutil authored Nov 15, 2024
2 parents 1b42855 + 24c6288 commit e4117a6
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 2 deletions.
4 changes: 4 additions & 0 deletions Example/Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
49E598B127B230AF00C1F2CF /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E598B027B230AF00C1F2CF /* ExampleApp.swift */; };
49E598B827B230AF00C1F2CF /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49E598B727B230AF00C1F2CF /* Preview Assets.xcassets */; };
49E598CC27B230AF00C1F2CF /* SingleValueUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E598CB27B230AF00C1F2CF /* SingleValueUITests.swift */; };
94AB9E8C2CE39FDC008967E2 /* SegmentedUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AB9E8B2CE39FD5008967E2 /* SegmentedUITests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -60,6 +61,7 @@
49E598C727B230AF00C1F2CF /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
49E598CB27B230AF00C1F2CF /* SingleValueUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleValueUITests.swift; sourceTree = "<group>"; };
49E598DB27B2312500C1F2CF /* swiftui-pick-better */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "swiftui-pick-better"; path = ..; sourceTree = "<group>"; };
94AB9E8B2CE39FD5008967E2 /* SegmentedUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedUITests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -177,6 +179,7 @@
49E598CA27B230AF00C1F2CF /* ExampleUITests */ = {
isa = PBXGroup;
children = (
94AB9E8B2CE39FD5008967E2 /* SegmentedUITests.swift */,
4994CA8727EE729C00F54306 /* MutliValueUITests.swift */,
49E598CB27B230AF00C1F2CF /* SingleValueUITests.swift */,
4994CA6F27EE23C100F54306 /* SingleOptionalValueTests.swift */,
Expand Down Expand Up @@ -403,6 +406,7 @@
49A241AE2800E7BA003DD15F /* XCUIElement+Trigger.swift in Sources */,
4994CA7027EE23C100F54306 /* SingleOptionalValueTests.swift in Sources */,
4994CA8027EE2B6E00F54306 /* XCUIElementQuery+ExactlyOneMatching.swift in Sources */,
94AB9E8C2CE39FDC008967E2 /* SegmentedUITests.swift in Sources */,
4994CA8927EE72B300F54306 /* MutliValueUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
23 changes: 23 additions & 0 deletions Example/ExampleUITests/ExampleUITestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ class ExampleUITestCase: XCUITestCase {
try allElements().matching(identifier: "multiValueSectionNavItem").firstMatch
}

func segmentedNavItem() throws -> XCUIElement {
try allElements().matching(identifier: "segmentedNavItem").firstMatch
}

func singleValuePicker() throws -> XCUIElement {
try allElements().matching(identifier: "singleValuePicker").exactlyOneMatch()
}
Expand All @@ -91,6 +95,25 @@ class ExampleUITestCase: XCUITestCase {
try allElements().matching(identifier: "multiValuePicker").exactlyOneMatch()
}

func segmentedPicker() throws -> XCUIElement {
try allElements().matching(identifier: "segmentedPicker").firstMatch
}

func segmentedButtonZero() throws -> XCUIElement {
try allElements().children(matching: .button)
.element(matching: elementCompoundOrPredicate(labeled: ["Segmented 0"]))
}

func segmentedButtonOne() throws -> XCUIElement {
try allElements().children(matching: .button)
.element(matching: elementCompoundOrPredicate(labeled: ["Segmented 1"]))
}

func segmentedButtonTwo() throws -> XCUIElement {
try allElements().children(matching: .button)
.element(matching: elementCompoundOrPredicate(labeled: ["Segmented 2"]))
}

func cellZero() throws -> XCUIElement {
try allElements().children(matching: .cell)
.element(matching: elementCompoundOrPredicate(labeled: ["Cell - 0", "Cell - 0, Selected"]))
Expand Down
65 changes: 65 additions & 0 deletions Example/ExampleUITests/SegmentedUITests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SegmentedUITests.swift
// PickBetter
//
// Copyright © 2024 MFB Technologies, Inc. All rights reserved. All rights reserved.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

import XCTest
#if os(iOS)
import Example
#elseif os(macOS)
import Example_macOS
#endif

final class SegmentedUITests: ExampleUITestCase {
@MainActor
override var _sectionNavItem: (@MainActor () throws -> XCUIElement)? {
segmentedNavItem
}

@MainActor
override var _picker: (@MainActor () throws -> XCUIElement)? {
segmentedPicker
}

func testSelectSegmentedPickerItems() async throws {
_ = try sectionNavItem().waitForExistence(timeout: 1)
XCTAssert(try sectionNavItem().exists, "Navigation link/tab for 'Segmented' picker must exist")
try sectionNavItem().trigger()

_ = try picker().waitForExistence(timeout: 1)
XCTAssert(try picker().exists, "Segmented picker must exist")
_ = try segmentedButtonZero().waitForExistence(timeout: 1)
_ = try segmentedButtonOne().waitForExistence(timeout: 1)

XCTAssert(try segmentedButtonZero().exists, "Cell Zero must exist")
XCTAssert(try segmentedButtonOne().exists, "Cell One must exist")
XCTAssert(
try segmentedButtonZero().isSelected && (!segmentedButtonOne().isSelected),
"Initial state should have cell Zero selected"
)

try segmentedButtonOne().trigger()
XCTAssert(try segmentedButtonOne().isSelected, "'1' should be selected after being tapped")
XCTAssert(try !segmentedButtonZero().isSelected, "'0' should not be selected before being tapped.")

try segmentedButtonTwo().trigger()
XCTAssert(
try segmentedButtonTwo().isSelected,
"'2' should still be selected without being tapped again to deselect"
)
XCTAssert(try !segmentedButtonOne().isSelected, "'1' should be selected after being tapped.")

try segmentedButtonOne().trigger()
XCTAssert(
try segmentedButtonOne().isSelected,
"'1' should still be selected without being tapped again to deselect"
)
XCTAssert(
try !segmentedButtonTwo().isSelected,
"'2' should no longer be selected after being tapped again to deselect"
)
}
}
21 changes: 21 additions & 0 deletions Example/Shared/Sources/Shared/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public struct ContentView: View {
private var singleSectionNavItem: String { "singleValueSectionNavItem" }
private var singleOptionalSectionNavItem: String { "singleOptionalValueSectionNavItem" }
private var multiSectionNavItem: String { "multiValueSectionNavItem" }
private var segmentedNavItem: String { "segmentedNavItem" }

// MARK: Content

Expand Down Expand Up @@ -111,6 +112,22 @@ public struct ContentView: View {
#endif
}

private func segmentedPicker() -> some View {
SegmentedPickerView(items: Array(items.prefix(3)))
}

private var segmentedNavLabel: some View {
#if os(iOS)
Image(systemName: "s.circle")
#elseif os(macOS) || os(tvOS)
HStack {
Image(systemName: "n.circle")
Text("Segmented")
}
.font(.title)
#endif
}

private func navigationButton<Label>(label: Label, tag: TabOption, id: String) -> some View where Label: View {
accessibleNavigation(tag: tag, id: id) {
#if os(iOS)
Expand Down Expand Up @@ -162,6 +179,7 @@ public struct ContentView: View {
id: singleOptionalSectionNavItem
)
navigationButton(label: multiValueNavLabel, tag: .multiValue, id: multiSectionNavItem)
navigationButton(label: segmentedNavLabel, tag: .segmented, id: segmentedNavItem)
Spacer()
}
.frame(width: 75)
Expand All @@ -174,6 +192,7 @@ public struct ContentView: View {
id: singleOptionalSectionNavItem
)
navigationButton(label: multiValueNavLabel, tag: .multiValue, id: multiSectionNavItem)
navigationButton(label: segmentedNavLabel, tag: .segmented, id: segmentedNavItem)
}
#elseif os(tvOS)
TabView {
Expand Down Expand Up @@ -202,6 +221,8 @@ public struct ContentView: View {
singleOptionalValue()
case .multiValue:
multiValue()
case .segmented:
segmentedPicker()
}
Spacer()
}
Expand Down
44 changes: 44 additions & 0 deletions Example/Shared/Sources/Shared/SegmentedPicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SegmentedPicker.swift
// PickBetter
//
// Copyright © 2024 MFB Technologies, Inc. All rights reserved. All rights reserved.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

import Foundation
import PickBetter
import SwiftUI

public struct SegmentedPickerView: View {
private let items: [Item]
@State private var selection: Item.ID = 0

public init(items: [Item]) {
self.items = items
}

// MARK: Accessibility Ids

private var segmentedPickerString: String { "segmentedPicker" }

public struct SegmentedItemLabel: View {
private let itemId: Int

public init(itemId: Int) {
self.itemId = itemId
}

public var body: some View {
Text("Segmented \(itemId)")
}
}

public var body: some View {
LazyView(
BetterPicker(items, selection: $selection, content: { SegmentedItemLabel(itemId: $0.id) })
.betterPickerStyle(SegmentedBetterPickerStyle(frameWidth: 500.0))
.accessibilityIdentifier(segmentedPickerString)
)
}
}
1 change: 1 addition & 0 deletions Example/Shared/Sources/Shared/TabOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public enum TabOption: Hashable, Sendable {
case singleValue
case singleOptionalValue
case multiValue
case segmented
}
1 change: 1 addition & 0 deletions Sources/PickBetter/BetterPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public struct BetterPicker<SelectionBox, ItemContent>: View where SelectionBox:
listOutput: AnyView(forEachStyledItems),
cellCount: items.count,
selectionCount: selection.count,
selectionIndexSet: IndexSet(items.enumerated().filter { selection.contains($1.0) }.map(\.offset)),
selectionLabels: items.filter { selection.contains($0.0) }.map { AnyView($0.1()) }
)
let styledBody = style.makeView(configuration)
Expand Down
4 changes: 2 additions & 2 deletions Sources/PickBetter/BetterPickerStyleCellConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import SwiftUI
/// Configuration payload passed to `BetterPickerStyle.makeListCell(_:)`
public struct BetterPickerStyleListCellConfiguration {
/// Callback for when the cell is selected
private let select: () -> Void
public let select: () -> Void

/// Callback for when the cell is deselected
private let deselect: () -> Void
public let deselect: () -> Void

/// Label for the cell
public let label: () -> AnyView
Expand Down
3 changes: 3 additions & 0 deletions Sources/PickBetter/BetterPickerStyleConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public struct BetterPickerStyleConfiguration {
/// The number of selections
public let selectionCount: Int

/// The indicies of all selected items
public let selectionIndexSet: IndexSet

/// The label views for the selected cells
public let selectionLabels: [AnyView]
}
Loading

0 comments on commit e4117a6

Please sign in to comment.