I am very new to Swift and programming in general.
I am trying to add a Pop Up Picker on a textfield and when the user selects the item from the picker, they can press OK with that item displayed in the textfield and the PopUp disappear.
I have successfully implemented this with a Pop Up Date Picker as I have used this from GutHub successfully. I thought it would be easy to mimic this code for my Pop Up Picker which has proven to be more difficult than expected.
I have a sepeate XIB file which holds the View with the Picker and OK Button. I then have 2 swift files one for the PopViewController and the other for the PopPicker.
Not even sure if this code is correct but the error I am getting is that my Picker does not conform to protocol. Code is below for both files.
PopEngineViewController
import UIKit
protocol EnginePickerViewControllerDelegate : class {
func enginePickerVCDismissed(string: UITextField?)
}
class PopEngineViewController: UIViewController {
#IBOutlet weak var container: UIView!
#IBOutlet weak var enginePicker: UIPickerView!
weak var delegate : EnginePickerViewControllerDelegate?
override convenience init() {
self.init(nibName: "PopEnginePicker", bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidDisappear(animated: Bool) {
self.delegate?.enginePickerVCDismissed(nil)
}
}
and PopEnginePicker
import UIKit
public class PopEnginePicker : NSObject, UIPopoverPresentationControllerDelegate, EnginePickerViewControllerDelegate {
public typealias PopEnginePickerCallback = (forTextField : UITextField)->()
var enginePickerVC : PopEngineViewController
var popover : UIPopoverPresentationController?
var textField : UITextField!
var dataChanged : PopEnginePickerCallback?
var presented = false
var offset : CGFloat = 8.0
public init(forTextField: UITextField) {
enginePickerVC = PopEngineViewController()
self.textField = forTextField
super.init()
}
public func pick(inViewController : UIViewController, dataChanged : PopEnginePickerCallback) {
if presented {
return // we are busy
}
enginePickerVC.delegate = self
enginePickerVC.modalPresentationStyle = UIModalPresentationStyle.Popover
enginePickerVC.preferredContentSize = CGSizeMake(500,208)
popover = enginePickerVC.popoverPresentationController
if let _popover = popover {
_popover.sourceView = textField
_popover.sourceRect = CGRectMake(self.offset,textField.bounds.size.height,0,0)
_popover.delegate = self
self.dataChanged = dataChanged
inViewController.presentViewController(enginePickerVC, animated: true, completion: nil)
presented = true
}
}
func adaptivePresentationStyleForPresentationController(PC: UIPresentationController!) -> UIModalPresentationStyle {
return .None
}
}
Not even sure if I am going down the complete wrong path however I want it to look like the below as I have done with the date picker as it shows in the link below:
http://coding.tabasoft.it/ios/a-simple-ios8-popdatepicker/
Related
I am trying to use MVVM. I am going to VC2 from VC1. I am updating the viewModel.fromVC = 1, but the value is not updating in the VC2.
Here is what I mean:
There is a viewModel, in it there is a var fromVC = Int(). Now, in vc1, I am calling the viewModel as
let viewModel = viewModel().
Now, on the tap of button, I am updating the viewModel.fromVC = 8. And, moving to the next screen. In the next screen, when I print fromVC then I get the value as 0 instead of 8.
This is how the VC2 looks like
class VC2 {
let viewModel = viewModel()
func abc() {
print(viewModel.fromVC)
}
}
Now, I am calling abc() in viewDidLoad and the fromVC is printed as 0 instead of 8. Any help?
For the MVVM pattern you need to understand that it's a layer split in 2 different parts: Inputs & Outputs.
Int terms of inputs, your viewModel needs to catch every event from the viewController, and for the Outputs, this is the way were the viewModel will send data (correctly formatted) to the viewController.
So basically, if we have a viewController like this:
final class HomeViewController: UIViewController {
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
titleLabel.text = "toto"
}
}
We need to extract the responsibilities to a viewModel, since the viewController is handling the touchUp event, and owning the data to bring to th label.
By Extracting this, you will keep the responsibility correctly decided and after all, you'll be able to test your viewModel correctly 🙌
So how to do it? Easy, let's take a look to our futur viewModel:
final class HomeViewModel {
// MARK: - Private properties
private let title: String
// MARK: - Initializer
init(title: String) {
self.title = title
}
// MARK: - Outputs
var titleText: ((String) -> Void)?
// MARK: - Inputs
func viewDidLoad() {
titleText?("")
}
func buttonDidPress() {
titleText?(title)
}
}
So now, by doing this, you are keeping safe the different responsibilities, let's see how to bind our viewModel to our previous viewController :
final class HomeViewController: UIViewController {
// MARK: - public var
var viewModel: HomeViewModel!
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
// MARK: - Private func
private func bind(to viewModel: HomeViewModel) {
viewModel.titleText = { [weak self] title in
self?.titleLabel.text = title
}
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
viewModel.buttonDidPress()
}
}
So one thing is missing, you'll asking me "but how to initialise our viewModel inside the viewController?"
Basically you should once again extract responsibilities, you could have a Screens layer which would have the responsibility to create the view like this:
final class Screens {
// MARK: - Properties
private let storyboard = UIStoryboard(name: StoryboardName, bundle: Bundle(for: Screens.self))
// MARK: - Home View Controller
func createHomeViewController(with title: String) -> HomeViewController {
let viewModel = HomeViewModel(title: title)
let viewController = storyboard.instantiateViewController(withIdentifier: "Home") as! HomeViewController
viewController.viewModel = viewModel
return viewController
}
}
And finally do something like this:
let screens = Screens()
let homeViewController = screens.createHomeViewController(with: "Toto")
But the main subject was to bring the possibility to test it correctly, so how to do it? very easy!
import XCTest
#testable import mvvmApp
final class HomeViewModelTests: XCTestCase {
func testGivenAHomeViewModel_WhenViewDidLoad_titleLabelTextIsEmpty() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
viewModel.titleText = { title in
XCTAssertEqual(title, "")
expectation.fulfill()
}
viewModel.viewDidLoad()
waitForExpectations(timeout: 1.0, handler: nil)
}
func testGivenAHomeViewModel_WhenButtonDidPress_titleLabelTextIsCorrectlyReturned() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
var counter = 0
viewModel.titleText = { title in
if counter == 1 {
XCTAssertEqual(title, "toto")
expectation.fulfill()
}
counter += 1
}
viewModel.viewDidLoad()
viewModel.buttonDidPress()
waitForExpectations(timeout: 1.0, handler: nil)
}
}
And that's it 💪
Because I needed a UINavigationController inside another UINavigationController (and this is not possible by default), I created a UIViewController that acts as a UINavigationController, but dit does not subclass from UINavigationController.
The second NavigationController (the one that does to subclass UINavigationController), presents (depending on the ViewModel's state) a controller.
This is the custom NavigationController:
class OnboardingViewController: UIViewController {
// MARK: - Outlets
#IBOutlet weak var containerView: UIView!
// MARK: - Internal
private var viewModel: OnboardingViewModel = OnboardingViewModel()
// MARK: - View flow
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.isHidden = true
navigateToNextFlow()
}
override var preferredFocusEnvironments: [UIFocusEnvironment] {
switch viewModel.state {
case .recommendations: return recommendationsController.preferredFocusEnvironments
default: return super.preferredFocusEnvironments
}
}
// MARK: - Handlers
var navigateToNextHandler: (() -> Void)?
// MARK: - Controllers
private var recommendationsController: OnboardingRecommendationsViewController {
let controller = UIViewController.instantiate(from: "Onboarding Recommendations") as OnboardingRecommendationsViewController
controller.navigateToNextHandler = { [unowned self] in
self.viewModel.state = .done
self.navigateToNextFlow(animated: true)
}
return controller
}
// MARK: - Navigation
private func navigateToNextFlow(animated: Bool = false) {
switch viewModel.state {
case .recommendations:
add(child: recommendationsController, to: containerView)
case .done:
viewModel.finish()
navigateToNextHandler?()
}
updateFocusIfNeeded()
setNeedsFocusUpdate()
}
}
This is the childViewController:
class OnboardingRecommendationsViewController: UIViewController {
// MARK: - Outlets
#IBOutlet weak var onOffButton: UIButton!
#IBOutlet weak var finishButton: UIButton!
// MARK: - Internal
fileprivate let viewModel: OnboardingRecommendationsViewModel = OnboardingRecommendationsViewModel()
// MARK: - View flow
override func viewDidLoad() {
super.viewDidLoad()
setupLabels()
setupOnOffButton()
}
// MARK: - Handlers
var navigateToNextHandler: (() -> Void)?
// MARK: - Focus
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [finishButton, onOffButton]
}
}
The finishButton is beneath the onOffButton in the storyboard. I'm trying to set the initial focus on the finishButton instead of the onOffButton. But the user can focus the onOffButton if he wants.
Whatever I try, it just doesn't work. The preferredFocusEnvironments gets called, but the focus of the buttons stays in the wrong order.
What am I doing wrong?
Sorry for the late answer. It turned out that the viewController I was pushing, defined was as let, so I pushed a few instances over themselves, and that's why it seems that the preferredFocusEnvironments was not working. I actually saw a new instance of the ViewController with another initial focus order. Changing the variable declaration of the viewController from let to lazy var did the trick. So, in the end, it had really nothing to do with preferredFocusEnvironments not working. But thanks for the input!
Did you try like this?
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [finishButton]
}
Or you can disable userInteraction for onOffButton until finishButton gets focused. (Not a good solution though)
You should set restoresFocusAfterTransition = false to avoid the default behavior. And then, in preferredFocusEnvironments return the view you want to focus
New to Swift. I have a simple UI: put a UINavigationBar on top of a UIWebView, and a the right bar button item to have an action that shows a menu, to allow the use choose different pages to show in the web view.
Show the view controller looks like:
class ViewController: UIViewController {
#IBOutlet weak var webView: UIWebView!
#IBOutlet weak var menu: UIBarMenuItem!
#IBOutlet weak var viewnav: UIView!
override func viewDidLoad() {
super.viewDidLoad();
let url = URL(string:"about:blank")
let req = URLRequest(url:url!)
webView.loadRequest(req)
}
#obj func dummy(){
}
#IBAction func MenuShow(sender: UIBarButtonItem){
let menu = UIMenuController.shared
viewnav.becomeFirstResponder()
menu.setTargetRect(viewnav.frame, in:viewnav)
let dummy = UIMenuItem(title:"Dummy", action: #selector(dummy))
menu.menuItems = [dummy]
menu.setMenuVisible(true, animated: true)
//for test only; should move to menu item actions
let url = URL(string:"https://www.apple.com")
let req = URLRequest(url:url!)
webView.loadRequest(req)
}
}
(I have connected the web view, the bar button to the UI object; for viewnav I tried adding a new dummy view in Main.storyboard or using the existing navigation bar, both have the same result)
The resulting app shows the empty page and when I hit the menu button, jumps into Apple's home page, so the above code runs as expected. But the menu didn't show up, so what is wrong for the code above?
(there are a few other simular questions like this, but they didn't seem to solving the problem)
This answer gives the solution:
override var canBecomeFirstResponder: Bool {
return true
}
And add this line to the viewDidLoad method
view.becomeFirstResponder()
Full version:
class ViewController: UIViewController {
#IBOutlet weak var webView: UIWebView!
#IBOutlet weak var menuButton: UIBarMenuItem!
override func viewDidLoad() {
super.viewDidLoad();
let url = URL(string:"about:blank")
let req = URLRequest(url:url!)
webView.loadRequest(req)
view.becomeFirstResponder()
let menu = UIMenuController.shared
let dummy = UIMenuItem(title:"Dummy", action: #selector(dummy))
menu.menuItems = [dummy]
}
override var canBecomeFirstResponder: Bool {
return true
}
#obj func dummy(){
let url = URL(string:"https://www.apple.com")
let req = URLRequest(url:url!)
webView.loadRequest(req)
menu.setMenuVisible(true, animated: false)
}
#IBAction func MenuShow(sender: UIBarButtonItem){
let menu = UIMenuController.shared
let bv = menuButton.value(forKey: "view") as? UIView
menu.setTargetRect(bv!.frame, in:view)
menu.setMenuVisible(true, animated: true)
}
}
I have a small project with an NSTextview and a delegate that catches changes in text, as below. The object EditViewHandler works fine when it's a global but crashes when text is added to the view if it's local to viewDidLoad(). So this is obviously the wrong way to do it but what would be the correct way of doing this:
#IBOutlet var EditPaneOutlet: NSTextView!
override func viewDidLoad() {
super.viewDidLoad()
let e = EditViewHandler( EditPaneOutlet: EditPaneOutlet )
}
class EditViewHandler : NSObject, NSTextViewDelegate {
var EditPaneOutlet: NSTextView! = nil
init( EditPaneOutlet: NSTextView ) {
super.init()
self.EditPaneOutlet = EditPaneOutlet
self.EditPaneOutlet!.delegate = self
}
func textDidChange(_ notification: Notification) {
print( "text changed")
}
}
I have a very strange behavior on a NSTextField.
I update the value of the NSTextField through a delegate. Sometimes it gets updated and sometimes not. I issued a print statement before to ensure that I have the correct value. What the print statement shows and what is being displayed on the NSTextField is different.
Any idea what could be the root cause ?
import Cocoa
var mtserialport = MTSerialHandler()
class ManualViewController: NSViewController, MTSerialHandlerDelegate {
#IBOutlet var txtStatus : NSTextField!
#IBOutlet var txtQueue : NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
init_ctrl()
// Delegates
mtserialport.delegate = self
}
func init_ctrl() {
self.txtQueue.stringValue = "0"
}
// This is the function called from a delegate
// mt_serialport delegate
// print shows updateQueue:0 or 1, textQueue would stay to a previous value. i.e:3
func updateQueue(qu: UInt) {
print("updateQueue:" + String(qu))
self.txtQueue.stringValue = String(qu)
}
}