How to detect a 'Click' gesture in SwiftUI tvOS - swift

Using:
SwiftUI
Swift 5
tvOS
Xcode Version 11.2.1
I just want to detect a click gesture on the URLImage below
JFYI I am very new to Xcode, Swift and SwiftUI (less than 3 weeks).
URLImage(URL(string: channel.thumbnail)!,
delay: 0.25,
processors: [ Resize(size: CGSize(width:isFocused ? 300.0 : 225.0, height:isFocused ? 300.0 : 225.0), scale: UIScreen.main.scale) ],
content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
})
.frame(width: isFocused ? 300.0 : 250.0, height:isFocused ? 300.0 : 250.0)
.clipShape(Circle())
.overlay(
Circle().stroke( isFocused ? Color.white : Color.black, lineWidth: 8))
.shadow(radius:5)
.focusable(true, onFocusChange:{ (isFocused) in
withAnimation(.easeInOut(duration:0.3)){
self.isFocused = isFocused
}
if(isFocused){
self.manager.bannerChannel = self.channel
print(self.manager.bannerChannel)
self.manager.loadchannelEPG(id: self.channel.id)
}
})
.padding(20)
}
The only workaround I have found is wrapping it in a NavigationLink
or a Button but then focusable on the button doesn't run.
I found out that focusable runs on a Button/NavigationLink if I add corner radius to it but then the default click action doesn't run
Also, TapGesture is not available in tvOS
Since Gestures are available maybe there is a way using gestures that I cannot figure out.
OR
If there is a way to tap into focusable on a button (although this is the less favoured alternative since this changes the look I want to achieve).

Edit: onTapGesture() is now available starting in tvOS 16
tvOS 16
struct ContentView: View {
#FocusState var focused1
#FocusState var focused2
var body: some View {
HStack {
Text("Clickable 1")
.foregroundColor(self.focused1 ? Color.red : Color.black)
.focusable(true)
.focused($focused1)
.onTapGesture {
print("clicked 1")
}
Text("Clickable 2")
.foregroundColor(self.focused2 ? Color.red : Color.black)
.focusable(true)
.focused($focused2)
.onTapGesture {
print("clicked 2")
}
}
}
}
Previous Answer for tvOS 15 and earlier
It is possible, but not for the faint of heart. I came up with a somewhat generic solution that may help you. I hope in the next swiftUI update Apple adds a better way to attach click events for tvOS and this code can be relegated to the trash bin where it belongs.
The high level explanation of how to do this is to make a UIView that captures the focus and click events, then make a UIViewRepresentable so swiftUI can use the view. Then the view is added to the layout in a ZStack so it's hidden, but you can receive focus and respond to click events as if the user was really interacting with your real swiftUI component.
First I need to make a UIView that captures the events.
class ClickableHackView: UIView {
weak var delegate: ClickableHackDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
}
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
if event?.allPresses.map({ $0.type }).contains(.select) ?? false {
delegate?.clicked()
} else {
superview?.pressesEnded(presses, with: event)
}
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
delegate?.focus(focused: isFocused)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var canBecomeFocused: Bool {
return true
}
}
The clickable delegate:
protocol ClickableHackDelegate: class {
func focus(focused: Bool)
func clicked()
}
Then make a swiftui extension for my view
struct ClickableHack: UIViewRepresentable {
#Binding var focused: Bool
let onClick: () -> Void
func makeUIView(context: UIViewRepresentableContext<ClickableHack>) -> UIView {
let clickableView = ClickableHackView()
clickableView.delegate = context.coordinator
return clickableView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<ClickableHack>) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, ClickableHackDelegate {
private let control: ClickableHack
init(_ control: ClickableHack) {
self.control = control
super.init()
}
func focus(focused: Bool) {
control.focused = focused
}
func clicked() {
control.onClick()
}
}
}
Then I make a friendlier swiftui wrapper so I can pass in any kind of component I want to be focusable and clickable
struct Clickable<Content>: View where Content : View {
let focused: Binding<Bool>
let content: () -> Content
let onClick: () -> Void
#inlinable public init(focused: Binding<Bool>, onClick: #escaping () -> Void, #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.focused = focused
self.onClick = onClick
}
var body: some View {
ZStack {
ClickableHack(focused: focused, onClick: onClick)
content()
}
}
}
Example usage:
struct ClickableTest: View {
#State var focused1: Bool = false
#State var focused2: Bool = false
var body: some View {
HStack {
Clickable(focused: self.$focused1, onClick: {
print("clicked 1")
}) {
Text("Clickable 1")
.foregroundColor(self.focused1 ? Color.red : Color.black)
}
Clickable(focused: self.$focused2, onClick: {
print("clicked 2")
}) {
Text("Clickable 2")
.foregroundColor(self.focused2 ? Color.red : Color.black)
}
}
}
}

If you'd like to avoid UIKit, you can achieve the desired solution with Long Press Gesture by setting a really small duration of pressing.
1. Only Press:
If you only need to handle the pressing action and don't need long pressing at all.
ContentView()
.onLongPressGesture(minimumDuration: 0.01, pressing: { _ in }) {
print("pressed")
}
2. Press and Long press:
If you need to handle both pressing and Long pressing.
var longPress: some Gesture {
LongPressGesture(minimumDuration: 0.5)
.onEnded { _ in
print("longpress")
}
}
ContentView()
.highPriorityGesture(longPress)
.onLongPressGesture(minimumDuration: 0.01, pressing: { _ in }) {
print("press")
}

Related

How can I detect press gesture within ButtonStyle in SwiftUI? [duplicate]

