Index out of range (Table with expandable rows) in Swift 3 - swift3

Hello i am trying to make a shop page on my application. The shop page contains a table with a title that is clickable so it expands into subcategories of that title. Etc click coffee and get some different kind of coffee to buy from.
Title = Drinks, Sandwiches & Drinks in the picture below.
subcategory from title = Ham & cheese, Chicken, bacon & curry .... and so on.
Now the expanding and closing the titles work fine. But when i press + (add iteem to cart) i get a out out of index if it's clicked in a specific order.
if i expand sandwiches (like it is in the picture) and then expand drinks (the one below sandwiches) and then click on the plus on any subcategory in sandwiches and then close sandwiches again and press on plus on any subcategory in Drinks (the one below the now closed sandwiches) the application will crash with a out of index error. Only this combination gives this error.
How the code works:
/* Create Cells */
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -
> UITableViewCell {
// Row is DefaultCell
if let rowData = destinationData?[indexPath.row] {
let defaultCell = Bundle.main.loadNibNamed("TableViewCellMenuItems", owner: self, options: nil)?.first as! TableViewCellMenuItems
// tableView.dequeueReusableCell(withIdentifier: "TableViewCellMenuItems", for: indexPath).first as! TableViewCellMenuItems
defaultCell.menuName.text = rowData.name
defaultCell.menuPicture.image = rowData.image
defaultCell.menuFoldPic.image = #imageLiteral(resourceName: "ic_keyboard_arrow_down")
defaultCell.selectionStyle = .none
return defaultCell
}
// Row is ExpansionCell
else {
if let rowData = destinationData?[getParentCellIndex(expansionIndex: indexPath.row)] {
// print("her åbner vi")
// // Create an ExpansionCell
let expansionCell =
Bundle.main.loadNibNamed("TableViewCellMenuItemExpanded", owner: self, options: nil)?.first as! TableViewCellMenuItemExpanded
// Get the index of the parent Cell (containing the data)
let parentCellIndex = getParentCellIndex(expansionIndex: indexPath.row)
// Get the index of the flight data (e.g. if there are multiple ExpansionCells
let flightIndex = indexPath.row - parentCellIndex - 1
// Set the cell's data
expansionCell.itemPrice.text = String (describing: "\(rowData.menuItems![flightIndex].price) kr")
expansionCell.itemName.text = rowData.menuItems?[flightIndex].name
expansionCell.itemCount.text = String (describing: rowData.menuItems![flightIndex].count)
// expansionCell.itemCount.text = String (describing: indexPath.row)
expansionCell.itemAdd.tag = Int(indexPath.row)
expansionCell.itemMinus.tag = Int(indexPath.row)
expansionCell.itemAdd.addTarget(self, action: #selector(HomeSelectedShopViewController.plus(_:)), for: .touchUpInside)
expansionCell.itemMinus.addTarget(self, action: #selector(HomeSelectedShopViewController.minus(_:)), for: .touchUpInside)
expansionCell.selectionStyle = .none
return expansionCell
}
}
return UITableViewCell()
}
as you can see the expansionCells get a tag for my function plus and minus (so i can know which indexPath i should add up or down)
Plus function:
func plus(_ sender: AnyObject?) {
if let rowData = destinationData?[getParentCellIndex(expansionIndex: sender!.tag)] {
self.table.reloadData()
// Get the index of the parent Cell (containing the data)
let parentCellIndex = getParentCellIndex(expansionIndex: sender!.tag)
// Get the index of the flight data (e.g. if there are multiple ExpansionCells
let flightIndex = sender!.tag - parentCellIndex - 1
print(sender!.tag)
print(flightIndex)
print(rowData.menuItems!.count)
var con = 0;
for a in rowData.menuItems! {
print("her er navn : \(a.name) og her er id \(con)")
con += 1
}
let data = rowData.menuItems![flightIndex]
let item = Items(name: data.name, price: data.price, count: data.count)
var counter = 0;
for x in self.basket {
if(x.name == item.name || x.price == item.price)
{
counter = counter+1;
}
}
if(counter >= 99)
{
}
else
{
DispatchQueue.main.async {
self.basket.append(item)
self.updateBasket()
rowData.menuItems![flightIndex].count = rowData.menuItems![flightIndex].count+1;
self.table.reloadData()
}
}
}
}
Rest of the functions:
/* Expand cell at given index */
private func expandCell(tableView: UITableView, index: Int) {
// print("when is this run 1")
// Expand Cell (add ExpansionCells
if let flights = destinationData?[index]?.menuItems {
for i in 1...flights.count {
destinationData?.insert(nil, at: index + i)
tableView.insertRows(at: [NSIndexPath(row: index + i, section: 0) as IndexPath] , with: .top)
}
}
}
/* Contract cell at given index */
private func contractCell(tableView: UITableView, index: Int) {
// print("when is this run 4")
if let flights = destinationData?[index]?.menuItems {
for i in 1...flights.count {
destinationData?.remove(at: index+1)
tableView.deleteRows(at: [NSIndexPath(row: index+1, section: 0) as IndexPath], with: .top)
}
}
}
/* Get parent cell index for selected ExpansionCell */
private func getParentCellIndex(expansionIndex: Int) -> Int {
// print("when is this run 5")
var selectedCell: Menu?
var selectedCellIndex = expansionIndex
while(selectedCell == nil && selectedCellIndex >= 0) {
selectedCellIndex -= 1
selectedCell = destinationData?[selectedCellIndex]
}
return selectedCellIndex
}
func minus(_ sender: AnyObject?) {
if let rowData = destinationData?[getParentCellIndex(expansionIndex: sender!.tag)] {
// Get the index of the parent Cell (containing the data)
let parentCellIndex = getParentCellIndex(expansionIndex: sender!.tag)
// Get the index of the flight data (e.g. if there are multiple ExpansionCells
let flightIndex = sender!.tag - parentCellIndex - 1
let data = rowData.menuItems![flightIndex]
let item = Items(name: data.name, price: data.price, count: data.count)
DispatchQueue.main.async {
var counter = 0;
for x in self.basket {
if(x.name == item.name || x.price == item.price)
{
if(item.count == 0)
{
break
}
else
{
self.basket.remove(at: counter)
self.updateBasket()
rowData.menuItems![flightIndex].count = rowData.menuItems![flightIndex].count-1;
self.table.reloadData()
break
}
}
counter = counter+1;
}
}
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let data = destinationData?[indexPath.row] {
// If user clicked last cell, do not try to access cell+1 (out of range)
if(indexPath.row + 1 >= (destinationData?.count)!) {
expandCell(tableView: tableView, index: indexPath.row)
}
else {
// If next cell is not nil, then cell is not expanded
if(destinationData?[indexPath.row+1] != nil) {
expandCell(tableView: tableView, index: indexPath.row)
} else {
contractCell(tableView: tableView, index: indexPath.row)
}
}
}
}
I dont really know how to solve this, all i know is that the plus function gets out of range on this bit "let data = rowData.menuItems![flightIndex]".

Related

ARKit and RealityKit - ARSessionDelegate is retaining 14 ARFrames

I am classifying images per frame from ARSession delegate by Vision framework and CoreML in an Augmented Reality app, with ARKit and RealityKit. While processing a frame.capturedImage I am not requesting another frame.capturedImage for performance.
The camera is not giving smooth experience, it gets stuck time to time. Seems like a frame loss.
And I am getting this Warning:
[Session] ARSession <0x122cc3710>: ARSessionDelegate is retaining 14 ARFrames. This can lead to future camera frames being dropped.
My Codes:
import Foundation
import SwiftUI
import RealityKit
import ARKit
import CoreML
struct ARViewContainer: UIViewRepresentable {
var errorFunc: ()->Void
var frameUpdateFunc: ()->Void
#Binding var finalLabel:String
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal,.vertical]
config.environmentTexturing = .automatic
if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh){
config.sceneReconstruction = .mesh
}
arView.session.delegate = context.coordinator
arView.session.run(config)
context.coordinator.myView = arView
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(finalLabel: $finalLabel, self, funct: self.errorFunc, frameUpdateFunc: self.frameUpdateFunc)
}
class Coordinator: NSObject, ARSessionDelegate {
var objectDetectionService = ObjectDetectionService()
var myView:ARView?
#Binding var finalLabel:String
var parent: ARViewContainer
var efunc:()->Void
var frameUpdateFunc:()->Void
var isLoopShouldContinue = true
var lastLocation: SCNVector3?
//let model = try? MobileNetV2(configuration: .init())
private let classifier = VisionClasifier(mlModel: try? MobileNetV2(configuration: .init()).model)
private var currentBuffer: CVPixelBuffer? = nil
init(finalLabel:Binding<String>,_ arView: ARViewContainer,funct: #escaping ()->Void, frameUpdateFunc:#escaping ()->Void) {
parent = arView
self.efunc = funct
self.frameUpdateFunc = frameUpdateFunc
_finalLabel = finalLabel
}
func session(_ session: ARSession, didFailWithError error: Error) {
//print("Error Tanvir: ",error)
self.efunc()
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
if isLoopShouldContinue{
self.classifyFrame(currentFrame: frame)
}
let transform = SCNMatrix4(frame.camera.transform)
let orientation = SCNVector3(-transform.m31, -transform.m32, transform.m33)
let location = SCNVector3(transform.m41, transform.m42, transform.m43)
let currentPositionOfCamera = orientation + location
if let lastLocation = lastLocation {
let speed = (lastLocation - currentPositionOfCamera).length()
isLoopShouldContinue = speed < 0.0025
}
lastLocation = currentPositionOfCamera
}
// When ARKit detects a new anchor, it will add it to the ARSession
// Whenever there is a newly added ARAnchor, you will get that anchor here.
// In this short tutorial, we will target the ARPlaneAnchor, and use the information stored
// in that anchor for visualization.
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
guard let myView = myView else {
return
}
for anchor in anchors {
if anchor is ARPlaneAnchor {
let planeAnchor = anchor as! ARPlaneAnchor
//addPlaneEntity(with: planeAnchor, to: myView)
}
}
}
// ARKit will automatically track and update the ARPlaneAnchor.
// We use that anchor to update the `skin` of the plane.
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
guard let myView = myView else {
return
}
for anchor in anchors {
if anchor is ARPlaneAnchor {
let planeAnchor = anchor as! ARPlaneAnchor
//updatePlaneEntity(with: planeAnchor, in: myView)
}
}
}
// When ARKit remove an anchor from the ARSession, you will get the removed
// anchor here.
func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {
guard let myView = myView else {
return
}
for anchor in anchors {
if anchor is ARPlaneAnchor {
let planeAnchor = anchor as! ARPlaneAnchor
//removePlaneEntity(with: planeAnchor, from: myView)
}
}
}
func addAnnotation(rectOfInterest rect: CGRect, text: String,width:Float,height:Float) {
let point = CGPoint(x: rect.midX, y: rect.midY)
print("point:", point)
//let scnHitTestResults = myView.hitTest(point,
// options: [SCNHitTestOption.searchMode: SCNHitTestSearchMode.all.rawValue])
//guard !scnHitTestResults.contains(where: { $0.node.name == BubbleNode.name }) else { return }
let raycastResult = myView!.raycast(from: point, allowing: .estimatedPlane, alignment: .any)
// guard let raycastQuery = myView!.raycastQuery(from: point,
// allowing: .existingPlaneInfinite,
// alignment: .horizontal),
// let raycastResult = myView.session.raycast(raycastQuery).first else { return }
guard let raycastResult = raycastResult.first else{
print("raycast result failed")
return
}
let anchorExists = myView!.scene.anchors.contains(where: {$0.name == text})
guard anchorExists == false else{
print("anchor Already exists")
return
}
let position = raycastResult.worldTransform.columns.3
let myEntity = create2dEntity(with: position, boundingBox: rect, raycastResult: raycastResult,width:width ,height:height)
let planeAnchorEntity = AnchorEntity()
planeAnchorEntity.name = text
planeAnchorEntity.position = simd_make_float3(position)
planeAnchorEntity.addChild(myEntity)
// Finally, add the entity to scene.
myView!.scene.addAnchor(planeAnchorEntity)
print("anchor added: ", planeAnchorEntity.name)
}
func classifyFrame(currentFrame:ARFrame){
//let currentImageName = photos[currentIndex]
// 2
// 3
print("inside Classify")
//print("CurrentBuffer", currentBuffer)
guard self.currentBuffer == nil else {
//print("CurrentBuffer: ",currentBuffer)
//self.finalLabel = "current buffer problem"
return
}
self.currentBuffer = currentFrame.capturedImage
// guard let model = self.model else {
// return "Model not Found."
// }
let img = CIImage(cvImageBuffer: currentFrame.capturedImage)
let cgImage = convertCIImageToCGImage(inputImage: img)
guard let cgImage = cgImage else{
print("can not convert CGImage")
self.finalLabel = "can not convert CGImage"
return
}
objectDetectionService.detect(on: .init(pixelBuffer: currentFrame.capturedImage)) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let response):
self.finalLabel = response.classification.description
print("Real Width: ",response.boundingBox.width)
let rectOfInterest = VNImageRectForNormalizedRect(
response.boundingBox,
Int(self.myView!.bounds.width),
Int(self.myView!.bounds.height))
self.addAnnotation(rectOfInterest: rectOfInterest, text: response.classification.description,width: Float(response.boundingBox.width),height: Float(response.boundingBox.height))
print("Success:",response.classification.description)
self.currentBuffer = nil
case .failure(let error):
self.finalLabel = "Detection Failed"
print("Detection failure: ",error.localizedDescription)
self.currentBuffer = nil
break
}
}
}
}
}
func convertCIImageToCGImage(inputImage: CIImage) -> CGImage? {
let context = CIContext(options: nil)
if let cgImage = context.createCGImage(inputImage, from: inputImage.extent) {
return cgImage
}
return nil
}
// The ARPlaneAnchor contains the information we need to create the `skin` of the plane.
func addPlaneEntity(with anchor: ARPlaneAnchor, to view: ARView) {
let planeAnchorEntity = AnchorEntity(.plane([.any],
classification: [.any],
minimumBounds: [0.01, 0.01]))
let planeModelEntity = createPlaneModelEntity(with: anchor)
// Give Entity a name for tracking.
planeAnchorEntity.name = anchor.identifier.uuidString + "_anchor"
planeModelEntity.name = anchor.identifier.uuidString + "_model"
// Add ModelEntity as a child of AnchorEntity.
// AnchorEntity handles `position` of the plane.
// ModelEntity handles the `skin` of the plane.
planeAnchorEntity.addChild(planeModelEntity)
// Finally, add the entity to scene.
view.scene.addAnchor(planeAnchorEntity)
}
func create2dEntity(with position: simd_float4, boundingBox: CGRect, raycastResult:ARRaycastResult, width:Float,height:Float ) -> ModelEntity{
var planeMesh: MeshResource
var color: UIColor
print("horizotal plane")
color = UIColor.red.withAlphaComponent(0.5)
print("Constant width: 0.1 but BoundingBox Width: ",boundingBox.width)
planeMesh = .generatePlane(width: 0.1, height: 0.1)
return ModelEntity(mesh: planeMesh, materials: [SimpleMaterial(color: color, roughness: 0.25, isMetallic: false)])
}
func createPlaneModelEntity(with anchor: ARPlaneAnchor) -> ModelEntity {
var planeMesh: MeshResource
var color: UIColor
if anchor.alignment == .horizontal {
print("horizotal plane")
color = UIColor.blue.withAlphaComponent(0.5)
planeMesh = .generatePlane(width: anchor.extent.x, depth: anchor.extent.z)
} else if anchor.alignment == .vertical {
print("vertical plane")
color = UIColor.yellow.withAlphaComponent(0.5)
planeMesh = .generatePlane(width: anchor.extent.x, height: anchor.extent.z)
} else {
fatalError("Anchor is not ARPlaneAnchor")
}
return ModelEntity(mesh: planeMesh, materials: [SimpleMaterial(color: color, roughness: 0.25, isMetallic: false)])
}
func removePlaneEntity(with anchor: ARPlaneAnchor, from arView: ARView) {
guard let planeAnchorEntity = arView.scene.findEntity(named: anchor.identifier.uuidString+"_anchor") else { return }
arView.scene.removeAnchor(planeAnchorEntity as! AnchorEntity)
}
func updatePlaneEntity(with anchor: ARPlaneAnchor, in view: ARView) {
var planeMesh: MeshResource
guard let entity = view.scene.findEntity(named: anchor.identifier.uuidString+"_model") else { return }
let modelEntity = entity as! ModelEntity
if anchor.alignment == .horizontal {
planeMesh = .generatePlane(width: anchor.extent.x, depth: anchor.extent.z)
} else if anchor.alignment == .vertical {
planeMesh = .generatePlane(width: anchor.extent.x, height: anchor.extent.z)
} else {
fatalError("Anchor is not ARPlaneAnchor")
}
modelEntity.model!.mesh = planeMesh
}
import SceneKit
extension SCNVector3 {
func length() -> Float {
return sqrtf(x * x + y * y + z * z)
}
}
func -(l: SCNVector3, r: SCNVector3) -> SCNVector3 {
return SCNVector3Make(l.x - r.x, l.y - r.y, l.z - r.z)
}
func +(l: SCNVector3, r: SCNVector3) -> SCNVector3 {
return SCNVector3(l.x + r.x, l.y + r.y, l.z + r.z)
}
func /(l: SCNVector3, r: Float) -> SCNVector3 {
return SCNVector3(l.x / r, l.y / r, l.z / r)
}
Detection: (Here is the problem, I guess, in detect method)
import Foundation
import UIKit
import CoreML
import Vision
import SceneKit
class ObjectDetectionService {
var mlModel = try! VNCoreMLModel(for: YOLOv3Int8LUT().model)
//let model = try? YOLOv3Int8LUT(configuration: .init())
lazy var coreMLRequest: VNCoreMLRequest = {
return VNCoreMLRequest(model: mlModel,
completionHandler: self.coreMlRequestHandler)
}()
private var completion: ((Result<Response, Error>) -> Void)?
func detect(on request: Request, completion: #escaping (Result<Response, Error>) -> Void) {
self.completion = completion
//let orientation = .up
let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: request.pixelBuffer)
do {
try imageRequestHandler.perform([coreMLRequest])
} catch {
self.complete(.failure(error))
return
}
}
}
private extension ObjectDetectionService {
func coreMlRequestHandler(_ request: VNRequest?, error: Error?) {
if let error = error {
complete(.failure(error))
return
}
guard let request = request, let results = request.results as? [VNRecognizedObjectObservation] else {
complete(.failure(RecognitionError.resultIsEmpty))
return
}
guard let result = results.first(where: { $0.confidence > 0.8 }),
let classification = result.labels.first else {
complete(.failure(RecognitionError.lowConfidence))
return
}
let response = Response(boundingBox: result.boundingBox,
classification: classification.identifier)
complete(.success(response))
}
func complete(_ result: Result<Response, Error>) {
DispatchQueue.main.async {
self.completion?(result)
self.completion = nil
}
}
}
enum RecognitionError: Error {
case unableToInitializeCoreMLModel
case resultIsEmpty
case lowConfidence
}
extension ObjectDetectionService {
struct Request {
let pixelBuffer: CVPixelBuffer
}
struct Response {
let boundingBox: CGRect
let classification: String
}
}
Why am I getting this warning, and How to get the camera smooth experience?
The session(_ session: ARSession, didUpdate frame: ARFrame) delegate method is called very frequently: many times per second. If your classifyFrame method is doing too much work, it will retain the ARFrame object until after the next frame is delivered to the delegate.
ARKit will warn you when too many frames are retained, typically because a queue is blocked in your delegate.

