How to Mutate State of UITableViewCell using ReactorKit? - rx-swift

I am using ReactorKit
I have a cell HCheckBoxTableViewCell which has a UILabel for showing title and a UIImageView to show checked or unchecked state.
class HCheckBoxTableViewCell: TableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var checkImageView: UIImageView!
func bind(reactor: MedicineCellReactor) {
reactor.state.map({ $0.message })
.bind(to: titleLabel.rx.text)
.disposed(by: disposeBag)
reactor.state.map({ $0.isSelected })
.subscribe(onNext: { [weak self] isSelected in
self?.checkImageView.image = isSelected == true ? Images.checked : Images.unchecked
}).disposed(by: disposeBag)
}
}
I have MedicineCellReactor which stores the state of the above cell.
final class MedicineCellReactor: Reactor {
typealias Action = NoAction
struct State {
var id: String
var message: String?
var isSelected: Bool
}
let initialState: State
init(id: String, title: String, isSelected: Bool) {
self.initialState = State(id: id, message: title, isSelected: isSelected)
}
}
I have MedicalConditionSelectionReactor which maintains the state of MedicalListViewController who populates the UITableView with the above cell.
When any row is tapped Action.toggleCondition is called with the respected row index. So I just want to toggle the value of isSelected property & then change the image of the cell to checked or unchecked based on the property value. Since the currentState and initialState of MedicineCellReactor are both immutable. How do I achieve this? I don't want to reload the whole tableView.
class MedicalConditionSelectionReactor: Reactor {
// represent user actions
enum Action {
case loadConditions
case addNewCondition
case removeCondition
case toggleCondition(index: Int)
case save
}
// represent state changes
enum Mutation {
case loadedMedicalConditions([MedicalConditionSelectionSectionItem])
case reloadList
}
// represents the current view state
struct State {
var sections: [MedicalConditionSelectionSection] = [MedicalConditionSelectionSection(items: [])]
}
let initialState: State = State()
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .loadConditions:
let medicalConditions = OptionsModelDBProvider<RealmMedicalConditionModel>().fetch()
let conditions = medicalConditions.toArray().map { item -> MedicalConditionSelectionSectionItem in
// let isSelected = arrayOfMedicalConditionID.contains("\(item.ID)")
let medicalCell = MedicineCellReactor(id: "\(item.ID)", title: item.name, isSelected: false)
return MedicalConditionSelectionSectionItem.medicalCondition(medicalCell)
}
return Observable.of(Mutation.loadedMedicalConditions(conditions))
case let .toggleCondition(indexOfCell):
let state = currentState.sections[0].items[indexOfCell]
switch state {
case let .medicalCondition(reactor):
// TODO:- How to toggle the isSelected bool property of MedicineCellReactor.State. Since this is immutable.
// reactor.currentState.isSelected = false
break
default: break
}
return Observable.of(Mutation.reloadList)
default:
return Observable.of(Mutation.reloadList)
}
}
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case let .loadedMedicalConditions(conditions):
state.sections[0].items = conditions
default:
break
}
return state
}
}

Related

SwiftUI Button compiler bug or mine?

I have the following code:
import SwiftUI
enum OptionButtonRole {
case normal
case destructive
case cancel
}
struct OptionButton {
var title: String
var role: OptionButtonRole
let id = UUID()
var action: () -> Void
}
struct OptionSheet: ViewModifier {
#State var isPresented = true
var title: String
var buttons: [OptionButton]
init(title: String, buttons: OptionButton...) {
self.title = title
self.buttons = buttons
}
func body(content: Content) -> some View {
content
.confirmationDialog(title,
isPresented: $isPresented,
titleVisibility: .visible) {
ForEach(buttons, id: \.title) { button in
let role: ButtonRole? = button.role == .normal ? nil : button.role == .destructive ? .destructive : .cancel
Button(button.title, role: role, action: button.action)
}
}
}
}
It builds and my app shows the option sheet with the specified buttons.
However, if I use an alternative Button.init, i.e. if I replace the body with the following code:
func body(content: Content) -> some View {
content
.confirmationDialog(title,
isPresented: $isPresented,
titleVisibility: .visible) {
ForEach(buttons, id: \.title) { button in
let role: ButtonRole? = button.role == .normal ? nil : button.role == .destructive ? .destructive : .cancel
Button(role: role, action: button.action) {
Text(button.title)
}
}
}
}
Then, Xcode hangs on build with the following activity:
Is there an error in my code or is this a compiler bug (Xcode Version 14.1 (14B47b))?
While your code is technically correct, the ability of view logic to evaluate variable values can get quite compiler-intensive, especially when you have multiple chained ternaries and logic inside a ForEach (which seems to make a bigger difference than one would probably think).
I'd be tempted to move the conditional logic outside the loop altogether, so that you're calling a method rather than needing to evaluate and store a local variable. You could make this a private func in your view, or as an extension to your custom enum. For example:
extension OptionButtonRole {
var buttonRole: ButtonRole? {
switch self {
case .destructive: return .destructive
case .cancel: return .cancel
default: return nil
}
}
}
// in your view
ForEach(buttons, id: \.title) { button in
Button(role: button.role.buttonRole, action: button.action) {
Text(button.title)
}
}

