Swift: How to link Touch Bar controls to main window controls - swift

I'm new to Swift/macOS dev, plenty of dev experience otherwise though. Just trying to make something rudimentary.
Here's my app storyboard:
I'm trying to get:
the Touch Bar slider to change when the slider on the main window changes
vice versa
update the Touch Bar Label button with the Int value of the slider.
Q) How do I achieve this?
Note: The main window slider control is wired up and working when I manipulate it e.g.
#IBOutlet weak var mySlider: NSSlider!
#IBAction func mySlider_Changed(_ sender: NSSlider) {
//... stuff happens here.
}

You'll want your view controller to have some explicit model/state of what the value of these sliders have. e.g.
class ViewController : NSViewController {
var value: Double
}
Then you can connect the sliders and textfield to update or display this value.
Approach 1: Target/Action/SetValue
This follows the use of explicit IBActions that you had started. In response to that action, we'll pull the doubleValue from the slider and update the ViewController's model from that:
#IBAction func sliderValueChanged(_ sender: NSSlider) {
value = sender.doubleValue
}
The second piece is updating everything to reflect that new value. With Swift, we can just use the didSet observer on the ViewController's value property to know when it changes and update all of the controls, e.g:
#IBOutlet weak var touchBarSlider: NSSlider!
#IBOutlet weak var windowSlider: NSSlider!
#IBOutlet weak var windowTextField: NSTextField!
var value: Double {
didSet {
touchBarSlider.doubleValue = value
windowSlider.doubleValue = value
windowTextField.doubleValue = value
}
}
And that's it. You can add a number formatter to the textfield so it nicely displays the value, which you can do in Interface Builder or programmatically. And any other time you change the value, all of the controls will still get updated since they are updated in the didSet observer instead of just the slider action methods.
Approach 2: Bindings
Bindings can eliminate a lot of this boiler plate code when it comes to connecting model data to your views.
With bindings you can get rid of the outlets and the action methods, and have the only thing left in the view controller be:
class ViewController: NSViewController {
#objc dynamic var value: Double
}
The #objc dynamic makes the property be KVO compliant, which is required when using bindings.
The other piece is establishing bindings from the controls to our ViewController's value property. For all of the controls this is done by through the bindings inspector pane, binding the 'Value' of the control to the View Controller's value key path:
And that's it. Again, you could add a number formatter to the textfield, and any other changes to the value property will still update your controls since it will trigger the bindings to it. (you can also still use the didSet observer for value to make other changes that you can't do with bindings)

Related

How to share a builder class across multiple instances in swift

I am trying to become a better developer and just started to study and use software patterns in small test projects.
In my current project I focused on the builder pattern.
Basically all I have is an App with two Views (Storyboards), A single ViewController-Class (used by all of the views) and one Builder-Class that creates an object with two properties.
The Builder-Class is implemented in the ViewController.
class ViewController: UIViewController {
#IBOutlet var startTimePicker: UIDatePicker!
#IBOutlet var workingHoursPicker: UIDatePicker!
#IBAction func setStartTimeButtonPressed(_ sender: Any) {
setStartTime()
}
#IBAction func setWorkingHoursButtonPressed(_ sender: Any) {
setWorkingHours()
}
var workdayBuilder: WorkdayBuilder = ConcreteWorkdayBuilder()
....
My current problem is that every time I navigate to the next View (using a Segue) the ViewController will be instantiated again and loses the value I set in the previous view.
I know that I could technically just pass the value to the next ViewController and then set it there or that I could set the value and then pass the instance of my builder to the next view. But that would kind of destroy the purpose of the builder in this project IMO.
I then thought that I could wrap my Builder into a Singleton and use it like a shared instance across the views:
final class WorkdayBuilderSingleton {
static let sharedInstance: WorkdayBuilder = ConcreteWorkdayBuilder()
init() {}
}
But this lead to another error in my VC when I tried to set a property. "Cannot assign to property: 'sharedInstance' is a 'let' constant".
So I am left with some questions and hope you can help me.
Is it a good or OK practice in general to make a builder a singleton or did I make a bad mistake here?
If it is OK, can I change mit static let into a static var or would that kill the singleton?

