Generic view controller won't work with delegate and extension - ios

I already posted a question but it was not clear about what I want. As #AlainT suggested, I filed a new one.
I have a typealias tuple
public typealias MyTuple<T> = (key: T, value: String)
A protocol:
public protocol VCADelegate: class {
associatedtype T
func didSelectData(_ selectedData: MyTuple<T>)
}
A view controller (VCA) with a table view
class VCA<T>: UIViewController, UITableViewDelegate, UITableViewDataSource {
var dataList = [MyTuple<T>]()
weak var delegate: VCADelegate? // Error: can only be used as a generic constraint
// ...
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.didSelectData(dataList[indexPath.row])
}
}
In another view controller (VCB), I create a VCA and pass through a dataList
func callVCA() {
let vcA = VCA<String>()
vcA.dataList = [(key: "1", value:"Value 1"),
(key: "2", value:"Value 2")]
}
What I want to do is to have a dataList without knowing key's data type in VCA. Only when VCB calls VCA then I know the data type of the key. Creating a generic view controller will cause an issue with delegate. Any way to solve this problem without having to change to closure completion?
The other issue of using a generic view controller is I can't extend it. Any idea?

This is a standard type-erasure situation, though in this particular case I'd just pass a closure (since there's only one method).
Create a type eraser instead of a protocol:
public struct AnyVCADelegate<T> {
let _didSelectData: (MyTuple<T>) -> Void
func didSelectData(_ selectedData: MyTuple<T>) { _didSelectData(selectedData)}
init<Delegate: VCADelegate>(delegate: Delegate) where Delegate.T == T {
_didSelectData = delegate.didSelectData
}
}
Use that instead of a delegate:
class VCA<T>: UIViewController, UITableViewDataSource UITableViewDelegate {
var dataList = [MyTuple<T>]()
var delegate: AnyVCADelegate<T>?
// ...
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.didSelectData(dataList[indexPath.row])
}
}
Your underlying problem is that protocols with associated types are not proper types themselves. They're only type constraints. If you want to keep it a PAT, that's fine, but then you have to make VCA generic over the Delegate:
class VCA<Delegate: VCADelegate>: UIViewController, UITableViewDelegate {
var dataList = [MyTuple<Delegate.T>]()
weak var delegate: Delegate?
init(delegate: Delegate?) {
self.delegate = delegate
super.init(nibName: nil, bundle: nil)
}
required init(coder: NSCoder) { super.init(nibName: nil, bundle: nil) }
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.didSelectData(dataList[indexPath.row])
}
}
class VCB: UIViewController, VCADelegate {
func didSelectData(_ selectedData: MyTuple<String>) {}
func callVCA() {
let vcA = VCA(delegate: self)
vcA.dataList = [(key: "1", value:"Cinnamon"),
(key: "2", value:"Cloves")]
}
}
As a rule, protocols with associated types (PATs) are a very powerful, but special-purpose tool. They aren't a replacement for generics (which are a general purpose tool).
For this particular problem, though, I'd probably just pass a closure. All a type eraser is (usually) is a struct filled with closures. (Some day the compiler will probably just write them for us, and much of this issue will go away and PATs will be useful in day-to-day code, but for now it doesn't.)

Related

what is the function of NSObject in StoryBoard / Interface Builder?