I want to toggle the state of another cell with a button tap in UICollectionViewCell

The data and number of categories can be changed, so we implemented it as a collectionView.
Initially, I want only index 0 to be given the selected background color.
Also, when another button is selected, I want to give the selected background color only to the selected button.
What kind of code should I add? Please give me advice
(For reference, it consists of a collectionView within the tableViewCell.)
class CategoryTableViewCell: UITableViewCell {
let categoryList = ["All", "Question", "Community"]
override func awakeFromNib() {
super.awakeFromNib()
set(categoryList)
}
func set(_ dataList: [String]) {
Observable.of(dataList).bind(to: collectionView.rx.items(cellIdentifier: "CategoryCollectionViewCell", cellType: CategoryCollectionViewCell.self)) { _, data, cell in
cell.titleLabel.text = data
}
.disposed(by: disposeBag)
collectionView.rx.itemSelected
.subscribe(onNext: { [weak self] indexPath in
let cell = self?.collectionView.cellForItem(at: indexPath) as? CategoryCollectionViewCell
cell?.containerButton.isSelected = true
})
.disposed(by: disposeBag)
collectionView.rx.setDelegate(self)
.disposed(by: disposeBag)
}
}
class CategoryCollectionViewCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
if containerButton.isSelected {
titleLabel.textColor = .white
containerButton.backgroundColor = .black
} else {
titleLabel.textColor = .black
containerButton.backgroundColor = .white
}
}
}
In order to do this, you have to bind all three buttons to the same Observable source and that source has to listen to all three buttons. Each button will have its own logic for determining what it should look like.
I prefer to set this up in a stack view rather than a collection view because the collection view adds a bunch of unneeded functionality and complexity. However, if forced to use it for some reason, I would do something like this:
class CategoryTableViewCell: UITableViewCell {
enum Category: String, CaseIterable {
case all = "All"
case question = "Question"
case community = "Community"
}
private var collectionView: UICollectionView!
private(set) var disposeBag = DisposeBag()
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
// call this from inside the tableView.rx.items closure.
func set() {
let selected = PublishSubject<Category>()
Observable.just(Category.allCases)
.bind(to: collectionView.rx.items(
cellIdentifier: "CategoryCollectionViewCell",
cellType: CategoryCollectionViewCell.self
)) { _, category, cell in
cell.configure(category: category, selected: selected.asObservable())
.bind(onNext: selected.onNext)
.disposed(by: cell.disposeBag)
}
.disposed(by: disposeBag)
collectionView.rx.setDelegate(self)
.disposed(by: disposeBag)
}
}
class CategoryCollectionViewCell: UICollectionViewCell {
private var titleLabel: UILabel!
private var containerButton: UIButton!
private(set) var disposeBag = DisposeBag()
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
func configure(category: CategoryTableViewCell.Category,
selected: Observable<CategoryTableViewCell.Category>) -> Observable<CategoryTableViewCell.Category> {
titleLabel.text = category.rawValue
let isSelected = selected
.startWith(.all)
.map { $0 == category }
disposeBag.insert(
isSelected
.bind(to: containerButton.rx.isSelected),
isSelected.map { $0 ? UIColor.white : .black }
.bind(to: titleLabel.rx.textColor),
isSelected.map { $0 ? UIColor.black : .white }
.bind(to: containerButton.rx.backgroundColor)
)
return containerButton.rx.tap
.map { category }
.asObservable()
}
}

SwiftUI - Dynamic NSPredicate If Statement

