Update label in UIPageView - swift

Relatively new to Xcode/Swift, but have some experience in other languages.
I am at the infancy stages of an application and am trying to achieve the following functionality:
In a UIPageView environment, when the user swipes forward, the label (whose value is an integer) increases by 1. When the user swipes back, the label decreases by 1.
I have a single UIViewController as the content page with storyboard ID PageContentViewController.
Essentially, my thoughts are to declare an integer variable initially set to 0 and then create a swipedBack() and swipedRight() function which updates the value of the variable and then sets the text of the label on the PageContentViewController to the value of that variable. The functions then return the new PageContentViewController.
I am getting some peculiar behaviour. Namely, the app is building and running fine.
The initial screen loads with a label of 0. Then I swipe forward, and it updates to 1. BUT THEN... if i swipe forward again, the label stays as one. BUT THEN... if I keep going forward, it then acts as desired - goes to 2, then 3. Then if i reverse direction, it becomes even more peculiar.
If I'm at a value of four, then swipe back, It will decrease to 3. But then swiping back again it will go back to 4, then continuing swiping, it will behave as expected (4,3,2,1...). I obviously have a conceptual misunderstanding and would appreciate some help.
Here's my code:
import UIKit
class ViewController: UIPageViewController, UIPageViewControllerDataSource {
var arrPageTitle: NSArray = NSArray()
var t: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.setViewControllers([getViewControllerAtIndex()] as [UIViewController], direction: UIPageViewControllerNavigationDirection.forward, animated: false, completion: nil)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
return swipedBack()
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
return swipedForward()
}
func getViewControllerAtIndex() -> PageContentViewController {
let pageContentViewController = self.storyboard?.instantiateViewController(withIdentifier: "PageContentViewController") as! PageContentViewController
pageContentViewController.strTitle = "\(t)"
return pageContentViewController
}
func swipedForward() -> PageContentViewController {
let pageContentViewController = self.storyboard?.instantiateViewController(withIdentifier: "PageContentViewController") as! PageContentViewController
t = t + 1
pageContentViewController.strTitle = "\(t)"
return pageContentViewController
}
func swipedBack() -> PageContentViewController {
let pageContentViewController = self.storyboard?.instantiateViewController(withIdentifier: "PageContentViewController") as! PageContentViewController
t = t - 1
pageContentViewController.strTitle = "\(t)"
return pageContentViewController
}
}

I think this is probably because once you change directions, the most recent UIViewController is still in memory, and thus the delegate function is not called.
I would recommend using the pageViewControllerWillTransitionTo function instead. This should be called every time you swipe. Similarly you could use pageViewControllerDidFinishAnimating if you want the call to happen after you swipe.
In order to detect which direction you swiped, you could try and use a UISwipeGestureRecognizer at the same time, and call functions that will increment/decrement the variable value. Actually, now that I think about it, you may just be able to use this gesture recognizer without the delegate function. Up to you.

Related

Unable to lock rotation for one view controller in IOS10

Background
I have an app that uses AVFoundation in order to have a custom camera. This happens in the OCRViewController. When I take a picture I send the captured picture to a different view ImagePreviewViewController.
I am using Xcode 10.2.1 (10E1001) with Swift 5
The Goal
What I would like to achieve is to lock the orientation of the ImagePreviewViewController to the original orientation of the image. I already know how to get the orientation of the image but I am not able to lock the orientation of the view.
I get the image rotation as such: let imageOri = capturedImage?.imageOrientation
What did I try?
I tried the accepted answers at and several other sources:
How to lock orientation just for one view controller?
How to lock orientation of one view controller to portrait mode only in Swift
https://www.hackingwithswift.com/example-code/uikit/how-to-lock-a-view-controllers-orientation-using-supportedinterfaceorientations
Reading the documentation at https://developer.apple.com/documentation/uikit/uiviewcontroller#//apple_ref/occ/instm/UIViewController/supportedInterfaceOrientations under Handling View Rotation the following is stated:
I also tried the many suggested solutions while writing this query, however, the majority appears to use the following approach (or a variation of it), and it does not work for me.
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
}
override func shouldAutorotate() -> Bool{
return false
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
return UIInterfaceOrientation.Portrait
}
As of iOS 8, all rotation-related methods are deprecated. Instead, rotations are treated as a change in the size of the view controller’s view and are therefore reported using the viewWillTransition(to:with:) method.
However, I am not sure how to progress from here.
Interesting code snippets
The following method is in my OCRViewController, here I instantiate the ImagePreviewViewController and attach the captured image.
func displayCapturedPhoto(capturedPhoto : UIImage) {
let imagePreviewViewController = storyboard?.instantiateViewController(withIdentifier: "ImagePreviewViewController") as! ImagePreviewViewController
imagePreviewViewController.capturedImage = capturedPhoto
navigationController?.pushViewController(imagePreviewViewController, animated: true)
}
Using the below override function inside my ImagePreviewViewController I am able to detect the orientation of the view controller.
override func viewDidAppear(_ animated: Bool) {
if UIDevice.current.orientation.isLandscape {
print("Landscape")
} else {
print("Portrait")
}
}
To restrict the rotation of one screen, use this.
In AppDelegate
var restrictRotation = Bool()
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if !restrictRotation {
return .portrait
} else {
return .all
}
}
In your viewcontroller add the function,
func restrictRotation(restrict : Bool) -> Void {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
appDelegate?.restrictRotation = restrict
}
In the ViewDidload() method, call the function to disable rotation.
self.restrictRotation(restrict: false)
in viewWillDisappear() method, call the function to enable rotation.
self.restrictRotation(restrict: true)

