Detect active NSSplitViewItem - swift

I have a SplitView with (several) SplitViewItems. Echt SplitViewItem has a ViewController with multiple views in it.
I need to detect which of the SplitViewItems has the focus of the user.
For example: if the user clicks on any control/view (or navigates to it in any other way), the background of the SplitViewItem that contains that view item should change. As I do not know which/how many views will be enclosed in the ViewController in the SplitViewItem, I'd prefer to detect which SplitViewItem is the 'active' one in the SplitViewController.
I have been searching for a solution all day long. I could not find any kind of notification, nor found a way to solve this managing the responder chain.
Can anybody please point me in the right direction? A (swift) code example would be highly appreciated.
Thanks!

It took me quite some research, but I found a working solution. Not the most elegant, but working.
I found the best approach to be to add an event monitor to the SplitViewController.
In viewDidLoad() add the following code:
NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .leftMouseDown, .flagsChanged]) { [unowned self] (theEvent) -> NSEvent? in
let eventLocation = theEvent.locationInWindow
let numberOfSplitViews = self.splitViewItems.count
var activeIndex: Int?
for index in 0..<numberOfSplitViews {
let view = self.splitViewItems[index].viewController.view
let locationInView = view.convert(eventLocation, from: nil)
if ((locationInView.x > 0) && (locationInView.x < view.bounds.maxX) && (locationInView.y > 0) && (locationInView.y < view.bounds.maxY)) {
activeIndex = index
break
}
}
switch theEvent.type {
case .keyDown:
print("key down in pane \(activeIndex)")
self.keyDown(with: theEvent)
case .leftMouseDown, .rightMouseDown:
print("mouse down in pane \(activeIndex)")
self.mouseDown(with: theEvent)
case .flagsChanged:
print("flags changed in pane \(activeIndex)")
self.flagsChanged(with: theEvent)
default:
print("captured some unhandled event in pane \(activeIndex)")
}
return theEvent
}
(you may have to tweak the relevant events to your own liking. Also, you may need to remover the monitor using NSEvent.removeMonitor(_:)).
In addition (out of scope of this question), you might also want to consider making the variable activeIndex an observable class variable (I did this using RxSwift), allowing you to easily react on any change happening inside the 'active pane'.
Any more elegant/simple solutions are welcome!

Related

Swift array bounds error. Carefully constrained index - out of bounds anyway. Why?

Friends
I have some code very similar to the block below.
The line: let item = selectedErrorList[i] is blowing up because of a out of bounds error.
The context is a dialogue with two tabs: One displaying a ScrollView with ~100 elements, the other a ScrollView with about 4 elements. When the user scrolls to the bottom of the big list and then clicks on the tab of the small list which will rerun this code, the error occurs
I am convinced that i is constrained to the indexes of selectedList and when the tab is changed everything should be reinitialised. That would make this a bug in Swift.
Does this ring any bells for anyone? Seen a similar problem?
var selectedErrorList = array of items;
ScrollView(showsIndicators: true){
VStack(spacing: 0) {
ForEach(0..<selectedErrorList.count){ i in
let item = selectedErrorList[i]
// Do something with item
}
}
}
shouldn't it be : ForEach(0..<(selectedErrorList.count - 1)) ?
array index starts a zero , ends at number of indexes minus one.
Solved this.
Needed this to maintain the index:
private func adjustIndex(i: inout Int) -> Int {
// This function is needed to increment the index `i`. Because SwiftUI will not let code into a declaration but will let a function call, and `i++` is not a thing, let alone a function call, in Swift.
let ret = i
i += 1
return ret
}
var i = 1
var selectedErrorList = array of items;
ScrollView(showsIndicators: true){
VStack(spacing: 0) {
ForEach(0..<selectedErrorList.count){ i in
let item = selectedErrorList[i]
// Do something with item
someThingWithItem(item, adjustIndex(i: &i))
}
}
}
Using the idiom that I was resulted in a warning from Swift (I had not noticed due to clutter in the console)
The adjustIndex function is a tacky hack but gets around the fact that statements in a SwiftUI view are tricky. I am new to SwiftUI and I have not figgured those rules yet, but:
ForEach(0..<selectedErrorList.count){ i in
let item = selectedErrorList[i]
i += 1 // Error here
someThingWithItem(item, i)
}
will not work.
As a interesting aside:
ScrollView(showsIndicators: true){
LazyVStack(spacing: 0) {
resulted in elements being added into the output ScrollView out of order when scrolling and switching between tabs.
I did think that SwiftUI did all =the GUI work in one thread. I misunderstand, but that is another issue.

Text label not updating on screen, even though it shows up on `print()`

I'm writing an application which has an NSSplitViewController as the main View-Controller. I have it linked-up so that clicking a button in the menubar will trigger an #IBAction which then calls a function in one of the sub-View-Controllers.
if let board = storyboard {
let imageController = board.instantiateController(withIdentifier: "VC_image_ID") as! VC_image
imageController.viewDidAppear() // necessary or else I'll get an "Unexpectedly found nil" later on
DispatchQueue.global().async{imageController.processImage(path)} // path variable was set earlier in the code, but not shown here
}
Inside the sub-View-Controller (named VC_image), I'm trying to change the stringValue of a label by using the following code:
public func processImage(_ path: String) {
DispatchQueue.main.async {
self.imageText.stringValue = path
print(self.imageText.stringValue)
}
}
Although the imageText.stringValue actually seems to have changed based on the fact that it prints the updated text (through the console), the text in the window never changes. Why is this happening? The reason is probably really obvious to professionals, but I'm still an amateur and can't figure it out. Thanks.

Detecting swipes on all four directions on WatchKit using the Storyboard

I am trying to detect the swipes on the AppleWatch on all four directions. Yet I am not clear how to assign many values to the direction of the WKSwipeGestureRecognizer I inserted in the Storyboard. I tried with:
swiper.direction = [.right , .left , .up , .down]
that was funnily accepted by the compiler, differently of using the bitwise |, but, using this configuration, function:
#IBAction func swipe(_ sender: Any) {
switch swiper.direction {
case WKSwipeGestureRecognizerDirection.right:
print("Swiped right")
case WKSwipeGestureRecognizerDirection.down:
print("Swiped down")
case WKSwipeGestureRecognizerDirection.left:
print("Swiped left")
case WKSwipeGestureRecognizerDirection.up:
print("Swiped up")
default:
break
}
}
is very seldom called and when it is the swiper.direction is invariably .right.
Apple is very cryptic at:
https://developer.apple.com/reference/watchkit/wkswipegesturerecognizer/1650828-direction
by saying:
"The default value of this property is right. You may specify more
than one direction to track swipes in multiple directions with the
same gesture recognizer."
without revealing how.
At any rate, totally ignoring what Apple says, putting four WKSwipeGestureRecognizer in the storyboard each with a different direction and target does the job.
1) Select WKSwipeGestureRecognizer from Utilities box on the right side
2) From the attributes inspectors, check "Delays touches began" and select "Swipe" type that you need
-
3) Ctrl + drag IBAction to your viewController and implement function

