OTC View autolayout in SwiftUI - ios

I'm new to swiftui and swift basically, i made One-time-code screen, and here i have a problem. When i run project on my old phone (iphone 6) when keyboard appears, my textfield size changes (it gets very thin vertically). So i was wondering is there any way to add autolayout for older devices?
Here is my code
struct OneTimeCodeBoxes: View {
#Binding var codeDict: [Int: String]
#State var firstResponderIndex = 0
var onCommit: (()->Void)?
var body: some View {
HStack {
ForEach(0..<codeDict.count) { i in
let isEmpty = codeDict[i]?.isEmpty == true
OneTimeCodeInput(
index: i,
codeDict: $codeDict,
firstResponderIndex: $firstResponderIndex,
onCommit: onCommit
)
.aspectRatio(1, contentMode: .fit)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: isEmpty ? 1 : 2)
.foregroundColor(isEmpty ? .secondary : .green))
}
}
}
}
struct OneTimeCodeBoxes_Previews: PreviewProvider {
static var previews: some View {
OneTimeCodeBoxes(codeDict: .constant([0: "", 1: "", 2: "", 3: ""]))
.padding()
.previewLayout(.sizeThatFits)
}
}
Here is my OneTimeCodeInput part of code
struct OneTimeCodeInput: UIViewRepresentable {
typealias UIViewType = UITextField
let index: Int
#Binding var codeDict: [Int: String]
#Binding var firstResponderIndex: Int
var onCommit: (()->Void)?
class Coordinator: NSObject, UITextFieldDelegate {
let index: Int
#Binding var codeDict: [Int: String]
#Binding var firstResponderIndex: Int
private lazy var codeDigits: Int = codeDict.count
init(index: Int, codeDict: Binding<[Int: String]>, firstResponderIndex: Binding<Int>) {
self.index = index
self._codeDict = codeDict
self._firstResponderIndex = firstResponderIndex
}
#objc func textFieldEditingChanged(_ textField: UITextField) {
print("textField.text!", textField.text!)
guard textField.text!.count == codeDigits else { return }
codeDict = textField.text!.enumerated().reduce(into: [Int: String](), { dict, tuple in
let (index, char) = tuple
dict.updateValue(String(char), forKey: index)
})
firstResponderIndex = codeDigits - 1
}
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool
{
print("replacementString", string)
if string.isBackspace {
codeDict.updateValue("", forKey: index)
firstResponderIndex = max(0, textField.text == "" ? index - 1 : index)
return false
}
for i in index..<min(codeDict.count, index + string.count) {
codeDict.updateValue(string.stringAt(index: i - index), forKey: i)
// print(codeDict)
firstResponderIndex = min(codeDict.count - 1, index + string.count)
}
return true
}
}
func makeCoordinator() -> Coordinator {
.init(index: index, codeDict: $codeDict, firstResponderIndex: $firstResponderIndex)
}
func makeUIView(context: Context) -> UITextField {
let tf = BackspaceTextField(onDelete: {
firstResponderIndex = max(0, index - 1)
})
tf.addTarget(context.coordinator, action: #selector(Coordinator.textFieldEditingChanged), for: .editingChanged)
tf.delegate = context.coordinator
tf.keyboardType = .numberPad
tf.textContentType = .oneTimeCode
tf.font = .preferredFont(forTextStyle: .largeTitle)
tf.textAlignment = .center
return tf
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = codeDict[index]
if index == firstResponderIndex {
uiView.becomeFirstResponder()
}
if index == firstResponderIndex,
codeDict.values.filter({ !$0.isEmpty }).count == codeDict.count
{
onCommit?()
}
}
}
extension OneTimeCodeInput {
class BackspaceTextField: UITextField {
var onDelete: (()->Void)?
init(onDelete: (()->Void)?) {
self.onDelete = onDelete
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func deleteBackward() {
super.deleteBackward()
onDelete?()
}
}
}
This is how it shows on Ipnone13 simulator, and it's correct. I'm trying to do same on older devices

Related

Swift - Autofill does not populate UITextField

When a user uses Autofill (not password generation - rather, when they tap a login and use iCloud Keychain to log in - see User taps on Autofill item, if FaceID isn't completed immediately, the UITextField does not populate with the user's username and password. It only fills some of the time.
Since my app is written in SwiftUI, I use a custom TextField.
struct AutoFocusTextField<V: Hashable>: UIViewRepresentable {
#Binding var text: String
let placeholder: String
var id: V
#Binding var firstResponder: V?
var onCommit: () -> Void
var formatText: () -> Void
var inputAccessoryView: UIToolbar? = nil
var secureEntry: Binding<Bool>? = nil
var returnKeyType: UIReturnKeyType
var autoCorrection: UITextAutocorrectionType
var capitalize: UITextAutocapitalizationType
var keyboardType: UIKeyboardType
var contentType: UITextContentType?
var datePicker: UIDatePicker?
init(text: Binding<String>, placeholder: String, id: V, firstResponder: Binding<V?>, onCommit: #escaping (() -> Void) = {}, formatText: #escaping (() -> Void) = {},
secureEntry: Binding<Bool>? = nil, returnKeyType: UIReturnKeyType = .default, autoCorrection: UITextAutocorrectionType = .no, capitalize: UITextAutocapitalizationType = .none, keyboardType: UIKeyboardType = .default, contentType: UITextContentType? = .none, datePicker: UIDatePicker? = nil) {
self.id = id
_text = text
_firstResponder = firstResponder
self.placeholder = placeholder
self.onCommit = onCommit
self.formatText = formatText
self.secureEntry = secureEntry
self.returnKeyType = returnKeyType
self.autoCorrection = autoCorrection
self.capitalize = capitalize
self.keyboardType = keyboardType
self.contentType = contentType
self.datePicker = datePicker
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text,
format: format,
onStartEditing: startedEditing,
onEndEditing: finishedEditing,
onReturnTap: returnTap)
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [NSAttributedString.Key.foregroundColor: UIColor(named: "primary-gray"),
NSAttributedString.Key.font: UIFont(name: "MDPrimer-Regular", size: 16)
])
textField.textColor = UIColor(named: "primary-black")
textField.font = UIFont(name: "MDPrimer-Regular", size: 16)
textField.tintColor = UIColor(named: "primary-black")
if let datePicker = datePicker {
textField.inputView = datePicker
datePicker.datePickerMode = .date
datePicker.preferredDatePickerStyle = .wheels
datePicker.addTarget(context.coordinator, action: #selector(Coordinator.dateChanged(_:)), for: .valueChanged)
}
else {
textField.keyboardType = keyboardType
}
textField.tintColor = UIColor(named: "primary-lilac")!
textField.isSecureTextEntry = secureEntry?.wrappedValue ?? false
textField.autocorrectionType = autoCorrection
textField.returnKeyType = returnKeyType
textField.autocapitalizationType = capitalize
textField.textContentType = contentType ?? .none
textField.contentVerticalAlignment = .center
textField.contentHorizontalAlignment = .center
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
if id == firstResponder, uiView.isFirstResponder == false {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func startedEditing() {
if id != firstResponder {
firstResponder = id
}
}
func finishedEditing() {
guard id == firstResponder else { return }
firstResponder = nil
}
func format() {
self.formatText()
}
func returnTap() {
self.onCommit()
}
}
protocol TextFieldReturnKeyProtocol {
func returnTapped()
}
class Coordinator: NSObject, UITextFieldDelegate, TextFieldReturnKeyProtocol {
#Binding private var text: String
private let format: (() -> Void)
private let onStartEditing: (() -> Void)
private let onEndEditing: (() -> Void)
private let onReturnTap: (() -> Void)
var previousText: String?
var nextText: String?
init(text: Binding<String>, format: #escaping (() -> Void), onStartEditing: #escaping (() -> Void), onEndEditing: #escaping (() -> Void), onReturnTap: #escaping (() -> Void)) {
_text = text
self.onStartEditing = onStartEditing
self.onEndEditing = onEndEditing
self.onReturnTap = onReturnTap
self.format = format
super.init()
}
#objc func textFieldDidChange(_ textField: UITextField) {
DispatchQueue.main.async { [weak self] in
self?.text = textField.text ?? ""
self?.format()
}
}
#objc func dateChanged(_ sender: UIDatePicker) {
let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy"
self.text = df.string(from: sender.date)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
onStartEditing()
}
func textFieldDidEndEditing(_ textField: UITextField) {
onEndEditing()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
returnTapped()
return true
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard var safeText = textField.text else { return true }
DispatchQueue.main.async {
var str = string
if string.count > 1 && textField.textContentType == .telephoneNumber && textField.keyboardType == .numberPad {
if string.count > 11 && string.starts(with: "+1") {
str.removeFirst(2)
textField.text = str
} else if string.count > 10 && string.starts(with: "1") {
str.removeFirst()
textField.text = str
}
}
}
return true
}
#objc func returnTapped() {
onReturnTap()
}
}
This is wrapped in a larger SwiftUI view.
struct AutoFocusTextFieldWrapper: View {
#Binding var text: String
let placeholder: String
let id: ResponderFields
#Binding var firstResponder: ResponderFields?
let onCommit: () -> Void
var hasError: Bool = false
var secureEntry: Binding<Bool>? = nil
var returnKeyType: UIReturnKeyType = .default
var autoCorrection: UITextAutocorrectionType = .no
var capitalize: UITextAutocapitalizationType = .none
var keyboardType: UIKeyboardType = .default
var contentType: UITextContentType? = .none
var datePicker: UIDatePicker? = nil
var formatText: () -> Void
var enableBorder: Bool = true
var highlight: Bool = false
var body: some View {
AutoFocusTextField(text: $text, placeholder: placeholder, id: id, firstResponder: $firstResponder, onCommit: onCommit, formatText: formatText, secureEntry: secureEntry, returnKeyType: returnKeyType, autoCorrection: autoCorrection, capitalize: capitalize, keyboardType: keyboardType, contentType: contentType, datePicker: datePicker)
.padding(EdgeInsets(top: 0, leading: 24, bottom: 0, trailing: 24))
.frame(height: 48)
.frame(maxWidth: .infinity)
}
}
And then, here is the Login View.
struct LoginView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#EnvironmentObject var loginRegistrationViewModel: LoginRegistrationViewModel
#EnvironmentObject var viewModel: LoginViewModel
var dismiss: () -> Void
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 0) {
NavigationTitle(title: viewModel.navigationTitle, description: viewModel.navigationDescription, backButtonAction: dismiss)
AutoFocusTextFieldWrapper(
text: $viewModel.email,
placeholder: "Email",
id: .loginEmail,
firstResponder: $viewModel.firstResponder,
onCommit: {
viewModel.firstResponder = .loginPassword
},
hasError: viewModel.hasError,
returnKeyType: .next,
keyboardType: .emailAddress,
contentType: .username,
formatText: {},
highlight: viewModel.successLoggingIn
)
.padding(EdgeInsets(top: 48, leading: 0, bottom: 0, trailing: 0))
AutoFocusTextFieldWrapper(
text: $viewModel.password,
placeholder: "Password",
id: .loginPassword,
firstResponder: $viewModel.firstResponder,
onCommit: {
UIApplication.shared.endEditing()
},
hasError: viewModel.hasError,
secureEntry: .constant(true),
returnKeyType: .next,
contentType: .password,
formatText: {},
highlight: viewModel.successLoggingIn
)
.padding(EdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 0))
Spacer()
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
For some reason, the autofill does not always work. I've noticed that if I comment out the code in startedEditing and finishedEditing, it seems to work more consistently. Any idea on why it's not working all the time?