UITextField.text value won't change on GooglePlaceAutocomplete

I am working on a form app on iOS 11 using Swift 4. I'd like to put the return of Google's selector (PlaceAutocomplete) in the UITextField of one of the cells of my UITableView. The issue is that despite assigning new values, the text fields remain blank. After debugging for a while it seems that something is discarding the content of my UITextField when the location is being selected.
These are the GooglePlaceAutocomplete callbacks with the result value assignment :
extension MyUITableViewController, GMSAutocompleteViewControllerDelegate {
func viewController(_ viewController: GMSAutocompleteViewController, didAutocompleteWith place: GMSPlace) {
print("Place name: \(place.name)")
let indexPath4 = IndexPath(row: 4, section: 0)
let cell4 = tableView.cellForRow(at: indexPath4) as! InputPlacePicker
cell4.inputTextField.text = place.name
dismiss(animated: true, completion: nil)
}
func viewController(_ viewController: GMSAutocompleteViewController, didFailAutocompleteWithError error: Error) {
print("Error: ", error.localizedDescription)
}
func wasCancelled(_ viewController: GMSAutocompleteViewController) {
dismiss(animated: true, completion: nil)
}
}
And this is the call to GooglePlaceAutocomplete
#objc func pickPlace() {
let autocompleteController = GMSAutocompleteViewController()
autocompleteController.delegate = self
present(autocompleteController, animated: true, completion: nil)
}
Changing the cells directly is probably not a good idea, since everytime the UITableView is reloaded, cells get recycled and depending on your tableView(_:cellForRowAt:) implementation, your UITextFieldmight get overwritten or end up somewhere else.
It is a better practice to have some kind of model or state in your view controller, which holds the state for each cell in the different sections.
Change this state (for instance a list of place names) in your GooglePlacesAutocomplete delegate and call reloadData on your tableView to get the new data from the changed state.
In your tableView(_:cellForRowAt:) implementation, you set inputTextField.text = place.name before you return it, this way all cells should end up with the right place name from your internal state.
Okay! I understood what you said Kie and you're right. I'll implement something to save my field values.
But in my case, it is the GMSAutocompleteViewController which reloads my UITableView because it was called with present(). I changed that to add it to my navigationController and now my field persists.
Thanks

Using a UISegmentedControl like a UISwitch