Indicate when tableView indexPath is last one

I am implementing showing post part, I want it to do pagination, I got Query part. What I understand is I set limited number of posts when user reached last post that updated in tableView, query fetches more objects by next set limit.
For these, I need something indicates when tableView indexPath is reached at last one or before last one.
I searched some, I think this is good one, but I don't know what does mean.
Get notified when UITableView has finished asking for data?
Could anyone explain me what does mean(how it works) and how to subclass in UITableView's reloadData for this?
fun reloadData() {
print("begin reload")
super.reloadData()
print("end reload")
}
I add my fetching code, I don't think it is working.
var limit = 10
var skip = 0
func fetchAllObjectsFromParse() {
//empty postArray
postsArray = []
//bring data from parse
let query = PFQuery(className: "Posts")
query.limit = limit
query.orderByDescending("createdAt")
query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, error) -> Void in
if error == nil && objects != nil{
for object in objects! {
self.postsArray.append(object)
}
if (objects!.count == self.limit){
let query = PFQuery(className: "Posts")
self.skip += self.limit
query.skip = self.skip
query.limit = self.limit
print(self.limit)
print(self.skip)
query.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if error == nil && objects != nil {
for object in objects! {
self.postsArray.append(object)
print(objects?.count)
}
}
})
dispatch_async(dispatch_get_main_queue(),{
self.tableView.reloadData()
})
}
}else{
print(error?.localizedDescription)
}
}
}
First let's tackle pagination. Now pagination isn't undoable but it does require your server to accept pages so that it knows which set of results to return. For example if you have 100 results in group sizes of twenty, in your url request you'd have to specify if you wanted page 0 - 4.
I would personally recommend using an open-source library to take care of pagination for you. Take a look at MMRecord, it does have some pagination support for you. If you are feeling ambitious and you want to implement it on your own, you'll need to build some networking service, best built on something like AFNetworking that keeps track of the last requested page.
In swift, that would look something like this:
NetworkManager.GET('http://someURI', parameters: [:], page: 0)
Somehow you have to keep the state of which page is next around. A better design, as suggested here, is to use protocols and some sort of datasource wrapper that abstracts the mechanism from the client.
Now you also asked how to detect when a table view scrolls to the end so you can fire off a network request. One way to do it is simple observe scrollViewDidScroll (UITableView inherits from UIScrollView) and check to see if the contentOffset is >= self.tableView.contentSize - self.tableView.frame.size.height (which will be the content offset of when the table is all the way scrolled). However you want the user of your application to be able to keep scrolling without having to scroll to the bottom, waiting for it to load, then continue scrolling so what I would is fire the request when they've scrolled 3/4 down the page or even halfway if you know the request will take a long time. That way by the time they get to the bottom, it'll already have loaded the data and the user will never know the difference.
EDIT:
I would add a few suggestions for you apart from my answer. 1) Take a look at this: http://artsy.github.io/blog/2015/09/24/mvvm-in-swift/
It talks about the MVVM design pattern which is much better for holding state instead of having state variables in your view controller.
2) your function, fetchAllObjectsFromParse is a bad name for a couple of reasons. First, the name implies that the function fetches all the objects at once, which isn't the case if you want it paginated.
Maybe something like this:
func fetchObjectsForModel(model: String, atPage page: Int, withBlock block: ([PFObejct], NSError?) -> Void {
}
And then make another function which will generate your query:
func generateQueryWithSkips(skip: Int, limit: Int) -> PFQuery {}

How to obtain the selected text from the frontmost application in macOS?

I will soon be working on an application which needs to get the currently selected text in the frontmost application window, be it Safari, Pages, TextEdit, Word, etc., and do something with that text.
My goal is to find a solution that works with as much applications as possible. So far I thought about using AppleScript, but that would limit the amount of applications which could be used with my service. At least these common applications must be supported: Safari, Firefox (no AppleScript?), Word, Pages, Excel, TextEdit, ...
I also thought about keeping the clipboard's content in a temporary variable then simulating a text copy operation (Cmd-C), getting the text and then put the original content back in. This would probably highlight the Edit menu item when the copy operation is simulated and seems a bit hacky to me. IMO this solution doesn't seem good enough for a commercial product.
I am also looking to get more than the selection (i.e: the complete contents of the page in Safari or Word, etc.) to add some additional features in the future.
Any ideas/details on how to implement this behavior?
Thanks in advance for any hints!
N.B: I need to support at least 10.4 and up, but ideally older than 10.4 too.
UPDATE:
The solution I've opted for: Using the "Chain of Responsibility" design pattern (GOF) to combine 3 different input methods (Pasteboard, AppleScript and Accessibility), using the best available input source automatically.
Note that when using NSAppleScript's executeAndReturnError: method which returns an NSAppleEventDescriptor (let's say a "descriptor" instance), for the [descriptor stringValue] method to return something, in your AppleScript you must use "return someString" OUTSIDE of a "tell" block else nothing will be returned.
Here's the Swift 5.5 implementation of what is described in the accepted answer.
extension AXUIElement {
static var focusedElement: AXUIElement? {
systemWide.element(for: kAXFocusedUIElementAttribute)
}
var selectedText: String? {
rawValue(for: kAXSelectedTextAttribute) as? String
}
private static var systemWide = AXUIElementCreateSystemWide()
private func element(for attribute: String) -> AXUIElement? {
guard let rawValue = rawValue(for: attribute), CFGetTypeID(rawValue) == AXUIElementGetTypeID() else { return nil }
return (rawValue as! AXUIElement)
}
private func rawValue(for attribute: String) -> AnyObject? {
var rawValue: AnyObject?
let error = AXUIElementCopyAttributeValue(self, attribute as CFString, &rawValue)
return error == .success ? rawValue : nil
}
}
Now, wherever you need to get the selected text from the frontmost application, you can just use AXUIElement.focusedElement?.selectedText.
As mentioned in the answer, this is not 100% reliable. So we're also implementing the other answer which simulates Command + C and copies from the clipboard. Also, ensure to remove the new item from the Clipboard if not required.
If you don't need selected text very frequently, you can programmatically press Command+C, then get the selected text from clipboard. But during my test, this is only works if you turn off App Sandbox (can't submit to Mac App Store).
Here is the Swift 3 code:
func performGlobalCopyShortcut() {
func keyEvents(forPressAndReleaseVirtualKey virtualKey: Int) -> [CGEvent] {
let eventSource = CGEventSource(stateID: .hidSystemState)
return [
CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(virtualKey), keyDown: true)!,
CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(virtualKey), keyDown: false)!,
]
}
let tapLocation = CGEventTapLocation.cghidEventTap
let events = keyEvents(forPressAndReleaseVirtualKey: kVK_ANSI_C)
events.forEach {
$0.flags = .maskCommand
$0.post(tap: tapLocation)
}
}
performGlobalCopyShortcut()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { // wait 0.05s for copy.
let clipboardText = NSPasteboard.general().readObjects(forClasses: [NSString.self], options: nil)?.first as? String ?? ""
print(clipboardText)
}
Accessibility will work, but only if access for assistive devices is on.
You'll need to get the current application, then get its focused UI element, then get its selected text ranges and its value (whole text) and selected text ranges. You could just get its selected text, but that would either concatenate or ignore multiple selections.
Be prepared for any of those steps to fail: The app may not have any windows up, there may be no UI element with focus, the focused UI element may have no text, and the focused UI element may have only an empty selected text range.