Trying to create a custom drop-down and facing issues in passing on data and retrieving the user selection from the custom drop-down. The solution however works when I use static var.
In the below code, I have a viewController class from where I am programmatically calling a popOver.
let countryDropDown = customDropdown()
#IBAction func btMultiCountry(_ sender: Any) {
ViewController.countryDropDown.removeAll() //fuction to clear the array before load
for c in g.country{ //Array from which all items are loaded on the custom dropdown
countryDropDown.addItems(labelText: c.countryName!, toggleState: 1) //passing the values in the add function to update the Array
}
countryDropDown.showDropdown(btMultiCountry) //programatically calling the function to display popover that has the collection view with the countries as items.
}
class customDropdown: NSViewController {
#IBOutlet weak var cvDropDown: NSCollectionView!
var pop = NSPopover() // if I put static var, the popover closes with the OK button as required, however, without static the OK button doest do anything
var dropDownLabelText = [String]() //if I put static var, the values are shown on the collection view without any issue. Without static the array is blank that was load from additems function
var dropDownToggle = [Int]() // same as above
var selectedItemsIndex: [Int] {
var a = [Int]()
for (i,t) in dropDownToggle.enumerated() {
if t == 1 {
a.append(i)
print(a)
}
}
return a
}
func showDropdown(_ sender: NSButton){
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
let VC = storyboard.instantiateController(withIdentifier: "dropDownForm") as? customDropdown
pop.contentViewController = VC
pop.behavior = NSPopover.Behavior.transient
pop.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.minY)
print(pop)
}
#IBAction func btOK(_ sender: Any) { //this function does not close the popover if there is not static mention above
pop.close()
}
I tried putting static when declaring the variables and it worked. I do not want static and want to create different instances of the class that gives me selectedItemsIndex
For eg:
let countryDropDown = customDropdown()
let personDropDown = customDropdown()
I should be able to get a different values for countryDropDown.selectedItemsIndex and personDropDown.selectedItemsIndex
Any help is appreciated.
Related
I'm using modal sheets (slide down from top) to get user input. I currently have 2 that I think are identical except for the UI, each a NIB + NSWindowController-subclass pair. One works as expected, binding input to an array controller and table view. When trying to use the other, the window property of the NSWindowController is nil.
This code works:
#IBAction func addItemButtonClicked(_ button: NSButton) {
let window = document?.windowForSheet
let windowController = NewItemSheetController()
windowController.typeChoices = newItemSheetTypeChoices
windowController.windowTitle = newItemSheetTitle
print(#function, windowController.window) // output below
window?.beginSheet(windowController.window!, completionHandler: { response in
// The sheet has finished. Did user click OK?
if response == NSApplication.ModalResponse.OK {
let structure = (self.newItemSheetController?.structure)!
self.document?.dataSource.structures.append(structure)
}
// All done with window controller.
self.newItemSheetController = nil
})
newItemSheetController = windowController
}
The output of the print statement: "addItemButtonClicked(_:) Optional()"
This code doesn't:
#IBAction func addItemButtonClicked(_ button: NSButton) {
let window = document?.windowForSheet
let windowController = NewRecurrenceItemSheetController()
windowController.windowTitle = newItemSheetTitle
print(#function, windowController.window)
window?.beginSheet(windowController.window!, completionHandler: { response in
// The sheet has finished. Did user click OK?
if response == NSApplication.ModalResponse.OK {
let recurrence = (self.newItemSheetController?.recurrence)!
self.document?.dataSource.recurrences.append(recurrence)
}
// All done with window controller.
self.newItemSheetController = nil
})
newItemSheetController = windowController
}
The output of the print statement: "addItemButtonClicked(_:) nil"
Classes NewItemSheetController and NewRecurrenceItemSheetController are subclasses of NSWindowController and differ only with NSNib.Name and properties related to differing UI. As far as I can see, the XIBs and Buttons are "wired" similarly. The XIBs use corresponding File's Owner. Window objects have default class.
#objcMembers
class NewItemSheetController: NSWindowController {
/// other properties here
dynamic var windowTitle: String = "Add New Item"
override var windowNibName: NSNib.Name? {
return NSNib.Name(stringLiteral: "NewItemSheetController")
}
override func windowDidLoad() {
super.windowDidLoad()
titleLabel.stringValue = windowTitle
}
// MARK: - Outlets
#IBOutlet weak var titleLabel: NSTextField!
#IBOutlet weak var typeChooser: NSPopUpButton!
// MARK: - Actions
#IBAction func okayButtonClicked(_ sender: NSButton) {
window?.endEditing(for: nil)
dismiss(with: NSApplication.ModalResponse.OK)
}
#IBAction func cancelButtonClicked(_ sender: NSButton) {
dismiss(with: NSApplication.ModalResponse.cancel)
}
func dismiss(with response: NSApplication.ModalResponse) {
window?.sheetParent?.endSheet(window!, returnCode: response)
}
}
Why does one return instantiate a windowController object with a nil-valued window property?
In Interface Builder, the XIB Window needed to be attached to File's Owner with a Window outlet and delegate. Thanks #Willeke.
I am working on saving data process,
because my data is small, so I want to save in local data storage.
I want a make time schedule.
Because I have single 35 buttons and label(each buttons and label are paired), So I am using UIButton tag now. And I have a problem on using other local data "tag".
I tired to make new func on global swift file(same swift file). but it is not able to call tag, because tag is only can use when UIbutton pressed.
I want to use tag property on viewDidLoad(), not only in UIbuttonPressed.
override func viewDidLoad() {
if let items = UserDefaults.standard.array(forKey: "SubjectList") as? [String] {
subjectArray = items
UITextLabel**1**.text = subjectArray**[1-1]**
}
}
#IBAction func buttonPressed(_ sender: UIButton) {
var textField = UITextField()
let newItem = textField.text!
if sender.tag == 1{
self.subjectArray[sender.tag-1] = newItem
print("1")
}
UserDefaults.standard.set(self.subjectArray, forKey: "SubjectList")
}
Create outlet collections for all the paired labels and buttons
#IBOutlet weak var allLbls:[UILabel]!
#IBOutlet weak var allBts:[UIButton]!
In viewDidLoad
if let items = UserDefaults.standard.array(forKey: "SubjectList") as? [String] {
subjectArray = items
subjectArray.indices?.forEach {
allLbls[$0].text = subjectArray[$0]
allBts[$0].tag = $0
}
}
I have two xib file, one which shows login view and another which shows the steps what to do after the login is successful. I am having hard time to make it work. I have created macos project not ios and using safariservices so that it will work for the safari extension either.
Here is what i have done
import SafariServices
class SafariExtensionViewController: SFSafariExtensionViewController {
#IBOutlet weak var passwordMessage: NSTextField!
#IBOutlet weak var emailMessage: NSTextField!
#IBOutlet weak var message: NSTextField!
#IBOutlet weak var email: NSTextField!
#IBOutlet weak var password: NSSecureTextField!
static let shared = SafariExtensionViewController()
override func viewDidLoad() {
self.preferredContentSize = NSSize(width: 300, height: 250)
message.stringValue = ""
emailMessage.stringValue = ""
passwordMessage.stringValue = ""
}
override func viewDidAppear() {
if let storedEmail = UserDefaults.standard.object(forKey: "email") as? String {
if let stepView = Bundle.mainBundle.loadNibNamed(NSNib.Name(rawValue: "ExtensionStepsViewController"), owner: nil, topLevelObjects: nil)[0] {
self.view.addSubview(stepView)
}
}
}
#IBAction func userLogin(_ sender: Any) {
let providedEmailAddress = email.stringValue
let providedPassword = password.stringValue
let isEmailAddressValid = isValidEmailAddress(emailAddressString: providedEmailAddress)
self.message.stringValue = ""
emailMessage.stringValue = ""
passwordMessage.stringValue = ""
if isEmailAddressValid && providedPassword.count > 0 {
/* login process is handled here and store the email in local storage /*
/* TODO for now email is not stored in browser localstorage which has to be fixed */
let controller = "ExtensionStepsViewController"
let subview = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: controller), bundle: nil)
self.view.addSubview(subview.view)
}
}
}
This way i get error like Type Bool has no subscript members my file structure looks something like this.
SafariExtensionViewController.xib (main one which is shown initially
with login screen)
SafariExtensionViewController.swift
ExtensionStepsViewController.xib(this view should be shown when user
is logged in instead of login screen)
ExtensionStepsViewController.swift
I am using xcode 10, swift 4, everything new.
UPDATE
I used the following block both in viewDidAppear(if there is email in localstorage then show extension steps view instead of login screen) and inside login function when the login is success but it does not navigate to that ExtensionStepsView
let controller = "ExtensionStepsViewController"
let subview = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: controller), bundle: nil)
self.view.addSubview(subview.view)
Use case is show login at initial but if user is logged in then show another view but issue is now the view are merged
You got the error "Type Bool has no subscript members" because loadNibNamed(_:owner:topLevelObjects:) method of Bundle returns Bool struct that has no subscript members so you can't write like
true[0]
How to use this method correctly see the link and example from there:
var topLevelObjects : NSArray?
if Bundle.main.loadNibNamed(NSNib.Name(rawValue: "ExtensionStepsViewController"), owner: self, topLevelObjects: &topLevelObjects) {
let topLevelObjects!.first(where: { $0 is NSView }) as? NSView
}
Views were merged because you didn't remove previous views from the superview and added view from ExtensionStepsViewController to the same superview.
You can do the following steps to complete your issue:
Make SafariExtensionViewController inherited from SFSafariExtensionViewController that will be container (and parent) for two child view controllers such as LoginViewController and ExtensionStepsViewController and will be used to navigate between ones.
Make separately LoginViewController and ExtensionStepsViewController (both inherited from simple NSViewController) and its xibs.
Right after user logins transit from LoginViewController to ExtensionStepsViewController
As an example but instead of ParentViewController you have to use your implementation SafariExtensionViewController as I explain above in the first step.
public protocol LoginViewControllerDelegate: class {
func loginViewControllerDidLoginSuccessful(_ loginVC: LoginViewController)
}
public class LoginViewController: NSViewController {
weak var delegate: LoginViewControllerDelegate?
#IBAction func login(_ sender: Any) {
// login logic
let isLoginSuccessful = true
if isLoginSuccessful {
self.delegate?.loginViewControllerDidLoginSuccessful(self)
}
}
}
public class ExtensionStepsViewController: NSViewController {
}
public class ParentViewController: NSViewController, LoginViewControllerDelegate {
weak var login: LoginViewController! // using of force unwrap is anti-pattern. consider other solutions
weak var steps: ExtensionStepsViewController!
public override func viewDidLoad() {
let login = LoginViewController(nibName: NSNib.Name(rawValue: "LoginViewController"), bundle: nil)
login.delegate = self
// change login view frame if needed
login.view.frame = self.view.frame
self.view.addSubview(login.view)
// instead of setting login view frame you can add appropriate layout constraints
self.addChildViewController(login)
self.login = login
let steps = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: "ExtensionStepsViewController"), bundle: nil)
steps.view.frame = self.view.frame
self.addChildViewController(steps)
self.steps = steps
}
// MARK: - LoginViewControllerDelegate
public func loginViewControllerDidLoginSuccessful(_ loginVC: LoginViewController) {
self.transition(from: self.login, to: self.steps, options: .slideLeft) {
// completion handler logic
print("transition is done successfully")
}
}
}
Here is a swift playground with this example.
UPD:
You can instantiate NSViewController in several ways:
Use NSStoryboard that allows to load view of NSViewController from .storyboard file:
let storyboard = NSStoryboard(name: NSStoryboard.Name("NameOfStoryboard"), bundle: nil)
let viewController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("NSViewControllerIdentifierInStoryboard"))
Use appropriate initialiser of NSViewController to load view of it from .xib file:
let steps = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: "ExtensionStepsViewController"), bundle: nil)
Use default initialiser but you have to load view directly by overriding loadView() method if name of xib file is different from name of view controller class:
let steps = ExtensionStepsViewController()
// Also you have to override loadView() method of ExtensionStepsViewController.
I am watching the video series
Swift Talk #5
Connecting View Controllers
url: https://talk.objc.io/episodes/S01E05-connecting-view-controllers
In this video series they remove all the prepareForSegue and use an App class to handle the connection between different view controllers.
I want to replicate this, but specifically only in my current view model; but what I don't get is how to connect view controllers through a view model (or even if you're meant to)
In their code, at github: https://github.com/objcio/S01E05-connecting-view-controllers/blob/master/Example/AppDelegate.swift
They use do this within their view controller
var didSelect: (Episode) -> () = { _ in }
This runs;
func showEpisode(episode: Episode) {
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
detailVC.episode = episode
navigationController.pushViewController(detailVC, animated: true)
}
In the same way, I want to use my ViewController to use my ViewModel for a menu button press (relying on tag).
My code follows;
struct MainMenuViewModel {
enum MainMenuTag: Int {
case newGameTag = 0
}
func menuButtonPressed(tag: Int) {
guard let tagSelected = MainMenuTag.init(rawValue: tag) else {
return
}
switch tagSelected {
case .newGameTag:
print ("Pressed new game btn")
break
}
}
func menuBtnDidPress(tag: Int) {
print ("You pressed: \(tag)")
// Do a switch here
// Go to the next view controller? Should the view model even know about navigation controllers, pushing, etc?
}
}
class MainMenuViewController: UIViewController {
#IBOutlet var mainMenuBtnOutletCollection: [UIButton]!
var didSelect: (Int) -> () = { _ in }
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func mainMenuBtnPressed(_ sender: UIButton) {
let tag = (sender).tag
self.didSelect(tag)
}
}
What I don't understand is how do I connect the command
self.didSelect(tag)
to the function
func menuButtonPressed(tag: Int)
within my ViewModel
As I understand it, according to the swift talk video is that the idea is that the view controller are "plain" and that the view model handles all the major stuff, like menu button presses and then moving to different view controllers as necessary.
How do I connect the didSelect item to my viewModel function?
Thank you.
You should set didSelect property for your controller like here:
func showEpisode(episode: Episode) {
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
detailVC.episode = episode
detailVC.didSelect = { episode in
// do whatever you need
// for example dismiss detailVC
self.navigationController.popViewController(animated: true)
// or call the model methods
self.model.menuButtonPressed(episode)
}
navigationController.pushViewController(detailVC, animated: true)
}
I know that this has to be a simple fix, but can't seem to understand why my code is not working. Basically I am trying to send a value from a text field in 1 view to a 2nd view's label.
ViewController.swift
#IBOutlet var Text1st: UITextField
#IBAction func Goto2ndView(sender: AnyObject) {
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2
//view2.Label2nd.text=text;
self.navigationController.pushViewController(view2, animated: true)
}
MyView2.swift
#IBOutlet var Label2nd: UILabel
override func viewDidLoad() {
super.viewDidLoad()
var VC = ViewController()
var string = (VC.Text1st.text) //it doesn't like this, I get a 'Can't unwrap Option.. error'
println(string)
}
-------EDITED UPDATED CODE FROM (drewag)-------
ViewController.swift
let text = "text"
var sendString = Text1st.text
println(sendString) //successfully print it out.
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2
view2.Label2nd.text=sendString;
self.navigationController.pushViewController(view2, animated: true)
MyView2.swift
#IBOutlet var Label2nd: UILabel
override func viewDidLoad() {
super.viewDidLoad()
var VC = ViewController()
var string = self.Label2nd.text
println(string) //still getting the error of an unwrap optional.none
}
var VC = ViewController() creates a new instance of ViewController. Unless there is a default value, you are not going to get any value out of VC.Text1st.text. You really should use a string variable on your second view controller to pass the data to it.
Also, a note on common formatting:
Class names should start with a capital letter (as you have)
Method / function names should start with a lower case letter
UIViewController subclasses should have "Controller" included in their name, otherwise, it looks like it is a subclass of UIView which is an entirely different level of Model View Controller (the architecture of all UIKit and Cocoa frameworks)
Edit:
Here is some example code:
class ViewController1 : UIViewController {
...
func goToSecondView() {
var viewController = ViewController2()
viewController.myString = "Some String"
self.navigationController.pushViewController(viewController, animated: true)
}
}
class ViewController2 : UIViewController {
var myString : String?
func methodToUseMyString() {
if let string = self.myString {
println(string)
}
}
...
}
Note, I am not creating ViewController2 using a storyboard. I personally prefer avoiding storyboards because they don't scale well and I find editing them to be very cumbersome. You can of course change it to create the view controller out of the storyboard if you prefer.
jatoben is correct that you want to use optional binding. IBOutlets are automatically optionals so you should check the textfield to see if it is nil.
if let textField = VC.Text1st {
println(textField.text)
}
This should prevent your app from crashing, but it will not print out anything because your text field has not yet been initialized.
Edit:
If you want to have a reference to your initial ViewController inside your second you're going to have to change a few things. First add a property on your second viewcontroller that will be for the first view controller:
#IBOutlet var Label2nd: UILabel //existing code
var firstVC: ViewController? //new
Then after you create view2, set it's firstVC as the ViewController you are currently in:
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2 //already in your code
view2.firstVC = self //new
Finally in your viewDidLoad in your second view controller, use firstVC instead of the ViewController you recreated. It will look something like this:
override func viewDidLoad() {
super.viewDidLoad()
if let textField = firstVC?.Text2nd {
println(textField.text)
}
}
Use optional binding to unwrap the property:
if let string = VC.Text1st.text {
println(string)
}