SwiftUI: Is it possible to automatically move to the next textfield after 1 character is entered? - textfield

I trying to make a SwiftUI app where after entering one letter in a TextField the cursor automatically moves to the next TextField. The UI is pretty much like this.
In Swift/IB, it looks like this was done with delegates and adding a target like in this post:
How to move to the next UITextField automatically in Swift
But can't find any documentation for using delegates/targets in SwiftUI.
I tried following this post:
SwiftUI TextField max length
But this has not worked for me. Setting the .prefix(1) does not seem to make a difference. The TextField still accepts any amount of characters and when moved to the next TextField does not reduce the characters entered to only the first character.
In SwiftUI's current state, is it possible to automatically move to the next TextField after 1 character is entered?
Thanks for any help!

It can be done in iOS 15 with FocusState
import SwiftUI
///Sample usage
#available(iOS 15.0, *)
struct PinParentView: View {
#State var pin: Int = 12356
var body: some View {
VStack{
Text(pin.description)
PinView(pin: $pin)
}
}
}
#available(iOS 15.0, *)
struct PinView: View {
#Binding var pin: Int
#State var pinDict: [UniqueCharacter] = []
#FocusState private var focusedField: UniqueCharacter?
var body: some View{
HStack{
ForEach($pinDict, id: \.id, content: { $char in
TextField("pin digit", text:
Binding(get: {
char.char.description
}, set: { newValue in
let newest: Character = newValue.last ?? "0"
//This check is only needed if you only want numbers
if Int(newest.description) != nil{
char.char = newest
}
//Set the new focus
DispatchQueue.main.async {
setFocus()
}
})
).textFieldStyle(.roundedBorder)
.focused($focusedField, equals: char)
})
}.onAppear(perform: {
//Set the initial value of the text fields
//By using unique characters you can keep the order
pinDict = pin.description.uniqueCharacters()
})
}
func setFocus(){
//Default to the first box when focus is not set or the user reaches the last box
if focusedField == nil || focusedField == pinDict.last{
focusedField = pinDict.first
}else{
//find the index of the current character
let idx = pinDict.firstIndex(of: focusedField!)
//Another safety check for the index
if idx == nil || pinDict.last == pinDict[idx!]{
focusedField = pinDict.first
}else{
focusedField = pinDict[idx! + 1]
}
}
//Update the Binding that came from the parent
setPinBinding()
}
///Updates the binding from the parent
func setPinBinding(){
var newPinInt = 0
for n in pinDict{
if n == pinDict.first{
newPinInt = Int(n.char.description) ?? 0
}else{
newPinInt = Int(String(newPinInt) + n.char.description) ?? 0
}
}
pin = newPinInt
}
}
//Convert String to Unique characers
extension String{
func uniqueCharacters() -> [UniqueCharacter]{
let array: [Character] = Array(self)
return array.uniqueCharacters()
}
func numberOnly() -> String {
self.trimmingCharacters(in: CharacterSet(charactersIn: "-0123456789.").inverted)
}
}
extension Array where Element == Character {
func uniqueCharacters() -> [UniqueCharacter]{
var array: [UniqueCharacter] = []
for char in self{
array.append(UniqueCharacter(char: char))
}
return array
}
}
//String/Characters can be repeating so yu have to make them a unique value
struct UniqueCharacter: Identifiable, Equatable, Hashable{
var char: Character
var id: UUID = UUID()
}
#available(iOS 15.0, *)
struct PinView_Previews: PreviewProvider {
static var previews: some View {
PinParentView()
}
}

Related

How to add elements to a HStack Using ForEach SwiftUi

