Combine Framework Update UI doesn't work properly - ios

I want to try Combine framework, very simple usage, press a UIButton, and update UILabel.
My idea is:
Add a publisher
#Published var cacheText: String?
Subscribe
$cacheText.assign(to: \.text, on: cacheLabel)
assign a value when button pressed.
cacheText = "testString"
Then the label's text should be updated.
The problem is when the button pressed, the #Published value is updated, but the UILabel value doesn't change.
e.g the cacheLabel1 was assigned 123 initially but not 789 when button pressed.
Here's the full code:
ViewModel.swift
import Foundation
import Combine
class ViewModel {
#Published var cacheText: String?
func setup(_ text: String) {
cacheText = text
}
init() {
setup("123")
}
}
ViewController.swift
class ViewController: UIViewController {
#IBOutlet weak var cacheLabel: UILabel!
var viewModel = ViewModel()
#IBAction func buttonPressed(_ sender: Any) {
viewModel.setup("789")
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$cacheText.assign(to: \.text, on: cacheLabel)
}
}
Not sure if I missed something, thanks for the help.

The pipeline is dying before you have a chance to tap the button. You have to preserve it, like this:
var storage = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$cacheText.assign(to: \.text, on: cacheLabel).store(in: &storage)
}

Related

Swift4- insert a string of text into textview field in another view/class

I am recreating a simple app that is a paramedic case logger, effectively it is a bunch of buttons or textfields that get their .text value time stamped and pasted into a UItextView field to create a chronological list of action to refer back to when a paramedic writes up their patient notes.
I have created a tab bar controlled app using the Xcode template.
I have created the UIbuttons and UItextfields that create and paste the string into a test UItextview on the same field.
I have created a func that creates the time stamp string
I run into trouble getting the same string to show on the UItextview on the 2nd view.
RESEARCH
As I am only new to this I have read lots of posts on here with user defaults, segues, delegates/protocols and cannot seem to effectively implement these in my specific circumstance (still not super sure I understand those enough yet to do it).
I have got the point that I have saved the string into a user.default value and then I can use that value in the 2nd view with a func to insert the text and clear the user.default value but I cannot seem to get the function to run from the first view? (get the encounter nil value error).
I feel like this should be a pretty simple process, so not getting it to function is very frustrating...
QUESTION
So I was hoping someone might be able to demo the simple way to take a UItextField.text input from FirstViewController and then when a UIbutton is pressed on FirstViewController pass the string to SecondViewController and insert it into a UITextView field (I don't want to display the 2nd view).
From this demo I'm hoping I can reverse engineer a solution into my code as a learning opportunity without just having my exact code handed to me on a silver platter.
import UIKit
class FirstViewController: UIViewController, UITextFieldDelegate {
// View OUTLETS
#IBOutlet weak var customNoteField: UITextField!
#IBOutlet weak var reportTextViewFieldTemp: UITextView!
#IBOutlet weak var lastEntryTV: UITextView!
#IBOutlet weak var caseIDTF: UITextField!
// Class VARIABLES
let timeButtonArray = ["blank0", "Call Received1", "Dispatched2", "Enroute3", "On Scene4", "At Patient5", "Depart Scene6", "At Hospital7", "Available8",]
// VIEW DID LOAD STUFF
override func viewDidLoad() {
super.viewDidLoad()
self.hideKeyboardWhenTappedAround()
self.customNoteField.delegate = self
}
//TIMING BUTTON PRESSED
#IBAction func timeButtonPressed(_ sender: UIButton) {
//GATHER DATA
let timingButtonInput = timeButtonArray[sender.tag]
let DTGstamp = dateTimeStamp()
//FORM STRING
let buttonData = ("\(DTGstamp) \(timingButtonInput) \n")
//SEND buttonDATA TO ReportViewController
/*??? The part I need help with*/
}
//CUSTOM ENTRY UITextfield PRESSED
#IBAction func customNoteDone(_ sender: Any) {
//GATHER DATA
let DTGstamp = dateTimeStamp()
let customNote = customNoteField.text
//FORM STRING
let customData = ("\(DTGstamp) \(customNote) \n")
//TEST FOR EMPTY TEXT
if customNote != "" {
//SEND customDATA TO ReportViewController
/*??? The part I need help with*/
}
//FUNCTION TO GENERATE DATE,TIME GROUP AS CONSTANT
func dateTimeStamp() -> String {
var date = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "ddLLLyy HH:mm:ss"
print (dateFormatter.string(from: date))
return (dateFormatter.string(from: date))
}
}
//
// ReportViewController.swift
// EMS Case Logger
//
// Created by Allan Ravenscroft on 3/3/19.
// Copyright © 2019 Allan Ravenscroft. All rights reserved.
//
import UIKit
class ReportViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var reportTextViewField: UITextView!
#IBOutlet weak var reportEmailButton: UIButton!
#IBOutlet weak var reportEditButton: UIButton!
#IBOutlet weak var reportClearButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
hideKeyboardWhenTappedAround()
reportEditButton.layer.cornerRadius = reportEditButton.frame.height / 2
reportEmailButton.layer.cornerRadius = reportEmailButton.frame.height / 2
reportClearButton.layer.cornerRadius = reportClearButton.frame.height / 2
}
//Actions when the clear button is pressed
#IBAction func clearButtonPressed(_ sender: Any) {
reportTextViewField.text = ""
}
//Actions when the edit button is pressed
#IBAction func editButtonPressed(_ sender: Any) {
}
//Actions when the email button is pressed
#IBAction func emailButtonPressed(_ sender: Any) {
showMailComposer(bodytext: reportTextViewField.text)
}
}
I know this is not a demo, but maybe can help you
Make a new NSObject file
inside the class put a new variable
import UIKit
class test: NSObject {
var a:String = ""
}
at your ViewController declare a new var referencing the class
let testVar = test()
in your customNote checker
test.a = customNote
then at your 2nd ViewController, do the same thing
let testVar = test()
testVar.a //this is your customNote String
hope this helps