I have a Button. I want to set custom background color for highlighted state. How can I do it in SwiftUI?
Button(action: signIn) {
Text("Sign In")
}
.padding(.all)
.background(Color.red)
.cornerRadius(16)
.foregroundColor(.white)
.font(Font.body.bold())
Updated for SwiftUI beta 5
SwiftUI does actually expose an API for this: ButtonStyle.
struct MyButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding()
.foregroundColor(.white)
.background(configuration.isPressed ? Color.red : Color.blue)
.cornerRadius(8.0)
}
}
// To use it
Button(action: {}) {
Text("Hello World")
}
.buttonStyle(MyButtonStyle())
As far as I can tell, theres no officially supported way to do this as of yet. Here is a little workaround that you can use. This produces the same behavior as in UIKit where tapping a button and dragging your finger off of it will keep the button highlighted.
struct HoverButton<Label: View>: View {
private let action: () -> ()
private let label: () -> Label
init(action: #escaping () -> (), label: #escaping () -> Label) {
self.action = action
self.label = label
}
#State private var pressed: Bool = false
var body: some View {
Button(action: action) {
label()
.foregroundColor(pressed ? .red : .blue)
.gesture(DragGesture(minimumDistance: 0.0)
.onChanged { _ in self.pressed = true }
.onEnded { _ in self.pressed = false })
}
}
}
I was looking for a similar functionality and I did it in the following way.
I created a special View struct returning a Button in the style I need, in this struct I added a State property selected. I have a variable named 'table' which is an Int since my buttons a round buttons with numbers on it
struct TableButton: View {
#State private var selected = false
var table: Int
var body: some View {
Button("\(table)") {
self.selected.toggle()
}
.frame(width: 50, height: 50)
.background(selected ? Color.blue : Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
}
Then I use in my content View the code
HStack(spacing: 10) {
ForEach((1...6), id: \.self) { table in
TableButton(table: table)
}
}
This creates an horizontal stack with 6 buttons which color blue when selected and red when deselected.
I am not a experienced developer but just tried all possible ways until I found that this is working for me, hopefully it is useful for others as well.
This is for the people who are not satisfied with the above solutions, as they raise other problems such as overlapping gestures(for example, it's quite hard to use this solution in scrollview now). Another crutch is to create a custom button style like this
struct CustomButtonStyle<Content>: ButtonStyle where Content: View {
var change: (Bool) -> Content
func makeBody(configuration: Self.Configuration) -> some View {
return change(configuration.isPressed)
}
}
So, we should just transfer the closure which will return the state of the button and create the button based on this parameter. It will be used like this:
struct CustomButton<Content>: View where Content: View {
var content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
Button(action: { }, label: {
EmptyView()
})
.buttonStyle(CustomButtonStyle(change: { bool in
Text("\(bool ? "yo" : "yo2")")
}))
}
}
Okey let me clear everything again. Here is the exact solution
Create the below button modifier.
struct StateableButton<Content>: ButtonStyle where Content: View {
var change: (Bool) -> Content
func makeBody(configuration: Configuration) -> some View {
return change(configuration.isPressed)
}
}
Then use it like below one
Button(action: {
print("Do something")
}, label: {
// Don't create your button view in here
EmptyView()
})
.buttonStyle(StateableButton(change: { state in
// Create your button view in here
return HStack {
Image(systemName: "clock.arrow.circlepath")
Text(item)
Spacer()
Image(systemName: "arrow.up.backward")
}
.padding(.horizontal)
.frame(height: 50)
.background(state ? Color.black : Color.clear)
}))
You need to define a custom style that can be used to provide the two backgrounds for normal and highlighted states:
Button(action: {
print("action")
}, label: {
Text("My Button").padding()
})
.buttonStyle(HighlightableButtonStyle(normal: { Color.red },
highlighted: { Color.green }))
// Custom button style
#available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct HighlightableButtonStyle<N, H>: ButtonStyle where N: View, H: View {
private let alignment: Alignment
private let normal: () -> N
private let highlighted: () -> H
init(alignment: Alignment = .center, #ViewBuilder normal: #escaping () -> N, #ViewBuilder highlighted: #escaping () -> H) {
self.alignment = alignment
self.normal = normal
self.highlighted = highlighted
}
func makeBody(configuration: Configuration) -> some View {
return ZStack {
if configuration.isPressed {
configuration.label.background(alignment: alignment, content: normal)
}
else {
configuration.label.background(alignment: alignment, content: highlighted)
}
}
}
}

UISwitch.appearance().onTintColor not working since iOS 14 [duplicate]

I've implemented a toggle after following Apple's tutorial on user input. Currently, it looks like this:
This is the code that produces this UI:
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Show Favorites only")
}
}
}
Now, I'd like the Toggle's on-color to be blue instead of green.
I tried:
Toggle(isOn: $showFavoritesOnly) {
Text("Show Favorites only")
}
.accentColor(.blue)
.foregroundColor(.blue)
.background(Color.blue)
None of these worked and I wasn't able to find any other modifiers, such as tintColor.
How do I change the color of a Toggle?
SwiftUI 3.0
Using tint
A new modifier was introduced that can also change the Toggle color:
Toggle(isOn: $isToggleOn) {
Text("Red")
Image(systemName: "paintpalette")
}
.tint(.red)
Toggle(isOn: $isToggleOn) {
Text("Orange")
Image(systemName: "paintpalette")
}
.tint(.orange)
SwiftUI 2.0
Using SwitchToggleStyle
You can now set a tint color for the on position only in SwiftUI 2.0:
Toggle(isOn: $isToggleOn) {
Text("Red")
Image(systemName: "paintpalette")
}
.toggleStyle(SwitchToggleStyle(tint: Color.red))
Toggle(isOn: $isToggleOn) {
Text("Orange")
Image(systemName: "paintpalette")
}
.toggleStyle(SwitchToggleStyle(tint: Color.orange))
SwiftUI 1.0
Using ToggleStyle
I created a new ToggleStyle to change the three colors of the Toggle (on color, off color, and the thumb).
struct ColoredToggleStyle: ToggleStyle {
var label = ""
var onColor = Color(UIColor.green)
var offColor = Color(UIColor.systemGray5)
var thumbColor = Color.white
func makeBody(configuration: Self.Configuration) -> some View {
HStack {
Text(label)
Spacer()
Button(action: { configuration.isOn.toggle() } )
{
RoundedRectangle(cornerRadius: 16, style: .circular)
.fill(configuration.isOn ? onColor : offColor)
.frame(width: 50, height: 29)
.overlay(
Circle()
.fill(thumbColor)
.shadow(radius: 1, x: 0, y: 1)
.padding(1.5)
.offset(x: configuration.isOn ? 10 : -10))
.animation(Animation.easeInOut(duration: 0.1))
}
}
.font(.title)
.padding(.horizontal)
}
}
Examples of Use
Toggle("", isOn: $toggleState)
.toggleStyle(
ColoredToggleStyle(label: "My Colored Toggle",
onColor: .green,
offColor: .red,
thumbColor: Color(UIColor.systemTeal)))
Toggle("", isOn: $toggleState2)
.toggleStyle(
ColoredToggleStyle(label: "My Colored Toggle",
onColor: .purple))
From the SwiftUI Book
Just use UIAppearance APIs:
UISwitch.appearance().onTintColor = UIColor.blue
It'll of course by default change the appearance of all the instances of UISwitch, as per UIAppearance documentation.
NOTE: Tested as of Xcode 11 beta 5.
SwiftUI 2.0 (Post WWDC-2020)
Using the new SwiftUI enhancements you can use the .toggleStyle modifier.
// Switch tinting
Toggle(isOn: $order.notifyWhenReady) {
Text("Send notification when ready")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Note this only works for iOS14/iPadOS14/macOS11 and above.
I haven't found a way to directly change a Toggle color yet but an alternative way to have a blue switch or any other custom views, is to create a custom view of your own. To make a custom blue toggle in its simplest form:
struct BlueToggle : UIViewRepresentable {
func makeUIView(context: Context) -> UISwitch {
UISwitch()
}
func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.onTintColor = UIColor.blue
}
}
struct ContentView : View {
var body: some View {
BlueToggle()
}
}
Result:
You can modify the global onTintColor for all UISwitch objects inside init().
#State var enable_dhcp = true
init()
{
UISwitch.appearance().onTintColor = .red
}
var body: some View
{
Toggle("DHCP", isOn: $enable_dhcp)
}
Building off #mohammad-reza-farahani 's solution, here is a fully uncompromising approach to getting the configurability of UISwitch with the implementation protocols if SwiftUI.
First wrap a UISwitch in a UIViewRepresentable and set the colors as you wish:
final class CustomToggleWrapper: UIViewRepresentable {
var isOn: Binding<Bool>
init(isOn: Binding<Bool>) {
self.isOn = isOn
}
func makeUIView(context: Context) -> UISwitch {
UISwitch()
}
func updateUIView(_ uiView: UISwitch, context: Context) {
// On color
uiView.onTintColor = UIColor.blue
// Off color
uiView.tintColor = UIColor.red
uiView.layer.cornerRadius = uiView.frame.height / 2
uiView.backgroundColor = UIColor.red
uiView.isOn = isOn.wrappedValue
// Update bound boolean
uiView.addTarget(self, action: #selector(switchIsChanged(_:)), for: .valueChanged)
}
#objc
func switchIsChanged(_ sender: UISwitch) {
isOn.wrappedValue = sender.isOn
}
}
Second, create a custom toggle style using the wrapped UISwitch:
struct CustomToggleStyle: ToggleStyle {
func makeBody(configuration: Self.Configuration) -> some View {
let toggle = CustomToggleWrapper(isOn: configuration.$isOn)
return HStack {
configuration.label
Spacer()
toggle
}
}
}
Implement a Toggle as you normally would, and apply your CustomToggleStyle:
struct TestView: View {
#State private var isOn: Bool = true
var body: some View {
Toggle(
isOn: $isOn
) {
Text("Test: \(String(isOn))")
}.toggleStyle(CustomToggleStyle()).padding()
}
}
Karol Kulesza and George Valkov have provided a very easy to implement solution. I just wanted to add that you can place the code below inside the app delegate's didFinishLaunching method as well.
UISwitch.appearance().onTintColor = .blue
You can also create more specific appearance configurations with
appearance(whenContainedInInstancesOf:)
See https://www.hackingwithswift.com/example-code/uikit/what-is-the-uiappearance-proxy
As the original question was just about changing the toggle on colour and not full Toggle visual customisation, I think something like this would do:
import SwiftUI
struct CustomToggle: UIViewRepresentable {
#Binding var isOn: Bool
func makeCoordinator() -> CustomToggle.Coordinator {
Coordinator(isOn: $isOn)
}
func makeUIView(context: Context) -> UISwitch {
let view = UISwitch()
view.onTintColor = UIColor.red
view.addTarget(context.coordinator, action: #selector(Coordinator.switchIsChanged(_:)), for: .valueChanged)
return view
}
func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.isOn = isOn
}
class Coordinator: NSObject {
#Binding private var isOn: Bool
init(isOn: Binding<Bool>) {
_isOn = isOn
}
#objc func switchIsChanged(_ sender: UISwitch) {
_isOn.wrappedValue = sender.isOn
}
}
}
// MARK: - Previews
struct CustomToggle_Previews: PreviewProvider {
static var previews: some View {
ViewWrapper()
}
struct ViewWrapper: View {
#State(initialValue: false) var isOn: Bool
var body: some View {
CustomToggle(isOn: $isOn)
.previewLayout(.fixed(width: 100, height: 100))
}
}
}
The easist way is setting UISwitch.appearance().onTintColor = UIColor.red before using toggle and use SwiftUI Toggle like below.
UISwitch.appearance().onTintColor = UIColor.red
...
let toggle = Toggle(isOn: $vm.dataUsePermission, label: {
Text(I18N.permit_data_usage)
.font(SwiftUI.Font.system(size: 16, weight: .regular))
})
if #available(iOS 14.0, *) {
toggle.toggleStyle(
SwitchToggleStyle(tint: Color(UIColor.m.blue500))
)
} else {
toggle.toggleStyle(SwitchToggleStyle())
}
...
You can alse use same Toggle interface in SwiftUI but different name, and change tint color.
TintableSwitch(isOn: .constant(true), label: {
Text("Switch")
})
Toggle(isOn: .constant(true), label: {
Text("Switch")
})
If only need Toggle without Label, then
TintableUISwitch(isOn: .constant(true))
Use below code.
import SwiftUI
public struct TintableSwitch<Label>: View where Label: View {
#Binding var isOn: Bool
var label: Label
public init(isOn: Binding<Bool>, #ViewBuilder label: () -> Label) {
self._isOn = isOn
self.label = label()
}
public var body: some View {
HStack {
label
Spacer()
TintableUISwitch(isOn: $isOn, onTintColor: .red) // πŸ“Œ CHANGE HERE
}
}
}
public struct TintableUISwitch: UIViewRepresentable {
#Binding var isOn: Bool
private var onTintColor: UIColor
public init(isOn: Binding<Bool>, onTintColor: UIColor = UIColor.m.blue500) {
self._isOn = isOn
self.onTintColor = onTintColor
}
public func makeUIView(context: Context) -> UISwitch {
let uiSwitch = UISwitch()
uiSwitch.addTarget(
context.coordinator,
action: #selector(Coordinator.valueChanged(_:)),
for: .valueChanged
)
uiSwitch.onTintColor = onTintColor
uiSwitch.isOn = isOn
return uiSwitch
}
public func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.isOn = isOn
}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
public class Coordinator: NSObject {
var tintableSwitch: TintableUISwitch
init(_ tintableSwitch: TintableUISwitch) {
self.tintableSwitch = tintableSwitch
}
#objc
func valueChanged(_ sender: UISwitch) {
tintableSwitch.isOn = sender.isOn
}
}
}
struct TintableSwitch_Previews: PreviewProvider {
static var previews: some View {
VStack {
TintableSwitch(isOn: .constant(true), label: {
Text("Switch")
})
Toggle(isOn: .constant(true), label: {
Text("Switch")
})
}
}
}
struct TintableUISwitch_Previews: PreviewProvider {
static var previews: some View {
TintableUISwitch(isOn: .constant(true))
}
}
You can change the toggle color in IOS 15.0 using a tint modifier.
Toggle(isOn: $isToggleOn) {
Text("Toggle")
}.tint(.red)
and below IOS 15.0, You can use toggleStyle modifier to change the toggle color but it will be depreciated in the future.
Toggle(isOn: $isToggleOn) {
Text("Toggle")
}.toggleStyle(SwitchToggleStyle(tint: .red))
I would change #Mark Moeykens answer a little bit to avoid having the button tap animation. A better solution would be:
#available(iOS 13.0, *)
struct ColoredToggleStyle: ToggleStyle {
var label = ""
var onColor = UIColor.proacPrimaryBlue.suColor
var offColor = UIColor.systemGray5.suColor
var thumbColor = Color.white
func makeBody(configuration: Self.Configuration) -> some View {
HStack {
Text(label)
Spacer()
RoundedRectangle(cornerRadius: 16, style: .circular)
.fill(configuration.isOn ? onColor : offColor)
.frame(width: 50, height: 29)
.overlay(
Circle()
.fill(thumbColor)
.shadow(radius: 1, x: 0, y: 1)
.padding(1.5)
.offset(x: configuration.isOn ? 10 : -10))
.animation(Animation.easeInOut(duration: 0.1))
.onTapGesture {
configuration.isOn.toggle()
}
}
.font(.title)
.padding(.horizontal)
}
}
This https://stackoverflow.com/a/56480720/5941807 (for now whit Xcode 11 beta 6) is a solution. To switch between to option a fast way is using the boolean instead of if/else:
showFavoritesOnly ? .red : .blue
for foreground:
Toggle(isOn: $showGreeting) {
Text("Show Favorites only").foregroundColor(showFavoritesOnly ? .blue : .gray)
}
for tint:
uiView.onTintColor = showFavoritesOnly ? UIColor.blue : UIColor.gray
In addition for custom colors: https://stackoverflow.com/a/57744208/5941807

