рд╣реЗрд▓реЛ, рд╣реЗрдмреНрд░! рдореИрдВ рдЖрдкрдХреЛ Shaw Mishali рджреНрд╡рд╛рд░рд╛ рд░реЗрд╡реЗрдВрдбрд░рд▓рд┐рдЪ рдбреЙрдЯ рдХреЙрдо рдХреЗ рд▓реЗрдЦ "рдЯреЗрд╕реНрдЯрд┐рдВрдЧ рдпреЛрд░ рдЖрд░рдПрдХреНрд╕рд╕реНрд╡рд┐рдлреНрдЯ рдХреЛрдб" рдХрд╛ рдЕрдиреБрд╡рд╛рдж рдкреНрд░рд╕реНрддреБрдд рдХрд░рддрд╛ рд╣реВрдВ ред
RxSwift рдХреЗ рд╕рд╛рде рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛рддреНрдордХ рдЕрдиреБрдкреНрд░рдпреЛрдЧ рд▓рд┐рдЦрдирд╛ рд╡реИрдЪрд╛рд░рд┐рдХ рд░реВрдк рд╕реЗ "рд╕рд╛рдорд╛рдиреНрдп рддрд░реАрдХреЗ рд╕реЗ" рд▓рд┐рдЦрдиреЗ рд╕реЗ рдЕрд▓рдЧ рд╣реЛрддрд╛ рд╣реИред рдпрд╣ рдЗрд╕ рдЕрд░реНрде рдореЗрдВ рднрд┐рдиреНрди рд╣реИ рдХрд┐ рдЖрдкрдХреЗ рдЖрд╡реЗрджрди рдореЗрдВ рд╡рд╕реНрддреБрдУрдВ рдХрд╛ рдЖрдорддреМрд░ рдкрд░ рд╕реНрдкрд╖реНрдЯ рдЕрд░реНрде рдирд╣реАрдВ рд╣реЛрдЧрд╛, рдЗрд╕рдХреЗ рдмрдЬрд╛рдп рдЙрдиреНрд╣реЗрдВ рд╕рдордп рдЕрдХреНрд╖ рдкрд░ рдореВрд▓реНрдпреЛрдВ рдХреА рдПрдХ рдзрд╛рд░рд╛ рджреНрд╡рд╛рд░рд╛ рджрд░реНрд╢рд╛рдпрд╛ рдЬрд╛рдПрдЧрд╛ , рдЬрд┐рд╕реЗ RxSwift рдХреЗ рд░реВрдк рдореЗрдВ рдЬрд╛рдирд╛ рдЬрд╛рддрд╛ рд╣реИ Observable
ред рдпрд╣ рд▓реЗрдЦ рдЖрдкрдХреЛ RxSwift рдХреЛрдб рдХреЗ рдкрд░реАрдХреНрд╖рдг рдХреА рдХреБрдВрдЬреА рджреЗрдЧрд╛ред
Observable
- рдПрдХ рд╢рдХреНрддрд┐рд╢рд╛рд▓реА рддрдВрддреНрд░ рдЬреЛ рдЖрдкрдХреЛ рдПрдХ рдбреЗрд╡рд▓рдкрд░ рдХреЗ рд░реВрдк рдореЗрдВ, рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХрд╛ рдЬрд╡рд╛рдм рджреЗрдиреЗ рдФрд░ рдпрд╣ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрддрд╛ рд╣реИ рдХрд┐ рдЖрдкрдХреЗ рдЖрд╡реЗрджрди рдХреА рд╕реНрдерд┐рддрд┐ рд╣рдореЗрд╢рд╛ рдЕрджреНрдпрддрд┐рдд рд╣реИред рд╕рднреА рд▓рд╛рднреЛрдВ рдХреЗ рд╕рд╛рде рдпрд╣ рдкреНрд░рджрд╛рди рдХрд░рддрд╛ рд╣реИ, рдкрд░реАрдХреНрд╖рдг Observable
рдХреЗрд╡рд▓ рд╕рд╛рдорд╛рдиреНрдп рдореВрд▓реНрдпреЛрдВ рдХреЗ рд▓рд┐рдП XCTAssert рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реБрдП рдРрд╕рд╛ рдХреЛрдИ рддреБрдЪреНрдЫ рдХрд╛рд░реНрдп рдирд╣реАрдВ рд╣реИред рд▓реЗрдХрд┐рди рдЪрд┐рдВрддрд╛ рдордд рдХрд░реЛ - рдпрд╣ рд▓реЗрдЦ RxSwift рдХреЗ рдкрд░реАрдХреНрд╖рдг рдореЗрдВ рдПрдХ рд╡рд┐рд╢реЗрд╖рдЬреНрдЮ рдмрдирдиреЗ рдкрд░ рдЖрдкрдХрд╛ рдорд╛рд░реНрдЧрджрд░реНрд╢рди рдХрд░реЗрдЧрд╛!
рдпрд╣ рдЖрд▓реЗрдЦ рдЖрдкрдХреЛ рд╕рд┐рдЦрд╛рдПрдЧрд╛ рдХрд┐ Observable
рдереНрд░реЗрдбреНрд╕ рдХреЗ рд▓рд┐рдП рдпреВрдирд┐рдЯ рдЯреЗрд╕реНрдЯ рдХреИрд╕реЗ рдмрдирд╛рдПрдВ ред рдЖрдк RxSwift рдХреЛрдб рдХреЗ рдкрд░реАрдХреНрд╖рдг рдХреЗ рд▓рд┐рдП рдЙрдкрд▓рдмреНрдз рдХреБрдЫ рддрдХрдиреАрдХреЛрдВ рдХреЗ рд╕рд╛рде-рд╕рд╛рде рдХреБрдЫ рдпреБрдХреНрддрд┐рдпреЛрдВ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЬрд╛рдиреЗрдВрдЧреЗред рдЪрд▓реЛ рд╢реБрд░реВ рдХрд░рддреЗ рд╣реИрдВред
рдиреЛрдЯ: рдпрд╣ рд▓реЗрдЦ рдорд╛рдирддрд╛ рд╣реИ рдХрд┐ рдЖрдк рдкрд╣рд▓реЗ рд╕реЗ рд╣реА RxSwift рдФрд░ XCTest рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рд╕рд░рд▓ рдкрд░реАрдХреНрд╖рдг рд▓рд┐рдЦрдиреЗ рдХреЗ рд▓рд┐рдП рджреЛрдиреЛрдВ рд╕реЗ рдкрд░рд┐рдЪрд┐рдд рд╣реИрдВред
рд╢реБрд░реВ рдХрд░рдирд╛
рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛рд╢реАрд▓ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдЕрдЪреНрдЫреА рддрд░рд╣ рд╕реЗ рдкреНрд░рджрд░реНрд╢рди рдХрд░рддреЗ рд╣реИрдВ рдЬрдм рдЖрдкрдХреЛ рд╕рд╛рдордЧреНрд░реА рдмрджрд▓рдиреЗ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрддреА рд╣реИ, рдЗрд╕рд▓рд┐рдП рд╣рдо рдЗрд╕ рддрд░рд╣ рдХреЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░реЗрдВрдЧреЗред
рд╕реНрдЯрд╛рд░реНрдЯрд░ рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдбрд╛рдЙрдирд▓реЛрдб рдХрд░реЗрдВ ред рдЖрдкрдХреЛ рдЗрд╕ рдЯреНрдпреВрдЯреЛрд░рд┐рдпрд▓ рдХреЗ рд▓рд┐рдП рд╕реНрдЯрд╛рд░реНрдЯрд░ рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдорд┐рд▓реЗрдЧрд╛: рд░реЗрдЯреНрд░реЛрдиреЛрдо рдПрдХ рдореЗрдЯреНрд░реЛрдиреЛрдорд┐рдХ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд╣реИ рдЬрд┐рд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рдЖрдк рд╕рдВрдЧреАрдд рдХрд╛ рдЕрднреНрдпрд╛рд╕ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред рдЬреИрд╕рд╛ рдХрд┐ рдЖрдк рд╕рдордЭ рд╕рдХрддреЗ рд╣реИрдВ, рдЗрд╕ рддрдереНрдп рдХреЗ рдХрд╛рд░рдг рдХрд┐ рд╕рдордп рдХреЗ рд╕рд╛рде рдореЗрдЯреНрд░реЛрдиреЛрдо рдХрд╛рдо рдХрд░рддреЗ рд╣реИрдВ, рдЖрдкрдХреЛ рдмрд╣реБрдд рд╕рд╛рд░реЗ рддрд░реНрдХ рджреЗрдЦрдиреЗ рд╣реЛрдВрдЧреЗ рдЬрд┐рдиреНрд╣реЗрдВ рдкрд░реАрдХреНрд╖рдг рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред
Raytronome.xcworkspace. Main.storyboard. , .
. Play . (Signature) (Tempo).

