How do I set the text of a label from outside of viewDidAppear? - swift

I'm writing a Mac (Swift) application on Xcode which gets data from a command and asynchronously changes the stringValue of some text in the window. I already figured out the asynchronous part from here, but I can't seem to figure out how to actually change the text, since Xcode seems to require it to be in viewDidAppear. Unfortunately I can't put the function which runs the command in viewDidAppear since it is called by another file and needs to be a public func (as far as I know).
Here are a couple of methods I tried:
1. Call a function inside viewDidAppear which changes the text:
self.viewDidAppear().printText("testing!") // this part is where the "New Output" line is on the attached link above
...
override func viewDidAppear() {
func printText(_ string: String) {
textLabel.stringValue = string
}
}
Result: Value of tuple type '()' has no member 'printText' (on the first line)
2. Change an already-declared variable to the current message, then use Notification Center to tell viewDidAppear to change the text.
var textToPrint = "random text" // directly inside the class
let nc = NotificationCenter.default // directly inside the class
...
self.textToPrint = "testing!" // in place of the "New Output" line in the link above
self.nc.post(name: Notification.Name("printText"), object: nil) // in place of the "New Output" line in the link above
...
#objc func printText2() { // directly inside the class
textLabel.stringValue = textToPrint // directly inside the class
} // directly inside the class
...
override func viewDidAppear() {
nc.addObserver(self, selector: #selector(printText2), name: Notification.Name("printText"), object: nil)
}
For this one, I had to put printText2 outside of viewDidAppear because apparently selectors (for Notification Center) only work if you do that.
Result: NSControl.stringValue must be used from main thread only (on textLabel.stringValue line).
Also, the text never changes.
So I need to either somehow change the label's text directly from the asynchronous function, or to have viewDidAppear do it (also transmitting the new message).
...................................................................
Extra project code requested by Upholder of Truth
import Cocoa
class VC_image: NSViewController, NSWindowDelegate {
#IBOutlet var textLabel: NSTextField!
public func processImage(_ path: String) { // this function is called by another file
previewImage()
}
public func previewImage() {
if let path = Bundle.main.path(forResource: "bashscript", ofType: "sh") {
let task3 = Process()
task3.launchPath = "/bin/sh"
task3.arguments = [path]
let pipe3 = Pipe()
task3.standardOutput = pipe3
let outHandle = pipe3.fileHandleForReading
outHandle.readabilityHandler = { pipe3 in
if let line = String(data: pipe3.availableData, encoding: String.Encoding.utf8) {
// Update your view with the new text here
let messageToPrint = line.components(separatedBy: " ")
if (messageToPrint.count == 6) {
DispatchQueue.main.async {
self.textLabel.stringValue = "testing!"
}
}
} else {
print("Error decoding data: \(pipe3.availableData)")
}
}
task3.launch()
}
}
}

Related

Swift launch view only when data received

I'm getting info from an API using the following function where I pass in a string of a word. Sometimes the word doesn't available in the API if it doesn't available I generate a new word and try that one.
The problem is because this is an asynchronous function when I launch the page where the value from the API appears it is sometimes empty because the function is still running in the background trying to generate a word that exists in the API.
How can I make sure the page launches only when the data been received from the api ?
static func wordDefin (word : String, completion: #escaping (_ def: String )->(String)) {
let wordEncoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let uri = URL(string:"https://dictapi.lexicala.com/search?source=global&language=he&morph=false&text=" + wordEncoded! )
if let unwrappedURL = uri {
var request = URLRequest(url: unwrappedURL);request.addValue("Basic bmV0YXlhbWluOk5ldGF5YW1pbjg5Kg==", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let decoder = JSONDecoder()
let empty = try decoder.decode(Empty.self, from: data)
if (empty.results?.isEmpty)!{
print("oops looks like the word :" + word)
game.wordsList.removeAll(where: { ($0) == game.word })
game.floffWords.removeAll(where: { ($0) == game.word })
helper.newGame()
} else {
let definition = empty.results?[0].senses?[0].definition
_ = completion(definition ?? "test")
return
}
}
}
catch {
print("connection")
print(error)
}
}
dataTask.resume()
}
}
You can't stop a view controller from "launching" itself (except not to push/present/show it at all). Once you push/present/show it, its lifecycle cannot—and should not—be stopped. Therefore, it's your responsibility to load the appropriate UI for the "loading state", which may be a blank view controller with a loading spinner. You can do this however you want, including loading the full UI with .isHidden = true set for all view objects. The idea is to do as much pre-loading of the UI as possible while the database is working in the background so that when the data is ready, you can display the full UI with as little work as possible.
What I'd suggest is after you've loaded the UI in its "loading" configuration, download the data as the final step in your flow and use a completion handler to finish the task:
override func viewDidLoad() {
super.viewDidLoad()
loadData { (result) in
// load full UI
}
}
Your data method may look something like this:
private func loadData(completion: #escaping (_ result: Result) -> Void) {
...
}
EDIT
Consider creating a data manager that operates along the following lines. Because the data manager is a class (a reference type), when you pass it forward to other view controllers, they all point to the same instance of the manager. Therefore, changes that any of the view controllers make to it are seen by the other view controllers. That means when you push a new view controller and it's time to update a label, access it from the data property. And if it's not ready, wait for the data manager to notify the view controller when it is ready.
class GameDataManager {
// stores game properties
// updates game properties
// does all thing game data
var score = 0
var word: String?
}
class MainViewController: UIViewController {
let data = GameDataManager()
override func viewDidLoad() {
super.viewDidLoad()
// when you push to another view controller, point it to the data manager
let someVC = SomeOtherViewController()
someVC.data = data
}
}
class SomeOtherViewController: UIViewController {
var data: GameDataManager?
override func viewDidLoad() {
super.viewDidLoad()
if let word = data?.word {
print(word)
}
}
}
class AnyViewController: UIViewController {
var data: GameDataManager?
}