I am currently following a video tutorial course about test driven development of iOS in Swift, but when testing Table View in View Controller, I get stuck, since I don't understand why we need NSObject in Interface builder like the picture below:
Movie Library Data Service is inheritted NSObject class:
the class of MovieLibraryDataService is like this:
import UIKit
class MovieLibraryDataService: NSObject, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
}
and the MovieLibraryDataService class will be used in XCTestCase is like this :
#testable import FilmFest
class LibraryViewControllerTests: XCTestCase {
var sut: LibraryViewController!
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
sut = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LibraryViewControllerID") as! LibraryViewController
_ = sut.view
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
// MARK: Nil Checks
func testLibraryVC_TableViewShouldNotBeNil() {
XCTAssertNotNil(sut.libraryTableView)
}
// MARK: Data Source
func testDataSource_ViewDidLoad_SetsTableViewDataSource() {
XCTAssertNotNil(sut.libraryTableView.dataSource)
XCTAssertTrue(sut.libraryTableView.dataSource is MovieLibraryDataService)
}
// MARK: Delegate
func testDelegate_ViewDidLoad_SetsTableViewDelegate() {
XCTAssertNotNil(sut.libraryTableView.delegate)
XCTAssertTrue(sut.libraryTableView.delegate is MovieLibraryDataService)
}
// MARK: Data Service Assumptions
func testDataService_ViewDidLoad_SingleDataServiceObject() {
XCTAssertEqual(sut.libraryTableView.dataSource as! MovieLibraryDataService, sut.libraryTableView.delegate as! MovieLibraryDataService)
}
}
and the definition of LibraryViewController:
import UIKit
class LibraryViewController: UIViewController {
#IBOutlet weak var libraryTableView: UITableView!
#IBOutlet var dataService: MovieLibraryDataService!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.libraryTableView.dataSource = dataService
self.libraryTableView.delegate = dataService
}
}
I really don't understand why I need to make that MovieLibraryDataService class
I usually use:
self.libraryTableView.dataSource = self
self.libraryTableView.delegate = self
but why do I need to write :
self.libraryTableView.dataSource = dataService
self.libraryTableView.delegate = dataService
MovieLibraryDataService is just another class which implements UITableViewDataSource and UITableViewDelegate, with the difference that the storyboard instantiates it and that storyboard-created instance is bound via the IBOutlet #IBOutlet var dataService: MovieLibraryDataService!. All objects in the storyboard are storyboard-created, that’s why one has to bind them to variables to use if they are not bound to other variables you use already.
Naming the variable dataService is just a fancy way of saying that it is supposed to serve, in this case as dataSource and delegate for a tableView, since it implements those delegate-protocols.
Since the dataService is instantiated by the storyboard, you could try if you can bind the dataService inside the storyboard to tableView. This is possible because you have the dataService referencable in the storyboard. This would replace setting dataSource and delegate in the viewController.
Another example
AppDelegate is also an NSObject inside the Storyboard so the UIApplication/NSApplication inside the storyboard is able to reference that to use, avoids having to set up AppDelegate yourself outside a storyboard. (Would otherwise be ugly, maybe that’s macOS only though, since mac apps have to show a Menu even if its empty.)
Edit
You can use NSObject on the Storyboard for different purposes, one of them could be also the delegation. Instead of setting it programatically like:
self.libraryTableView.dataSource = self
self.libraryTableView.delegate = self
you can hold control and then set the corresponding delegates like below:
Original Answer
Because MovieLibraryDataService that conforms to UITableViewDataSource and UITableViewDelegate and not your LibraryViewController.
If you want to change it as you are always used to, change your code to:
class LibraryViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var libraryTableView: UITableView!
var dataService = MovieLibraryDataService()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.libraryTableView.dataSource = self
self.libraryTableView.delegate = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
}
Personally I would suggest to keep it as you have it, as View Controllers tend to grow and become like we call it Massive View Controller

Protocol Design in Swift

I have a single view controller class that I reuse on multiple screens, on different screens I want to supply a different data source. My VC looks something like this:
class SpotViewController: UIViewController {
var dataSource: SpotDataSource!
}
Now I want to have two different Data Source Classes
class PersonalSpotDataSource {}
class ExploreSpotDataSource {}
Now I want to create some sort of a shared protocol that makes the above two classes implmenent the following properties
var spots: [Spot]
var title: String
And then I want to that shared construct to conform UITableViewDataSource since it has everything it needs which is just the list of spots and the title. And then going back to my first class I can supply either class (Personal or explore) as my VC's data source.
Is this possible?
So, first define your protocol:
protocol SpotDataSource {
var spots: [Spot] { get }
var title: String { get }
}
Then define your two classes to conform to that protocol:
class PersonalSpotDataSource: SpotDataSource {
let spots = [Spot(name: "foo"), Spot(name: "bar")]
let title = "Foobar"
}
class ExploreSpotDataSource: SpotDataSource {
let spots = [Spot(name: "baz"), Spot(name: "qux")]
let title = "Bazqux"
}
Clearly, your implementations of these will be more sophisticated than the above, but this illustrates the basic idea.
Anyway, having done that, define a UITableViewDataSource that uses this protocol:
class SpotTableViewDataSource: NSObject {
let spotDataSource: SpotDataSource
init(spotDataSource: SpotDataSource) {
self.spotDataSource = spotDataSource
super.init()
}
}
extension SpotTableViewDataSource: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return spotDataSource.spots.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SpotCell", for: indexPath) as! SpotCell
cell.spotLabel.text = spotDataSource.spots[indexPath.row].name
return cell
}
}
Finally, define you table view controller to use this UITableViewDataSource, passing it whatever SpotDataSource you want:
class PersonalSpotTableViewController: UITableViewController {
private let dataSource = SpotTableViewDataSource(spotDataSource: PersonalSpotDataSource())
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataSource
}
}
Note, in that table view controller class, I defined the data source to be stored property because table view controllers don't keep strong reference to their data sources and/or delegates, but we want it to stay around.
Obviously, it would have been more graceful if we could have just put the UITableViewDataSource methods inside the SpotDataSource protocol's default implementation, but sadly we can't. So you have to create a concrete object that conforms to UITableViewDataSource and uses your SpotDataSource protocol, like outlined above.

