How do you get a reference property to trigger a property observer?
In order to demonstrate my problem I wrote a simple MVC program with one button and one label. The button increments a counter in the model and displays the value of the counter in the label in the view controller.
The problem is that the counter increment (in the model) does not trigger the didSet observer ( in the view controller)
Here is the model file:
import Foundation
class MvcModel {
var counter: Int
var message: String
init(counter: Int, message: String) {
self.counter = counter
self.message = message
}
}
// create instance
var model = MvcModel(counter: 0, message: "" )
// counting
func incrementCounter() {
model.counter += 1
model.message = "Counter Value: \(model.counter)"
//print(model.message)
}
Here is the view controller file:
import Cocoa
class ViewController: NSViewController {
let model1 = model
var messageFromModel = model.message {
didSet {
updateDisplayCounterLabel()
}
}
// update Label
func updateDisplayCounterLabel() {
DisplayCounterLabel.stringValue = model1.message
}
// Label
#IBOutlet weak var DisplayCounterLabel: NSTextField! {
didSet {
DisplayCounterLabel.stringValue = "counter not started"
}
}
// Button
#IBAction func IncrementButton(_ sender: NSButton) {
incrementCounter()
print("IBAction: \(model1.message)")
}
}
I guess the problem is linked to reference property (as I have been able to make this program work with a model based on a struct).
I would appreciate if someone could tell me how to deal with property observers and reference property and make this kind of MVC work as I plan to use it in real programs.
You could create a delegate for MvcModel
protocol MvcModelDelegate {
func didUpdateModel(counter:Int)
}
next you add a delegate property to MvcModel
class MvcModel {
var counter: Int {
didSet {
delegate?.didUpdateModel(counter: counter)
}
}
var message: String
var delegate: MvcModelDelegate?
init(counter: Int, message: String) {
self.counter = counter
self.message = message
}
}
then you make the ViewController class conform to MvcModelDelegate and finally you set model.delegate = self into the viewDidLoad
class Controller: UIViewController, MvcModelDelegate {
let model = MvcModel(counter: 0, message: "hello")
override func viewDidLoad() {
super.viewDidLoad()
self.model.delegate = self
}
func didUpdateModel(counter: Int) {
print("new value for counter \(counter)")
}
}
In case someone is interested here is the code as suggested by Noam.
Model File:
import Foundation
protocol MvcModelDelegate {
func didUpDateModel(message: String)
}
class MvcModel {
var counter: Int
var message: String {
didSet {
delegate?.didUpDateModel(message: message)
}
}
var delegate: MvcModelDelegate?
init(counter: Int, message: String) {
self.counter = counter
self.message = message
}
}
// create instance
var model = MvcModel(counter: 0, message: "" )
// counting
func incrementCounter() {
model.counter += 1
model.message = "Counter Value: \(model.counter)"
}
}
View Controller File
import Cocoa
class ViewController: NSViewController, ModelDelegate {
// communication link to the model
var model1 = model
var messageFromModel = messageToLabel
override func viewDidLoad() {
super.viewDidLoad()
self.model1.delegate = self
}
// update display
func didUpdateModel(message: String) {
//self.Label1.stringValue = model1.message
self.Label1.stringValue = messageFromModel
}
// Label
#IBOutlet weak var Label1: NSTextField! {
didSet {
Label1.stringValue = " counter not started"
}
}
// Button
#IBAction func testButton(_ sender: NSButton) {
incrementCounter()
}
}
Related
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var chargeLabel: UILabel!
#IBOutlet weak var bigDogLabel: UILabel!
#IBOutlet weak var cafeImageView: UIImageView!
override func viewDidLoad() {
}
struct DogCafe{
var name: String
var charge: Int
var bigDog: Bool
var cafeImage: UIImage
init(name: String, charge: Int, bigDog: Bool, cafeImage: UIImage) {
self.name = name
self.charge = charge
self.cafeImage = cafeImage
self.bigDog = bigDog
}
func message() {
nameLabel.text = "카페 \(name) 입니다"
chargeLabel.text = "입장료는 \(charge) 입니다"
cafeImageView.image = cafeImage
if bigDog == true {
bigDogLabel.text = "대형견 출입이 가능한 매장입니다"
} else {
bigDogLabel.text = "대형견 출입이 불가능한 매장입니다"
}
}
}
#IBAction func ohdodokButton(_ sender: UIButton) {
let dogCafe = DogCafe(name: "카페 오도독", charge: 3500, bigDog: true, cafeImage: #imageLiteral(resourceName: "ohdodok"))
dogCafe.message()
}
#IBAction func meltingButton(_ sender: UIButton) {
let dogCafe2 = DogCafe(name: "멜팅", charge: 5000, bigDog: false, cafeImage: #imageLiteral(resourceName: "melting"))
dogCafe2.message()
}
}
func message() {
nameLabel.text = "카페 \(name) 입니다"
chargeLabel.text = "입장료는 \(charge) 입니다"
cafeImageView.image = cafeImage
if bigDog == true {
bigDogLabel.text = "대형견 출입이 가능한 매장입니다"
} else {
bigDogLabel.text = "대형견 출입이 불가능한 매장입니다"
}
Instance member 'nameLabel' of type 'ViewController' cannot be used on instance of nested type 'ViewController.DogCafe'
Instance member 'chargeLabel' of type 'ViewController' cannot be used on instance of nested type 'ViewController.DogCafe'
Instance member 'cafeImageView' of type 'ViewController' cannot be used on instance of nested type 'ViewController.DogCafe'
Instance member 'bigDogLabel' of type 'ViewController' cannot be used on instance of nested type 'ViewController.DogCafe'
you can use like below
#IBAction func ohdodokButton(_ sender: UIButton) {
let dogCafe = DogCafe(name: "카페 오도독", charge: 3500, bigDog: true, cafeImage: #imageLiteral(resourceName: "ohdodok"))
setCurrentDogAttributes(currentDog: dogCafe)
}
#IBAction func meltingButton(_ sender: UIButton) {
let dogCafe2 = DogCafe(name: "멜팅", charge: 5000, bigDog: false, cafeImage: #imageLiteral(resourceName: "melting"))
setCurrentDogAttributes(currentDog: dogCafe)
}
func setCurrentDogAttributes(currentDog:DogCafe){
nameLabel.text=currentDog.name
//Set your Values Here
}
Your approach is wrong. You need to read up on separation of concerns. I'd also suggest reading up on the MVC (Model View Controller) design pattern, which is a pretty common way to do development on iOS.
In your code, DogCafe is a model object. It contains data that you want your app to manipulate and display.
A model object should not know anything about how it is being used. It just holds data.
Your view controller should take the model object and display it to its views. One way to handle that is to add a display(cafe:) method to your view controller:
class ViewController: UIViewController {
// The rest of your view controller variables and functions would go here...
func display(cafe: DogCafe) {
nameLabel.text = "카페 \(cafe.name) 입니다"
chargeLabel.text = "입장료는 \(cafe.charge) 입니다"
cafeImageView.image = cafe.cafeImage
if cafe.bigDog == true {
bigDogLabel.text = "대형견 출입이 가능한 매장입니다"
} else {
bigDogLabel.text = "대형견 출입이 불가능한 매장입니다"
}
}
}
I'm attempting to initialize this ViewController class. I am not using the MVC design strategy so ignore the bad conventions used (if any).
How do I initialize this class properly?
Error: 'required' initializer 'init(coder:)' must be provided by subclass of 'UIViewController'
Context: This is a calculator app that when any of the buttons are pressed. It will go find the senders title and simply put if one of the three vars are nil, it will store it in that optional.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBOutlet weak var answerLabel: UILabel!
//global vars for all funcs
var selection1: Int? {
didSet { answerLabel.text = String(selection1!) }
}
var selection2: String? {
didSet { answerLabel.text = selection2! }
}
var selection3: Int? {
didSet { answerLabel.text = String(selection3!) }
}
var answer: Int {
didSet { answerLabel.text = String(answer) }
}
init() {
}
#IBAction func touchButton(_ sender: UIButton) {
if selection1 == nil {
selection1 = Int(sender.currentTitle!)
print("Selection set in first pos.")
} else if selection2 == nil {
selection2 = sender.currentTitle
} else if selection3 == nil {
selection3 = Int(sender.currentTitle!)
} else {
calculate(firstNum: selection1!, operation: selection2!, secondNum: selection3!)
}
}
func calculate(firstNum: Int, operation: String, secondNum: Int) {
switch operation {
case "+":
answer = firstNum + secondNum
case "-":
answer = firstNum - secondNum
case "x":
answer = firstNum * secondNum
case "/":
answer = firstNum / secondNum
default:
answerLabel.text = "Something went wrong!"
}
}
}
Initialization depends on a couple of condition.
If you are using storyboard, you can just remove the init and your VC will have default initializer. Make sure either all of your properties have default value or they are optional.
If you are using xib or just creating view programmatically you can have custom convenience initializer where you pass some extra data this way.
class MyViewController: ViewController {
var answer: Int
convenience init(answer: Int) {
self.init()
self.answer = answer
// Do other setup
}
}
Your controller is being instantiated from the storyboard. A safe place to configure initial views is during the controller's call to viewDidLoad, ie:
override func viewDidLoad() {
super.viewDidLoad()
// configure your views and subviews here
}
I'm trying to bind a UITextField to a viewModel, however whatever i do i keep getting Cannot invoke 'bind' with an argument list of type '(to: EmailViewModel). What am i doing wrong?
SignUpViewModel
class SignUpViewModel {
let model: SignUpModel
private let disposeBag = DisposeBag()
let emailFieldViewModel = EmailViewModel()
init(model :SignUpModel) {
self.model = model
}
}
EmailViewModel
struct EmailViewModel : FieldViewModel {
var value: Variable<String> = Variable("")
var errorValue: Variable<String?> = Variable(nil)
let title = "Email"
let errorMessage = "Email is wrong"
func validate() -> Bool {
let emailPattern = "[A-Z0-9a-z._%+-]+#([A-Za-z0-9.-]{2,64})+\\.[A-Za-z]{2,64}"
guard validateString(value.value, pattern:emailPattern) else {
errorValue.value = errorMessage
return false
}
errorValue.value = nil
return true
}
}
viewcontroller
class SignUpViewController: UIViewController {
#IBOutlet var emailField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
configureBinding()
}
private func configureBinding() {
// binding
self.emailField.rx.text.bind(to: viewModel.emailFieldViewModel)
}
}
The bind function expects an object that conforms to the ObserverType protocol. Here, EmailViewModel does not conform to that type, hence the error.
Writing an extension to make EmailViewModel conform to the ObserverType protocol would solve the compilation error.
extension EmailViewModel: ObserverType {
func on(_ event: Event<String?>) {
switch event {
case .next(let newValue): value.value = newValue ?? ""
case .error(_), .completed: fatalError("Completion and error are not handled")
}
}
}
I have a WKCrownSequencer that triggers an action in my pushed interface controller and the first time through everything works fine. When I go back to root interface controller regardless of the method (pop or reloadRootcontrollers) the digital crown no longer works in first interface controller nor the second one. The StartInterfaceController is the rootInterfaceController and the MidWorkoutInterfaceController is the pushed one.
import WatchKit
import Foundation
class StartInterfaceController:
WKInterfaceController,CLLocationManagerDelegate {
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
#IBAction func start() {
WKInterfaceController.reloadRootControllers(
withNames: ["midWorkout"], contexts: []
)
}
The second interface controller is below.
import WatchKit
import Foundation
class MidWorkoutInterfaceController: WKInterfaceController, WKCrownDelegate {
override func awake(withContext context: Any?) {
super.awake(withContext: context)
print("viewdidAwake")
print("ViewWillActivate")
crownSequencer.delegate = self
crownSequencer.focus()
WKInterfaceDevice.current().play(.success)
currentPhase = 0
let workoutType = UserDefaults.standard.object(forKey: "CurrentType") as? [String] ?? [ "Swimming", "T1"]
orderOfEventsSetter = workoutType
updateCurrentPhase()
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
crownSequencer.focus()
}
var clockTimer: Timer!
func workoutStarted(){
clockTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
self?.totalTimeOutlet.setText( String(describing: -1 * Int(self!.startDate!.timeIntervalSinceNow)))
self?.splitTimeOutlet.setText( String(describing: -1 * Int(self!.currentStartDate!.timeIntervalSinceNow)))
}
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
var startDate: Date?
var currentStartDate: Date?
//Outlets
#IBOutlet var currentPhaseOutlet: WKInterfaceLabel!
#IBOutlet var totalTimeOutlet: WKInterfaceLabel!
#IBOutlet var splitTimeOutlet: WKInterfaceLabel!
#IBOutlet var currentPaceOutlet: WKInterfaceLabel!
#IBOutlet var totalDistanceOutlet: WKInterfaceLabel!
var orderOfEventsSetter: Array<String>{
get{
return orderOfEvents
}
set{
var tempArray = ["GPS Locking In"]
for phase in newValue {
tempArray.append(phase)
}
orderOfEvents = tempArray
}
}
private var orderOfEvents: [String] = []
var currentPhase = 0 {
didSet{
if !orderOfEvents.isEmpty {
updateCurrentPhase()
}
}
}
func updateCurrentPhase(){
currentPhaseOutlet.setText(orderOfEvents[currentPhase])
}
//timing for location requests
//Corelocation Section
//CoreMotion Section
///crown control
var currentDialRotation = 0.0
let dialRotationRange = Range(uncheckedBounds: (lower: -Double.pi / 4, upper: Double.pi / 4))
let constantForTimer: TimeInterval = 0.1
var justTransitioned = false
func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
currentDialRotation += rotationalDelta
if !dialRotationRange.contains(currentDialRotation){
currentDialRotation = 0.0
justTransitioned = true
makeTransition()
//make so two transitions cannot happen right after each other
}
print(currentDialRotation)
}
func crownDidBecomeIdle(_ crownSequencer: WKCrownSequencer?) {
print(String(describing: orderOfEvents[currentPhase]))
print("crown stopped")
}
func makeTransition(){
print(currentPhase)
print(orderOfEvents.count)
if (currentPhase) == orderOfEvents.count - 1 {
endWorkout()
}
else if (currentPhase == 0){
WKInterfaceDevice.current().play(.start)
let dateFormat = DateFormatter()
dateFormat.dateFormat = "mm/dd/yyyy"
startDate = Date()
currentStartDate = Date()
workoutStarted()
currentPhase += 1
}
else{
WKInterfaceDevice.current().play(.start)
print("transitioning to " + String(describing: orderOfEvents[currentPhase + 1]))
currentStartDate = Date()
stopTimers()
currentPhase += 1
}
}
#IBAction func endWorkoutButton() {
endWorkout()
}
func endWorkout(){
stopTimers()
clockTimer.invalidate()
alerts()
}
func alerts(){
let saveAction = WKAlertAction(title: "Save",
style: WKAlertActionStyle.default) {
self.goToStartScreen()
}
let discardAction = WKAlertAction(title: "Discard Workout",
style: WKAlertActionStyle.cancel) {
self.goToStartScreen()
}
presentAlert(withTitle: "Workout Complete",
message: "Would you like to save the workout?",
preferredStyle: WKAlertControllerStyle.alert,
actions: [saveAction, discardAction])
}
func goToStartScreen(){
crownSequencer.resignFocus()
self.popToRootController()
}
func stopTimers(){
if orderOfEvents[currentPhase] == "Running"{
}
if orderOfEvents[currentPhase] == "Biking"{
}
if currentPhase == orderOfEvents.count {
clockTimer.invalidate()
}
}
}
According to Apple's document here:
"...Only one object in your interface can have focus at any given time, so if your interface also contains picker objects or has scrollable scenes, you must coordinate changes in focus accordingly. For example, calling the sequencer's focus method causes any picker objects or interface controllers to resign focus. When the user taps on a picker object, the currently active sequencer resigns focus, and the selected picker object gains the focus... "
And you are to lose the focus at any time unpredictable...
"...If the user taps a picker object or a scrollable scene in your interface, the system automatically removes the focus from any active crown sequencer..."
My Object:
//Object to observe
struct Text {
let savedUserHeader: String
let savedUserText: String
}
ClassA where I create the object:
//First Class
class A {
func somefunc(){
let a = Text(savedUserHeader: "testHeader", savedUserText: "testText")
}
}
In classB, I want to observe if a new object was created:
//Second Class
class B {
var text: Text? {
didSet{
headerlabel.text = text.savedUserHeader
saveUserLabel.text = text?.savedUserText
}
}
}
Delegate
You can use the Delegate pattern to observe, let create a protocol called ADelegate:
protocol ADelegate {
func didCreateText(text: Text)
}
Then, add a variable called delegate in the class A and pass a Text object into didCreateText(text:) method in the somefunc():
//First Class
class A {
var delegate: ADelegate?
func somefunc(){
let a = Text(savedUserHeader: "testHeader", savedUserText: "testText")
delegate?.didCreateText(text: a)
}
}
Next, when you create an object A in B, set a.delegate = self and implement ADelegate protocol:
//Second Class
class B: ADelegate {
var headerlabel: UILabel!
var saveUserLabel: UILabel!
var a = A()
var text: Text? {
didSet{
headerlabel.text = text?.savedUserHeader
saveUserLabel.text = text?.savedUserText
}
}
init() {
a.delegate = self
}
func didCreateText(text: Text) {
print("savedUserHeader: \(text.savedUserHeader)")
print("savedUserText: \(text.savedUserText)")
}
}
That's it! The method didCreateText(text:) will be called when an object Text is created in the somefunc() method:
let b = B()
b.a.somefunc()
Notification Center
Another solution is NotificationCenter. Let post a notification when create a Text object:
func somefunc(){
let a = Text(savedUserHeader: "testHeader", savedUserText: "testText")
NotificationCenter.default.post(name: Notification.Name("addText"), object: a)
}
And observe in the class B:
init() {
NotificationCenter.default.addObserver(self, selector: #selector(observeText(noti:)), name: Notification.Name("addText"), object: nil)
}
#objc func observeText(noti: Notification) {
if let text = noti.object as? Text {
print("savedUserHeader: \(text.savedUserHeader)")
print("savedUserText: \(text.savedUserText)")
}
}
Let test it:
let b = B()
let a = A()
a.somefunc()
You will see the result:
savedUserHeader: testHeader
savedUserText: testText