When setting .attributedText of a UITextView twice in quick succession, the second assignment has no effect

I have a very simple UIViewController subclass which configures its view in viewDidLoad:
class TextViewController: UIViewController {
private var textView: UITextView?
var htmlText: String? {
didSet {
updateTextView()
}
}
private func updateTextView() {
textView?.setHtmlText(htmlText)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
textView = UITextView()
// add as subview, set constraints etc.
updateTextView()
}
}
(.setHtmlText is an extension on UITextView which turns HTML into an NSAttributedString, inspired by this answer)
An instance of TextViewController is created, .htmlText is set to "Fetching...", an HTTP request is made and the viewcontroller is pushed onto a UINavigationController.
This results in a call to updateTextView which has no effect (.textView is still nil), but viewDidLoad ensures the current text value is shown by calling it again. Shortly afterwards, the HTTP request returns a response, and .htmlText is set to the body of that response, resulting in another call to updateTextView.
All of this code is run on the main queue (confirmed by setting break points and inspecting the stack trace), and yet unless there is a significant delay in the http get, the final text displayed is the placeholder ("Fetching..."). Stepping through in the debugger reveals that the sequence is:
1. updateTextView() // htmlText = "Fetching...", textView == nil
2. updateTextView() // htmlText = "Fetching...", textView == UITextView
3. updateTextView() // htmlText = <HTTP response body>
4. setHtmlText(<HTTP response body>)
5. setHtmlText("Fetching...")
So somehow the last call to setHtmlText appears to overtake the first. Similarly bizarrely, looking back up the call stack from #5, while setHtmlText is claiming that it was passed "Fetching...", it's caller believes it's passing the HTTP HTML body.
Changing the receiver of the HTTP response to do this:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { vc.htmlText = html }
Rather than the more conventional:
DispatchQueue.main.async { vc.htmlText = html }
... does result in the expected final text being displayed.
All of this behaviour is reproducible on simulator or real device. A slightly hacky feeling "solution" is to put another call to updateTextView in viewWillAppear, but that's just masking what's going on.
Edited to add:
I did wonder whether it was adequate to just have one call to updateTextView in viewWillAppear, but it needs to be called from viewDidLoad AND viewWillAppear for the final value to be displayed.
Edited to add requested code:
let theVc = TextViewController()
theVc.htmlText = "<i>Fetching...</i>"
service.get(from: url) { [weak theVc] (result: Result<String>) in
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
DispatchQueue.main.async {
switch result {
case .success(let html):
theVc?.htmlText = html
case .error(let err):
theVc?.htmlText = "Failed: \(err.localizedDescription)"
}
}
}
navigationController.pushViewController($0, animated: true)
Edited to add simplified case, eliminating the HTTP service, with the same behaviour:
let theVc = TextViewController()
theVc.htmlText = "<i>Before...</i>"
DispatchQueue.main.async {
theVc.htmlText = "<b>After</b>"
}
navigationController.pushViewController(theVc, animated: true)
This yields an equivalent sequence of calls to updateTextView() as before:
"Before", no textView yet
"Before"
"After"
And yet "Before" is what I see on-screen.
Setting a break point at the start of setHtmlText ("Before") and stepping through reveals that while the first pass is in NSAttributedString(data:options:documentAttributes:) the run-loop is re-entered and the second assignment ("After") is given chance to run to completion, assigning it's result to .attributedText. Then, the original NSAttributedString is given chance to complete and it immediately replaces .attributedText.
This is a quirk of the way NSAttributedStrings are generated from HTML (see somebody having similar issues when populating a UITableView)
I solved this by eliminating your extension and simply writing the code that sets the text view’s attributed text to use a serial dispatch queue. Here is my TextViewController:
#IBOutlet private var textView: UITextView?
let q = DispatchQueue(label:"textview")
var htmlText: String? {
didSet {
updateTextView()
}
}
override func viewDidLoad() {
super.viewDidLoad()
updateTextView()
}
private func updateTextView() {
guard self.isViewLoaded else {return}
guard let s = self.self.htmlText else {return}
let f = self.textView!.font!
self.q.async {
let modifiedFont = String(format:"<span style=\"font-family: '-apple-system', 'HelveticaNeue'; font-size: \(f.pointSize)\">%#</span>", s)
DispatchQueue.main.async {
let attrStr = try! NSAttributedString(
data: modifiedFont.data(using: .unicode, allowLossyConversion: true)!,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil)
self.textView!.attributedText = attrStr
}
}
}
Adding print statements reveals that everything happens in the expected order (the order in which htmlText gets set).
How about this way to solve the problem?
private var textView: UITextView? = UITextView()
remove updateTextView() and textView = UITextView() in ViewDidLoad()