UIViewController тАФ MetronomeViewController.swift, MetronomeViewModel.swift -, .
Observable
RxSwift Observable
.
, .
; . Observable
, , () .

, :
, . , MetronomeViewModel
, ViewModel - .
MetronomeViewModel.swift. ViewModel , : , ( ), , , , ,

UI. :
- 4
- 4/4
- 120
- Play/Pause isPlaying
- , , .
- "" .
.even
.odd
тАФ
RxSwift, RxBlocking RxTest. .
RxBlocking
RaytronomeTests.swift.
; RxSwift, RxCocoa, RxTest RxBlocking, viewModel
setUp()
MetronomeViewModel
, 4
. , , . RxBlocking!
RxBlocking тАФ RxSwift, : Observable
BlockingObservable
, Observable , .

, тАФ , completed
error
тАФ
RxBlocking , :
toArray()
: .first()
: .last()
: .
, first()
.
RaytronomeTests
:
func testNumeratorStartsAt4() throws {
XCTAssertEqual(try viewModel.numeratorText.toBlocking().first(), "4")
XCTAssertEqual(try viewModel.numeratorValue.toBlocking().first(), 4)
}
func testDenominatorStartsAt4() throws {
XCTAssertEqual(try viewModel.denominatorText.toBlocking().first(), "4")
}
toBlocking()
BlockingObservable
, first()
. XCTAssert .
, throws
, RxBlocking . throws
try!
.
Command-U

