Is this Swift syntax efficient? - ios

Would this for loop fire each time the getter is accessed ? Or does Swift cache it 'behind the scenes' ?
var colors: [UIColor] {
get {
var colors = [UIColor]()
let palette = [UIColor.redColor(), UIColor.greenColor(), UIColor.blueColor(), UIColor.orangeColor(), UIColor.purpleColor(), UIColor.yellowColor()]
var paletteIndex = 0
for _ in 0..<photos.count {
colors.append(palette[paletteIndex])
paletteIndex = paletteIndex == (palette.count - 1) ? 0 : ++paletteIndex
}
return colors
}
}
In objective-c a getter like this would be positioned behind a check on the private ivar, so that the ivar is set once and then the ivar returned on subsequent calls.

It fires everytime the getter is called. No way this could be optimized away.
You might want to keep a preset colors array in sync with the photos property, i.e. change it directly when setting the photos.
I also dislike the tight coupling in that place which seems unnecessary. I think it might be worthwhile to refactor this and just return a particular color for a given photo index (which is trivial using the modulo operator).
So my suggestion is to keep the palette as an instance variable, and just make your method return palette[index % palette.count], which will give the correct color immediately.

I will be doing this, as #Eiko suggested
let palette = [UIColor.redColor(), UIColor.greenColor(), UIColor.blueColor(), UIColor.orangeColor(), UIColor.purpleColor(), UIColor.yellowColor()]
override func viewDidLoad() {
super.viewDidLoad()
...
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("AnnotatedPhotoCell", forIndexPath: indexPath) as UICollectionViewCell
cell.contentView.backgroundColor = palette[indexPath.item % palette.count]
return cell
}

You are using computed property which do not actually store a value and the value of a computed named value or a computed property is not stored in memory. It's in the swift programming book

You can use lazy initialisation:
lazy var colors : [UIColor] = {
var colors = [UIColor.redColor]
// blablabla
return colors
}()
Which will be run just once when you try to access the colors the first time. However if the colors need to update throughout your class lifetime i suggest using a function to recapculate them:
func recalcColors() -> [UIColor] {
var colors = [UIColor.redColor]
// blablabla
return colors
}
lazy var colors = recalcColors()
And when you need to update them you can call colors = recalcColors()

From a Swift logic perspective, it will run every time, but that said, there are some circumstances when the optimizer could turn the entire calculation into a constant, in which case it wouldn’t matter.
There are two things standing in the way of that: the palette array, and the photos.count property. In theory, the palette array never changes, but the compiler can’t know this because it doesn’t know that a function like redColor() always returns the same value. So hoist that array creation out of the property.
photos.count there is probably no fixing, assuming photos changes in size dynamically during the course of the program.
But this will give you the same effect and not require the creation of any arrays at all:
struct PaletteCycler {
let palette = [
UIColor.redColor(), UIColor.greenColor(),
UIColor.blueColor(), UIColor.orangeColor(),
UIColor.purpleColor(), UIColor.yellowColor(),
]
subscript(idx: Int)->UIColor {
return palette[idx%palette.count]
}
}
let colors = PaletteCycler()
Since everything is constant there is very little runtime cost to fetching a color. Unlike the original version, this does not create an array every time. Since mod and fetch is pretty efficient, and the palette variable is constant, it should be fast.
By the way, if things weren’t so constant and you really did want an array, you could rewrite your loop using map like so:
let palette = [
UIColor.redColor(), UIColor.greenColor(),
UIColor.blueColor(), UIColor.orangeColor(),
UIColor.purpleColor(), UIColor.yellowColor(),
]
var colors: [UIColor] = {
// Swift 2.0 syntax. For 1.2, write
// indices(photos).map
return photos.indices.map {
palette[$0 % palette.count]
}
}

Related

Set the titles of a UISegmentedControl (built in IB, not code) from enum

