UIBindingObserver for UINavigationBar does not work as expected - ios

I bind to UIBindingObserver for barTintColor new UIColor and title, but it does not showing. Wired thing is that when I drag UIViewController back and release everything appear
Code
extension Reactive where Base: UINavigationBar {
var barTintColor: UIBindingObserver<Base, UIColor> {
return UIBindingObserver(UIElement: self.base) { navigationBar, barTintColor in
navigationBar.barTintColor = barTintColor
}
}
}
class BaseViewController: UIViewController {
private(set) var tintColor: Variable<UIColor> = Variable(ColorConstants.tintColor)
override func viewDidLoad() {
super.viewDidLoad()
if let navigationBar = self.navigationController?.navigationBar {
self.tintColor.asObservable()
.bind(to: navigationBar.rx.barTintColor)
.addDisposableTo(self.disposeBag)
}
}
}
class TasksListViewController: BaseViewController {
var viewModel: TasksListViewModel!
...
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.colorObsrvable
.unwrap()
.bind(to: self.tintColor)
.addDisposableTo(self.disposeBag)
self.viewModel.titleObsrvable
.unwrap()
.bind(to: self.rx.title)
.addDisposableTo(self.disposeBag)
}
}
class TasksListViewModel {
private(set) var titleObsrvable: Observable<String?>!
private(set) var colorObsrvable: Observable<UIColor?>!
init(from goal: Goal) {
let goalPredicate = NSPredicate(format: "(SELF = %#)", self.goal.objectID)
self.goalObservable = CoreDataModel.shared.rx.fetchObject(predicate: goalPredicate)
self.titleObsrvable = goalObservable.map { $0?.name }
self.colorObsrvable = goalObservable.map { $0?.color }
}
}
extension Reactive where Base: CoreDataModel {
func fetchObjects<T: NSManagedObject>(predicate: NSPredicate? = nil) -> Observable<[T]> {
let entityName = String(describing: T.self)
let fetchRequest = NSFetchRequest<T>(entityName: entityName)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
fetchRequest.predicate = predicate
return base.managedObjectContext.rx.entities(fetchRequest: fetchRequest)
}
}
class Goal is just class Entitiy
What I have tried
Yes it's MainThread, I tested. And also run this task on GCD MainThread ❌
added observeOn(MainScheduler.sharedInstance) does not help ❌
put breakpoints inside the UIBindingObserver when drag and release it does not call ❌
added title in storyboard to NavBar and it works, but with wired delay. First it shows "Ti..." and then "Title" ✅
Replaced(And its works✅ idk why):
self.titleObsrvable = goalObservable.map { $0?.name }
self.colorObsrvable = goalObservable.map { $0?.color }
with:
self.titleObsrvable = Observable.of("1234")
self.colorObsrvable = Observable.of(UIColor.red)
Any idea what I'm doing wrong?
If you want to check whole project you can see it here:
https://github.com/Yerkenabildin/my-everest

It seems to be some sort of race condition. You are setting the barTintColor at the same moment that the navigation controller is starting its animation. If you make sure you don't change the tin color until after the animation is complete, it will work the way you want.
Also, you have two different binders to rx.barTintColor of the same navigation bar and they are feeding the bar conflicting information. You should only have a single binder if at all possible.

Related

Accessibility identifier for UI subviews based on parent view id