Cocoa: How to re-read file content after file has been changed from another app using NSDocument?

I have a document based cocoa app that opens an .md file to display the markdown content in a nice format. If I change the .md file in another app like textedit, I want to reload the views in my app.
Here's what I have working so far:
import Cocoa
class Document: NSDocument {
var fileContent = "Nothing yet :("
override init() {
// Add your subclass-specific initialization here.
super.init()
}
override class var autosavesInPlace: Bool {
return false
}
override func makeWindowControllers() {
// Returns the Storyboard that contains your Document window.
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController
self.addWindowController(windowController)
}
override func data(ofType typeName: String) throws -> Data {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func read(from data: Data, ofType typeName: String) throws {
fileContent = (try String(data: data, encoding: .utf8))!
}
// this fn is called every time textEdit changes the file content.
override func presentedItemDidChange() {
// Here is the PROBLEM:
// HOW do I access the new file content?
}
}
Here is the problem
presentedItemDidChange() is called every time textEdit makes a change. That works great. But I can't for the life of me figure out how then to access the new file content, so I can reassign fileContent = newContent. Any thoughts?
I would call for the document readFromURL:ofType:error: as described here.

Getting data out of a firebase function in Swift [duplicate]

In my iOS app, I have two Firebase-related functions that I want to call within viewDidLoad(). The first picks a random child with .queryOrderedByKey() and outputs the child's key as a string. The second uses that key and observeEventType to retrieve child values and store it in a dict. When I trigger these functions with a button in my UI, they work as expected.
However, when I put both functions inside viewDidLoad(), I get this error:
Terminating app due to uncaught exception 'InvalidPathValidation', reason: '(child:) Must be a non-empty string and not contain '.' '#' '$' '[' or ']''
The offending line of code is in my AppDelegate.swift, highlighted in red:
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate
When I comment out the second function and leave the first inside viewDidLoad, the app loads fine, and subsequent calls of both functions (triggered by the button action) work as expected.
I added a line at the end of the first function to print out the URL string, and it doesn't have any offending characters: https://mydomain.firebaseio.com/myStuff/-KO_iaQNa-bIZpqe5xlg
I also added a line between the functions in viewDidLoad to hard-code the string, and I ran into the same InvalidPathException issue.
Here is my viewDidLoad() func:
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
pickRandomChild()
getChildValues()
}
Here is the first function:
func pickRandomChild () -> String {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
})
return movieToGuess
}
Here is the second function:
func getChildValues() -> [String : AnyObject] {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
})
return movieDict
)
And for good measure, here's the relevant portion of my AppDelegate.swift:
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
FIRApp.configure()
return true
}
I'm guessing Swift is executing the code not in the order I expect. Does Swift not automatically wait for the first function to finish before running the second? If that's the case, why does this pairing work elsewhere in the app but not in viewDidLoad?
Edit: The issue is that closures are not called in order.
I'm not sure what your pickRandomChild() and getChildValues() methods are, so please post them as well, but the way I fixed this type issue was by sending the data through a closure that can be called in your ViewController.
For example when I wanted to grab data for a Full Name and Industry I used this. This method takes a Firebase User, and contains a closure that will be called upon completion. This was defined in a class specifically for pulling data.
func grabDataDict(fromUser user: FIRUser, completion: (data: [String: String]) -> ()) {
var myData = [String: String]()
let uid = user.uid
let ref = Constants.References.users.child(uid)
ref.observeEventType(.Value) { (snapshot, error) in
if error != nil {
ErrorHandling.defaultErrorHandler(NSError.init(coder: NSCoder())!)
return
}
let fullName = snapshot.value!["fullName"] as! String
let industry = snapshot.value!["industry"] as! String
myData["fullName"] = fullName
myData["industry"] = industry
completion(data: myData)
}
}
Then I defined an empty array of strings in the Viewcontroller and called the method, setting the variable to my data inside the closure.
messages.grabRecentSenderIds(fromUser: currentUser!) { (userIds) in
self.userIds = userIds
print(self.userIds)
}
If you post your methods, however I can help you with those specifically.
Edit: Fixed Methods
1.
func pickRandomChild (completion: (movieToGuess: String) -> ()) {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
// Put whatever you want to return here.
completion(movieToGuess)
})
}
2.
func getChildValues(completion: (movieDict: [String: AnyObject]) -> ()) {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
// Put whatever you want to return here.
completion(movieDict)
})
}
Define these methods in some model class, and when you call them in your viewcontroller, you should be able to set your View Controller variables to movieDict and movieToGuess inside each closure. I made these in playground, so let me know if you get any errors.
Your functions pickRandomChild() and getChildValues() are asynchronous, therefore they only get executed at a later stage, so if getChildValues() needs the result of pickRandomChild(), it should be called in pickRandomChild()'s completion handler / delegate callback instead, because when one of those are called it is guaranteed that the function has finished.
It works when you comment out the second function and only trigger it with a button press because there has been enough time between the app loading and you pushing the button for the asynchronous pickRandomChild() to perform it action entirely, allowing getChildValues() to use its returned value for its request.