Im fairly new to Swift and I'm trying to produce a HStack (that will be used in a progress bar) of element and to be able to add elements with a button.
Im not sure if I should use a variable in the ForEach(1..<Variable) section or use another method. Here is the code I have so far but it did not work.
struct ContentView: View {
#State var fill : CGFloat = 0
#State var NumberOfCircles : Int = 0
var body: some View {
HStack(spacing:100) {
ForEach(0..<NumberOfCircles){ _ in
MyShape()
}
Button(action: {NumberOfCircles = 5}, label: {
Text("Button")
})
}
ForEach in SwiftUI needs a constant range to loop over. However,
as the error suggests, if you conform to Identifiable or use ForEach(_:id:content:) and provide an explicit id it is happy. So try this:
struct ContentView: View {
#State var fill: CGFloat = 0
#State var NumberOfCircles: Int = 0
var body: some View {
HStack(spacing: 20) {
ForEach(0..<NumberOfCircles, id: \.self){ _ in // <-- here
MyShape()
}
Button(action: {NumberOfCircles = 5}){
Text("Button")
}
}
}
}
I'm not sure what's your problem, but I tested this code and it works:
struct ContentView: View {
#State var numberOfCircles = 1
var body: some View {
VStack {
HStack {
ForEach(0..<numberOfCircles, id:\.self) { _ in
Circle()
.frame(width: 30, height: 30)
}
}
Button { numberOfCircles = 5 } label: {
Text("Add Circles")
}
}
}
}
Btw, naming convention for variables in Swift is camelCase. That means that declaring a variable you should name it numberOfCircles , not NumberOfCircles . The first uppercase letter is reserved for naming Classes, Structs and Protocols.

Picture Storage as URL in SQLite DB in Swift 5

I have a swift project that uses an array of pictures. The array of pictures is kept in storage in the filmanager.default.urls path. The URLs are then kept in a SQLite database. I am able to add to the array without a problem, however, I run into an issue whenever I try to recall it from memory. I do not know if I am saving it wrong or loading it wrong. I will post the code for how I did that here.
func save(images: Array<UIImage>, identifier: String) -> Array<String> {
var picCounter = 0
var URLs: Array<String> = []
for pic in images {
let id = identifier + "makeSureThisCantAccidentallyBeAnID" + String(picCounter)
let jpgImageData = pic.jpegData(compressionQuality: 0.5)
let documentURL = fileManager.urls(for: .documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first
let path = documentURL!.appendingPathComponent(id + ".png")
do {
try jpgImageData!.write(to: path)
} catch {
print(error)
}
picCounter += 1
URLs.append(path.absoluteString)
}
return URLs
}
That is my save function. It should store the URLs and return an array of URLs in string format.
Here is my loading function, it should take an array of URLs and return the array of images.
func recover(URLarray: Array<String>, identifier: String) -> Array<UIImage> {
if URLarray[0] == "fake url" {
let pic = #imageLiteral(resourceName: "LaunchScreen")
let fakeArray = [pic]
return fakeArray
}
var picCounter = 0
var imageArray: Array<UIImage> = []
for url in URLarray {
//let fileData = FileManager.default.contents(atPath: url)
guard let image = UIImage(contentsOfFile: url) else { return imageArray }
picCounter += 1
imageArray.append(image)
}
return imageArray
}
The "#imageliteral" is a literal image that is used instead of an array if there is no array to load. This should theoretically recall every image without returning nil. Lastly, I will post my SQLite saving function
func saveNote(note: Note) {
connect()
var statement: OpaquePointer? = nil
if sqlite3_prepare_v2(
database,
"UPDATE remember SET person = ?, memories = ?, imageurl = ? WHERE rowid = ?",
-1,
&statement,
nil
) == SQLITE_OK {
sqlite3_bind_text(statement, 1, NSString(string: note.person).utf8String, -1, nil)
sqlite3_bind_text(statement, 2, NSString(string: note.memories).utf8String, -1, nil)
var imageURL: String = ""
for url in note.imageURLs {
imageURL.append(url)
imageURL.append("#")
}
sqlite3_bind_text(statement, 3, NSString(string: imageURL).utf8String, -1, nil)
sqlite3_bind_int(statement, 4, note.id)
if sqlite3_step(statement) != SQLITE_DONE {
print("Error saving note")
}
}
else {
print("Error creating note update statement")
}
sqlite3_finalize(statement)
}
This is how I tried to save it. I had to store a URL array as one string so I decided to do so by making it a string I could split with the character "#" if you think that is the problem then please tell me what character or method I could use to do this, as I have tried changing the character. If anyone helps me this far then I am extremely grateful! Thank you!

Animate the drawing of an MKPolyline on MKMapView

I’ve got an MKMapView to animate a line by adding a line, removing it, adding a minor segment and re-adding it to the map. However, this puts a lot of overhead on the phone and doesn’t look the best. I noticed Google Maps and Uber have cleanly animated lines for showing routes that run smoothly no matter what the length or route type. Does anyone have any suggestions for a solution which is less energy-draining and looks cleaner?
Thanks, SebO.
An array of coordinates will be needed. If you have only beginning and end coordinates, get array of coordinates using below code
func getPointsOnRoute(from: CLLocation?, to: CLLocation?, on mapView: MKMapView?) -> [CLLocation]? {
let NUMBER_OF_PIXELS_TO_SKIP: Int = 120
//lower number will give a more smooth animation, but will result in more layers
var ret = [Any]()
var fromPoint: CGPoint? = nil
if let aCoordinate = from?.coordinate {
fromPoint = mapView?.convert(aCoordinate, toPointTo: mapView)
}
var toPoint: CGPoint? = nil
if let aCoordinate = to?.coordinate {
toPoint = mapView?.convert(aCoordinate, toPointTo: mapView)
}
let allPixels = getAllPoints(from: fromPoint!, to: toPoint!)
var i = 0
while i < (allPixels?.count)! {
let pointVal = allPixels![i] as? NSValue
ret.append(point(toLocation: mapView, from: (pointVal?.cgPointValue)!)!)
i += NUMBER_OF_PIXELS_TO_SKIP
}
ret.append(point(toLocation: mapView, from: toPoint!)!)
return ret as? [CLLocation] }
Having array of coordinates add rendering of the overlays in MKMapViewDelegate’s delegate method — mapView(_:rendererFor:).
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
guard let polyline = overlay as? MKPolyline else {
return MKOverlayRenderer()
}
let polylineRenderer = MKPolylineRenderer(overlay: polyline)
polylineRenderer.strokeColor = .black
polylineRenderer.lineWidth = 2
return polylineRenderer
}
mapView.addOverlay(polyline) // add it to mapview
render the polyline in small segments to create the animation effect
var drawingTimer: Timer?
// ....// Somewhere in your View Controller
func animate(route: [CLLocationCoordinate2D], duration: TimeInterval, completion: (() -> Void)?) {
guard route.count > 0 else { return }
var currentStep = 1
let totalSteps = route.count
let stepDrawDuration = duration/TimeInterval(totalSteps)
var previousSegment: MKPolyline?
drawingTimer = Timer.scheduledTimer(withTimeInterval: stepDrawDuration, repeats: true) { [weak self] timer in
guard let self = self else {
// Invalidate animation if we can't retain self
timer.invalidate()
completion?()
return
}
if let previous = previousSegment {
// Remove last drawn segment if needed.
self.mapView.removeOverlay(previous)
previousSegment = nil
}
guard currentStep < totalSteps else {
// If this is the last animation step...
let finalPolyline = MKPolyline(coordinates: route, count: route.count)
self.mapView.addOverlay(finalPolyline)
// Assign the final polyline instance to the class property.
self.polyline = finalPolyline
timer.invalidate()
completion?()
return
}
// Animation step.
// The current segment to draw consists of a coordinate array from 0 to the 'currentStep' taken from the route.
let subCoordinates = Array(route.prefix(upTo: currentStep))
let currentSegment = MKPolyline(coordinates: subCoordinates, count: subCoordinates.count)
self.mapView.addOverlay(currentSegment)
previousSegment = currentSegment
currentStep += 1
}
}

NSComboBox items misplaced if user drag an app window on mac OS

I work at desktop application for OS X and macOS and have issue with native NSComboBox (not subclass).
It is the problem that comboBox items misplaces as you can see on screenshot below.
Also a have a warning in XCode console output:
This application is trying to draw a very large combo box, 28 points tall. Vertically resizable combo boxes are not supported, but it happens that 10.4 and previous drew something that looked kind of sort of okay. The art in 10.5 does not break up in a way that supports that drawing. This application should be revised to stop using large combo boxes. This warning will appear once per app launch.
Could this be a warning associated with this problem? I tried to fix warning but failed.
I use Xcode 8.2.1 and OS X El Capitan 10.11.6.
Edited:
Current ViewController contained in custom navigation controller and have combobox delegate/datasource implementation.
View added as below to navigation container:
addChildViewController(viewController)
viewController.view.frame.size = contentView != nil ? contentView!.frame.size : CGSize.zero
viewController.view.frame.origin = CGPoint.zero
viewController.view.autoresizingMask = [.viewWidthSizable, .viewHeightSizable]
if let animation = animation {
contentView?.layer?.add(animation, forKey: animation.type)
}
contentView?.addSubview(viewController.view)
ComboBox datasource implementation
public func numberOfItems(in comboBox: NSComboBox) -> Int {
switch comboBox {
case companyComboBox:
return filteredCompanies?.count ?? 0
case projectComboBox:
return filteredProjects?.count ?? 0
default:
return 0
}
}
public func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
switch comboBox {
case companyComboBox:
return company(at: index)?.name
case projectComboBox:
return project(at: index)?.name
default:
return nil
}
}
public func comboBox(_ comboBox: NSComboBox, indexOfItemWithStringValue string: String) -> Int {
switch comboBox {
case companyComboBox:
return selectedCompany == nil ? -1 : comboBox.indexOfSelectedItem
case projectComboBox:
return selectedProject == nil ? -1 : comboBox.indexOfSelectedItem
default:
return -1
}
}
public func comboBox(_ comboBox: NSComboBox, completedString string: String) -> String? {
switch comboBox {
case companyComboBox:
let bestMatched = bestMatchedCompany(for: string)
filteredCompanies = filterCompanies(by: string)
return bestMatched?.name
case projectComboBox:
let bestMatched = bestMatchedProject(for: string)
filteredProjects = filterProjects(by: string)
return bestMatched?.name
default:
return nil
}
}
ComboBox delegate implementation:
//MARK: NSComboBoxDelegate
func comboBoxSelectionDidChange(_ notification: Notification) {
let comboBox = notification.object as! NSComboBox
let selectedIndex = comboBox.indexOfSelectedItem
guard selectedIndex >= 0 else { return }
switch comboBox {
case companyComboBox:
selectedCompany = company(at: selectedIndex)
case projectComboBox:
selectedProject = project(at: selectedIndex)
default:
break
}
}
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
guard let comboBox = control as? NSComboBox, comboBox.stringValue.characters.count > 0 else { return false }
if (commandSelector == #selector(insertBacktab(_:))) {
switch comboBox {
case companyComboBox:
filteredCompanies = filterCompanies(by: comboBox.stringValue)
case projectComboBox:
filteredProjects = filterProjects(by: comboBox.stringValue)
default:
break
}
} else if commandSelector == #selector(deleteBackward(_:)) {
//Need to filter by substring of comboBox.stringValue because before deleteBackward comboBox has old text value.
let selectedTextRange = textView.selectedRanges.first as! NSRange
let endIndex = comboBox.stringValue.index(comboBox.stringValue.startIndex,
offsetBy: selectedTextRange.length > 0 ? selectedTextRange.location : selectedTextRange.location - 1)
let filterValue = comboBox.stringValue.substring(with: comboBox.stringValue.startIndex..<endIndex)
switch comboBox {
case companyComboBox:
filteredCompanies = filterCompanies(by: filterValue)
case projectComboBox:
filteredProjects = filterProjects(by: filterValue)
default:
break
}
} else if commandSelector == #selector(insertNewline(_:)) || commandSelector == #selector(insertTab(_:)) {
switch comboBox {
case companyComboBox:
let company = bestMatchedCompany(for: comboBox.stringValue)
if comboBox.stringValue == company?.name {
selectedCompany = company
}
case projectComboBox:
let project = bestMatchedProject(for: comboBox.stringValue)
if comboBox.stringValue == project?.name {
selectedProject = project
}
default:
break
}
}
return false
}
Do you have an idea how to fix it?
Thanks!

