SwiftUI: Action of custom button not triggered - xcode

Testing some stuff on tvOS with SwiftUI. When i add a custom style to a button, the "action" of the button is not getting triggered. In this example, i wanna print "pressed", but adding functions is not working either.
Is it possible to implement an custom action trigger aswell or what am i doing wrong?
For clarification why i wanna have a custom button style.
When i dont use any buttonstyle, the "action" is working, but the "onFocusChange" function is never getting called. WHICH I NEED!
But.. when i use a buttonstyle, the onFocusChange is working but the action is not.....
struct CustomButton: View {
#State private var buttonFocus: Bool = false
var body: some View {
VStack(alignment: .center){
Button(action: {
print("pressed")
})
{
Text("Save")
}
.buttonStyle(TestButtonStyle(focused: buttonFocus))
.focusable(true, onFocusChange: { (changed) in
self.buttonFocus.toggle()
})
}
}
}
Buttonstyle:
struct TestButtonStyle: ButtonStyle {
let focused: Bool
public func makeBody(configuration: TestButtonStyle.Configuration) -> some View {
configuration.label
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 5).fill(Color.red))
.scaleEffect(focused ? 1.5 : 1.0)
}
}

Its working with SwiftUI 2.0 now.
You can use the following environment Object on your View you want to be focused.
Also, add a State or Binding to bubble up to your parent view.
#Environment(\.isFocused) var isFocused: Bool
#Binding var focusedValue: Bool
Then, you can call the following modifier, which gets called when the View is getting focused or not. Here you change your Binding or State variable.
.onChange(of: isFocused, perform: { value in
self.focusedValue = value
})
Finally, you can use your Binding or State to modify your View.
.scaleEffect(self.focused ? 1.1 : 1)
.animation(.linear(duration: 0.1))

Related

How to observe for modifier key pressed (e.g. option, shift) with NSNotification in SwiftUI macOS project?

I want to have a Bool property, that represents that option key is pressed #Publised var isOptionPressed = false. I would use it for changing SwiftUI View.
For that, I think, that I should use Combine to observe for key pressure.
I tried to find an NSNotification for that event, but it seems to me that there are no any NSNotification, that could be useful to me.
Since you are working through SwiftUI, I would recommend taking things just a step beyond watching a Publisher and put the state of the modifier flags in the SwiftUI Environment. It is my opinion that it will fit in nicely with SwiftUI's declarative syntax.
I had another implementation of this, but took the solution you found and adapted it.
import Cocoa
import SwiftUI
import Combine
struct KeyModifierFlags: EnvironmentKey {
static let defaultValue = NSEvent.ModifierFlags([])
}
extension EnvironmentValues {
var keyModifierFlags: NSEvent.ModifierFlags {
get { self[KeyModifierFlags.self] }
set { self[KeyModifierFlags.self] = newValue }
}
}
struct ModifierFlagEnvironment<Content>: View where Content:View {
#StateObject var flagState = ModifierFlags()
let content: Content;
init(#ViewBuilder content: () -> Content) {
self.content = content();
}
var body: some View {
content
.environment(\.keyModifierFlags, flagState.modifierFlags)
}
}
final class ModifierFlags: ObservableObject {
#Published var modifierFlags = NSEvent.ModifierFlags([])
init() {
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
self?.modifierFlags = event.modifierFlags
return event;
}
}
}
Note that my event closure is returning the event passed in. If you return nil you will prevent the event from going farther and someone else in the system may want to see it.
The struct KeyModifierFlags sets up a new item to be added to the view Environment. The extension to EnvironmentValues lets us store and
retrieve the current flags from the environment.
Finally there is the ModifierFlagEnvironment view. It has no content of its own - that is passed to the initializer in an #ViewBuilder function. What it does do is provide the StateObject that contains the state monitor, and it passes it's current value for the modifier flags into the Environment of the content.
To use the ModifierFlagEnvironment you wrap a top-level view in your hierarchy with it. In a simple Cocoa app built from the default Xcode template, I changed the application SwiftUI content to be:
struct KeyWatcherApp: App {
var body: some Scene {
WindowGroup {
ModifierFlagEnvironment {
ContentView()
}
}
}
}
So all of the views in the application could watch the flags.
Then to make use of it you could do:
struct ContentView: View {
#Environment(\.keyModifierFlags) var modifierFlags: NSEvent.ModifierFlags
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
if(modifierFlags.contains(.option)) {
Text("Option is pressed")
} else {
Text("Option is up")
}
}
.padding()
}
}
Here the content view watches the environment for the flags and the view makes decisions on what to show using the current modifiers.
Ok, I found easy solution for my problem:
class KeyPressedController: ObservableObject {
#Published var isOptionPressed = false
init() {
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event -> NSEvent? in
if event.modifierFlags.contains(.option) {
self?.isOptionPressed = true
} else {
self?.isOptionPressed = false
}
return nil
}
}
}