SwiftUI on macOS - handle single-click and double-click at the same time

Consider this view in SwiftUI:
struct MyView: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 200, height: 200)
.onTapGesture {
print("single clicked")
}
}
}
Now we're handling single-click. Say you wanna handle double-click too, but with a separate callback.
You have 2 options:
Add double click handler after single click - this doesn't work at all
Add double click handler before single click handler, this kinda works:
struct MyView: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 200, height: 200)
.onTapGesture(count: 2) {
print("double clicked")
}
.onTapGesture {
print("single clicked")
}
}
}
Double-click handler is called properly, but single-click handler is called after a delay of about 250ms.
Any ideas how to resolve this?
Here is possible approach (tested with Xcode 11.2 / macOS 10.15)
struct MyView: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 200, height: 200)
.gesture(TapGesture(count: 2).onEnded {
print("double clicked")
})
.simultaneousGesture(TapGesture().onEnded {
print("single clicked")
})
}
}
The gesture-based solutions don't work correctly when selection is enabled on the List, since the first tap will delay the actual selection. I have made a double click modifier which works on any view and seems to solve it the way I expect in all cases:
extension View {
/// Adds a double click handler this view (macOS only)
///
/// Example
/// ```
/// Text("Hello")
/// .onDoubleClick { print("Double click detected") }
/// ```
/// - Parameters:
/// - handler: Block invoked when a double click is detected
func onDoubleClick(handler: #escaping () -> Void) -> some View {
modifier(DoubleClickHandler(handler: handler))
}
}
struct DoubleClickHandler: ViewModifier {
let handler: () -> Void
func body(content: Content) -> some View {
content.overlay {
DoubleClickListeningViewRepresentable(handler: handler)
}
}
}
struct DoubleClickListeningViewRepresentable: NSViewRepresentable {
let handler: () -> Void
func makeNSView(context: Context) -> DoubleClickListeningView {
DoubleClickListeningView(handler: handler)
}
func updateNSView(_ nsView: DoubleClickListeningView, context: Context) {}
}
class DoubleClickListeningView: NSView {
let handler: () -> Void
init(handler: #escaping () -> Void) {
self.handler = handler
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if event.clickCount == 2 {
handler()
}
}
}
https://gist.github.com/joelekstrom/91dad79ebdba409556dce663d28e8297

