Monitor for Keypress in Meun/Picker in SwiftUI on MacOS - swiftui

I'm learning SwiftUI programming by trying to duplicate basic features in the MacOS Finder.
The groups button in the Finder window (screenshot below) has me stumped. Clicking the menu shows the group options, while option-clicking shows the sort options. I can't figure out how that's done.
My basic code is as follows:
Menu {
if NSEvent.modifierFlags.contains(.option) {
Picker(selection: viewSorts, label: EmptyView()) {
ForEach(viewSorts) { sort in
Text(sort.name).tag(sort)
}
}
.labelsHidden()
.pickerStyle(InlinePickerStyle())
} else {
Picker(selection: viewGroups, label: EmptyView()) {
ForEach(viewGroups) { group in
Text(group.name).tag(group)
}
.labelsHidden()
.pickerStyle(InlinePickerStyle())
}
} Label: {
Image(systemName: "square.grid.3x1.below.line.grid.1x2")
}
It works, however NSEvent.modifierFlags.contains(.option) never evaluates to true.
This post has two examples that I used to try to fix the problem:
Using onTapGesture with EventModifiers:
#State private var showSort = false
Menu {
if showSort {
// ... show sort picker ...
} else {
// ... show group picker ...
}
} Label: {
Image(systemName: "square.grid.3x1.below.line.grid.1x2")
}
.gesture(TapGesture().modifiers(.option).onEnded {
showSort = true
})
.onTapGesture {
showSort = false
}
And another using a CGKeyCode extension:
import CoreGraphics
extension CGKeyCode
{
static let kVK_Option : CGKeyCode = 0x3A
static let kVK_RightOption: CGKeyCode = 0x3D
var isPressed: Bool {
CGEventSource.keyState(.combinedSessionState, key: self)
}
static var optionKeyPressed: Bool {
return Self.kVK_Option.isPressed || Self.kVK_RightOption.isPressed
}
}
Menu {
if CGKeyCode.optionIsPressed {
// ... show sort picker ...
} else {
// ... show group picker ...
}
} Label: {
Image(systemName: "square.grid.3x1.below.line.grid.1x2")
}
And from these two posts (1, 2), addLocalMonitorForEvents:
#State private var showSort = false
Menu {
if showSort {
// ... show sort picker ...
} else {
// ... show group picker ...
}
} Label: {
Image(systemName: "square.grid.3x1.below.line.grid.1x2")
}
.onAppear() {
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (keyEvent) -> NSEvent? in
if keyEvent.modifierFlags == .option {
showSort = true
} else {
showSort = false
}
return keyEvent
}
}
The answer is probably staring at me in the face, but I just can't see it! Thank you for any help!
UPDATE: onContinuousHover does work, but it only updates when the mouse is moving over the menu.
.onContinuousHover { _ in
showSort = NSEvent.modifierFlags.contains(.option) ? true : false
}
But onTapGesture doesn't work
.onTapGesture {
showSort = NSEvent.modifierFlags.contains(.option) ? true : false
}

Related

Multiple sheet(item: ) triggered by SwipeActions button and ToolBar buttons gets nil object in the first time in SwiftUI

I am using swipeActions in ForEach loop of a List and toolbar buttons to show different sheets on my SwiftUI view. But every first time I swipe left and click the Edit button, the object of that line is nil. If I do the same swipe and click thing again, everything goes well. Anyone else had this kind of bug before? Thank you.
Here is the related code:
struct LanguagesView: View {
#State var activeSheet: ActiveSheet?
#State var toBeEdit: MeLanguage?
var body: some View {
NavigationView {
List {
ForEach(self.meLanguages, id: \.id) { lan in
HStack {
Text("\(lan.wrappedName)")
.font(.headline)
}.swipeActions(allowsFullSwipe: false) {
Button(
action: {
self.activeSheet = .editLanguage
self.toBeEdit = lan
},
label: { Label("Edit", systemImage: "pencil") }
) .tint(.indigo)
}
}
}
.sheet(item: $activeSheet,
onDismiss: {
self.toBeEdit = nil
}
){
item in
switch item {
case .addLanguage:
AddLanguage()
case .sortLanguages:
SortLanguagesView()
case .editLanguage:
if self.toBeEdit != nil {
EditLanguageView( meLanguage: self.toBeEdit! )
}
else {
Text("self.toBeEdit is nil")
}
default:
Text("No such button on ContentView.")
}
}
.toolbar {
ToolbarItemGroup {
HStack {
Text("\(self.meLanguages.count) Languages on Card").font(.headline)
self.barButtons
}
}
}
}
}
var barButtons: some View {
HStack {
Button(
action: {
self.activeSheet = .sortLanguages
},
label: { Label("Sort Languages", systemImage: "arrow.up.arrow.down.circle")
}
).id("sortLanguages")
Button(
action: {
self.activeSheet = .addLanguage
},
label: { Label("Add Language",
systemImage: "plus")
.imageScale(.large)
}
)
}
}
}
If I only think of the sheet triggered by swipeActions Edit button, the code below works perfectly. But I still need other sheets triggered by ToolBar buttons.
.sheet(item: self.$toBeEdit, content: { toBeEdit in
EditLanguageView( meLanguage: toBeEdit)
})
After more searching I realised it's not something about SwipeActions. This is actually similar to this question:
SwiftUI presenting sheet with Binding variable doesn't work when first presented
So I added an hidden Text after the ForEach loop and the problem solved.
ForEach(self.meLanguages, id: \.id) { ... }
if self.toBeEdit != nil {
Text("\(self.toBeEdit!.wrappedName)").hidden()
}