I have an enum that I use to describe the subjective accuracy of a quantity.
enum MeasurementAccuracy: Int {
typealias RawValue = Int
case pending = 0
case approximate = 1
case atLeast = 2
case exact = 3
var description: String {
switch self {
case .pending: return "pending"
case .approximate: return "approximate"
case .atLeast: return "at least"
case .exact: return "exact"
}
}
}
(The quantity may relate to a variety of different real-world "objects", for example the measurement of 500 seeds, which might be approximate, or the measurement of 30 seedlings, which might be at least, or the measurement of 12 plants, which might be accurate/exact.)
I decided to use a segmented control in a custom UITableViewCell to allow the user to select the MeasurementAccuracy for their measurement.
It's a UISegmentedControl with 4 segments, built in a storyboard.
I know I can assign headings for each of these segments by:
typing the string into the Attributes Inspector of the storyboard...
writing the titles individually...
segmentedControl.setTitle("pending", forSegmentAt: 0)
I've done some reading:
Swift - Use an enum to map UISegmentedControl
Swift Guide to Map Filter Reduce
Swift enums and UISegmentedControl
I'm guessing my actual problem is that I'm struggling to get my head around the use of advanced loop functions (e.g. .map) on collections.
It's seems I'm making life difficult for myself, however I want to learn how to set the titles efficiently in code, from an enum (or other), so that in future, more complicated multi-use cases can be dealt with simply.
I'm stuck. Any thoughts, hints or solutions?
UPDATE
CUSTOM CELL class:
protocol DelegateForPickerAccuracy: class {
func accuracy(_ selectedSegmentIndex: Int?)
}
class TableViewCell_SegmentedControl: UITableViewCell {
// MARK: - Public Properties
weak var delegateForPickerAccuracy: DelegateForPickerAccuracy?
// MARK: - IB Outlets & Actions
#IBOutlet weak var segmentedControl: UISegmentedControl!
#IBAction func segmentedControlActionChangeValue(_ sender: Any) {
delegateForPickerAccuracy?.accuracy(segmentedControl.selectedSegmentIndex)
}
}
Add CaseIterable to the enum MeasurementAccuracy
enum MeasurementAccuracy: Int, CaseIterable {
case pending = 0, approximate = 1, atLeast = 2, exact = 3
var description: String {
switch self {
case .pending: return "pending"
case .approximate: return "approximate"
case .atLeast: return "at least"
case .exact: return "exact"
}
}
}
//UISegmentedControl created programmatically
And initialize the UISegmentedControl with .allCases property.
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let segmentedControl = UISegmentedControl(items: MeasurementAccuracy.allCases.map { $0.description.capitalized })
view.addSubview(segmentedControl)
//add constraints or set frame for segmentedControl
}
//UISegmentedControl created in storyboard
Or if you have the segment control in the storyboard, enumerate the MeasurementAccuracy.allCases array and insert a segment for each element
override func viewDidLoad() {
super.viewDidLoad()
segmentControl.removeAllSegments()
MeasurementAccuracy.allCases.forEach {
segmentedControl.insertSegment(withTitle: $0.description.capitalized, at: segmentedControl.numberOfSegments, animated: false)
}
}
Thanks to Rajesh for pointing me in the right direction...
To be clear...
Using code, I want to set the titles of an existing segmented control, that is embedded in a custom UITableViewCell, noting specifically that the control and the custom cell are built in IB / storyboard.
The following code works perfectly...
As suggested by Rajesh, adopt CaseIterable protocol in the enum:
enum MeasurementAccuracy: Int, CaseIterable {...}
Then use the .setTitle convenience method within the MeasurementAccuracy.allCases.forEach iteration:
#IBOutlet weak var segmentedControl: UISegmentedControl! {
didSet {
MeasurementAccuracy.allCases.forEach {
segmentedControl.setTitle($0.description, forSegmentAt: $0.rawValue)
}
}
}
Maybe because of CaseIterable protocol, no need for typealias RawValue = Int in the enum, but not certain on that. Please comment if you know otherwise.

How do you Change the UILabel.text property to iterate through a array of strings and not hardcode an index