Is it possible to use a UISegmentedControl with 3 segments as if it was a three-way UISwitch? I tried to use one as a currency selector in the settings section of my app with no luck, it keeps reseting to the first segment when I switch views and that creates a big mess.
I proceeded like that:
IBAction func currencySelection(_ sender: Any) {
switch segmentedControl.selectedSegmentIndex {
case 0:
WalletViewController.currencyUSD = true
WalletViewController.currencyEUR = false
WalletViewController.currencyGBP = false
MainViewController().refreshPrices()
print(0)
case 1:
WalletViewController.currencyUSD = false
WalletViewController.currencyEUR = true
WalletViewController.currencyGBP = false
MainViewController().refreshPrices()
print(1)
case 2:
WalletViewController.currencyUSD = false
WalletViewController.currencyEUR = false
WalletViewController.currencyGBP = true
MainViewController().refreshPrices()
print(2)
default:
break
}
}
The UISegmentedControl is implemented in the
SettingsViewController of the app to choose between currencies to
display in the MainViewController.
(Taken from a comment in #pacification's answer.)
This was the missing piece I was looking for. It provides a lot of context.
TL;DR;
Yes, you can use a three segment UISegmentedControl as a three-way switch. The only real requirement is that you can have only one value or state selected.
But I wasn't grasping why your code referred to two view controllers and some of switching views resulting in resetting the segment. One very good way to do what you want is to:
Have MainViewController present SettingsViewController. Presenting it modally means the user is only doing one thing at a time. When they are making setting changes, you do not want them adding new currency values.
Create a delegate protocol in SettingsViewController and make MainViewController conform to it. This tightly-couples changes made to the settings to the view controller interested in what those changes are.
Here's a template for what I'm talking about:
SettingsViewController:
protocol SettingsVCDelegate {
func currencyChanged(sender: SettingsViewController)
}
class SettingsViewController : UIViewController {
var delegate:SettingsVCDelegate! = nil
var currency:Int = 0
#IBAction func valueChanged(_ sender: UISegmentedControl) {
currency = sender.selectSegmentIndex
delegate.currencyChanged(sender:self)
}
}
MainViewController:
class MainViewController: UIViewController, SettingsVCDelegate {
var currency:Int = 0
let settingsVC = SettingsViewController()
override func viewDidLoad() {
super.viewDidLoad()
settingsVC.delegate = self
}
func presentSettings() {
present(settingsVC, animated: true, completion: nil)
}
func currencyChanged(sender:SettingsViewController) {
currency = sender.currency
}
}
You can also create an enum of type Int to make your code more readable, naming each value as currencyUSD, currencyEUR, and currencyGBP. I'll leave that to you as a learning exercise.
it keeps reseting to the first segment when I switch views
yes, it is. to avoid this situation you should set the correct switch value to the segmentedControl.selectedSegmentIndex every time when you load your view with UISegmentedControl.
UPD
Ok, the behavior of MainViewController can be similar to this:
final class MainViewController: UIViewController {
private var savedValue = 0
override func viewDidLoad() {
super.viewDidLoad()
}
func openSettingsController() {
let viewController = SettingsController.instantiate() // simplify code a bit. use the full controller initialization
viewController.configure(value: savedValue, onValueChanged: { [unowned self] value in
self.savedValue = value
})
navigationController?.pushViewController(viewController, animated: true)
}
}
And the SettingsViewController:
final class SettingsViewController: UIViewController {
#IBOutlet weak var segmentedControl: UISegmentedControl!
private var value: Int = 0
var onValueChanged: ((Int) -> Void)?
func configure(value: Int, onValueChanged: #escaping ((Int) -> Void)) {
self.value = value
self.onValueChanged = onValueChanged
}
override func viewDidLoad() {
super.viewDidLoad()
segmentedControl.selectedSegmentIndex = value
}
#IBAction func valueChanged(_ sender: UISegmentedControl) {
onValueChanged?(sender.selectedSegmentIndex)
}
}
The main idea that you should keep your selected value if you moving from SettingsViewController. For this thing you can create closure
var onValueChanged: ((Int) -> Void)?
that pass back to MainViewController the selected UISegmentedControl value. And in future when you will open the SettingsViewController again you just configure() this value and set it to UI.

Adding a new page into PageViewController