Problem with .disable modifier in ContextMenu SwiftUI

I found interesting action in my program:
struct Test: View {
#State private var redButton: Bool = false
var body: some View {
List {
ForEach(1...10, id: \.self) { numbers in
Button {
redButton = false
} label: {
Text("Button \(numbers)")
}.contextMenu {
Button {
//action code
redButton = true
} label: {
Text("Deactivate")
}.disabled(redButton)
}
}
}
}
}
If u run this code and press "Deactivate" in contexMenu, contextMenu will be disabled only for 6..10 buttons, this code switching off/on contextMenu element randomly (try increase or decrease Lists elements and press "Deactivate" on random List element).
If U remove List all working correctly with one Button.
Maybe I need work with dispatchQueue.main.async when change redButton status?
What I doing wrong?
Correct code:
struct Test: View {
#State var redButton: Bool = false
var body: some View {
List {
ForEach(1...3, id: \.self) { numbers in
Menu("Actions \(numbers)") {
Button("Deactivate", action: {
redButton = true
})
Button("Activate", action: {
redButton = false
})
Button(action: {}) {
Label("Delete", systemImage: "trash")
}.disabled(redButton)
Button(action: {}) {
Label("Call", systemImage: "phone")
}.disabled(redButton)
}
}
}
}
}

SwiftUI: fullScreenCover with no animation?

I have this view:
struct TheFullCover: View {
#State var showModal = false
var body: some View {
Button(action: {
showModal.toggle()
}) {
Text("Show Modal")
.padding()
.foregroundColor(.blue)
}
.background(Color(.white))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.red, lineWidth:1)
)
.fullScreenCover(isPresented: $showModal, onDismiss: {
}, content: {
VStack {
Text("Here I am")
TheFullCover()
}
})
}
}
Every time I press the Button, the modal screen comes up fullscreen. All works great.
Question:
How do I disable the slide up animation? I want the view to be presented immediately fullscreen without animating to it.
Is there a way to do that?
A possible solution is to disable views animation completely (and then, if needed, enable again in .onAppear of presenting content), like
Button(action: {
UIView.setAnimationsEnabled(false) // << here !!
showModal.toggle()
}) {
and then
}, content: {
VStack {
Text("Here I am")
TheFullCover()
}
.onAppear {
UIView.setAnimationsEnabled(true) // << here !!
}
})
Tested with Xcode 13 / iOS 15
AFAIK the proper to do it as of today is using transaction https://developer.apple.com/documentation/swiftui/transaction
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
showModal.toggle()
}
I also created a handy extension for this:
extension View {
func withoutAnimation(action: #escaping () -> Void) {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
action()
}
}
}
which can be used like this:
withoutAnimation {
// do your thing
}
.fullScreenCover(isPresented: isPresented) {
content()
.background(TransparentBackground())
}
.transaction({ transaction in
transaction.disablesAnimations = true
})
this should work, based on #asamoylenko's answer
At the moment, I find it easier to use UIKit for presentation in SwiftUI.
someView
.onChange(of: isPresented) { _ in
if isPresented {
let vc = UIHostingController(rootView: MyView())
vc.modalPresentationStyle = .overFullScreen
UIApplication.shared.rootVC?.present(vc, animated: false)
} else {
UIApplication.shared.rootVC?.dismiss(animated: false)
}
}
Why not use an overlay instead?
.overlay {
if isLoading {
ZStack {
ProgressView()
}
.background(BackgroundCleanerView())
}
}

TabView disconnects when rotating to Landscape due to SwiftUI's re-render of parent-Views

Using Swift5.3.2, iOS14.4.1, XCode12.4,
As the following code shows, I am working with a quite complex TabView in Page-Mode in SwiftUI.
i.e. using iOS14's new possibility to show Pages:
.tabViewStyle(PageTabViewStyle())
Everything works.
Except, if I rotate my iPhone from Portrait to Landscape, the TabView disconnects and sets the selectedTab index to 0 (i.e. no matter where you scrolled to, rotating iPhone resets unwontedly to page 0).
The parent-View itself is in a complex View hierarchy. And one of the parent-View's of the TabView is updated during the TabView is shown (and swiped). And this might be the problem that the TabView gets re-rendered when rotating to Landscape.
What can I do to keep the TabView-Page during iPhone rotation ??
Here is the code:
import SwiftUI
struct PageViewiOS: View {
var body: some View {
ZStack {
Color.black
MediaTabView()
CloseButtonView()
}
}
}
And the MediaTabView at question:
struct MediaTabView: View {
#EnvironmentObject var appStateService: AppStateService
#EnvironmentObject var commService: CommunicationService
#State private var tagID = ""
#State private var selectedTab = 0
#State private var uniqueSelected = 0
#State private var IamInSwipingAction = false
var body: some View {
let myDragGesture = DragGesture(minimumDistance: 10)
.onChanged { _ in
IamInSwipingAction = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(5000)) {
IamInSwipingAction = false // workaround: because onEnded does not work...
}
}
.onEnded { _ in
IamInSwipingAction = false
}
TabView(selection: self.$selectedTab) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
switch appStateService.appState {
case .content(let tagID):
return list.tagId == tagID
default:
return false
}
}) {
if list.paths.count > 0 {
ForEach(list.paths.indices, id: \.self) { index in
ZoomableScrollView {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
} else if url.containsVideo {
CustomPlayerView(url: url)
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.onAppear() {
if uniqueSelected != selectedTab {
uniqueSelected = selectedTab
if IamInSwipingAction && (commService.communicationRole == .moderatorMode) {
commService.send(thCmd: THCmd(key: .swipeID, sender: "", content: URL(fileURLWithPath: list.paths[index]).lastPathComponent))
}
}
}
}
} else {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
if Constants.TrihowAlbum.tagIdArrayTrihowAlbum.contains(tagID) {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
} else {
Text(LocalizedStringKey("TagNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
}
.onAppear() {
switch appStateService.appState {
case .content(let tagID):
self.tagID = tagID
default:
self.tagID = ""
}
}
.tabViewStyle(PageTabViewStyle())
.onTHComm_ReceiveCmd(service: commService) { (thCmd) in
switch thCmd.key {
case .swipeID:
if (commService.communicationRole == .moderatorMode) || (commService.communicationRole == .discoveryMode) {
selectTabFromCmdID(fileName: thCmd.content)
} else {
break
}
default:
break
}
}
.simultaneousGesture(myDragGesture)
}
}
extension MediaTabView {
private func selectTabFromCmdID(fileName: String) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
return list.tagId == tagID
}) {
if list.paths.count > 0 {
if let idx = list.paths.firstIndex(where: { (urlPath) -> Bool in
if let url = URL(string: urlPath) {
return url.lastPathComponent == fileName
} else { return false }
}) {
selectedTab = idx
}
}
}
}
}

appletv how to react on focus change event

i tried to change the textcolor of a button on AppleTV on focus change and this is my code (see below).
Unfortunately the code with if focus .. is never called. What am i doing wrong?
Thank you for any help!
struct ContentView: View {
#State var textColor : Color = .white
var body: some View {
VStack {
Button(action: {
self.textColor = .black
}) {
Text("tap me")
}
.focusable(true) { (focus) in
if focus {
self.textColor = .blue
} else {
self.textColor = .green
}
}
Button(action: {
self.textColor = .black
}) {
Text("another tap me")
}
.focusable(true) { (focus) in
if focus {
self.textColor = .blue
} else {
self.textColor = .green
}
}
}
}
}
The .focusable adds capability for elements non-focusable by nature, like Text (or Image), but Button is focusable as-is, so nothing happens.
The following modifications of your example works (tested with Xcode 11.2):
var body: some View {
VStack {
Text("Focusable").foregroundColor(textColor)
.focusable(true) { (focus) in
if focus {
self.textColor = .blue
} else {
self.textColor = .green
}
}
Button(action: {
self.textColor = .black
}) {
Text("Button")
}
}
}