I'm building a document-based app with SwiftUI on MacOS Big Sur. I want to prevent users from saving the files as it is intended only as a viewer. How can I do this in SwiftUI?
I'm forced to implement the writing method due to the protocol FileDocument
import SwiftUI
struct MyDoc: FileDocument {
static var readableContentTypes: [UTType]
init(configuration: ReadConfiguration) throws {
<#code#> // reading
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
<#code#> // writing
}
}
My app looks something like this
import SwiftUI
#main
struct MyDocApp: App {
var body: some Scene {
DocumentGroup(viewing: MyDoc.self) { file in
ContentView(document: file.$document)
}
}
}
I've already selected 'Read Only' in 'Signing and Capabilities'
I'm not sure how else to prevent this in SwiftUI.
Cmd+S triggers the save behaviour and the menu options are still included.
Here's a sample project demonstrating the issue - Sample Project
I guess one option is to throw an error in the write method and handle it telling the user they aren't allowed to save files. But this seems a bit... hacky!
Can anyone help?
I couldn't find a way either, and ended up doing the-hacky-way:
struct MyDoc: FileDocument {
...
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
throw CocoaError(.fileWriteNoPermission)
}
}
At least it's readable
Related
Problem
I am working on a simple SwiftUI document-based app on macOS (12.0.1) with Xcode 13.2.1.
In my app, I need two TextEditors in the document view. One displays the content of the document, and the other is used to support some other functions.
What is troubling me is that when editing in either of these text editors, the document is set edited, as is in this screenshot. I wonder whether there is a way to prevent the second TextEditor from doing this.
Code
This is the DocumentView to display the document with 2 TextEditors. Only the first one is meant to show the document content.
struct DocumentView: View {
#Binding var document: Document
#State private var string = ""
var body: some View {
TextEditor(text: $document.text)
TextEditor(text: $string)
}
}
The code for the app and the document structure is the same as the Xcode template:
import SwiftUI
#main
struct SwiftUIApp: App {
var body: some Scene {
DocumentGroup(newDocument: CPDocument()) { a in
DocumentView(document: a.$document)
}
}
}
import SwiftUI
import UniformTypeIdentifiers
struct Document: FileDocument {
var text: String
init(text: String = "Document Content") {
self.text = text
}
static var readableContentTypes: [UTType] { [.plainText] }
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
text = string
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = text.data(using: .utf8)!
return .init(regularFileWithContents: data)
}
}
What I've tried
I've searched the web but found nothing helpful.
I tried replacing TextEditor with TextField, and that worked for the problem. But a text editing area with only a single line is not what I want.
I tried a hacky solution which is to save the document after editing, but the application crashed with error message Performing #selector(saveDocument:) from sender __SwiftValue 0x600003efac40.
TextEditor(text: $string)
.onChange(of: string) { _ in
NSApp.sendAction(#selector(NSDocument.save(_:)), to: NSApp.keyWindow?.windowController?.document, from: self)
}
I wonder whether it's possible to do that with pure SwiftUI and what would be the best way to solve the problem. Thanks in advance.
The following trivial macOS app is written in SwiftUI 2.0.
import SwiftUI
#main
struct TempApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}
struct ContentView: View {
var body: some View {
Text("Hello, beep!").padding()
}
}
When in the foreground, this app will emit an error beep on certain keystrokes (like "a"). What's the simplest way to suppress this beep?
An Xcode project illustrating this (and the answer) can be found here.
There are many older related questions on SO, but none of these are specifically about doing this in SwiftUI 2.0.
You can suppress the beep by adding a local monitor for the .keyDown event at the top level. This can be done simply in ContentView.init(), like so:
struct ContentView: View {
var body: some View {
Text("Hello, silence!").padding()
}
init() {
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { _ in return nil }
}
}
This technique was inspired by this answer.
What I'm trying to do is store some data from a MacOS App with NSDocument provided class in a file. I decided to use SwiftUI , but all tutorials I found are using Storyboards. And from those I cannot adapt how to get the data from my textfield into my NSDocument class.
As far as I got it I need to init my variables in the NSDocument class like this
class Document: NSDocument {
#objc dynamic var contents = "Foo"
public init(contentString: String) {
self.contents = contentString
}
/* ... */
}
and in the same class I can save this string using
override func data(ofType typeName: String) throws -> Data {
return contents.data(using: .utf8) ?? Data()
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
So in my view generated with SwiftUI I can access this using
struct MainTableView: View {
#State var doc = Document.init()
var body: some View {
TextField("My text", text: self.$doc.contents)
}
}
But - as I'm using only an instance it always saves "Foo" - no matter what I type into my TextField.
Besides - another question that will follow up right away: On the long run I don't want to store a string only. I'll have 3 different 2D-Arrays with different data-structures. Is NSDocument able to handle this by itself or do I need to convert those to JSON/XML/...-String and store this as a file?
There's a lot of code my app normally runs that I would like to skip in Previews. Code that is time-consuming and has no visible effects (such as initializing audio devices). I'm trying to figure out how skip it for previews.
There is an easy way to run code only in the production build of an app using the DEBUG macro. But I don't know of anything similar for non-Preview builds (because Previews presumably build the same code as non-Previews).
I thought that setting a variable, previewMode, within my ViewModel, would work. That way I could set it to true only within the PreviewProvider:
struct MainView_Previews: PreviewProvider {
static var previews: some View {
let vm = ViewModel(previewMode: true)
return MainView(viewModel: vm)
}
}
and when I created the ViewModel within the SceneDelegate, I could set previewMode to false:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let vm = ViewModel(previewMode: false)
let mainView = MainView(viewModel: vm)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: mainView)
self.window = window
window.makeKeyAndVisible()
}
}
so that I can enclose any code I don't want to run for previews in if !previewMode { ••• }
Unfortunately the code is still running. Evidently the scene() function is getting called whenever my preview updates. :(
How can I specify code to not run for previews?
thanks!
The only working solution I've found is to use the ProcessInfo.processInfo.environment value for key XCODE_RUNNING_FOR_PREVIEWS. It's set to "1" only when running in preview mode:
let previewMode: Bool = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
See this post.
Practically Live-Preview mode run-time does not differ much from Simulator Debug mode run-time. And this, of course, as intended to give us quick (as possible) feedback of our code execution.
Anyway here are some findings... that might be used as solution/workaround for some cases that detection of Preview is highly desirable.
So created from scratch SwiftUI Xcode template project and in all functions of generated entities add print(#function) instruction.
ContentView.swift
import SwiftUI
struct ContentView: View {
init() {
print(#function)
}
var body: some View {
print(#function)
return someView()
.onAppear {
print(#function)
}
}
private func someView() -> some View {
print(#function)
return Text("Hello, World!")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
print(#function)
return ContentView()
}
}
Perform Debug Preview and see output:
application(_:didFinishLaunchingWithOptions:)
application(_:configurationForConnecting:options:)
scene(_:willConnectTo:options:)
init()
sceneWillEnterForeground(_:)
sceneDidBecomeActive(_:)
2020-06-12 16:08:14.460096+0300 TestPreview[70945:1547508] [Agent] Received remote injection
2020-06-12 16:08:14.460513+0300 TestPreview[70945:1547508] [Agent] Create remote injection Mach transport: 6000026c1500
2020-06-12 16:08:14.460945+0300 TestPreview[70945:1547482] [Agent] No global connection handler, using shared user agent
2020-06-12 16:08:14.461216+0300 TestPreview[70945:1547482] [Agent] Received connection, creating agent
2020-06-12 16:08:15.355019+0300 TestPreview[70945:1547482] [Agent] Received message: < DTXMessage 0x6000029c94a0 : i2.0e c0 object:(__NSDictionaryI*) {
"updates" : <NSArray 0x7fff8062cc40 | 0 objects>
"id" : [0]
"scaleFactorHint" : [3]
"providerName" : "11TestPreview20ContentView_PreviewsV"
"products" : <NSArray 0x600000fcc650 | 1 objects>
} > {
"serviceCommand" : "forwardMessage"
"type" : "display"
}
__preview__previews
init()
__preview__body
__preview__someView()
__preview__body
__preview__body
__preview__someView()
__preview__body
As it is clear complete workflow of app launching has been performed at start AppDelegate > SceneDelegate > ContentView > Window and only after this the PreviewProvider part.
And in this latter part we see something interesting - all functions of ContentView in Preview mode have __preview prefix (except init)!!
So, finally, here is possible workaround (DISCLAIMER!!! - on your own risk - only demo)
The following variant
struct ContentView: View {
var body: some View {
return someView()
.onAppear {
if #function.hasPrefix("__preview") {
print("Hello Preview!")
} else {
print("Hello World!")
}
}
}
private func someView() -> some View {
if #function.hasPrefix("__preview") {
return Text("Hello Preview!")
} else {
return Text("Hello World!")
}
}
}
Gives this
I use Alamofire for networking in my iOS application. I need to run this app in iOS 7+. I want to indicate network activity in status bar, so I created this struct:
struct ActivityManager {
static var activitiesCount = 0
static func addActivity() {
if activitiesCount == 0 {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
}
activitiesCount++
}
static func removeActivity() {
if activitiesCount > 0 {
activitiesCount--
if activitiesCount == 0 {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}
}
}
But I don't know, where to call in code addActivity() and removeActivity() methods. I don't want to write them with every request. I want to, that they will be called automatically with every request.
I tried also use pure NSURLSession and NSURLSessionTask and extend them:
extension NSURLSessionTask {
func resumeWithActivity() {
ActivityManager.addAction()
self.resume()
}
}
public extension NSURLSession {
func OwnDataTaskWithRequest(request: NSURLRequest!, ownCompletionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)?) -> NSURLSessionDataTask! {
return self.dataTaskWithRequest(request, completionHandler: { (data, response, error) in
ActivityManager.removeAction()
ownCompletionHandler!(data, response, error)
})
}
}
Then I used them like this:
var session: NSURLSession = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
session.OwnDataTaskWithRequest(request) { (data, response, error) -> Void in
// some logic here
}.resumeWithActivity()
But this approach doesn't work. In iOS 7 is NSURLSession extension not visible. I created a bug report for this (with sample project).
Can you please give me some advise, how to reach my goal? With or without Alamofire?
If you don't want to call your functions manually for every request and that you want to use Alamofire, I suggest you to improve it to add the network activity indicator feature.
Have a look at the source of Alamofire
You need to register 2 notification observers in your ActivityManager and then trigger the notifications at the relevant places either in Alamofire.Manager or in Alamofire.Request.
Also have a look at the source of AFNetworkActivityIndicatorManager which implement the feature you want in AFNetworking.