Using shared classes for different views

I have an onboarding user flow:
Name -> Age -> Gender
Each of the screens shares the same structure:
Question (top)
Input (middle)
Continue (bottom)
I have a class OnboardingHelper.swift that creates a class to set the question box and continue button:
class UserOnboardingHelper{
var text: String
var questionbox: UIView
var viewController: UIViewController
var continueButton: UIButton
init(text: String, questionbox: UIView, viewController: UIViewController, continueButton: UIButton){
self.text = text
self.questionbox = questionbox
self.viewController = viewController
self.continueButton = continueButton
}
func setQuestionBox(){
//sets question box
}
func setContinueButton(){
//sets continue button
enableContinueButton()
addContinueButtonPath()
}
func enableContinueButton(){
//enables continue button
}
func disableContinueButton(){
//disables continue button
}
func addContinueButtonPath(){
//sets path of continue button based on which view
}
}
In each of the onboarding ViewControllers I am setting the class in ViewDidLoad():
class NamePageViewController: UIViewController, UITextFieldDelagate {
#IBOutlet weak var questionbox: UIView!
#IBOutlet weak var continueButton: UIButton!
#IBOutlet weak var inputLabel: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
let namePageSettings = UserOnboardingHelper(text: "What is your name", questionbox: questionbox, viewController: self, continueButton: continueButton)
namePageSettings.setQuestionBox()
namePageSettings.setContinueButton()
inputLabel.delegate = self
if nameIsFilled {
namePageSettings.enableContinueButton()
} else{
namePageSettings.disableContinueButton()
}
}
}
The issue is that in the ViewController I textFieldDidEndEditing() function which needs to call the namePageSettings class from viewDidLoad()
func textFieldDidEndEditing(_ textField: UITextField){
if (textField.text?.empty)!{
//I want to call disableContinueButton() from UserOnboardingHelper
} else {
//I want to enable enableContinueButton() from UserOnboardingHelper
}
}
Trying to understand if:
The overall approach is correct and if not, what's the best way
If the above approach is in the right direction, how should disableContinueButton() and enableContinueButton() be called?
Thanks in advance! Sorry if the approach is really dumb - I'm still trying to wrap my head around classes.
You can have the view controller have a weak reference to the onboarding helper, so you can still call helper methods without creating a retain cycle.
In NamePageViewController, add a property:
weak var userOnboardingHelper: UserOnboardingHelper?
Then, in UserOnboardingHelper's initializer, add:
self.viewController.userOnboardingHelper = self
You can now call the onboarding helper's methods in the view controller:
userOnboardingHelper.disableContinueButton()
userOnboardingHelper.enableContinueButton()