How do I get the position of the cursor of TextEditor in SwiftUI?

So in my text editor, I'd like to know the position of the cursor geometrically. I'm also planning to append some text after that position.
So how do I do this?
Okay... So I figured out a way to do this.
First, I created a struct to store the cursor positions
import foundation
struct CursorPosition {
start: Int
end: Int
}
Then I initialize it to be static
class Global {
public static var cursorPosition = CursorPosition(start: 0, end: 0)
}
Then finally, I created a custom view to match the SwiftUI TextEditor and listen for selection change and update the CursorPosition
import UIKit
import SwiftUI
fileprivate struct UITextViewWrapper: UIViewRepresentable {
typealias UIViewType = UITextView
#Binding var text: String
var onDone: (() -> Void)?
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
let textField = UITextView()
textField.delegate = context.coordinator
textField.isEditable = true
textField.font = UIFont.preferredFont(forTextStyle: .body)
textField.isSelectable = true
textField.isUserInteractionEnabled = true
textField.isScrollEnabled = true
textField.backgroundColor = UIColor.clear
if nil != onDone {
textField.returnKeyType = .done
}
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, onDone: onDone)
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
var onDone: (() -> Void)?
init(text: Binding<String>, onDone: (() -> Void)? = nil) {
self.text = text
self.onDone = onDone
}
func textViewDidChange(_ uiView: UITextView) {
text.wrappedValue = uiView.text
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let onDone = self.onDone, text == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
func textViewDidChangeSelection(_ textView: UITextView) {
if let range = textView.selectedTextRange {
Global.cursorPosition.start = textView.offset(from: textView.beginningOfDocument, to: range.start)
Global.cursorPosition.end = textView.offset(from: textView.beginningOfDocument, to: range.end)
}
}
}
}
struct EditText: View {
private var placeholder: String
private var onCommit: (() -> Void)?
#Binding private var text: String
private var internalText: Binding<String> {
Binding<String>(get: { self.text } ) {
self.text = $0
self.showingPlaceholder = $0.isEmpty
}
}
#State private var showingPlaceholder = false
init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
self._text = text
self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
}
var body: some View {
UITextViewWrapper(text: self.internalText, onDone: onCommit)
.background(placeholderView, alignment: .topLeading)
}
var placeholderView: some View {
Group {
if showingPlaceholder {
Text(placeholder).foregroundColor(.gray)
.padding(.leading, 4)
.padding(.top, 8)
}
}
}
}
And using it:
EditText("", text: $text)
.onChange(of: text){ _ in
let cursorStart = Global.cursorPosition.start
}

