How do I search for hashTag in SearchController? - swift

I'm going to search for hashtags. The current code is now complete with the creation of a search bar. I am using RxSwift and Mvvm pattern. Here's the code I've been working on so far.How should I search for hashtags?
func setUI() {
self.searchBar = UISearchBar(frame: CGRect(x:300, y:0, width:300, height:20))
searchBtn.rx.tap.subscribe(onNext: { _ in
self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
UIView.animate(withDuration: 0.25) {
self.searchBar.frame = CGRect(x:0, y:0, width:300, height:20)
}
}).disposed(by: rx.disposeBag)
}
This is ViewController.
private let disposeBag = DisposeBag()
struct input {
let loadData: Signal<Void>
}
struct output {
let result: Signal<String>
let loadApplyList: PublishRelay<friends>
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let result = PublishSubject<String>()
let loadApplyList = PublishRelay<friends>()
input.loadData.asObservable().subscribe(onNext: { [weak self] in
guard let self = self else { return }
api.getFriend().subscribe(onNext: { (response, statuscode) in
switch statuscode {
case .ok:
if let response = response {
loadApplyList.accept(response)
}
default:
print("default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
return output(result: result.asSignal(onErrorJustReturn: ""), loadApplyList: loadApplyList)
}
}
And this is viewModel

Related

use a switch to change the value of boolean saved core data value

In my swift code below right now the code saves 2 boolean values to a core data boolean value. The two values are true false. I would like the user to turn on the switch and have the 2nd value be true as well. So it would be true true. I am trying to do that in func alternate() but I dont know how to exactly effect a specific value. Looking for any kind of help.
import UIKit;import CoreData
class ViewController: UIViewController {
var lbl = UILabel()
var sw = UISwitch()
var checkmarkButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
[ lbl,sw,checkmarkButton].forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
$0.layer.borderWidth = 1
}
helpBool.shareInstance.saveBoolean(true)
helpBool.shareInstance.saveBoolean(false)
getBool(imageNo: 1)
}
func getBool(imageNo:Int) {
// first check the array bounds
let info = helpBool.shareInstance.fetchBool()
if info.count > imageNo {
// if info[imageNo].bool {
if info[imageNo].bool == true {
checkmarkButton.setImage(UIImage(named:"unnamed"), for: .normal);
}
if info[imageNo].bool == false {
checkmarkButton.setImage(nil, for: .normal);
}
// }
}
}
#objc func alternate(){
//fetch alternate
getBool(imageNo: 1)
if sw.isOn = true {
helpBool.shareInstance.saveBoolean()
}
else {
helpBool.shareInstance.saveBoolean()
}
}
}
class helpBool: UIViewController{
static let shareInstance = helpBool()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func saveBoolean(_ boolean: Bool) {
let imageInstance = OnOff(context: context)
imageInstance.bool = boolean
do {
try context.save()
print("text is saved")
} catch {
print(error.localizedDescription)
}
}
func fetchBool() -> [OnOff] {
var fetchingImage = [OnOff]()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "OnOff")
do {
fetchingImage = try context.fetch(fetchRequest) as! [OnOff]
} catch {
print("Error while fetching the image")
}
return fetchingImage
}
}

Why is my alert controller not dismissing in my Swift app?

I have a custom view controller class which presents an alert controller when it receives a not connected notification from my network services class. I want to dismiss the alert controller when I receive a further connected notification from the network services class but I am unable to dismiss the presenting alert.
Here is my custom view controller class;
class CompanyPriceListVC: UITableViewController {
// MARK: - Properties
let companyStockSymbols = ["AAPL","AMZN", "MSFT", "GOOGL", "TSLA", "META","COINBASE:BTC", "BINANCE:BTCUSDT", "BINANCE:BNBBTC", "IC MARKETS:1", "IC MARKETS:2"]
let defaultStockInfo = StockInfo(tradeConditions: nil, price: 0.00, symbol: "-", timestamp: 0.0, volume: 0.0)
lazy var companyStockInfo = [StockInfo](repeating: defaultStockInfo, count: companyStockSymbols.count)
var companyDetailsVC:CompanyDetailsVC?
var tableRowSelected: Int?
let defaultPriceHistory = PriceHistory(previous: nil, relative: 0.0, absolute: 0.0)
lazy var priceHistory = [PriceHistory](repeating: defaultPriceHistory , count: companyStockSymbols.count)
var deviceOrientation: Orientation = .portrait
let activityIndicator: UIActivityIndicatorView = {
let activityIndicator = UIActivityIndicatorView()
activityIndicator.style = .large
activityIndicator.hidesWhenStopped = true
activityIndicator.color = .white
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
activityIndicator.startAnimating()
return activityIndicator
}()
let isConnected = Notification.Name(rawValue: isConnectedNotificationKey)
let isNotConnected = Notification.Name(rawValue: isDisconnectedNotificationKey)
let alertController = UIAlertController(title: "Connectivity Error", message: "Network connection lost. Please check your WiFi settings, mobile data settings or reception coverage.", preferredStyle: .alert)
// MARK: - Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
if UIDevice.current.orientation.isPortrait {
self.deviceOrientation = .portrait
self.tableView.register(CompanyStockCellPortrait.self, forCellReuseIdentifier: "CompanyStockCellPortrait")
} else {
self.deviceOrientation = .landscape
self.tableView.register(CompanyStockCellLandscape.self, forCellReuseIdentifier: "CompanyStockCellLandscape")
}
configureNavigationBar()
view.backgroundColor = .black
configureTableView()
configureConstraints()
createObservers()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func createObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(CompanyPriceListVC.dismissAndFetchStockInfo), name: isConnected, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(CompanyPriceListVC.notifyUserOnScreen(notification:)), name: isNotConnected, object: nil)
}
#objc private func fetchStockInfo() {
NetworkServices.sharedInstance.fetchStockInfo(symbols: companyStockSymbols, delegate: self)
}
#objc private func notifyUserOnScreen(notification: NSNotification) {
self.present(alertController, animated: true)
}
#objc public func dismissAndFetchStockInfo() {
print("DEBUG: isBeingPresented: \(alertController.isBeingPresented)")
if alertController.isBeingPresented {
self.alertController.dismiss(animated: true, completion: nil)
}
fetchStockInfo()
}
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
coordinator.animate(alongsideTransition: { (context) in
guard let windowInterfaceOrientation = self.windowInterfaceOrientation else { return }
if windowInterfaceOrientation.isPortrait {
self.deviceOrientation = .portrait
self.tableView.register(CompanyStockCellPortrait.self, forCellReuseIdentifier: "CompanyStockCellPortrait")
self.tableView.reloadData()
} else {
self.deviceOrientation = .landscape
self.tableView.register(CompanyStockCellLandscape.self, forCellReuseIdentifier: "CompanyStockCellLandscape")
self.tableView.reloadData()
}
})
}
private var windowInterfaceOrientation: UIInterfaceOrientation? {
return self.view.window?.windowScene?.interfaceOrientation
}
// MARK: - TableView Data Source Methods
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return companyStockInfo.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch deviceOrientation {
case .portrait:
let tableViewCellPortrait = tableView.dequeueReusableCell(withIdentifier: "CompanyStockCellPortrait", for: indexPath) as! CompanyStockCellPortrait
tableViewCellPortrait.symbolValue.text = companyStockInfo[indexPath.row].symbol
var latestPrice = companyStockInfo[indexPath.row].price
latestPrice = round(latestPrice * 100.0) / 100.00
tableViewCellPortrait.priceValue.text = String(format: "%.2f", arguments:[latestPrice])
if let _ = priceHistory[indexPath.row].previous {
let absoluteDifference = priceHistory[indexPath.row].absolute
let relativeDifference = priceHistory[indexPath.row].relative
if absoluteDifference > 0.0 {
tableViewCellPortrait.priceDirectionImageView.image = UIImage(systemName: "arrowtriangle.up.fill")
tableViewCellPortrait.priceDirectionImageView.tintColor = .systemGreen
tableViewCellPortrait.relativePriceDiffValue.text = String(format: "%.2f%%", arguments: [relativeDifference])
tableViewCellPortrait.relativePriceDiffValue.textColor = .systemGreen
tableViewCellPortrait.absolutePriceDiffValue.text = String(format: "(%.2f)", arguments: [absoluteDifference])
tableViewCellPortrait.absolutePriceDiffValue.textColor = .systemGreen
} else if absoluteDifference < 0.0 {
tableViewCellPortrait.priceDirectionImageView.image = UIImage(systemName: "arrowtriangle.down.fill")
tableViewCellPortrait.priceDirectionImageView.tintColor = .systemRed
tableViewCellPortrait.relativePriceDiffValue.text = String(format: "%.2f%%", arguments: [relativeDifference])
tableViewCellPortrait.relativePriceDiffValue.textColor = .systemRed
tableViewCellPortrait.absolutePriceDiffValue.text = String(format: "(%.2f)", arguments: [absoluteDifference])
tableViewCellPortrait.absolutePriceDiffValue.textColor = .systemRed
} else {
tableViewCellPortrait.priceDirectionImageView.image = UIImage(systemName: "diamond.fill")
tableViewCellPortrait.priceDirectionImageView.tintColor = .white
tableViewCellPortrait.relativePriceDiffValue.text = "0.00%"
tableViewCellPortrait.relativePriceDiffValue.textColor = .white
tableViewCellPortrait.absolutePriceDiffValue.text = "(0.00)"
tableViewCellPortrait.absolutePriceDiffValue.textColor = .white
}
} else {
tableViewCellPortrait.priceDirectionImageView.image = UIImage()
tableViewCellPortrait.priceDirectionImageView.tintColor = .white
tableViewCellPortrait.relativePriceDiffValue.text = ""
tableViewCellPortrait.absolutePriceDiffValue.text = ""
}
return tableViewCellPortrait
case .landscape:
let tableViewCellLandscape = tableView.dequeueReusableCell(withIdentifier: "CompanyStockCellLandscape", for: indexPath) as! CompanyStockCellLandscape
tableViewCellLandscape.symbolValue.text = companyStockInfo[indexPath.row].symbol
var latestPrice = companyStockInfo[indexPath.row].price
latestPrice = round(latestPrice * 100.0) / 100.00
tableViewCellLandscape.priceValue.text = String(format: "%.2f", arguments:[latestPrice])
if let _ = priceHistory[indexPath.row].previous {
let absoluteDifference = priceHistory[indexPath.row].absolute
let relativeDifference = priceHistory[indexPath.row].relative
if absoluteDifference > 0.0 {
tableViewCellLandscape.priceDirectionImageView.image = UIImage(systemName: "arrowtriangle.up.fill")
tableViewCellLandscape.priceDirectionImageView.tintColor = .systemGreen
tableViewCellLandscape.relativePriceDiffValue.text = String(format: "%.2f%%", arguments: [relativeDifference])
tableViewCellLandscape.relativePriceDiffValue.textColor = .systemGreen
tableViewCellLandscape.absolutePriceDiffValue.text = String(format: "(%.2f)", arguments: [absoluteDifference])
tableViewCellLandscape.absolutePriceDiffValue.textColor = .systemGreen
} else if absoluteDifference < 0.0 {
tableViewCellLandscape.priceDirectionImageView.image = UIImage(systemName: "arrowtriangle.down.fill")
tableViewCellLandscape.priceDirectionImageView.tintColor = .systemRed
tableViewCellLandscape.relativePriceDiffValue.text = String(format: "%.2f%%", arguments: [relativeDifference])
tableViewCellLandscape.relativePriceDiffValue.textColor = .systemRed
tableViewCellLandscape.absolutePriceDiffValue.text = String(format: "(%.2f)", arguments: [absoluteDifference])
tableViewCellLandscape.absolutePriceDiffValue.textColor = .systemRed
} else {
tableViewCellLandscape.priceDirectionImageView.image = UIImage(systemName: "diamond.fill")
tableViewCellLandscape.priceDirectionImageView.tintColor = .white
tableViewCellLandscape.relativePriceDiffValue.text = "0.00%"
tableViewCellLandscape.relativePriceDiffValue.textColor = .white
tableViewCellLandscape.absolutePriceDiffValue.text = "(0.00)"
tableViewCellLandscape.absolutePriceDiffValue.textColor = .white
}
} else {
tableViewCellLandscape.priceDirectionImageView.image = UIImage()
tableViewCellLandscape.priceDirectionImageView.tintColor = .white
tableViewCellLandscape.relativePriceDiffValue.text = ""
tableViewCellLandscape.absolutePriceDiffValue.text = ""
}
return tableViewCellLandscape
}
}
// MARK: - TableView Delegate Methods
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100.0
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
companyDetailsVC = CompanyDetailsVC(companyStockInfo: companyStockInfo[indexPath.row])
tableRowSelected = indexPath.row
navigationController?.pushViewController(companyDetailsVC!, animated: true)
}
// MARK: - Helper Functions
private func configureNavigationBar() {
UINavigationBar.appearance().titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
navigationController?.navigationBar.topItem?.title = "Trade Tracker"
navigationController?.navigationBar.barStyle = .black
navigationController?.navigationBar.prefersLargeTitles = true
}
private func configureTableView() {
tableView.delegate = self
tableView.dataSource = self
tableView.separatorStyle = .singleLine
tableView.separatorColor = .white
tableView.tableFooterView = UIView()
}
private func configureConstraints() {
tableView.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
activityIndicator.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 250)
])
tableView.layoutSubviews()
}
}
// MARK: - Network Service Delegate Method
extension CompanyPriceListVC: NetworkServicesDelegate {
func sendStockInfo(stocksInfo: [String : StockInfo]) {
DispatchQueue.main.async {
// Compute absolute and relative price differences
for (index, symbol) in self.companyStockSymbols.enumerated() {
if stocksInfo[symbol] != nil {
self.priceHistory[index].previous = self.companyStockInfo[index].price
self.companyStockInfo[index] = stocksInfo[symbol]!
var latestPrice = self.companyStockInfo[index].price
latestPrice = round(latestPrice * 100.0) / 100.00
if let previous = self.priceHistory[index].previous {
let previousPrice = round(previous * 100.0) / 100.0
var absoluteDifference = latestPrice - previousPrice
absoluteDifference = round(absoluteDifference * 100.0) / 100.0
self.priceHistory[index].absolute = absoluteDifference
var relativeDifference = 0.0
if previousPrice != 0.00 {
relativeDifference = (absoluteDifference / previousPrice) * 100.0
relativeDifference = round(relativeDifference * 100) / 100.0
}
self.priceHistory[index].relative = relativeDifference
}
}
}
self.tableView.reloadData()
self.activityIndicator.stopAnimating()
guard let rowSelected = self.tableRowSelected else { return }
self.companyDetailsVC!.companyStockInfo = self.companyStockInfo[rowSelected]
self.companyDetailsVC!.relativeDifference = self.priceHistory[rowSelected].relative
self.companyDetailsVC!.absoluteDifference = self.priceHistory[rowSelected].absolute
}
}
}
and here is my network services class;
import UIKit
import Starscream
import Network
protocol NetworkServicesDelegate: AnyObject {
func sendStockInfo(stocksInfo: [String: StockInfo])
}
final class NetworkServices {
static let sharedInstance = NetworkServices()
var request = URLRequest(url: FINNHUB_SOCKET_STOCK_INFO_URL!)
var socket: WebSocket!
public private(set) var isConnected = false
var stocksInfo: [String: StockInfo] = [:]
var socketResults: [String: [StockInfo]] = [:]
weak var delegate: NetworkServicesDelegate?
var stockSymbols: [String] = []
private init() {
request.timeoutInterval = 5
socket = WebSocket(request: request)
socket.delegate = self
}
private let queue = DispatchQueue.global()
private let monitor = NWPathMonitor()
public func startMonitoring() {
monitor.start(queue: queue)
self.monitor.pathUpdateHandler = { [weak self] path in
if path.status == .satisfied {
// connect the socket
self?.socket.connect()
} else {
// disconnect the socket
self?.socket.disconnect()
// post notification that socket is now disconnected
DispatchQueue.main.async {
let name = Notification.Name(rawValue: isDisconnectedNotificationKey)
NotificationCenter.default.post(name: name, object: nil)
}
}
}
}
public func stopMonitoring() {
monitor.cancel()
}
func fetchStockInfo(symbols: [String], delegate: CompanyPriceListVC) {
stockSymbols = symbols
self.delegate = delegate
for symbol in symbols {
let string = FINNHUB_SOCKET_MESSAGE_STRING + symbol + "\"}"
socket.write(string: string)
}
}
private func parseJSONSocketData(_ socketString: String) {
self.socketResults = [:]
self.stocksInfo = [:]
let decoder = JSONDecoder()
do {
let socketData = try decoder.decode(SocketData.self, from: socketString.data(using: .utf8)!)
guard let stockInfoData = socketData.data else { return }
for stockInfo in stockInfoData {
let symbol = stockInfo.symbol
if self.socketResults[symbol] == nil {
self.socketResults[symbol] = [StockInfo]()
}
self.socketResults[symbol]?.append(stockInfo)
}
for (symbol, stocks) in self.socketResults {
for item in stocks {
if self.stocksInfo[symbol] == nil {
self.stocksInfo[symbol] = item
} else if item.timestamp > self.stocksInfo[symbol]!.timestamp {
self.stocksInfo[symbol] = item
}
}
}
self.delegate?.sendStockInfo(stocksInfo: self.stocksInfo)
} catch {
print("DEBUG: error: \(error.localizedDescription)")
}
}
func fetchCompanyInfo(symbol: String, completion: #escaping (CompanyInfo?, UIImage?)->()) {
let urlString = FINNHUB_HTTP_COMPANY_INFO_URL_STRING + symbol + "&token=" + FINNHUB_API_TOKEN
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error fetching company info: \(error)")
}
guard let data = data else { return }
let decoder = JSONDecoder()
do {
let companyInfo = try decoder.decode(CompanyInfo.self, from: data)
guard let logoURL = URL(string: companyInfo.logo) else { return }
let task = URLSession.shared.dataTask(with: logoURL) { data, response, error in
if let error = error {
print("Error fetching logo image: \(error)")
}
guard let data = data else { return }
guard let logoImage = UIImage(data: data) else { return }
completion(companyInfo, logoImage)
}
task.resume()
} catch {
print("Error decoding JSON: \(error)")
completion(nil, nil)
}
}
task.resume()
}
}
extension NetworkServices: WebSocketDelegate {
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected(_):
self.isConnected = true
// post notification that socket is now connected
let name = Notification.Name(rawValue: isConnectedNotificationKey)
NotificationCenter.default.post(name: name, object: nil)
case .disconnected(let reason, let code):
print("DEBUG: Got disconnected.")
self.isConnected = false
case .cancelled:
print("DEBUG: cancelled.")
// post notification that socket is not connected
case .reconnectSuggested(let suggestReconnect):
print("DEBUG: suggestReconnect = \(suggestReconnect)")
// post notification that socket is not connected
case .viabilityChanged(let viabilityChanged):
print("DEBUG: viabilityChange = \(viabilityChanged)")
case .error(let error):
print("DEBUG error: \(String(describing: error?.localizedDescription))")
case .text(let socketString):
parseJSONSocketData(socketString)
default:
break
}
}
}
I've tried querying the isBeingPresented property of the alert controller but it always tests as false even though I can see the alert controller is being presented.
You could do a check if the currently presented UIViewController is actually the alertController like this:
#objc public func dismissAndFetchStockInfo() {
if presentedViewController == alertController {
alertController.dismiss(animated: true, completion: nil)
}
fetchStockInfo()
}
This is because isBeingPresented is only valid inside the view[Will|Did][Disa|A]ppear methods.