Set Toggle color in SwiftUI

I've implemented a toggle after following Apple's tutorial on user input. Currently, it looks like this:
This is the code that produces this UI:
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Show Favorites only")
}
}
}
Now, I'd like the Toggle's on-color to be blue instead of green.
I tried:
Toggle(isOn: $showFavoritesOnly) {
Text("Show Favorites only")
}
.accentColor(.blue)
.foregroundColor(.blue)
.background(Color.blue)
None of these worked and I wasn't able to find any other modifiers, such as tintColor.
How do I change the color of a Toggle?
SwiftUI 3.0
Using tint
A new modifier was introduced that can also change the Toggle color:
Toggle(isOn: $isToggleOn) {
Text("Red")
Image(systemName: "paintpalette")
}
.tint(.red)
Toggle(isOn: $isToggleOn) {
Text("Orange")
Image(systemName: "paintpalette")
}
.tint(.orange)
SwiftUI 2.0
Using SwitchToggleStyle
You can now set a tint color for the on position only in SwiftUI 2.0:
Toggle(isOn: $isToggleOn) {
Text("Red")
Image(systemName: "paintpalette")
}
.toggleStyle(SwitchToggleStyle(tint: Color.red))
Toggle(isOn: $isToggleOn) {
Text("Orange")
Image(systemName: "paintpalette")
}
.toggleStyle(SwitchToggleStyle(tint: Color.orange))
SwiftUI 1.0
Using ToggleStyle
I created a new ToggleStyle to change the three colors of the Toggle (on color, off color, and the thumb).
struct ColoredToggleStyle: ToggleStyle {
var label = ""
var onColor = Color(UIColor.green)
var offColor = Color(UIColor.systemGray5)
var thumbColor = Color.white
func makeBody(configuration: Self.Configuration) -> some View {
HStack {
Text(label)
Spacer()
Button(action: { configuration.isOn.toggle() } )
{
RoundedRectangle(cornerRadius: 16, style: .circular)
.fill(configuration.isOn ? onColor : offColor)
.frame(width: 50, height: 29)
.overlay(
Circle()
.fill(thumbColor)
.shadow(radius: 1, x: 0, y: 1)
.padding(1.5)
.offset(x: configuration.isOn ? 10 : -10))
.animation(Animation.easeInOut(duration: 0.1))
}
}
.font(.title)
.padding(.horizontal)
}
}
Examples of Use
Toggle("", isOn: $toggleState)
.toggleStyle(
ColoredToggleStyle(label: "My Colored Toggle",
onColor: .green,
offColor: .red,
thumbColor: Color(UIColor.systemTeal)))
Toggle("", isOn: $toggleState2)
.toggleStyle(
ColoredToggleStyle(label: "My Colored Toggle",
onColor: .purple))
From the SwiftUI Book
Just use UIAppearance APIs:
UISwitch.appearance().onTintColor = UIColor.blue
It'll of course by default change the appearance of all the instances of UISwitch, as per UIAppearance documentation.
NOTE: Tested as of Xcode 11 beta 5.
SwiftUI 2.0 (Post WWDC-2020)
Using the new SwiftUI enhancements you can use the .toggleStyle modifier.
// Switch tinting
Toggle(isOn: $order.notifyWhenReady) {
Text("Send notification when ready")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Note this only works for iOS14/iPadOS14/macOS11 and above.
I haven't found a way to directly change a Toggle color yet but an alternative way to have a blue switch or any other custom views, is to create a custom view of your own. To make a custom blue toggle in its simplest form:
struct BlueToggle : UIViewRepresentable {
func makeUIView(context: Context) -> UISwitch {
UISwitch()
}
func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.onTintColor = UIColor.blue
}
}
struct ContentView : View {
var body: some View {
BlueToggle()
}
}
Result:
You can modify the global onTintColor for all UISwitch objects inside init().
#State var enable_dhcp = true
init()
{
UISwitch.appearance().onTintColor = .red
}
var body: some View
{
Toggle("DHCP", isOn: $enable_dhcp)
}
Building off #mohammad-reza-farahani 's solution, here is a fully uncompromising approach to getting the configurability of UISwitch with the implementation protocols if SwiftUI.
First wrap a UISwitch in a UIViewRepresentable and set the colors as you wish:
final class CustomToggleWrapper: UIViewRepresentable {
var isOn: Binding<Bool>
init(isOn: Binding<Bool>) {
self.isOn = isOn
}
func makeUIView(context: Context) -> UISwitch {
UISwitch()
}
func updateUIView(_ uiView: UISwitch, context: Context) {
// On color
uiView.onTintColor = UIColor.blue
// Off color
uiView.tintColor = UIColor.red
uiView.layer.cornerRadius = uiView.frame.height / 2
uiView.backgroundColor = UIColor.red
uiView.isOn = isOn.wrappedValue
// Update bound boolean
uiView.addTarget(self, action: #selector(switchIsChanged(_:)), for: .valueChanged)
}
#objc
func switchIsChanged(_ sender: UISwitch) {
isOn.wrappedValue = sender.isOn
}
}
Second, create a custom toggle style using the wrapped UISwitch:
struct CustomToggleStyle: ToggleStyle {
func makeBody(configuration: Self.Configuration) -> some View {
let toggle = CustomToggleWrapper(isOn: configuration.$isOn)
return HStack {
configuration.label
Spacer()
toggle
}
}
}
Implement a Toggle as you normally would, and apply your CustomToggleStyle:
struct TestView: View {
#State private var isOn: Bool = true
var body: some View {
Toggle(
isOn: $isOn
) {
Text("Test: \(String(isOn))")
}.toggleStyle(CustomToggleStyle()).padding()
}
}
Karol Kulesza and George Valkov have provided a very easy to implement solution. I just wanted to add that you can place the code below inside the app delegate's didFinishLaunching method as well.
UISwitch.appearance().onTintColor = .blue
You can also create more specific appearance configurations with
appearance(whenContainedInInstancesOf:)
See https://www.hackingwithswift.com/example-code/uikit/what-is-the-uiappearance-proxy
As the original question was just about changing the toggle on colour and not full Toggle visual customisation, I think something like this would do:
import SwiftUI
struct CustomToggle: UIViewRepresentable {
#Binding var isOn: Bool
func makeCoordinator() -> CustomToggle.Coordinator {
Coordinator(isOn: $isOn)
}
func makeUIView(context: Context) -> UISwitch {
let view = UISwitch()
view.onTintColor = UIColor.red
view.addTarget(context.coordinator, action: #selector(Coordinator.switchIsChanged(_:)), for: .valueChanged)
return view
}
func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.isOn = isOn
}
class Coordinator: NSObject {
#Binding private var isOn: Bool
init(isOn: Binding<Bool>) {
_isOn = isOn
}
#objc func switchIsChanged(_ sender: UISwitch) {
_isOn.wrappedValue = sender.isOn
}
}
}
// MARK: - Previews
struct CustomToggle_Previews: PreviewProvider {
static var previews: some View {
ViewWrapper()
}
struct ViewWrapper: View {
#State(initialValue: false) var isOn: Bool
var body: some View {
CustomToggle(isOn: $isOn)
.previewLayout(.fixed(width: 100, height: 100))
}
}
}
The easist way is setting UISwitch.appearance().onTintColor = UIColor.red before using toggle and use SwiftUI Toggle like below.
UISwitch.appearance().onTintColor = UIColor.red
...
let toggle = Toggle(isOn: $vm.dataUsePermission, label: {
Text(I18N.permit_data_usage)
.font(SwiftUI.Font.system(size: 16, weight: .regular))
})
if #available(iOS 14.0, *) {
toggle.toggleStyle(
SwitchToggleStyle(tint: Color(UIColor.m.blue500))
)
} else {
toggle.toggleStyle(SwitchToggleStyle())
}
...
You can alse use same Toggle interface in SwiftUI but different name, and change tint color.
TintableSwitch(isOn: .constant(true), label: {
Text("Switch")
})
Toggle(isOn: .constant(true), label: {
Text("Switch")
})
If only need Toggle without Label, then
TintableUISwitch(isOn: .constant(true))
Use below code.
import SwiftUI
public struct TintableSwitch<Label>: View where Label: View {
#Binding var isOn: Bool
var label: Label
public init(isOn: Binding<Bool>, #ViewBuilder label: () -> Label) {
self._isOn = isOn
self.label = label()
}
public var body: some View {
HStack {
label
Spacer()
TintableUISwitch(isOn: $isOn, onTintColor: .red) // πŸ“Œ CHANGE HERE
}
}
}
public struct TintableUISwitch: UIViewRepresentable {
#Binding var isOn: Bool
private var onTintColor: UIColor
public init(isOn: Binding<Bool>, onTintColor: UIColor = UIColor.m.blue500) {
self._isOn = isOn
self.onTintColor = onTintColor
}
public func makeUIView(context: Context) -> UISwitch {
let uiSwitch = UISwitch()
uiSwitch.addTarget(
context.coordinator,
action: #selector(Coordinator.valueChanged(_:)),
for: .valueChanged
)
uiSwitch.onTintColor = onTintColor
uiSwitch.isOn = isOn
return uiSwitch
}
public func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.isOn = isOn
}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
public class Coordinator: NSObject {
var tintableSwitch: TintableUISwitch
init(_ tintableSwitch: TintableUISwitch) {
self.tintableSwitch = tintableSwitch
}
#objc
func valueChanged(_ sender: UISwitch) {
tintableSwitch.isOn = sender.isOn
}
}
}
struct TintableSwitch_Previews: PreviewProvider {
static var previews: some View {
VStack {
TintableSwitch(isOn: .constant(true), label: {
Text("Switch")
})
Toggle(isOn: .constant(true), label: {
Text("Switch")
})
}
}
}
struct TintableUISwitch_Previews: PreviewProvider {
static var previews: some View {
TintableUISwitch(isOn: .constant(true))
}
}
You can change the toggle color in IOS 15.0 using a tint modifier.
Toggle(isOn: $isToggleOn) {
Text("Toggle")
}.tint(.red)
and below IOS 15.0, You can use toggleStyle modifier to change the toggle color but it will be depreciated in the future.
Toggle(isOn: $isToggleOn) {
Text("Toggle")
}.toggleStyle(SwitchToggleStyle(tint: .red))
I would change #Mark Moeykens answer a little bit to avoid having the button tap animation. A better solution would be:
#available(iOS 13.0, *)
struct ColoredToggleStyle: ToggleStyle {
var label = ""
var onColor = UIColor.proacPrimaryBlue.suColor
var offColor = UIColor.systemGray5.suColor
var thumbColor = Color.white
func makeBody(configuration: Self.Configuration) -> some View {
HStack {
Text(label)
Spacer()
RoundedRectangle(cornerRadius: 16, style: .circular)
.fill(configuration.isOn ? onColor : offColor)
.frame(width: 50, height: 29)
.overlay(
Circle()
.fill(thumbColor)
.shadow(radius: 1, x: 0, y: 1)
.padding(1.5)
.offset(x: configuration.isOn ? 10 : -10))
.animation(Animation.easeInOut(duration: 0.1))
.onTapGesture {
configuration.isOn.toggle()
}
}
.font(.title)
.padding(.horizontal)
}
}
This https://stackoverflow.com/a/56480720/5941807 (for now whit Xcode 11 beta 6) is a solution. To switch between to option a fast way is using the boolean instead of if/else:
showFavoritesOnly ? .red : .blue
for foreground:
Toggle(isOn: $showGreeting) {
Text("Show Favorites only").foregroundColor(showFavoritesOnly ? .blue : .gray)
}
for tint:
uiView.onTintColor = showFavoritesOnly ? UIColor.blue : UIColor.gray
In addition for custom colors: https://stackoverflow.com/a/57744208/5941807

