Skip to content

Commit

Permalink
Merge pull request #16 from wibosco/bug/endIndexRange
Browse files Browse the repository at this point in the history
Index out-of-bounds crash
  • Loading branch information
wibosco authored Mar 1, 2020
2 parents a770757 + 96591ab commit 0271316
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 54 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

---

## [2.0.1](https://github.com/wibosco/GhostTypewriter/releases/tag/2.0.1)

* Resolved bug where the character index being revealed would go out-of-sync with the actual character being revealed resulting in a crash.
* Added unit tests for different scripts.
* Updated `fastlane` to 2.142.0.

## [2.0.0](https://github.com/wibosco/GhostTypewriter/releases/tag/2.0.0)

* Updated the project to use Swift 5.
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
source "https://rubygems.org"

gem "fastlane", "2.141.0"
gem "fastlane", "2.142.0"
16 changes: 8 additions & 8 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ GEM
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.7)
fastlane (2.141.0)
fastlane (2.142.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
Expand Down Expand Up @@ -86,8 +86,8 @@ GEM
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.10.0)
faraday (~> 0.12)
googleauth (0.11.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
Expand Down Expand Up @@ -118,12 +118,12 @@ GEM
rouge (2.0.7)
rubyzip (1.3.0)
security (0.1.3)
signet (0.12.0)
signet (0.13.0)
addressable (~> 2.3)
faraday (~> 0.9)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.7)
simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
Expand All @@ -140,7 +140,7 @@ GEM
unf_ext (0.0.7.6)
unicode-display_width (1.6.1)
word_wrap (1.0.0)
xcodeproj (1.14.0)
xcodeproj (1.15.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
Expand All @@ -155,7 +155,7 @@ PLATFORMS
ruby

DEPENDENCIES
fastlane (= 2.141.0)
fastlane (= 2.142.0)

BUNDLED WITH
1.16.4
2 changes: 1 addition & 1 deletion GhostTypewriter.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|

s.name = "GhostTypewriter"
s.version = "2.0.0"
s.version = "2.0.1"
s.summary = "A UILabel subclass that adds a type writing animation effect."

s.homepage = "http://www.williamboles.me"
Expand Down
43 changes: 21 additions & 22 deletions GhostTypewriter/Label/TypewriterLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ public class TypewriterLabel: UILabel {
var timerFactory: TimerFactoryType = TimerFactory()

/// Timer instance that control's the animation.
private var animationTimer: TimerType?
private var timer: TimerType?

/// Current index for next character to be animated on screen.
private var currentCharacterAnimationIndex: String.Index?
/// Current offset for next character to be revealed.
private var currentCharacterOffset: Int = 0

///Type alias for completion closure.
public typealias TypewriterLabelCompletion = () -> ()
Expand Down Expand Up @@ -49,7 +49,7 @@ public class TypewriterLabel: UILabel {
Tidies the animation up if it's still in progress by invalidating the timer.
*/
deinit {
animationTimer?.invalidate()
timer?.invalidate()
}

// MARK: - Controls
Expand All @@ -60,28 +60,27 @@ public class TypewriterLabel: UILabel {
- Parameter completion: A callback closure for when the type writing animation is complete.
*/
public func startTypewritingAnimation(completion: TypewriterLabelCompletion? = nil) {
guard let attributedText = attributedText else {
completion?()
return
}

self.completion = completion

if currentCharacterAnimationIndex == nil {
currentCharacterAnimationIndex = attributedText.string.startIndex
if currentCharacterOffset == 0 {
hideAttributedText()
}

animationTimer = timerFactory.buildScheduledTimer(withTimeInterval: typingTimeInterval, repeats: true, block: { _ in
guard let characterIndex = self.currentCharacterAnimationIndex, characterIndex < attributedText.string.endIndex else {
timer = timerFactory.buildScheduledTimer(withTimeInterval: typingTimeInterval, repeats: true, block: { _ in
/*
As each character is revealed the `attributedText` property value of this label
is overridden so we need to keep fetching it inside this timer block.
*/
guard let attributedText = self.attributedText, self.currentCharacterOffset < attributedText.string.count else {
completion?()
self.stopTypewritingAnimation()
return
}

let characterIndex = attributedText.string.index(attributedText.string.startIndex, offsetBy: self.currentCharacterOffset)
self.revealCharacter(atIndex: characterIndex)
self.currentCharacterAnimationIndex = attributedText.string.index(after: characterIndex)

self.currentCharacterOffset += 1
})

isAnimating = true
Expand All @@ -94,7 +93,7 @@ public class TypewriterLabel: UILabel {
*/
private func revealCharacter(atIndex characterIndex: String.Index) {
let range = characterIndex...characterIndex

updateAttributedTextVisibility(to: alpha, range: range)
}

Expand All @@ -106,8 +105,8 @@ public class TypewriterLabel: UILabel {
public func stopTypewritingAnimation() {
isAnimating = false

animationTimer?.invalidate()
animationTimer = nil
timer?.invalidate()
timer = nil
}

/**
Expand All @@ -120,7 +119,7 @@ public class TypewriterLabel: UILabel {
public func resetTypewritingAnimation() {
stopTypewritingAnimation()
hideAttributedText()
currentCharacterAnimationIndex = nil
currentCharacterOffset = 0
}

/**
Expand All @@ -139,7 +138,7 @@ public class TypewriterLabel: UILabel {
public func completeTypewritingAnimation() {
stopTypewritingAnimation()
showAttributedText()
currentCharacterAnimationIndex = nil
currentCharacterOffset = 0

completion?()
}
Expand Down Expand Up @@ -169,7 +168,6 @@ public class TypewriterLabel: UILabel {
guard let attributedText = attributedText else {
return
}

let range = attributedText.string.startIndex..<attributedText.string.endIndex

updateAttributedTextVisibility(to: alpha, range: range)
Expand All @@ -187,7 +185,8 @@ public class TypewriterLabel: UILabel {
}

let attributedString = NSMutableAttributedString(attributedString: attributedText)
attributedText.enumerateAttribute(.foregroundColor, in: NSRange(range, in: attributedString.string), options: []) { (value, range, stop) -> Void in
let nsRange = NSRange(range, in: attributedText.string)
attributedText.enumerateAttribute(.foregroundColor, in: nsRange, options: []) { (value, range, stop) -> Void in
let color: UIColor
if let colorValue = value as? UIColor {
color = colorValue
Expand Down
85 changes: 63 additions & 22 deletions GhostTypewriterTests/Tests/TypewriterLabelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class TypewriterLabelTests: XCTestCase {
sut = TypewriterLabel()
sut.text = "Test"
sut.textColor = .black
sut.typingTimeInterval = 0.01
}

override func tearDown() {
Expand Down Expand Up @@ -69,19 +70,19 @@ class TypewriterLabelTests: XCTestCase {
sut.startTypewritingAnimation()

waitForExpectations(timeout: 3.0, handler: nil)

timerClosure?(timerFactory.mockTimer)

sut.attributedText!.enumerateAttribute(.foregroundColor, in: NSMakeRange(0, sut.attributedText!.length), options: []) { (value, range, _) -> Void in
let blackRange = NSRange(location: 0, length: 1)
let clearRange = NSRange(location: 1, length: 3)

if range == blackRange {
XCTAssertEqual(value as! UIColor, UIColor.black)
XCTAssertEqual(value as! UIColor, UIColor.black)
} else if range == clearRange {
XCTAssertEqual(value as! UIColor, UIColor.clear)
XCTAssertEqual(value as! UIColor, UIColor.clear)
} else {
XCTFail("Unexpected color")
XCTFail("Unexpected color")
}
}

Expand All @@ -90,28 +91,27 @@ class TypewriterLabelTests: XCTestCase {
sut.attributedText!.enumerateAttribute(.foregroundColor, in: NSMakeRange(0, sut.attributedText!.length), options: []) { (value, range, _) -> Void in
let blackRange = NSRange(location: 0, length: 2)
let clearRange = NSRange(location: 2, length: 2)

if range == blackRange {
XCTAssertEqual(value as! UIColor, UIColor.black)
XCTAssertEqual(value as! UIColor, UIColor.black)
} else if range == clearRange {
XCTAssertEqual(value as! UIColor, UIColor.clear)
XCTAssertEqual(value as! UIColor, UIColor.clear)
} else {
XCTFail("Unexpected color")
XCTFail("Unexpected color")
}
}
}

func test_start_triggerCompletionCallback() {
func test_start_completes_triggerCompletionCallback() {
let handlerExpectation = expectation(description: "handlerExpectation")

sut.startTypewritingAnimation {
handlerExpectation.fulfill()
}

waitForExpectations(timeout: 3.0, handler: nil)
}

func test_start_animationCompletes_revealFullText() {
func test_start_completes_revealFullText() {
let handlerExpectation = expectation(description: "handlerExpectation")
sut.startTypewritingAnimation {
handlerExpectation.fulfill()
Expand All @@ -122,10 +122,8 @@ class TypewriterLabelTests: XCTestCase {
let fullRange = NSMakeRange(0, sut.attributedText!.length)

sut.attributedText!.enumerateAttribute(.foregroundColor, in: fullRange, options: []) { (value, range, _) -> Void in
let blackRange = NSRange(location: 0, length: 4)

XCTAssertEqual(value as! UIColor, UIColor.black)
XCTAssertEqual(range, blackRange)
XCTAssertEqual(range, fullRange)
}
}

Expand Down Expand Up @@ -161,6 +159,49 @@ class TypewriterLabelTests: XCTestCase {
}
}

// MARK: CharacterSystems

func test_start_completes_latinCharacters_revealFullText() {
verifyStartRevealsFullText(for: "Test")
}

func test_start_completes_phoneticCharacters_revealFullText() {
verifyStartRevealsFullText(for: "pər")
}

func test_start_completes_cyrillicCharacters_revealFullText() {
verifyStartRevealsFullText(for: "испытания")
}

func test_start_completes_arabicCharacters_revealFullText() {
verifyStartRevealsFullText(for: "اختبار")
}

func test_start_completes_simplifiedChineseCharacters_revealFullText() {
verifyStartRevealsFullText(for: "测试")
}

private func verifyStartRevealsFullText(for text: String) {
let font = UIFont.systemFont(ofSize: 21, weight: .light)
let attributedString = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: font])

sut.attributedText = attributedString

let handlerExpectation = expectation(description: "handlerExpectation")
sut.startTypewritingAnimation {
handlerExpectation.fulfill()
}

waitForExpectations(timeout: 3.0, handler: nil)

let fullRange = NSMakeRange(0, sut.attributedText!.length)

sut.attributedText!.enumerateAttribute(.font, in: fullRange, options: []) { (value, range, _) -> Void in
XCTAssertEqual(value as! UIFont, font)
XCTAssertEqual(range, fullRange)
}
}

// MARK: Reset

func test_reset_animationCompleted_hidesFullText() {
Expand Down Expand Up @@ -223,7 +264,7 @@ class TypewriterLabelTests: XCTestCase {
sut.timerFactory = timerFactory
sut.restartTypewritingAnimation()

waitForExpectations(timeout: 3, handler: nil)
waitForExpectations(timeout: 3, handler: nil)
}

func test_reset_animationInProgress_hidesText_timerFires_revealsFirstCharacter() {
Expand All @@ -247,13 +288,13 @@ class TypewriterLabelTests: XCTestCase {
sut.attributedText!.enumerateAttribute(.foregroundColor, in: NSMakeRange(0, sut.attributedText!.length), options: []) { (value, range, _) -> Void in
let blackRange = NSRange(location: 0, length: 1)
let clearRange = NSRange(location: 1, length: 3)

if range == blackRange {
XCTAssertEqual(value as! UIColor, UIColor.black)
XCTAssertEqual(value as! UIColor, UIColor.black)
} else if range == clearRange {
XCTAssertEqual(value as! UIColor, UIColor.clear)
XCTAssertEqual(value as! UIColor, UIColor.clear)
} else {
XCTFail("Unexpected color")
XCTFail("Unexpected color")
}
}

Expand All @@ -278,7 +319,7 @@ class TypewriterLabelTests: XCTestCase {
}

secondTimerClosure?(secondTimerFactory.mockTimer)

sut.attributedText!.enumerateAttribute(.foregroundColor, in: NSMakeRange(0, sut.attributedText!.length), options: []) { (value, range, _) -> Void in
let blackRange = NSRange(location: 0, length: 1)
let clearRange = NSRange(location: 1, length: 3)
Expand Down Expand Up @@ -326,7 +367,7 @@ class TypewriterLabelTests: XCTestCase {
sut.startTypewritingAnimation()

wait(for: [timerExpectation], timeout: 3.0)

timerClosure?(timerFactory.mockTimer)

let invalidateExpectation = expectation(description: "invalidateExpectation")
Expand Down
Binary file modified typingAnimation.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 0271316

Please sign in to comment.