signatureText
4/4
, tempoText
120 BPM
.
,
func testSignatureStartsAt4By4() throws {
XCTAssertEqual(try viewModel.signatureText.toBlocking().first(), "4/4")
}
func testTempoStartsAt120() throws {
XCTAssertEqual(try viewModel.tempoText.toBlocking().first(), "120 BPM")
}
RxBlocking
, RxBlocking , "" . , :
- , , , , RxBlocking . , тАФ тАФ RxBlocking
- RxBlocking .
Observable
, BlockingObservable
. - , , RxBlocking , .
- RxBlocking , .
. : Play/Pause isPlaying
, (tappedPlayPause
). .
RxTest
, RxBlocking , , , .
RxTest!
RxTest RxBlocking, , , . - , тАФ TestScheduler
.

тАФ RxSwift, , ,
RxSwift , .
: " ?"
RxTest тАФ TestScheduler
тАФ . , Observable
Observer
, "" .
тАФ .
TestScheduler
. DisposeBag
Disposable
. viewModel
:
var scheduler: TestScheduler!
var disposeBag: DisposeBag!
, setUp()
, TestScheduler
DisposeBag
:
scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()
TestScheduler
initialClock
, " " . DisposeBag . .
!
"" Play/Pause , isPlaying
.
:
Observable
"" tappedPlayPause.Observer
' isPlaying
.- .
, !
. RxTest-:
func testTappedPlayPauseChangesIsPlaying() {
let isPlaying = scheduler.createObserver(Bool.self)
viewModel.isPlaying
.drive(isPlaying)
.disposed(by: disposeBag)
scheduler.createColdObservable([.next(10, ()),
.next(20, ()),
.next(30, ())])
.bind(to: viewModel.tappedPlayPause)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(isPlaying.events, [
.next(0, false),
.next(10, true),
.next(20, false),
.next(30, true)
])
}
, . :
TestScheduler
TestableObserver
, Observable
тАФ Bool. Observaer
тАФ events, .drive()
"" viewModel.isPlaying
TestableObserver
. "" .Observable
, "" tappedPlayPause
. , Observable
, TestableObservable
, TestScheduler
"" .start()
.- XCTAssertEqual RxTest, , isPlaying , .
10
, 20
30
, , 0
тАФ isPlaying
.
? : viewModel . , .

