Using shared classes for different views - ios

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()

Related

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
}

Swift Call component from UIViewController in a UIView [duplicate]

This question already has answers here:
What does "Fatal error: Unexpectedly found nil while unwrapping an Optional value" mean?
(16 answers)
Closed 6 years ago.
I'm having a problem, I have a UIButton in a UIViewController class and I want to enable that button after an animation that happens in a UIView class that is in another file.
class MainViewController: UIViewController {
#IBOutlet weak var nextButton: UIButton!
#IBAction func nextButtonPressed(sender: UIButton) {
nextButton.enable = false
}
}
When I try to call the nextButton from the viewController class after the animation is done I get this error:
EXC_BAD_INSTRUCTION(code = EXC_I386_INVOP, subcode = 0x0)
I get the error on the line where I set the nextButton enable to true.
class CustomView: UIView {
var vc = MainViewController()
func animationEnded() {
vc.nextButton = true
}
}
I don't have a clue what I'm missing and I would appreciate some help. Thanks
You encounter an error because in your CustomView you create a new MainViewController, you're not using the one initialized from the storyboard. That new MainViewController doesn't have any of its properties initialized, so nextButton is nil, hence the crash when you try to access it.
What you want to do is notify your controller from the view that the animation has ended so that the controller can update the button (since the controller owns the button). The standard way to do this in Cocoa is to use the delegate pattern like so:
class MainViewController: UIViewController, CustomViewDelegate
{
#IBOutlet weak var nextButton: UIButton!
#IBOutlet weak var customView: CustomView!
#IBAction func nextButtonPressed(sender: UIButton) {
self.nextButton.enabled = false
}
override func awakeFromNib() {
super.awakeFromNib()
self.customView.delegate = self
}
func customViewAnimationDidEnd(customView: CustomView) {
self.nextButton.enabled = true
}
}
protocol CustomViewDelegate : class
{
func customViewAnimationDidEnd(customView: CustomView)
}
class CustomView: UIView
{
weak var delegate: CustomViewDelegate? = nil
func animationEnded() {
self.delegate?.customViewAnimationDidEnd(self)
}
}
In this implementation the controller is the view delegate and gets notified when interesting events happen in the view (like a particular animation ending).
Make a delegate in your UIView that tells when it should hapen
protocol CustomViewDelegate {
func pushThatButton()
}
in CustomView class put this:
weak var delegate: CustomViewDelegate?
then
func animationEnded() {
delegate.pushThatButton()
}
and in UIViewController
class MainViewController: UIViewController, CustomViewDelegate {
and implement delegate ofc
func pushThatButton()
nextButton.sendActionsForControlEvents(.TouchUpInside)
}
almost forget, do an outlet to your view in viewController and setup delegate! in viewDidLoad() or when you will find this best
customViewOutlet.delegate = self

Swift Objects in different class not updating

