How to make a button modify another button functions - ios

I have two UIViewController. In the first one, I have a button that adds some views, one at a time, to the main view. In the second one, I set up a store, so that when I press on a button I unlock some features of my app. Now, I perfectly know (I hope) how to handle the part where I make the VCs comunicate and I trigger some other easy functions, what I don't know is how to make the store button increase the button's functions.
WHAT I NEED:
Right now the button adds a maximum of 10 views (complete version). I want that before the user buys my app, he gets to add a maximum of 3 views and then, when he buys it, the function I already have (the one to add 10 views)starts to work and replaces the other one.
MAIN VIEW CONTROLLER
var messageArray = [UIView] ()
I attached all of my UIView from the storyboard and I appended them to my array in the viewDid load like this: messageArray.append(View1)
#IBAction func addMessageViewButton(_ sender: Any) {
let hiddenViews = messageArray.filter { $0.isHidden }
guard !hiddenViews.isEmpty else {
let sheet = UIAlertController(title: "max reached", message: nil, preferredStyle: .actionSheet)
let ok = UIAlertAction(title: "OK", style: .cancel, handler: nil)
let closeAll = UIAlertAction(title: "Close all", style: .destructive) { (addMessage) in
view1.isHidden = true
view2.isHidden = true
view3.isHidden = true
view4.isHidden = true
view5.isHidden = true
view6.isHidden = true
view7.isHidden = true
view8.isHidden = true
view9.isHidden = true
view10.isHidden = true
}
sheet.addAction(ok)
sheet.addAction(closeAll)
present(sheet, animated: true, completion: nil)
return
}
let randomHiddenView = hiddenViews.randomElement()
randomHiddenView?.isHidden = false
}
SHOP VIEW CONTROLLER
Here I won't post all of the code because it would be too much and of course unnecessary, since the important thing to know here is that there's a button and if the user presses it and he proceeds with the purchase, he will get the function I posted up here working instead of the one that allows him to have just 3 views.
func unlock() {
let appdelegate = UIApplication.shared.delegate
as! AppDelegate
appdelegate.viewController!.functionToHave10Views()
//viewControlled is declared in the app delegate like this ` var viewController: ViewController?`
//I know I don't physically have `functionToHave10Views()`, but I guess I'll turn the code of my button into a function, so just to be clear, I'm referring to that function.
buyButton.isEnabled = false
}

In your main view controller:
var isLocked = true
#IBAction func addMessageViewButton(_ sender: Any) {
if isLocked {
// Do something for when is locked
} else {
// Do something for when is unlocked
}
}
Then in your shop view controller:
func unlock() {
let appdelegate = UIApplication.shared.delegate as! AppDelegate
appdelegate.viewController!.isLocked = false
buyButton.isEnabled = false
}

Related

What is wrong with this UIAlertAction code? Won't allow a click/tap and move forward in the app