How to detect a tap gesture location in SwiftUI?

(For SwiftUI, not vanilla UIKit)
Very simple example code to, say, display red boxes on a gray background:
struct ContentView : View {
#State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
var body: some View {
return ZStack {
Color.gray
.tapAction {
// TODO: add an entry to self.points of the location of the tap
}
ForEach(self.points.identified(by: \.debugDescription)) {
point in
Color.red
.frame(width:50, height:50, alignment: .center)
.offset(CGSize(width: point.x, height: point.y))
}
}
}
}
I'm assuming instead of tapAction, I need to have a TapGesture or something? But even there I don't see any way to get information on the location of the tap. How would I go about this?
Well, after some tinkering around and thanks to this answer to a different question of mine, I've figured out a way to do it using a UIViewRepresentable (but by all means, let me know if there's an easier way!) This code works for me!
struct ContentView : View {
#State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
var body: some View {
return ZStack(alignment: .topLeading) {
Background {
// tappedCallback
location in
self.points.append(location)
}
.background(Color.white)
ForEach(self.points.identified(by: \.debugDescription)) {
point in
Color.red
.frame(width:50, height:50, alignment: .center)
.offset(CGSize(width: point.x, height: point.y))
}
}
}
}
struct Background:UIViewRepresentable {
var tappedCallback: ((CGPoint) -> Void)
func makeUIView(context: UIViewRepresentableContext<Background>) -> UIView {
let v = UIView(frame: .zero)
let gesture = UITapGestureRecognizer(target: context.coordinator,
action: #selector(Coordinator.tapped))
v.addGestureRecognizer(gesture)
return v
}
class Coordinator: NSObject {
var tappedCallback: ((CGPoint) -> Void)
init(tappedCallback: #escaping ((CGPoint) -> Void)) {
self.tappedCallback = tappedCallback
}
#objc func tapped(gesture:UITapGestureRecognizer) {
let point = gesture.location(in: gesture.view)
self.tappedCallback(point)
}
}
func makeCoordinator() -> Background.Coordinator {
return Coordinator(tappedCallback:self.tappedCallback)
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<Background>) {
}
}
I was able to do this with a DragGesture(minimumDistance: 0). Then use the startLocation from the Value on onEnded to find the tap's first location.
Update iOS 16
Starting form iOS 16 / macOS 13, the onTapGesture modifier makes available the location of the tap/click in the action closure:
struct ContentView: View {
var body: some View {
Rectangle()
.frame(width: 200, height: 200)
.onTapGesture { location in
print("Tapped at \(location)")
}
}
}
Original Answser
The most correct and SwiftUI-compatible implementation I come up with is this one. You can use it like any regular SwiftUI gesture and even combine it with other gestures, manage gesture priority, etc...
import SwiftUI
struct ClickGesture: Gesture {
let count: Int
let coordinateSpace: CoordinateSpace
typealias Value = SimultaneousGesture<TapGesture, DragGesture>.Value
init(count: Int = 1, coordinateSpace: CoordinateSpace = .local) {
precondition(count > 0, "Count must be greater than or equal to 1.")
self.count = count
self.coordinateSpace = coordinateSpace
}
var body: SimultaneousGesture<TapGesture, DragGesture> {
SimultaneousGesture(
TapGesture(count: count),
DragGesture(minimumDistance: 0, coordinateSpace: coordinateSpace)
)
}
func onEnded(perform action: #escaping (CGPoint) -> Void) -> _EndedGesture<ClickGesture> {
self.onEnded { (value: Value) -> Void in
guard value.first != nil else { return }
guard let location = value.second?.startLocation else { return }
guard let endLocation = value.second?.location else { return }
guard ((location.x-1)...(location.x+1)).contains(endLocation.x),
((location.y-1)...(location.y+1)).contains(endLocation.y) else {
return
}
action(location)
}
}
}
The above code defines a struct conforming to SwiftUI Gesture protocol. This gesture is a combinaison of a TapGesture and a DragGesture. This is required to ensure that the gesture was a tap and to retrieve the tap location at the same time.
The onEnded method checks that both gestures occurred and returns the location as a CGPoint through the escaping closure passed as parameter. The two last guard statements are here to handle multiple tap gestures, as the user can tap slightly different locations, those lines introduce a tolerance of 1 point, this can be changed if ones want more flexibility.
extension View {
func onClickGesture(
count: Int,
coordinateSpace: CoordinateSpace = .local,
perform action: #escaping (CGPoint) -> Void
) -> some View {
gesture(ClickGesture(count: count, coordinateSpace: coordinateSpace)
.onEnded(perform: action)
)
}
func onClickGesture(
count: Int,
perform action: #escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: count, coordinateSpace: .local, perform: action)
}
func onClickGesture(
perform action: #escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: 1, coordinateSpace: .local, perform: action)
}
}
Finally View extensions are defined to offer the same API as onDragGesture and other native gestures.
Use it like any SwiftUI gesture:
struct ContentView : View {
#State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
var body: some View {
return ZStack {
Color.gray
.onClickGesture { point in
points.append(point)
}
ForEach(self.points.identified(by: \.debugDescription)) {
point in
Color.red
.frame(width:50, height:50, alignment: .center)
.offset(CGSize(width: point.x, height: point.y))
}
}
}
}
An easy solution is to use the DragGesture and set minimumDistance parameter to 0 so that it resembles the tap gesture:
Color.gray
.gesture(DragGesture(minimumDistance: 0).onEnded({ (value) in
print(value.location) // Location of the tap, as a CGPoint.
}))
In case of a tap gesture it will return the location of this tap. However, it will also return the end location for a drag gesture – what's also referred to as a "touch up event". Might not be the desired behavior, so keep it in mind.
It is also possible to use gestures.
There is a few more work to cancel the tap if a drag occurred or trigger action on tap down or tap up..
struct ContentView: View {
#State var tapLocation: CGPoint?
#State var dragLocation: CGPoint?
var locString : String {
guard let loc = tapLocation else { return "Tap" }
return "\(Int(loc.x)), \(Int(loc.y))"
}
var body: some View {
let tap = TapGesture().onEnded { tapLocation = dragLocation }
let drag = DragGesture(minimumDistance: 0).onChanged { value in
dragLocation = value.location
}.sequenced(before: tap)
Text(locString)
.frame(width: 200, height: 200)
.background(Color.gray)
.gesture(drag)
}
}
Just in case someone needs it, I have converted the above answer into a view modifier which also takes a CoordinateSpace as an optional parameter
import SwiftUI
import UIKit
public extension View {
func onTapWithLocation(coordinateSpace: CoordinateSpace = .local, _ tapHandler: #escaping (CGPoint) -> Void) -> some View {
modifier(TapLocationViewModifier(tapHandler: tapHandler, coordinateSpace: coordinateSpace))
}
}
fileprivate struct TapLocationViewModifier: ViewModifier {
let tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
func body(content: Content) -> some View {
content.overlay(
TapLocationBackground(tapHandler: tapHandler, coordinateSpace: coordinateSpace)
)
}
}
fileprivate struct TapLocationBackground: UIViewRepresentable {
var tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
func makeUIView(context: UIViewRepresentableContext<TapLocationBackground>) -> UIView {
let v = UIView(frame: .zero)
let gesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapped))
v.addGestureRecognizer(gesture)
return v
}
class Coordinator: NSObject {
var tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
init(handler: #escaping ((CGPoint) -> Void), coordinateSpace: CoordinateSpace) {
self.tapHandler = handler
self.coordinateSpace = coordinateSpace
}
#objc func tapped(gesture: UITapGestureRecognizer) {
let point = coordinateSpace == .local
? gesture.location(in: gesture.view)
: gesture.location(in: nil)
tapHandler(point)
}
}
func makeCoordinator() -> TapLocationBackground.Coordinator {
Coordinator(handler: tapHandler, coordinateSpace: coordinateSpace)
}
func updateUIView(_: UIView, context _: UIViewRepresentableContext<TapLocationBackground>) {
/* nothing */
}
}
Using some of the answers above, I made a ViewModifier that is maybe useful:
struct OnTap: ViewModifier {
let response: (CGPoint) -> Void
#State private var location: CGPoint = .zero
func body(content: Content) -> some View {
content
.onTapGesture {
response(location)
}
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onEnded { location = $0.location }
)
}
}
extension View {
func onTapGesture(_ handler: #escaping (CGPoint) -> Void) -> some View {
self.modifier(OnTap(response: handler))
}
}
Then use like so:
Rectangle()
.fill(.green)
.frame(width: 200, height: 200)
.onTapGesture { location in
print("tapped: \(location)")
}
Using DragGesture with minimumDistance broke scroll gestures on all the views that are stacked under. Using simultaneousGesture did not help. What ultimately did it for me was using sequencing the DragGesture to a TapGesture inside simultaneousGesture, like so:
.simultaneousGesture(TapGesture().onEnded {
// Do something
}.sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { value in
print(value.startLocation)
}))
In iOS 16 and MacOS 13 there are better solutions, but to stay compatible with older os versions, I use this rather simple gesture, which also has the advantage of distinguish between single- and double-click.
var combinedClickGesture: some Gesture {
SimultaneousGesture(ExclusiveGesture(TapGesture(count: 2),TapGesture(count: 1)), DragGesture(minimumDistance: 0) )
.onEnded { value in
if let v1 = value.first {
var count: Int
switch v1 {
case .first(): count = 2
case .second(): count = 1
}
if let v2 = value.second {
print("combinedClickGesture couunt = \(count) location = \(v2.location)")
}
}
}
}
As pointed out several times before itΒ΄s a problem when the view already is using DragGesture, but often it is fixed when using the modifier:
.simultaneousGesture(combinedClickGesture)
instead of
.gesture(combinedClickGesture)
Posting this for others who still have to support iOS 15.
It's also possible using GeometryReader and CoordinateSpace. The only downside is depending on your use case you might have to specify the size of the geometry reader.
VStack {
Spacer()
GeometryReader { proxy in
Button {
print("Global tap location: \(proxy.frame(in: .global).center)")
print("Custom coordinate space tap location: \(proxy.frame(in: .named("StackOverflow")))")
} label: {
Text("Tap me I know you want it")
}
.frame(width: 42, height: 42)
}
.frame(width: 42, height: 42)
Spacer()
}
.coordinateSpace(name: "StackOverflow")