How can I create a predicate so that when the user selects "Full Body" it returns the entire list with no predicate? Right now, it is returning "part" which corresponds to the muscle groups I have set (Abs, Legs, Push, Pull). I want to return all of the options when "Full Body" is selected. How could I write an If statement so that the predicate is not used?
import SwiftUI
var parts = ["Abs", "Legs", "Push", "Pull", "Full Body"]
struct ExerciseList: View {
#State private var selectedPart = " "
var body: some View {
NavigationView {
VStack (alignment: .leading) {
NavigationLink(destination: AddExerciseView()){
Text("Add Exercise")
.fontWeight(.bold)
}
Picker("Body Part", selection: $selectedPart) {
ForEach(parts, id:\.self) { part in
Text(part)
}
}.pickerStyle(.segmented)
ListView(part:selectedPart)
}
}
}
}
import SwiftUI
struct ListView: View {
var part: String
#FetchRequest var exercises: FetchedResults<Exercise>
init(part: String) {
self.part = part
self._exercises = FetchRequest(
entity: Exercise.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
)
}
var body: some View {
List(exercises) { e in
Text(e.exercisename)
}
}
}
It's not a good idea to init objects inside View structs because the heap allocation slows things down. You could either have all the predicates created before hand or create one when the picker value changes, e.g. something like this:
// all the Picker samples in the docs tend to use enums.
enum Part: String, Identifiable, CaseIterable {
case abs
case legs
case push
case pull
case fullBody
var id: Self { self }
// Apple sometimes does it like this
// var localizedName: LocalizedStringKey {
// switch self {
// case .abs: return "Abs"
// case .legs: return "Legs"
// case .push: return "Push"
// case .pull: return "Pull"
// case .fullBody: return "Full Body"
// }
// }
}
struct ExerciseListConfig {
var selectedPart: Part = .fullBody {
didSet {
if selectedPart == .fullBody {
predicate = nil
}
else {
// note this will use the lower case string
predicate = NSPredicate(format: "musclegroup == %#", selectedPart.rawValue)
}
}
}
var predicate: NSPredicate?
}
struct ExerciseList: View {
#State private var config = ExerciseListConfig()
var body: some View {
NavigationView {
VStack (alignment: .leading) {
Picker("Body Part", selection: $config.selectedPart) {
//ForEach(Part.allCases) // Apple sometimes does this but means you can't easily change the display order.
Text("Abs").tag(Part.abs)
Text("Legs").tag(Part.legs)
Text("Push").tag(Part.push)
Text("Pull").tag(Part.pull)
Text("Full Body").tag(Part.fullBody)
}.pickerStyle(.segmented)
ExerciseListView(predicate:config.predicate)
}
}
}
}
struct ExerciseListView: View {
// var part: String
let predicate: NSPredicate?
// #FetchRequest var exercises: FetchedResults<Exercise>
init(predicate: NSPredicate?) {
self.predicate = predicate
// self._exercises = FetchRequest(
// entity: Exercise.entity(),
// sortDescriptors: [],
//
// predicate: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
// )
}
var body: some View {
Text(predicate?.description ?? "")
// List(exercises) { e in
// Text(e.exercisename)
// }
}
}
Since you are using Core Data you might want to use an Int enum in the entity for less storage and faster queries.
you could try this simple approach in ListView:
init(part: String) {
self.part = part
self._exercises = FetchRequest(
entity: Exercise.entity(),
sortDescriptors: [],
predicate: (part == "Full Body")
? nil
: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
)
}

How do I debug SwiftUI AttributeGraph cycle warnings?