Why isn't my method getting called?

I have a viewController communicating with DownloaderHandler using DownloaderDelegate protocol.
My protocol is defined as:
protocol DownloaderDelegate : class {
func didReceive(data:Data)
}
I have a viewController
class ViewController: UIViewController {
weak var downloadHandler : DownloaderHandler?
override func viewDidLoad() {
super.viewDidLoad()
downloadHandler = DownloaderHandler()
downloadHandler?.delegate = self
changeBackground()
}
func changeBackground (){
let googleURL = URL(fileURLWithPath: "https://www.google.com/doodle4google/images/splashes/featured.png")
print(googleURL)
downloadHandler?.downloadData(url:googleURL) // Line BB
}
}
extension ViewController : DownloaderDelegate{
func didReceive(data: Data) {
let image = UIImage(data: data)
let imageView = UIImageView(image: image!)
view.insertSubview(imageView, at: 0)
}
}
And I have a Delegating class as :
class DownloaderHandler : NSObject, URLSessionDelegate{
weak var delegate :DownloaderDelegate?
var downloadsSession: URLSession = {
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration)
return session // Line AA
}()
func downloadData(url: URL){ // Line CC
downloadsSession.dataTask(with: url){ data, response, error in
print("error is \(error), data is \(data) and response is \(response)") // Line DD
if let badError = error {
print(" the url didn't succeeed error is \(badError.localizedDescription)")
}
else if let someResponse = response as? HTTPURLResponse {
if someResponse.statusCode == 200{
self.delegate?.didReceive(data: data!)
}
}
}
}
}
Using breakpoints: Line AA, gets loaded. Line BB calls. Line CC never gets called. Why? What am I doing wrong?!
You have declared:
weak var downloadHandler : DownloaderHandler?
Then you say:
downloadHandler = DownloaderHandler()
downloadHandler is a weak reference, and nothing else retains this DownloaderHandler instance, so it vanishes in a puff of smoke after it is created. Your logging shows it being created, but if you were to log on its deinit you would also see it vanish immediately afterward. By the time you say downloadHandler?.downloadData(url:googleURL), your downloadHandler reference is nil and so nothing happens; you are talking to nobody at that point.
[You are probably slavishly following a mental rule that delegate references should be weak. But that rule is predicated on the assumption that the delegate has an independent existence, and thus should not be "owned" by the referrer. This object, however, has no independent existence; it is more a decorator object (what I would call a Helper). Thus, the reference needs to be strong. The back-reference is still weak, so you won't get a circular retain cycle.]
Remove the "weak" qualifier from the downloadHandler property on your view controller.
As it is the only reference to the downloadHandler object, it will be removed from memory as soon as the viewDidLoad method finishes executing.
You can make a small test; add a breakpoint to line BB and check if downloadHandler has a value. I suspect it will be "nil", because it is a weak property.