I have been trying to figure this out for days. I have searched everywhere its very simple but I'm so confused.
import UIKit
class ViewController: UIViewController {
var arrays = ["A", "b", "C", "D", "E", "F"]
#IBOutlet weak var ChangeText: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
ChangeText.text = arrays[0]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func Button() {
ChangeText.text = arrays[1]
}
}
How do you change the text property which is an optional string in a xcode project to iterate through your array of strings without hardcoding the index of the array.
What I want is for the text in the label to change every time I press the button however given the value is Hardcoded it changes once and then that's it.
I've tried a for in loop and my own function, however I keep on getting errors and I don't know where to place my function or loop. So I've gotten rid of it.
Thank you for your patience and help
Just store the currentIndex as a property
import UIKit
class ViewController: UIViewController {
var arrays = ["A", "b", "C", "D", "E", "F"]
var currentIndex = 0
#IBOutlet weak var ChangeText: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
ChangeText.text = arrays[currentIndex]
}
#IBAction func Button() {
currentIndex++
if (currentIndex == arrays.count {
currentIndex = 0
}
ChangeText.text = arrays[currentIndex]
}
}
Set a index variable on top, and give it a value 0.
var index: Int = 0
Inside your func Button() function, increment count like this :
index = index + 1
and then :
if count<arrays.index
{
ChangeText.text = arrays[index]
}
Your question appears to have multiple aspects to it. The loop and not using an index in the answer is a matter of Scope. Variable Scope is global, local, or static. That is a matter of where they exist and when they stop existing. (there are more variable types but I don't think we should go into those here).
Now, the "other" aspect isn't in traditional programming. You're wondering why you can loop through everything but only one shows up. This is basic render-loop. It's for games, graphics, but not taught in language tutorials.
The program runs its code, all of it, and at the end it displays something on screen. So if you loop through everything, like this:
for(string s in array) {
textArea.text = s;
}
it will finish and always display the last one. Because it finished that loop before it rendered. What you want is to not change it until it's already visible.
iOS is basically a game loop. Or a graphics rendering loop. You don't have to worry about it, and you'll never see it. But if you want to know how to work with it, the Views Programming Guide, Animations Programming Guide, and Core Graphics Programming Guide will teach you how they work. At this stage though, I think you should focus more on the core language features.
I hope you've gotten some progress though. And always feel free to print things to the screen to learn

When using Computed variable and Snapkit: No common superview between views

So here's the thing, I'm declaring a property like this:
var aNameLabel: UILabel {
guard let foo = Applicant.sharedInstance.realName else {
return UILabel(text: "获取姓名失败", color: .whiteColor())
}
return UILabel(text: foo, color: .whiteColor())
}
And when I try to add constraint to the aNameLabel after I did someView.addSubView(aNameLabel), the app would crash every time at this constraint-adding thing, and says No common superview between views
However, when I change the variable into a let constant like this:
let aNameLabel = UILabel(text: "Allen", color: .whiteColor())
The constraint will be added with no complaint. Somebody can help me with this?
UPDATE
With the help of #par , I've changed my code into this:
var aNameLabel: UILabel = {
guard let foo = Applicant.sharedInstance.realName else {
return UILabel(text: "BAD", color: .whiteColor())
}
return UILabel(text: foo, color: .whiteColor())
}()
And then the aNameLabel would always be assigned with value "BAD", while actually my guard let is successful. How do I fix this?
The problem is that you are creating a new UILabel every time you access the aNameLabel variable (a computed property function runs every time you access it). Presumably you are doing the same thing for the superview of this view (when you access someView in someView.addSubview() in your example above). If so, that's why there's no common superview and you are crashing.
You should create only one instance of each UIView used by your view controller, so creating a variable as a constant as you've shown is a great approach, or you can use a closure-initializer pattern like so:
var aNameLabel: UILabel = {
return UILabel(...)
}()
Notice in the above example the parentheses after the closing brace. Because it's a closure-initializer it will only be called once just like a let constant.
Often a UIView created with let isn't appropriate because constant properties need to be initialized before init() returns, and if you're creating views in a view controller you won't have a chance to add views until loadView() is called. In this case, declare your UIViews as implicitly-unwrapped optionals. They will be set to nil by init() which meets the initialization requirement, then you can set them to actual views later when viewDidLoad() is called, for example:
class MyViewController: UIViewController {
var someSubview: UIView!
override func viewDidLoad() {
super.viewDidLoad()
someSubview = UIView()
view.addSubview(someSubview)
// now set constraints with SnapKit
}
}

Can I control how many cells stay in memory in a UICollectionView (Swift)?

Is it possible to keep more cells in memory? I am having trouble with scrolling. Could I, for instance, keep 3 screens worth of cells in memory instead of just one?
If so, how could one do that?
Below is a screenshot of some of my cells. It is just 3 labels. They are self-sizing. Maybe that is what is taking so long.
Or it might very well be that I am doing something wrong in my collectionView:cellForItemAtIndexPath:
Here is that code:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let wordCell: ReadArticleCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("wordCell", forIndexPath: indexPath) as ReadArticleCollectionViewCell
wordCell.pronunciationLabelView.hidden = false
wordCell.pronunciationLabelView.textColor = UIColor.blackColor()
wordCell.layer.shadowOpacity = 0
wordCell.layer.shadowRadius = 0
wordCell.layer.shadowColor = UIColor.clearColor().CGColor
wordCell.underlineLabelView.backgroundColor = UIColor.blackColor()
var arrayOfParagraphsOfWordStrings = [[String]]()
for paragraph in self.arrayOfParagraphsOfSentencesOfWordStrings {
arrayOfParagraphsOfWordStrings.append(paragraph.reduce([], +)) //is this my culprit? I am doing this so I can use a 3d array as a 2d array datasource but still later be able to map from the 2d array's index to the corresponding index in the 3d array.
}
if let word = arrayOfParagraphsOfWordStrings[indexPath.section][indexPath.row] as String? {
if let pinyinArrayForWord = self.pinyinArray[indexPath.section][indexPath.row] as String? {
if let pinyin = convertPinyinNumbersToToneMarks(self.pinyinArray[indexPath.section][indexPath.row]) as String? {
wordCell.pronunciationLabelView.text = pinyin
}
else {
wordCell.pronunciationLabelView.text = self.pinyinArray[indexPath.section][indexPath.row]
}
if wordCell.pronunciationLabelView.text == "" {
wordCell.pronunciationLabelView.text = "n/a"
wordCell.pronunciationLabelView.hidden = true
}
if self.pinyinQuantityArray[indexPath.section][indexPath.row] > 1 {
// println(pinyinQuantityArray[indexPath.section][indexPath.row])
wordCell.pronunciationLabelView.textColor = UIColor.purpleColor()
}
}
if word == "Score Paragraph" {
wordCell.wordLabelView.hidden = false
wordCell.pronunciationLabelView.hidden = true
wordCell.pronunciationLabelView.textColor = UIColor.redColor()
}
switch self.wordScoreArray[indexPath.section][indexPath.row] {
case 5...10:
wordCell.pronunciationLabelView.hidden = true
case 1...10:
wordCell.underlineLabelView.backgroundColor = UIColor.blueColor()
case (-10)...(-1):
wordCell.underlineLabelView.backgroundColor = UIColor.greenColor()
default:
wordCell.underlineLabelView.backgroundColor = wordCell.underlineLabelView.backgroundColor
}
if self.wordTouchedArray[indexPath.section][indexPath.row] == true {
// wordCell.underlineLabelView.backgroundColor = UIColor.orangeColor()
// wordCell.layer.shadowOffset = CGSize(width: 10, height: 20)
wordCell.layer.shadowOpacity = 0.75
wordCell.layer.shadowRadius = 6
wordCell.layer.shadowColor = UIColor.yellowColor().CGColor
// wordCell.underlineLabelView.layer.borderColor = UIColor.blackColor().CGColor
// wordCell.underlineLabelView.layer.borderWidth = 0.25
// wordCell.underlineLabelView.layer.shadowColor = UIColor.blackColor().CGColor
// wordCell.underlineLabelView.layer.shadowOffset = CGSize(width: 1, height: 1)
}
if self.wordLookedUpArray[indexPath.section][indexPath.row] == true {
// wordCell.underlineLabelView.backgroundColor = UIColor.blackColor()
wordCell.layer.shadowOpacity = 0.75
wordCell.layer.shadowRadius = 6
wordCell.layer.shadowColor = UIColor.yellowColor().CGColor
}
wordCell.wordLabelView.text = arrayOfParagraphsOfWordStrings[indexPath.section][indexPath.row]
}
return wordCell
}
You should move this code to a ViewDidload or ViewWillAppear (before loading Collection view). You re-call this each time you get your cell to be drawn. It's unnecessary, I suppose.
var arrayOfParagraphsOfWordStrings = [[String]]()
for paragraph in self.arrayOfParagraphsOfSentencesOfWordStrings {
arrayOfParagraphsOfWordStrings.append(paragraph.reduce([], +)) //is this my culprit? I am doing this so I can use a 3d array as a 2d array datasource but still later be able to map from the 2d array's index to the corresponding index in the 3d array.
}
Could I, for instance, keep 3 screens worth of cells in memory instead of just one?
There is no point to keep more than a single screen worth of cells in memory, because they are rendered one screen at a time. Cells represent a view in MVC structure, so you need to create only as many as you need.
On the other hand, keeping the data for your cells in memory makes perfect sense. Data represents a model in MVC; that is where the caching should happen.
Right now I can only scroll less than an inch before major lagging starts.
This happens because of the code in collectionView:cellForItemAtIndexPath: needs to access data that has not been cached in your data source. To fix this problem split the code that prepares your cells into two parts: one part that fetches and caches the data and prepares everything that you need to make a UICollectionViewCell without actually making it, and another part that takes a possibly recycled UICollectionViewCell, quickly pairs it up with the data from the cache, and sends it on its way to UICollectionView.
EDIT : (in response to the edit of the question)
var arrayOfParagraphsOfWordStrings = [[String]]()
for paragraph in self.arrayOfParagraphsOfSentencesOfWordStrings {
arrayOfParagraphsOfWordStrings.append(paragraph.reduce([], +))
}
is this my culprit? I am doing this so I can use a 3d array as a 2d array datasource but still later be able to map from the 2d array's index to the corresponding index in the 3d array.
Yes, that is the problem. You perform this conversion for every single cell, and then you take a single word from it. You should refactor your code to move this specific part into some other place, store arrayOfParagraphsOfWordStrings in an instance variable, and use it in your collectionView:cellForItemAtIndexPath: code without rebuilding it for each cell.

