ios effective coding: run loops & ui

21
iOS Effective Coding Session #1 - Run Loops & UI J.M. Schaeffer

Upload: jm-schaeffer

Post on 20-Jan-2017

45 views

Category:

Mobile


3 download

TRANSCRIPT

Page 1: iOS Effective Coding: Run Loops & UI

iOS Effective Coding Session #1 - Run Loops & UI

J.M. Schaeffer

Page 2: iOS Effective Coding: Run Loops & UI

Sample Codefinal class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 static let timeInterval: TimeInterval = 0.2 func startAnimation() { Timer.scheduledTimer( timeInterval: type(of: self).timeInterval,

target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true)

} // MARK: - Timer func bounceAndIncrement(timer: Timer) { bounce(imagesView.subviews[animationIndex]) animationIndex = (animationIndex + 1) % imagesView.subviews.count } private func bounce(_ view: UIView) { … } }

Page 3: iOS Effective Coding: Run Loops & UI

Sample Codefinal class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 static let timeInterval: TimeInterval = 0.2 func startAnimation() { Timer.scheduledTimer( timeInterval: type(of: self).timeInterval,

target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true)

} // MARK: - Timer func bounceAndIncrement(timer: Timer) { bounce(imagesView.subviews[animationIndex]) animationIndex = (animationIndex + 1) % imagesView.subviews.count } private func bounce(_ view: UIView) { … } }

Page 4: iOS Effective Coding: Run Loops & UI

Chaotic Animation After Scrollingfinal class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 static let timeInterval: TimeInterval = 0.2 func startAnimation() { Timer.scheduledTimer( timeInterval: type(of: self).timeInterval,

target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true)

} // MARK: - Timer func bounceAndIncrement(timer: Timer) { bounce(imagesView.subviews[animationIndex]) animationIndex = (animationIndex + 1) % imagesView.subviews.count } private func bounce(_ view: UIView) { … } }

Page 5: iOS Effective Coding: Run Loops & UI

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { timer?.invalidate() let timer = Timer.scheduledTimer( timeInterval: type(of: self).timeInterval,

target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true)

self.timer = timer } // MARK: - Timer func bounceAndIncrement(timer: Timer) { bounce(imagesView.subviews[animationIndex]) animationIndex = (animationIndex + 1) % imagesView.subviews.count } private func bounce(_ view: UIView) { … }

Chaotic Animation After Scrolling

Page 6: iOS Effective Coding: Run Loops & UI

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { timer?.invalidate() let timer = Timer.scheduledTimer( timeInterval: type(of: self).timeInterval,

target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true)

self.timer = timer } // MARK: - Timer func bounceAndIncrement(timer: Timer) { bounce(imagesView.subviews[animationIndex]) animationIndex = (animationIndex + 1) % imagesView.subviews.count } private func bounce(_ view: UIView) { … }

Chaotic Animation After Scrolling

Page 7: iOS Effective Coding: Run Loops & UI

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { timer?.invalidate() let timer = Timer.scheduledTimer( timeInterval: type(of: self).timeInterval,

target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true)

self.timer = timer } // MARK: - Timer func bounceAndIncrement(timer: Timer) { bounce(imagesView.subviews[animationIndex]) animationIndex = (animationIndex + 1) % imagesView.subviews.count } private func bounce(_ view: UIView) { … }

Timers: strong OR weak?TimerTimers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see NSRunLoop and Threading Programming Guide. Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

Page 8: iOS Effective Coding: Run Loops & UI

Timer: strong OR weak?final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { timer?.invalidate() let timer = Timer.scheduledTimer( timeInterval: type(of: self).timeInterval,

target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true)

self.timer = timer } // MARK: - Timer func bounceAndIncrement(timer: Timer) { bounce(imagesView.subviews[animationIndex]) animationIndex = (animationIndex + 1) % imagesView.subviews.count } private func bounce(_ view: UIView) { … }

TimerTimers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see NSRunLoop and Threading Programming Guide. Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

Page 9: iOS Effective Coding: Run Loops & UI

TimerTimers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see NSRunLoop and Threading Programming Guide. Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { timer?.invalidate() let timer = Timer.scheduledTimer( timeInterval: type(of: self).timeInterval,

target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true)

self.timer = timer } // MARK: - Timer func bounceAndIncrement(timer: Timer) { bounce(imagesView.subviews[animationIndex]) animationIndex = (animationIndex + 1) % imagesView.subviews.count } private func bounce(_ view: UIView) { … }

