I'm having a issue in Unwinding/Closing a View Controller.
My navigation bar in this view controller has two buttons. The left one is an "Unwind/exit" button that functions fine and closes the current view controller and returns the user back to the main setting screen. The second button is a "Save" button that commits the users data to the NSUserdefaults. Both buttons do what they are designed. Although I would like the "Save" button to do one more thing.
I would like the Navigation Bar "Save" button to continue to save as data as its designed, but I would also like it to "Unwind/Exit" that view controller and return to the main Settings view controller just like the "Unwind/Exit" navigation bar button does.
Here is my code for the view controller. As you will be able to see, i am calling the "backButtonTapped" function from within my "settingSaveEmailAddress" function. I know the "backButtonTapped" function is being triggered because the "print" command is printing in the debug window. Although it is saving my data, it is failing to close the view controller and unwinding back to the main settings view controller.
import UIKit
class SettingsViewController: UIViewController {
#IBOutlet weak var settingEmailAddress: UITextField!
#IBOutlet weak var settingEmailAddressLastUpdate: UILabel!
// Constant keys for NSUserDefaults look ups
static let settingDefaultEmailAddress = "EMAIL_ADDRESS"
static let settingDefaultEmailAddressLastUpdated = "EMAIL_ADDRESS_LAST_UPDATED"
//End Setting - Email Address
#IBAction func settingSaveEmailAddress(sender: AnyObject) {
if (settingEmailAddress.text!.characters.count > 0) {
let prefs = NSUserDefaults.standardUserDefaults()
prefs.setObject(settingEmailAddress.text, forKey: SettingsViewController.settingDefaultEmailAddress)
saveTimestampEmailAddress()
}
dismissKeyboard()
print(settingEmailAddress.text)
backButtonTapped(self)
}
//End Setting - Email Address
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment these if you want to clear out the saved defaults
//let appDomain = NSBundle.mainBundle().bundleIdentifier!
//NSUserDefaults.standardUserDefaults().removePersistentDomainForName(appDomain)
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(SettingsViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
// update labels from NSUserDefaults
getUserPreferences()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// Updates the view with the user values already stored in NSUserDefaults
func getUserPreferences() {
let prefs = NSUserDefaults.standardUserDefaults()
// Get Email Address
if let email = prefs.stringForKey(SettingsViewController.settingDefaultEmailAddress) {
settingEmailAddress.text = email
}
// Get the last time something was stored
let dateFormatter = NSDateFormatter()
dateFormatter.dateStyle = NSDateFormatterStyle.LongStyle
dateFormatter.timeStyle = .MediumStyle
if let lastUpdateStored = (prefs.objectForKey(SettingsViewController.settingDefaultEmailAddressLastUpdated) as? NSDate) {
settingEmailAddressLastUpdate.text = "Last Update:" + dateFormatter.stringFromDate(lastUpdateStored)
} else {
settingEmailAddressLastUpdate.text = "Last Update: Never"
}
}
// MARK: - Keyboard responders so the keyboard goes away when we're done editing.
// Dismiss the keyboard when the user is finished editing.
func dismissKeyboard(){
// Resign the first responder status.
view.endEditing(true)
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
// Saves the timestamp of when the user has made a change to the NSUserDefaults
func saveTimestampEmailAddress() {
let prefs = NSUserDefaults.standardUserDefaults()
let timestamp = NSDate()
prefs.setObject(timestamp, forKey: SettingsViewController.settingDefaultEmailAddressLastUpdated)
let dateFormatter = NSDateFormatter()
dateFormatter.dateStyle = NSDateFormatterStyle.LongStyle
dateFormatter.timeStyle = .MediumStyle
settingEmailAddressLastUpdate.text = "Last Update:" + dateFormatter.stringFromDate(timestamp)
}
func backButtonTapped(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
print("Exit Triggered")
}
}
You can write :-
In Objective-C
[self.navigationController popToRootViewControllerAnimated:YES];
In Swift
func popToRootViewControllerAnimated(_ animated: Bool) -> [UIViewController]?
at the end of your "Save" buttons method if your "Main Setting" viewController is the root viewController.
I created this function and I am now getting the expected functionality when I press "Save".
Save NSUserDefault data
Close current ViewController and return to
my main settings view controller "SettingsMainTableViewController".
func closeVCReturnSettings() {
for controller in self.navigationController!.viewControllers as Array {
if controller.isKindOfClass(SettingsMainTableViewController) {
self.navigationController?.popToViewController(controller as UIViewController, animated: true)
break
}
print("Return to settings")
}
}
Related
I need help regarding a problem that I have been wanting to solve for a long time.
It is a navigation between view controllers (children) that take information from the parent. The parent is updated if there is a notification. And I want the children to update instantly along with the parent.
It would be a Home controller where I show basic information.
Then a button inside the Home controller that would take the View Controller that has the navigation that I mentioned.
I attach a basic scheme so you can understand it better
I would like to receive an idea even if it is basic in a programmatic way to start thinking about logic.
It would also help me a lot how to make the TOP NAVIGATION BAR between steps (step 1, step 2...), and when I click on any step that opens the child corresponding to that step.
The User type is nothing more than a dictionary with Strings, Ints and some data. (I can did the notification system and works, i only mention that can work in this way)
I have also thought that when I press the home controller button it will load 4 view controllers (one for each step) and it will consume load time, right?
Any help would get me out of stress
Thanks a lot!!
// Home controller
class HomeController: UIViewController {
let showSteps: UIButton = {
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(showSteps), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
addSubview(showSteps)
}
#objc func showSteps() {
let controller = ShowSteps()
self.present(controller, animated: true, completion: nil)
}
}
// UIViewController with Navigation Bar
class ShowSteps: UIViewController {
private var user: User?
override func viewDidLoad() {
// update the User variable
Service.shared.fetchUserData(uid: "XXX") { user in
self.user = user
}
// listen notifications to update the user
let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(updateUser), name: NSNotification.Name(rawValue: "updateUser"), object: nil)
/// 1. HERE I NEED TO ADD A NAVIGATION BAR IN THE TOP OF THE VIEW, THAT APPEAR IN ALL CHILDS
/// 2. I NEED TO NAVIGATE TO OTHER STEPS WITH A FUNCTION
}
#objc func dissmissView(){
/// 3. how can i remove from view all childs 4 in this case. but can be 6 o 7.... And then dismiss actual ShowSteps
self.dismiss(animated: true, completion: nil)
}
#objc func updateUser() {
// update user if notification called
Service.shared.fetchUserData(uid: "XXX") { user in
self.user = user
}
}
}
// that is a child example
class VCexample1: UIViewController {
/// 4. how can i see the navigation bar that has the parent? How can i send to other step from here?
/// 5. how can i get the user variable from ShowSteps?
/// 6. how can i navigate to other step from here?
///
let BarButton: UIButton = {
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(goToOtherStep), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
/// show user data
}
}
// that is a other child example
class VCexample2: UIViewController {
...
}
// that is a other child example
class VCexample3: UIViewController {
...
}
// that is a other child example
class VCexample4: UIViewController {
...
}
IS BASIC BASIC CODE. ITS NOT PERFECT. I HAVE PUT IT WITH ERRORS BUT SHORT SO THAT IT IS BETTER UNDERSTOOD
Simple example of delegation :
// User data
struct UserData {
var name: String
}
// this how child get info from parentVC
protocol childVCDelegate {
// delegete user func or var (only one may be usefull)
func getUserData() -> UserData
func getUserName() -> String
var userData: UserData {get set}
}
// the parent view controller who keep the user data
class ParentVC: UIViewController, childVCDelegate {
var userData: UserData = UserData(name: "nacho111")
func getUserData() -> UserData {
userData
}
func getUserName() -> String {
userData.name
}
}
// child VC with a label updated with user name from parentVC
class ChildVC: UIViewController {
var label = UILabel()
var delegate: childVCDelegate?
// either a copy of user in child view
var userInChildVC: UserData?
// either access user in Parent VC
var userInParentVC: UserData? {
delegate?.userData
}
// fonction to register in notification center to get userData changes from parentVC
func userDataChanged() {
// only one of the 2 options is usefull
userInChildVC = delegate?.getUserData()
label.text = userInChildVC?.name
label.text = delegate?.userData.name
label.text = delegate?.getUserName()
}
}
// the tab bar which create a childVC and connect it to the parent
// via delegate property of ChildVC
class TabBar: UIViewController {
var childVC: [ChildVC] = []
// visible child VC
var visibleChildVC: Int? {
willSet {
if let index = visibleChildVC {
childVC[index].view.isHidden = true
}
}
didSet {
if let index = visibleChildVC {
childVC[index].view.isHidden = false
}
}
}
func createChildVC(withParentVC parentVC: ParentVC,
visible: Bool = false) {
let ch1 = ChildVC()
ch1.delegate = parentVC
childVC.append(ch1)
// adding child VC view in view hierarchy : optional
view.addSubview(ch1.view)
if visible {
visibleChildVC = childVC.count - 1
} else {
ch1.view.isHidden = true
}
}
func displayChildVC(atIndex index: Int) {
visibleChildVC = index
}
}
I been reading iOS Programming by Big Nerd Ranch and doing the work in chapter 15 about cameras when I encountered this problem.
The camera will only pop up if I hold down the camera button. I added a breakpoint at my takePicture function and noticed that tapping doesn’t even call the function but holding does. Both delegates are included in my controller so that shouldn’t be a problem. The button is a UIBarButtonItem is that connected to my takePicture func on my controller.
import UIKit
class DetailViewController: UIViewController, UITextFieldDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#IBOutlet var nameField: CustomTextField!
#IBOutlet var serialNumberField: CustomTextField!
#IBOutlet var valueField: CustomTextField!
#IBOutlet var dateLabel: UILabel!
#IBOutlet var imageView: UIImageView!
#IBAction func backgroundTapped(_ sender: UITapGestureRecognizer) {
view.endEditing(true)
}
#IBAction func takePicture(_ sender: UIBarButtonItem) {
let imagePicker = UIImagePickerController()
// If the device has a camera, take a picture; otherwise, just pic from photo library
if UIImagePickerController.isSourceTypeAvailable(.camera) {
imagePicker.sourceType = .camera
} else {
imagePicker.sourceType = .photoLibrary
}
imagePicker.delegate = self
// Place image picker on the screen
present(imagePicker, animated: true, completion: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.identifier {
case "changeDate"?:
let dateCreatedViewController = segue.destination as! DateCreatedViewController
dateCreatedViewController.item = item
default:
preconditionFailure("Unexpected segue identifier.")
}
}
var item: Item! {
didSet {
navigationItem.title = item.name
}
}
let numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
return formatter
}()
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
nameField.text = item.name
serialNumberField.text = item.serialNumber
valueField.text = numberFormatter.string(from: NSNumber(value: item.valueInDollars))
dateLabel.text = dateFormatter.string(from: item.dateCreated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Clear first responder
view.endEditing(true)
// "Save" changes to item
item.name = nameField.text ?? ""
item.serialNumber = serialNumberField.text
if let valueText = valueField.text, let value = numberFormatter.number(from: valueText) {
item.valueInDollars = value.intValue
} else {
item.valueInDollars = 0
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
The permissions for accessing the camera and library were also added. Does it take a while for the camera to load? Whats going on?
The problem is that your recognizer gesture event is affecting your button.
It is called first that the tap on the button.
First delete the recognizer gesture in your viewcontroller.
And consider using an extension for your viewcontrollers as follows:
extension UIViewController {
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
func dismissKeyboard() {
view.endEditing(true)
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.hideKeyboardWhenTappedAround()
}
It looks like while connected to debugger UIImagePickerController takes longer to initialize, disconnect device from debugger and it should open faster. You can also try to put opening code into:
DispatchQueue.main.async {
}
I have an application with 2 views.
On the first view, the user enter a login, hits on search button.
The application retrieves through an api information about the login,
is redirected on the second view where it displays information about the login.There is a back button on the second view to go back on the first view to be able to search a different login
In the second view, I treat both cases (Landscape/Portrait)
What happens is the following : first time I enter the login, Portrait and landscape cases are well treated on the result view.
When I go back and search a new login, It will only display the mode in which you entered the second time (if you entered in portrait mode, it will
only display in portrait mode, the same with landscape) Is there something that I should be doing ?
here is the code for the first view :
var login = ""
class FirstViewController: UIViewController {
#IBOutlet weak var Input: UITextField!
#IBAction func sendlogin(_ sender: UIButton) {
if let text = Input.text, !text.isEmpty {
login = text
performSegue(withIdentifier: "callaffiche", sender: self)
return
} else {
return
}
}
here is the code for the second view (I will only post viewDidLoad, the back button creation along its function and the creation of the imageuser as there are many objects. But all of them are created with the same principle ) AfficheElemts calls the creation of every single object( ButtonBackAffiche, ImageAffiche are called within AfficheElements
override func viewDidLoad() {
super.viewDidLoad()
print("ds viewdidload")
let qos = DispatchQoS.userInitiated.qosClass
let queue = DispatchQueue.global(qos: qos)
queue.sync {
self.aut.GetToken(completionHandler: { (hasSucceed) in
if hasSucceed {
print("good")
DispatchQueue.main.async {
self.AfficheElements()
}
} else {
print("bad")
}
})
}
}
func ButtonBackAffiche () {
let button = UIButton()
button.restorationIdentifier = "ReturnToSearch"
button.setTitle("Back", for: .normal)
button.setTitleColor(UIColor.black, for: .normal)
button.isEnabled = true
button.addTarget(self, action: #selector(ReturnToSearch(button:)), for: .touchUpInside)
if UIDevice.current.orientation.isPortrait {
button.frame = CGRect (x:CGFloat(180),y:CGFloat(600),width:50,height:30)
} else {
button.frame = CGRect (x:CGFloat(350),y:CGFloat(360),width:50,height:30)
}
self.view.addSubview(button)
}
func ImageAffiche () {
//Get image (Data) from URL Internet
let strurl = NSURL(string: image_url)!
let dtinternet = NSData(contentsOf:strurl as URL)!
let bongtuyet:UIImageView = UIImageView ()
if UIDevice.current.orientation.isPortrait {
print("portrait ds imageview")
bongtuyet.frame = CGRect (x:10,y:80,width:30,height:30)
} else {
print("landscape ds imageview")
bongtuyet.frame = CGRect (x:CGFloat(200),y:CGFloat(80),width:90,height:120)
}
bongtuyet.image = UIImage(data:dtinternet as Data)
self.view.addSubview(bongtuyet)
}
func ReturnToSearch(button: UIButton) {
performSegue(withIdentifier: "ReturnToSearch", sender: self)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
print("ds viewwilltransition")
if UIDevice.current.orientation.isPortrait {
print("portrait")
} else {
print("landscape")
}
while let subview = self.view.subviews.last {
subview.removeFromSuperview()
}
AfficheElements()
}
One last thing, I have put traces in the code and the creation of the objects, it goes through the right portion of the code but does not display the objects at the right locations.
Thanks #Spads for taking time to help me.
I finally figured it out. I was not calling the function AfficheElements
int the main queue.
DispatchQueue.main.async {
self.AfficheElements()
}
in viewWillTransition solved it.
I have a function which should "toggle" a bar button item by changing between 2 images.
class Buttons {
func ToggleBarButton(button : UIBarButtonItem, name : String, location : BarButtonLocation, isEnabled : Bool, viewController : UIViewController) {
var iconName = name
if (!isEnabled) {
iconName += "EnabledIcon"
} else {
iconName += "DisabledIcon"
}
let newIcon = UIImage(named: iconName)
let newButton = UIBarButtonItem(image: newIcon, style: .Plain, target: self, action: button.action);
switch location {
case BarButtonLocation.Left:
viewController.navigationItem.leftBarButtonItem = newButton;
viewController.navigationItem.leftBarButtonItem?.tintColor = UIColor.blackColor();
case BarButtonLocation.SecondLeft:
viewController.navigationItem.leftBarButtonItems?[1] = newButton
viewController.navigationItem.leftBarButtonItems?[1].tintColor = UIColor.blackColor()
default:
return;
}
}
}
I also have a view controller class, in which there is the action of the bar button item.
class GradesViewController: UIViewController {
var isFilterEnabled = false
var isViewEnabled = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func filterButton_Pressed(sender: UIBarButtonItem) {
Buttons().ToggleBarButton(sender, name : "Filter", location: BarButtonLocation.Left, isEnabled: isFilterEnabled, viewController: self);
isFilterEnabled = !isFilterEnabled;
}
#IBAction func viewButton_Pressed(sender: UIBarButtonItem) {
Buttons().ToggleBarButton(sender, name : "View", location: BarButtonLocation.SecondLeft, isEnabled: isViewEnabled, viewController: self);
isViewEnabled = !isViewEnabled;
}
}
On first press it successfully changes the image to the enabled form, but on second press it doesn't do anything (press event doesn't even fire). I checked, and button.action is correctly identified as "filterButton_Pressed:". What's the problem, or is there an easier way to do this? Thanks for the answer in advance.
Put the break statement after each case and try.
And also remove the semi colons.
I just realized the problem was that I copied the code from the view controller to the button class, and didn't change target: self to target: viewController. But thanks for all the answers anyways...
I'm have a WebView:
func loadPage() {
let request = NSURLRequest (URL: url!)
myBrowser.loadRequest(request)
}
Which load RTF document from this URL:
var url: NSURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("Chapter1", ofType: "rtf")!)!
How can I save last scroll position to NSUserDefaults, to get it then (if user pause reading and resume it).
Or maybe other type or method to make bookmark in this case.
Note: Ive been trying this:
var myBookmark: CGPoint = CGPointMake(0, 0)
override func viewWillDisappear(animated: Bool) {
myBookmark = myBrowser.scrollView.contentOffset
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
myBrowser.scrollView.delegate = self
myBrowser.scrollView.setContentOffset(myBookmark, animated: false)
}
But it's not take any effect.
UPD1: translate my VAR name.
To save and retrieve your scrollview offset from NSUserDefaults, set your UIWebView's delegate and try this:
var viewLaidoutSubviews = false // <-- variable to prevent the viewDidLayoutSubviews code from happening more than once
// Save your content offset when the view disappears
override func viewWillDisappear(animated: Bool) {
var userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setValue(NSStringFromCGPoint(myBrowser.scrollView.contentOffset), forKey: "scroll_offset")
userDefaults.synchronize()
viewLaidoutSubviews = false
}
// Retrieve and set your content offset when the view re-appears
// and its subviews are first laid out
override func viewDidLayoutSubviews() {
if (!viewLaidoutSubviews) {
// If a scroll_offset is store in NSUserDefaults, use it
var userDefaults = NSUserDefaults.standardUserDefaults()
var scrollOffset:CGPoint? = CGPointFromString(userDefaults.valueForKey("scroll_offset") as? NSString)
if let position:CGPoint = scrollOffset {
myBrowser.scrollView.delegate = self
myBrowser.scrollView.setContentOffset(position, animated: false)
}
viewLaidoutSubviews = true
}
}
And utilize you UIWebView's webViewDidFinishLoad delegate method to update the scroll offset in the case that the web view didn't finish rendering before the view laid out its subviews:
// Retrieve and set your content offset once the web view has
// finished rendering (in case it hasn't finished rendering
// by the time the view's subviews were laid out)
func webViewDidFinishLoad(webView: UIWebView) {
// If a scroll_offset is store in NSUserDefaults, use it
var userDefaults = NSUserDefaults.standardUserDefaults()
var scrollOffset:CGPoint? = CGPointFromString(userDefaults.valueForKey("scroll_offset") as? NSString)
if let position:CGPoint = scrollOffset {
myBrowser.scrollView.delegate = self
myBrowser.scrollView.setContentOffset(position, animated: true)
}
}
But note, your NSUserDefaults value will also persist between app launches unless you delete it.