SwiftUI Tooltip on hover

SwiftUI provides the .help() modifier but it is too small, cannot be customised and takes too long to appear to actually serve its intended purpose. I would like to create a tooltip that appears immediately on hover and is larger, similar to the one that appears on hovering on an icon in the Dock.
Something like this:
Is this possible to create from SwiftUI itself? I've tried using a popover but it prevents hover events from propagating once its open, so I can't make it close when the mouse moves away.
Solution #1: Check for .onHover(...)
Use the .onHover(perform:) view modifier to toggle a #State property to keep track of whether your tooltip should be displayed:
#State var itemHovered: Bool = false
var body: some View {
content
.onHover { hover in
itemHovered = hover
}
.overlay(
Group {
if itemHovered {
Text("This is a tooltip")
.background(Color.white)
.foregroundColor(.black)
.offset(y: -50.0)
}
}
)
}
Solution #2: Make a Tooltip Wrapper View
Create a view wrapper that creates a tooltip view automatically:
struct TooltipWrapper<Content>: View where Content: View {
#ViewBuilder var content: Content
var hover: Binding<Bool>
var text: String
var body: some View {
content
.onHover { hover.wrappedValue = $0 }
.overlay(
Group {
if hover.wrappedValue {
Text("This is a tooltip")
.background(Color.white)
.foregroundColor(.black)
.offset(y: -50.0)
}
}
)
}
}
Then you can call with
#State var hover: Bool = false
var body: some View {
TooltipWrapper(hover: $hover, text: "This is a tooltip") {
Image(systemName: "arrow.right")
Text("Hover over me!")
}
}
From this point, you can customize the hover tooltip wrapper to your liking.
Solution #3: Use my Swift Package
I wrote a 📦 Swift Package that makes SwiftUI a little easier for personal use, and it includes a tooltip view modifier that boils the solution down to:
import ShinySwiftUI
#State var showTooltip: Bool = false
var body: some View {
MyView()
.withTooltip(present: $showTooltip) {
Text("This is a tooltip!")
}
}
Notice you can provide your own custom views in the tooltip modifier above, like Image or VStack. Alternatively, you could use HoverView to get a stateful hover variable to use solely within your view:
HoverView { hover in
Rectangle()
.foregroundColor(hover ? .red : .blue)
.overlay(
Group {
if hover { ... }
}
)
}

SwiftUI Sheet Crash with Occasional Warning about Modifying State