Once scheduled on a run loop, the timer fires at the specified interval until it is invalidated. A non-repeating timer invalidates itself immediately after it fires. However, for a repeating timer, you must invalidate the timer object yourself by calling its invalidate method. Calling this method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate method from the same thread on which the timer was installed. Invalidating the timer immediately disables it so that it no longer affects the run loop. The run loop then removes the timer (and the strong reference it had to the timer), either just before the invalidate method returns or at some later point. Once invalidated, timer objects cannot be reused.

Memory Leak

Page 10: iOS Effective Coding: Run Loops & UI

TimerTimers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see NSRunLoop and Threading Programming Guide. Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { timer?.invalidate() let timer = Timer.scheduledTimer( timeInterval: type(of: self).timeInterval,

target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true)

self.timer = timer }

// MARK: - UIView override func removeFromSuperview() { super.removeFromSuperview() timer?.invalidate() } // MARK: - Timer func bounceAndIncrement(timer: Timer) { bounce(imagesView.subviews[animationIndex])

Once scheduled on a run loop, the timer fires at the specified interval until it is invalidated. A non-repeating timer invalidates itself immediately after it fires. However, for a repeating timer, you must invalidate the timer object yourself by calling its invalidate method. Calling this method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate method from the same thread on which the timer was installed. Invalidating the timer immediately disables it so that it no longer affects the run loop. The run loop then removes the timer (and the strong reference it had to the timer), either just before the invalidate method returns or at some later point. Once invalidated, timer objects cannot be reused.

Memory Leak

Page 11: iOS Effective Coding: Run Loops & UI

Animation Freezing when ScrollingTimerA timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. […] If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.

Timers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see RunLoop and Threading Programming Guide.

+ timerWithTimeInterval:target:selector:userInfo:repeats: Creates and returns a new Timer object initialized with the specified object and selector.DiscussionYou must add the new timer to a run loop, using addTimer:forMode:. Then, after seconds seconds have elapsed, the timer fires, sending the message aSelector to target. (If the timer is configured to repeat, there is no need to subsequently re-add the timer to the run loop.)

+ scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: Creates and returns a new Timer object and schedules it on the current run loop in the default mode.

Run LoopsRun Loop ModesDefault.defaultModeThe default mode is the one used for most operations. Most of the time, you should use this mode to start your run loop and configure your input sources.

Event tracking.eventTrackingModeCocoa uses this mode to restrict incoming events during mouse-dragging loops and other sorts of user interface tracking loops.

Common modes.commonModesThis is a configurable group of commonly used modes. Associating an input source with this mode also associates it with each of the modes in the group. For Cocoa applications, this set includes the default, modal, and event tracking modes by default. Core Foundation includes just the default mode initially.最BEST

PRACTICE

Page 12: iOS Effective Coding: Run Loops & UI

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { timer?.invalidate() let timer = Timer.scheduledTimer( timeInterval: type(of: self).timeInterval,

target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true)

self.timer = timer }

// MARK: - UIView override func removeFromSuperview() { super.removeFromSuperview() timer?.invalidate() } // MARK: - Timer func bounceAndIncrement(timer: Timer) { bounce(imagesView.subviews[animationIndex])

Animation Freezing when Scrolling

Page 13: iOS Effective Coding: Run Loops & UI

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { timer?.invalidate() let timer = Timer( timeInterval: type(of: self).timeInterval, target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true) self.timer = timer RunLoop.current.add(timer, forMode: .commonModes) }

// MARK: - UIView override func removeFromSuperview() { super.removeFromSuperview() timer?.invalidate() } // MARK: - Timer

Animation Freezing when Scrolling

Page 14: iOS Effective Coding: Run Loops & UI

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { timer?.invalidate() let timer = Timer( timeInterval: type(of: self).timeInterval, target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true) self.timer = timer RunLoop.current.add(timer, forMode: .commonModes) }

// MARK: - UIView override func removeFromSuperview() { super.removeFromSuperview() timer?.invalidate() } // MARK: - Timer

Animation Freezing when Scrolling

Page 15: iOS Effective Coding: Run Loops & UI

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { timer?.invalidate() let timer = Timer( timeInterval: type(of: self).timeInterval, target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true) self.timer = timer RunLoop.current.add(timer, forMode: .commonModes) }

// MARK: - UIView override func removeFromSuperview() { super.removeFromSuperview() timer?.invalidate() } // MARK: - Timer

Tweaking

Page 16: iOS Effective Coding: Run Loops & UI

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { imagesView.subviews.forEach { $0.layer.removeAllAnimations() }

timer?.invalidate() let timer = Timer( timeInterval: type(of: self).timeInterval, target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true) self.timer = timer RunLoop.current.add(timer, forMode: .commonModes) }