Command-u. 5 .

, 0
, 10, 20 30 , .
RxTest (Date
) , VirtualTimeUnit
( Int
).
RxSwift тАФ TestScheduler
.
, тАФ , , , 10
10 , . .
, TestScheduler
, ?
:
func testModifyingNumeratorUpdatesNumeratorText() {
let numerator = scheduler.createObserver(String.self)
viewModel.numeratorText
.drive(numerator)
.disposed(by: disposeBag)
scheduler.createColdObservable([.next(10, 3),
.next(15, 1)])
.bind(to: viewModel.steppedNumerator)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(numerator.events, [
.next(0, "4"),
.next(10, "3"),
.next(15, "1")
])
}
func testModifyingDenominatorUpdatesNumeratorText() {
let denominator = scheduler.createObserver(String.self)
viewModel.denominatorText
.drive(denominator)
.disposed(by: disposeBag)
scheduler.createColdObservable([.next(10, 2),
.next(15, 4),
.next(20, 3),
.next(25, 1)])
.bind(to: viewModel.steppedDenominator)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(denominator.events, [
.next(0, "4"),
.next(10, "8"),
.next(15, "32"),
.next(20, "16"),
.next(25, "4")
])
}
func testModifyingTempoUpdatesTempoText() {
let tempo = scheduler.createObserver(String.self)
viewModel.tempoText
.drive(tempo)
.disposed(by: disposeBag)
scheduler.createColdObservable([.next(10, 75),
.next(15, 90),
.next(20, 180),
.next(25, 60)])
.bind(to: viewModel.tempo)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(tempo.events, [
.next(0, "120 BPM"),
.next(10, "75 BPM"),
.next(15, "90 BPM"),
.next(20, "180 BPM"),
.next(25, "60 BPM")
])
}
:
testModifyingNumeratorUpdatesNumeratorText
: , .testModifyingDenominatorUpdatesNumeratorText
: , .testModifyingTempoUpdatesTempoText
: ,
, , . , 3
, 1
. , numeratorText
"4"
( ), "3"
, , , "1"
.
, denominatorText
, BPM
Command-U, 8 . !

, !
. :
func testModifyingSignatureUpdatesSignatureText() {
let signature = scheduler.createObserver(String.self)
viewModel.signatureText
.drive(signature)
.disposed(by: disposeBag)
scheduler.createColdObservable([.next(5, 3),
.next(10, 1),
.next(20, 5),
.next(25, 7),
.next(35, 12),
.next(45, 24),
.next(50, 32)
])
.bind(to: viewModel.steppedNumerator)
.disposed(by: disposeBag)
scheduler.createColdObservable([.next(15, 2),
.next(30, 3),
.next(40, 4)
])
.bind(to: viewModel.steppedDenominator)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(signature.events, [
.next(0, "4/4"),
.next(5, "3/4"),
.next(10, "1/4"),
.next(15, "1/8"),
.next(20, "5/8"),
.next(25, "7/8"),
.next(30, "7/16"),
.next(35, "12/16"),
.next(40, "12/32"),
.next(45, "24/32"),
.next(50, "32/32")
])
}
! , , . steppedNumerator
steppedDenominator
, signatureText
.
:

