Skip to content

Commit 484a358

Browse files
authored
Merge pull request #7 from Inxel/update_segmented_control
Update segmented control
2 parents 964bde2 + 08dbd76 commit 484a358

4 files changed

Lines changed: 155 additions & 54 deletions

File tree

Example/CustomizableSegmentedControlExample/CustomizableSegmentedControlExample/CustomizableSegmentedControlExampleView.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ struct CustomizableSegmentedControlExampleView: View {
3232
CustomizableSegmentedControl(
3333
selection: $selection,
3434
options: options,
35-
insets: insets,
36-
interSegmentSpacing: interSegmentSpacing,
37-
animation: animation.value,
3835
selectionView: selectionView,
3936
segmentContent: { option, isPressed in
4037
segmentView(title: option.title, imageName: option.imageName, isPressed: isPressed)
@@ -45,6 +42,9 @@ struct CustomizableSegmentedControlExampleView: View {
4542
.segmentAccessibilityValue { index, totalSegmentsCount in
4643
"Custom accessibility value. Current segment is \(index) of \(totalSegmentsCount)"
4744
}
45+
.insets(insets)
46+
.segmentedControlSlidingAnimation(animation.value)
47+
.segmentedControl(interSegmentSpacing: interSegmentSpacing)
4848
.background(Color.blue)
4949
.clipShape(RoundedRectangle(cornerRadius: 14))
5050
}
@@ -56,15 +56,15 @@ struct CustomizableSegmentedControlExampleView: View {
5656
CustomizableSegmentedControl(
5757
selection: $selection,
5858
options: options,
59-
insets: insets,
60-
interSegmentSpacing: interSegmentSpacing,
61-
contentStyle: .withBlendMode(),
62-
animation: animation.value,
6359
selectionView: selectionView,
6460
segmentContent: { option, isPressed in
6561
segmentView(title: option.title, imageName: option.imageName, isPressed: isPressed)
6662
}
6763
)
64+
.insets(insets)
65+
.segmentedControlContentStyle(.blendMode())
66+
.segmentedControlSlidingAnimation(animation.value)
67+
.segmentedControl(interSegmentSpacing: interSegmentSpacing)
6868
.background(Color.blue)
6969
.clipShape(RoundedRectangle(cornerRadius: 14))
7070
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//
2+
// CustomizableSegmentedControl+EnvironmentValues.swift
3+
//
4+
//
5+
// Created by Artyom Zagoskin on 10.10.2023.
6+
//
7+
8+
import SwiftUI
9+
10+
private struct SegmentedControlInsetsKey: EnvironmentKey {
11+
static var defaultValue: EdgeInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0)
12+
}
13+
14+
private struct SegmentedControlInterSegmentSpacingKey: EnvironmentKey {
15+
static var defaultValue: CGFloat = .zero
16+
}
17+
18+
private struct SegmentedControlContentStyleKey: EnvironmentKey {
19+
static var defaultValue: CustomizableSegmentedControlContentStyle = .default
20+
}
21+
22+
private struct SegmentedControlSlidingAnimationKey: EnvironmentKey {
23+
static var defaultValue: Animation = .default
24+
}
25+
26+
extension EnvironmentValues {
27+
28+
var segmentedControlInsets: EdgeInsets {
29+
get { self[SegmentedControlInsetsKey.self] }
30+
set { self[SegmentedControlInsetsKey.self] = newValue }
31+
}
32+
33+
var segmentedControlInterSegmentSpacing: CGFloat {
34+
get { self[SegmentedControlInterSegmentSpacingKey.self] }
35+
set { self[SegmentedControlInterSegmentSpacingKey.self] = newValue }
36+
}
37+
38+
var segmentedControlContentStyle: CustomizableSegmentedControlContentStyle {
39+
get { self[SegmentedControlContentStyleKey.self] }
40+
set { self[SegmentedControlContentStyleKey.self] = newValue }
41+
}
42+
43+
var segmentedControlSlidingAnimation: Animation {
44+
get { self[SegmentedControlSlidingAnimationKey.self] }
45+
set { self[SegmentedControlSlidingAnimationKey.self] = newValue }
46+
}
47+
48+
}
49+
50+
public extension CustomizableSegmentedControl {
51+
52+
func insets(_ insets: EdgeInsets) -> some View {
53+
environment(\.segmentedControlInsets, insets)
54+
}
55+
56+
func insets(top: CGFloat? = nil, leading: CGFloat? = nil, bottom: CGFloat? = nil, trailing: CGFloat? = nil) -> some View {
57+
environment(\.segmentedControlInsets, .init(
58+
top: top ?? segmentedControlInsets.top,
59+
leading: leading ?? segmentedControlInsets.leading,
60+
bottom: bottom ?? segmentedControlInsets.bottom,
61+
trailing: trailing ?? segmentedControlInsets.trailing
62+
))
63+
}
64+
65+
func insets(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View {
66+
switch edges {
67+
case .vertical:
68+
return insets(top: length, bottom: length)
69+
case .horizontal:
70+
return insets(leading: length, trailing: length)
71+
case .top:
72+
return insets(top: length)
73+
case .leading:
74+
return insets(leading: length)
75+
case .bottom:
76+
return insets(bottom: length)
77+
case .trailing:
78+
return insets(trailing: length)
79+
case .all:
80+
return insets(top: length, leading: length, bottom: length, trailing: length)
81+
default:
82+
assertionFailure("Unavailable type of edge")
83+
return insets(
84+
top: segmentedControlInsets.top,
85+
leading: segmentedControlInsets.leading,
86+
bottom: segmentedControlInsets.bottom,
87+
trailing: segmentedControlInsets.trailing
88+
)
89+
}
90+
}
91+
92+
}
93+
94+
public extension View {
95+
96+
func segmentedControl(interSegmentSpacing: CGFloat) -> some View {
97+
environment(\.segmentedControlInterSegmentSpacing, interSegmentSpacing)
98+
}
99+
100+
func segmentedControlContentStyle(_ style: CustomizableSegmentedControlContentStyle) -> some View {
101+
environment(\.segmentedControlContentStyle, style)
102+
}
103+
104+
func segmentedControlSlidingAnimation(_ animation: Animation) -> some View {
105+
environment(\.segmentedControlSlidingAnimation, animation)
106+
}
107+
108+
}

Sources/CustomizableSegmentedControl/CustomizableSegmentedControl.swift

Lines changed: 16 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,18 @@
11
import SwiftUI
22

3-
/// Style of segment content
4-
public enum CustomizableSegmentedControlContentStyle {
5-
/// Default style. You configure color for all states of content.
6-
case `default`
7-
/// Blend mode style. You configure colors, but some of them depends on background.
8-
/// - parameters:
9-
/// - contentBlendMode: Blend mode applies to content. Default is difference.
10-
/// - firstLevelOverlayBlendMode: Blend mode applies to first level overlay. Default is hue.
11-
/// - highestLevelOverlayBlendMode: Blend mode applies to highest level overlay. Default is overlay..
12-
case withBlendMode(
13-
contentBlendMode: BlendMode = .difference,
14-
firstLevelOverlayBlendMode: BlendMode = .hue,
15-
highestLevelOverlayBlendMode: BlendMode = .overlay
16-
)
17-
}
18-
193
// MARK: - Segmented Control
204

215
public struct CustomizableSegmentedControl<Option: Hashable & Identifiable, SelectionView: View, SegmentContent: View>: View {
226

237
// MARK: - Properties
248

9+
@Environment(\.segmentedControlInsets) var segmentedControlInsets
10+
@Environment(\.segmentedControlInterSegmentSpacing) var interSegmentSpacing
11+
@Environment(\.segmentedControlSlidingAnimation) var slidingAnimation
12+
@Environment(\.segmentedControlContentStyle) var contentStyle
13+
2514
@Binding private var selection: Option
2615
private let options: [Option]
27-
private let insets: EdgeInsets
28-
private let interSegmentSpacing: CGFloat
29-
private let contentStyle: CustomizableSegmentedControlContentStyle
30-
private let animation: Animation
3116
private let selectionView: () -> SelectionView
3217
private let segmentContent: (Option, Bool) -> SegmentContent
3318

@@ -55,19 +40,11 @@ public struct CustomizableSegmentedControl<Option: Hashable & Identifiable, Sele
5540
public init(
5641
selection: Binding<Option>,
5742
options: [Option],
58-
insets: EdgeInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0),
59-
interSegmentSpacing: CGFloat = 0,
60-
contentStyle: CustomizableSegmentedControlContentStyle = .default,
61-
animation: Animation = .default,
6243
selectionView: @escaping () -> SelectionView,
6344
@ViewBuilder segmentContent: @escaping (Option, Bool) -> SegmentContent
6445
) {
6546
self._selection = selection
6647
self.options = options
67-
self.insets = insets
68-
self.interSegmentSpacing = interSegmentSpacing
69-
self.contentStyle = contentStyle
70-
self.animation = animation
7148
self.selectionView = selectionView
7249
self.segmentContent = segmentContent
7350
self.optionIsPressed = Dictionary(uniqueKeysWithValues: options.lazy.map { ($0.id, false) })
@@ -82,7 +59,7 @@ public struct CustomizableSegmentedControl<Option: Hashable & Identifiable, Sele
8259
content: segmentContent(option, optionIsPressed[option.id, default: false]),
8360
selectionView: selectionView(),
8461
isSelected: selection == option,
85-
animation: animation,
62+
animation: slidingAnimation,
8663
contentBlendMode: contentStyle.contentBlendMode,
8764
firstLevelOverlayBlendMode: contentStyle.firstLevelOverlayBlendMode,
8865
highestLevelOverlayBlendMode: contentStyle.highestLevelOverlayBlendMode,
@@ -98,7 +75,7 @@ public struct CustomizableSegmentedControl<Option: Hashable & Identifiable, Sele
9875
.zIndex(selection == option ? 0 : 1)
9976
}
10077
}
101-
.padding(insets)
78+
.padding(segmentedControlInsets)
10279
}
10380

10481
}
@@ -107,12 +84,12 @@ public struct CustomizableSegmentedControl<Option: Hashable & Identifiable, Sele
10784

10885
extension CustomizableSegmentedControl {
10986

110-
fileprivate struct Segment<SelectionView: View, Content: View>: View {
87+
fileprivate struct Segment<SegmentSelectionView: View, Content: View>: View {
11188

11289
// MARK: - Properties
11390

11491
let content: Content
115-
let selectionView: SelectionView
92+
let selectionView: SegmentSelectionView
11693
let isSelected: Bool
11794
let animation: Animation
11895
let contentBlendMode: BlendMode?
@@ -181,20 +158,12 @@ extension CustomizableSegmentedControl {
181158
public init(
182159
selection: Binding<Option>,
183160
options: [Option],
184-
insets: EdgeInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0),
185-
interSegmentSpacing: CGFloat = 0,
186-
contentStyle: CustomizableSegmentedControlContentStyle = .default,
187-
animation: Animation = .default,
188161
selectionView: SelectionView,
189162
@ViewBuilder segmentContent: @escaping (Option, Bool) -> SegmentContent
190163
) {
191164
self.init(
192165
selection: selection,
193166
options: options,
194-
insets: insets,
195-
interSegmentSpacing: interSegmentSpacing,
196-
contentStyle: contentStyle,
197-
animation: animation,
198167
selectionView: { selectionView },
199168
segmentContent: segmentContent
200169
)
@@ -230,26 +199,26 @@ private extension CustomizableSegmentedControlContentStyle {
230199
switch self {
231200
case .default:
232201
return nil
233-
case .withBlendMode(let blendMode, _, _):
234-
return blendMode
202+
case .blendMode(let mode, _, _):
203+
return mode
235204
}
236205
}
237206

238207
var firstLevelOverlayBlendMode: BlendMode? {
239208
switch self {
240209
case .default:
241210
return nil
242-
case .withBlendMode(_, let blendMode, _):
243-
return blendMode
211+
case .blendMode(_, let mode, _):
212+
return mode
244213
}
245214
}
246215

247216
var highestLevelOverlayBlendMode: BlendMode? {
248217
switch self {
249218
case .default:
250219
return nil
251-
case .withBlendMode(_, _, let blendMode):
252-
return blendMode
220+
case .blendMode(_, _, let mode):
221+
return mode
253222
}
254223
}
255224

@@ -263,7 +232,7 @@ public extension CustomizableSegmentedControl {
263232
///
264233
/// - Parameters:
265234
/// - completion: Takes index and total count. Returns neccessary string
266-
func segmentAccessibilityValue(_ completion: @escaping (Int, Int) -> String) -> some View {
235+
func segmentAccessibilityValue(_ completion: @escaping (Int, Int) -> String) -> Self {
267236
var copy = self
268237
copy.segmentAccessibilityValueCompletion = completion
269238
return copy
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// CustomizableSegmentedControlContentStyle.swift
3+
//
4+
//
5+
// Created by Artyom Zagoskin on 10.10.2023.
6+
//
7+
8+
import SwiftUI
9+
10+
/// Style of segment content
11+
public enum CustomizableSegmentedControlContentStyle {
12+
/// Default style. You configure color for all states of content.
13+
case `default`
14+
/// Blend mode style. You configure colors, but some of them depends on background.
15+
/// - parameters:
16+
/// - contentBlendMode: Blend mode applies to content. Default is difference.
17+
/// - firstLevelOverlayBlendMode: Blend mode applies to first level overlay. Default is hue.
18+
/// - highestLevelOverlayBlendMode: Blend mode applies to highest level overlay. Default is overlay..
19+
case blendMode(
20+
contentBlendMode: BlendMode = .difference,
21+
firstLevelOverlayBlendMode: BlendMode = .hue,
22+
highestLevelOverlayBlendMode: BlendMode = .overlay
23+
)
24+
}

0 commit comments

Comments
 (0)