// MARK: - UIView override func removeFromSuperview() { super.removeFromSuperview() timer?.invalidate() }

Tweaking

Page 17: iOS Effective Coding: Run Loops & UI

final class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { imagesView.subviews.forEach { $0.layer.removeAllAnimations() }

timer?.invalidate()

animationIndex = Int(arc4random_uniform(UInt32(imagesView.subviews.count))) let timer = Timer( timeInterval: type(of: self).timeInterval, target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true) self.timer = timer RunLoop.current.add(timer, forMode: .commonModes) }

// MARK: - UIView override func removeFromSuperview() { super.removeFromSuperview()

Tweaking

Page 18: iOS Effective Coding: Run Loops & UI

Timer.tolerancefinal class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { imagesView.subviews.forEach { $0.layer.removeAllAnimations() }

timer?.invalidate()

animationIndex = Int(arc4random_uniform(UInt32(imagesView.subviews.count))) let timer = Timer( timeInterval: type(of: self).timeInterval, target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true) timer.tolerance = type(of: self).timeInterval self.timer = timer RunLoop.current().add(timer, forMode: .commonModes) }

// MARK: - UIView override func removeFromSuperview() { super.removeFromSuperview()

Timer Tolerance In iOS 7 and later and OS X v10.9 and later, you can specify a tolerance for a timer (tolerance). Allowing the system flexibility in when a timer fires improves the ability of the system to optimize for increased power savings and responsiveness. The timer may fire at any time between its scheduled fire date and the scheduled fire date plus the tolerance. The timer will not fire before the scheduled fire date. For repeating timers, the next fire date is calculated from the original fire date regardless of tolerance applied at individual fire times, to avoid drift. The default value is zero, which means no additional tolerance is applied. The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of the tolerance property.

As the user of the timer, you will have the best idea of what an appropriate tolerance for a timer may be. A general rule of thumb, though, is to set the tolerance to at least 10% of the interval, for a repeating timer. Even a small amount of tolerance will have a significant positive impact on the power usage of your application. The system may put a maximum value of the tolerance.

最BEST

PRACTICE

Page 19: iOS Effective Coding: Run Loops & UI

Slighter Tweakingfinal class LoadingCollectionViewCell: UICollectionViewCell { @IBOutlet private weak var imagesView: UIView! private var animationIndex: Int = 0 private weak var timer: Timer? static let timeInterval: TimeInterval = 0.2 func startAnimation() { imagesView.subviews.forEach { $0.layer.removeAllAnimations() } timer?.invalidate()

let time = DispatchTime.now() + Double(arc4random_uniform(1000)) / 1000.0 * type(of: self).timeInterval DispatchQueue.main.after(when: time) { if self.timer == nil { self.animationIndex = Int(arc4random_uniform(UInt32(self.imagesView.subviews.count)))

let timer = Timer( timeInterval: type(of: self).timeInterval, target: self, selector: #selector(self.bounceAndIncrement(_:)), userInfo: nil, repeats: true) timer.tolerance = type(of: self).timeInterval self.timer = timer RunLoop.current.add(timer, forMode: .commonModes) } }

Page 20: iOS Effective Coding: Run Loops & UI

Keywords<Timer>.invalidate()

RunLoop.current.add(<Timer>, forMode: .commonModes)

<Timer>.tolerance

<UIView>.subviews.forEach { $0.layer.removeAllAnimations() } <UIView>.subviews.forEach { $0.removeFromSuperview() }

arc4random_uniform(<UInt32>)

DispatchQueue.main.after(when: …) { … }

Page 21: iOS Effective Coding: Run Loops & UI

selector: Selector("bounceAndIncrement:"), userInfo: nil, repeats: true) timer.tolerance = LoadingCollectionViewCell.timeInterval self.timer = timer NSRunLoop.current.addTimer(timer, forMode: NSRunLoopCommonModes) } } } // MARK: - UIView override func removeFromSuperview() { super.removeFromSuperview() timer?.invalidate() } // MARK: - Timer func bounceAndIncrement(_ timer: Timer) { bounce(imagesView.subviews[animationIndex]) animationIndex = (animationIndex + 1) % imagesView.subviews.count } private func bounce(_ view: UIView) { let scale = CGFloat(0.8) view.transform = CGAffineTransform(scaleX: scale, y: scale) UIView.animate( withDuration: 1.0, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 200.0, options: [], animations: { view.transform = .identity }, completion: nil) } }

Annex: Bounce Animation