I have a custom view that has some subviews like labels and text fields. When I use multiple custom views into one controller I want to know the subview Accessibility Identifier. What I want to achieve is that set parent identifier(parent_accessibility_identifier) and than subview identifier can be an extension of it (eg parent_accessibility_identifier_label, parent_accessibility_identifier_text_field). Can we do this by setting the parent identifier accessibly to false and adding labels and text into the child's view but is there any better way to do it? this code doesn't work in the subview class.
public override var accessibilityIdentifier: String? {
didSet {
if let accessibilityIdentifier = self.accessibilityIdentifier {
self.accessibilityIdentifier = accessibilityIdentifier + "_label"
}
}
}
this work in the custom view class
public override var accessibilityIdentifier: String? {
didSet {
guard let accessibilityIdentifier = accessibilityIdentifier else { return }
textLabel.accessibilityIdentifier = accessibilityIdentifier + "_text_label"
}
}
I would suggest using swizzling for that. It enables you to override the behavior of opened properties and functions of system-level frameworks.
First, put the swizzling code somewhere:
///
extension UIView {
/**
*/
class func swizzle() {
let orginalSelectors = [
#selector(getter: accessibilityIdentifier),
]
let swizzledSelectors = [
#selector(getter: swizzledAccessibilityIdentifier)
]
let orginalMethods = orginalSelectors.map { class_getInstanceMethod(UIView.self, $0) }
let swizzledMethods = swizzledSelectors.map { class_getInstanceMethod(UIView.self, $0) }
orginalSelectors.enumerated().forEach { item in
let didAddMethod = class_addMethod(
UIView.self,
item.element,
method_getImplementation(swizzledMethods[item.offset]!),
method_getTypeEncoding(swizzledMethods[item.offset]!))
if didAddMethod {
class_replaceMethod(
UIView.self,
swizzledSelectors[item.offset],
method_getImplementation(orginalMethods[item.offset]!),
method_getTypeEncoding(orginalMethods[item.offset]!))
} else {
method_exchangeImplementations(orginalMethods[item.offset]!, swizzledMethods[item.offset]!)
}
}
}
/// that's where you override `accessibilityIdentifier` getter for
#objc var swizzledAccessibilityIdentifier: String {
if let parentIdentifier = superview?.accessibilityIdentifier {
return "\(parentIdentifier)_\(self.swizzledAccessibilityIdentifier)"
} else {
return self.swizzledAccessibilityIdentifier
}
}
}
Call UIView.swizzle(). Usually, you do it at the app launch. Somewhere in the AppDelegate or similar.
Setup view hierarchy, assign identifiers and test:
class ParentView: UIView {}
class SubclassedChildView: UIView {}
let parentView = ParentView()
let child1 = SubclassedChildView()
let child2 = UIView()
parentView.accessibilityIdentifier = "parent"
child1.accessibilityIdentifier = "child1"
child2.accessibilityIdentifier = "child2"
parentView.addSubview(child1)
child1.addSubview(child2)
print(parentView.accessibilityIdentifier) // "parent"
print(child1.accessibilityIdentifier) // "parent_child1"
print(child2.accessibilityIdentifier) // "parent_child1_child2"

How can I simulate a tableView row selection using RxSwift

