How to ensure only one document in document group application - swiftui

I am using DocumentGroup to handle documents for me. Unfortunately its behavior is very different on BigSur I have to support when multiple documents are opened. Is there a way to ensure that pervious documents closes before opening a new one? The code snippet is below:
struct DocumentScene: Scene, Equatable {
var body: some Scene {
DocumentGroup(newDocument: V_WorkflowDocument()) {
file in
GeometryReader{
geometry in
ContentView(document: file.$document)
}
}
.commands {
CommandMenu("Run") {
RunCommand()
}
}
}

Related

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 run function every time app is opened

I have a very troubling problem. I have searched for days on how to solve it. I have some code that I want to run every time the app is opened, not just when the app is launched for the first time. I've basically tried everything available. I've tried scenePhase, I've tried AppDelegate, I've tried onAppear, I've tried init and custom extensions to the View class, I've even tried simply running a function in the view, but nothing is working. I'll show my code here.
#main
struct CouponDeckApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
AppContentView()
}
}
}
struct AppContentView: View {
init() {
let userDefaults = UserDefaults.standard
if userDefaults.value(forKey: "hour") == nil { // 1
userDefaults.set(9, forKey: "hour") // 2
}
// 3
if userDefaults.value(forKey: "minute") == nil {
userDefaults.set(30, forKey: "minute")
}
}
#State var currentview: String = "Main"
var body: some View {
Group {
switch currentview {
case "Main":
MainView(currentview: $currentview)
case "Form":
FormView(currentview: $currentview)
case "Settings":
SettingsView(currentview: $currentview)
default:
if currentview.contains("Coupon") {
CouponView(currentview: $currentview)
}
else {
EditView(currentview: $currentview)
}
}
}
}
}
//MainView(), CouponView(), FormView(), etc.
I'm starting to suspect that the problem is with the switch statement in AppContentView that allows you to move between the different views.
Does anyone know:
A. Why this is happening,
B. How to fix it, or
C. Another alternative?
Thanks in advance!
P.S. I'm running my code on the simulator.
Here is a very simple way, using native scenePhase, I did not make it more complicated. You can use Preference method as well for better result! But onChange is good enough for this example:
struct ContentView: View {
#Environment(\.scenePhase) var scenePhase
var body: some View {
Text("Welcome to my App!")
.onAppear() { customFunction() }
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
customFunction()
}
}
}
}
func customFunction() {
print("App is opened!")
}
The simple problem was that it doesn't work when you close out of the app. I realized if you just exit the app but don't completely close out of it, it works just fine.
I also learned about the NotificationCenter's applications to this. By triggering a response when UIApplication sends out the willEnterForegroundNotification by using the onReceive method, you can trigger a response that way.
Do it in your AppDelegate's application(_:didFinishLaunchingWithOptions:).

How to use #FocusedBinding

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 }
}
}

How to Receive NotificationCenter Post in SwiftUI

I've seen a dozen or so tutorials on how to use Combine and receive a Notification of a task being completed. It seems they all show linear code - the publisher and receiver all in the same place, one row after another.
Publishing a notification is as easy as the code below:
// background download task complete - notify the appropriate views
DispatchQueue.main.async {
NotificationCenter.default.post(name: .dataDownloadComplete, object: self, userInfo: self.dataCounts)
}
extension Notification.Name {
static let dataDownloadComplete = Notification.Name("dataDownloadComplete")
}
SwiftUI has the onReceive() modifier, but I can't find any way to connect the above to a "listener" of the posted notification.
How does a View receive this Notification
FYI, after several days of reading and putting together the confusing-to-me Combine tutorials, I discovered these two methods of receiving the notification. For ease, they are included in the same View. (Some not-related details have been omitted.)
In my case, a fetch (performed on a background thread) is batch loading info into Core Data for several entities. The View was not being updated after the fetch completed.
// in the background fetch download class
...
var dataCounts: [DataSources.Source : Int] = [:]
...
// as each source of data has been imported
self.dataCounts[source] = numberArray.count
...
// in a view
import Combine
struct FilteredPurchasesView: View {
private var downloadCompletePublisher: AnyPublisher<Notification, Never> {
NotificationCenter.default
.publisher(for: .dataDownloadComplete)
.eraseToAnyPublisher()
}
private var publisher = NotificationCenter.default
.publisher(for: .dataDownloadComplete)
.map { notification in
return notification.userInfo as! [DataSources.Source : Int]
}
.receive(on: RunLoop.main)
var body: some View {
List {
ForEach(numbers.indices, id: \.self) { i in
NavigationLink(destination: NumberDetailView(number: numbers[i])) {
NumberRowView(number: numbers[i])
}
.id(i)
}
}
.add(SearchBar(searchText: $numberState.searchText))
.onReceive(downloadCompletePublisher) { notification in
print("dataDownload complete (FilteredPurchasesView 1)")
if let info = notification.userInfo as? [DataSources.Source:Int], let purchaseCount = info[DataSources.Source.purchased] {
if purchaseCount > 0 {
// now the view can be updated/redrawn
} else {
print("purchase update count = 0")
}
}
}
.onReceive(publisher) { notification in
print("dataDownload complete (FilteredPurchasesView 2)")
}
}
}
Some notes about this:
During the first couple of attempts, the FilteredPurchasesView had not yet been initialized. This meant there was no Subscriber to listen for the posted notification.
Both of the Publisher vars are available. As of this writing, I cannot explain why or how they work.
Both onReceive() modifiers contain the notification.
Comments, ideas and feedback welcome.

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