I'm enrolled in a Swift/iOS Bootcamp course on Udemy. I've not had any issues prior to this one with any of the code or lessons, but this has me stumped. The module is about setting up UIAlertController and UIAlertAction, and resetting a quiz app to the beginning of the quiz. The quiz works as it should (at this point at least), however, once the Alert pops up, I can't click on it. It doesn't recognize any of the taps or clicks or move forward to the function that should be called when the click is received.
https://imgur.com/Z7Oc6kU
I'm following the code as written in the course, but one of the issues could possibly be that the course is using an older version of Swift/Xcode than I am, but I'm not 100% positive. That could be causing some confusion.
However, I have tried to utilize the Apple API Documentation, at one point, copying all of their sample code and adjusting to fit my needs (in terms of function to call after 'OK'), but even that code didn't register a click. I've searched on Stack, altered code based on other forums, and nothing has worked.
func nextQuestion() {
if questionNumber <= allQuestions.list.count - 1 {
var nextQuestion = allQuestions.list[questionNumber]
questionLabel.text = nextQuestion.questionText
}
else {
let alert = UIAlertController(title: "Quiz Over", message: "You've Finished the Quiz, Press Restart to Start Over", preferredStyle: .alert)
let restartAction = UIAlertAction(title: "Restart", style: .default) { (UIAlertAction) in
self.startOver()
}
alert.addAction(restartAction)
self.present(alert, animated: true, completion: nil)
}
}
As mentioned, the 'Restart' button on the Alert should force the quiz to restart. The startOver() function resets the questionNumber to 0, and calls a nextQuestion() function, as well as printing a line for me to know if the function has been called, which would mean that the tap is registered, but the startOver() function is incorrect, but it won't print the line, which indicates that the function isn't being called at all.
Full ViewController:
import UIKit
class ViewController: UIViewController {
//Place your instance variables here
let allQuestions = QuestionBank()
var pickedAnswer : Bool = false
var questionNumber : Int = 0
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var scoreLabel: UILabel!
#IBOutlet var progressBar: UIView!
#IBOutlet weak var progressLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let firstQuestion = allQuestions.list[0]
questionLabel.text = firstQuestion.questionText
}
#IBAction func answerPressed(_ sender: AnyObject) {
if sender.tag == 1 {
pickedAnswer = true
}
else if sender.tag == 2 {
pickedAnswer = false
}
checkAnswer()
nextQuestion()
}
func updateUI() {
}
func nextQuestion() {
questionNumber += 1
if questionNumber <= allQuestions.list.count - 1 {
var nextQuestion = allQuestions.list[questionNumber]
questionLabel.text = nextQuestion.questionText
}
else {
let alert = UIAlertController(title: "Quiz Over", message: "You've Finished the Quiz, Press Restart to Start Over", preferredStyle: .alert)
let restartAction = UIAlertAction(title: "Restart", style: .default) { action in self.startOver()
}
alert.addAction(restartAction)
self.present(alert, animated: false, completion: nil)
}
}
func checkAnswer() {
if pickedAnswer == allQuestions.list[questionNumber].answer{
print("good")
}
else if pickedAnswer != allQuestions.list[questionNumber].answer{
print("bad")
}
}
func startOver() {
questionNumber = -1
nextQuestion()
print(questionNumber)
print("Restarting")
}
}
I can't reproduce any issue based on the code you've shown, as you can see:
... so I think this is may be a problem with your Xcode setup. Possible issue:
You are building to the iPhone XR simulator which is slow.
You may have an old computer.
And you seem to have turned on Slow Animations in the Simulator app by mistake.
At the least try turning off Slow Animations and build for the 5s simulator. This will make the Simulator a lot more responsive. But on old machinery there may still be issues.
Try to present in main thread:
DispatchQueue.main.async {
self.present(alert, animated: true, completion: nil)
}

Trigger UIAlertViewController Based on Time