How do you get a text field input in you view controller code?

I’m trying to make Xcode print "Nice!" when you type in "Hi". I've used a IBOutlet, but I don’t know how to use the user input in my code. Also BTW I'm using Storyboard and not SwiftUI. It also gives me an error when I try to compare the datatype UIViewController and a String. Here is my view controller code(with the default App Delegate and Scene Delegate code):
import UIKit
class ViewController: UIViewController {
#IBOutlet var yeet: [UITextField]!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func fuel(_ yeet:UIViewController) -> Int {
if yeet == ("hi") {
print("Nice!")
}
}
}
your textfield show be setup as
#IBOutlet weak var textFeildName: UITextField!
you will need to change a couple things inside of your file to prevent a crash. I'd delete the textfield and drag it into the assistant view and give it a new name.
but before you press "connect" press the "outlet" tab and change it to "Action" and then a new selector should come up select "Editing Did End" and go to the top and press "Did End On Exit"
after that is done would want to reference the variable of the text field:
example:
#IBAction func TextFieldName(_ sender: Any) {
if(self.TextFeildName.text!.contains("hi")){
print("Nice!")
}
}
On top of all this, you do not compare strings with == that's only if you compare 2 separate strings for example stringOne == stringTwo if you are comparing or asking if a string contains anything you'd want to use the developing language specific string container IE: .contains
Also, please do not include "Xcode" as a tag with your question, as that should be reserved for Xcode related problems. not Swift or objective-c coding issues.

Following Apple 'Food Tracker' tutorial for Xcode - can't get button to change label text

I'm following official iOS Apps tutorial to make a basic Single View Application in Xcode.
Literally all we have done so far is:
Added a label to the UI and set initial text to 'Meal name:'
Added a textbox to the UI
Added a button to the UI
Then we've added some very simple code to the View Controller declaring the label as an outlet and a button action which, when invoked, should change the label's text to Default Text.
My ViewController code is now identical to the tutorial code namely:
class ViewController: UIViewController {
//MARK: Properties
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var mealNameLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
//MARK: Actions
#IBAction func setDefaultLabelText(_ sender: UIButton) {
mealNameLabel.text = "Default Text"
}
}
The problem is that when I press the button in simulator, I get an error message saying an implicitly unwrapped optional value is nil. (app launches fine, it's just when pressing the button)
As I understand it this means something is blank that can't be, but the only optionals I have are:
The textbox, which isn't blank because before I press the button I write 'biscuits' or something in it
The label text, which isn't blank because it's set to 'Meal name:' by default
I really can't work out what supposedly has a nil value that is triggering this error?
As I understand it this means something is blank that can't be
No , This means you need to make sure outlet
mealNameLabel.text = "Default Text" // here mealNameLabel is nil
is connected to the label in IB

Alternate way to organize functions in a NSViewController subclass?

(Swift, OSX)
I currently have a view controller (named ViewController) that manages the view of my application with storyboards, consisting of a couple of text boxes for user input. If the user enters something incorrect, it calls a function that alerts the user to their error (with a NSAlert modal), and offers them an option: reset their input, reset their input and previous output, or just dismiss the window.
Problem is, that alert code alone is about 60 lines of code (with functions to display appropriate messages), and I want to move it into another class -- a subclass of ViewController, entitled ErrorResponse, so it can still access the user interface to appropriately reset their input (i.e., clear an input string field), and do the other tasks mentioned above while avoiding a massive wall of functions in the ViewController
I've tried creating a subclass of ViewController so the user interface properties are inherited and I can simply access them, however Xcode wants the subclass to implement an init method with NSCoder. (To my knowledge, this is how Xcode sets up a storyboard or .xib to communicate with the controller, as part of the NSViewController class, but this is unrelated to my implmenetation). When I attempt to pass nil or a default initialized NSCoder() object, I get obscure crashes.
So, is there any way to organize these functions? Or should I just keep them all in the ViewController?
Edit: I also have an existing class called ErrorCheck that performs the check to see if there are any errors. Within the ViewContoller, when the input is entered, there is a guard statement with a method from ErrorCheck, and if the input is invalid in some way, the corresponding methods in ErrorResponse are called.
Code snippet:
class ViewController: NSViewController {
// Input and output text fields
#IBOutlet weak var inputStr: NSTextField!
#IBOutlet weak var outputStr: NSTextField!
// Input and output conversion selection
#IBOutlet weak var inputSegments: NSSegmentedControl!
#IBOutlet weak var outputSegments: NSSegmentedControl!
#IBAction func outputIsSelected(sender: NSSegmentedControl) {
// Passing all UI elements in (not shown)
// Not ideal, want to be global obj (see below code)
guard ErrorCheck().stringIsNotEmpty(inputStr.stringValue) else {
ErrorResponse().invalidEmptyInput()
return
}
// 0 = DNA, 1 = mRNA, 2 = English
if (inputSegments.selectedSegment == 0) {
checkPossibleConversionAndConvertDNA()
} else if (inputSegments.selectedSegment == 1) {
checkPossibleConversionAndConvertmRNA()
} else if (inputSegments.selectedSegment == 2) {
checkPossibleConversionAndConvertEnglish()
}
}
}
If I don't subclass ViewController, there is another issue: I can't globally define an object for ErrorResponse, as I can't pass the UI values within the same scope as they are being initialized, so currently I have initialized an ErrorCheck object in each corresponding function... and its messy.
I ended up just moving the methods into ViewController. Not the prettiest, but it's functional.