swiftUI ScrollView UIRefreshControl overlay on navigation bar

Until iOS 15.2 , the SwiftUI ScrollView doesn't support refreshable{}, so I using UIRefreshControl to solve this issues. Everything went smoothly and success was just one step away ;-(
full code ContentView.swift :
import SwiftUI
extension UIView {
func viewsInHierarchy<ViewType: UIView>() -> [ViewType]? {
var views: [ViewType] = []
viewsInHierarchy(views: &views)
return views.count > 0 ? views : nil
}
fileprivate func viewsInHierarchy<ViewType: UIView>(views: inout [ViewType]) {
subviews.forEach { eachSubView in
if let matchingView = eachSubView as? ViewType {
views.append(matchingView)
}
eachSubView.viewsInHierarchy(views: &views)
}
}
func searchViewAnchestorsFor<ViewType: UIView>(
_ onViewFound: (ViewType) -> Void
) {
if let matchingView = self.superview as? ViewType {
onViewFound(matchingView)
} else {
superview?.searchViewAnchestorsFor(onViewFound)
}
}
func searchViewAnchestorsFor<ViewType: UIView>() -> ViewType? {
if let matchingView = self.superview as? ViewType {
return matchingView
} else {
return superview?.searchViewAnchestorsFor()
}
}
func printViewHierarchyInformation(_ level: Int = 0) {
printViewInformation(level)
self.subviews.forEach { $0.printViewHierarchyInformation(level + 1) }
}
func printViewInformation(_ level: Int) {
let leadingWhitespace = String(repeating: " ", count: level)
let className = "\(Self.self)"
let superclassName = "\(self.superclass!)"
let frame = "\(self.frame)"
let id = (self.accessibilityIdentifier == nil) ? "" : " `\(self.accessibilityIdentifier!)`"
print("\(leadingWhitespace)\(className): \(superclassName)\(id) \(frame)")
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) { }
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
//parent.navigationController?.navigationBar.prefersLargeTitles = true
//parent.navigationItem.largeTitleDisplayMode = .never
self.onResolve(parent)
// print("didMove(toParent: \(parent)")
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
HStack {
Spacer()
Text("Content")
Spacer()
}
Spacer()
}
.background(Color.red)
}
.navigationTitle("title")
.navigationBarTitleDisplayMode(.inline)
.overlay(
ViewControllerResolver { parent in
var scrollView: UIScrollView?
if let scrollViewsInHierarchy: [UIScrollView] = parent.view.viewsInHierarchy() {
if scrollViewsInHierarchy.count == 1, let firstScrollViewInHierarchy = scrollViewsInHierarchy.first {
scrollView = firstScrollViewInHierarchy
}
}
if let scrollView = scrollView {
guard scrollView.refreshControl == nil else { return }
let refreshControl = UIRefreshControl()
refreshControl.transform = CGAffineTransform(scaleX: 0.75, y: 0.75)
refreshControl.layoutIfNeeded()
let uiAction = UIAction(handler: { uiAction in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
refreshControl.endRefreshing()
}
})
refreshControl.addAction(uiAction, for: .primaryActionTriggered)
scrollView.refreshControl = refreshControl
}
}
.frame(width: 0, height: 0)
)
}
}
}

