How to use #FocusedBinding - swiftui

I've tried, without success, to use the new property wrapper #FocusedBinding.
The code example given here by a Frameworks Engineer, and placed below, during beta 1 period for iOS 14 and Big Sur compiles, but it doesn't seem to work for both OSs, for enabling the keyboard shortcuts.
Does anyone knows if something changed in the meantime, and how, or is something still under development?
// This example runs on macOS, iOS, and iPadOS.
//
// Big Sur Seed 1 has some known issues that prevent state-sharing between
// windows and the main menu, so this example doesn't currently behave as
// expected on macOS. Additionally, the Commands API is disabled on iOS in Seed
// 1. These issues will be addressed in future seeds.
//
// The Focused Value API is available on all platforms. The Commands and
// Keyboard Shortcut APIs are available on macOS, iOS, iPadOS, and
// tvOS—everywhere keyboard input is accepted.
#main
struct MessageApp : App {
var body: some Scene {
WindowGroup {
MessageView()
}
.commands {
MessageCommands()
}
}
}
struct MessageCommands : Commands {
// Try to observe a binding to the key window's `Message` model.
//
// In order for this to work, a view in the key window's focused view
// hierarchy (often the root view) needs to publish a binding using the
// `View.focusedValue(_:_:)` view modifier and the same `\.message` key
// path (anologous to a key path for an `Environment` value, defined
// below).
#FocusedBinding(\.message) var message: Message?
// FocusedBinding is a binding-specific convenience to provide direct
// access to a wrapped value.
//
// `FocusedValue` is a more general form of the property wrapper, designed
// to work with all value types, including bindings. The following is
// functionally equivalent, but places the burden of unwrapping the bound
// value on the client.
// #FocusedValue(\.message) var message: Binding<Message>?
var body: some Commands {
CommandMenu("Message") {
Button("Send", action: { message?.send() })
.keyboardShortcut("D") // Shift-Command-D
.disabled(message?.text.isEmpty ?? true)
}
}
}
struct MessageView : View {
#State var message = Message(text: "Hello, SwiftUI!")
var body: some View {
TextEditor(text: $message.text)
.focusedValue(\.message, $message)
.frame(idealWidth: 600, idealHeight: 400)
}
}
struct Message {
var text: String
// ...
mutating func send() {
print("Sending message: \(text)")
// ...
}
}
struct FocusedMessageKey : FocusedValueKey {
typealias Value = Binding<Message>
}
extension FocusedValues {
var message: FocusedMessageKey.Value? {
get { self[FocusedMessageKey.self] }
set { self[FocusedMessageKey.self] = newValue }
}
}

Related

How to jump back "n" seconds from current position in video using the YouTubePlayerKit Package in SwiftUI