UITableView with controller separate from ViewController

I'm a newbie to Swift and XCode, taking a class in iOS development this summer. A lot of projects we're doing and examples I'm seeing for UI elements like PickerViews, TableViews, etc. are defining everything in the ViewController.swift file that acts as the controller for the main view. This works fine, but I'm starting to get to the point of project complexity where I'd really like all of my code to not be crammed into the same Swift file. I've talked to a friend who does iOS development on the side, he said this is sane and reasonable and well in-line with proper object-oriented programming... but I just can't seem to get it to work. Through trial and error I've gotten to this situation: the app runs in the simulator, the UITableView appears, but I'm not getting it populated with entries. I can get it working just fine when all the code is in the ViewController, but once I start trying to create a new controller class and make an instance of that class the dataSource/delegate of the UITableView I start getting nothing. I feel like I'm either missing some core understanding of Swift here, or doing something wrong with the Interface Builder in XCode.
My end result should be a UITableView with three entries in it; currently I'm getting a UITableView with no entries. I'm following along with a few different examples I've Googled, but primarily this other SO question: UITableView example for Swift
ViewController.swift:
import UIKit
class ViewController: UIViewController{
#IBOutlet var stateTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
var viewController = StateViewController()
self.stateTableView.delegate = viewController
self.stateTableView.dataSource = viewController
}
}
StateViewController.swift:
import UIKit
class StateViewController: UITableViewController{
var states = ["Indiana", "Illinois", "Nebraska"]
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return states.count;
}
func tableView(cellForRowAttableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = UITableViewCell(style:UITableViewCellStyle.default, reuseIdentifier:"cell")
cell.textLabel?.text = states[indexPath.row]
return cell
}
}
In XCode I have the UITableView hooked up to the View Controller; the outlets are set to dataSource and delegate and the referencing outlet is stateTableView.
I'm not getting any errors; I do get a warning on my `var viewController = StateViewController()' statement in ViewController.swift where it wants me to use a constant, but switching it to a constant doesn't change the behavior (this is as it should be, I assume).
Originally I assumed that the error was in my StateViewController.swift file, where I'm not creating an object that adheres to the UITableViewDataSource or UITableViewDelegate protocol, but if I even add them into the class statement I immediately get errors like "Redundant conformance of 'StateViewController' to protocol 'UITableViewDataSource'" - I'm reading that this is because inheriting from UITableViewController automatically inherits the other protocols as well.
The last thing I tried was instead referring to self.states in the StateViewController's tableView functions, but I'm pretty sure self in Swift works the same as it does in Python and it feels like I'm just trying to add magic words at this point.
I've investigated as far as my currently-limited Swift knowledge can take me, so any answer that explains what I'm doing wrong rather than just telling me what to fix would be very appreciated.
Your issue is being caused by a memory management problem. You have the following code:
override func viewDidLoad() {
super.viewDidLoad()
var viewController = StateViewController()
self.stateTableView.delegate = viewController
self.stateTableView.dataSource = viewController
}
Think about the lifetime of the viewController variable. It ends when the end of viewDidLoad is reached. And since a table view's dataSource and delegate properties are weak, there is no strong reference to keep your StateViewController alive once viewDidLoad ends. The result, due to the weak references, is that the dataSource and delegate properties of the table view revert back to nil after the end of viewDidLoad is reached.
The solution is to create a strong reference to your StateViewController. Do this by adding a property to your view controller class:
class ViewController: UIViewController{
#IBOutlet var stateTableView: UITableView!
let viewController = StateViewController()
override func viewDidLoad() {
super.viewDidLoad()
self.stateTableView.delegate = viewController
self.stateTableView.dataSource = viewController
}
}
Now your code will work.
Once you get that working, review the answer by Ahmed F. There is absolutely no reason why your StateViewController class should be a view controller. It's not a view controller in any sense. It's simply a class that implements the table view data source and delegate methods.
Although I find it more readable and understandable to implement dataSource/delegate methods in the same viewcontroller, what are you trying to achive is also valid. However, StateViewController class does not have to be a subclass of UITableViewController (I think that is the part that you are misunderstanding it), for instance (adapted from another answer for me):
import UIKit
// ViewController File
class ViewController: UIViewController {
var handler: Handler!
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
handler = Handler()
tableView.dataSource = handler
}
}
Handler Class:
import UIKit
class Handler:NSObject, UITableViewDataSource {
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("myCell")
cell?.textLabel?.text = "row #\(indexPath.row + 1)"
return cell!
}
}
You can also use adapter to resolve this with super clean code and easy to understand, Like
protocol MyTableViewAdapterDelegate: class {
func myTableAdapter(_ adapter:MyTableViewAdapter, didSelect item: Any)
}
class MyTableViewAdapter: NSObject {
private let tableView:UITableView
private weak var delegate:MyTableViewAdapterDelegate!
var items:[Any] = []
init(_ tableView:UITableView, _ delegate:MyTableViewAdapterDelegate) {
self.tableView = tableView
self.delegate = delegate
super.init()
tableView.dataSource = self
tableView.delegate = self
tableView.rowHeight = UITableViewAutomaticDimension
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func setData(data:[Any]) {
self.items = data
reloadData()
}
func reloadData() {
tableView.reloadData()
}
}
extension MyTableViewAdapter: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Hi im \(indexPath.row)"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
delegate?.myTableAdapter(self, didSelect: items[indexPath.row])
}
}
Use Plug and Play
class ViewController: UIViewController, MyTableViewAdapterDelegate {
#IBOutlet var stateTableView: UITableView!
var myTableViewAdapter:MyTableViewAdapter!
override func viewDidLoad() {
super.viewDidLoad()
myTableViewAdapter = MyTableViewAdapter(stateTableView, self)
}
func myTableAdapter(_ adapter: MyTableViewAdapter, didSelect item: Any) {
print(item)
}
}
You are trying to set datasource and delegate of UITableView as UITableViewController. As #Ahmad mentioned its more understandable in same class i.e. ViewController, you can take clear approach separating datasource and delegate of UITableView from UIViewController. You can make subclass of NSObject preferably and use it as datasource and delgate class of your UITableView.
You can also also use a container view and embed a UITableViewController. All your table view code will move to your UITableViewController subclass.Hence seprating your table view logic from your View Controller
Hope it helps. Happy Coding!!
The way I separate those concerns in my projects, is by creating a class to keep track of the state of the app and do the required operations on data. This class is responsible for getting the actual data (either creating it hard-coded or getting it from the persistent store). This is a real example:
import Foundation
class CountriesStateController {
private var countries: [Country] = [
Country(name: "United States", visited: true),
Country(name: "United Kingdom", visited: false),
Country(name: "France", visited: false),
Country(name: "Italy", visited: false),
Country(name: "Spain", visited: false),
Country(name: "Russia", visited: false),
Country(name: "Moldova", visited: false),
Country(name: "Romania", visited: false)
]
func toggleVisitedCountry(at index: Int) {
guard index > -1, index < countries.count else {
fatalError("countryNameAt(index:) - Error: index out of bounds")
}
let country = countries[index]
country.visited = !country.visited
}
func numberOfCountries() -> Int {
return countries.count
}
func countryAt(index: Int) -> Country {
guard index > -1, index < countries.count else {
fatalError("countryNameAt(index:) - Error: index out of bounds")
}
return countries[index]
}
}
Then, I create separate classes that implement the UITableViewDataSource and UITableViewDelegate protocols:
import UIKit
class CountriesTableViewDataSource: NSObject {
let countriesStateController: CountriesStateController
let tableView: UITableView
init(stateController: CountriesStateController, tableView: UITableView) {
countriesStateController = stateController
self.tableView = tableView
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
super.init()
self.tableView.dataSource = self
}
}
extension CountriesTableViewDataSource: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// return the number of items in the section(s)
return countriesStateController.numberOfCountries()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// return a cell of type UITableViewCell or another subclass
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
let country = countriesStateController.countryAt(index: indexPath.row)
let countryName = country.name
let visited = country.visited
cell.textLabel?.text = countryName
cell.accessoryType = visited ? .checkmark : .none
return cell
}
}
import UIKit
protocol CountryCellInteractionDelegate: NSObjectProtocol {
func didSelectCountry(at index: Int)
}
class CountriesTableViewDelegate: NSObject {
weak var interactionDelegate: CountryCellInteractionDelegate?
let countriesStateController: CountriesStateController
let tableView: UITableView
init(stateController: CountriesStateController, tableView: UITableView) {
countriesStateController = stateController
self.tableView = tableView
super.init()
self.tableView.delegate = self
}
}
extension CountriesTableViewDelegate: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected row at index: \(indexPath.row)")
tableView.deselectRow(at: indexPath, animated: false)
countriesStateController.toggleVisitedCountry(at: indexPath.row)
tableView.reloadRows(at: [indexPath], with: .none)
interactionDelegate?.didSelectCountry(at: indexPath.row)
}
}
And this is how easy is to use them from the ViewController class now:
import UIKit
class ViewController: UIViewController, CountryCellInteractionDelegate {
public var countriesStateController: CountriesStateController!
private var countriesTableViewDataSource: CountriesTableViewDataSource!
private var countriesTableViewDelegate: CountriesTableViewDelegate!
private lazy var countriesTableView: UITableView = createCountriesTableView()
func createCountriesTableView() -> UITableView {
let tableViewOrigin = CGPoint(x: 0, y: 0)
let tableViewSize = view.bounds.size
let tableViewFrame = CGRect(origin: tableViewOrigin, size: tableViewSize)
let tableView = UITableView(frame: tableViewFrame, style: .plain)
return tableView
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
guard countriesStateController != nil else {
fatalError("viewDidLoad() - Error: countriesStateController was not injected")
}
view.addSubview(countriesTableView)
configureCountriesTableViewDelegates()
}
func configureCountriesTableViewDelegates() {
countriesTableViewDataSource = CountriesTableViewDataSource(stateController: countriesStateController, tableView: countriesTableView)
countriesTableViewDelegate = CountriesTableViewDelegate(stateController: countriesStateController, tableView: countriesTableView)
countriesTableViewDelegate.interactionDelegate = self
}
func didSelectCountry(at index: Int) {
let country = countriesStateController.countryAt(index: index)
print("Selected country: \(country.name)")
}
}
Note that ViewController didn't create the countriesStateController object, so it must be injected. We can do that from the Flow Controller, from the Coordinator or Presenter, etc. I did it from AppDelegate like so:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let countriesStateController = CountriesStateController()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
if let viewController = window?.rootViewController as? ViewController {
viewController.countriesStateController = countriesStateController
}
return true
}
/* ... */
}
If it's never injected - we get a runt-time crash, so we know we must fix it straight away.
This is the Country class:
import Foundation
class Country {
var name: String
var visited: Bool
init(name: String, visited: Bool) {
self.name = name
self.visited = visited
}
}
Note how clean and slim the ViewController class is. It's less than 50 lines, and if create the table view from Interface Builder - it becomes 8-9 lines smaller.
ViewController above does what it's supposed to do, and that's to be a mediator between View and Model objects. It doesn't really care if the table displays one type or many types of cells, so the code to register the cell(s) belongs to CountriesTableViewDataSource class, which is responsible to create each cell as needed.
Some people combine CountriesTableViewDataSource and CountriesTableViewDelegate in one class, but I think it breaks the Single Responsibility Principle. Those two classes both need access to the same DataProvider / State Controller object, and ViewController needs access to that as well.
Note that View Controller had now way to know when didSelectRowAt was called, so we needed to create an additional protocol inside UITableViewDelegate:
protocol CountryCellInteractionDelegate: NSObjectProtocol {
func didSelectCountry(at index: Int)
}
And we also need a delegate property to make the communication possible:
weak var interactionDelegate: CountryCellInteractionDelegate?
Note that neither CountriesTableViewDataSource not CountriesTableViewDelegate class knows about the existence of the ViewController class. Using Protocol-Oriented-Programming - we could even remove the tight-coupling between those two classes and the CountriesStateController class.