Am I using firebase api incorrectly?

Whenever paging in tableview, the view model is running fetchDataRx. It works well, but I don't think it's right to sort the entire data every time you paginate and call the addSnapShotListener. If my code is not correct, how can I correct it?
// MARK: ViewController.swift
timeLineTableView.rx.didScroll
.withLatestFrom(viewModel.activated)
.subscribe(onNext: { [weak self] isActivated in
if !isActivated {
guard let self = self else { return }
let position = self.timeLineTableView.contentOffset.y
if position > self.timeLineTableView.contentSize.height - 100 - self.timeLineTableView.frame.size.height {
self.viewModel.fetchPosts.onNext(())
}
}
})
.disposed(by: disposeBag)
//MARK: ViewModel.swift
let fetchPosts: AnyObserver<Void>
let fetching = PublishSubject<Void>()
fetchPosts = fetching.asObserver()
fetching
.do(onNext: { _ in activating.onNext(true) })
.withLatestFrom(posts)
.map { $0.count }
.flatMap{ (count) -> Observable<[post]> in
fireBaseService.fetchDataRx(startIdx: count) }
.map { $0.map { ViewPost(post: $0) } }
.do(onNext: { _ in activating.onNext(false) })
.do(onError: { err in error.onNext(err) })
.subscribe(onNext: { newPosts in
let oldData = posts.value
posts.accept(oldData + newPosts)
})
.disposed(by: disposeBag)
//MARK: FirebaseService.swift
protocol FirebaseServiceProtocol {
func fetchDataRx(startIdx: Int) -> Observable<[post]>
func fetchData(startIdx: Int, completion: #escaping (Result<[post], Error>) -> Void)
}
class FireBaseService: FirebaseServiceProtocol {
func fetchDataRx(startIdx: Int) -> Observable<[post]> {
return Observable.create { (observer) -> Disposable in
self.fetchData(startIdx: startIdx) { result in
switch result {
case .success(let data):
observer.onNext(data)
case .failure(let error):
observer.onError(error)
}
observer.onCompleted()
}
return Disposables.create()
}
}
func fetchData(startIdx: Int, completion: #escaping (Result<[post], Error>) -> Void) {
let db = Firestore.firestore()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
if startIdx == 0 {
DispatchQueue.global().async {
let first = db.collection("lolCourt")
.order(by: "date")
.limit(to: 8)
var nextPosts = [post]()
first.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
guard let url = document.data()["url"] as? String else {
continue
}
guard let champion1 = document.data()["champion1"] as? String else {
continue
}
guard let champion1Votes = document.data()["champion1Votes"] as? Double else {
continue
}
guard let champion2 = document.data()["champion2"] as? String else {
continue
}
guard let champion2Votes = document.data()["champion2Votes"] as? Double else {
continue
}
guard let text = document.data()["text"] as? String else {
continue
}
guard let date = document.data()["date"] as? Double else {
continue
}
nextPosts.append(post(url: url,
champion1: champion1,
champion1Votes: champion1Votes,
champion2: champion2,
champion2Votes: champion2Votes,
text: text,
date: formatter.string(from: Date(timeIntervalSince1970: date))))
}
}
completion(.success(nextPosts))
}
}
}
else {
DispatchQueue.global().async {
let first = db.collection("lolCourt")
.order(by: "date")
.limit(to: startIdx)
first.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving : \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
return
}
let next = db.collection("lolCourt")
.order(by: "date")
.start(afterDocument: lastSnapshot)
.limit(to: 8)
var nextPosts = [post]()
next.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
guard let url = document.data()["url"] as? String else {
continue
}
guard let champion1 = document.data()["champion1"] as? String else {
continue
}
guard let champion1Votes = document.data()["champion1Votes"] as? Double else {
continue
}
guard let champion2 = document.data()["champion2"] as? String else {
continue
}
guard let champion2Votes = document.data()["champion2Votes"] as? Double else {
continue
}
guard let text = document.data()["text"] as? String else {
continue
}
guard let date = document.data()["date"] as? Double else {
continue
}
nextPosts.append(post(url: url,
champion1: champion1,
champion1Votes: champion1Votes,
champion2: champion2,
champion2Votes: champion2Votes,
text: text,
date: formatter.string(from: Date(timeIntervalSince1970: date))))
}
}
completion(.success(nextPosts))
}
}
}
}
}
}
I think you are doing it wrong... I would expect to see something more like this:
class FireBaseService {
func getPage<T>(query: Query? = nil, build: #escaping ([String: Any]) -> T) -> Observable<([T], nextPage: Query?)> {
Observable.create { observer in
let db = Firestore.firestore()
let page = query ?? db.collection("lolCourt")
.order(by: "date")
.limit(to: 8)
let listener = page
.addSnapshotListener { snapshot, error in
guard let snapshot = snapshot else { observer.onError(error ?? RxError.unknown); return }
let items = snapshot.documents.map { build($0.data()) }
if let lastSnapshot = snapshot.documents.last {
let next = page
.start(afterDocument: lastSnapshot)
observer.onSuccess((items, nextPage: next))
}
else {
observer.onSuccess((items, nextPage: nil))
}
}
return Disposables.create { listener.remove() }
}
}
}
Use the above in your favorite state machine system. Here is an example using my CLE library.
// in view controller
let fireBaseService = FireBaseService()
let activityIndicator = ActivityIndicator()
let errorRouter = ErrorRouter()
func getPage(nextPage: Query?) -> Observable<([Post?], nextPage: Query?)> {
fireBaseService.getPage(query: nextPage, build: Post.init(dict:))
.rerouteError(errorRouter)
.trackActivity(activityIndicator)
}
let posts = cycle(
inputs: [
getPage(nextPage: nil).map(ViewModel.Input.response),
timeLineTableView.rx.reachedBottom(offset: 20).map(to: ViewModel.Input.next)
],
initialState: ([Post?](), nextPage: Query?.none),
environment: getPage(nextPage:),
reduce: ViewModel.reduce(state:input:getPage:)
)
.map { $0.0.compactMap { $0 } }
and the view model:
enum ViewModel {
enum Input {
case response([Post?], nextPage: Query?)
case next
}
static func reduce(state: inout ([Post?], nextPage: Query?), input: Input, getPage: #escaping (Query) -> Observable<([Post?], nextPage: Query?)>) -> Observable<Input> {
switch input {
case let .response(posts, nextPage):
state.0 += posts
state.nextPage = nextPage
case .next:
guard let nextPage = state.nextPage else { break }
return getPage(nextPage)
.map(Input.response)
}
return .empty()
}
}

