I'm building a library for static table views and it works fine, but I encountered a problem with generic closures.
It looks like this so far:
orderForm = Form(tableView: orderTable) { f in
f.section { s in
s.footer("Při platbě nejsou účtovány žádné další poplatky.")
s.cell("Selection")
.configure { (cell, path) in
let c = cell as! ProfileSelectionCell
c.titleLabel?.text = "Způsob platby"
c.detailLabel?.text = self.paymentType.first
}
s.cell("Selection")
.configure { (cell, path) in
let c = cell as! ProfileSelectionCell
c.titleLabel?.text = "Balíček"
c.detailLabel?.text = "20 kr. za 1000 Kc"
}.selected { path in
}
}
}
I wanna have the "cell" variable already cast to appropriate type, in this case ProfileSelectionCell.
Here is the source for the cell class:
class Cell {
internal let id: String
internal var configure: ((cell: UITableViewCell, path: NSIndexPath) -> Void)?
internal var selected: ((path: NSIndexPath) -> Void)?
init(id: String) {
self.id = id
}
func configure(config: ((cell: UITableViewCell, path: NSIndexPath) -> Void)?) -> Self {
self.configure = config
return self
}
func selected(selected: (path: NSIndexPath) -> Void) -> Self {
self.selected = selected
return self
}}
My problem is that if I make the configure method generic, it is not possible to store the config closure to the cell variable and if I make the whole cell generic, I can't save the cell to an array in Section class and so on..
Is this solvable in any way?
You can make the Cell class generic, e.g.
class Cell<T : UITableViewCell> {
}
and then use T instead of every UITableViewCell.
Unfortunately you would have to have the same in both Section and Form classes, too. That would work for tables with one type of cells but it won't probably work for tables with multiple cell types. In that case you will always need casting somewhere.
Related
I have an UITableView and a users variable whose signature is like:
let users: Variable<[User]?> = Variable<[User]?>(nil)
While I'm trying to bind this array of users in a UITableView, I'm getting the error Generic parameter 'Self' could not be inferred. This only occurs when I'm observing an Optional type. Why does this happen? What's the workaround for this situation?
Here is an example of how I'm doing:
private func bind() {
// Does not compile: Generic parameter 'Self' could not be inferred
users.asObservable().bind(to:
tableView.rx.items(cellIdentifier: "UserCell",
cellType: UITableViewCell.self)) { (index, user, cell) in
// Cell setup.
}.disposed(by: disposeBag)
}
You see this error because the compiler cannot infer the type of the Optional variable users passed into the following functions which work on generics and need to be able to infer the type.
An Optional in Swift is actually implemented as shown below. I guess in case of an Optional being potentially nil aka .none the compiler cannot infer the type for methods like e.g. items and bind(to:) which work on generics.
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
/// The absence of a value.
///
/// In code, the absence of a value is typically written using the `nil`
/// literal rather than the explicit `.none` enumeration case.
case none
/// The presence of a value, stored as `Wrapped`.
case some(Wrapped)
/// Creates an instance that stores the given value.
public init(_ some: Wrapped)
//...
}
Workaround 1.): You could just use filterNil() (RxOptionals lib) to avoid the problem.
private func bind() {
users.asObservable().filterNil().bind(to:
tableView.rx.items(cellIdentifier: "UserCell",
cellType: UITableViewCell.self)) { (index, user, cell) in
// Cell setup.
}.disposed(by: disposeBag)
}
Workaround 2.): Make users non-optional. If you have no users just set an empty array as value.
let users: Variable<[User]> = Variable<[User]>([])
Workaround 3.): Use Nil-Coalescing Operator ?? in map function like
private func bind() {
users.asObservable().map { optionalUsers -> [User] in
return optionalUsers ?? []
}
.bind(to:
tableView.rx.items(cellIdentifier: "UserCell",
cellType: UITableViewCell.self)) { (index, user, cell) in
// Cell setup.
}.disposed(by: disposeBag)
}
Sidenote: Variable is deprecated in the latest version of RxSwift
Just for reference:
Implementation of items
public func items<S: Sequence, Cell: UITableViewCell, O : ObservableType>
(cellIdentifier: String, cellType: Cell.Type = Cell.self)
-> (_ source: O)
-> (_ configureCell: #escaping (Int, S.Iterator.Element, Cell) -> Void)
-> Disposable
where O.E == S {
return { source in
return { configureCell in
let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper<S> { (tv, i, item) in
let indexPath = IndexPath(item: i, section: 0)
let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! Cell
configureCell(i, item, cell)
return cell
}
return self.items(dataSource: dataSource)(source)
}
}
}
Implementation of bind(to:)
public func bind<O: ObserverType>(to observer: O) -> Disposable where O.E == E {
return self.subscribe(observer)
}
I had the same error without an optional type. It happened because I was using a UICollectionViewCell subclass in a UITableView.
After reading the Swift documentation and various online tutorials, it still a little hard to wrap my head around reference types versus value types.
I'm following a tutorial from a Swift TDD book to understand OOP architecture in Swift. The tutorial is based on creating a to do list app. In the beginning of the book we created a struct to resemble each to do item. Next we created an class called ItemManger to manage the array that holds to do items and another array to hold checked off to do items. I can understand the idea of to do items being made from a struct because its a value type which creates a new instance every time its instantiated and the itemManager being made from a class since we only need one itemManger to keep track of to do items. The question that I've come up with is when we create an instance of the ItemManager type (which is a class) inside another class or view controller, will this refer to the same class we created before in which we'll be able to access the to do item arrays?
Before this book, I assumed in order to keep track of variables from a class, we have to mark them as static.
Here is the itemManager class:
import Foundation
class ItemManager {
var toDoCount: Int {return toDoItems.count }
var doneCount: Int {return doneItems.count }
private var toDoItems: [ToDoItem] = []
private var doneItems: [ToDoItem] = []
func add(item: ToDoItem) {
if !toDoItems.contains(item){
toDoItems.append(item)
}
}
func checkItem(at index: Int) {
let item = toDoItems.remove(at: index)
doneItems.append(item)
}
func doneItem(at index: Int) -> ToDoItem{
return doneItems[index]
}
func item(at index: Int) -> ToDoItem{
return toDoItems[index]
}
func removeAll(){
toDoItems.removeAll()
doneItems.removeAll()
}
}
Here is another class where we create an instance variable of the ItemManager type:
import UIKit
enum Section: Int {
case toDo
case done
}
class ItemListDataProvider: NSObject, UITableViewDataSource, UITableViewDelegate {
var itemManager: ItemManager?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let itemManager = itemManager else{return 0}
guard let itemSection = Section(rawValue: section)else{ fatalError() }
let numberOfRows: Int
switch itemSection {
case .toDo:
numberOfRows = itemManager.toDoCount
case .done:
numberOfRows = itemManager.doneCount
}
return numberOfRows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
guard let itemManager = itemManager else { fatalError() }
guard let section = Section(rawValue: indexPath.section) else { fatalError() }
let item: ToDoItem
switch section {
case .toDo:
item = itemManager.item(at: indexPath.row)
case .done:
item = itemManager.doneItem(at: indexPath.row)
}
cell.configCell(with: item)
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
}
In the code you posted you are not creating an instance of ItemManager.
Here is another class where we create an instance variable of the ItemManager type:
ItemListDataProvider can have a ItemManager but it does not create one. Creating an instance of a class works by calling it's constructor like this:
// Creates an instance of ItemManager and assigns it to itemManager
let itemManager = ItemManager()
Because you did not show where your item manager is created, the question
will this refer to the same class we created before in which we'll be able to access the to do item arrays?
can not really be answered. Where did you create an instance of ItemManager and what did you do with it?
Here is an example:
let itemManagerA = ItemManager()
let itemListDataProviderA() = ItemListDataProvider()
itemListDataProviderA.itemManager = itemManagerA
let itemListDataProviderB() = ItemListDataProvider()
itemListDataProviderB.itemManager = itemManagerA
In this example both ItemListProviders have the same ItemManager and thus have access to the same item arrays.
On the contrary if you are doing something like this:
let itemManagerA = ItemManager()
let itemListDataProviderA() = ItemListDataProvider()
itemListDataProviderA.itemManager = itemManagerA
let itemManagerB = ItemManager() // <-- This creates a SECOND instance of ItemManager
let itemListDataProviderB() = ItemListDataProvider()
itemListDataProviderB.itemManager = itemManagerB // <-- We use the SECOND instance instead of the first one for itemListDataProviderB
both ItemListProviders have different instances of ItemListProvider and do not have access to the same items.
enum Sections: Int {
case parent
case general
}
struct Parent {
let name: String
}
enum General: Int {
case manage
case settings
func title() -> String {
switch self {
case .manage:
return "Manage"
case .settings:
return "Settings"
}
}
}
struct DataProvider {
func data(at index: NSIndexPath) -> ? {
let section = Sections(rawValue: index.section)!
switch section {
case .parent:
print("parent \(Parent(name: "Venkat"))")
return Parent(name: "Venkat")
case .general:
let general = General(rawValue: index.row)!
print(general.title())
return general
}
}
}
Here, func data(at index: NSIndexPath) needs to return value type based on indexpath. I tried with protocol but it need property requirement to handle in cell level. Any other way to implement the method and also "General" enum implementation
You could make a shared parent/protocol, then the function could return a shared parent instance, which you can then conditionally down cast. Or you could have the function return AnyObject which is a shared parent.
func data(atIndex: NSIndexPath) -> AnyObject {/*implementation*/}
/*
* Later in another function
*/
let someObj = data(atIndex:index)
if let parentObj = someObj as? Parent
{
// Do what you need with the parent object, possibly save it to a parent ref
}
And you can do that similarly for the General type. This is not a super scalable system because if you have 3-4 more types in a function that you want to return it gets messy with checking which type it is.
At that point though you would probably want to redesign your code, the return type of function should be constant whenever possible.
My Program's Flow
I have a Class called News Item. in it, I have a method that goes to a server and fetches JSON Data and creates a NewsItem instance that has each of the JSON data details in a for loop using the SWIFTYJson library as follows:
static func downloadNewsItems(completionBlock block : ([NewsItem]) -> ()) {
var newsItems = [NewsItem]()
let url = NSURL(string: "http://a-url-that-has-stuff")
let networkService = NetworkService(url: url!)
networkService.downloadData { (data) -> Void in
let jsonData = JSON(data:data)
for item in jsonData["stories"].arrayValue {
let newsArticle = NewsItem()
newsArticle.storyCategory = item["category"].string
newsArticle.titleText = item["title"].string
newsArticle.paragraph1 = item["paragraph1"].string
newsArticle.paragraph2 = item["paragraph2"].string
newsArticle.featureImage = item["headerImage"].string
newsArticle.storyDate = item["date"].string
newsArticle.majorReference = item["majorReference"].string
newsArticle.fact = item["fact"].string
newsItems.append(newsArticle)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
block(newsItems)
})
}
}
I have a collectionViewController that calls this method, gets the objects and appends them to an array which i then use to load the collection view data.
My Problem
If you look at the code, at this point, every newsArticle must have all the properties of the news item for this to work. I don't like this behaviour. I'm looking at adding more properties to the NewsItem class that aren't required but if available, should be loaded and properly instantiated. For example, I want to add a second image to the News Item class but not every news item will have a second image. If I add a news item that doesn't have 'titleText' for example, to the backend, the code will break.
I want to add this second image feature, and if a news item has a 'second image', then I want to instantiate a UIImageView and add it to the collectionView with this image.
I know I'm supposed to use optionals somehow but i can't quite crack it. I'm using Swift 3. Optionals, I must admit, have been the bane of my existence since i started swift. Any help would be appreciated. Danke!
EDIT
I'm implementing the newsItem class as follows:
class HomeViewController: UIViewController {
var newsItems = [NewsItem]()
override func viewDidLoad() {
super.viewDidLoad()
NewsItem.downloadNewsItems{ (newsItems) -> () in
self.newsItems = []
self.newsItems = newsItems
self.collectionView.reloadData()
}
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return newsItems.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! NewsCollectionViewCell
let story = newsItems[indexPath.item]
cell.configureCell(story)
return cell
}
}
The configureCell method just downloads anything that needs to be downloaded and then adds the text/images/links to the cell properties. Nothing special there.
I'm loading a list of objects from a core data database into a table view.
class ScheduleViewController: UITableViewController {
private var items: [AnyObject]?
// MARK: - Table view data source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let itemCount = items?.count {
return itemCount
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("DayScheduleCell", forIndexPath: indexPath) as DayScheduleCell
if let act = items[indexPath.row] as Activity {
if act.client != nil {
// ...
}
}
return cell
}
}
The data is retrieved inside a closure so I have declared an items array as an optional because it might be nil in the first run.
I'm getting the error '[AnyObject]?' does not have a member named 'subscript' at this line if let act = items[indexPath.row] as? Activity.
I can't figure out how to resolve this.
The array is declared as:
private var items: [AnyObject]?
so, as you also said, it's an optional
In swift an optional is an enum, so a type on its own - and as an optional, it can contain either a nil value or an object of the contained type.
You want to apply the subscript to the array, not to the optional, so before using it you have to unwrap the array from the optional
items?[indexPath.row]
but that's not all - you also have to use the conditional downcast:
as? Activity
because the previous expression can evaluate to nil
So the correct way to write the if statement is
if let act = items?[indexPath.row] as? Activity {
First of all you need to unwrap or optional chain the items as it can be nil. AnyObject has different behaviour when getting as element from array, due to the fact that AnyObject can be a function. You would have to cast from items like this:
if let act = items?[indexPath.row] as AnyObject? as Activity? {
if act.client != nil {
// ...
}
}
If items will never contain a function you can use
private var items: [Any]?
instead and cast with:
if let act = items?[indexPath.row] as? Activity {
if act.client != nil {
// ...
}
}
I have fixed my problem by convert index var type from UInt to Int
let obj = items[Int(index)]
Hope this can help someone who still get this problem.