Using undo manager with mac catalyst - ios

I'm working on adding a mac build target to an iOS application. I have the basics working and would like to implement undo/redo functionality.
In a traditional AppKit application you get this for free. When you create a new application, the prepopulated main menu has the Edit item and Undo and Redo under it. The view controller has an undoManager, you just registerUndo on it (preferably setActionName as well) and everything works. Hotkeys, menu item title changes and state changes (disable redo when at the top of the stack etc.) all work out of the box.
Adding a catalyst build target to an iOS project also creates a default menu with a top level Edit menu and Undo/Redo menu items. These do not seem to adopt the built-in functionality. Do I really need to manually recreate all that is free with AppKit or is there something I'm missing?

Are you calling becomeFirstResponder() on the view that you're registering undo events? This was tripping me up for awhile.
I wasn't able to get action names to show up in the Catalyst Edit menu, but I did see the Undo/Redo stack working properly from UIKit code without having to dip into AppKit.

Registering undo on NSWindow.undoManager works for me. But I had to use a hidden/private API to access the NSWindow instance using Dynamic library:
let nsWindow = Dynamic.NSApplication.sharedApplication.delegate.hostWindowForUIWindow(view.window)
let undoManager: UndoManager? = nsWindow.undoManager

I was able to get this working in my Catalyst app only by calling becomeFirstResponder() in viewDidAppear and no sooner. I was originally calling it in viewDidLoad and also tried viewWillAppear, neither of which correctly register my undo manager with macOS.
Relevant view controller sample code:
class MyViewController: UIViewController {
override var undoManager: UndoManager? {
return myCustomUndoManager
}
override var canBecomeFirstResponder: Bool {
true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
becomeFirstResponder()
}
}

Related

EA showBluetoothAccessoryPicker not showing in SwiftUI

I have created a small program that needs to connect to an external accessory. I have been able to do so successfully with UIKit and the EA framework.
The problem I am having is that I have a SwiftUI based app that needs to use the External Accessory but when I call the showBluetoothAccessoryPicker function, the picker window does not show.
What I have tried so far:
I have read that you need to have the app delegate with
var window: UIWin
setup correctly to see the picker window. So I created a class
class AppDelegate: UIApplicationDelegate
{
var window: UIWindow?
}
I then associated the app delegate with my swiftui "App" struct like this:
#main
struct POC: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
TabSelectionView()
}
}
}
Nevertheless, when I call showBluetoothAccessoryPicker the picker window does not show.
I see the following in the output log:
2021-09-20 13:16:31.804796-0500 POC[2150:1514447] IAPDHasLaunched: kIAPAvailableNotification iapdAvailableState 0 -> 0
2021-09-20 13:16:31.805791-0500 POC[2150:1514447] IAP2DHasLaunched: kIAP2AvailableNotification iap2dAvailableState 0 -> 0
2021-09-20 13:16:31.807219-0500 POC[2150:1514447] -[EAAccessoryManager _initFromSingletonCreationMethod] isRunningOnMac
This log appears correct and matches the output from a working demo app that uses UIKit. It's just that the picker window does not show.
Does anybody know what needs to be done to get the picker window to appear correctly for SwiftUI apps? Is there any SwiftUI examples of connecting to an External Accessory that anybody could point me towards?
Thanks in advance for any help.
UPDATE: It's also broken in UIKit! Feedback reported as FB9856371.
Apparently the new UIScene-based mechanism kills it (and since SwiftUI is based on the new scene-based mechanism, it only looks like it's a problem with SwiftUI).
If you roll back to the UIApplicationDelegate-based lifecycle, it works again.
OLD ANSWER:
It's just broken in SwiftUI right now.
Further experimentation shows that the EAExternalAccessory picker seems to query the UIApplicationDelegate's window property to find out where to display itself in.
Alas, that being an optional property, which is not implemented in the SwiftUI's version of the AppDelegate, it only gets nil and does not show up.
It also seems that the #UIApplicationDelegateAdaptor approach does not work in this case, because it does not really act as a stand-in for the real app delegate, but rather gets the methods forwarded.
A way to work around this is to use the UIKit application lifecycle and present your SwiftUI app via the UIHostingController.
It might also be possible to hack this via method swizzling, but if so, I don't know how.
Please open a bug report.

What is the reset(_:) method

I am learning iOS app development, and I came across a line which confused me:
Also add a call from reset(_:) so it works when you reset the app
But I can't find a reset(_:) function anywhere, both in ViewController and AppDelegate. Do I have to create the function, or is it something different?
So let's add the additional infos:
Your quote is from Intro to App Development with Swift, Apple's iBook., chapter 17.6 Polishing the Interface, in Disabling sliders subpart.
You missed the previous part:
#IBAction func reset(_ sender: AnyObject) {​} Open the Connections inspector. You’ll see that the button has been
connected to the Touch Up Inside event. This is the standard event
used ​for most buttons. Your reset button will set the value of each
slider to 1 and the isOn property of each switch to false. Add that
code to the new action method.
So your quote was talking about that method that you should have added previously on ViewController.swift in the chapter 17.5 Reset Button.

Buttons in iOS app don't respond to touches around the edge of the screen

I'm relatively new to iOS development, but I'm having a go at working on some open source code for an old game that used to be pretty popular (Eden World Builder)
I've made quite a lot of progress in cleaning up the codebase, making small changes. But there's an issue I can't seem to fix. Every button in the game will not respond to taps around the edges of the screen. If a button is in the corner of the screen, you will have to tap towards the bottom of the button.
I've tried to move the buttons away from the edges, and they work, but that isn't practical for use. So there's something preventing the edges of the screen from registering button taps for some reason, and it doesn't seem to be anything to do with the button target areas themselves.
One thing I've noticed: This game is currently on the App Store, even though it hasn't been updated since 2015. In the App Store version (Which is built from the same code that I have) the issue doesn't occur. It must be something to do with building it in a newer version of Xcode, right?
Any assistance would be very helpful, this has been frustrating me for weeks now. Thanks
The answer to your first question is most likely either that your button is outside its parent view, or a gesture recognizer is interfering.
If a button extends beyond its parent views boundaries (or any of its higher parents' boundaries), it will still be visible as long as the parent doesn't have clipping enabled. The result is that you will still see the button, but it will only respond when touching the parts that are inside the parent view. You can find this visually by using Xcode View UI Hierarchy found in the Debug Navigator.
If it is gesture recognizers that interfere with your button, there are several solutions that might work. Several are described in the link you got from #Anbu in the comment.
The answer to your second question is that old apps are linked against old frameworks. Even if they run on the latest iOS version, they still pull in older versions of the framework, causing them to (mostly) continue work as before. This is done to keep compatibility with legacy code.
Try adding this to viewDidAppear
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let window = view.window,
let recognizers = window.gestureRecognizers {
recognizers.forEach { r in
r.delaysTouchesBegan = false
r.cancelsTouchesInView = false
r.isEnabled = false
}
}
}

I'm getting a lock alert when trying to connect/add elements to the viewcontroller in Xcode 7.3

I have a ViewController that all of the sudden will not allow me to add a new element or connect an existing element to an IBAction. I've looked through sample code as well and googled the s*%& out of it without finding a solution.
My ViewController.swift file contains this code:
#IBAction func helpButtonTapped(sender: AnyObject) {
}
And I'm trying to connect a very basic button.
Also, note that I've double-checked to be sure that all elements and views are set to Lock: Nothing.
Any help would be amazing!
Go to menu Editor then Localization Locking then choose Nothing.
You probably have enabled one of the other locking options accidentally.