I am a Swift noob and am making a simple weather app. I used the Page-Based Application template.
The problem I have is the following:
When the user adds a city I call addCity and successfully append the new city name to my cities array. When I print that array in that function, it shows the new city at the end.
However, the function viewControllerAtIndex that creates a new page seems to use the old version of that array, without the new city appended. When I print the cities array, it is missing the new city name. Therefore, when the users swipes there is won't be a new page for the new city rendered. The user has to restart the app in order for the new city to show up.
I created a screen capture video to illustrate the problem.
https://youtu.be/DbMqgJ0lONk
(the cities array should also show "London", I think I just didn't restart the app)
I would appreciate any help here!
import UIKit
class ModelController: NSObject, UIPageViewControllerDataSource {
var rootViewController = RootViewController()
var cities = [""]
let defaults = UserDefaults.standard
override init() {
super.init()
self.cities = self.defaults.stringArray(forKey: "SavedStringArray") ?? [String]()
if self.cities == [""] || self.cities.count == 0 {
self.cities = ["Current Location"]
}
}
func addCity(name:String) {
self.cities.append(name)
self.defaults.set(self.cities, forKey: "SavedStringArray")
print ("cities from addCity:")
print (self.cities)
}
func viewControllerAtIndex(_ index: Int, storyboard: UIStoryboard) -> DataViewController? {
// Return the data view controller for the given index.
if (self.cities.count == 0) || (index >= self.cities.count) {
return nil
}
// Create a new view controller and pass suitable data.
let dataViewController = storyboard.instantiateViewController(withIdentifier: "DataViewController") as! DataViewController
//get city name
dataViewController.dataObject = self.cities[index]
print ("cities in viewControllerAtIndex:")
print (self.cities)
return dataViewController
}
func indexOfViewController(_ viewController: DataViewController) -> Int {
// Return the index of the given data view controller.
// For simplicity, this implementation uses a static array of model objects and the view controller stores the model object; you can therefore use the model object to identify the index.
return self.cities.index(of: viewController.dataObject) ?? NSNotFound
}
// MARK: - Page View Controller Data Source
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
var index = self.indexOfViewController(viewController as! DataViewController)
if (index == 0) || (index == NSNotFound) {
return nil
}
index -= 1
return self.viewControllerAtIndex(index, storyboard: viewController.storyboard!)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
var index = self.indexOfViewController(viewController as! DataViewController)
if index == NSNotFound {
return nil
}
index += 1
if index == self.cities.count {
return nil
}
return self.viewControllerAtIndex(index, storyboard: viewController.storyboard!)
}
}
The problem is, that you use two different instances of ModelController. One for the RootViewController and another in the TableViewController. They don't know each other.
A couple of options to address the problem:
1.) Hand over the same instance of ModelController to TableViewController when you segue into it.
E.g. by adding this prepare(for segue:) method toRootViewController`
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if(segue.identifier == "Locations") {
let destVC: TableViewController = segue.destination as! TableViewController;
destVC.modelViewController = self.modelController;
}
}
This will ensure that the same ModelController will be handed over.
Note: you have to add this identifier ("Locations") to the segue going from Edit-button to the TableViewController scene.
Note 2: this code is untested and doesn't probably even compile. I'm not having Xcode available right now.
2.) Ensure that there cannot be more than one instance of ModelController (Singleton)
One random web link: https://thatthinginswift.com/singletons/

NSTableView detect NSTableColumn for selected cell at start of cell edition

I'm trying to programatically get get a a column.identifier for the cell that is being edited. I'm trying to get by registering my NSViewController for NSControlTextDidBeginEditingNotification and when I get the notification I track the data by mouse location:
var selectedRow = -1
var selectedColumn: NSTableColumn?
func editingStarted(notification: NSNotification) {
selectedRow = participantTable.rowAtPoint(participantTable.convertPoint(NSEvent.mouseLocation(), fromView: nil))
let columnIndex = participantTable.columnAtPoint(participantTable.convertPoint(NSEvent.mouseLocation(), fromView: nil))
selectedColumn = participantTable.tableColumns[columnIndex]
}
The problem I have is that the mouse location is giving me the wrong data, is there a way to get the mouse location based on the location of the table, or could there be a better way to get this information?
PS. My NSViewController is NSTableViewDelegate and NSTableViewDataSource, my NSTableView is View Based and connects to an ArrayController which updates correctly, and I could go to my Model object and detect changes in the willSet or didSet properties, but I need to detect when a change is being made by the user and this is why I need to detect the change before it happens on the NSTableView.
This question is 1 year old but I got the same issue today and fixed it. People helped me a lot here so I will contribute myself if someone found this thread.
Here is the solution :
1/ Add the NSTextFieldDelegate to your ViewController :
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, NSTextFieldDelegate {
2/ When a user wants to edit a cell, he had first to select the row. So we will detect that with this delegate function :
func tableViewSelectionDidChange(_ notification: Notification) {
let selectedRow = self.tableView.selectedRow
// If the user selected a row. (When no row is selected, the index is -1)
if (selectedRow > -1) {
let myCell = self.tableView.view(atColumn: self.tableView.column(withIdentifier: "myColumnIdentifier"), row: selectedRow, makeIfNecessary: true) as! NSTableCellView
// Get the textField to detect and add it the delegate
let textField = myCell.textField
textField?.delegate = self
}
}
3/ When the user will edit the cell, we can get the event (and the data) with 3 different functions. Pick the ones you need :
override func controlTextDidBeginEditing(_ obj: Notification) {
// Get the data when the user begin to write
}
override func controlTextDidEndEditing(_ obj: Notification) {
// Get the data when the user stopped to write
}
override func controlTextDidChange(_ obj: Notification) {
// Get the data every time the user writes a character
}