I have UITable to display different animals. When you select a cell in the table, a new view controller with a large UIImage is pushed. Currently, when you zoom in on the image, a UIAlertView is triggered that asks the user if they would like to download hi res images. If they click yes, the "hi-res-flag" is set to "yes" in user defaults and they no longer see the pop up. However, if they select no, the hi-res-flag will continue to pop up each time they zoom in on a photo.
Instead, if they answer no, I would like to have this flag pop up occasionally. Not every time the click a cell in the species table, nor every time they open the app. Something more like once or twice a month. Is there a way to use time in the logic of an iOS app? For instance, erase the value set for "high-res-flag" (if already equals 'no') in user defaults, once a month?
Store the time you showed the alert last in the user preferences, and then check that value every time before you present the alert whether a certain time has passed.
I have written a time checker class that does the job. The code is in Swift. You can use it from your Objective-C code as well. You can find this code in gist here.
Solution
Below, you use the viewWillAppear delegate method to see if the hiResFlag is existing. If it is present and false, then you check to see if you can display the popup:
import UIKit
class ImageViewController: UIViewController {
//Whenever you enter the Image View Controller, you check whether to show popup or not
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let hiResFlag = hiResFlag {
if hiResFlag == false {
if PopUpTimeChecker.shouldShowPopUp() {
self.presentAlert()
}
}
}
}
func presentAlert() {
let alert = UIAlertController.init(title: nil, message: "Show Pop up", preferredStyle: .alert)
let action = UIAlertAction.init(title: "Yeahh!", style: .default, handler: nil)
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
}
}
The following code implements the time-checking algorithm. Edit popUpTimeInterval below for setting your minimum time. Right now, it is set to be 15 days (in seconds). Once in every 15 days the pop-up will be shown when you call the shouldShowPopUp method.
import UIKit
//Below 4 variables, I have made them Global. No need to make them global in your case
#objc var popUpTimeInterval: UInt64 = 1296000 //15 days in seconds
#objc var hiResFlag: Bool? {
get {
return UserDefaults.standard.object(forKey: "HiResFlag") as? Bool
}
set {
UserDefaults.standard.setValue(newValue, forKey: "HiResFlag")
}
}
#objc var isFirstTimePopUp: Bool {
get {
let value = UserDefaults.standard.object(forKey: "IsFirstTimePopUp")
return value == nil ? true : value as! Bool
}
set {
UserDefaults.standard.setValue(newValue, forKey: "IsFirstTimePopUp")
}
}
#objc var lastDateOfPopUp: Date? {
get {
return UserDefaults.standard.object(forKey: "LastDateOfPopUp") as? Date
}
set {
UserDefaults.standard.setValue(newValue, forKey: "LastDateOfPopUp")
}
}
#objc class PopUpTimeChecker {
#objc static fileprivate func setLastPopUpDate() {
//Setting current date to last shown pop up date
lastDateOfPopUp = Date()
}
#objc static fileprivate func timeIntervalSinceLastPopUp() -> UInt64 {
//Returning how much time (in seconds) has passed from last popup date until now
return UInt64(Date().timeIntervalSince(lastDateOfPopUp!))
}
#objc static func shouldShowPopUp() -> Bool {
//We proceed further only if we have the last date when pop up was displayed, else we create and set it as the current date
if let _ = lastDateOfPopUp {
let timeInterval = timeIntervalSinceLastPopUp()
if timeInterval > popUpTimeInterval {
self.setLastPopUpDate()
return true //Show pop up
} else {
if isFirstTimePopUp {
//If this is the first time, you just allow the pop up to show, don't allow otherwise
isFirstTimePopUp = false
return true
} else {
return false
}
}
} else {
self.setLastPopUpDate() //Since we don't have a last date, we set it here for starting off
return self.shouldShowPopUp() //Recursively call method
}
}
}

Warning: Attempt to present ZMNavigationController on **.ViewController whose view is not in the window hierarchy

Guys i am facing an odd problem with NavigationController. Existing answers did not help at all !!!!
Here is basic scenario of the app:
There are two views - Main and Second view
In main view there is a button when i happen to tap goes into second view using segue.
In second view after i enter a certain field in text view and click on a button called "join" it triggers "joinMeeting()" function
and meeting should be joined.
However, when i do that debugger shows me:
"Warning: Attempt to present on
<***.ViewController: *****> whose view is not in the window
hierarchy!"
So i have read most of the tread and given that it happens because of viewDidAppear method but i have nth to be done before viewDidAppear. Everything happens after button is clicked.
joinMeeting() is successfully called and print method returns 0 which means no issue(https://developer.zoom.us/docs/ios/error-codes/) and successful SDK connection however after this "Warning" error is shown in debugger and nothing happens in the app.
If it helps following is the code that triggers joinBtn:
/**
Triggers when Join Button is clicked from second view.
*/
#IBAction func joinMeeting(_ sender: Any) {
if( activityID.text == "" ) {
let alert = UIAlertController(title: "Field is Blank", message: "Activity ID cannot be blank.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
return;
}
let ms: MobileRTCMeetingService? = MobileRTC.shared().getMeetingService()
if ms != nil {
ms?.delegate = self;
// //For Join a meeting
let paramDict: [AnyHashable: Any] = [
kMeetingParam_Username: kSDKUserName,
kMeetingParam_MeetingNumber: activityID.text!,
]
let ret: MobileRTCMeetError? = ms?.joinMeeting(with: paramDict)
print("onJoinaMeeting ret:\(String(describing: ret))")
}
}
Please help if anyone knows or have an idea on what i am missing here.
Here is what solved the issue:
Storyboard Configuration:
ViewController --Segue: Show--> JoinViewController
#IBAction func onClickJoin(_ sender: AnyObject) {
//Main storyBoard
let initialVC = UIStoryboard(name: "Main", bundle:nil).instantiateInitialViewController() as! UIViewController
let appDelegate = (UIApplication.shared.delegate as! AppDelegate)
appDelegate.window?.rootViewController = initialVC
//Rest of the code
}
Just Add Following code on that controller in which you want to perform calling:
override func viewWillAppear(_ animated: Bool) {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
appDelegate?.window?.rootViewController = self
}
Unfortunately, none of the above solutions worked for me.
So Here is my solution.
Add this line
MobileRTC.shared().setMobileRTCRootController( self.navigationController)
=> When user taps of Join Call Button.
Make sure these conditions should meet as well.
ViewController which is used to open the ZOOM meeting should be a part of root navigation controller
DO NOT present modally the current Zoom Meeting Joining View Controller. Always push it to the root navigation controller.