. 9 !
, .
:
4/4
24/32
.- "-" ; 16/16, 8/8, , , 4/4, 24/16, 24/8 24/4 тАФ .
: , ,
:
func testModifyingDenominatorUpdatesNumeratorValueIfExceedsMaximum() {
let numerator = scheduler.createObserver(Double.self)
viewModel.numeratorValue
.drive(numerator)
.disposed(by: disposeBag)
scheduler.createColdObservable([
.next(5, 4),
.next(15, 3),
.next(20, 2),
.next(25, 1)
])
.bind(to: viewModel.steppedDenominator)
.disposed(by: disposeBag)
scheduler.createColdObservable([.next(10, 24)])
.bind(to: viewModel.steppedNumerator)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(numerator.events, [
.next(0, 4),
.next(10, 24),
.next(15, 16),
.next(20, 8),
.next(25, 4)
])
}
, , ! :
- ,
TestableObserver
"" numeratorValue
- , .
32
, 24
( ). 24/32
. , numeratorValue
. schelduer
numeratorValue

! Command-U:
XCTAssertEqual failed: ("[next(4.0) @ 0, next(24.0) @ 10]") is not equal to ("[next(4.0) @ 0, next(24.0) @ 10, next(16.0) @ 15, next(8.0) @ 20, next(4.0) @ 25]") -
! .
, numeratorValue
24
, , 24/16
24/4
. :
- ,
4/8
. - ,
7/8
. - .
4/4
, 7/4
тАФ !

, . :]
.
MetronomeViewModel.swift , numeratorValue
:
numeratorValue = steppedNumerator
.distinctUntilChanged()
.asDriver(onErrorJustReturn: 0)
:
numeratorValue = steppedNumerator
.distinctUntilChanged()
.asDriver(onErrorJustReturn: 0)
steppedNumerator
, steppedNumerator
maxNumerator
.
Command-U, 10 . !

viewModel
. , 78%. !
: , Edit Scheme... - , , Tests, Options Code Coverage. Gather coverage for some targets Raytronome . Report Navigator
, . тАФ .
, /, , ( )
тАФ 32
. RaytronomeTests.swift :
func testBeatBy32() {
viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/32"),
autoplay: true,
beatScheduler: scheduler)
let beat = scheduler.createObserver(Beat.self)
viewModel.beat.asObservable()
.take(8)
.bind(to: beat)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(beat.events, [])
}
. - :
viewModel
. 4/32
, , tappedPlayPause
.
. , viewModel SerialDispatchQueueScheduler
, TestScheduler
, , .TestableObserver
Beat
8 beat
. 8
тАФ , , .scheduler
. , , , тАФ .
Command-U. :
XCTAssertEqual failed: ("[next(first) @ 1, next(regular) @ 2, next(regular) @ 3, next(regular) @ 4, next(first) @ 5, next(regular) @ 6, next(regular) @ 7, next(regular) @ 8, completed @ 8]") is not equal to ("[]") тАФ
, , ? 1 8.
, - , 4/32 4/4. , .
Meter(signature: "4/32")
Meter(signature: "4/4")
Command-U. - - .
, ! , - , . , ? -, VirtualTimeUnit
, .
120 BPM
, 4
( 4/4
), 0.5
. 32
, 0.0625
.
, тАФ , TestScheduler
, VirtualTimeUnit
.
resolution
. resolution
тАФ TestScheduler
1
.
0.0625/1
1
, 0.5/1
1, .
, resolution
, .
viewModel
, , :
scheduler = TestScheduler(initialClock: 0, resolution: 0.01)
resolution
resoulution

