I've been using NewsApi . and it works perfectly but all of a sudden I got this error :
Thread 10: Fatal error: unableToParse
is anyone familiar with this error
my view controller :
class HomeViewController: UIViewController , UITableViewDelegate, UITableViewDataSource ,UIScrollViewDelegate , UITabBarDelegate {
var ref: DatabaseReference!
var i: Int = 0
#IBOutlet weak var bookMark: UITabBarItem!
// outlets
#IBOutlet weak var tableView: UITableView!
var articles = [NewsArticle]() {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
let newsAPI = NewsAPI(apiKey: "//////////")
private let imageView = UIImageView(image: UIImage(named: "logo"))
//// table View
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath.row == 0)
{
let cell = Bundle.main.loadNibNamed("FeaturesTableViewCell", owner: self, options: nil)?.first as! FeaturesTableViewCell
return cell
}
else if (indexPath.row == 1)
{
var breakingNews : String = ""
let cell = Bundle.main.loadNibNamed("BreakingNewsViewCell", owner: self, options: nil)?.first as! BreakingNewsViewCell
for article in articles
{
breakingNews = breakingNews+" - "+article.title
}
cell.newsLabel.text = breakingNews
return cell
}
else{
let cell = Bundle.main.loadNibNamed("NewViewCellTableViewCell", owner: self, options: nil)?.first as! NewViewCellTableViewCell
cell.titleLabel.text = "NEWS"
return cell
}
}
override func viewDidLoad() {
navigationItem.hidesBackButton = true
newsAPI.getTopHeadlines(q:"a"){ result in
switch result {
case .success(let articles):
self.articles = articles
case .failure(let error):
fatalError("\(error)")
}
}
tableView.delegate = self
tableView.dataSource = self
// setupMenuBar()
}
}
and i got that error marked in :
case .failure(let error):
but it used work perfectly
NewsApi Class:
public typealias NewsAPIRequestHandler<T> = ((Result<[T], NewsAPIError>) -> ())
public class NewsAPI {
private let provider: NewsProvider
private let decoder: NewsAPIDecoder
public init(apiKey: String) {
self.provider = NewsProvider(apiKey: apiKey)
self.decoder = NewsAPIDecoder()
}
init(provider: NewsProvider, sourceDecoder: NewsAPIDecoder) {
self.provider = provider
self.decoder = sourceDecoder
}
#discardableResult
public func getSources(category: NewsCategory = .all, language: NewsLanguage = .all, country: NewsCountry = .all, completion: #escaping NewsAPIRequestHandler<NewsSource>) -> URLSessionDataTask? {
let targetAPI = NewsAPITarget.sources(category: category, language: language, country: country)
return request(targetAPI, completion: completion)
}
#discardableResult
public func getTopHeadlines(q: String? = nil,
sources: [String]? = nil,
category: NewsCategory = .all,
language: NewsLanguage = .all,
country: NewsCountry = .all,
pageSize: Int? = nil,
page: Int? = nil,
completion: #escaping NewsAPIRequestHandler<NewsArticle>) -> URLSessionDataTask? {
let targetAPI = NewsAPITarget.topHeadlines(q: q, sources: sources, category: category, language: language, country: country, pageSize: pageSize, page: page)
return request(targetAPI, completion: completion)
}
}
private extension NewsAPI {
func request<T: Decodable>(_ target: NewsAPITarget, completion: #escaping NewsAPIRequestHandler<T>) -> URLSessionDataTask? {
return provider.request(target) { data, error in
guard let data = data else {
completion(.failure(error ?? .unknown))
return
}
do {
let result: [T] = try self.decoder.decode(data: data)
completion(.success(result))
} catch let error {
let newsAPIError = (error as? NewsAPIError) ?? .unknown
completion(.failure(newsAPIError))
}
}
}
}
and as for newsAPIError class it has :
public enum NewsAPIError: Error {
case unknown
case unableToParse
case requestFailed
case invalidEndpointUrl
case serviceError(code: String, message: String)
}
please any help is welcome and thank you
Related
I am writing an application to track the cryptocurrency exchange rate. The api has separate url requests for each coin. Here is a JSON response coming from the server, to a request for one coin:
{
"status": {
"elapsed": 2,
"timestamp": "2022-08-23T06:10:16.417580964Z"
},
"data": {
"id": "1e31218a-e44e-4285-820c-8282ee222035",
"serial_id": 6057,
"symbol": "BTC",
"name": "Bitcoin",
"slug": "bitcoin",
"contract_addresses": null,
"_internal_temp_agora_id": "9793eae6-f374-46b4-8764-c2d224429791",
"market_data": {
"price_usd": 20946.467798282705,
"price_btc": 1,
"price_eth": 13.351682485155417,
"volume_last_24_hours": 7635594314.553516,
"real_volume_last_24_hours": 6038552423.10257,
"volume_last_24_hours_overstatement_multiple": 1.2644742944254175,
"percent_change_usd_last_1_hour": null,
"percent_change_btc_last_1_hour": null,
"percent_change_eth_last_1_hour": null,
"percent_change_usd_last_24_hours": -2.1478472228280485,
"percent_change_btc_last_24_hours": 0.11113305637977958,
"percent_change_eth_last_24_hours": 0.0518833986287626,
"ohlcv_last_1_hour": null,
"ohlcv_last_24_hour": null,
"last_trade_at": "2022-08-23T06:10:15Z"
}
I need to send several url requests and convert the received responses into a table where each cell is a certain coin corresponding to a certain url request.
I wrote a model and a service layer, but when sending two requests, instead of two cells in the table, I get one cell that displays data from the 1st request, and then abruptly changes to data from the second request.
The code is given below:
Сontroller
final class WalletController: UIViewController {
private let walletTable = UITableView()
private let service = WalletService()
private var data: [DataWallet] = []
private let identifier = "walletCell"
private var pointSorted = 1
private let queue = DispatchQueue.main
// MARK: Life cycle
override func viewDidLoad() {
super.viewDidLoad()
setUpView()
configData()
}
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.setNavigationBarHidden(false, animated: false)
self.navigationItem.setHidesBackButton(true, animated: true)
}
// MARK: setUpView
private func setUpView() {
// NavigationBar
createCustomNavigationBar()
// LogOutBarButton
let logOutButton = createCustomButton(titleName: "LogOut", selector: #selector(logOutButtonTapped))
navigationItem.leftBarButtonItem = logOutButton
// SortedBarButton
let sortedButton = createCustomButton(titleName: "Sorted", selector: #selector(sortedButtonTapped))
navigationItem.rightBarButtonItem = sortedButton
// TableView
walletTable.backgroundColor = #colorLiteral(red: 0.9381344914, green: 0.9331676364, blue: 0.9246369004, alpha: 1)
walletTable.separatorColor = #colorLiteral(red: 0.1599435508, green: 0.185090214, blue: 0.167404592, alpha: 1)
walletTable.delegate = self
walletTable.dataSource = self
walletTable.register(UITableViewCell.self, forCellReuseIdentifier: identifier)
view.addSubview(walletTable)
walletTable.snp.makeConstraints { maker in
maker.left.top.right.bottom.equalToSuperview().inset(0)
}
}
#objc private func logOutButtonTapped() {
let startController = StartController()
navigationController?.pushViewController(startController, animated: true)
}
private func configData() {
service.addCoin { [weak self] result in
switch result {
case .success(let dataBoy):
self?.data = [dataBoy]
DispatchQueue.main.async {
self?.walletTable.reloadData()
}
case.failure(let error):
print(error)
}
}
}
#objc private func sortedButtonTapped() {
if pointSorted == 1 {
data = data.sorted{ $0.capital < $1.capital }
pointSorted = pointSorted - 1
} else {
data = data.sorted{ $0.country < $1.country }
pointSorted = pointSorted + 1
}
walletTable.reloadData()
}
}
// MARK: Delegate
extension WalletController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
}
// MARK: DataSource
extension WalletController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = walletTable.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
cell.backgroundColor = #colorLiteral(red: 0.9381344914, green: 0.9331676364, blue: 0.9246369004, alpha: 1)
let coin = data[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = coin.symbol
content.secondaryText = String(coin.market_data.percent_change_usd_last_1_hour ?? 0.0)
cell.contentConfiguration = content
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let coin = data[indexPath.row]
let infoController = InfoController()
queue.async {
infoController.firstTextLabel.text = coin.name
infoController.firstNumberLabel.text = coin.symbol
infoController.secondTextLabel.text = coin.name
infoController.secondNumberLabel.text = String(coin.market_data.price_btc ?? 0.0)
infoController.thirdTextLabel.text = coin.name
infoController.thirdNumberLabel.text = String(coin.market_data.price_usd ?? 0.0)
}
navigationController?.pushViewController(infoController, animated: true)
}
}
Service layer
final class WalletService {
func addCoin(completion: #escaping (Result<DataWallet, Error>) -> Void) {
guard let urlBtc = URL(string: "https://data.messari.io/api/v1/assets/btc/metrics") else { return }
guard let urlEth = URL(string: "https://data.messari.io/api/v1/assets/eth/metrics") else { return }
let taskBtc = URLSession.shared.dataTask(with: urlBtc) { data, _, error in
if let error = error {
completion(.failure(error))
} else if let data = data {
do {
let result = try JSONDecoder().decode(Response.self, from: data)
completion(.success(result.data))
} catch {
completion(.failure(error))
}
}
}
taskBtc.resume()
let taskEth = URLSession.shared.dataTask(with: urlEth) { data, _, error in
if let error = error {
completion(.failure(error))
} else if let data = data {
do {
let result = try JSONDecoder().decode(Response.self, from: data)
completion(.success(result.data))
} catch {
completion(.failure(error))
}
}
}
taskEth.resume()
}
}
Model
struct Response: Codable {
let status: Status
let data: DataWallet
}
struct Status: Codable {
let elapsed: Int?
let timestamp: String?
}
struct DataWallet: Codable {
let id: String?
let symbol: String?
let name: String?
let market_data: MarketData
}
struct MarketData: Codable {
let price_usd: Double?
let price_btc: Double?
let percent_change_usd_last_1_hour: Double?
let percent_change_btc_last_1_hour: Double?
}
Can you tell me what I'm doing wrong and how to fix this situation?
I will be grateful for any help!
Your controlflow is wrong. For now if you call your service addCoin function you are calling the completionhandler twice. Everytime you are calling the completionhandler your data array gets set containing only the newest value:
self?.data = [dataBoy]
The most simple solution here would be to append the data instead of setting it.
self?.data.append(dataBoy)
and you would need to clear [data] at the start of the function.
private func configData() {
data = [] //<- add this to clear the collection
service.addCoin { [weak self] result in
switch result {
case .success(let dataBoy):
self?.data.append(dataBoy)
DispatchQueue.main.async {
self?.walletTable.reloadData()
}
case.failure(let error):
print(error)
}
}
}
I have an app where a user can search items for sale. I am currently trying to add functionality so that users can view items sorted with the prices from low to high. After users tap the sort button they are presented with a another view controller that shows different sort options. The first being "Price: Low to high". After low to high is tapped I send back a function using protocol and delegate to the current ResultsTableViewController . Everything works until I try to sort based on the price. When I print(data) I get back
[app.UserPostData(id: Optional("61E93673-24B8-42BE-B0AA-7185A0F26A39"), userSub: Optional("e345fdac-7eb5-4cde-a421-aa758b05d999"), fileName: Optional("823AD3EF-2890-40C4-A1F2-47FCDDA5A64B"),userPostedImage: Optional(<UIImage:0x600002e9b330 anonymous {1239, 1241}>), description: Optional("example description"), dateUploaded: Optional("2020-07-28")), app.UserPostData(id: Optional("9373925E-5526-4D92-B104-7981CD226669"), userSub: Optional("e345fdac-7eb5-4cde-a421-aa758b05d999"), fileName: Optional("0703758F-60AE-4566-A0A0-0EF16C1711BE"), price: Optional(50000),userPostedImage: Optional(<UIImage:0x600002e9fb10 anonymous {1238, 1240}>), description: Optional("example description"), dateUploaded: Optional("2020-07-28")), app.UserPostData(id: Optional("0FC678A6-B308-4A05-8B36-093768375A79"), userSub: Optional("e345fdac-7eb5-4cde-a421-aa758b05d999"), fileName: Optional("555ADDD0-5E58-4577-A2FD-52B6F47AC747"), price: Optional(650), userPostedImage: Optional(<UIImage:0x600002e80240 anonymous {1238, 1240}>), description: Optional("example description"), dateUploaded: Optional("2020-07-27"))]
How can I sort/rearrange the array to items with prices shown low to high? Inside of my sort func I get an error saying Binary operator '>' cannot be applied to two 'Int?' operands .
struct UserPostData {
var id: String?
var userSub: String?
var fileName: String?
var price: Int?
var userPostedImage: UIImage?
var description: String?
var dateUploaded: String?
}
class ResultsTableViewController: UITableViewController, priceLowToHigh, priceHightoLow {
func sortPriceLowToHigh(sort: String) {
print(sort)
print(data)
data.sort(by: { $0.price > $1.price}) // This is where I get the error
}
func sortPriceHighToLow(sort: String) {
print(sort)
}
var data: [UserPostData] = []
var userSubID = String()
let activityIndicator = UIActivityIndicatorView()
override func viewDidLoad() {
super.viewDidLoad()
setView()
getData()
}
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return data.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "Results") as! CellResults
cell.image = data[indexPath.section].userPostedImage
cell.price = data[indexPath.section].price
cell.layoutSubviews()
self.tableView.rowHeight = UITableView.automaticDimension
return cell
}
func setView(){
let sortButton = UIBarButtonItem(title: "Sort", style: .plain, target: self, action: #selector(sortPressed))
navigationItem.rightBarButtonItem = sortButton
}
func getData(){
activityIndicator.hidesWhenStopped = true
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
activityIndicator.style = .large
activityIndicator.startAnimating()
tableView.addSubview(activityIndicator)
NSLayoutConstraint.activate([activityIndicator.centerXAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.centerYAnchor)])
userSubID = AWSMobileClient.default().userSub!
let post = Post.keys
let predicate = post.search == search
_ = Amplify.API.query(request: .list(SellerPost.self, where: predicate)) { event in
switch event {
case .success(let result):
switch result {
case .success(let posts):
DispatchQueue.main.async {
self.tableView.reloadData()
for element in posts{
_ = Amplify.Storage.downloadData(key: element.filename,progressListener: { progress in
print("Progress: \(progress)")},
resultListener: { (event) in
switch event {
case let .success(data):
DispatchQueue.main.async {
self.data.append(UserPostData.init(id: element.id, userSub: element.userSub, fileName: element.filename, price: element.price, userPostedImage: UIImage(data: data), description: element.description,dateUploaded: element.dateUploaded))
self.tableView.reloadData()
self.activityIndicator.stopAnimating()
}
print("Completed: \(data)")
case let .failure(storageError):
print("Failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
}
})
}
}
case .failure(let error):
print("Got failed result with \(error.errorDescription)")
}
case .failure(let error):
print("Got failed event with error \(error)")
}
}
self.tableView.register(CellResults.self, forCellReuseIdentifier: "Results")
}
#objc func sortPressed(){
print("sort clicked")
let sTVC = SortViewController()
sTVC.priceLowToHighDelegate = self
sTVC.priceHighToLowDelegate = self
sTVC.modalPresentationStyle = .custom
self.present(sTVC, animated: true, completion: nil)
}
}
You can’t sort using an optional value so if it is nil you need to supply a default value using ??. For sorting a min or max value is most suitable. Use min if you want to sort nil values last or max for the opposite
data.sort(by: { $0.price ?? Int.min > $1.price ?? Int.min })
RxDatasource in RxSwift [RxTableViewSectionedAnimatedDataSource] Reload Animation don't update data source. What mistake I am doing? I am even unable to bind my action with button properly.
TableDataSource and Table editing commands
struct TableDataSource {
var header: String
var items: [Item]
var SectionViewModel: SectionViewModel
}
extension TableDataSource: AnimatableSectionModelType {
var identity: String {
return header
}
type alias Item = Data
init(original: TableDataSource, items: [Item]) {
self = original
self.items = items
self.sectionViewModel = original.sectionViewModel
}
}
enum TableViewEditingCommand {
case deleteSingle(IndexPath)
case clearAll(IndexPath)
case readAll(IndexPath)
}
struct SectionedTableViewState {
var sections: [TableDataSource]
init(sections: [TableDataSource]) {
self.sections = sections
}
func execute(command: TableViewEditingCommand) -> SectionedTableViewState {
var sections = self.sections
switch command {
// Delete single item from datasource
case .deleteSingle(let indexPath):
var items = sections[indexPath.section].items
items.remove(at: indexPath.row)
if items.count <= 0 {
sections.remove(at: indexPath.section)
} else {
sections[indexPath.section] = TableDataSource(
original: sections[indexPath.section],
items: items) }
// Clear all item from datasource with isUnread = false
case .clearAll(let indexPath):
sections.remove(at: indexPath.section)
// Mark all item as read in datasource with isUnread = true
case .readAll(let indexPath):
var items = sections[indexPath.section].items
items = items.map { var unread = $0
if $0.isUnRead == true { unreadData.isUnRead = false }
return unreadData
}
sections.remove(at: indexPath.section)
if sections.count > 0 {
let allReadItems = sections[indexPath.section].items + items
sections[indexPath.section] = TableDataSource(
original: sections[indexPath.section],
items: allReadItems)
}
}
return SectionedTableViewState(sections: sections)
}
}
This is my controller and its extensions
class ViewController: UIViewController, Storyboardable {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var closeButton: UIButton!
#IBOutlet weak var titleText: UILabel!
var viewModel: ViewModel!
let disposeBag = DisposeBag()
let sectionHeight: CGFloat = 70
let dataSource = ViewController.dataSource()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
bindInitials()
bindDataSource()
bindDelete()
}
private func bindInitials() {
tableView.delegate = nil
tableView.rx.setDelegate(self)
.disposed(by: disposeBag)
registerNibs()
}
private func registerNibs() {
let headerNib = UINib.init(
nibName: TableViewSection.identifier,
bundle: nil)
tableView.register(
headerNib,
forHeaderFooterViewReuseIdentifier: TableViewSection.identifier)
}
}
extension ViewController: Bindable {
func bindViewModel() {
bindActions()
}
private func bindDataSource() {
bindDelete()
// tableView.dataSource = nil
// Observable.just(sections)
// .bind(to: tableView.rx.items(dataSource: dataSource))
// .disposed(by: disposeBag)
}
private func bindDelete() {
/// TODO: to check and update delete code to work properly to sink with clear all and mark all as read
guard let sections = self.viewModel?.getSections() else {
return
}
let deleteState = SectionedTableViewState(sections: sections)
let deleteCommand = tableView.rx.itemDeleted.asObservable()
.map(TableViewEditingCommand.deleteSingle)
tableView.dataSource = nil
Observable.of(deleteCommand)
.merge()
.scan(deleteState) {
(state: SectionedTableViewState,
command: TableViewEditingCommand) -> SectionedTableViewState in
return state.execute(command: command) }
.startWith(deleteState) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
private func bindActions() {
guard let openDetailsObserver = self.viewModel?.input.openDetailsObserver,
let closeObserver = self.viewModel?.input.closeObserver else {
return
}
viewModel.output.titleTextDriver
.drive(titleText.rx.text)
.disposed(by: disposeBag)
// viewModel.input.dataSourceObserver
// .mapObserver({ (result) -> [Data] in
// return result
// })
// .disposed(by: disposeBag)
/// Close button binding with closeObserver
closeButton.rx.tap
.bind(to: (closeObserver))
.disposed(by: disposeBag)
/// Tableview item selected binding with openDetailsObserver
tableView.rx.itemSelected
.map { indexPath in
return (self.dataSource[indexPath.section].items[indexPath.row])
}.subscribe(openDetailsObserver).disposed(by: disposeBag)
}
}
extension ViewController: UITableViewDelegate {
static func dataSource() -> RxTableViewSectionedAnimatedDataSource<TableDataSource> {
return RxTableViewSectionedAnimatedDataSource(
animationConfiguration: AnimationConfiguration(insertAnimation: .fade,
reloadAnimation: .fade,
deleteAnimation: .fade),
configureCell: { (dataSource, table, idxPath, item) in
var cell = table.dequeueReusableCell(withIdentifier: TableViewCell.identifier) as? TableViewCell
let cellViewModel = TableCellViewModel(withItem: item)
cell?.setViewModel(to: cellViewModel)
return cell ?? UITableViewCell()
}, canEditRowAtIndexPath: { _, _ in return true })
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard var headerView = tableView.dequeueReusableHeaderFooterView(
withIdentifier: TableViewSection.identifier)
as? TableViewSection
else { return UITableViewHeaderFooterView() }
let viewModel = self.dataSource[section].sectionViewModel
headerView.setViewModel(to: viewModel)
headerView.dividerLine.isHidden = section == 0 ? true : false
headerView.section = section
let data = TableViewEditingCommand.clearAll(IndexPath(row: 0, section: section ?? 0))
// /// Section button binding with closeObserver
// headerView.sectionButton.rx.tap
// .map(verseNum -> TableViewEditingCommand in (TableViewEditingCommand.deleteSingle))
// .disposed(by: disposeBag)
headerView.sectionButtonTappedClosure = { [weak self] (section, buttonType) in
if buttonType == ButtonType.clearAll {
self?.showClearAllConfirmationAlert(section: section, buttonType: buttonType)
} else {
self?.editAction(section: section, buttonType: buttonType)
}
}
return headerView
}
func editAction(section: Int, buttonType: ButtonType) {
var sections = self.dataSource.sectionModels
let updateSection = (sections.count == 1 ? 0 : section)
switch buttonType {
/// Clear all
case .clearAll:
sections.remove(at: updateSection)
let data = SectionedTableViewState(sections: sections)
self.tableView.dataSource = nil
Observable.of(data)
.startWith(data) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
/// Mark all as read
case .markAllAsRead:
if updateSection == 0 { sections = self.viewModel.getSections() }
var items = sections[updateSection].items
items = items.map { var unread = $0
if $0.isUnRead == true { unread.isUnRead = false }
return unread
}
sections.remove(at: updateSection)
let allReadItems = sections[updateSection].items + items
sections[updateSection] = TableDataSource(
original: sections[updateSection],
items: allReadItems)
let data = SectionedTableViewState(sections: sections)
self.tableView.dataSource = nil
Observable.of(data)
.startWith(data) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
func showClearAllConfirmationAlert(section: Int, buttonType: ButtonType) {
let alert = UIAlertController(title: "Clear All",
message: "Are you sure, you want to clear all Items?",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
switch action.style{
case .default:
self.editAction(section: section, buttonType: buttonType)
case .cancel: break
case .destructive: break
default:break
}}))
let cancel = UIAlertAction(title: "Cancel", style: .default, handler: { action in
})
alert.addAction(cancel)
self.present(alert, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return sectionHeight
}
}
View model for controller
class ViewModel {
private enum Constants {
static let titleText = "Test".localized
static let testHistoryHeaderText = "test".localized
static let unreadHeaderText = "Unread".localized
}
struct Input {
let dataSourceObserver: AnyObserver<[Data]>
let openDetailsObserver: AnyObserver<Data>
let closeObserver: AnyObserver<Void>
let sectionButtonTappedObserver: AnyObserver<IndexPath>
}
struct Output {
let titleTextDriver: Driver<String>
let dataSourceDriver: Driver<[Data]>
let viewComplete: Observable<DataCoordinator.Event>
}
let input: Input
let output: Output
private let dataSourceSubject = PublishSubject<[Data]>()
private let closeSubject = PublishSubject<Void>()
private let openDetailsSubject = BehaviorSubject<Data>(value:Data())
private let sectionButtonTappedSubject = PublishSubject<IndexPath>()
private let disposeBag = DisposeBag()
init(withRepository repository: Repository) {
input = Input(
dataSourceObserver: dataSourceSubject.asObserver(),
openDetailsObserver: openDetailsSubject.asObserver(),
closeObserver: closeSubject.asObserver(), sectionButtonTappedObserver: sectionButtonTappedSubject.asObserver()
)
let closeEventObservable = closeSubject.asObservable().map { _ in
return Coordinator.Event.goBack
}
let openDetailsEventObservable = openDetailsSubject.asObservable().map { _ in
return Coordinator.Event.goToDetails
}
let viewCompleteObservable = Observable.merge(closeEventObservable, openDetailsEventObservable)
let list = ViewModel.getData(repository: repository)
output = Output(
titleTextDriver: Observable.just(Constants.titleText).asDriver(onErrorJustReturn: Constants.titleText),
dataSourceDriver: Observable.just(list).asDriver(onErrorJustReturn: list),
viewComplete: viewCompleteObservable)
}
///TODO: To be updated as per response after API integration
static func getData(repository: Repository) -> [Data] {
return repository.getData()
}
func getSections() -> [TableDataSource] {
let List = ViewModel.getData(repository: Repository())
let unread = list.filter { $0.isUnRead == true }
let read = list.filter { $0.isUnRead == false }
let headerText = String(format:Constants.unreadHeaderText, unread.count)
let sections = [TableDataSource(
header: headerText,
items: unread,
sectionViewModel: SectionViewModel(
withHeader: headerText,
buttonType: ButtonType.markAllAsRead.rawValue,
items: unread)),
TableDataSource(
header: Constants.historyHeaderText,
items: read,
SectionViewModel: SectionViewModel(
withHeader: Constants.historyHeaderText,
buttonType: ButtonType.clearAll.rawValue,
items: read))]
return sections
}
}
i think your problem is with :
var identity: String {
return header
}
make a uuid and pass it to identity:
let id = UUID().uuidString
var identity: String {
return id
}
Using an older version of MessageKit, I made some changes to the library to get rid of the errors due to it being written in an older Swift version.
I integrated the Chat App into my own project, but the message text does not leave the message input bar when the send button is clicked.
Message.Swift File:
struct Message: MessageType {
let id: String?
let content: String
let sentDate: Date
let sender: SenderType
var kind: MessageKind {
if let image = image {
return .photo(image as! MediaItem)
} else {
return .text(content)
}
}
var messageId: String {
return id ?? UUID().uuidString
}
var image: UIImage? = nil
var downloadURL: URL? = nil
init(user: User, content: String) {
sender = Sender(id: user.uid, displayName: AppSettings.displayName)
self.content = content
sentDate = Date()
id = nil
}
init(user: User, image: UIImage) {
sender = Sender(id: user.uid, displayName: AppSettings.displayName)
self.image = image
content = ""
sentDate = Date()
id = nil
}
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard let sentDate = data["created"] as? Date else {
return nil
}
guard let senderID = data["senderID"] as? String else {
return nil
}
guard let senderName = data["senderName"] as? String else {
return nil
}
id = document.documentID
self.sentDate = sentDate
sender = Sender(id: senderID, displayName: senderName)
if let content = data["content"] as? String {
self.content = content
downloadURL = nil
} else if let urlString = data["url"] as? String, let url = URL(string: urlString) {
downloadURL = url
content = ""
} else {
return nil
}
}
}
extension Message: DatabaseRepresentation {
var representation: [String : Any] {
var rep: [String : Any] = [
"created": sentDate,
"senderID": sender.senderId,
"senderName": sender.displayName
]
if let url = downloadURL {
rep["url"] = url.absoluteString
} else {
rep["content"] = content
}
return rep
}
}
extension Message: Comparable {
static func == (lhs: Message, rhs: Message) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: Message, rhs: Message) -> Bool {
return lhs.sentDate < rhs.sentDate
}
}
ChatViewController:
private func save(_ message: Message) {
reference?.addDocument(data: message.representation) { error in
if let e = error {
print("Error sending message: \(e.localizedDescription)")
return
}
self.messagesCollectionView.scrollToBottom()
}
}
extension ChatViewController: MessagesLayoutDelegate {
func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize {
return .zero
}
func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize {
return CGSize(width: 0, height: 8)
}
func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
return 0
}
}
extension ChatViewController: MessagesDataSource {
func currentSender() -> SenderType {
return Sender(id: user.uid, displayName: AppSettings.displayName)
}
func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
return messages.count
}
func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int {
return messages.count
}
func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
return messages[indexPath.section]
}
func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
let name = message.sender.displayName
return NSAttributedString(
string: name,
attributes: [
.font: UIFont.preferredFont(forTextStyle: .caption1),
.foregroundColor: UIColor(white: 0.3, alpha: 1)
]
)
}
}
extension ChatViewController: MessageInputBarDelegate {
func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {
let message = Message(user: user, content: text)
save(message)
inputBar.inputTextView.text = ""
}
my viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
guard let id = channel.id else {
navigationController?.popViewController(animated: true)
return
}
reference = db.collection(["channels", id, "thread"].joined(separator: "/"))
messageListener = reference?.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error listening for channel updates: \(error?.localizedDescription ?? "No error")")
return
}
snapshot.documentChanges.forEach { change in
self.handleDocumentChange(change)
}
}
messageInputBar.delegate = self
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
}
It seems, that your pod or carthage file use messageKit with latest version like "3.0.0". So, there is some changes in delegate.
Try to change this peace of code:
extension ChatViewController: MessageInputBarDelegate {
func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {
let message = Message(user: user, content: text)
save(message)
inputBar.inputTextView.text = ""
}
with code below
extension ChatViewController: MessageInputBarDelegate {
func inputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {
let message = Message(user: user, content: text)
save(message)
inputBar.inputTextView.text = ""
}
}
If you want to use your old syntax, set messageKit version to some old in your pod or carthage file.
What I am trying to do is to have data from Parse be retrieved from columns by object order. All labels are connected to their respective outlets and all of the outputs retrieve their correct data.
When I run it and open a cell in the tableview it crashes and gives me Thread 1: EXC_BAD_INSTRUCTION (code=EXC>I386_INVOP, subcode=0x0) on this line: self.navBar.topItem?.title = output1 if I select the first cell, and then on this line: self.navBar.topItem?.title = output1b if I select the second cell.
Here is the full function:
firstObject is grabbing the first object in the "eventsdetail" column
secondObject is grabbing the second object in the "eventsdetail" column
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var query = PFQuery(className: "eventsdetail")
let runkey = query.orderByAscending("ID")
runkey.findObjectsInBackgroundWithBlock {
(objects: [PFObject]?, error : NSError?) -> Void in
if error == nil {
if let objects = objects as [PFObject]! {
for object in objects {
var firstObject = objects[0]
var secondObject = objects[1]
let output1 = firstObject.objectForKey("navTitle") as! String!
let output2 = firstObject.objectForKey("articleTitle") as! String!
let output3 = firstObject.objectForKey("written") as! String!
let output4 = firstObject.objectForKey("date") as! String!
let output5 = firstObject.objectForKey("article") as! String!
let output1b = secondObject.objectForKey("navTitle") as! String!
let output2b = secondObject.objectForKey("articleTitle") as! String!
let output3b = secondObject.objectForKey("written") as! String!
let output4b = secondObject.objectForKey("date") as! String!
let output5b = secondObject.objectForKey("article") as! String!
if indexPath.row == 0 {
self.performSegueWithIdentifier("0a", sender: nil)
self.tableview.deselectRowAtIndexPath(indexPath, animated: true)
self.navBar.topItem?.title = output1
self.articleTitle.text = output2
self.writtenBy.text = output3
self.date.text = output4
self.article.text = output5
} else if indexPath.row == 1 {
self.performSegueWithIdentifier("0a", sender: nil)
self.tableview.deselectRowAtIndexPath(indexPath, animated: true)
self.navBar.topItem?.title = output1b
self.articleTitle.text = output2b
self.writtenBy.text = output3b
self.date.text = output4b
self.article.text = output5b
}
}
}
}
}
}
If there is an easier way of doing this, please mention it, if not try to just solve this method's problem. I know it isn't the cleanest way of doing things.
I am not sure of how you Parse your data but if it can help you, here's how I would do:
//
// Test.swift
// Test
//
// Created by Charles-Olivier Demers on 16-01-04.
//
import UIKit
//import Parse
class EventViewController: UIViewController, UITableViewDelegate {
private var _info = [EventDetails]()
override func viewDidLoad() {
fetchData()
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier("0a", sender: nil)
self.tableview.deselectRowAtIndexPath(indexPath, animated: true)
self.navBar.topItem?.title.text = _info[indexPath.row].navTitle()
self.articleTitle.text = _info[indexPath.row].articleTitle()
self.writtenBy.text = _info[indexPath.row].writtenBy()
self.date.text = _info[indexPath.row].date()
self.article.text = _info[indexPath.row].article()
}
func fetchData() {
let query = PFQuery(className: "eventsDetails")
query.orderByAscending("ID")
query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, error: NSError?) -> Void in
if error == nil {
if let objects = objects {
for object in objects {
let navTitleObject = object["navTitle"] as! String
let articleTitleObject = object["articleTitle"] as! String
let writtenByObject = object["writtenByObject"] as! String
let dateObject = object["dateObject"] as! String
let articleObject = object["articleObject"] as! String
_info.append(EventDetails(navTitle: navTitleObject, articleTitle: articleTitleObject, writtenBy: writtenByObject, date: dateObject, article: articleObject))
}
}
}
else {
print("Error #\(error!.code)")
}
}
}
}
class EventDetails {
private var _navTitle: String!
private var _articleTitle: String!
private var _writtenBy: String!
private var _date: String!
private var _article: String!
init(navTitle: String, articleTitle: String, writtenBy: String, date: String, article: String) {
self._navTitle = navTitle
self._article = articleTitle
self._writtenBy = writtenBy
self._date = date
self._article = article
}
func navTitle() -> String {
return _navTitle
}
func articleTitle() -> String {
return _articleTitle
}
func writtenBy() -> String {
return _writtenBy
}
func date() -> String {
return _date
}
func article() -> String {
return _article
}
}
First of all, I would create a class named EventDetails. This class will take all the property of EventsDetails class on Parse. So when you fetch your data, you append the data you fetch in an array of EventDetails class in Swift.
After that in your
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
you take the value in your EventDetails array with the indexPath.row and you fill your Table View.
That is how I would do.