I need the stepper and label to reset back to 0 at the same time that my variables reset. The problem is the steppers and labels are in a different class and are not resetting when the variables do. I tried using delegates(if someone can show me the best way that would be great) instead of an instance of my view controller, but I can't get anything to work. Thanks for any help in advance.
ViewController:
class ViewController: UIViewController
{
var colors = CircleView()
#IBOutlet weak var circleView1: CircleView!
#IBOutlet weak var blueStepper: UIStepper!
#IBOutlet weak var greenStepper: UIStepper!
#IBOutlet weak var redStepper: UIStepper!
#IBAction func stepperChange(sender: UIStepper)
{
circleView1.redd1 = Int(redStepper.value);
redValue.text = Int(sender.value).description;
}
#IBAction func stepperChange1(sender: UIStepper)
{
circleView1.greenn1 = Int(greenStepper.value);
greenValue.text = Int(sender.value).description;
}
#IBAction func stepperChange2(sender: UIStepper)
{
circleView1.bluee1 = Int(blueStepper.value);
blueValue.text = Int(sender.value).description;
}
}
UIView:
class CircleView: UIView
{
var colors1=ViewController()
func updateStepper
{
if(redd1==Int(red1)&&greenn1==Int(green1)&&bluee1==Int(blue1))
{
redd1=0;
greenn1=0;
bluee1=0;
colors1.redStepper.value=0.0;//
colors1.greenStepper.value=0.0;//
colors1.blueStepper.value=0.0;//
}
}
}
I do not quite understand your code, like the "if" condition in your CircleView, the lack of parameters to the method "updateStepper". I am assuming you just wrote some "swift-pseucode" and I will ignore some parts of it to explain how you could implement a delegate for it. Below is an example code:
import UIKit
protocol CircleViewDelegate: class {
func updateStepper(view: CircleView)
}
class ViewController: UIViewController, CircleViewDelegate{
#IBOutlet weak var circleView1: CircleView!
#IBOutlet weak var blueStepper: UIStepper!
#IBOutlet weak var greenStepper: UIStepper!
#IBOutlet weak var redStepper: UIStepper!
var circleViewDelegate: CircleView!
override func viewDidLoad() {
super.viewDidLoad()
circleViewDelegate = circleView1
circleViewDelegate!.delegate = self
}
func updateStepper(view: CircleView) {
//code you want to execute when you call updateStepper() in the CircleView()
}
}
class CircleView: UIView {
weak var delegate: CircleViewDelegate?
func updateStepper() {
//whenever you want your viewController to updated other views based
//on a condition inside an element like UIView, you can use a delegate
//this way, your code is executed by the ViewController whenever you want
delegate?.updateStepper(self)
}
}
A callback in your UIView must be set to call "updateStepper" when you want. Unfortunately, I didn't quite understand the time it should be called according to your question.
I hope this helps!
Have you tried NSNotification?
If it's always going to reset to zero, then create a func without the if statement in CircleView:
func resetStepper(not: NSNotification) {
r1 = 0
g1 = 0
b1 = 0
c1.rStep.value = 0.0
c1.bStep.value = 0.0
c1.gStep.value = 0.0
}
Also in CircleView's createView func, add:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "resetStepper:", name: "ResetStepper", object: nil)
Then in the view controller, post a notification from whichever button is calling it.
#IBAction func callReset(sender: AnyObject) {
NSNotificationCenter.defaultCenter().postNotificationName("ResetStepper", anObject: nil)
}
That will send the notification that CircleView is listening for to call the function.
Hope that works for you.

Sending value back from detail to master

I am using a master-detail model in Swift.
However, I want to send a class object created in detail view back to master view. I wrote a unwind function in the master view, but I cannot see the back button in the detail view so I cannot ctrl+drag it to the exit.
Does anyone know how to set the back button to make it visible?
Rather than worrying about hooking up something to the back button, you can update the model directly as the user updates the fields in the detail view controller. To do this you can pass a reference to some model object that contains the properties to be updated (make sure that's a reference type, e.g., a class, and not a struct, though).
For example:
class Person {
var firstName: String?
var lastName: String?
}
class MasterViewController: UIViewController {
#IBOutlet weak var firstNameLabel: UILabel!
#IBOutlet weak var lastNameLabel: UILabel!
var person = Person()
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let destination = segue.destinationViewController as? DetailViewController {
destination.person = person
}
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
firstNameLabel.text = person.firstName
lastNameLabel.text = person.lastName
}
}
class DetailViewController: UIViewController,UITextFieldDelegate {
var person: Person?
#IBOutlet weak var firstNameTextField: UITextField!
#IBOutlet weak var lastNameTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
firstNameTextField.text = person?.firstName
lastNameTextField.text = person?.lastName
}
// Note, I specified the detail view controller to be the delegate
// for the two text fields in IB: I then can detect when editing is
// done and act accordingly.
func textFieldDidEndEditing(textField: UITextField) {
switch textField {
case firstNameTextField:
person?.firstName = textField.text
case lastNameTextField:
person?.lastName = textField.text
default:
assert(false, "unidentified textField \(textField)")
}
}
}
You can have master view controller update itself in viewDidAppear, like I did above, or, better, you could add observers for the model properties. But hopefully it illustrates the basic idea.

Resources