I would like to know how I can loop through an array and change a name after recognizer.state .ended. When I use forEach method it shows me only the last item from the array.
Array:
let persons = [
Name(name: "Theo", imageName: "theoPhoto"),
Name(name: "Matt", imageName: "mattPhoto"),
Name(name: "Tom", imageName: "tomPhoto")
]
UIPanGestureRecognizer:
#IBAction func handleCard(recognizer: UIPanGestureRecognizer) {
guard let card = recognizer.view else { return }
switch recognizer.state {
case .began:
initialCGRect = card.frame
case .changed:
handleChanged(recognizer, card)
case .ended:
handleEnded(recognizer, card)
default:
return
}
}
fileprivate func handleChanged(_ recognizer: UIPanGestureRecognizer, _ card: UIView) {
let panning = recognizer.translation(in: view)
let degrees : CGFloat = panning.x / 20
let angle = degrees * .pi / 180
let rotationTransformation = CGAffineTransform(rotationAngle: angle)
card.transform = rotationTransformation.translatedBy(x: panning.x, y: panning.y)
}
fileprivate func handleEnded(_ recognizer: UIPanGestureRecognizer, _ card: UIView) {
let transitionDirection: CGFloat = recognizer.translation(in: view).x > 0 ? 1 : -1
let shuldDismissCard = abs(recognizer.translation(in: view).x) > treshold
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
if shuldDismissCard {
card.frame = CGRect(x: 600 * transitionDirection, y: 0, width: card.frame.width, height: card.frame.height)
} else {
card.transform = CGAffineTransform.identity
}
}) { (_) in
// Complete animation, bringing card back
card.transform = CGAffineTransform.identity
}
}
forEach method:
func setupCards() {
persons.forEach { (Person) in
cardView.image = UIImage(named: Person.imageName)
cardLabel.text = Person.name
}
}
If I put it on complete animation it's looping through an array and shows me the last item.
Your setupCards() method is wrong. It will always display the last item because you're updating only a single cardView. Also don't use the Person class name in the loop closure. You should hold a CardView array, and change the data accordingly.
func setupCards() {
var i = 0
persons.forEach { (p) in
let cardView = self.cards[i]
cardView.image = UIImage(named: p.imageName)
cardView.cardLabel.text = p.name
i += 1
}
}
Related
This is my data structure
struct SPPWorkout: Codable {
static let setKey = "Sets"
static let exerciseID = "id"
var id: Double? = 0.0
var duration: String?
var calories: Int?
var date: String?
var exercises: [ExerciseSet]
[...]
}
struct ExerciseSet: Codable {
let id: String
let name: String
var reps: Int
var weight: Double
[...]
}
extension ExerciseSet: Equatable {
static func ==(lhs: ExerciseSet, rhs: ExerciseSet) -> Bool {
return lhs.id == rhs.id
}
}
and in a SwiftUI view I'm trying to modify an ExerciseSet from user input
#State private var sppWorkout: SPPWorkout!
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ?? 0.0
self.editingIndex = nil
})
}
The issue is here
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ??
and I've tried in all ways to update it, both from the view and with a func in SPPWorkout. I've also tried to replace the object at index
var newSet = ExerciseSet(id: [...], newValues)
self.exercises[editingIndex] = newSet
but in no way it wants to update. I'm sure that somewhere it creates a copy that it edits but I have no idea why and how to set the new values.
Edit: if I try to delete something, it's fine
sppWorkout.exercises.removeAll(where: { $0 == sppWorkout.exercises[index]})
Edit 2:
It passes the guard statement and it does not change the values in the array.
Edit 3:
At the suggestion below from Jared, I've copied the existing array into a new one, set the new values then tried to assign the new one over to the original one but still, it does not overwrite.
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
print(sppWorkout.exercises)
guard let editingIndex = editingIndex else { return }
var copyOfTheArray = sppWorkout.exercises
copyOfTheArray[editingIndex].reps = Int(reps) ?? 0
copyOfTheArray[editingIndex].weight = Double(weight) ?? 0.0
//Copy of the array is updated correctly, it has the new values
sppWorkout.exercises = copyOfTheArray
//Original array doesn't get overwritten. It still has old values
self.editingIndex = nil
Edit 4: I've managed to make progress by extracting the model into a view model and updating the values there. Now the values get updated in sppWorkout, but even though I call objectWillChange.send(), the UI Update doesn't trigger.
full code:
class WorkoutDetailsViewModel: ObservableObject {
var workoutID: String!
#Published var sppWorkout: SPPWorkout!
func setupData(with workoutID: String) {
sppWorkout = FileIOManager.readWorkout(with: workoutID)
}
func update(_ index: Int, newReps: Int, newWeight: Double) {
let oldOne = sppWorkout.exercises[index]
let update = ExerciseSet(id: oldOne.id, name: oldOne.name, reps: newReps, weight: newWeight)
sppWorkout.exercises[index] = update
self.objectWillChange.send()
}
}
struct WorkoutDetailsView: View {
var workoutID: String!
#StateObject private var viewModel = WorkoutDetailsViewModel()
var workout: HKWorkout
var dateFormatter: DateFormatter
#State private var offset = 0
#State private var isShowingOverlay = false
#State private var editingIndex: Int?
#EnvironmentObject var settingsManager: SettingsManager
#Environment(\.dismiss) private var dismiss
var body: some View {
if viewModel.sppWorkout != nil {
VStack {
ListWorkoutItem(workout: workout, dateFormatter: dateFormatter)
.padding([.leading, .trailing], 10.0)
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
let index = viewModel.sppWorkout.exercises.firstIndex(of: exercise) ?? 0
DetailListSetItem(exerciseSet: viewModel.sppWorkout.exercises[index], set: index + 1)
.environmentObject(settingsManager)
.swipeActions {
Button(role: .destructive, action: {
viewModel.sppWorkout.exercises.removeAll(where: { $0 == viewModel.sppWorkout.exercises[index]})
} ) {
Label("Delete", systemImage: "trash")
}
Button(role: .none, action: {
isShowingOverlay = true
editingIndex = index
} ) {
Label("Edit", systemImage: "pencil")
}.tint(.blue)
}
}
.padding([.leading, .trailing], -30)
//iOS 16 .scrollContentBackground(.hidden)
}
.overlay(alignment: .bottom, content: {
editOverlay
.animation(.easeInOut (duration: 0.5), value: isShowingOverlay)
})
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
do {
try FileIOManager.write(viewModel.sppWorkout, toDocumentNamed: "\(viewModel.sppWorkout.id ?? 0).json")
} catch {
Debugger.log(error: error.localizedDescription)
}
dismiss()
}){
Image(systemName: "arrow.left")
})
} else {
Text("No workout details found")
.italic()
.fontWeight(.bold)
.font(.system(size: 35))
.onAppear(perform: {
viewModel.setupData(with: workoutID)
})
}
}
#ViewBuilder private var editOverlay: some View {
if isShowingOverlay {
ZStack {
Button {
isShowingOverlay = false
} label: {
Color.clear
}
.edgesIgnoringSafeArea(.all)
VStack{
Spacer()
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
print(viewModel.sppWorkout.exercises)
print("dupa aia:\n")
viewModel.update(editingIndex, newReps: Int(reps) ?? 0, newWeight: Double(weight) ?? 0.0)
print(viewModel.sppWorkout.exercises)
self.editingIndex = nil
})
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color("popupBackground"),
lineWidth: 3)
)
}
}
}
}
}
So I got a very good explanation on reddit on what causes the problem. Thank you u/neddy-seagoon if you are reading this.
The explanation
. I believe that updating an array will not trigger a state update. The only thing that will, with an array, is if the count changes. So
sppWorkout.exercises[index].reps = newReps
will not cause a trigger. This is not changing viewModel.sppWorkout.exercises.indices
So all I had to to was modify my List from
List(viewModel.sppWorkout.exercises, id: \.id)
to
List(viewModel.sppWorkout.exercises, id: \.hashValue)
as this triggers the list update because the hashValue does change when updating the properties of the entries in the list.
For the line
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
Replace with
List(viewModel.sppWorkout.exercises, id: \.self) { exercise in
I am very new to Swift and coding in general and I have a question about accessing specific items in an array. I want to only show two items(creatures) from the creatures array in my view instead of returning all of the items. How would I go about doing this?
Here is the view where I want to show only two items(creatures):
struct PlayingCreatures: View {
#EnvironmentObject var data : CreatureZoo
var body: some View {
// Want to only show 2 animals on this screen
VStack {
ZStack {
ForEach(data.creatures) {creature in
Text(creature.emoji)
.resizableFont()
.offset(creature.offset)
.rotationEffect(creature.rotation)
.animation(.spring(response: 0.5, dampingFraction: 0.5), value: creature.rotation)
.animation(.default.delay(data.indexFor(creature) / 10), value: creature.offset)
}
}
.onTapGesture {
data.randomizeOffsets()
}
}
}
}
Here is the view where the data is stored:
class CreatureZoo : ObservableObject {
#Published var creatures = [
Creature(name: "Gorilla", emoji: "🦍"),
Creature(name: "Peacock", emoji: "🦚"),
Creature(name: "Squid", emoji: "🦑"),
Creature(name: "T-Rexxx", emoji: "🦖"),
Creature(name: "Sloth", emoji: "🦥")
]
}
struct Creature : Identifiable {
var name : String
var emoji : String
var id = UUID()
var offset = CGSize.zero
var rotation : Angle = Angle(degrees: 0)
// Should be hidden probably
extension CreatureZoo {
func randomizeOffsets() {
for index in creatures.indices {
creatures[index].offset = CGSize(width: CGFloat.random(in: -200...200), height: CGFloat.random(in: -200...200))
creatures[index].rotation = Angle(degrees: Double.random(in: 0...720))
}
}
func synchronizeOffsets() {
let randomOffset = CGSize(width: CGFloat.random(in: -200...200), height: CGFloat.random(in: -200...200))
for index in creatures.indices {
creatures[index].offset = randomOffset
}
}
func indexFor(_ creature: Creature) -> Double {
if let index = creatures.firstIndex(where: { $0.id == creature.id }) {
return Double(index)
}
return 0.0
}
}
Use prefix
ForEach(Array(data.creatures.prefix(2))) {creature in
This code works perfectly fine, but I am assuming there is a much more efficient way of doing things like this. I am providing the first four items of an array of strings (this list can contain an unlimited amount of variables) to the user as a "snapshot" view. The code below is inside of a custom cell class.
var tray: Tray? {
didSet {
guard let trayTitle = tray?.title else { return }
guard let trayItems = tray?.items else { return }
// tray.items will give an array of strings
trayTitleLabel.text = trayTitle
if trayItems.count == 1 {
firstSnapshotLabel.text = trayItems[0]
secondSnapshotLabel.text = ""
thirdSnapshotLabel.text = ""
fourthSnapshotLabel.text = ""
} else if trayItems.count == 2 {
firstSnapshotLabel.text = trayItems[0]
secondSnapshotLabel.text = trayItems[1]
thirdSnapshotLabel.text = ""
fourthSnapshotLabel.text = ""
} else if trayItems.count == 3 {
firstSnapshotLabel.text = trayItems[0]
secondSnapshotLabel.text = trayItems[1]
thirdSnapshotLabel.text = trayItems[2]
fourthSnapshotLabel.text = ""
} else if trayItems.count >= 4 {
firstSnapshotLabel.text = trayItems[0]
secondSnapshotLabel.text = trayItems[1]
thirdSnapshotLabel.text = trayItems[2]
fourthSnapshotLabel.text = trayItems[3]
} else {
firstSnapshotLabel.text = ""
secondSnapshotLabel.text = ""
thirdSnapshotLabel.text = ""
fourthSnapshotLabel.text = ""
}
I then manually set up each label:
let firstSnapshotLabel: UILabel = {
let label = UILabel()
label.attributes(text:"Item 1", textColor: Colors.appDarkGrey, alignment: .left, font: Fonts.rubikRegular, size: 12, characterSpacing: 0.5, backgroundColor: nil)
return label
}()
let secondSnapshotLabel: UILabel = {
let label = UILabel()
label.attributes(text:"Item 2", textColor: Colors.appDarkGrey, alignment: .left, font: Fonts.rubikRegular, size: 12, characterSpacing: 0.5, backgroundColor: nil)
return label
}()
let thirdSnapshotLabel: UILabel = {
let label = UILabel()
label.attributes(text:"Item 3", textColor: Colors.appDarkGrey, alignment: .left, font: Fonts.rubikRegular, size: 12, characterSpacing: 0.5, backgroundColor: nil)
return label
}()
let fourthSnapshotLabel: UILabel = {
let label = UILabel()
label.attributes(text:"Item 4", textColor: Colors.appDarkGrey, alignment: .left, font: Fonts.rubikRegular, size: 12, characterSpacing: 0.5, backgroundColor: nil)
return label
}()
As I said, the code above gives me what I wanted, but it is extremely messy and inefficient.
Here is something else I've tried that didn't quite work. I am assuming that the "snapshotLabels" were loaded into the view before the "snapshots" array was populated. I assume this because when I print the "snapshots" I get the populated array, but when I print the "snapshotLabels" I get an empty array.
var tray: Tray? {
didSet {
guard let trayTitle = tray?.title else { return }
guard let trayItems = tray?.items else { return }
// tray.items will give an array of strings
trayTitleLabel.text = trayTitle
for item in trayItems {
snapshots.append(item)
}
}
}
var snapshots = [] as [String]
lazy var snapshotLabels = snapshots.prefix(4).map { item -> UILabel in
let label = UILabel()
label.attributes(text:"\(item)", textColor: Colors.appDarkGrey, alignment: .left, font: Fonts.rubikRegular, size: 12, characterSpacing: 0.5, backgroundColor: nil)
return label
}
Tray struct and array of instances:
struct Tray {
let title: String
let items: [String]
}
let trays = [Tray(title: "Groceries", items: ["Apples","Water","Milk","Eggs","Fruity Pebbles","Bread","Butter","Flour","Yogurt","Pop-Tarts"]),
Tray(title: "Home", items: ["Laundry","Trash","Clean desk"]),
Tray(title: "Miscellaneous", items: ["Call mom","Feed dogs"]),
Tray(title: "Bucket List", items: ["Skydiving","England","Pilot's license","Learn spanish","Barcelona","Italy"]),
Tray(title: "Dinner", items: ["Pasta","Crabs","Tacos","Pizza","Steak","Cheesesteaks","Chicken and Rice"])
]
Does anyone know why this is happening?
Thanks! :D
Make your labels an array
let labels = (0..<4).map {
let label = UILabel()
label.attributes(text:"Item \($0)", textColor: Colors.appDarkGrey, alignment: .left, font: Fonts.rubikRegular, size: 12, characterSpacing: 0.5, backgroundColor: nil)
return label
}
then iterate over your tray items to set the value on the corresponding label
(0..<trayItems.count).forEach { labels[$0].text = trayItems[$0] }
(trayItems.count..<labels.count).forEach { $0.text = "" }
I have a global array called cannonArray that I initialized before any function. I have a touchesBegan function that appends values to the array. When I print the array from within the touchesBegan function, it prints perfectly. However, when I print the array from the didMoveToViewFunction, nothing updates and it simply prints [] (and doesn't change as it should because it is global). I also have another function called createEnemies that is on a timer and it has the same problem (prints proper appended array within the function, but not in another one). Thanks! :)
var cannonArray = [SKSpriteNode]()
var enemyArray = [SKSpriteNode]()
override func didMove(to view: SKView) {
enemyCreationTimer = Timer.scheduledTimer(timeInterval: 0.75, target: self, selector: #selector(createEnemies), userInfo: nil, repeats: true)
print(enemyArray) //prints empty array
print(cannonArray) // prints empty array
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: self)
cannon = SKSpriteNode(imageNamed: "cannon")
cannon.size = CGSize(width: 50, height: 50)
cannon.position = location
if frameArray[0].contains(location) || frameArray[1].contains(location) {
print("cant")
}else {
self.addChild(cannon)
self.cannonArray.append(cannon)
//if i print here it works
}
}
#objc func createEnemies() {
enemiesSpawned += 1
if enemiesSpawned > enemyNumber {
enemyCreationTimer.invalidate()
}else {
enemy = SKSpriteNode(imageNamed: "enemy")
enemy.position = CGPoint(x: spriteOne.position.x, y: CGFloat(650))
enemy.size = CGSize(width: 25, height: 25)
self.addChild(enemy)
enemy.zPosition = 0
enemy.physicsBody = SKPhysicsBody(circleOfRadius: 12.5)
enemy.physicsBody?.affectedByGravity = false
enemy.run(moveEnemies())
enemyArray.append(enemy)
//if i print here it works
}
}
Im grabbing the photos from library using a cocoa pod called BSImagePicker. The Images are then stored into a photoArray. I want to take those images from the array and present them in an image view that's held inside the scrollView. However the images aren't showing up at all.
This is what I've currently tried.
This is the code that the cocoa pod uses to store images into the array:
func openImagePicker() {
let vc = BSImagePickerViewController()
self.bs_presentImagePickerController(
vc,
animated: true,
select: { (assest: PHAsset) -> Void in },
deselect: { (assest: PHAsset) -> Void in },
cancel: { (assest: [PHAsset]) -> Void in },
finish: { (assest: [PHAsset]) -> Void in
for i in 0..<assest.count {
self.SelectedAssets.append(assest[i])
}
self.convertAssetToImages()
},
completion: nil
)
print(photoArray)
setupImages(photoArray)
}
this is the code that converts asset to image
extension addImages {
func convertAssetToImages() -> Void {
if SelectedAssets.count != 0{
for i in 0..<SelectedAssets.count{
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
var thumbnail = UIImage()
option.isSynchronous = true
let width = scrollView.frame.width
let height = scrollView.frame.height
manager.requestImage(for: SelectedAssets[i], targetSize: CGSize(width: width, height: height), contentMode: .aspectFill, options: option, resultHandler: {(result,info) -> Void in
thumbnail = result!
})
let data = thumbnail.jpegData(compressionQuality: 1)
let newImage = UIImage(data: data! as Data)
self.photoArray.append(newImage! as UIImage)
}
}
}
}
this code takes image from photo array into scrollview
func setupImages(_ images: [UIImage]){
for a in 0..<photoArray.count {
let theImage = UIImageView()
theImage.image = self.photoArray[a]
let xPosition = UIScreen.main.bounds.width * CGFloat(a)
self.scrollView.frame = CGRect(x: xPosition, y: 0, width: self.scrollView.frame.width, height: self.scrollView.frame.height)
theImage.contentMode = .scaleAspectFit
self.scrollView.contentSize.width = self.scrollView.frame.width * CGFloat(a + 1)
self.scrollView.addSubview(theImage)
}
}
I have no idea why it isn't working.