How can I make a variable inside a function global?

I currently have the following function called saveRun()
func saveRun() {
let startLoc = locations[0]
let endLoc = locations[locations.count - 1]
let startLat = startLoc.coordinate.latitude
let startLong = startLoc.coordinate.longitude
let endLat = endLoc.coordinate.latitude
let endLong = endLoc.coordinate.longitude
//1. Create the alert controller
let alert = UIAlertController(title: "Save the Run", message: "Choose a name: ", preferredStyle: .alert)
//2. Add the text field
alert.addTextField { (textField) in
textField.text = ""
}
// 3. Grab the value from the text field, and print it when the user clicks OK
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] (_) in
let textField = alert?.textFields![0] // Force unwrapping because we know it exists.
// Create name for run
let runName = textField?.text
let run = self.databaseRef.child(runName!)
let user = FIRAuth.auth()?.currentUser?.uid
// Enter run info into db
run.child("startLat").setValue(startLat)
run.child("startLong").setValue(startLong)
run.child("endLat").setValue(endLat)
run.child("endLong").setValue(endLong)
run.child("distance").setValue(self.distance)
run.child("time").setValue(self.seconds)
run.child("user").setValue(user)
// Enter locations into db
var i = 0
for location in self.locations {
run.child("locations").child("\(i)").child("lat").setValue(location.coordinate.latitude)
run.child("locations").child("\(i)").child("long").setValue(location.coordinate.longitude)
i = i + 1
self.performSegue(withIdentifier: DetailSegueName, sender: nil)
}
}))
// 4. Present the alert
self.present(alert, animated: true, completion: nil)
}
My problem is that I am trying to extract 'runName' from the action that I am adding when the user clicks 'Ok' on the alert controller and using it in the following function:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let detailViewController = segue.destination as? DetailViewController {
detailViewController.runName = self.runName
}
}
When I try to print 'runName' in DetailViewController, the value of runName is nil. The issue I think is that I cannot set a global variable inside the action I have added as it is in a function. Is there any other way I can obtain this variable's value and use it outside of the function?
Class YourClassName:UIViewController {
var runName:String = "" // This will be global for your class
//Remove local decalration of runName variable
func saveRun() { // your function
alert.addAction(
//.....
self.runName = textfield?.text
)
}
}
Now you can use in whole class.
I solved this thanks to #DSDharma pointing out that even if 'runName' was set as a global variable, using it as a global variable inside of an alert function block required the 'self' keyword.
For example, before I had the following inside of the alert function block:
let runName = textField?.text
This needed to be changed to:
self.runName = textField?.text

Displaying a UIAlertController in GameScene (SpriteKit/Swift)

