Instantly share code, notes, and snippets.
Last active
May 20, 2024 15:27
-
Star
(7)
7
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save tgrapperon/034069d6116ff69b6240265132fd9ef7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct ContentView: View { | |
@State var path: [String] = [] | |
func navigationButton(value: String) -> some View { | |
NavigationButton { | |
path.append(value) | |
} label: { | |
Text("NavigationButton") | |
} | |
.environment(\.isNavigationActive, path.last == value) | |
} | |
var body: some View { | |
NavigationStack(path: $path) { | |
List { | |
NavigationLink("NavigationLink", value: "NavigationLink") | |
navigationButton(value: "NavigationButton In List") | |
} | |
.navigationDestination(for: String.self) { value in | |
Text(value) | |
} | |
.safeAreaInset(edge: .top) { | |
HStack { | |
navigationButton(value: "NavigationButton") | |
.padding() | |
navigationButton(value: "Styled NavigationButton") | |
.buttonStyle(.borderedProminent) | |
.padding() | |
} | |
} | |
} | |
} | |
} | |
public struct NavigationButton<Label: View>: View { | |
let action: () -> Void | |
let label: Label | |
public init( | |
action: @escaping () -> Void, | |
@ViewBuilder label: () -> Label | |
) { | |
self.action = action | |
self.label = label() | |
} | |
@Environment(\.colorScheme) var colorScheme | |
@Environment(\.isNavigationActive) var isNavigationActive | |
@State var isInList: Bool = false | |
@State var isPressedInList: Bool = false | |
var isSelectedInList: Bool { isInList && (isNavigationActive || isPressedInList) } | |
public var body: some View { | |
Button(action: action) { | |
HStack { | |
label | |
if isInList { | |
Spacer() | |
_DisclosureIndicator() | |
.foregroundColor(colorScheme == .dark ? .white : .black) | |
} | |
} | |
.applyIf(isInList) { | |
$0 | |
// We remove the accent tint | |
.tint(.primary) | |
// Since we'll set a ButtonStyle in List, we'll lose the extended | |
// touch area, so we need to compensate: | |
.frame(maxWidth: .infinity) | |
.contentShape(Rectangle().inset(by: -64)) | |
} | |
} | |
.applyIf(isInList) { // `buttonStyle` of buttons that are not in a List is preserved | |
$0.buttonStyle(ListButtonStyle(isPressed: $isPressedInList)) | |
} | |
.listRowBackground( | |
ListRowBackground(isSelected: isSelectedInList) | |
.onAppear { isInList = true } | |
) | |
} | |
// This view provides a workaround to animate `listRowBackground` | |
struct ListRowBackground: View { | |
let isSelected: Bool | |
// We need to set this value asynchronously to get animations in `listRowBackground` | |
@State var isSelected_Render: Bool = false | |
// Plaform dependent values | |
var normalListBackgroundColor: Color { | |
Color(uiColor: .systemBackground) | |
} | |
var selectedListBackgroundColor: Color { | |
Color(uiColor: .systemGray4) | |
} | |
var body: some View { | |
ZStack { | |
if isSelected_Render { | |
selectedListBackgroundColor | |
} else { | |
normalListBackgroundColor | |
} | |
} | |
.animation( | |
// We only want to animate release | |
isSelected_Render ? nil : .easeOut(duration: 0.2), | |
value: isSelected_Render | |
) | |
.onChange(of: isSelected) { isSelected in | |
DispatchQueue.main.async { | |
isSelected_Render = isSelected | |
} | |
} | |
} | |
} | |
} | |
struct ListButtonStyle: ButtonStyle { | |
var isPressed: Binding<Bool> | |
func makeBody(configuration: Configuration) -> some View { | |
configuration.label | |
.onChange(of: configuration.isPressed) { | |
isPressed.wrappedValue = $0 | |
} | |
} | |
} | |
extension View { | |
// We know why we use this and its limitations | |
@ViewBuilder | |
func applyIf<Modified>( | |
_ predicate: @autoclosure () -> Bool, | |
apply modifier: (Self) -> Modified | |
) -> some View where Modified: View { | |
if predicate() { | |
modifier(self) | |
} else { | |
self | |
} | |
} | |
} | |
struct NavigationIsActiveKey: EnvironmentKey { | |
static var defaultValue: Bool { false } | |
} | |
extension EnvironmentValues { | |
var isNavigationActive: Bool { | |
get { self[NavigationIsActiveKey.self] } | |
set { self[NavigationIsActiveKey.self] = newValue } | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment