How to detect a single mouse down event in SwiftUI on macOS - macos

I want to detect ONE mouse down and ONE mouse up event on my view (a simple Rectangle). Here is the code I already made. Unfortunately I got a lot of 'mouse down' and 'mouse up' on the console. This not what I want. I want just one 'mouse down' when the mouse is pressed on my rectangle and one 'mouse up' when the mouse is released.
var body: some View {
Rectangle()
.onAppear(perform: {
NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown]) { event in
print ("mouse down")
return event
}
NSEvent.addLocalMonitorForEvents(matching: [.leftMouseUp]) { event in
print ("mouse up")
return event
}
})
}

I found the solution by using a View modifier
//
// ContentView.swift
// OnPressedOnRelease
//
import SwiftUI
struct ContentView: View {
#State private var message: String = "Click on rectangle"
var body: some View {
VStack {
Image(systemName: "magicmouse")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("\(message)")
Rectangle()
.modifier(PressActions(
onPress: {
// Do something on press...
message = "Mouse down"
},
onRelease: {
// Do something on release...
message = "Mouse up"
}
))
}
.padding()
.frame(width: 200, height: 200)
}
}
struct PressActions: ViewModifier {
var onPress: () -> Void
var onRelease: () -> Void
func body(content: Content) -> some View {
content
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
onPress()
})
.onEnded({ _ in
onRelease()
})
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

I've recently worked with Gestures and a good solution is to do this :
struct ContentView: View {
#State var dragGestureValue: DragGesture.Value?
var body: some View {
Rectangle()
.gesture(pressActionsGesture())
.padding()
}
private func pressActionsGesture() -> some Gesture {
return DragGesture(minimumDistance: 0)
.onChanged {
// If nil, it means its the beginning of the DragGesture
if dragGestureValue == nil {
print("Mouse Down")
}
// Gives DragGesture informations to our #State value
dragGestureValue = $0
}
.onEnded { _ in
print("Mouse Up")
dragGestureValue = nil
}
}
}

Related

SwiftUI: animating tab item addition/removal in tab bar

In my app I add/remove a subview to/from a TabView based on some condition. I'd like to animate tab item addition/removal in tab bar. My experiment (see code below) shows it's not working. I read on the net that TabView support for animation is quite limited and some people rolled their own implementation. But just in case, is it possible to implement it?
import SwiftUI
struct ContentView: View {
#State var showBoth: Bool = false
var body: some View {
TabView {
Button("Test") {
withAnimation {
showBoth.toggle()
}
}
.tabItem {
Label("1", systemImage: "1.circle")
}
if showBoth {
Text("2")
.tabItem {
Label("2", systemImage: "2.circle")
}
.transition(.slide)
}
}
}
}
Note: moving transition() call to the Label passed to tabItem() doesn't work either.
As commented Apple wants the TabBar to stay unchanged throughout the App.
But you can simply implement your own Tabbar with full control:
struct ContentView: View {
#State private var currentTab = "One"
#State var showBoth: Bool = false
var body: some View {
VStack {
TabView(selection: $currentTab) {
// Tab 1.
VStack {
Button("Toggle 2. Tab") {
withAnimation {
showBoth.toggle()
}
}
} .tag("One")
// Tab 2.
VStack {
Text("Two")
} .tag("Two")
}
// custom Tabbar buttons
Divider()
HStack {
OwnTabBarButton("One", imageName: "1.circle")
if showBoth {
OwnTabBarButton("Two", imageName: "2.circle")
.transition(.scale)
}
}
}
}
func OwnTabBarButton(_ label: String, imageName: String) -> some View {
Button {
currentTab = label
} label: {
VStack {
Image(systemName: imageName)
Text(label)
}
}
.padding([.horizontal,.top])
}
}

Calling a sheet from a menu item

I have a macOS app that has to display a small dialog with some information when the user presses the menu item "Info".
I've tried calling doing this with a .sheet but can't get it to display the sheet. Code:
#main
struct The_ThingApp: App {
private let dataModel = DataModel()
#State var showsAlert = false
#State private var isShowingSheet = false
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
}
.commands {
CommandMenu("Info") {
Button("Get Info") {
print("getting info")
isShowingSheet.toggle()
}
.sheet(isPresented: $isShowingSheet) {
VStack {
Text("Some stuff to be shown")
.font(.title)
.padding(50)
Button("Dismiss",
action: { isShowingSheet.toggle() })
}
}
}
}
}
}
How would I display a sheet from a menu item?
However, if a sheet is not the way to do it (I think given the simplicity of what I need to show, it would be it), how would you suggest I do it? I tried creating a new view, like I did with the preferences window, but I can't call it either from the menu.
put the sheet directly on ContentView:
#main
struct The_ThingApp: App {
#State private var isShowingSheet = false
var body: some Scene {
WindowGroup {
ContentView()
// here VV
.sheet(isPresented: $isShowingSheet) {
VStack {
Text("Some stuff to be shown")
.font(.title)
.padding(50)
Button("Dismiss",
action: { isShowingSheet.toggle() })
}
}
}
.commands {
CommandMenu("Info") {
Button("Get Info") {
print("getting info")
isShowingSheet.toggle()
}
}
}
}
}

SwiftUI background image moves when keyboard shows

I have an image background, which should stay in place when the keyboard shows, but instead it moves up together with everything on the screen. I saw someone recommend using ignoresSafeArea(.keyboard), and this question Simple SwiftUI Background Image keeps moving when keyboard appears, but neither works for me. Here is my super simplified code sample. Please keep in mind that while the background should remain unchanged, the content itself should still avoid the keyboard as usual.
struct ProfileAbout: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("write something", text: $text)
Spacer()
Button("SomeButton") {}
}
.background(
Image("BackgroundName")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.keyboard)
)
}
}
Here a possible salvation:
import SwiftUI
struct ContentView: View {
#Environment(\.verticalSizeClass) var verticalSizeClass
#State var valueOfTextField: String = String()
var body: some View {
GeometryReader { proxy in
Image("Your Image name here").resizable().scaledToFill().ignoresSafeArea()
ZStack {
if verticalSizeClass == UserInterfaceSizeClass.regular { TextFieldSomeView.ignoresSafeArea(.keyboard) }
else { TextFieldSomeView }
VStack {
Spacer()
Button(action: { print("OK!") }, label: { Text("OK").padding(.horizontal, 80.0).padding(.vertical, 5.0).background(Color.yellow).cornerRadius(5.0) }).padding()
}
}
.position(x: proxy.size.width/2, y: proxy.size.height/2)
}
}
var TextFieldSomeView: some View {
return VStack {
Spacer()
TextField("write something", text: $valueOfTextField).padding(5.0).background(Color.yellow).cornerRadius(5.0).padding()
Spacer()
}
}
}
u can use GeometryReader
get parent View size
import SwiftUI
import Combine
struct KeyboardAdaptive: ViewModifier {
#State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.padding(.bottom, keyboardHeight)
.onReceive(Publishers.keyboardHeight) {
self.keyboardHeight = $0
}
}
}
}
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { $0.keyboardHeight }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
extension View {
func keyboardAdaptive() -> some View {
ModifiedContent(content: self, modifier: KeyboardAdaptive())
}
}
struct ProfileAbout: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("write something", text: $text)
Spacer()
Button("SomeButton") {}
}
.background(
Image("BackgroundName")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.keyboard)
)
.keyboardAdaptive()
}
}

SwiftUI macOS sheet does not dismiss sometimes

I have implemented a sheet to edit the values of a client.
It's normally possible to edit the client and close the sheet after pressing the OK-Button. But if the sheet is open for a longer time it is not possible to dismiss the sheet. Nothing happens and they only way to proceed is to quit the program.
Does anyone have an idea why this happens sometimes?
struct ContentView: View {
#State private var showingEditClient = false
var body: some View {
VStack{
HStack {
Button(action: showEditClientSheet) {
Text("Edit Client")
}
.sheet(isPresented: $showingEditClient) {
EditClientSheet()
}
}
}
.frame(minWidth: 400, minHeight: 400)
}
func showEditClientSheet(){
showingEditClient.toggle()
}
}
struct EditClientSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var name = "Max"
var body: some View {
VStack {
Form {
TextField("Name", text: $name)
}
HStack{
Button(action: cancel) {
Text("Abbrechen")
}
Button(action: editClient) {
Text("Ok")
}
}
}
.frame(minWidth: 200, minHeight: 200)
}
func editClient() {
NSApp.keyWindow?.makeFirstResponder(nil)
//Check if content is correct to save
if name != "" {
//store the changes
self.presentationMode.wrappedValue.dismiss()
}else {
//show Alert
}
}
func cancel() {
self.presentationMode.wrappedValue.dismiss()
}
}

Scroll SwiftUI List to new selection

If you have a SwiftUI List with that allows single selection, you can change the selection by clicking the list (presumably this makes it the key responder) and then using the arrow keys. If that selection reaches the end of the visible area, it will scroll the whole list to keep the selection visible.
However, if the selection object is updated in some other way (e.g. using a button), the list will not be scrolled.
Is there any way to force the list to scroll to the new selection when set programmatically?
Example app:
import SwiftUI
struct ContentView: View {
#State var selection: Int? = 0
func changeSelection(_ by: Int) {
switch self.selection {
case .none:
self.selection = 0
case .some(let sel):
self.selection = max(min(sel + by, 20), 0)
}
}
var body: some View {
HStack {
List((0...20), selection: $selection) {
Text(String($0))
}
VStack {
Button(action: { self.changeSelection(-1) }) {
Text("Move Up")
}
Button(action: { self.changeSelection(1) }) {
Text("Move Down")
}
}
}
}
}
I tried several solutions, one of them I'm using in my project (I need horizontal paging for 3 lists). And here are my observations:
I didn't find any methods to scroll List in SwiftUI, there is no mention about it in documentation yet;
You may try ScrollView (my variant below, here is other solution), but these things might look monstroid;
Maybe the best way is to use UITableView: tutorial from Apple and try scrollToRowAtIndexPath method (like in this answer).
As I wrote, here is my example, which, of course, requires refinement. First of all ScrollView needs to be inside GeometryReader and you can understand the real size of content. The second thing is that you need to control your gestures, which might be difficult. And the last one: you need to calculate current offset of ScrollViews's content and it could be other than in my code (remember, I tried to give you example):
struct ScrollListView: View {
#State var selection: Int?
#State private var offset: CGFloat = 0
#State private var isGestureActive: Bool = false
func changeSelection(_ by: Int) {
switch self.selection {
case .none:
self.selection = 0
case .some(let sel):
self.selection = max(min(sel + by, 30), 0)
}
}
var body: some View {
HStack {
GeometryReader { geometry in
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(0...29, id: \.self) { line in
ListRow(line: line, selection: self.$selection)
.frame(height: 20)
}
}
.content.offset(y: self.isGestureActive ? self.offset : geometry.size.height / 4 - CGFloat((self.selection ?? 0) * 20))
.gesture(DragGesture()
.onChanged({ value in
self.isGestureActive = true
self.offset = value.translation.width + -geometry.size.width * CGFloat(self.selection ?? 1)
})
.onEnded({ value in
DispatchQueue.main.async { self.isGestureActive = false }
}))
}
}
VStack {
Button(action: { self.changeSelection(-1) }) {
Text("Move Up")
}
Spacer()
Button(action: { self.changeSelection(1) }) {
Text("Move Down")
}
}
}
}
}
of course you need to create your own "list row":
struct ListRow: View {
#State var line: Int
#Binding var selection: Int?
var body: some View {
HStack(alignment: .center, spacing: 2){
Image(systemName: line == self.selection ? "checkmark.square" : "square")
.padding(.horizontal, 3)
Text(String(line))
Spacer()
}
.onTapGesture {
self.selection = self.selection == self.line ? nil : self.line
}
}
}
hope it'll be helpful.
In the new relase of SwiftUI for iOs 14 and MacOs Big Sur they added the ability to programmatically scroll to a specific cell using the new ScrollViewReader:
struct ContentView: View {
let colors: [Color] = [.red, .green, .blue]
var body: some View {
ScrollView {
ScrollViewReader { value in
Button("Jump to #8") {
value.scrollTo(8)
}
ForEach(0..<10) { i in
Text("Example \(i)")
.frame(width: 300, height: 300)
.background(colors[i % colors.count])
.id(i)
}
}
}
}
}
Then you can use the method .scrollTo() like this
value.scrollTo(8, anchor: .top)
Credit: www.hackingwithswift.com
I am doing it this way:
1) Reusable copy-paste component:
import SwiftUI
struct TableViewConfigurator: UIViewControllerRepresentable {
var configure: (UITableView) -> Void = { _ in }
func makeUIViewController(context: UIViewControllerRepresentableContext<TableViewConfigurator>) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<TableViewConfigurator>) {
//let tableViews = uiViewController.navigationController?.topViewController?.view.subviews(ofType: UITableView.self) ?? [UITableView]()
let tableViews = UIApplication.nonModalTopViewController()?.navigationController?.topViewController?.view.subviews(ofType: UITableView.self) ?? [UITableView]()
for tableView in tableViews {
self.configure(tableView)
}
}
}
2) Extension on UIApplication to find top view controller in hierarchy
extension UIApplication {
class var activeSceneRootViewController: UIViewController? {
if #available(iOS 13.0, *) {
for scene in UIApplication.shared.connectedScenes {
if scene.activationState == .foregroundActive {
return ((scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate)?.window??.rootViewController
}
}
} else {
// Fallback on earlier versions
return UIApplication.shared.keyWindow?.rootViewController
}
return nil
}
class func nonModalTopViewController(controller: UIViewController? = UIApplication.activeSceneRootViewController) -> UIViewController? {
print(controller ?? "nil")
if let navigationController = controller as? UINavigationController {
return nonModalTopViewController(controller: navigationController.topViewController ?? navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return nonModalTopViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
let top = nonModalTopViewController(controller: presented)
if top == presented { // just modal
return controller
} else {
print("Top:", top ?? "nil")
return top
}
}
if let navigationController = controller?.children.first as? UINavigationController {
return nonModalTopViewController(controller: navigationController.topViewController ?? navigationController.visibleViewController)
}
return controller
}
}
3) Custom part - Here you implement your solution for UITableView behind List like scrolling:
Use it like modifier on any view in List in View
.background(TableViewConfigurator(configure: { tableView in
if self.viewModel.statusChangeMessage != nil {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
let lastIndexPath = IndexPath(row: tableView.numberOfRows(inSection: 0) - 1, section: 0)
tableView.scrollToRow(at: lastIndexPath, at: .bottom, animated: true)
}
}
}))

Resources