I have a UITableViewController setup as below.
View Controller
class FeedViewController: BaseTableViewController, ViewModelAttaching {
var viewModel: Attachable<FeedViewModel>!
var bindings: FeedViewModel.Bindings {
let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.mapToVoid()
.asDriverOnErrorJustComplete()
let refresh = tableView.refreshControl!.rx
.controlEvent(.valueChanged)
.asDriver()
return FeedViewModel.Bindings(
fetchTrigger: Driver.merge(viewWillAppear, refresh),
selection: tableView.rx.itemSelected.asDriver()
)
}
override func viewDidLoad() {
super.viewDidLoad()
}
func bind(viewModel: FeedViewModel) -> FeedViewModel {
viewModel.posts
.drive(tableView.rx.items(cellIdentifier: FeedTableViewCell.reuseID, cellType: FeedTableViewCell.self)) { _, viewModel, cell in
cell.bind(to: viewModel)
}
.disposed(by: disposeBag)
viewModel.fetching
.drive(tableView.refreshControl!.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.errors
.delay(0.1)
.map { $0.localizedDescription }
.drive(errorAlert)
.disposed(by: disposeBag)
return viewModel
}
}
View Model
class FeedViewModel: ViewModelType {
let fetching: Driver<Bool>
let posts: Driver<[FeedDetailViewModel]>
let selectedPost: Driver<Post>
let errors: Driver<Error>
required init(dependency: Dependency, bindings: Bindings) {
let activityIndicator = ActivityIndicator()
let errorTracker = ErrorTracker()
posts = bindings.fetchTrigger
.flatMapLatest {
return dependency.feedService.getFeed()
.trackActivity(activityIndicator)
.trackError(errorTracker)
.asDriverOnErrorJustComplete()
.map { $0.map(FeedDetailViewModel.init) }
}
fetching = activityIndicator.asDriver()
errors = errorTracker.asDriver()
selectedPost = bindings.selection
.withLatestFrom(self.posts) { (indexPath, posts: [FeedDetailViewModel]) -> Post in
return posts[indexPath.row].post
}
}
typealias Dependency = HasFeedService
struct Bindings {
let fetchTrigger: Driver<Void>
let selection: Driver<IndexPath>
}
}
I have binding that detects when a row is selected, a method elsewhere is triggered by that binding and presents a view controller.
I am trying to assert that when a row is selected, a view is presented, however I cannot successfully simulate a row being selected.
My attempted test is something like
func test_ViewController_PresentsDetailView_On_Selection() {
let indexPath = IndexPath(row: 0, section: 0)
sut.start().subscribe().disposed(by: rx_disposeBag)
let viewControllers = sut.navigationController.viewControllers
let vc = viewControllers.first as! FeedViewController
vc.tableView(tableView, didSelectRowAt: indexPath)
XCTAssertNotNil(sut.navigationController.presentedViewController)
}
But this fails with the exception
unrecognized selector sent to instance
How can I simulate the row is selected in my unit test?
The short answer is you don't. Unit tests are not a place for UIView objects and UI object manipulation. You want to add a UI Testing target for that sort of test.

Listen for updates in the data model array when one of its properties changes

I have a custom UITableViewCell which has a data model array, and a UILabel as this:
class ItemCustomizationCollectionViewCell: UITableViewCell {
var customizationData: CustomizationData?
let priceLabel: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 18)
label.textAlignment = .left
label.textColor = UIColor.gray
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
//other init and constraints
}
Now, the CustomizationData looks like this:
class CustomizationData {
let title: String
var customizationOptions: [PickerOption]
var choosenOption: PickerOption?
init(title: String, customizationOptions: [PickerOption], choosenOption: PickerOption?) {
self.title = title
self.customizationOptions = customizationOptions
self.choosenOption = choosenOption
}
}
and the PickerOption is:
class PickerOption {
let title: String
let price: String
init(title: String, price: String) {
self.title = title
self.price = price
}
}
My Question is:
I want to listen to customizationData's choosenOption gets set, and get the title to change the UILabel's text to the choosenOption's price.
I tried to add a didSet to the customizationData but it's not being called as one of its properties is changing. Not the customizationData itself.
What you can do is possible, but to trigger a didSet customizationOptions of you need to change its value, basically in 2 ways:
if customizationOptions is a reference type, you need to change it with another reference
if customizationOptions is a value type such as an Array, the didSet should be called also if you change a value inside it, because the array change the struct completely.
To understand better here is an example.
Using a value type:
class Mine {
var list: [String] = ["Hello"] {
didSet {
print("Array changed \(list)")
}
}
}
var mine = Mine()
mine.list.append("World") //Array changed ["Hello", "World"]
Using a reference type:
class Mine {
var list: NSArray = ["Hello"] {
didSet {
print("Array changed \(list)")
}
}
}
var mine = Mine()
mine.list.adding("World")
//Doesn't print nothing
mine.list = ["World"]
//print World
I think you can do this using delegation. Create a CustomizationDataDelegate protocol:
protocol CustomizationDataDelegate {
func choosenOptionDidChange(to newValue: PickerOption)
}
Then create a delegate property in the CustomizationData class, for instance:
internal var delegate : CustomizationDataDelegate?
And add a didSet to choosenOption:
var choosenOption : PickerOption? {
didSet{
if let _ = choosenOption {
delegate?.choosenOptionDidChange(to choosenOption!)
}
}
}
Then register the ItemCustomizationCollectionViewCell to the CustomizationDataDelegate protocol, and then when you have some customization data, make sure the delegate is set to the cell's value:
class ItemCustomizationCollectionViewCell: UITableViewCell, CustomizationDataDelegate {
var customizationData: CustomizationData? {
didSet {
if let _ = customizationData {
if let _ = customizationData!.choosenOption {
// You need this in case the choosenOption has already been set
choosenOptionDidChange(to: customizationData!.choosenOption)
}
// You need this to listen for future changes
customizationData!.delegate = self
}
}
}
let priceLabel: UILabel = {
// ...
// In your class' function declarations...
//# MARK:- CustomizationDataDelegate methods
func choosenOptionDidChange(to newValue: PickerOption) -> Void {
// Update the label's value here based on newValue
}
}
Hope that helps.

Dynamically filter results with RxSwift and Realm

I have a very simple project, where I want to dynamically filter content in UITableView regarding pressed index in UISegmentedControl. I'm using MVVM with RxSwift, Realm and RxDataSources. So my problem, that if I want to update content in UITableView I need to create 'special' DisposeBag, only for that purposes, and on each selection in UISegmentedControl nil it and create again. Only in this case, if I'm understand right, subscription is re-newed, and UITableView displays new results from Realm.
So is there any better way to do such operation? Without subscribing every time, when I switch tab in UISegmentedControl. Here's my code:
//ViewController
class MyViewController : UIViewController {
//MARK: - Props
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var segmentedControl: UISegmentedControl!
let dataSource = RxTableViewSectionedReloadDataSource<ItemsSection>()
let disposeBag = DisposeBag()
var tableViewBag: DisposeBag!
var viewModel: MyViewModel = MyViewModel()
//MARK: - View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.setupRxTableView()
}
//MARK: - Setup observables
fileprivate func setupRxTableView() {
dataSource.configureCell = { ds, tv, ip, item in
let cell = tv.dequeueReusableCell(withIdentifier: "ItemCell") as! ItemTableViewCell
return cell
}
bindDataSource()
segmentedControl.rx.value.asDriver()
.drive(onNext: {[weak self] index in
guard let sSelf = self else { return }
switch index {
case 1:
sSelf.bindDataSource(filter: .active)
case 2:
sSelf.bindDataSource(filter: .groups)
default:
sSelf.bindDataSource()
}
}).disposed(by: disposeBag)
}
private func bindDataSource(filter: Filter = .all) {
tableViewBag = nil
tableViewBag = DisposeBag()
viewModel.populateApplying(filter: filter)
}).bind(to: self.tableView.rx.items(dataSource: dataSource))
.disposed(by: tableViewBag)
}
}
//ViewModel
class MyViewModel {
func populateApplying(filter: Filter) -> Observable<[ItemsSection]> {
return Observable.create { [weak self] observable -> Disposable in
guard let sSelf = self else { return Disposables.create() }
let realm = try! Realm()
var items = realm.objects(Item.self).sorted(byKeyPath: "date", ascending: false)
if let predicate = filter.makePredicate() { items = items.filter(predicate) }
let section = [ItemsSection(model: "", items: Array(items))]
observable.onNext(section)
sSelf.itemsToken = items.addNotificationBlock { changes in
switch changes {
case .update(_, _, _, _):
let section = [ItemsSection(model: "", items: Array(items))]
observable.onNext(section)
default: break
}
}
return Disposables.create()
}
}
}
Don't recall if this is breaking MVVM off the top of my head, but would Variable not be what you're looking for?
Variable<[TableData]> data = new Variable<[TableData]>([])
func applyFilter(filter: Predicate){
data.value = items.filter(predicate) //Any change to to the value will cause the table to reload
}
and somewhere in the viewController
viewModel.data.rx.asDriver().drive
(tableView.rx.items(cellIdentifier: "ItemCell", cellType: ItemTableViewCell.self))
{ row, data, cell in
//initialize cells with data
}