Why is this not saving?

I am trying to save a simple piece of information using NSUserdefaults. I am trying to save a SKSprite to have an alpha of 1. Here is how I am doing it.
First scene: Level select (sprite alpha is 0.2)
When user completes Level: (edit sprite in Level Select to equal one)
GameViewController:
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = levelselectscene {
// Set the scale mode to scale to fit the window
scene.scaleMode = .fill
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
override var shouldAutorotate: Bool {
return true
}
Level Select:
override func didMove(to view: SKView) {
if unlockLevelTwoButton == true {
levelselectscene?.childNode(withName: "LevelTwoButton")?.alpha = 1
UserDefaults.standard.set(unlockLevelTwoButton, forKey: "LevelTwoUnlocked")
print("I got this far")
}
}
Level One:
func didBegin(_ contact: SKPhysicsContact) {
var bodyA = contact.bodyA
var bodyB = contact.bodyB
let threeStars = SKScene(fileNamed: "LevelCompleted3Star")
let fadeAction = SKAction.fadeAlpha(by: 1, duration: 0.45)
if bodyA.categoryBitMask == 1 && bodyB.categoryBitMask == 2 || bodyA.categoryBitMask == 2 && bodyB.categoryBitMask == 1{
print("TEST")
levelOneCompleted() //islevelonecompleted
unlockLevelTwoButton = true
//3 stars
threeStars?.scaleMode = .fill
self.view?.presentScene(threeStars!, transition: .fade(withDuration: 0.3))
}
3 Stars:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if isLevelOneCompleted == true{
unlockLevelTwoButton = true
UserDefaults.standard.set(isLevelOneCompleted, forKey: "LevelOne")
UserDefaults.standard.synchronize()
levelselectscene?.scaleMode = .fill
levelselectscene?.childNode(withName: "levelTwoButton")?.alpha = 1
self.view?.presentScene(levelselectscene)
}
To me, it looks like the information should save. What am I doing wrong? I also have the keys set to retrieve:
if let z = UserDefaults.standard.object(forKey: "LevelTwoButton")
{
unlockLevelTwoButton = z as! Bool
}
Can't figure out why it's not saving!
Based on the code you've shown, you are saving it with one name, and retrieving it with a different name (LevelTwoUnlocked) vs (LevelTwoButton)

Creating A Numbered List In UITextView Swift 3

I am trying to make a numbered list out of the information the user inputs into a UITextView. For example,
List item one
List item two
List item three
Here is the code that I have tried but does not give me the desired effect.
var currentLine: Int = 1
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// Add "1" when the user starts typing into the text field
if (textView.text.isEmpty && !text.isEmpty) {
textView.text = "\(currentLine). "
currentLine += 1
}
else {
if text.isEmpty {
if textView.text.characters.count >= 4 {
let str = textView.text.substring(from:textView.text.index(textView.text.endIndex, offsetBy: -4))
if str.hasPrefix("\n") {
textView.text = String(textView.text.characters.dropLast(3))
currentLine -= 1
}
}
else if text.isEmpty && textView.text.characters.count == 3 {
textView.text = String(textView.text.characters.dropLast(3))
currentLine = 1
}
}
else {
let str = textView.text.substring(from:textView.text.index(textView.text.endIndex, offsetBy: -1))
if str == "\n" {
textView.text = "\(textView.text!)\(currentLine). "
currentLine += 1
}
}
}
return true
}
as suggested here: How to make auto numbering on UITextview when press return key in swift but had no success.
Any help is much appreciated.
You can subclass UITextView, override method willMove(toSuperview and add an observer for UITextViewTextDidChange with a selector that break up your text into lines enumerating and numbering it accordingly. Try like this:
class NumberedTextView: UITextView {
override func willMove(toSuperview newSuperview: UIView?) {
frame = newSuperview?.frame.insetBy(dx: 50, dy: 80) ?? frame
backgroundColor = .lightGray
NotificationCenter.default.addObserver(self, selector: #selector(textViewDidChange), name: .UITextViewTextDidChange, object: nil)
}
func textViewDidChange(notification: Notification) {
var lines: [String] = []
for (index, line) in text.components(separatedBy: .newlines).enumerated() {
if !line.hasPrefix("\(index.advanced(by: 1))") &&
!line.trimmingCharacters(in: .whitespaces).isEmpty {
lines.append("\(index.advanced(by: 1)). " + line)
} else {
lines.append(line)
}
}
text = lines.joined(separator: "\n")
// this prevents two empty lines at the bottom
if text.hasSuffix("\n\n") {
text = String(text.characters.dropLast())
}
}
}
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let textView = NumberedTextView()
view.addSubview(textView)
}
}

Creating THREE pickerViews for converting the units

Here is the main screen of the app :
I have successfully linked these pickerView to each other, then I have tried to assign the calculation in converting but not to avail :
ex: mile to killo
but I could not find a way to do so. I have tried to use "Switch" still nothing happen. I just need someone can show me how can I convert from a certain unit to another unit through the textFields. For example, if you enter a value in a certain texField the converted result will in the other textField and vise versa.
Here is my Code:
import UIKit
class ViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate, UITextFieldDelegate{
#IBOutlet weak var mainPicker: UIPickerView!
#IBOutlet weak var leftPicker: UIPickerView!
#IBOutlet weak var rightPicker: UIPickerView!
#IBOutlet weak var textFieldLeft: UITextField!
#IBOutlet weak var textFielfRight: UITextField!
#IBOutlet weak var equal: UILabel!
var leftPickerData : [String] = []
var rightPickerData : [String] = []
var dataDict:NSMutableDictionary!
var mainPickerData:NSArray!
var leftRightPickerData:NSArray!
//yourPicker.backgroundColor = UIColor(patternImage: UIImage(named: "back.jpg")!)
override func viewDidLoad() {
super.viewDidLoad()
mainPicker.backgroundColor = .clear
rightPicker.backgroundColor = .clear
leftPicker.backgroundColor = .clear
// Connect data to ViewController ..
self.mainPicker.delegate = self
self.mainPicker.dataSource = self
self.leftPicker.delegate = self
self.leftPicker.dataSource = self
self.rightPicker.delegate = self
self.rightPicker.dataSource = self
self.textFieldLeft.delegate = self
self.textFielfRight.delegate = self
let theWidth = view.frame.size.width
let theHeight = view.frame.size.height
mainPicker.center = CGPoint(x: theWidth/2, y: theHeight/2 - 182.5)
leftPicker.center = CGPoint(x: theWidth/2 - 100, y: theHeight/2)
rightPicker.center = CGPoint(x: theWidth/2 + 100, y: theHeight/2)
textFieldLeft.center = CGPoint(x: theWidth/2 - 90, y: theHeight/2 + 110)
textFielfRight.center = CGPoint(x: theWidth/2 + 90, y: theHeight/2 + 110)
equal.center = CGPoint(x: theWidth/2, y: theHeight/2 + 110)
dataDict = ["Area":["Square Mile", "Square Yard", "Square Foot", "Square Inch", "Hectare", "Acre", "Square Kilometer", "Square Meter", "Square Centimeter", " Square Millimeter"]
,"Energy":["Btus", "Calories", "Ergs", "Foot-Pounds", "Joules", "Kilogram-Calories", "Kilogram-Meters", "Kilowatt-Hours", "Newton-Meters", "Watt-Hours"], "Length":["Mile", "Yard", "Foot", "Inch", "Kilometer", "Meter", "Centimeter", "Millimeter"], "Power": ["Btus/Minute", "Foot-Pounds/Min", "Foot-Pounds/Sec", "Horsepower", "Kilowatts", "Watts"], "Pressure": ["Pounds/Sqr Ft", "Pounds/Sqr In", "Atmospheres", "Bars", "In of Mercury", "Cm of Mercury", "Kilograms/Sqr Meter", "Pascals"], "Speed": ["Knots", "Miles/Hr", "Miles/Min", "Feet/Min", "Feet/Sec", "Kilometers/Hr", "Kilometer/Min", "Meters/Sec"], "Temperature": ["Celsius C˚", "Fahrenheit", "Kelvin"], "Time": ["Years", "Months", "Weeks", "Days", "Hours", "Minutes", "Seconds", "Millisconds", "Microseconds", " Nanoseconds"], "Volume": ["Cupic Feet","Cubic Meter", "Gallon (Imp)", "Gallon (US)", "Quart (US)", "Pint (US)", "Fluid Oz", "Cup", "Tablespoon", "Teaspoon", "Dram (US)", "Liter"], "Weight": ["Short Ton (US)","Long Ton (UK)", "Pound (U.S)", "Ounce (US)", "Stone", "Metric Ton", "Kilogram", "Gram"]]
mainPickerData = dataDict.allKeys as NSArray!;
leftRightPickerData = dataDict.object(forKey: mainPickerData.firstObject as! String) as! NSArray
// Linking the textFields with the pickerViews.
//textFieldLeft.inputView = leftPicker;
// textFielfRight.inputView = rightPicker;
}
// The number of columns of data
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
// The number of rows of data
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
switch (pickerView.tag) {
case mainPicker.tag:
return mainPickerData.count
case leftPicker.tag,rightPicker.tag:
let currentSelectedIndex = mainPicker.selectedRow(inComponent: component)
leftRightPickerData = (dataDict.object(forKey: mainPickerData[currentSelectedIndex] as! String) as! NSArray)
return leftRightPickerData.count;
default:
break;
}
return 0;
}
// The data to return for the row and component (column) that's being passed in
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if leftPicker.tag == 2 {
return leftPickerData[row]
}else if rightPicker.tag == 3{
return rightPickerData[row]
}
return ""
}
// Catpure the picker view selection
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
// This method is triggered whenever the user makes a change to the picker selection.
// The parameter named row and component represents what was selected.
if(pickerView.tag == 1 ){
let currentSelectedIndex = mainPicker.selectedRow(inComponent: component)
leftRightPickerData = (dataDict.object(forKey: mainPickerData[currentSelectedIndex] as! String) as! NSArray)
leftPicker.reloadAllComponents()
rightPicker.reloadAllComponents()
if mainPicker.tag == mainPicker.selectedRow(inComponent: component) {
if leftPicker.tag == leftPicker.selectedRow(inComponent: component) && rightPicker.tag == rightPicker.selectedRow(inComponent: component){
textFieldLeft.text = textFielfRight.text
}
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textFieldLeft.resignFirstResponder()
textFielfRight.resignFirstResponder()
return true
}
func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
let titleData : String?
if(pickerView.tag == mainPicker.tag){
titleData = mainPickerData[row] as? String;
}
else{
let currentSelectedIndex = mainPicker.selectedRow(inComponent: 0)
leftRightPickerData = (dataDict.object(forKey: mainPickerData[currentSelectedIndex] as! String) as! NSArray)
titleData = leftRightPickerData[row] as? String;
}
let myTitle = NSAttributedString(string: titleData!, attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 12.0)!,NSForegroundColorAttributeName:UIColor.blue])
return myTitle;
}
}
I would do something like this as an example for different length values:
You would want to store some sort of array that tracks conversion values for each different unit of measurement, the best universal length unit is meters. It would be a good idea to make sure the corresponding value for each unit stays static, so create an enum:
enum Length: Int {
case mile = 0
case yard = 1
case foot = 2
case inch = 3
case kilometer = 4
// ... keep going with all lenght units of measurement
}
Then you should make your array of values for conversions, this is an array of double's based on the length values I listed in the enum above for conversion to meters:
// Store values with corresponding indicies in array to the Length enum's values
let meterConversions:[Double] = [1609.34, 0.9144, 0.3048, 0.0254, 1,000]
// Store values of length unit descriptions to print after conversion in TextField, long hand or short hand whatever you prefer. (this is optional)
let lengthUnits = ["Mile", "Yard", "Foot", "Inch", "Kilometer"]
Then create some conversion methods:
// Convert length type to meters
func convertToMeters(type: Length, unitValue: Double) -> Double {
return (meterConversions[type.rawValue] * unitValue)
}
// Convert meters back to length type
func convertFromMeters(type: Length, meterValue: Double) -> Double {
return meterValue/meterConversions[type.rawValue]
}
// Convert from length type to other length type
func convertType(from: Length, to: Length, unitValue: Double) -> Double {
// Convert from value to meters to start
let fromValueToMeters:Double = convertToMeters(type: from, unitValue: unitValue)
// Now use that value to convert back to desired unit
let newUnitValue:Double = convertFromMeters(type: to, meterValue: fromValueToMeters)
return newUnitValue
}
Then whenever the user selects a new row in either the left or right UIPickerView, update the calculation and handle it however you wish:
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
// Make sure there is actually some text in the left text field, otherwise return
guard let text = textFieldLeft.text else { print("textFieldLeft doesn't contain any text"); return }
// You are going to need a numerical (Double) value from the user's text
let stringAsDouble = Double(text)
// Now check that the text was actually a numerical value able to be converted to a double
if let value = stringAsDouble {
var typeA:Length!
var typeB:Length!
if pickerView == leftPicker {
typeA = Length(rawValue: row)!
typeB = Length(rawValue: rightPicker.selectedRow(inComponent: 0))!
converted = convertType(from: typeA, to: typeB, unitValue: value)
}
else if pickerView == rightPicker {
typeA = Length(rawValue: leftPicker.selectedRow(inComponent: 0))!
typeB = Length(rawValue: row)!
let value:Double = 0 // Determine user entered value from textField
converted = convertType(from: typeA, to: typeB, unitValue: value)
}
updateValueAfterConversion(originalValue: value, originalType: typeA, convertedValue: converted, convertedType: typeB)
} else {
print("Couldn't convert text to double value")
}
}
func updateValueAfterConversion(originalValue: Double, originalType: Length, convertedValue: Double, convertedType: Length) {
// Update text in both fields, lengthUnits part is optional if you want to print unit along with value.
// Update text on left side
textFieldLeft.text = "\(originalValue) \(lengthUnits[originalType.rawValue])"
// Update text on right side
textFieldRight.text = "\(convertedValue) \(lengthUnits[convertedType.rawValue])"
}
I will give you some tips:
Value in a text field is a String, you should convert it to Double and then multiply by different value between 2 units.
To save these different values, I have an idea: save [1, 1760, 5280, 63360, 1.609344, 1609.344, 160934.4, 1609344] for ["Mile", "Yard", "Foot", "Inch", "Kilometer", "Meter", "Centimeter", "Millimeter"] then do some math caculating.
You should listen to the notification UITextFieldTextDidChange for each UITextField to instantly calculate the equal value.
If you are a lazy developer, I found this for you: https://github.com/michalkonturek/MKUnits
Hope this helps.

