I have a tiny one-view SwiftUI app on macOS that acts as a front-end for hledger. I need it to prompt the user for a ledger file to use preferably with the minimum amount of extra code or user UI interactions (in that order of preference), and I would like for this to happen precisely once at launch ideally. My first idea was to do something like this:
func openHledgerFile() -> String {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = false
panel.canChooseDirectories = false
if panel.runModal() == .OK {
return panel.url?.path ?? ""
}
return ""
}
struct ExpenseControllerView: View {
private var ledgerFileName: String {
openHledgerFile() // Real code would make this condidional
}
var body: some View {
Text("Opened file \(ledgerFileName)")
}
}
however, that never shows the dialog at all.
Is there a way I can tell the declarative subsystem "if this value is not initialised then don't show the view, show the dialog"? I would like to avoid having an enitre view with just a "do thing" button.
I am trying to update a label to indicate status, but it is not being refreshed/shown as expected
func setStatusText(status: String) {
statusText.stringValue = status;
}
func doSomething() {
setStatusText(status: "Activating license"); // This is never shown
var rc = PerformAction()
if (rc == 0) {
setStatusText(status: "\(action) succeeded") // Correctly displayed
} else {
setStatusText(status: "\(action) failed") // Correctly displayed
}
The status update before the action is never being shown, but the status set right after the action is correctly displayed. What do I do to be sure the first status is shown?
Edited: I tried something else to try to isolate it. I added a button press, which will set text, sleep, and then set text again. I never see "first" in the status.
#IBAction func TestSettingText(_ sender: Any) {
self.statusText.stringValue = "first";
sleep(15)
self.statusText.stringValue = "second";
}
I don't know what exactly PerformAction does but it seems like it finishes quickly so you just can't see the Activating license status. It is there but only for a very brief moment and instantly changed to the next status
I have a code like this:
print("Migration Execution: Successfully uninstalled MCAfee")
migrationInfoPicture.image = NSImage(named: "Unroll")
migrationInfoText.stringValue = NSLocalizedString("Unrolling from old server... Please wait!", comment: "Unrolling")
while(!readFile(path:logfilePath)!.contains("result: 2 OK")) {
searchLogForError(scriptPath: scriptOnePath)
}
print("Migration Execution: Successfully unrolled from old server")
migrationInfoText.stringValue = NSLocalizedString("Setting up MDM profile... Please wait!", comment: "Setting up MDM")
while(!readFile(path:logfilePath)!.contains("result: 3 OK")) {
searchLogForError(scriptPath: scriptOnePath)
}
It actually works in the background, reading from the file works and logging works but since the GUI will be hanging executing a while loop with a quickly completed task, the image and the text changes will not be visible.
Code for searchForLogError is:
func searchLogForError(scriptPath:String) {
if((readFile(path:logfilePath)!.filter { $0.contains("ERROR") }).contains("ERROR")) {
print("Migration abborted")
migrationInfoPicture.image = NSImage(named: "FatalError")
migrationInfoText.stringValue = NSLocalizedString("An error occured: \n", comment: "Error occurence") + readFile(path:logfilePath)!.filter { $0.contains("ERROR") }[0]
migrationWarningText.stringValue = NSLocalizedString("In order to get further help, please contact: mac.workplace#swisscom.com", comment: "Error support information")
self.view.window?.level = .normal
btnExitApplicationOutlet.isHidden = false
getScriptProcess(path:scriptPath).terminate()
return
}
}
How can I achieve a visible change of NSImage and NSLocalizedString while constantly looking for log file change without a hanging GUI (or even with a hanging GUI, but with enough time to change the visible elements between the while-loops)?
Polling file system resources is a horrible practice. Don't do that. There are dedicated APIs to observe file system resources for example DispatchSourceFileSystemObject
Create a property
var fileSystemObject : DispatchSourceFileSystemObject?
and two methods to start and stop the observer. In the closure of setEventHandler insert the code to read the file
func startObserver(at url: URL)
{
if fileSystemObject != nil { return }
let fileDescriptor : CInt = open(url.path, O_EVTONLY);
if fileDescriptor < 0 {
print("Could not open file descriptor"))
return
}
fileSystemObject = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: [.write, .rename], queue: .global())
if fileSystemObject == nil {
close(fileDescriptor)
print"Could not create Dispatch Source"))
return
}
fileSystemObject!.setEventHandler {
if self.fileSystemObject!.mask.contains(.write) {
// the file has been modified, do something
}
}
fileSystemObject!.setCancelHandler {
close(fileDescriptor)
}
fileSystemObject!.resume()
}
func stopObserver()
{
fileSystemObject?.cancel()
fileSystemObject = nil
}
I need to send the status of the network to some analytics server, so I need to send it once the app starts. I tried to use Alamofire, but I usually get Unknown status, if there is some sort of delay it shows the right status :
These code would run in my AppDelegate (didFinishLaunchingWithOptions):
AFNetworkReachabilityManager.shared().startMonitoring()
AFNetworkReachabilityManager.shared().localizedNetworkReachabilityStatusString()
What is the best way to get the right status right away?
UPDATE 1:
I updated my code and tried to use completion handler, but why when I use this method it will print multiple YES?
connectedCompletionBlock({ connected in
if connected {
print("YES")
} else {
print("NO")
}
})
class func connectedCompletionBlock(_ completion: #escaping (_ connected: Bool) -> Void) {
AFNetworkReachabilityManager.shared().startMonitoring()
AFNetworkReachabilityManager.shared().setReachabilityStatusChange({ status in
var isConnected = false
let wifi = AFNetworkReachabilityStatus.reachableViaWiFi
let wwan = AFNetworkReachabilityStatus.reachableViaWWAN
if ( status == wifi || status == wwan) {
con = true
}
AFNetworkReachabilityManager.shared().stopMonitoring()
completion(isConnected)
})
}
Ok since nobody didn't answer, I think it's good to share the solution with you. The issue was this: I was calling this method on didFinishLaunchingWithOptions, and since it takes sometime for the Alamofire to figure out the connection status it would return unknown! I called it on applicationDidBecomeActive and it works fine now.
How do I keep my application running in the background?
Would I have to jailbreak my iPhone to do this? I just need this app to check something from the internet every set interval and notify when needed, for my own use.
Yes, no need to jailbreak. Check out the "Implementing long-running background tasks" section of this doc from Apple.
From Apple's doc:
Declaring Your App’s Supported Background Tasks
Support for some types of background execution must be declared in advance by the app that uses them. An app declares support for a service using its Info.plist file. Add the UIBackgroundModes key to your Info.plist file and set its value to an array containing one or more of the following strings: (see Apple's doc from link mentioned above.)
I guess this is what you required
When an iOS application goes to the background, are lengthy tasks paused?
iOS Application Background Downloading
This might help you ...
Enjoy Coding :)
Use local notifications to do that. But this will not check every time. You will have to set a time where you will check your specific event, you may shorten this by decreasing your time slot. Read more about local notification to know how to achieve this at:
http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Introduction/Introduction.html
I found a way, to keep app running in background by playing silence
Make sure, that you selected audio playback in background modes
Also, don't use this method for long time, since it consumes CPU resources and battery juice, but I think it's a suitable way to keep app alive for a few minutes.
Just create an instance of SilencePlayer, call play() and then stop(), when you done
import CoreAudio
public class SilencePlayer {
private var audioQueue: AudioQueueRef? = nil
public private(set) var isStarted = false
public func play() {
if isStarted { return }
print("Playing silence")
let avs = AVAudioSession.sharedInstance()
try! avs.setCategory(AVAudioSessionCategoryPlayback, with: .mixWithOthers)
try! avs.setActive(true)
isStarted = true
var streamFormat = AudioStreamBasicDescription(
mSampleRate: 16000,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked,
mBytesPerPacket: 2,
mFramesPerPacket: 1,
mBytesPerFrame: 2,
mChannelsPerFrame: 1,
mBitsPerChannel: 16,
mReserved: 0
)
let status = AudioQueueNewOutput(
&streamFormat,
SilenceQueueOutputCallback,
nil, nil, nil, 0,
&audioQueue
)
print("OSStatus for silence \(status)")
var buffers = Array<AudioQueueBufferRef?>.init(repeating: nil, count: 3)
for i in 0..<3 {
buffers[i]?.pointee.mAudioDataByteSize = 320
AudioQueueAllocateBuffer(audioQueue!, 320, &(buffers[i]))
SilenceQueueOutputCallback(nil, audioQueue!, buffers[i]!)
}
let startStatus = AudioQueueStart(audioQueue!, nil)
print("Start status for silence \(startStatus)")
}
public func stop() {
guard isStarted else { return }
print("Called stop silence")
if let aq = audioQueue {
AudioQueueStop(aq, true)
audioQueue = nil
}
try! AVAudioSession.sharedInstance().setActive(false)
isStarted = false
}
}
fileprivate func SilenceQueueOutputCallback(_ userData: UnsafeMutableRawPointer?, _ audioQueueRef: AudioQueueRef, _ bufferRef: AudioQueueBufferRef) -> Void {
let pointer = bufferRef.pointee.mAudioData
let length = bufferRef.pointee.mAudioDataByteSize
memset(pointer, 0, Int(length))
if AudioQueueEnqueueBuffer(audioQueueRef, bufferRef, 0, nil) != 0 {
AudioQueueFreeBuffer(audioQueueRef, bufferRef)
}
}
Tested on iOS 10 and Swift 4
I know this is not the answer to your question, but I think it is a solution.
This assumes that your trying to check something or get data from the internet on a regular basis?
Create a service that checks the internet every set interval for whatever it is you want to know, and create a push notification to alert you of it, if the server is down, or whatever it is your trying to monitor has changed state. Just an idea.
Yes you can do something like this. For that you need to set entry in info.plist to tell os that my app will run in background. I have done this while I wanted to pass user's location after particular time stamp to server. For that I have set "Required background modes" set to "App registers for location updates".
You can write a handler of type UIBackgroundTaskIdentifier.
You can already do this in the applicationDidEnterBackground Method