Fetching Core Data object in the background and tableView.reloadData()

I want to load Core Data objects in the background, and when it´s finished, should reload a TableView. The code below gives an empty tableView:
class ViewController: UIViewController {
var fetchRequest: NSFetchRequest<Venue>!
var venue:[Venue] = []
var coreDataStack: CoreDataStack!
override func viewDidLoad() {
super.viewDidLoad()
fetchRequest = Venue.fetchRequest()
coreDataStack.storeContainer.performBackgroundTask {(context) in
do {
self.venue = try context.fetch(self.fetchRequesrt)
} catch {
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
But if we write code in viewDidLoad() without chunk above, all works fine and tableView is populated with data:
venue = try! coreDataStack.managedContext.fetch(fetchRequesrt)
tableView.reloadData()
What is wrong with the code above?
For advices I would be very grateful!
Edit. That's the right solution:
class ViewController: UIViewController {
var fetchRequest: NSFetchRequest<Venue>!
var venue:[Venue] = []
var coreDataStack: CoreDataStack!
override func viewDidLoad() {
super.viewDidLoad()
fetchRequest = Venue.fetchRequest()
coreDataStack.storeContainer.performBackgroundTask {(context) in
do {
let venue = try context.fetch(self.fetchRequesrt)
venue.map {$0.objectID}.forEach{self.venue.append(self.coreDataStack.managedContext.object(with: $0) as! Venue)}
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
...
}
}
}
}
One more solution to use NSAsynchronousFetchRequest class. The example above would look like this:
class ViewController: UIViewController {
var fetchRequesrt: NSFetchRequest<Venue>!
var venue:[Venue] = []
var asyncFetchRequest: NSAsynchronousFetchRequest<Venue>!
var coreDataStack: CoreDataStack!
fetchRequest = Venue.fetchRequest()
override func viewDidLoad() {
super.viewDidLoad()
asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest:fetchRequest, completionBlock: { [unowned self]
(result: NSAsynchronousFetchResult) in
guard let venue = result.finalResult else { return }
self.venue = venue
self.tableView.reloadData()
})
do {
try coreDataStack.managedContext.execute(asyncFetchRequest)
} catch {
...
}
}
}
You should use NSFetchedResultController whenever interacting with Core Data and TableView itself. It is much stable and optimized than fetching data on your own.
I am very confused about your code, I have never seen a solution like that. Maybe you should make function with closure and on completion call reloadData, or put the whole block into beginUpdates like this:
self.tableView.beginUpdates()
coreDataStack.storeContainer.performBackgroundTask {(context) in
do {
self.venue = try context.fetch(self.fetchRequesrt)
} catch {/*error goes here*/}
}
self.tableView.endUpdates()
Anyway, I strongly recommend using NSFetchedResultsController because it is much easier for performing backgroundTasks.

Resources