textDocumentProxy only applicable in KeyboardViewController.swift?

I am trying to create a custom keyboard extension that pushes to another UIViewController that has a list of text that can be inserted onto the selected text field. It subclasses a UIInputViewController. However, when I try to use the textDocumentProxy to input text on didSelectRowAtIndexPath() nothing seems to be happening.
Does the textDocumentProxy only apply to the default KeyboardViewController.swift file that was generated or am I able to to create another UIViewController that subclasses UIInputViewController to insert text? My last resort would be to use delegation or create a listener that will call a function in KeyboardViewController.swift, but I just wanted to see if anyone knew a better way.
Thanks in advance!
Code example is shown below:
import UIKit
class AnotherInputViewController: UIInputViewController, UITableViewDelegate, UITableViewDataSource {
...
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: false)
var proxy = textDocumentProxy as UITextDocumentProxy
if let input = arrayTextToInput[indexPath.row] as? String {
proxy.insertText(input)
}
}
}
Just in case any noobie runs into this again using the keyboard API extension...
I just injected the KeyboardViewController object into the view controller I was presenting as shown below:
//within KeyboardViewController.swift
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: false)
let nextVC: NextViewController = NextViewController(keyboardVC: self.keyboardVC)
self.presentViewController(nextVC, animated: true, completion: nil)
}
and then just initialized the nextVC with the KeyboardViewController
//class NextViewController
var keyboardVC: KeyboardViewController
init(keyboardVC: KeyboardViewController) {
self.keyboardVC = keyboardVC
super.init(nibName: nil, bundle: nil);
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
From there you have access to keyboardVC and can use that object to call methods to insert text into whichever fields you wish. For example...
call keyboardVC.addText(input) from wherever you want
//class KeyboardViewController.swift
func addText(input: String) {
if let textDocumentProxy = self.textDocumentProxy as? UITextDocumentProxy {
textDocumentProxy.insertText(input)
}
}
Hope it helps !
I faced the same problem, and I fixed it by using Delegation.
where pass action of insert text to KeyboardViewController
First create delegate
protocol UITextDocumentProxyDelegate: AnyObject {
func insertText(_ text: String)
}
, then in AnotherInputViewController create variable delegate with type UITextDocumentProxyDelegate
weak var delegate: UITextDocumentProxyDelegate?
and replace your code to
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: false)
if let input = arrayTextToInput[indexPath.row] as? String {
delegate?.insertText(input)
}
}
, and finally in KeyboardViewController conform UITextDocumentProxyDelegate protocol and implement insertText method
extension KeyboardViewController: UITextDocumentProxyDelegate {
func insertText(_ text: String) {
proxy.insertText(text)
}
}

Pass specific Core Data through protocol in Swift

I am using protocols and delegates to send information from my View Controller to my Custom View Controller.
ViewController.swift ==
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIAlertViewDelegate, StartAlertViewVCDelegate {
func requiredText() -> String { return "test123" }
CustomViewController.swift ==
#objc protocol StartAlertViewVCDelegate {
func requiredText() -> String }
&
func requiredText(info: NSString) { println(info) }
This works fine and dandy, but my problem is that I actually need to send an item from Core Data. This item is selected from the didSelectRowAtIndexPath.. EX:
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
println("***** Row \(indexPath.row) selected *****")
var infoData = [NSManagedObject]()
let company = infoData[indexPath.row]
company.valueForKey("openDoor") as String?
self.selectedOpenDoor = company.valueForKey("openDoor") as String!
I need to pass the selectedOpenDoor string to my Custom View Controller.
But when I run the application, the compiler grabs selectedDoorOpen before anything has been put in the string (it runs at startup)
Is there anyway to pass the delegate after didSelectRow is fired?
Any suggestions?

Resources