UICollectionViewLayout not working as expected

I followed this Custom Collection View Layout tutorial from raywenderlich.com using xcode 8 and swift 3.
When I ran the app after implementing all methods requested in the tutorial, I got the following error:
'no UICollectionViewLayoutAttributes instance for -layoutAttributesForItemAtIndexPath: {length = 2, path = 0 - 11}'
So I have added the following method into my CollectionViewFlowLayout class:
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return self.cache[indexPath.row]
}
This is almost working except that some cells are overlaying existing cells when scrolling down and then disappear. If I scroll up, everything is working perfectly.
I don't understand the full logic yet of this code but I have reviewed and tested it several times and I cannot understand which part of the code is triggering this behaviour.
Any idea?
import UIKit
/* The heights are declared as constants outside of the class so they can be easily referenced elsewhere */
struct UltravisualLayoutConstants {
struct Cell {
/* The height of the non-featured cell */
static let standardHeight: CGFloat = 100
/* The height of the first visible cell */
static let featuredHeight: CGFloat = 280
}
}
class UltravisualLayout: UICollectionViewLayout {
// MARK: Properties and Variables
/* The amount the user needs to scroll before the featured cell changes */
let dragOffset: CGFloat = 180.0
var cache = [UICollectionViewLayoutAttributes]()
/* Returns the item index of the currently featured cell */
var featuredItemIndex: Int {
get {
/* Use max to make sure the featureItemIndex is never < 0 */
return max(0, Int(collectionView!.contentOffset.y / dragOffset))
}
}
/* Returns a value between 0 and 1 that represents how close the next cell is to becoming the featured cell */
var nextItemPercentageOffset: CGFloat {
get {
return (collectionView!.contentOffset.y / dragOffset) - CGFloat(featuredItemIndex)
}
}
/* Returns the width of the collection view */
var width: CGFloat {
get {
return collectionView!.bounds.width
}
}
/* Returns the height of the collection view */
var height: CGFloat {
get {
return collectionView!.bounds.height
}
}
/* Returns the number of items in the collection view */
var numberOfItems: Int {
get {
return collectionView!.numberOfItems(inSection: 0)
}
}
// MARK: UICollectionViewLayout
/* Return the size of all the content in the collection view */
override var collectionViewContentSize : CGSize {
let contentHeight = (CGFloat(numberOfItems) * dragOffset) + (height - dragOffset)
return CGSize(width: width, height: contentHeight)
}
override func prepare() {
let standardHeight = UltravisualLayoutConstants.Cell.standardHeight
let featuredHeight = UltravisualLayoutConstants.Cell.featuredHeight
var frame = CGRect.zero
var y:CGFloat = 0
for item in 0 ... (numberOfItems - 1) {
let indexPath = NSIndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
attributes.zIndex = item
var height = standardHeight
if indexPath.item == featuredItemIndex {
let yOffset = standardHeight * nextItemPercentageOffset
y = self.collectionView!.contentOffset.y - yOffset
height = featuredHeight
} else if item == (featuredItemIndex + 1) && item != numberOfItems {
let maxY = y + standardHeight
height = standardHeight + max((featuredHeight - standardHeight) * nextItemPercentageOffset, 0)
y = maxY - height
}
frame = CGRect(x: 0, y: y, width: width, height: height)
attributes.frame = frame
cache.append(attributes)
y = frame.maxY
}
}
/* Return all attributes in the cache whose frame intersects with the rect passed to the method */
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return self.cache[indexPath.row]
}
/* Return true so that the layout is continuously invalidated as the user scrolls */
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Just added that in the super.viewDidLoad of the view controller owning the UICollection view and it is now working as expected.
This is a quick fix and I believe there are a more appropriate way to fix this managing better the prefetching functions from IOS10.
if #available(iOS 10.0, *) {
collectionView!.prefetchingEnabled = false
}