UIViewRepresentable wont update my ios chart dataset

I am trying to my data derived from an API into a line chart but I can't seem to get it to work. I am storing the data in an observable object so it takes a few seconds to get it so it won't show up on my graph but when I hardcode data it works I am certain that I am getting the data but it simply won't show up. thanks
struct HomeView: View {
#State var tabIndex:Int = 0
#ObservedObject var homeViewModel = HomeViewModel()
init() {
homeViewModel.getTimelineBy("US")
}
var body: some View {
VStack(alignment: .center) {
TimelineChartView(timelineDataSet: self.$homeViewModel.countryTimeline)
}.frame(height: 500.0)
}
}
struct TimelineChartView: UIViewRepresentable {
#Binding var timelineDataSet: [ChartDataEntry]
func updateUIView(_ uiView: LineChartView, context: UIViewRepresentableContext<TimelineChartView>) {
}
var lineChart = LineChartView()
func makeUIView(context: UIViewRepresentableContext<TimelineChartView>) -> LineChartView {
setUpChart()
return lineChart
}
func setUpChart() {
lineChart.noDataText = "No Data Available"
lineChart.rightAxis.enabled = false
lineChart.backgroundColor = .white
let dataSets = [getLineChartDataSet()]
let yAxis = lineChart.leftAxis
yAxis.labelFont = .boldSystemFont(ofSize: 13)
yAxis.setLabelCount(5, force: false)
yAxis.labelTextColor = .black
yAxis.axisLineColor = .black
yAxis.labelPosition = .outsideChart
lineChart.xAxis.labelPosition = .bottom
lineChart.xAxis.labelFont = .boldSystemFont(ofSize: 13)
lineChart.xAxis.labelTextColor = .black
lineChart.xAxis.axisLineColor = .systemBlue
lineChart.animate(xAxisDuration: 2.5)
lineChart.notifyDataSetChanged()
let data = LineChartData(dataSets: dataSets)
data.setValueFont(.systemFont(ofSize: 7, weight: .black))
lineChart.data = data
}
func getChartDataPoints(selectedTimelineData: [ChartDataEntry]) -> [ChartDataEntry] {
var dataPoints: [ChartDataEntry] = []
for eachTimeline in selectedTimelineData {
let entry = ChartDataEntry(x: eachTimeline.x, y: eachTimeline.y)
dataPoints.append(entry)
}
return dataPoints
}
func getLineChartDataSet() -> LineChartDataSet {
let test = getChartDataPoints(selectedTimelineData: timelineDataSet)
let set = LineChartDataSet(entries: test, label: "DataSet")
set.lineWidth = 4
set.drawCirclesEnabled = false
set.mode = .cubicBezier
set.fillAlpha = 0.9
set.drawFilledEnabled = true
set.highlightColor = .systemRed
return set
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
Let me share some of my codes working. When the binding object is changed the chart will show the data changed.
struct HomeView: View {
#State var barData: [String: Double] = [String: Double]()
var body: some View {
NavigationView {
List {
CustomeView([BarChartVM(arg: self.$barData, title: "BarChart")])
}
}
}
}
struct CustomeView<Page:View>: View {
var viewControllers: [UIHostingController<Page>]
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
CustomeViewController(controllers: viewControllers)
}
}
struct CustomeViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[0]], direction: .forward, animated: true)
}
}
struct BarChartVM: UIViewRepresentable {
#Binding var arg: [String: Double]
var title: String = ""
let chart = BarChartView()
func makeUIView(context: UIViewRepresentableContext<BarChartVM>) -> BarChartView {
setUpChart()
return chart
}
func updateUIView(_ uiView: BarChartView, context: UIViewRepresentableContext<BarChartVM>) {
updateChartData()
}
func setUpChart() {
chart.noDataText = "No data available"
let pointArray = arg.sorted(by: <).map { $0.key }
}
func updateChartData() {
var entries = [BarChartDataEntry]()
let valueArray = arg.sorted(by: <).map { $1 }
for i in 0..<valueArray.count {
let entry = BarChartDataEntry(x: Double(i), yValues: [valueArray[i]])
entries.append( entry)
}
let set = BarChartDataSet(entries: entries, label: title)
set.colors = ChartColorTemplates.material()
let data = BarChartData(dataSet: set)
chart.data = data
}
}

