I have a group of local UIImages that I need to load and present in successive order when their respective cell is tapped. For example, I have 20 images of a hot dog that combine to form an animation. When the user taps the hot dog cell, the cell's UIImageView should animate the images.
I know how to use UIImageView's animationImages to achieve the animation. My problem is that retrieving all of these images from disk takes ~1.5 seconds and blocks the main thread.
I could instantiate a helper class in application(_:didFinishLaunchingWithOptions:) that loads these images from disk on a background thread so that they'll be in memory when needed, but this seems hacky.
Are there any better ways of quickly loading many images from disk?
Edit: These images are illustrations and thus are .png.
Edit2: Assume the sum of each image sequence is 1 MB. The image dimensions I'm testing with are 33-60% larger than the UIImageView's #3x requirements. I am waiting to confirm final UIImageView size before getting correct image sets from our designers, so the time should be cut significantly with properly sized assets, but I'm also testing on a physical iPhone X.
class ViewModel {
func getImages() -> [UIImage] {
var images: [UIImage] = []
for i in 0..<44 {
if let image = UIImage(named: "hotDog\(i).png") {
images.append(image)
}
}
return images
}
}
class ViewController: UIViewController {
private var viewModel: ViewModel!
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! CustomCell
let images = viewModel.getImages()
cell.animateImageView(withImages: images)
}
}
class CustomCell: UITableViewCell {
#IBOutlet weak var imageView: UIImageView!
func animateImageView(withImages images: [UIImage]) {
imageView.image = images.last
imageView.animationImages = images
imageView.animationDuration = TimeInterval(images.count / 20)
imageView.animationRepeatCount = 1
imageView.startAnimating()
}
}
I would suggest you try UIImage(contentsOfFile:) instead of UIImage(named:). In my quick test and found it to be more than an order of magnitude faster. It's somewhat understandable because it's doing a lot more (searching for the asset, cacheing the asset, etc.).
// slow
#IBAction func didTapNamed(_ sender: Any) {
let start = CFAbsoluteTimeGetCurrent()
imageView.animationImages = (0 ..< 20).map {
UIImage(named: filename(for: $0))!
}
imageView.animationDuration = 1.0
imageView.animationRepeatCount = 1
imageView.startAnimating()
print(CFAbsoluteTimeGetCurrent() - start)
}
// faster
#IBAction func didTapBundle(_ sender: Any) {
let start = CFAbsoluteTimeGetCurrent()
let url = Bundle.main.resourceURL!
imageView.animationImages = (0 ..< 20).map {
UIImage(contentsOfFile: url.appendingPathComponent(filename(for: $0)).path)!
}
imageView.animationDuration = 1.0
imageView.animationRepeatCount = 1
imageView.startAnimating()
print(CFAbsoluteTimeGetCurrent() - start)
}
Note, this presumes that you had the files in the resource directory, and you may have to modify this accordingly depending upon where they are in your project. Also note that I avoided doing Bundle.main.url(forResource:withExtension:) within the loop, because even that had an observable impact on performance.
Related
I am currently making a Swift application with multiple UIImage elements. I am at the point where I need to add animations into my game. In order to reduce the projects fie size I have made a single shatter animation for every colored glass object. I have successfully changed the Images tint color for the first image in the animation but not the proceeding images. What is the best way to achieve this with the following code?
shatterImages = createImagesArray(total: 26, imagePrefix: "Shatter")
func createImagesArray(total: Int, imagePrefix: String) -> [UIImage] {
var imageArray: [UIImage] = []
for imageCount in 1..<total {
let imageName = "\(imagePrefix)-\(imageCount).png"
let image = UIImage(named: imageName)!
imageArray.append(image)
}
return imageArray
}
func animate(imageView: UIImageView, images: [UIImage]) {
let templateImage = imageView.image?.withRenderingMode(.alwaysTemplate)
imageView.image = templateImage
imageView.tintColor = UIColor.blue
imageView.animationImages = images
imageView.animationDuration = 0.7
imageView.animationRepeatCount = 1
imageView.startAnimating()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if self.game.correctValue == self.game.rightDiamondValue {
self.animate(imageView: self.rightGlass, images: self.shatterImages)
self.updateLabel()
} else {
self.gameOver()
}
}
Using code found in another post on here, I was able to programmatically draw and erase a subview, including location, width, and height, as shown in func addLoadButton and func removeSubview below. I have also figured out how to programmatically draw a picture on the main View Controller, as shown in func trailerLoadImage below. However, after many hours and attempts, I have tried to programmatically add and remove images into that subview without success.
My end goal is to be able to press three different trailer load type buttons to insert three different images (button 1 loads image 1, button 2 loads image 2, etc.) in a subview located in a specific location on the screen, and to be able to remove the images one at a time (may not be in order put on screen) by tapping on the images with a finger. The subview can be permanent or can be created and removed programmatically (as used below).
What code would I use to insert an image or multiple different images into a subview that has already been created, to remove the image(s) in the reverse order added, and to clear all images out of the subview? If this can’t be done, an acceptable alternative would be the ability to remove the image from the main VC by either tapping on it or pressing a button to clear all added images.
//Class declaration
class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate {
//Boolean to include load type one in calculations
var trailerLoad : Bool = false
var trailerLoadDistanceFromFront = 20
//Boolean to include load type two in calculations
var trailerLoadTwo : Bool = false
var trailerLoadTwoDistanceFromFront = 80
//Boolean to include load type three in calculations
var trailerLoadThree : Bool = false
var trailerLoadThreeDistanceFromFront = 120
var trailerLoadWidth : Int = 0
var trailerLoadX : Int = 0
//Boolean true only when subView on trailer is active
var subViewActive : Bool = false
override func viewDidLoad() {
super.viewDidLoad()
//Picker view data sources and delegates included in here and work fine
}
//Adds subview for loads
#IBAction func addLoadButton(_ sender: Any) {
let trailerLoadView: UIView = UIView(frame: CGRect(x: 252, y: 233, width: 378, height: 100))
trailerLoadView.backgroundColor = .blue
trailerLoadView.alpha = 0.5
trailerLoadView.tag = 100
trailerLoadView.isUserInteractionEnabled = true
self.view.addSubview(trailerLoadView)
subViewActive = true
}
//If subViewActive is true, calls alert to get distance load type one is from front, moves on to insert and position image, changes trailerLoad bool to true
#IBAction func trailerLoadOneButton(_ sender: Any) {
//If subViewActive is true:
//Calls alert to get distance load type one is from front, puts in var trailerLoadDistanceFromFront
//Calls trailerLoadImage() to insert and position load type one image
//Changes bool trailerLoad to true
//If subViewActive is false:
//Calls alert to tell user that they need to click Add Load button (create subview) before adding load types one, two, or three
}
//Add trailer load type one image, scales and positions it relatively accurately in view.
//To be duplicated and modified for load types two and three in the future, with different images (trailerLoadTypeTwoPic and trailerLoadTypeThreePic)
func trailerLoadImage() {
trailerLoadWidth = 378 * 60 / trailerTotalLength
trailerLoadX = 378 * trailerLoadDistanceFromFront / trailerTotalLength
let imageView = UIImageView(frame: CGRect(x: (252 + trailerLoadX), y: (333 - trailerLoadWidth), width: trailerLoadWidth, height: trailerLoadWidth));
let image = UIImage(named: “trailerLoadTypeOnePic”);
imageView.image = image;
self.view.addSubview(imageView)
}
//Calls func removeSubview to remove subview
#IBAction func resetButton(_ sender: Any) {
removeSubview()
}
//Removes subview for loads
#objc func removeSubview(){
subViewActive = false
if let viewWithTag = self.view.viewWithTag(100) {
viewWithTag.removeFromSuperview()
}else{
print("No!")
}
}
}
Thank you very much to anybody that offers assistance or advice.
Don't use tags! Just create variables in global scope for your views
var imageViews = [UIImageView]()
then when you need to add them first append them to your array and then add them to view
imageViews.append(imageView)
view.addSubview(imageView)
Then when you need to remove your all views from their superview, use method removeFromSuperview() for each view in array
imageViews.forEach { $0.removeFromSuperview() }
imageViews.removeAll()
or if you need to remove just one view at specific index
imageViews[index].removeFromSuperview()
imageViews.remove(at: index)
I am trying to make an application function so that when the user presses one of the images(which are buttons) the background will change. My issue is that when the button is pressed the background will not change to the picture. I have been trying to look for answers but have found nothing. Is anyone able to help? https://i.stack.imgur.com/boEiZ.png <- The image is linked here because stack overflow won't allow me to imbed images.
#IBOutlet weak var background: NSImageView!
let grass = NSImage(named :NSImage.Name(rawValue: "wetgrass"))
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
background.image = grass
}
#IBAction func changeGrass(_ sender: Any) {
background.image = grass
}
}
Here is a tested answer try this
#IBAction func btnChangeImageClick(_ sender: UIButton) {
let imageFromButton : UIImage = sender.image(for: UIControlState.normal)!
imgBG.image = imageFromButton
}
Note: i make Any to UIButton in action so we can get image from that sender object
How to approach solving the problem:
Find out if the assignment upon button press is the problem; isolate image-setting from the button callbacks. You did that already by assigning an image in viewDidLoad ✓
Find out if the image is broken.
The image loading could fail:
let grass = NSImage(named: NSImage.Name(rawValue: "wetgrass"))
NSImage.init?(named:) produces a NSImage? aka Optional<NSImage> -- that means, loading the image file with that name could fail. Use this initializer when you're positive that ...
either your Bundle contains an image file named "wetgrass.png" that is copies into the app (check Build Settings > Copy Bundle Resources),
or your Assets asset catalogue contains an entry with the name "wetgrass".
So grass could be nil. And if it is nil, your assignment to background.image will simply remove any previously set image (if there ever was any), resetting the NSImageView to display a boring gray background.
To finally isolate and check if image assignment works, use an image that's guaranteed to exist, like the built-in ones:
let testImage = NSImage(named: .advanced)!
Note I force-unwrap the optional here with confidence since NSImage.Name.advanced is guaranteed to produce an image :)
This should work then:
override func viewDidLoad() {
super.viewDidLoad()
// Show initial image:
background.image = NSImage(named: .advanced)!
}
#IBAction func changeGrass(_ sender: Any) {
// Show grass or another built-in image:
background.image = grass ?? NSImage(named: .computer)!
}
So check your grass != nil
Try this:
#IBAction func changeGrass(_ sender: Any) {
if let myButton = sender as? UIButton, let currentImage = myButton.imageView?.image {
background.image = currentImage
// Animated
UIView.transition(with: background,
duration: 0.75,
options: .transitionCrossDissolve,
animations: {
self.background.image = currentImage
})
}
}
hello there i am trying to figure out what the issue is why i cant get a cross on the game board when a tap is made. the TTT class is the one below the first code and it is called in the main code. i will appreciate if some can help me on this
#IBOutlet var fields: [TTTImageView]!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func fieldTapped (recognizer:UITapGestureRecognizer) {
let tappedField = recognizer.view as! TTTImageView
tappedField.setPlayer(_player: "x.png")
}
func setupField () {
for index in 0 ... fields.count - 1 {
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(fieldTapped))
gestureRecognizer.numberOfTapsRequired = 1
fields[index].addGestureRecognizer(gestureRecognizer)
}
}
import UIKit
class TTTImageView: UIImageView {
var player:String?
var activated:Bool! = false
func setPlayer (_player:String) {
self.player = _player
if activated == false {
if _player == "x.png" {
self.image = UIImage (named: "x.png")
}else{
self.image = UIImage (named: "o.png")
}
activated = true
}
}
}
Make sure when you set fields[index].addGestureRecognizer(gestureRecognizer) to set isUserInteractionEnabled to true.
fields[index].isUserInteractionEnabled = true
Whenever you want to detect user activity (e.g. UITapGestureRecognizer on a UIImageView, you need to set this to true
EDIT:
I also wanted to mention something I noticed after looking at your code again: the method you are using for selectors has been deprecated. If you are using Swift 3 (which I highly recommend you do), you shouldn't call selectors like this: "functionName:" anymore. Now we use selectors like so: #selector(functionName(_:)). You should start updating your code to the most current syntax sooner rather than later.
The code below has a uicolor box when pressed, the box becomes a smaller uicolor box. The problem is that the color box can only be pressed one time. So the box can go from large to small but it cant go from small to big. How can the code be written so when the box is clicked the box goes from big to small or small to big every time.
import UIKit
class ViewController: UIViewController {
let colorview = UIView()
var initialc = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target:self, action:#selector(ViewController.myFunction(_:)))
colorview.userInteractionEnabled = true
colorview.addGestureRecognizer(tapGestureRecognizer)
colorview.translatesAutoresizingMaskIntoConstraints = false
colorview.backgroundColor = UIColor.blueColor()
self.view.addSubview((colorview))
let leadingc = colorview.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor)
let trailingC = colorview.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor)
let topc = colorview.topAnchor.constraintEqualToAnchor(self.view.topAnchor)
let bottomc = colorview.bottomAnchor.constraintEqualToAnchor(self.view.bottomAnchor, constant: -50)
initialc.appendContentsOf([leadingc,trailingC,topc,bottomc])
NSLayoutConstraint.activateConstraints(initialc)
}
func myFunction(sender: AnyObject) {
NSLayoutConstraint.deactivateConstraints(initialc)
let widthc = colorview.widthAnchor.constraintEqualToConstant(100)
let heightc = colorview.heightAnchor.constraintEqualToConstant(100)
let centerxc = colorview.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor)
let centeryc = colorview.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor)
NSLayoutConstraint.activateConstraints([widthc,heightc,centerxc,centeryc])
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
You are resizing the view in an awkward way. Instead of using constraints, don't place them. Tell the view where to go with its frame. If you need the view to animate the change between frames then use UIView.animateViewWithDuration() and inside the function do the frame changes you desire. This keeps transitions smooth and full control in your hands. Constraints are not as easy to implement properly during run time as frames.