Tooltip doesn't show up again

I have a Mac app that exclusively live on the menu bar. It has a progress bar and a label. The label shows the percentage of the progress of the task that's being carried out. I want to show more info when the user hovers the mouse pointer over the progress indicator.
When I set the tooltip initially and hover over, it displays without an issue.
But if I head over somewhere and open the menu app again and hover over again, the tooltip doesn't come up. I can't figure out why. Here's my code.
ProgressMenuController.swift
import Cocoa
class ProgressMenuController: NSObject {
#IBOutlet weak var menu: NSMenu!
#IBOutlet weak var progressView: ProgressView!
let menuItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
var progressMenuItem: NSMenuItem!
override func awakeFromNib() {
menuItem.menu = menu
menuItem.image = NSImage(named: "icon")
progressMenuItem = menu.itemWithTitle("Progress")
progressMenuItem.view = progressView
progressView.update(42)
}
#IBAction func quitClicked(sender: NSMenuItem) {
NSApplication.sharedApplication().terminate(self)
}
}
ProgressView.swift
import Cocoa
class ProgressView: NSView {
#IBOutlet weak var progressIndicator: NSProgressIndicator!
#IBOutlet weak var progressPercentageLabel: NSTextField!
func update(value: Double) {
dispatch_async(dispatch_get_main_queue()) {
self.progressIndicator.doubleValue = value
self.progressIndicator.toolTip = "3 out of 5 files has been copied"
self.progressPercentageLabel.stringValue = "\(value)%"
}
}
}
This is a demo app similar to my actual app. So the update() function is called only once and the values are hardcoded. But in my actual app, the progress is tracked periodically and the update() function gets called with it to update the values. The label's percentage value and the progress indicator's value get updated without a problem. The issue is only with the tooltip.
Is this expected behavior or am I missing something?
I ran into the same problem, and realized the issue was that only the currently focused window will display tool-tips, but after my app lost focus, it would never get it back. Focus usually transfers automatically when the user clicks on your window, but it isn't automatic for menu bar apps. Using NSApp.activate, you can regain focus onto your app:
override func viewWillAppear() {
super.viewWillAppear()
NSApp.activate(ignoringOtherApps: true)
}
sanche's answer worked for me as well, but I ended up moving the tool tips to my NSMenuItems instead so I wouldn't have to steal focus from the foreground app. NSMenuItem's tool tips seem to be handled as a special case so the app doesn't need to be focused.
This solution would make the tool tip apply to everything in the menu item and appear next to the menu rather than over it, but it looks like that might not be a problem in your case.