Eureka - Row to present multiple controllers

I'd like to create Eureka row to look and behave as Postal Address Row in create/edit Contact Screen in iOS Contacts App. I need to present Label or Country Picker when corresponding button in cell is pressed. Based on Eureka documentation:
every row that displays a new view controller must conform to PresenterRowType protocol
However this protocol is generic. So my understanding is that I can't show more than one child screen per row. Do I get this right? Is it possible to present more than one child screen?
What I have so far follows.
Row Protocols:
protocol PostalAddressFormatterConformance: class {
var streetUseFormatterDuringInput: Bool { get set }
var streetFormatter: Formatter? { get set }
var stateUseFormatterDuringInput: Bool { get set }
var stateFormatter: Formatter? { get set }
var postalCodeUseFormatterDuringInput: Bool { get set }
var postalCodeFormatter: Formatter? { get set }
var cityUseFormatterDuringInput: Bool { get set }
var cityFormatter: Formatter? { get set }
}
protocol LabeledRowConformance {
func onLabelButtonDidPress()
}
protocol CountryRowConformance {
func onCountryButtonDidPress()
}
protocol PostalAddressRowConformance: PostalAddressFormatterConformance, LabeledRowConformance, CountryRowConformance {
var placeholderColor : UIColor? { get set }
var streetPlaceholder : String? { get set }
var statePlaceholder : String? { get set }
var postalCodePlaceholder : String? { get set }
var cityPlaceholder : String? { get set }
}
Postal Address Row Base class:
class _PostalAddressRow<Cell: CellType>: Row<Cell>, PostalAddressRowConformance, CountryRowConformance, LabeledRowConformance, KeyboardReturnHandler where Cell: BaseCell, Cell: PostalAddressCellConformance {
//MARK: - LabeledRowConformance
func onLabelButtonDidPress() {
// TODO: Present Label Picker Screen
}
//MARK: - CountryRowConformance
func onCountryButtonDidPress() {
// TODO: Present Country Picker Screen
}
//MARK: - KeyboardReturnHandler
/// Configuration for the keyboardReturnType of this row
var keyboardReturnType : KeyboardReturnTypeConfiguration?
//MARK: - PostalAddressRowConformance
/// The textColor for the textField's placeholder
var placeholderColor : UIColor?
/// The placeholder for the street textField
var streetPlaceholder : String?
/// The placeholder for the state textField
var statePlaceholder : String?
/// The placeholder for the zip textField
var postalCodePlaceholder : String?
/// The placeholder for the city textField
var cityPlaceholder : String?
/// A formatter to be used to format the user's input for street
var streetFormatter: Formatter?
/// A formatter to be used to format the user's input for state
var stateFormatter: Formatter?
/// A formatter to be used to format the user's input for zip
var postalCodeFormatter: Formatter?
/// A formatter to be used to format the user's input for city
var cityFormatter: Formatter?
/// If the formatter should be used while the user is editing the street.
var streetUseFormatterDuringInput: Bool
/// If the formatter should be used while the user is editing the state.
var stateUseFormatterDuringInput: Bool
/// If the formatter should be used while the user is editing the zip.
var postalCodeUseFormatterDuringInput: Bool
/// If the formatter should be used while the user is editing the city.
var cityUseFormatterDuringInput: Bool
public required init(tag: String?) {
streetUseFormatterDuringInput = false
stateUseFormatterDuringInput = false
postalCodeUseFormatterDuringInput = false
cityUseFormatterDuringInput = false
super.init(tag: tag)
}
}
Postal Address Row Final:
final class PostalAddressRow: _PostalAddressRow<PostalAddressCell>, RowType {
public required init(tag: String? = nil) {
super.init(tag: tag)
// TODO
cellProvider = CellProvider<PostalAddressCell>(nibName: "PostalAddressCell")
}
}
Cell:
public protocol CountryCellConformance {
var countryButton: UIButton? { get }
}
public protocol PostalAddressCellConformance: CountryCellConformance {
var streetTextField: UITextField? { get }
var stateTextField: UITextField? { get }
var postalCodeTextField: UITextField? { get }
var cityTextField: UITextField? { get }
}
class _PostalAddressCell<T: PostalAddressType>: Cell<T>, PostalAddressCellConformance, UITextFieldDelegate, CellType {
#IBOutlet weak var changeLabelButton: UIButton!
//MARK: - CountryCellConformance
#IBOutlet weak var countryButton: UIButton?
//MARK: - PostalAddressCellConformance
#IBOutlet weak var streetTextField: UITextField?
#IBOutlet weak var stateTextField: UITextField?
#IBOutlet weak var postalCodeTextField: UITextField?
#IBOutlet weak var cityTextField: UITextField?
// ??? Style Color
#IBOutlet var separatorViews: [UIView]!
// Helper
var textFieldOrdering: [UITextField?] = []
//MARK: - Lifecycle
public required init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
open override func awakeFromNib() {
super.awakeFromNib()
textFieldOrdering = [streetTextField, stateTextField, postalCodeTextField, cityTextField]
}
deinit {
streetTextField?.delegate = nil
streetTextField?.removeTarget(self, action: nil, for: .allEvents)
stateTextField?.delegate = nil
stateTextField?.removeTarget(self, action: nil, for: .allEvents)
postalCodeTextField?.delegate = nil
postalCodeTextField?.removeTarget(self, action: nil, for: .allEvents)
cityTextField?.delegate = nil
cityTextField?.removeTarget(self, action: nil, for: .allEvents)
}
//MARK: - Actions
#IBAction func changeLabelButtonPressed(_ sender: Any) {
if let rowConformance = row as? LabeledRowConformance {
rowConformance.onLabelButtonDidPress()
}
}
#IBAction func countryButtonPressed(_ sender: Any) {
if let rowConformance = row as? CountryRowConformance {
rowConformance.onCountryButtonDidPress()
}
}
func internalNavigationAction(_ sender: UIBarButtonItem) {
guard let inputAccesoryView = inputAccessoryView as? NavigationAccessoryView else { return }
var index = 0
for field in textFieldOrdering {
if field?.isFirstResponder == true {
let _ = sender == inputAccesoryView.previousButton ? textFieldOrdering[index-1]?.becomeFirstResponder() : textFieldOrdering[index+1]?.becomeFirstResponder()
break
}
index += 1
}
}
func textFieldDidChange(_ textField : UITextField){
if row.baseValue == nil{
row.baseValue = PostalAddress()
}
guard let textValue = textField.text else {
switch(textField) {
case let field where field == streetTextField:
row.value?.street = nil
case let field where field == stateTextField:
row.value?.state = nil
case let field where field == postalCodeTextField:
row.value?.postalCode = nil
case let field where field == cityTextField:
row.value?.city = nil
default:
break
}
return
}
if let rowConformance = row as? PostalAddressRowConformance {
var useFormatterDuringInput = false
var valueFormatter: Formatter?
switch(textField) {
case let field where field == streetTextField:
useFormatterDuringInput = rowConformance.streetUseFormatterDuringInput
valueFormatter = rowConformance.streetFormatter
case let field where field == stateTextField:
useFormatterDuringInput = rowConformance.stateUseFormatterDuringInput
valueFormatter = rowConformance.stateFormatter
case let field where field == postalCodeTextField:
useFormatterDuringInput = rowConformance.postalCodeUseFormatterDuringInput
valueFormatter = rowConformance.postalCodeFormatter
case let field where field == cityTextField:
useFormatterDuringInput = rowConformance.cityUseFormatterDuringInput
valueFormatter = rowConformance.cityFormatter
default:
break
}
if let formatter = valueFormatter, useFormatterDuringInput{
let value: AutoreleasingUnsafeMutablePointer<AnyObject?> = AutoreleasingUnsafeMutablePointer<AnyObject?>.init(UnsafeMutablePointer<T>.allocate(capacity: 1))
let errorDesc: AutoreleasingUnsafeMutablePointer<NSString?>? = nil
if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) {
switch(textField){
case let field where field == streetTextField:
row.value?.street = value.pointee as? String
case let field where field == stateTextField:
row.value?.state = value.pointee as? String
case let field where field == postalCodeTextField:
row.value?.postalCode = value.pointee as? String
case let field where field == cityTextField:
row.value?.city = value.pointee as? String
default:
break
}
if var selStartPos = textField.selectedTextRange?.start {
let oldVal = textField.text
textField.text = row.displayValueFor?(row.value)
if let f = formatter as? FormatterProtocol {
selStartPos = f.getNewPosition(forPosition: selStartPos, inTextInput: textField, oldValue: oldVal, newValue: textField.text)
}
textField.selectedTextRange = textField.textRange(from: selStartPos, to: selStartPos)
}
return
}
}
}
guard !textValue.isEmpty else {
switch(textField){
case let field where field == streetTextField:
row.value?.street = nil
case let field where field == stateTextField:
row.value?.state = nil
case let field where field == postalCodeTextField:
row.value?.postalCode = nil
case let field where field == cityTextField:
row.value?.city = nil
default:
break
}
return
}
switch(textField){
case let field where field == streetTextField:
row.value?.street = textValue
case let field where field == stateTextField:
row.value?.state = textValue
case let field where field == postalCodeTextField:
row.value?.postalCode = textValue
case let field where field == cityTextField:
row.value?.city = textValue
default:
break
}
}
//MARK: - Setup
override func setup() {
super.setup()
height = { 149 }
selectionStyle = .none
for textField in textFieldOrdering {
textField?.addTarget(self,
action: #selector(_PostalAddressCell.textFieldDidChange(_:)), // TODO: Move in extension
for: .editingChanged)
textField?.textAlignment = .left
textField?.clearButtonMode = .whileEditing
textField?.delegate = self
textField?.font = .preferredFont(forTextStyle: .body)
}
for separator in separatorViews {
separator.backgroundColor = .gray
}
}
//MARK: - Update
override func update() {
super.update()
textLabel?.text = nil
detailTextLabel?.text = nil
imageView?.image = nil
for textField in textFieldOrdering {
textField?.isEnabled = !row.isDisabled
textField?.textColor = row.isDisabled ? .gray : .black
textField?.autocorrectionType = .no
textField?.autocapitalizationType = .words
}
streetTextField?.text = row.value?.street
streetTextField?.keyboardType = .asciiCapable
stateTextField?.text = row.value?.state
stateTextField?.keyboardType = .asciiCapable
postalCodeTextField?.text = row.value?.postalCode
postalCodeTextField?.keyboardType = .numbersAndPunctuation
cityTextField?.text = row.value?.city
cityTextField?.keyboardType = .asciiCapable
if let rowConformance = row as? PostalAddressRowConformance {
setPlaceholderToTextField(textField: streetTextField, placeholder: rowConformance.streetPlaceholder)
setPlaceholderToTextField(textField: stateTextField, placeholder: rowConformance.statePlaceholder)
setPlaceholderToTextField(textField: postalCodeTextField, placeholder: rowConformance.postalCodePlaceholder)
setPlaceholderToTextField(textField: cityTextField, placeholder: rowConformance.cityPlaceholder)
}
countryButton?.setTitle(String(describing: row.value?.country), for: .normal)
}
//MARK: - BaseCell Responder
override func cellCanBecomeFirstResponder() -> Bool {
return !row.isDisabled && (
streetTextField?.canBecomeFirstResponder == true ||
stateTextField?.canBecomeFirstResponder == true ||
postalCodeTextField?.canBecomeFirstResponder == true ||
cityTextField?.canBecomeFirstResponder == true
)
}
override func cellBecomeFirstResponder(withDirection direction: Direction) -> Bool {
return direction == .down ? textFieldOrdering.first??.becomeFirstResponder() ?? false : textFieldOrdering.last??.becomeFirstResponder() ?? false
}
override func cellResignFirstResponder() -> Bool {
return streetTextField?.resignFirstResponder() ?? true
&& stateTextField?.resignFirstResponder() ?? true
&& postalCodeTextField?.resignFirstResponder() ?? true
&& stateTextField?.resignFirstResponder() ?? true
&& cityTextField?.resignFirstResponder() ?? true
}
override var inputAccessoryView: UIView? {
if let v = formViewController()?.inputAccessoryView(for: row) as? NavigationAccessoryView {
guard let first = textFieldOrdering.first, let last = textFieldOrdering.last, first != last else { return v }
if first?.isFirstResponder == true {
v.nextButton.isEnabled = true
v.nextButton.target = self
v.nextButton.action = #selector(_PostalAddressCell.internalNavigationAction(_:)) // TODO: Move in extension
} else if last?.isFirstResponder == true {
v.previousButton.target = self
v.previousButton.action = #selector(_PostalAddressCell.internalNavigationAction(_:))
v.previousButton.isEnabled = true
} else {
v.previousButton.target = self
v.previousButton.action = #selector(_PostalAddressCell.internalNavigationAction(_:))
v.nextButton.target = self
v.nextButton.action = #selector(_PostalAddressCell.internalNavigationAction(_:))
v.previousButton.isEnabled = true
v.nextButton.isEnabled = true
}
return v
}
return super.inputAccessoryView
}
//MARK: - UITextFieldDelegate
func textFieldDidBeginEditing(_ textField: UITextField) {
formViewController()?.beginEditing(of: self)
formViewController()?.textInputDidBeginEditing(textField, cell: self)
}
func textFieldDidEndEditing(_ textField: UITextField) {
formViewController()?.endEditing(of: self)
formViewController()?.textInputDidEndEditing(textField, cell: self)
textFieldDidChange(textField)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return formViewController()?.textInputShouldReturn(textField, cell: self) ?? true
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return formViewController()?.textInputShouldEndEditing(textField, cell: self) ?? true
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
return formViewController()?.textInputShouldBeginEditing(textField, cell: self) ?? true
}
func textFieldShouldClear(_ textField: UITextField) -> Bool {
return formViewController()?.textInputShouldClear(textField, cell: self) ?? true
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
return formViewController()?.textInputShouldEndEditing(textField, cell: self) ?? true
}
//MARK: - Private
private func setPlaceholderToTextField(textField: UITextField?, placeholder: String?) {
if let placeholder = placeholder, let textField = textField {
if let color = (row as? PostalAddressRowConformance)?.placeholderColor {
textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [NSForegroundColorAttributeName: color])
} else {
textField.placeholder = placeholder
}
}
}
}
final class PostalAddressCell: _PostalAddressCell<PostalAddress> {
public required init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Models:
protocol CountryType: Equatable {
var country: Country? { get set }
}
func == <T: CountryType>(lhs: T, rhs: T) -> Bool {
return lhs.country == rhs.country
}
//
protocol PostalAddressType: CountryType {
var street: String? { get set }
var state: String? { get set }
var postalCode: String? { get set }
var city: String? { get set }
}
func == <T: PostalAddressType>(lhs: T, rhs: T) -> Bool {
return lhs.street == rhs.street && lhs.state == rhs.state && lhs.postalCode == rhs.postalCode && lhs.city == rhs.city && lhs.country == rhs.country
}
//
class PostalAddress: PostalAddressType {
var street: String?
var state: String?
var postalCode: String?
var city: String?
var country: Country?
public init() {}
public init(street: String?, state: String?, postalCode: String?, city: String?, country: Country?) {
self.street = street
self.state = state
self.postalCode = postalCode
self.city = city
self.country = country
}
}

Resources