I am very new to programming in SwiftUI (Currently running Xcode 14.2). I am using the YouTubePlayerKit 1.3.0 Package (https://swiftpackageindex.com/SvenTiigi/YouTubePlayerKit) to load a YouTube video and play it. One of the abilities I want to build into the app is to jump back "n" seconds in the video when a button is pushed and start playing the video from that point. There is a function called: getCurrentTime() that I believe returns an integer representing the elapsed time in seconds from the beginning of the video. I want to subtract "n" seconds from that value and use the: .seek(to: , allowSeekAhead: true) function to jump to the desired location of the video based on the above calculation. I have the code to a point where I can load the video and when I click the "SEEK" button it will jump to a static value I have hard coded into the script and play from that point. I am struggling with how to retrieve the current time, subtract "n" seconds and use that value in the .seek() function.
This is a link to information on the YouTubePlayerKit Package: https://github.com/SvenTiigi/YouTubePlayerKit
Any help would be greatly appreciated.
Below is the SwiftUI script I have so far. This is the description of the function I believe
will give me the time in seconds up to the current point in the video:
/*
/// Retrieve the elapsed time in seconds since the video started playing
/// - Parameter completion: The completion closure
func getCurrentTime(
completion: #escaping (Result<Double, YouTubePlayerAPIError>) -> Void
)
*/
import SwiftUI
import YouTubePlayerKit
struct ContentView: View {
let youTubePlayer = YouTubePlayer(
source: .url("https://www.youtube.com/watch?v=qL-ry_tz6V0"),
configuration: .init(
autoPlay: true
)
)
#State private var JumpTo: Double = 27
var body: some View {
ZStack {
YouTubePlayerView(self.youTubePlayer) { state in
// Overlay ViewBuilder closure to place an overlay View
// for the current `YouTubePlayer.State`
switch state {
case .idle:
ProgressView()
case .ready:
EmptyView()
case .error(let error):
Text(verbatim: "YouTube player couldn't be loaded")
}
}
HStack {
Button("PLAY") {
//Play video
youTubePlayer.play()
}
Button("PAUSE") {
// Pause video
youTubePlayer.pause()
}
Button("STOP") {
// Stop video
youTubePlayer.stop()
}
Button("SEEK") {
youTubePlayer.seek(to: JumpTo, allowSeekAhead: true)
}
}
.offset(y: 250)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
According to your comments the culprit seems to be:
Button("SEEK") {
JumpTo = youTubePlayer.getCurrentTime() // this line gives the error asnyc call and try without catch
youTubePlayer.seek(to: JumpTo, allowSeekAhead: true)
}
You are in an async context here. Swift async / concurrency is a very broad field that cannot be answered in a single answer. I would recommend you put some effort into reading this stuff up.
The reason for the 2 errors are:
youTubePlayer.getCurrentTime() is a throwing function. You need to catch any possible errors and either do something with them or dismiss them. Either way the Swift compiler demands from you a certain syntax. It is called do/catch clause.
do{
try .... //throwing function
} catch{
// handle error here or just print it
print(error)
}
Your function is asyncronous. It means the code you write will proceed without waiting for the result of the function. To handle this you need to await it. But as your Button("SEEK") { method does not support something like that you have to wrap it into a Task:
Task{
await ... // await async function
}
If you combine both aproaches you get....
Button("SEEK") {
Task{
do{
JumpTo = try await youTubePlayer.getCurrentTime()
// now you can manipulate it and do
Jumpto = .....
youTubePlayer.seek(to: JumpTo, allowSeekAhead: true)
} catch{
// handle error here or just print it
print(error)
}
}
}

crownSequencer equivalent in SwiftUI

I've historically used the WKInterfaceController's property called "crownSequencer" to listen to rotations in the crown (see code below). I can grab the "rotationalValue" of the crown's rotation with the function 'crownDidRotate' as shown below.
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
override func awake(withContext context: Any?) {
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
}
override func didAppear() {
super.didAppear()
crownSequencer.delegate = self
crownSequencer.focus()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
}
}
extension InterfaceController : WKCrownDelegate {
func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
print(rotationalDelta)
}
}
In SwiftUI, I can instead use the following code to listen to the crown within a View:
import SwiftUI
import SpriteKit
struct GameView: View {
#State var scrollAmount: Float = 0.0
// ...other code to initialise variables used
var body: some View {
SpriteView(scene: skScene!)
.focusable(true)
.digitalCrownRotation($scrollAmount, from: -70, through: 70, by: 0.1, sensitivity: .high, isContinuous: false, isHapticFeedbackEnabled: false)
.onChange(of: scrollAmount) { newValue in
//function )
}
}
This code listens to rotations and binds the "scrollAmount" to a state variable.
However, I can't seem to grab an equivalent of the "rotationalDelta" from the original crownSequencer methodology. Is there a way to grab a "rotationalDelta" in SwiftUI (i.e, grab the change in rotation as opposed to the scrollAmount, which reflects the current level of rotation).

Document based app shows 2 back chevrons on iPad

I did a small sample application to show my problem. I used the multi-platform document app template that Xcode 14.0.1 offers, creating my own package file format for this.
I want to create a document based app running on macOS and on iPad.
When running on macOS, everything works as expected.
On the iPad, when opening the app, the file chooser opens.
On opening an existing or creating a new file, the screen looks like this:
The left chevron does nothing, while the right chevron shows the document chooser again.
What's the left, ever so slightly larger chevron on the left doing here and how can I get of it? Is this an error with the framework that should be reported to Apple?
PS don't get distracted by the name of this sample app–the real app will need some navigation and I first thought the 2nd chevron show up cause of this–in the sample I built for this post, there is no navigation though. So this 2nd chevron seems to be a "built in" issue...
For the sake of completeness, here's my code:
import SwiftUI
#main
struct so_DocumentAppWithNavigationShowsMultipleChevronsApp: App {
var body: some Scene {
DocumentGroup(newDocument: so_DocumentAppWithNavigationShowsMultipleChevronsDocument()) { file in
ContentView(document: file.$document)
}
}
}
import UniformTypeIdentifiers
extension UTType {
static var appfroschFile: UTType {
UTType(importedAs: "ch.appfros.so-DocumentAppWithNavigationShowsMultipleChevrons")
}
}
struct so_DocumentAppWithNavigationShowsMultipleChevronsDocument: FileDocument {
var document: Document
init(document: Document = Document(text: "Hello, world!")) {
self.document = document
}
static var readableContentTypes: [UTType] { [.appfroschFile] }
init(configuration: ReadConfiguration) throws {
guard let fileWrappers = configuration.file.fileWrappers
else {
throw CocoaError(.fileReadCorruptFile)
}
guard let documentFileWrapper = fileWrappers["document"],
let data = documentFileWrapper.regularFileContents,
let string = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
document = try JSONDecoder().decode(Document.self, from: data)
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = try JSONEncoder().encode(document)
let documentFileWrapper = FileWrapper(regularFileWithContents: data)
let mainFileWrapper = FileWrapper(directoryWithFileWrappers: [
"document": documentFileWrapper
])
return mainFileWrapper
}
}
struct Document: Codable {
var text: String
}
struct ContentView: View {
#Binding var document: so_DocumentAppWithNavigationShowsMultipleChevronsDocument
var body: some View {
TextEditor(text: $document.document.text)
}
}
Can see the same problem with the default Document based app when using Xcode 14.1 (14B47) running on the iPad simulator with iOS 16.1. So definitely a bug (and worth reporting to A as such).
At a guess, the second, non-functional back button is what would have been the back button for navigating in SideBar. And the logic to not display when working on a document is what has been broken.
Fortunately simple workaround for the bug is to explicitly specify toolbar's role using the toolbarRole modifier, e.g.
#main
struct so_DocumentAppWithNavigationShowsMultipleChevronsApp: App {
var body: some Scene {
DocumentGroup(newDocument: so_DocumentAppWithNavigationShowsMultipleChevronsDocument()) { file in
ContentView(document: file.$document)
.toolbarRole(.navigationStack) // <-- Specifying this gets rid of double chevron on iOS
}
}
}

SwiftUI - ReferenceFileDocument - Inability to indicate a document needs saving

When creating a class conforming to ReferenceFileDocument, how do you indicate the document needs saving. i.e. the equivalent of the NSDocument's updateChangeCount method?
I've met the same problem that the SwiftUI ReferenceFileDocument cannot trigger the update. Recently, I've received feedback via the bug report and been suggested to register an undo.
Turns out the update of ReferenceFileDocument can be triggered, just like UIDocument, by registering an undo action. The difference is that the DocumentGroup explicitly implicitly setup the UndoManager via the environment.
For example,
#main
struct RefDocApp: App {
var body: some Scene {
DocumentGroup(newDocument: {
RefDocDocument()
}) { file in
ContentView(document: file.document)
}
}
}
struct ContentView: View {
#Environment(\.undoManager) var undoManager
#ObservedObject var document: RefDocDocument
var body: some View {
TextEditor(text: Binding(get: {
document.text
}, set: {
document.text = $0
undoManager?.registerUndo(withTarget: document, handler: {
print($0, "undo")
})
}))
}
}
I assume at this stage, the FileDocument is actually, on iOS side, a wrapper on top of the UIDocument, the DocumentGroup scene explicitly implicitly assign the undoManager to the environment. Therefore, the update mechanism is the same.
The ReferenceFileDocument is ObservableObject, so you can add any trackable or published property for that purpose. Here is a demo of possible approach.
import UniformTypeIdentifiers
class MyTextDocument: ReferenceFileDocument {
static var readableContentTypes: [UTType] { [UTType.plainText] }
func snapshot(contentType: UTType) throws -> String {
defer {
self.modified = false
}
return self.storage
}
#Published var modified = false
#Published var storage: String = "" {
didSet {
self.modified = true
}
}
}
ReferenceFileDocument exists for fine grained controll over the document. In comparison, a FileDocument has to obey value semantics which makes it very easy for SwiftUI to implement the undo / redo functionality as it only needs to make a copy before each mutation of the document.
As per the documentation of the related DocumentGroup initializers, the undo functionality is not provided automatically. The DocumentGroup will inject an instance of an UndoManger into the environment which we can make use of.
However an undo manager is not the only way to update the state of the document. Per this documentation AppKit and UIKit both have the updateChangeCount method on their native implementation of the UI/NSDocument object. We can reach this method by grabbing the shared document controller on macOS from within the view and finding our document. Unfortunately I don't have a simple solution for the iOS side. There is a private SwiftUI.DocumentHostingController type which holds a reference to our document, but that would require mirroring into the private type to obtain the reference to the native document, which isn't safe.
Here is a full example:
import SwiftUI
import UniformTypeIdentifiers
// DOCUMENT EXAMPLE
extension UTType {
static var exampleText: UTType {
UTType(importedAs: "com.example.plain-text")
}
}
final class MyDocument: ReferenceFileDocument {
// We add `Published` for automatic SwiftUI updates as
// `ReferenceFileDocument` refines `ObservableObject`.
#Published
var number: Int
static var readableContentTypes: [UTType] { [.exampleText] }
init(number: Int = 42) {
self.number = number
}
init(configuration: ReadConfiguration) throws {
guard
let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8),
let number = Int(string)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.number = number
}
func snapshot(contentType: UTType) throws -> String {
"\(number)"
}
func fileWrapper(
snapshot: String,
configuration: WriteConfiguration
) throws -> FileWrapper {
// For the sake of the example this force unwrapping is considered as safe.
let data = snapshot.data(using: .utf8)!
return FileWrapper(regularFileWithContents: data)
}
}
// APP EXAMPLE FOR MACOS
#main
struct MyApp: App {
var body: some Scene {
DocumentGroup.init(
newDocument: {
MyDocument()
},
editor: { file in
ContentView(document: file.document)
.frame(width: 400, height: 400)
}
)
}
}
struct ContentView: View {
#Environment(\.undoManager)
var _undoManager: UndoManager?
#ObservedObject
var document: MyDocument
var body: some View {
VStack {
Text(String("\(document.number)"))
Button("randomize") {
if let undoManager = _undoManager {
let currentNumber = document.number
undoManager.registerUndo(withTarget: document) { document in
document.number = currentNumber
}
}
document.number = Int.random(in: 0 ... 100)
}
Button("randomize without undo") {
document.number = Int.random(in: 0 ... 100)
// Let the system know that we edited the document, which will
// eventually trigger the auto saving process.
//
// There is no simple way to mimic this on `iOS` or `iPadOS`.
let controller = NSDocumentController.shared
if let document = controller.currentDocument {
// On `iOS / iPadOS` change the argument to `.done`.
document.updateChangeCount(.changeDone)
}
}
}
}
}
Unfortunatelly SwiftUI (v2 at this moment) does not provide a native way to mimic the same functionality, but this workaround is still doable and fairly consice.
Here is a gist where I extended the example with a custom DocumentReader view and a DocumentProxy which can be extended for common document related operations for more convenience: https://gist.github.com/DevAndArtist/eb7e8aa5e7134610c20b1a7aca358604

Dismiss a .sheet in SwiftUI after an async process has completed?

I'm trying to dismiss a .sheet in SwiftUI, after calling an async process to confirm the user's MFA code. (I'm using the AWS Amplify Framework).
I have a binding variable set on the main view, and reference it in the view the sheet presents with #Binding var displayMFAView: Bool. I have an authentication helper that tracks the user state: #EnvironmentObject var userAuthHelper: UserAuthHelper.
The following code dismisses the sheet as expected:
func confirmMFACode(verificationCode: String) {
// Code to confifm MFA...
print("User confirmed MFA")
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
}
However, if I call the auth process via Amplify's confirmSignIn method,
func confirmVerificationMFA(verificationCode: String) {
AWSMobileClient.default().confirmSignIn(challengeResponse: verificationCode) { (signInResult, error) in
if let error = error as? AWSMobileClientError {
// ... error handling ...
} else if let signInResult = signInResult {
switch (signInResult.signInState) {
case .signedIn:
print("User confirmed MFA")
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
default:
print("\(signInResult.signInState.rawValue)")
}
}
}
}
the sheet does not get dismissed. I have tried wrapping the variable assignment in DispatchQueue.main.async {..., but that hasn't solved the issue either.
...
DispatchQueue.main.async {
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
}
...
In fact, this throws the following into my logs:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
Wrapping the switch (... in a DispatchQueue per https://stackoverflow.com/a/58288437/217101 gave me the same warning in my log.
Admittedly I don't have a firm grasp on SwiftUI or AWS Amplify. What am I not understanding?
From what I can tell the async call does something unexpected with the state variables, but not with an EnvironmentObject. So, nstead of #Binding var displayMFAView: Bool, I stored displayMFAView in an EnvironmentObject,
#EnvironmentObject var settings: UserSettings
#State var mfaCode: String = ""
and then can show or hide the .sheet(... by updating a boolean in that object:
Button(action: {
self.signIn() // Async call happens here
self.settings.displayMFAView.toggle()
}) {
Text("Sign In")
}.sheet(isPresented: self.$settings.displayMFAView) {
// Example code to capture text
TextField("Enter your MFA code", text: self.$mfaCode)
}
Button(action: {
self.verifyMFACode(verificationCode: self.mfaCode) // async call
}) {
Text("Confirm")
}
In func verifyMFACode(), I can make an async call to validate my user, then toggle the sheet to disappear on success:
func verifyMFACode(verificationCode: String) {
AWSMobileClient.default().confirmSignIn(challengeResponse: verificationCode) {
...
case .signedIn:
self.settings.displayMFAView.toggle()
...