break strong reference cycle between Managed Object and Collectionview Cell

Update :
Had nothing to do with Core Data or the CollectionView. I never dismissed the ViewController holding the CollectionView. More in the answer below!
I am having a memory leak in Swift iOS. First I thought it was located in my fetch function, but then I tried something different. I broke the connection between my UICollectionViewCell and my Managed Object at various places. I am fetching images. So I replaced the result from my fetch with a random image asset that was added to an array for as many times as there were results. This always fixed my leak. So now I knew it wasn't a problem in my fetch function, any other function on the way to the Cell.
After some googling I found that Core Data has some tendency to create strong reference cycles on it's own. But I don't think that that is it. If it was, it shouldn't matter that I don't use the resulting array of images in a Cell. I should still have a leak, but when I don't connect the images from Core Data to the Cell I have no leak.
What I don't understand is why I have a leak in the first place. I thought that arrays work like values, not references. So putting images loaded from core data in an array should work like a copy and there shouldn't be any strong reference cycle...
I also found that refreshing the Managed Object that has the Binary Data atribute for the images doesn't fix the problem.
So what do I do now? Do I need to delete all the cells? Do I need to make UIImages out of the NSData from Core Data (glanced over something that said that Strings work like values but NSStrings don't, so maybe an NSData thingy works like a reference)? Do I need to find a way to refresh the attribute of an object?...
Thanks!
Fetch(also tried setting things as weak, doesn't work):
import UIKit
import CoreData
func getFilteredThumbImages (albumIdentifier: String, moc : NSManagedObjectContext?) -> [NSData]? {
var error: NSError?
let resultPredicate = NSPredicate(format: "iD = %#", albumIdentifier)
let albumfetchRequest = NSFetchRequest(entityName: "Album")
albumfetchRequest.predicate = resultPredicate
var results = moc!.executeFetchRequest(albumfetchRequest, error:&error)!
if error == nil {
weak var tempFThumbs = results.last!.mutableSetValueForKey("filteredThumbs")
weak var testFoundImages = tempFThumbs!.mutableSetValueForKey("imageData")
var foundImages = testFoundImages!.allObjects as [NSData]
moc!.refreshObject(results.last! as NSManagedObject, mergeChanges: false)
moc!.reset()
return foundImages
}
else {
moc!.refreshObject(results.last! as NSManagedObject, mergeChanges: false)
//appDel?.managedObjectContext?.reset()
moc!.reset()
return nil
}
}
UICollectionViewCell :
import UIKit
class CollectionViewCell: UICollectionViewCell {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
let textLabel: UILabel!
let imageView: UIImageView!
override init(frame: CGRect) {
NSLog("MyObject init")
super.init(frame: frame)
imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
imageView.backgroundColor = UIColor(red:1, green:0.992, blue:0.965, alpha:1)
imageView.contentMode = UIViewContentMode.ScaleAspectFit
imageView.clipsToBounds = true
contentView.backgroundColor = UIColor(red:1, green:0.992, blue:0.965, alpha:1)
contentView.addSubview(imageView)
}
}
Input images in Cell(thumbs is a variable array => [NSData]):
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
println("loading...")
weak var cell = collectionView.dequeueReusableCellWithReuseIdentifier("CollectionViewCell", forIndexPath: indexPath) as? CollectionViewCell
//cell!.imageView?.image = UIImage(named: "test")
if thumbs != nil {
cell!.imageView?.image = UIImage(data: thumbs![indexPath.row])
println("loading \(thumbs!.count) cells")
}
return cell!
}
The problem is that you've got a cycle in your storyboard. You are pushing from view controller A to view controller B, and then pushing from view controller B "back" to view controller A. And so on, every time. Thus, your navigation controller just piles up numerous copies of the same view controllers. And so you wind up with numerous copies of all those images.
It always surprises me that you don't get a notice when you do this sort of thing. It's a classic mistake, and all too easy to make. Surely Interface Builder could detect this kind of cycle and warn you. But it doesn't...

Resources