I'm getting a lot of AttributeGraph cycle warnings in my app that uses SwiftUI. Is there any way to debug what's causing it?
This is what shows up in the console:
=== AttributeGraph: cycle detected through attribute 11640 ===
=== AttributeGraph: cycle detected through attribute 14168 ===
=== AttributeGraph: cycle detected through attribute 14168 ===
=== AttributeGraph: cycle detected through attribute 44568 ===
=== AttributeGraph: cycle detected through attribute 3608 ===
The log is generated by (from private AttributeGraph.framework)
AG::Graph::print_cycle(unsigned int) const ()
so you can set symbolic breakpoint for print_cycle
and, well, how much it could be helpful depends on your scenario, but definitely you'll get error generated stack in Xcode.
For me this issue was caused by me disabling a text field while the user was still editing it.
To fix this, you must first resign the text field as the first responder (thus stopping editing), and then disable the text field.
I explain this more in this Stack Overflow answer.
For me, this issue was caused by trying to focus a TextField right before changing to the tab of a TabView containing the TextField.
It was fixed by simply focusing the TextField after changing the TabView tab.
This seems similar to what #wristbands was experiencing.
For me the issue was resolved by not using UIActivityIndicator... not sure why though. The component below was causing problems.
public struct UIActivityIndicator: UIViewRepresentable {
private let style: UIActivityIndicatorView.Style
/// Default iOS 11 Activity Indicator.
public init(
style: UIActivityIndicatorView.Style = .large
) {
self.style = style
}
public func makeUIView(
context: UIViewRepresentableContext<UIActivityIndicator>
) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
public func updateUIView(
_ uiView: UIActivityIndicatorView,
context: UIViewRepresentableContext<UIActivityIndicator>
) {}
}
#Asperi Here is a minimal example to reproduce AttributeGraph cycle:
import SwiftUI
struct BoomView: View {
var body: some View {
VStack {
Text("Go back to see \"AttributeGraph: cycle detected through attribute\"")
.font(.title)
Spacer()
}
}
}
struct TestView: View {
#State var text: String = ""
#State private var isSearchFieldFocused: Bool = false
var placeholderText = NSLocalizedString("Search", comment: "")
var body: some View {
NavigationView {
VStack {
FocusableTextField(text: $text, isFirstResponder: $isSearchFieldFocused, placeholder: placeholderText)
.foregroundColor(.primary)
.font(.body)
.fixedSize(horizontal: false, vertical: true)
NavigationLink(destination: BoomView()) {
Text("Boom")
}
Spacer()
}
.onAppear {
self.isSearchFieldFocused = true
}
.onDisappear {
isSearchFieldFocused = false
}
}
}
}
FocusableTextField.swift based on https://stackoverflow.com/a/59059359/659389
import SwiftUI
struct FocusableTextField: UIViewRepresentable {
#Binding public var isFirstResponder: Bool
#Binding public var text: String
var placeholder: String = ""
public var configuration = { (view: UITextField) in }
public init(text: Binding<String>, isFirstResponder: Binding<Bool>, placeholder: String = "", configuration: #escaping (UITextField) -> () = { _ in }) {
self.configuration = configuration
self._text = text
self._isFirstResponder = isFirstResponder
self.placeholder = placeholder
}
public func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.placeholder = placeholder
view.autocapitalizationType = .none
view.autocorrectionType = .no
view.addTarget(context.coordinator, action: #selector(Coordinator.textViewDidChange), for: .editingChanged)
view.delegate = context.coordinator
return view
}
public func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
switch isFirstResponder {
case true: uiView.becomeFirstResponder()
case false: uiView.resignFirstResponder()
}
}
public func makeCoordinator() -> Coordinator {
Coordinator($text, isFirstResponder: $isFirstResponder)
}
public class Coordinator: NSObject, UITextFieldDelegate {
var text: Binding<String>
var isFirstResponder: Binding<Bool>
init(_ text: Binding<String>, isFirstResponder: Binding<Bool>) {
self.text = text
self.isFirstResponder = isFirstResponder
}
#objc public func textViewDidChange(_ textField: UITextField) {
self.text.wrappedValue = textField.text ?? ""
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = true
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = false
}
}
}
For me the issue was that I was dynamically loading the AppIcon asset from the main bundle. See this Stack Overflow answer for in-depth details.
I was using enum cases as tag values in a TabView on MacOS. The last case (of four) triggered three attributeGraph cycle warnings. (The others were fine).
I am now using an Int variable (InspectorType.book.typeInt instead of InspectorType.book) as my selection variable and the cycle warnings have vanished.
(I can demonstrate this by commenting out the offending line respectively by changing the type of my selection; I cannot repeat it in another app, so there's obviously something else involved; I just haven't been able to identify the other culprit yet.)

OSX SwiftUI integrating NSComboBox Not refreshing current sélected