Difficulty with IBOutlets in Protocol/Delegate

I'm having difficulty with IBOutlets. I'm trying to allow the user to input a goal (called nameOfRewardText) in a table view controller (LoLAddGoalsTableViewController) and then when they click "Done", have that goal show up in a label called "currentGoalTextField" in a different view controller (LoLGoalViewController). I had been trying to implement this using a Save segue, but was advised to use a protocol with a delegate instead (Updating text in ViewController using Save function). Now that I've replaced the Save segue with the protocol and delegate, the inputted "nameOfRewardText" text is not showing up in the "currentGoalTextField" label, I suspect because the IBOutlets are no longer tied together properly. I've attached the code and screenshots of the Outlets below to try to clarify where I'm at. Does anyone know how I could fix the IBOutlets or if there's something else I need to add to get this working? I deleted the line where I assign nameOfRewardText.text to be goal.goalText, so I think nameOfRewardText isn't getting assigned to var goal? Maybe I'm using too many names for this text (nameOfRewardText, goalText, and currentGoalTextField) and that's complicating things? Any help at all would be greatly appreciated, as I'm very new to this! Thank you everybody!
Here is the struct goal:
import UIKit
struct Goal {
var goalText: String
var pointsToCompleteGoal: Int
var pointsEarnedTowardsGoal: Int
var repeatGoal: Bool
init(goalText: String, pointsToCompleteGoal: Int, pointsEarnedTowardsGoal: Int, repeatGoal: Bool = false) { //Made String non-optional. If issue later, can revert.
self.goalText = goalText
self.pointsToCompleteGoal = pointsToCompleteGoal
self.pointsEarnedTowardsGoal = pointsEarnedTowardsGoal
self.repeatGoal = repeatGoal
}
}
Here is the public protocol:
import Foundation
import UIKit
protocol GoalDelegate: class {
func passGoal(_ goal: Goal?)
}
Here is where the delegate is created, and as you can see, the statement where I assign nameOfRewardText.text to be goal.goalText is now gone:
import UIKit
class AddGoalsTableViewController: UITableViewController {
var goal:Goal?
var delegate: GoalDelegate?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// HASHED OUT THE BELOW BECAUSE REPLACING WITH DELEGATE:
// if segue.identifier == "SaveGoal" {
// let pointsNeededInt = Int(pointsNeededText.text!)
// let pointsEarnedInt = Int(goalProgressText.text!)
// goal = Goal(goalText: nameOfRewardText.text!, pointsToCompleteGoal: pointsNeededInt!, pointsEarnedTowardsGoal: pointsEarnedInt!)
// }
if let secondViewController = segue.destination as? LoLGoalViewController{
delegate = secondViewController
delegate?.passGoal(goal)
}
}
#IBOutlet var goalTableTitleText : UILabel!
#IBOutlet weak var goalProgressText: UILabel!
#IBOutlet weak var nameOfRewardText: UITextField!
#IBOutlet weak var pointsNeededText: UITextField!
#IBOutlet weak var repeatSwitch: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
Screen cap of AddGoalsTableViewController with Outlets:
Here I conform to the protocol and call the function passGoal:
import UIKit
class LoLGoalViewController: UIViewController, GoalDelegate {
#IBOutlet weak var currentGoalTextField: UILabel!
func passGoal(_ goal: Goal?) {
currentGoalTextField.text = goal?.goalText
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
extension LoLGoalViewController {
#IBAction func cancelToLoLGoalViewController(_ segue: UIStoryboardSegue) {
}
}
Screen cap of LoLGoalViewController with Outlets:
Your LoLGoalViewController view controller might not have fully loaded with all of its outlets. Adding on to my answer to your previous question, you can declare another variable in LolGoalViewController:
#IBOutlet weak var currentGoalTextField: UILabel!
var goalText: String = ""
In your passGoal method, set your string to the goalText variable instead of the label's text:
func passGoal(_ goal: Goal?) {
goalText = goal?.goalText
}
Lastly, in your viewDidLoad of LolGoalViewController, set the label text to be goalText:
override func viewDidLoad() {
super.viewDidLoad()
currentGoalTextField.text = goalText
}

How To Prompt a New Page After Inserting Name?

I created a simple page of my app today. And, now I want to expand it.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var myTextField: UITextField!
#IBOutlet weak var display: UILabel!
#IBAction func myButtonPressed(_ sender: Any) {
display.text = "Hi \(myTextField.text!)! What can I do for you
today?"
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
I want the app to prompt a new page after the user entered their name.
You can conform to the UITextViewDelegate and present a viewController in func textViewDidEndEditing(_ textView: UITextView) like:
extension ViewController: UITextFieldDelegate {
func textViewDidEndEditing(_ textView: UITextView) {
self.present(/*the targeted view controller*/)
}
}
You can add target like
// In viewDidLoad
myTextField.addTarget(self, action: #selector(onNameChange(sender:)), for: .editingChanged)
// on value change
#objc func onNameChange(sender:UITextField) {
// Do something
}
So far I got your query as:
You want to go to new ViewController when the user is done with filling his name in the textfield.
If I got you right then choose the "GO" (or any thing you wish from options) as the Return Key value inside 'Text Input Traits Section' of Attribute Inspector.
And now add this code in your view controller class with implementing UITextFieldDelegate:
func textFieldShouldReturn(_ textField: UITextField) -> Bool // called when 'return' key pressed. return false to ignore.
{
print(textField.tag)
if (textField.text?.isEmpty)! {
//show alert that text field is empty
return false
}
/* as per your case we have only one textfield,
So there no need of switch case and you can
directly present your next vc from here without having any button on UI */
return false
}

today extension shows "unable to load" after button event (iOS)

Good morning!
I have an "unable to load" problem in my iOS widget. I've read a lot of about the "unable to load" message but nothing fixed my problem. I'm not sure but I think my problem is to refresh the widget after changing my content.
My widget has one button and one label. If the user press the button the text from the label will changed - in this moment the widget shows "unable to load". Just a milisecond after pressing the button.
import UIKit
import NotificationCenter
class TodayViewController: UIViewController, NCWidgetProviding {
#IBOutlet var segment_att: UISegmentedControl!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)) {
completionHandler(NCUpdateResult.NewData)
}
func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {
return UIEdgeInsetsZero
}
#IBAction func button_ae(sender: AnyObject) {
let tableviewclass = TodayTableViewController()
tableviewclass.newData()
}
}
Important is that the label is shown in a TableViewCell of a TableViewController. So the TableViewController is embeded in the ViewController within a Container... The listener from the button call the method newdata() of the file of the TableViewController.
import UIKit
import NotificationCenter
class TodayTableViewController: UITableViewController, NCWidgetProviding {
#IBOutlet var table: UITableView!
#IBOutlet var label1: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
init()
}
func init() {
let meldung: String = "test"
label1.text = meldung
}
func newData() {
let meldung: String = "new test"
label1.text = meldung
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
The code is really simple and basic - so I'm wondering about the problem in this simple mechanism. I hope you can help me!
Thanks at all!
Your code assumes that label1 has been set when newData() is called, even immediately after the constructor is called.
Try using this optional chaining syntax instead, which will quietly fail if the property is nil:
import UIKit
import NotificationCenter
class TodayTableViewController: UITableViewController, NCWidgetProviding {
#IBOutlet var table: UITableView!
#IBOutlet var label1: UILabel!
var meldung: String = "test" // <-- meldung is property
override func viewDidLoad() {
super.viewDidLoad()
init()
}
func init() {
label1?.text = melding // <-- optional chaining
}
func newData() {
melding = "new test" // <-- set the class property
label1?.text = meldung // <-- optional chaining
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
and instead of calling newData(), you might instead just set the meldung property, e.g.:
tableviewclass.meldung = "new test"
as your viewDidLoad() will take care of setting the UILabel text from the property

Resources