How do I bind a ViewModel to a Collationview?

I'm trying to bind a view model to a collection view. But I don't know how to do it. I'm using MVVM pattern and RxSwift, and I've only tried table view binding before. Here's my view model and the view controller code I've done so far.
class SearchViewModel: ViewModelType {
private let disposeBag = DisposeBag()
struct input {
let loadData: Signal<Void>
}
struct output {
let result: Signal<String>
let loadApplyList: PublishRelay<friends>
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let result = PublishSubject<String>()
let loadApplyList = PublishRelay<friends>()
input.loadData.asObservable().subscribe(onNext: { [weak self] in
guard let self = self else { return }
api.getFriend().subscribe(onNext: { (response, statuscode) in
switch statuscode {
case .ok:
if let response = response {
loadApplyList.accept(response)
}
default:
print("default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
return output(result: result.asSignal(onErrorJustReturn: ""), loadApplyList: loadApplyList)
}
}
This is my ViewModel code
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
}
And this is my ViewController code.
How should the collection view bind?
Here is what your view model should look like:
class SearchViewModel {
// no need for a disposedBag. If you are putting a disposeBag in your view model, you are likely doing something wrong.
struct input {
let loadData: Signal<Void>
}
struct output {
let loadApplyList: Driver<[User]> // you should be passing an array here, not an object.
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let friendResult = input.loadData
.flatMapLatest {
api.getFriend()
.compactMap { $0.0.map(Result<friends, Error>.success) }
.asDriver(onErrorRecover: { Driver.just(Result<friends, Error>.failure($0)) })
}
let loadApplyList = friendResult
.compactMap { (result) -> [User]? in
guard case let .success(list) = result else { return nil }
return list.friends
}
return output(loadApplyList: loadApplyList)
}
}
Now in your view controller, you can bind it like this:
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
output.loadApplyList
.drive(collectionView.rx.items(cellIdentifier: "Cell", cellType: MyCellType.self)) { index, item, cell in
// configure cell with item here
}
.disposed(by: disposeBag)
}

RxDatasource in RxSwift reload animation don't update data source

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
}