I have a counting-upward object in StopWatch class and a label showing its value in ViewController class. I used #Published and #ObservedObject property wrappers for sharing and observing counter's value.
How could I automatically update counter's value in a label?
ViewController.swift
import UIKit
import SwiftUI
class ViewController: UIViewController {
#ObservedObject var stopWatch = StopWatch()
#IBOutlet var label: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
label.text = "\(self.stopWatch.counter)"
}
#IBAction func start(_ sender: UIButton) { self.stopWatch.start() }
#IBAction func stop(_ sender: UIButton) { self.stopWatch.stop() }
}
StopWatch.swift
class StopWatch: ObservableObject {
#Published var counter: Int = 0
var timer = Timer()
func start() {
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0,
repeats: true) { _ in
self.counter += 1
}
}
func stop() {
self.timer.invalidate()
}
}
The #ObservedObject works only inside SwiftUI view. In this case it is possible to observe published property directly via Publisher, like
import Combine
class ViewController: UIViewController {
let stopWatch = StopWatch()
#IBOutlet var label: UILabel!
private var cancellable: AnyCancellable!
override func viewDidLoad() {
super.viewDidLoad()
cancellable = stopWatch.$counter.sink { [weak self] newValue in
self?.label.text = "\(newValue)"
}
}
// ... other code
Related
I am quite new to Swift programming and I am trying to set a slider min, max and value in a NSToolbar. As an hypothetical exemple, I have a list of client and I want to use the slider in the toolbar to select a client data page. I will firt to load the client database in the NSViewController and count the number of client. Than I would like to set the slider in the toolbar minvalue to 1 and maxvalue to the number of client. I understand how to send slider values from the Windowcontroller to the ViewController but I did not found how to do the inverse , how to send data from the Viewcontroller to the Window controller in order to set the slider values.
I have attach an simple code based on this exemple https://github.com/gbdavid2/DavidCodes_macOS/tree/master/NSToolbar%20with%20Storyboards/NSToolbar%20with%20Storyboards
In this exemple, the Toolbar shows a Previous and an Next button that , when clicked, they change a counter value (count). I would like to send back that value from the ViewCoOntroller to the WindowController in order to display it in label and eventually, the slider value in the toolbar. Thanks for your help.
// WindowController.swift
import Cocoa
class WindowController: NSWindowController {
#IBOutlet weak var myBoutton: NSToolbarItem!
var viewController: ViewController {
get {
return self.window!.contentViewController! as! ViewController
}
}
override func windowDidLoad() {
super.windowDidLoad()
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
//viewController.myLabel.stringValue = "boo"
}
#IBAction func previous(_ sender: Any) {
viewController.updateMyLabelText(newText: "Prev Button clicked! ")
}
#IBAction func next(_ sender: Any) {
viewController.updateMyLabelText(newText: "Next Button clicked! ")
}
}
import Cocoa
class ViewController: NSViewController {
var count : Int = 0
#IBOutlet weak var myLabel: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func updateMyLabelText(newText: String){
if newText.contains("Prev") {count -= 1}
else if newText.contains("Next") {count += 1}
myLabel.stringValue = newText + String(count)
}
}
Another way to to achieve this is with Cocoa Bindings. Example:
In the toolbar are a Previous button, a Next button and a slider. The actions of the buttons are connected to the First Responder. The action methods are implemented in ViewController. The count property of ViewController has attributes #objc dynamic so it can be used with Cocoa Bindings.
class ViewController: NSViewController {
#objc dynamic var count: Int = 0
#IBAction func previous(_ sender: Any) {
count -= 1
}
#IBAction func next(_ sender: Any) {
count += 1
}
}
The slider in the toolbar is bound to the Window Controller, key path window.contentViewController.count.
In the view is a label with a number formatter. The value of the label is bound to the View Controller, key path count.
The window controller isn't subclassed.
There are multiple ways to achieve this.
One of the way is by creating a class [e.g: SliderManager] which keep tracks of current value and handles increment/decrement. You can get the current value of Slider with the help of Singleton in any Controller.
Here is an example implementation:
protocol SliderCountDelegate: NSObject {
func counterDidUpdate()
}
final class SliderCountManager {
static let shared = SliderCountManager()
var value: UInt8 = 0 // <-- Unsigned Integers: Only Positive numbers
weak var delegate: SliderCountDelegate?
public func increaseCounter() {
value += 1
delegate?.counterDidUpdate()
}
public func decreaseCounter() {
value -= 1
delegate?.counterDidUpdate()
}
}
Here is how you should use this in your code:
// WindowController.swift
import Cocoa
class WindowController: NSWindowController {
#IBOutlet weak var myBoutton: NSToolbarItem!
override func windowDidLoad() {
super.windowDidLoad()
}
#IBAction func previous(_ sender: Any) {
SliderCountManager.shared.increaseCounter()
print(SliderCountManager.shared.value) // <- Accessing Current value here
}
#IBAction func next(_ sender: Any) {
SliderCountManager.shared.decreaseCounter()
print(SliderCountManager.shared.value)
}
}
import Cocoa
class ViewController: NSViewController, SliderCountDelegate {
#IBOutlet weak var myLabel: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
SliderCountManager.shared.delegate = self // Set Delegate to `self`
}
override var representedObject: Any? {
didSet {
}
}
// Protocol conformance
func counterDidUpdate() {
myLabel.stringValue = String(SliderCountManager.shared.value)
}
}
Thanks for the proposed solutions. It certainly put me in the wrigth direction.
Here is what I did. In the WindowController , I set a toolbar with 1) button «previous», 2) button «next» and 3) a slider «slider».
Those are linked to the proper IBOutler and IBaction in the WindowController.
The viewController have a textLabel «myLabel»
The 2 buttons and the slider change the slider_ptr value in the ViewControler and is sent to myLabel. Also, the slider.label change according to the slider_pointer and the slider_max values. Here is the code for the windowController:
import Cocoa
class WindowController: NSWindowController {
#IBOutlet weak var slider: NSSlider!
#IBOutlet weak var sliderTB: NSToolbarItem!
var viewController: ViewController {
get {
return self.window!.contentViewController! as! ViewController
}
}
override func windowDidLoad() {
super.windowDidLoad()
setSlider() // set initial value based on ViewController
}
#IBAction func previous(_ sender: Any) {
viewController.previous (WindowController())
setSlider()
}
#IBAction func next(_ sender: Any) {
//viewController.updateMyLabelText(newText: "Prev Button clicked! ")
viewController.next (WindowController()) //send to VC function previous
// let pt = viewController.slider_ptr + 1
//let sMax = viewController.slider_max
setSlider()
//sliderTB.label = String(pt) + " de " + String(sMax)
}
#IBAction func sliderDidChange(_ sender: Any) {
viewController.sliderDidSlide (WindowController(), pointer: Int(slider.doubleValue))
setSlider()
// viewController.sliderDidSlide(PosiWC(), sValue: myslider.doubleValue)
}
func setSlider() {
/* myslider.minValue = 1
myslider.maxValue = Double(max)
myslider.integerValue = pointer*/
//print ("WCP58:" , myslider.integerValue )
let pt = viewController.slider_ptr
let sMax = viewController.slider_max
//slider (max : pt, pointer: sMax)
sliderTB.label = String(pt) + " de " + String(sMax)
slider.minValue = 1
slider.maxValue = Double(sMax)
slider.integerValue = pt
}
}
and for the Viewcontroller :
class ViewController: NSViewController {
var slider_ptr = 1 // slider position
var slider_max: Int = 0 //
#IBOutlet weak var myLabel: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
slider_max = 250
myLabel.stringValue = String(slider_ptr)
}
override var representedObject: Any? {
didSet {
}
}
func previous(_ sender: Any) {
if slider_ptr > 1 {
slider_ptr -= 1
}
else { NSSound.beep()}
myLabel.stringValue = String(slider_ptr)
}
func next(_ sender: Any) {
if slider_ptr < slider_max {
slider_ptr += 1
}
else { NSSound.beep()}
myLabel.stringValue = String(slider_ptr)
}
func sliderDidSlide(_ sender: Any, pointer : Int) {
print (pointer)
slider_ptr = pointer
myLabel.stringValue = String(slider_ptr)
}
}
I have a label in first view controller ViewController, and a func getting date avery second in second vc. I'd like to update label in first after timer starts in second. is it good to use protocol-delegate pattern? at this moment it is not working, time is going but not updating the view in first VC
my struct for protocol
protocol ViewControllerDelegate: class {
func changeLabelText(textToPass: String)
}
in first viewController
class ViewController: UIViewController, ViewControllerDelegate {
#IBOutlet weak var mainLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
func changeLabelText(textToPass: String) {
self.mainLabel.text = textToPass
self.view.layoutIfNeeded()
}
#IBAction func buttonTapped(_ sender: UIButton) {
let nextVC = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
nextVC.delegateSubscriber = self
present(nextVC, animated: true, completion: nil)
}
}
in secondVC
class SecondViewController: UIViewController {
//MARK: speed timer feature 1/3
private weak var timer: Timer?
private var timerDispatchSourceTimer : DispatchSourceTimer?
weak var delegateSubscriber : ViewControllerDelegate?
#IBOutlet weak var myTxtField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
startTimer(every: 1)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("appeared")
stopTimer()
}
private func startTimer(every timeInterval: TimeInterval) {
if #available(iOS 10.0, *) {
timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) { [weak self] _ in
let dateToPass = Date().description
print(dateToPass)
self?.delegateSubscriber?.changeLabelText(textToPass: dateToPass)
}
}
}
//MARK: speed timer feature 3/3
private func stopTimer() {
timer?.invalidate()
//timerDispatchSourceTimer?.suspend() // if you want to suspend timer
timerDispatchSourceTimer?.cancel()
}
#IBAction func buttonTapped(_ sender: UIButton) {
// delegateSubscriber?.changeLabelText(textToPass: self.myTxtField.text ?? "error")
dismiss(animated: true, completion: nil)
}
}
Just remove [weak self] from Timer closure
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
let dateToPass = Date().description
print(dateToPass)
self.delegateSubscriber?.changeLabelText(textToPass: dateToPass)
}
... then self isn't optional
I have two ViewControllers, FirstViewController and SecondViewController.
And there is UISlider on SecondViewController. There is an alarm on FirstViewController.
Now I want to transfer UISlider value, which is volume to alarm on FirstViewController.
ps. The function what I want to make is totally same as default iPhone setting slider.
I would be so happy so if you guys give me your knowledge, please.
import UIKit
import AVFoundation
class FirstViewController: UIViewController, AVAudioPlayerDelegate,
UITableViewDelegate, UITableViewDataSource{
let TODO = ["A", "B", "C"]
let notificationCenter = NotificationCenter.default
var volume = Float()
var counter = 0
var timer = Timer()
var startTime:Double = 0.0
var audioPlayer: AVAudioPlayer!
#IBOutlet weak var tableView: UITableView!
#IBAction func firstSwitch(_ sender: UISwitch)
{
if (sender).isOn
{
timer = Timer.scheduledTimer(withTimeInterval: 1 * 1, repeats: false, block: { timer in
self.audioPlayer.play()
self.audioPlayer.numberOfLoops = -1
print(self.audioPlayer.isPlaying)
})
}else{
timer.invalidate()
print("switch1stopped")
self.audioPlayer.stop()
}
}
notificationCenter.addObserver(self, selector: #selector(catchNotification(notification:)), name: NSNotification.Name(rawValue: "test"), object: nil)
}
#objc func catchNotification(notification: Notification) -> Void {
print("Catch notification")
audioPlayer.volume = volumeChane.value
//Use of unresolved identifier 'volumeChane'
}
///////////////////////////////////////////////////////////////////////////
import UIKit
import AVFoundation
class SecondViewController: UIViewController {
var audioPlayer: AVAudioPlayer!
let notificationCenter = NotificationCenter.default
#IBOutlet weak var volumeSlider: UISlider!
#IBOutlet weak var volumeLabel: UILabel!
#IBAction func volumeChange(_ sender: UISlider)
{
volumeLabel.text = String(Int(sender.value))
volumeSlider.value = sender.value
audioPlayer.volume = volumeSlider.value
notificationCenter.post(name: NSNotification.Name(rawValue: "test"), object: nil)
audioPlayer.play()
}
override func viewDidLoad()
{
super.viewDidLoad()
if let url=Bundle.main.url(forResource:"Alarm",withExtension:".mp3" )
{
do {
audioPlayer = try AVAudioPlayer(contentsOf:url)
audioPlayer?.play(atTime:1 * 10)
}catch{
audioPlayer = nil
}
}else{
fatalError("Url is nil")
}
}
extension Notification.Name
{
static let myNotificationName = Notification.Name("test")
}
Using NSNotificationCenter for a simple task such as this might be an overkill. You usually use closures to catch data changes in your components or view controllers.
In the SecondViewController create a variable containing a closure:
var onVolumeChange: ((value: Float) -> Void)?
Call it in the IBAction that monitors slider's onChange event.
#IBAction func volumeChange(_ sender: UISlider)
{
self.onVolumeChange?(sender.value)
}
Pass the onVolumeChange closure from the FirstViewController when navigating to the second one. I'm not sure how you perform navigation so I'll assume you do it programmatically.
let vc = UIStoryboard(name: "main", bundle: nil).instantiateViewController(withIdentifier: "SecondViewController")
vc.onVolumeChange = { value in
audioPlayer.volume = value
}
self.navigationController?.pushViewController(vc, animated: true)
How to get the values while moving the UISlider?
I'm using the following code:
ViewModel:
import Foundation
import RxSwift
final class ViewModel {
private let disposeBag = DisposeBag()
var value: Variable<Float>
init() {
self.value = Variable(Float(0.0))
}
}
ViewController:
#IBOutlet var slider: UISlider!
private var viewModel: ViewModel!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
viewModel = ViewModel()
slider.rx.value
.subscribe(onNext: { (value) in
self.viewModel.value = Variable(Float(value))
})
.addDisposableTo(disposeBag)
}
But this code does not work. What's my mistake?
You're replacing the Variable instead of inserting a new value into it. This is guaranteed to fail.
ViewModel.value should be a let instead of a var. You don't want to replace Variable, you want to assign a new value into it. While you are at it, make your ViewModel a struct:
struct ViewModel {
let value = Variable<Float>(0)
}
It can be a final class if you must, but value should still be a let not a var.
Your viewDidLoad should look like this:
public override func viewDidLoad() {
super.viewDidLoad()
slider.rx.value
.subscribe(onNext: { value in
self.viewModel.value.value = value
})
.disposed(by: disposeBag)
}
Or better yet:
public override func viewDidLoad() {
super.viewDidLoad()
slider.rx.value
.bind(to: viewModel.value)
.disposed(by: disposeBag)
}
Or even better... Whatever is subscribing to ViewModel.value should subscribe/bind directly to slider.rx.value instead. That way you can get rid of the middleman.
Something like this:
public class ViewController: UIViewController {
#IBOutlet weak var slider: UISlider!
#IBOutlet weak var label: UILabel!
private let disposeBag = DisposeBag()
public override func viewDidLoad() {
super.viewDidLoad()
slider.rx.value
.map { "The slider's value is \($0)" }
.bind(to: label.rx.text)
.disposed(by: disposeBag)
}
}
You will see the label's text change as you move the slider.
Not tested, but I would try:
override func viewDidLoad() {
viewModel = ViewModel()
slider.rx.value
.subscribe(onNext: { (value) in
self.viewModel.value.value = Float(value)
})
.addDisposableTo(disposeBag)
}
Also I would rename your value property in your viewModel to sliderValue (or whatever, but not value). If you do this, your code will look better:
self.viewModel.sliderValue.value = Float(value)
instead of
self.viewModel.value.value = ...
If you use new BehaviorRelay instead of old Variable:
struct MyViewModel {
let value = BehaviorRelay<Float>(value: 0)
}
class ViewController: UIViewController {
#IBOutlet var slider: UISlider!
#IBOutlet var valueLabel: UILabel!
private var viewModel = MyViewModel()
override func viewDidLoad() {
super.viewDidLoad()
slider.rx.value
.bind(to: viewModel.value)
.disposed(by: rx.disposeBag)
// If you want to listen and bind to a label
viewModel.value.asDriver()
.map { "Value: \($0 * 100)%" }
.drive(valueLabel.rx.text)
.disposed(by: rx.disposeBag)
}
}
I have two view controllers. I also have a universal variable called number. The first view controller has a label on it called mainLabel. My second view controller has a button on it. When the button is pressed it should subtract 200 from the variable number then update the mainLabel label. I can not figure out how to make mainLabel a label that works on the second view controller too.
First View Controller
import UIKit
var number:Int = 0
class ViewController: UIViewController {
#IBOutlet weak var mainLabel: UILabel!
#IBAction func backgroundButton(sender: AnyObject) {
number = number + 1
mainLabel.text = "\(number)"
NSUserDefaults.standardUserDefaults().setObject(number, forKey: "number")
}
override func viewDidLoad() {
super.viewDidLoad()
if NSUserDefaults.standardUserDefaults().objectForKey("number") != nil {
number = NSUserDefaults.standardUserDefaults().objectForKey("number") as! Int
}
mainLabel.text = "\(number)"
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Second View Controller
import UIKit
class SecondViewController: UIViewController {
#IBOutlet weak var buy1Label: UILabel!
#IBAction func buy1(sender: AnyObject) {
number = number - 200
buy1Label.text = "Bought!"
mainLabel.text = "\(number)"
}
}
class ViewController: UIViewController {
#IBOutlet weak var mainLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
NSNotificationCenter.defaultCenter().addObserverForName("NSNotificationName", object: nil, queue: nil) { (note) -> Void in
// Number changed, update your UILabel.
var number: Int = NSUserDefaults.standardUserDefaults().integerForKey("keyOfNumber")
self.mainLabel.text = "\(number)"
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func subtractNumberBy200() {
var number: Int = NSUserDefaults.standardUserDefaults().integerForKey("keyOfNumber")
number -= 200
NSUserDefaults.standardUserDefaults().setInteger(number, forKey: "keyOfNumber")
}
}
Hope this will works for you.
Wow, this question cuts to the core of what I have to deal with every day. Here is my first take on how to do this with notifications.
First, setup some common global things.
let NumberDidChangeNote = "NumberDidChangeNote" // A name for the notification.
// A function to get the value of number the same way every time.
func number() -> Int {
return NSUserDefaults.standardUserDefaults().integerForKey("number");
}
// A function to set the value of number the same way every time.
func setNumber(number: Int) {
NSUserDefaults.standardUserDefaults().setInteger(number, forKey: "number")
NSUserDefaults.standardUserDefaults().synchronize()
// Post a number did change notification
NSNotificationCenter.defaultCenter().postNotificationName(NumberDidChangeNote, object: nil)
}
The first view controller splits the roles of setting number and setting the label.
class ViewController: UIViewController {
#IBOutlet weak var mainLabel: UILabel!
#IBAction func backgroundButton(sender: AnyObject) {
setNumber(number() + 1)
}
dynamic func numberDidChange(note: NSNotification) {
mainLabel.text = "\(number())"
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Add an observer which will call numberDidChange() anytime the number changes.
let center = NSNotificationCenter.defaultCenter()
center.addObserver(self, selector: "numberDidChange", name: NumberDidChangeNote, object: nil)
// Fake an initial notification to numberDidChange() to set the initial value of the label.
numberDidChange(NSNotification(name: NumberDidChangeNote, object: nil))
}
override func viewWillDisappear(animated: Bool) {
// Remove the observer when not on the screen.
let center = NSNotificationCenter.defaultCenter()
center.removeObserver(self, name: NumberDidChangeNote, object: nil)
super.viewWillDisappear(animated)
}
}
Now the second view controller doesn't have to worry about the first view controller at all.
class SecondViewController: UIViewController {
#IBOutlet weak var buy1Label: UILabel!
#IBAction func buy1(sender: AnyObject) {
setNumber(number() - 200)
buy1Label.text = "Bought!"
}
}