Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to detect a 'Click' gesture in SwiftUI tvOS

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).

like image 888
Ahmed Sarfraz Avatar asked Dec 05 '19 17:12

Ahmed Sarfraz


2 Answers

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")
   }
like image 194
Ádám Nagy Avatar answered Nov 14 '22 07:11

Ádám Nagy


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)
            }
        }
    }
}
like image 35
Jeff Avatar answered Nov 14 '22 05:11

Jeff