Calculator in Swift a method [rangeOfString]

I am making calculator in Swift for IOS 8. All operand and operator works very well. Only problem is with "."
For example, 192.168.0.1 is invalid. The "." should only display once. Instead 192.16801 is valid.
See below code -
//Assume user has entered 192.168
//User cannot press "." button again now, "." should not appear again
var display.text 192.168`
if display.text.rangeOfString(".") != nil{
println("exists")
}
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self
}
//Textfield delegates
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { // return NO to not change text
switch string {
case "0","1","2","3","4","5","6","7","8","9":
return true
case ".":
let array = Array(textField.text)
var decimalCount = 0
for character in array {
if character == "." {
decimalCount++
}
}
if decimalCount == 1 {
return false
} else {
return true
}
default:
let array = Array(string)
if array.count == 0 {
return true
}
return false
}
}
}
the textField functions will return a value of true if you have entered a valid number, it will return a value of false if you input anything else than a number or if you enter a second “.”. If this functions returns false it will not change the value of the text field. Example: if a user (tries to) input 3.14a9 the a will be ignored and the textfield text will be 3.149. If a user inputs 3.45.12 the textfield will have a text value of 3.45.12
I’m not sure this is the question, but if you’re looking for a second “.” you can use the find function with a where for this:
let decimal: Character = "."
if let first = find(display.text, decimal)
where (find(display.text[first.successor()..<display.text.endIndex], decimal) != nil) {
println("second exists")
}
else {
println("none or one")
}
You can use regular expression to validate your textfield whenever its value changes.
Example of a regulator
/*
^ //begin string
([0-9]+)? //optional: has 0 or more digits
(\\.)? //optional: has a point
([0-9]+)? //optional: has 0 or more digits
$ //end string
*/
extension String {
func isValidNumber() -> Bool {
let regex = NSRegularExpression(pattern: "^([0-9]+)?(\\.)?([0-9]+)?$", options: .CaseInsensitive, error: nil)
return regex?.firstMatchInString(self, options: nil, range: NSMakeRange(0, countElements(self))) != nil
}
}
Used in the call-back of your UITextField:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
return textField.text.isValidNumber()
}
var text = "192.168.2"
var isValid = true
let range = text.rangeOfString(".")
if let range = range {
let subText = text.substringFromIndex(range.endIndex)
let range2 = subText.rangeOfString(".")
if range2 != nil {
isValid = false
}
}
println("isValid: \(isValid)") // isValid: false