рд╣реЗрд▓реЛ, рд╣реЗрдмреНрд░! рдореИрдВ рдЖрдкрдХреЛ 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/424/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. schelduernumeratorValue

! 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 ред