4/32
viewModel Command-U.
, .
XCTAssertEqual failed: ("[next(first) @ 6, next(regular) @ 12, next(regular) @ 18, next(regular) @ 24, next(first) @ 30, next(regular) @ 36, next(regular) @ 42, next(regular) @ 48, completed @ 48]") is not equal to ("[]") тАФ
6
. XCTAssertEqual
:
XCTAssertEqual(beat.events, [
.next(6, .first),
.next(12, .regular),
.next(18, .regular),
.next(24, .regular),
.next(30, .first),
.next(36, .regular),
.next(42, .regular),
.next(48, .regular),
.completed(48)
])
Command-U, , . !
- 4/4
.
:
func testBeatBy4() {
scheduler = TestScheduler(initialClock: 0, resolution: 0.1)
viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/4"),
autoplay: true,
beatScheduler: scheduler)
let beat = scheduler.createObserver(Beat.self)
viewModel.beat.asObservable()
.take(8)
.bind(to: beat)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(beat.events, [
.next(5, .first),
.next(10, .regular),
.next(15, .regular),
.next(20, .regular),
.next(25, .first),
.next(30, .regular),
.next(35, .regular),
.next(40, .regular),
.completed(40)
])
}
тАФ , resolution
, 0.1
, 4
.
Command-U, , 12 !
, , 99.25%
MetronomeViewModel
, . : beatType
.

beatType
тАФ , , , beatType
.even
.odd
. . , , :
func testBeatTypeAlternates() {
scheduler = TestScheduler(initialClock: 0, resolution: 0.1)
viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/4"),
autoplay: true,
beatScheduler: scheduler)
let beatType = scheduler.createObserver(BeatType.self)
viewModel.beatType.asObservable()
.take(8)
.bind(to: beatType)
.disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(beatType.events, [
.next(5, .even),
.next(10, .odd),
.next(15, .even),
.next(20, .odd),
.next(25, .even),
.next(30, .odd),
.next(35, .even),
.next(40, .odd),
.completed(40)
])
}
?
, . , RxSwift. RxBlocking , RxTest .
, Scheduler
, TestScheduler
.
рд╡рд╣рд╛рдБ рдмрд╣реБрдд рдЕрдзрд┐рдХ рд╣реИ рдХрд┐ рджреЛрдиреЛрдВ рдореЗрдВ рд╕реЗ рд╕реАрдЦрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ RxSwift рдФрд░ рдореЗрдВ RxBlocking рдЕрдкрдиреЗ рдЖрдВрддрд░рд┐рдХ рдХрд╛рдо, рдСрдкрд░реЗрдЯрд░реЛрдВ рдФрд░ рдЗрддрдиреЗ рдкрд░ -ред рдЬрд╛рд░реА рд░рдЦрдиреЗ рдХреЗ рд▓рд┐рдП рд╕рдмрд╕реЗ рдЕрдЪреНрдЫреА рдЬрдЧрд╣ рдЖрдзрд┐рдХрд╛рд░рд┐рдХ RxSwift рдкреНрд░рд▓реЗрдЦрди рд╣реИ , рд╕рд╛рде рд╣реА рд╕рд╛рде RxBlocking рдСрдкрд░реЗрдЯрд░реЛрдВ рдХреА рдПрдХ рд╕реВрдЪреА рднреА рд╣реИ ред
рдпрджрд┐ рдЖрдкрдХреЗ рдкрд╛рд╕ рд▓реЗрдЦ рдХреА рд╕рд╛рдордЧреНрд░реА рдкрд░ рдХреЛрдИ рдкреНрд░рд╢реНрди, рдпрд╛ рдЯрд┐рдкреНрдкрдгреА рд╣реИ - рдЯрд┐рдкреНрдкрдгрд┐рдпреЛрдВ рдореЗрдВ, рдпрд╛ рдореВрд▓ рд▓реЗрдЦ рдХреА рдЪрд░реНрдЪрд╛ рдореЗрдВ рдЖрдкрдХрд╛ рд╕реНрд╡рд╛рдЧрдд рд╣реИ ред рдкрдврд╝рдиреЗ рдХреЗ рд▓рд┐рдП рдзрдиреНрдпрд╡рд╛рдж! рдЪреЗрддрд╛рд╡рдиреА , рдЗрд╕ рд▓реЗрдЦ рдХрд╛ рдЕрдиреБрд╡рд╛рдж рд╣реИ рд▓реЗрдЦ рд╕реЗ raywenderlich.com ред