Xcode UI Test example

I have just recently learned about Unit Testing in Xcode. Now I am trying out Xcode 7 and I see there is a new group for UI Tests when I create a new project.
I watched the WWDC 2015 video and it was pretty good, but do you have a super simple example that I could go through myself? The video examples were a little too complex for me.
Notes
The answer below is my attempt to figure this out, but I welcome any better answers.
I have read these SO questions about UI Testing in Xcode but they are different: docs, reloading, App vs UI, ViewController, multiple, values and properties, pre XCode 7 projects.
Use Unit Tests to test the validity of methods in your classes. You use them to test the code you have written. (See my other example for setting up a simple Unit Test in Xcode.)
Use UI Tests to check the validity of the User Interface. Think of it like having your own robot to go through and do all the normal interactions with your app that a normal user would. This saves you the time of doing it yourself.
At the time of this writing, it is difficult to access many of the properties of the UI components, but just having a test go through tapping them and swiping them confirms that they are there.
Example
This is about the simplest setup and UI test that I could think of: a button that when pressed changes the text of a label.
Set up
Create a new project in Xcode 7+ for iOS 9.0+.
Make sure that Include UI Tests is checked
If you are adding UI tests to a project created before Xcode 7, see this answer. (File > New > Target > Test > Cocoa Touch UI Testing Bundle)
Add a UILabel and a UIButton to the storyboard
Create an #IBOutlet and #IBAction in the ViewController and make the label text change when the button is pressed.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
#IBAction func button(sender: AnyObject) {
label.text = "Hello"
}
}
Do the test
Open the YourProjectUITests file.
Put your curser in the testExample() method. (You can delete the comments)
Press the red Record button
In the app, (1) tap the label, (2) tap the button, and then (3) tap the label again. (4) Press the Record button again to stop recording. The following code should have been automatically generated for you:
func testExample() {
let app = XCUIApplication()
app.staticTexts["Label"].tap()
app.buttons["Button"].tap()
app.staticTexts["Hello"].tap()
}
Use the staticText lines as a starting point for making an XCTAssert. Now you should have:
func testExample() {
let app = XCUIApplication()
XCTAssert(app.staticTexts["Label"].exists)
app.buttons["Button"].tap()
XCTAssert(app.staticTexts["Hello"].exists)
}
Press the diamond on the left to run the UI Test. It should turn green when it passes.
That's it! This showed that the UIButton and UILabel exist and that the text of the label changed. If you want to see it fail (a good idea), you can change "Hello" to something else.
Further study
UI Testing in Xcode
Exploring the New UI Testing Features of Xcode 7
Xcode 7 UI testing, a first look
UI Testing in Xcode 7
#Suragch +1 for the answer. One thing I observed and want to share that every function inside the UI Test case must start with "test". You can append extra name after that. Only this way the button(for clicking to start the test) appears.

Resources