The simple way to display a UIAlertView, in swift is:
let alert = UIAlertView()
alert.title = "Alert!"
alert.message = "A wise message"
alert.addButtonWithTitle("Ok, thank you")
alert.show()
But this is now depreciated in iOS 9 and recommends using UIAlertController:
let myAlert: UIAlertController = UIAlertController(title: "Alert!", message: "Oh! Fancy", preferredStyle: .Alert)
myAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
self.presentViewController(myAlert, animated: true, completion: nil)
Which is great, but I'm using SpriteKit and inside a GameScene, which gives an error of Value of type 'GameScene' has no member 'presentViewController'...
Do I need to switch back to my ViewController to present this or is there a way to call it from a GameScene.
I found THIS answer, but it's Objective-C.
There are many ways to handle this situation, I do not recommend Jozemite Apps answer, because this will cause problems on apps with more than 1 view controller.(you want to present the alert on the current view controller, not the root)
My preferred way of doing it is through delegation.
What needs to be done is create a protocol to handle messaging:
import Foundation
protocol ViewControllerDelegate
{
func sendMessage(message:String);
}
In your view controller:
class ViewController : UIViewController, ViewControllerDelegate
{
...
func sendMessage(message:String)
{
//do alert view code here
}
//in the view controllers view did load event
func viewDidLoad()
{
var view = self.view as! GameSceneView
view.delegate = self
}
In your view code:
var delegate : ViewControllerDelegate
Finally in game scene where you want to present:
self.view.delegate?.sendMessage(message)
This way allows limited access to the VC, and can be modified with more options when needed.
Another way is to set up a notification system, and use NSNotificationCenter to pass a message from the scene to the current VC and have it send a message;
in ViewController
func viewDidLoad()
{
NSNotificationCenter.defaultCenter().addObserver(self,selector:"AlertMessage:",name:"AlertMessage",object:nil);
}
func AlertMessage(notification:NSNotification)
{
if(let userInfo = notification.userInfo)
{
let message = userInfo["message"]
....//do alert view call here
}
}
In Game scene code:
...at the spot you want to send a message
let userInfo = ["message":message];
NSNotificationCenter.defaultCenter.postNotificationNamed("AlertMessage",object:nil,userInfo:userInfo)
Another approach is to save the view controller pointer to game scene view:
//in Game Scene View code
var viewController : UIViewController;
//in the view controllers view did load event
func viewDidLoad()
{
var view = self.view as! GameSceneView
view.viewController = self
}
//finally in game scene where you want to present
let myAlert: UIAlertController = UIAlertController(title: "Alert!", message: "Oh! Fancy", preferredStyle: .Alert)
myAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
self.view.viewController.presentViewController(myAlert, animated: true, completion: nil)
Yet another way is to make your view controller global.
In view controller code:
private var _instance : UIViewController
class ViewController : UIViewController
{
class var instance
{
get
{
return _instance;
}
}
func viewDidLoad()
{
_instance = self;
}
}
Then just call
ViewController.instance!.
whenever you need access to your view controller.
Each of these methods have there strengths and weaknesses, so choose whatever way works best for you.
Try using this. I work in SpriteKit and I used this code for my in app purchase messages in my game, Chomp'd.
self.view?.window?.rootViewController?.presentViewController(myAlert, animated: true, completion: nil)
The answer from #Knight0fDragon is nice, but I think it's a little long.
I will just put here another solution using Podfile of Cocoapoad for new comers (others guys having the same problem).
first you need to install cocoapod and initiate it for your project (very easy to do; check some YouTube video or this link.
in your obtained Podfile, copy, and paste this: pod 'SIAlertView'. It is "A UIAlertView replacement with block syntax and fancy transition styles". (More details here. Please give credit to the libraries Authors you're using.
Finally, in your GameScene.swift file add the following after the import or after the closing bracket of the class GameScene
private extension GameScene {
func showPauseAlert() {
let alertView = SIAlertView(title: "Edess!!", andMessage: "Congratulations! test testing bla bla bla")
alertView.addButtonWithTitle("OK", type: .Default) { (alertView) -> Void in
print("ok was pushed")
}
alertView.show()
}
}
You can add many button with title if you want, and do whatever action you want. Here I just print "Ok was pushed".
The above cited links plus this one helped me to understand and work with this UIAlertView easily in my multiple-level game, to display alert on the "Pause" button of the game.

Resources