SwiftUI Picker is looking very bad on OSX especially when dealing with long item lists
Swiftui Picker on OSX with a long item list
And since did find any solution to limit the number of item displayed in by Picker on Osx , I decided to interface NSComboBox to SwiftUI
Everythings looks fine until the selection index is modified programmatically using the #Published index of the Observable Comboselection class instance (see code below) :
the updateNSView function of the NSViewRepresentable instance is called correctly then (print message visible on the log)
combo.selectItem(at: selected.index)
combo.selectItem(at: selected.index)
combo.objectValue = combo.objectValueOfSelectedItem
print("populating index change \(selected.index) to Combo : (String(describing: combo.objectValue))")
is executed correctly and the printed log shows up the correct information
But the NSComboBox textfield is not refreshed with the accurate object value
Does somebody here have an explanation ?? ; is there something wrong in code ??
here the all code :
import SwiftUI
class ComboSelection : ObservableObject {
#Published var index : Int
init( index: Int ) {
self.index = index
}
func newSelection( newIndex : Int ) {
index = newIndex
}
}
//
// SwiftUI NSComboBox component interface
//
struct SwiftUIComboBox : NSViewRepresentable {
typealias NSViewType = NSComboBox
var content : [String]
var nbLines : Int
var selected : ComboSelection
final class Coordinator : NSObject ,
NSComboBoxDelegate {
var control : SwiftUIComboBox
var selected : ComboSelection
init( _ control: SwiftUIComboBox , selected : ComboSelection ) {
self.selected = selected
self.control = control
}
func comboBoxSelectionDidChange(_ notification: Notification) {
print ("entering coordinator selection did change")
let combo = notification.object as! NSComboBox
selected.newSelection( newIndex: combo.indexOfSelectedItem )
}
}
func makeCoordinator() -> SwiftUIComboBox.Coordinator {
return Coordinator(self, selected:selected)
}
func makeNSView(context: NSViewRepresentableContext<SwiftUIComboBox>) -> NSComboBox {
let returned = NSComboBox()
returned.numberOfVisibleItems = nbLines
returned.hasVerticalScroller = true
returned.usesDataSource = false
returned.delegate = context.coordinator // Important : not forget to define delegate
for key in content{
returned.addItem(withObjectValue: key)
}
return returned
}
func updateNSView(_ combo: NSComboBox, context: NSViewRepresentableContext<SwiftUIComboBox>) {
combo.selectItem(at: selected.index)
combo.objectValue = combo.objectValueOfSelectedItem
print("populating index change \(selected.index) to Combo : \(String(describing: combo.objectValue))")
}
}
Please see updated & simplified your code with added some working demo. The main reason of issue was absent update of SwiftUI view hierarchy, so to have such update I've used Binding, which transfers changes to UIViewRepresentable and back. Hope this approach will be helpful.
Here is demo
Below is one-module full demo code (just set
window.contentView = NSHostingView(rootView:TestComboBox()) in app delegate
struct SwiftUIComboBox : NSViewRepresentable {
typealias NSViewType = NSComboBox
var content : [String]
var nbLines : Int
#Binding var selected : Int
final class Coordinator : NSObject, NSComboBoxDelegate {
var selected : Binding<Int>
init(selected : Binding<Int>) {
self.selected = selected
}
func comboBoxSelectionDidChange(_ notification: Notification) {
print ("entering coordinator selection did change")
if let combo = notification.object as? NSComboBox, selected.wrappedValue != combo.indexOfSelectedItem {
selected.wrappedValue = combo.indexOfSelectedItem
}
}
}
func makeCoordinator() -> SwiftUIComboBox.Coordinator {
return Coordinator(selected: $selected)
}
func makeNSView(context: NSViewRepresentableContext<SwiftUIComboBox>) -> NSComboBox {
let returned = NSComboBox()
returned.numberOfVisibleItems = nbLines
returned.hasVerticalScroller = true
returned.usesDataSource = false
returned.delegate = context.coordinator // Important : not forget to define delegate
for key in content {
returned.addItem(withObjectValue: key)
}
return returned
}
func updateNSView(_ combo: NSComboBox, context: NSViewRepresentableContext<SwiftUIComboBox>) {
if selected != combo.indexOfSelectedItem {
DispatchQueue.main.async {
combo.selectItem(at: self.selected)
print("populating index change \(self.selected) to Combo : \(String(describing: combo.objectValue))")
}
}
}
}
struct TestComboBox: View {
#State var selection = 0
let content = ["Alpha", "Beta", "Gamma", "Delta", "Epselon", "Zetta", "Eta"]
var body: some View {
VStack {
Button(action: {
if self.selection + 1 < self.content.count {
self.selection += 1
} else {
self.selection = 0
}
}) {
Text("Select next")
}
Divider()
SwiftUIComboBox(content: content, nbLines: 3, selected: $selection)
Divider()
Text("Current selection: \(selection), value: \(content[selection])")
}
.frame(width: 300, height: 300)
}
}

Resources