I have a SwiftUI button that, when clicked, a sheet displays a confirmation modal. When a button is clicked in that modal to confirm, I make a save to Core Data.
I'm getting one or all of a few nasty results:
The sheet just hangs and becomes unresponsive.
I get a warning: "Modifying state during view update, this will cause undefined behavior".
I get a crash.
Obviously I'm modifying the state becomes I'm deleting a thing, but I'm unclear on how to do this right.
Here is my view where I click delete:
struct MyView: View{
#State var showDeleteModal = false
var body: some View{
Button("Delete"){
showDeleteModal.toggle()
}
.sheet(isPresented: self.$showDeleteModal) {
ModalView(confirm: {
self.showDeleteModal.toggle()
//Save the object in my Core Data stuff
model.saveThing(thing: thing)
})
}
}
}
And here's my modal that has a callback function to confirm the deletion:
struct ModalView: View {
var confirm:() -> Void
var body: some View {
Button("Confirm"){
confirm()
}
}
}
How do I hide the modal and make the save (which removes the view from the screen) without interfering with SwiftUI's state?
I'd suggest using sheet(isPresented:onDismiss:content:) and calling model.saveThing in onDismiss:
struct MyView: View {
#State var showDeleteModal = false
var body: some View {
Button("Delete") {
showDeleteModal.toggle()
}
.sheet(isPresented: self.$showDeleteModal, onDismiss: onDismiss) {
ModalView()
}
}
func onDismiss() {
model.saveThing(thing: thing)
}
}
Then you can dismiss the sheet without knowing the parent's state - just use #Environment(\.presentationMode):
struct ModalView: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
Button("Confirm") {
presentationMode.wrappedValue.dismiss()
}
}
}
Note: as sheet can be closed without interacting with the confirm button, you can detect how it was closed using another #State variable - see:
SwiftUI: How to show an alert after a sheet is closed?

How do you display a view (in ContentView) depending on a variable of another view in SwiftUI?

Basically, in my ContentView, I'm trying to make a CreateItemButton() when the Binding variableName (not the variableName in ContentView, the variableName from the MapView class) is true. So....the pseudocode for ContentView looks something like:
struct ContentView: View {
#State private var variableName: Bool = false
var body: some View {
VStack {
ZStack {
MapView(variableName: $variableName)
//Display the CreateEventButton() view here if $variableName is true.
}
}
}
}
I was hoping there was either some function that I can call on the MapView() view to optionally display the CreateEventButton() view but the closest thing I found to what I need was the .sheet(isPresented: $variableName) function....and I need the view to display directly on top of the map, not pull up a new sheet with the view on it. Let me know if I can add anything to help make it clearer!
Here it is
ZStack {
MapView(variableName: $variableName)
if variableName {
CreateEventButton()
}
}

Select SwiftUI cell in Popover

I am trying to make a popover in SwiftUI using a UIHostingController with a list that can be tapped. First, the user name and password should be filled in, and then the user role should be tapped in the list, and the popover should be dismissed when the save button is tapped.
Also, the save button in the navigation bar should be disabled until the user information has been verified.
The Xcode playground for this can be fetched from my GitHub repository https://github.com/imyrvold/Popover
To be able to use the AddUserView as a rootView in UIHostingController, I had to use an Xcode storyboard, and add it to the Resources in the Xcode Playground.
import SwiftUI
import Combine
public struct AddUserView : View {
#ObjectBinding public var loginInfo: LoginInfo
#EnvironmentObject var viewModel: RoleViewModel
#State var selectedRole: Role? = nil
#Environment(\.isPresented) var isPresented: Binding<Bool>?
public var body: some View {
NavigationView {
VStack {
TextField(self.$loginInfo.firstName, placeholder: Text("First Name"))
TextField(self.$loginInfo.lastName, placeholder: Text("Last Name"))
TextField(self.$loginInfo.email, placeholder: Text("Email"))
SecureField(self.$loginInfo.password, placeholder: Text("Password"))
Divider()
List(self.viewModel.roles) { role in
RoleCell(role: role).tapAction {
self.selectedRole = role
}
}
}
.padding()
.navigationBarTitle(Text("Add User"))
.navigationBarItems(trailing:
Button(action: {
self.saveAction()
self.isPresented?.value = false
}) {
Text("Save")
})//.disabled(!self.loginInfo.isValid)
}
}
// MARK:- Action methods
func saveAction() {
}
}
The first problem I have is that when I uncomment the disabled(!self.loginInfo.isValid), all the TextField's are also disabled. Not sure if that is a bug in SwiftUI?
I also want to have the rolecell set the checkmark on the cell when tapped, but so far I have been unable to figure out how to do that.
And how can I dismiss the Popover when the save button is tapped?
(When running the playground, have to click the start playground a second time to run properly, the first time the Save popover doesn't work).
Have you tried this
.navigationBarItems(trailing:
Button(action: {
self.saveAction()
self.isPresented?.value = false
}) {
Text("Save")
}.disabled(!self.loginInfo.isValid))

Resources