diff --git a/RxCocoa/Common/Observables/NSObject+Rx.swift b/RxCocoa/Common/Observables/NSObject+Rx.swift index c25b66d8..f50dfe68 100644 --- a/RxCocoa/Common/Observables/NSObject+Rx.swift +++ b/RxCocoa/Common/Observables/NSObject+Rx.swift @@ -92,7 +92,7 @@ extension Reactive where Base: NSObject { #endif // Dealloc -extension Reactive where Base: NSObject { +extension Reactive where Base: AnyObject { /** Observable sequence of object deallocated events. @@ -211,7 +211,7 @@ let deallocSelector = NSSelectorFromString("dealloc") let rxDeallocatingSelector = RX_selector(deallocSelector) let rxDeallocatingSelectorReference = RX_reference_from_selector(rxDeallocatingSelector) -extension Reactive where Base: NSObject { +extension Reactive where Base: AnyObject { func synchronized( _ action: () -> T) -> T { objc_sync_enter(self.base) let result = action() @@ -220,7 +220,7 @@ extension Reactive where Base: NSObject { } } -extension Reactive where Base: NSObject { +extension Reactive where Base: AnyObject { /** Helper to make sure that `Observable` returned from `createCachedObservable` is only created once. This is important because there is only one `target` and `action` properties on `NSControl` or `UIBarButtonItem`. diff --git a/RxCocoa/iOS/UIControl+Rx.swift b/RxCocoa/iOS/UIControl+Rx.swift index 2e5eff59..2f9c45ac 100644 --- a/RxCocoa/iOS/UIControl+Rx.swift +++ b/RxCocoa/iOS/UIControl+Rx.swift @@ -63,7 +63,7 @@ extension Reactive where Base: UIControl { You might be wondering why the ugly `as!` casts etc, well, for some reason if Swift compiler knows C is UIControl type and optimizations are turned on, it will crash. */ - static func value(_ control: C, getter: @escaping (C) -> T, setter: @escaping (C, T) -> Void) -> ControlProperty { + static func value(_ control: C, getter: @escaping (C) -> T, setter: @escaping (C, T) -> Void) -> ControlProperty { let source: Observable = Observable.create { [weak weakControl = control] observer in guard let control = weakControl else { observer.on(.completed) @@ -80,7 +80,7 @@ extension Reactive where Base: UIControl { return Disposables.create(with: controlTarget.dispose) } - .takeUntil((control as! NSObject).rx.deallocated) + .takeUntil((control as NSObject).rx.deallocated) let bindingObserver = UIBindingObserver(UIElement: control, binding: setter) diff --git a/RxCocoa/iOS/UISwitch+Rx.swift b/RxCocoa/iOS/UISwitch+Rx.swift index 73338366..a5e4b19f 100644 --- a/RxCocoa/iOS/UISwitch+Rx.swift +++ b/RxCocoa/iOS/UISwitch+Rx.swift @@ -18,6 +18,10 @@ extension Reactive where Base: UISwitch { /** Reactive wrapper for `on` property. + + **⚠️Unlike other controls, Apple is reusing instances of UISwitch or a there is a leak, + so underlying observable sequence won't complete when nothing holds a strong reference + to UISwitch.⚠️** */ public var value: ControlProperty { return Reactive.value( diff --git a/RxExample/RxExample-iOSUITests/FlowTests.swift b/RxExample/RxExample-iOSUITests/FlowTests.swift new file mode 100644 index 00000000..fa5ac963 --- /dev/null +++ b/RxExample/RxExample-iOSUITests/FlowTests.swift @@ -0,0 +1,266 @@ +// +// FlowTests.swift +// RxExample-iOSUITests +// +// Created by Krunoslav Zaher on 8/20/16. +// Copyright © 2016 Krunoslav Zaher. All rights reserved. +// + +import XCTest + +class FlowTests : XCTestCase { + var app: XCUIApplication! + override func setUp() { + super.setUp() + + continueAfterFailure = false + self.app = XCUIApplication() + self.app.launchEnvironment = ["isUITest": ""] + self.app.launch() + } +} + +extension FlowTests { + func testAll() { + for test in [ + _testSearchWikipedia, + _testMasterDetail, + _testGitHubSignUp, + _testAnimatedPartialUpdates, + _testVisitEveryScreen + ] { + test() + wait(interval: 1.0) + } + } + + func _testGitHubSignUp() { + app.tables.allElementsBoundByIndex[0].cells.allElementsBoundByIndex[3].tap() + let username = app.textFields.allElementsBoundByIndex[0] + let password = app.secureTextFields.allElementsBoundByIndex[0] + let repeatedPassword = app.secureTextFields.allElementsBoundByIndex[1] + + username.tap() + username.typeText("rxrevolution") + + password.tap() + password.typeText("mypassword") + + repeatedPassword.tap() + repeatedPassword.typeText("mypassword") + + app.windows.allElementsBoundByIndex[0].coordinate(withNormalizedOffset: CGVector(dx: 14.50, dy: 80.00)).tap() + app.buttons["Sign up"].tap() + + waitForElementToAppear(app.alerts.element(boundBy: 0)) + + app.alerts.allElementsBoundByIndex[0].buttons.allElementsBoundByIndex[0].tap() + + goBack() + } + + func _testSearchWikipedia() { + app.tables.allElementsBoundByIndex[0].cells.allElementsBoundByIndex[12].tap() + + let searchField = app.tables.children(matching: .searchField).element + + searchField.tap() + + searchField.typeSlow(text: "banana") + searchField.clearText() + searchField.typeSlow(text: "Yosemite") + searchField.clearText() + + goBack() + } + + func _testMasterDetail() { + app.tables.allElementsBoundByIndex[0].cells.allElementsBoundByIndex[10].tap() + waitForElementToAppear(app.tables.allElementsBoundByIndex[0].cells.element(boundBy: 5)) + + let editButton = app.navigationBars.buttons["Edit"] + + editButton.tap() + + func reorderButtonForIndex(_ index: Int) -> XCUIElement { + return app.tables.cells.allElementsBoundByIndex[index].buttons.allElementsBoundByIndex.filter { element in + element.label.hasPrefix("Reorder ") + }.first! + } + + reorderButtonForIndex(5).press(forDuration: 1.5, thenDragTo: reorderButtonForIndex(2)) + + reorderButtonForIndex(7).press(forDuration: 1.5, thenDragTo: reorderButtonForIndex(4)) + + reorderButtonForIndex(1).press(forDuration: 1.5, thenDragTo: reorderButtonForIndex(3)) + + let doneButton = app.navigationBars.buttons["Done"] + doneButton.tap() + + app.tables.allElementsBoundByIndex[0].cells.allElementsBoundByIndex[6].tap() + + goBack() + goBack() + } + + func _testAnimatedPartialUpdates() { + app.tables.allElementsBoundByIndex[0].cells.allElementsBoundByIndex[11].tap() + + let randomize = app.navigationBars.buttons["Randomize"] + waitForElementToAppear(randomize) + + randomize.tap() + randomize.tap() + randomize.tap() + randomize.tap() + randomize.tap() + randomize.tap() + randomize.tap() + randomize.tap() + randomize.tap() + + goBack() + } + + func _testVisitEveryScreen() { + let count = Int(app.tables.allElementsBoundByIndex[0].cells.count) + XCTAssertTrue(count > 0) + + for i in 0 ..< count { + app.tables.allElementsBoundByIndex[0].cells.allElementsBoundByIndex[i].tap() + goBack() + } + } +} + +extension FlowTests { + func testControls() { + for test in [ + _testDatePicker, + _testBarButtonItemTap, + _testButtonTap, + _testSegmentedControl, + //_testUISwitch, + _testUITextField, + _testUITextView, + _testSlider + ] { + goToControlsView() + test() + goBack() + } + } + + func goToControlsView() { + let tableView = app.tables.element(boundBy: 0) + + waitForElementToAppear(tableView) + + tableView.cells.allElementsBoundByIndex[5].tap() + } + + func checkDebugLabelValue(_ expected: String) { + let textValue = app.staticTexts["debugLabel"].value as? String + XCTAssertEqual(textValue, expected) + } + + func _testDatePicker() { + let picker = app.datePickers.allElementsBoundByIndex[0] + picker.pickerWheels.element(boundBy: 0).coordinate(withNormalizedOffset: CGVector(dx: 0.49, dy: 0.65)).tap() + picker.pickerWheels.element(boundBy: 1).coordinate(withNormalizedOffset: CGVector(dx: 0.35, dy: 0.64)).tap() + picker.pickerWheels.element(boundBy: 2).coordinate(withNormalizedOffset: CGVector(dx: 0.46, dy: 0.64)).tap() + + wait(interval: 1.0) + + checkDebugLabelValue("UIDatePicker date 1970-01-02 01:01:00 +0000") + } + + func _testBarButtonItemTap() { + app.navigationBars.buttons["TapMe"].tap() + checkDebugLabelValue("UIBarButtonItem Tapped") + } + + func _testButtonTap() { + app.scrollViews.buttons["TapMe"].tap() + checkDebugLabelValue("UIButton Tapped") + } + + func _testSegmentedControl() { + let segmentedControl = app.scrollViews.segmentedControls.allElementsBoundByIndex[0] + segmentedControl.buttons["Second"].tap() + checkDebugLabelValue("UISegmentedControl value 1") + segmentedControl.buttons["First"].tap() + checkDebugLabelValue("UISegmentedControl value 0") + } + + func _testUISwitch() { + let switchControl = app.switches.allElementsBoundByIndex[0] + switchControl.tap() + checkDebugLabelValue("UISwitch value false") + switchControl.tap() + checkDebugLabelValue("UISwitch value true") + } + + func _testUITextField() { + let textField = app.textFields.allElementsBoundByIndex[0] + textField.tap() + textField.typeText("f") + checkDebugLabelValue("UITextField text f") + } + + func _testUITextView() { + let textView = app.textViews.allElementsBoundByIndex[0] + textView.tap() + textView.typeText("f") + checkDebugLabelValue("UITextView text f") + } + + func _testSlider() { + let slider = app.sliders.allElementsBoundByIndex[0] + slider.adjust(toNormalizedSliderPosition: 0) + checkDebugLabelValue("UISlider value 0.0") + } +} + +extension FlowTests { + + func goBack() { + let navigationBar = app.navigationBars.allElementsBoundByIndex[0] + navigationBar.coordinate(withNormalizedOffset: .zero).withOffset(CGVector(dx: 20, dy: 30)).tap() + wait(interval: 1.5) + } + + func waitForElementToAppear(_ element: XCUIElement, timeout: TimeInterval = 2, file: String = #file, line: UInt = #line) { + let existsPredicate = NSPredicate(format: "exists == true") + + expectation(for: existsPredicate, + evaluatedWith: element, + handler: nil) + + waitForExpectations(timeout: timeout) { (error) -> Void in + if (error != nil) { + let message = "Failed to find \(element) after \(timeout) seconds." + self.recordFailure(withDescription: message, inFile: file, atLine: line, expected: true) + } + } + } + + func wait(interval: TimeInterval) { + RunLoop.current.run(until: Date().addingTimeInterval(interval)) + } + +} + +extension XCUIElement { + func clearText() { + let backspace = "\u{8}" + let backspaces = Array(((self.value as? String) ?? "").characters).map { _ in backspace } + self.typeText(backspaces.joined(separator: "")) + } + + func typeSlow(text: String) { + for i in text.characters { + self.typeText(String(i)) + } + } +} diff --git a/RxExample/RxExample-iOSUITests/Info.plist b/RxExample/RxExample-iOSUITests/Info.plist new file mode 100644 index 00000000..6c6c23c4 --- /dev/null +++ b/RxExample/RxExample-iOSUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/RxExample/RxExample.xcodeproj/project.pbxproj b/RxExample/RxExample.xcodeproj/project.pbxproj index 7916a4e3..5f92a0e9 100644 --- a/RxExample/RxExample.xcodeproj/project.pbxproj +++ b/RxExample/RxExample.xcodeproj/project.pbxproj @@ -96,6 +96,7 @@ C88BB8C71B07E6C90064D411 /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E3C2321B03605B0010338D /* Dependencies.swift */; }; C88BB8CA1B07E6C90064D411 /* WikipediaAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C86E2F3B1AE5A0CA00C31024 /* WikipediaAPI.swift */; }; C88BB8CC1B07E6C90064D411 /* WikipediaPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C86E2F3C1AE5A0CA00C31024 /* WikipediaPage.swift */; }; + C88C2B2A1D67EC5200B01A98 /* FlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C88C2B291D67EC5200B01A98 /* FlowTests.swift */; }; C890A65D1AEC084100AFF7E6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C890A65C1AEC084100AFF7E6 /* ViewController.swift */; }; C89634081B95BE50002AE38C /* RxBlocking.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C8A468EF1B8A8BD000BF917B /* RxBlocking.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C89634091B95BE50002AE38C /* RxCocoa.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C8A468ED1B8A8BCC00BF917B /* RxCocoa.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -125,6 +126,8 @@ C8C46DAA1B47F7110020D71E /* WikipediaSearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C46DA51B47F7110020D71E /* WikipediaSearchCell.swift */; }; C8C46DAB1B47F7110020D71E /* WikipediaSearchCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C8C46DA61B47F7110020D71E /* WikipediaSearchCell.xib */; }; C8C46DAC1B47F7110020D71E /* WikipediaSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C46DA71B47F7110020D71E /* WikipediaSearchViewController.swift */; }; + C8CDF0AB1D67F8FC00C18F99 /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CDF0AA1D67F8FC00C18F99 /* UIApplication+Extensions.swift */; }; + C8CDF0C11D688DF700C18F99 /* UITableView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CDF0C01D688DF700C18F99 /* UITableView+Extensions.swift */; }; C8D132151C42B54B00B59FFF /* UIImagePickerController+RxCreate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D132141C42B54B00B59FFF /* UIImagePickerController+RxCreate.swift */; }; C8DF92CD1B0B2F84009BCF9A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DF92C81B0B2F84009BCF9A /* AppDelegate.swift */; }; C8DF92DF1B0B328B009BCF9A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DF92DE1B0B328B009BCF9A /* AppDelegate.swift */; }; @@ -288,6 +291,13 @@ remoteGlobalIDString = C88FA53F1C25C4CC00CCFEA4; remoteInfo = "RxTests-watchOS"; }; + C88C2B2C1D67EC5200B01A98 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C83366D51AD0293800C668A7 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C83366DC1AD0293800C668A7; + remoteInfo = "RxExample-iOS"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -386,6 +396,9 @@ C86E2F3C1AE5A0CA00C31024 /* WikipediaPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = WikipediaPage.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; C86E2F3D1AE5A0CA00C31024 /* WikipediaSearchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = WikipediaSearchResult.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; C88BB8DC1B07E6C90064D411 /* RxExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RxExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C88C2B271D67EC5200B01A98 /* RxExample-iOSUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "RxExample-iOSUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + C88C2B291D67EC5200B01A98 /* FlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowTests.swift; sourceTree = ""; }; + C88C2B2B1D67EC5200B01A98 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C890A65C1AEC084100AFF7E6 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; C8984CCE1C36BC3E001E4272 /* NumberCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberCell.swift; sourceTree = ""; }; C8984CCF1C36BC3E001E4272 /* NumberSectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberSectionView.swift; sourceTree = ""; }; @@ -410,6 +423,8 @@ C8C46DA51B47F7110020D71E /* WikipediaSearchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WikipediaSearchCell.swift; sourceTree = ""; }; C8C46DA61B47F7110020D71E /* WikipediaSearchCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = WikipediaSearchCell.xib; sourceTree = ""; }; C8C46DA71B47F7110020D71E /* WikipediaSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WikipediaSearchViewController.swift; sourceTree = ""; }; + C8CDF0AA1D67F8FC00C18F99 /* UIApplication+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extensions.swift"; sourceTree = ""; }; + C8CDF0C01D688DF700C18F99 /* UITableView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+Extensions.swift"; sourceTree = ""; }; C8D132141C42B54B00B59FFF /* UIImagePickerController+RxCreate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImagePickerController+RxCreate.swift"; sourceTree = ""; }; C8DF92C81B0B2F84009BCF9A /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C8DF92DE1B0B328B009BCF9A /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -456,6 +471,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C88C2B241D67EC5200B01A98 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -548,6 +570,7 @@ C8B290A51C959D2900E923D0 /* RxDataSources */, C83366DF1AD0293800C668A7 /* RxExample */, C849EF621C3190360048AC4A /* RxExample-iOSTests */, + C88C2B281D67EC5200B01A98 /* RxExample-iOSUITests */, C83366DE1AD0293800C668A7 /* Products */, ); sourceTree = ""; @@ -558,6 +581,7 @@ C83366DD1AD0293800C668A7 /* RxExample-iOS.app */, C88BB8DC1B07E6C90064D411 /* RxExample.app */, C849EF611C3190360048AC4A /* RxExample-iOSTests.xctest */, + C88C2B271D67EC5200B01A98 /* RxExample-iOSUITests.xctest */, ); name = Products; sourceTree = ""; @@ -773,6 +797,15 @@ path = GitHubSignup; sourceTree = ""; }; + C88C2B281D67EC5200B01A98 /* RxExample-iOSUITests */ = { + isa = PBXGroup; + children = ( + C88C2B291D67EC5200B01A98 /* FlowTests.swift */, + C88C2B2B1D67EC5200B01A98 /* Info.plist */, + ); + path = "RxExample-iOSUITests"; + sourceTree = ""; + }; C8984CCD1C36BC3E001E4272 /* TableViewPartialUpdates */ = { isa = PBXGroup; children = ( @@ -835,6 +868,8 @@ C8DF92E11B0B32DA009BCF9A /* Main.storyboard */, C8DF92E21B0B32DA009BCF9A /* RootViewController.swift */, C8DF92C81B0B2F84009BCF9A /* AppDelegate.swift */, + C8CDF0AA1D67F8FC00C18F99 /* UIApplication+Extensions.swift */, + C8CDF0C01D688DF700C18F99 /* UITableView+Extensions.swift */, ); path = iOS; sourceTree = ""; @@ -905,13 +940,31 @@ productReference = C88BB8DC1B07E6C90064D411 /* RxExample.app */; productType = "com.apple.product-type.application"; }; + C88C2B261D67EC5200B01A98 /* RxExample-iOSUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = C88C2B2E1D67EC5200B01A98 /* Build configuration list for PBXNativeTarget "RxExample-iOSUITests" */; + buildPhases = ( + C88C2B231D67EC5200B01A98 /* Sources */, + C88C2B241D67EC5200B01A98 /* Frameworks */, + C88C2B251D67EC5200B01A98 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + C88C2B2D1D67EC5200B01A98 /* PBXTargetDependency */, + ); + name = "RxExample-iOSUITests"; + productName = "RxExample-iOSUITests"; + productReference = C88C2B271D67EC5200B01A98 /* RxExample-iOSUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ C83366D51AD0293800C668A7 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0720; + LastSwiftUpdateCheck = 0800; LastUpgradeCheck = 0800; ORGANIZATIONNAME = "Krunoslav Zaher"; TargetAttributes = { @@ -922,12 +975,19 @@ }; C849EF601C3190360048AC4A = { CreatedOnToolsVersion = 7.2; + DevelopmentTeam = 783T66X79Y; LastSwiftMigration = 0800; TestTargetID = C83366DC1AD0293800C668A7; }; C88BB8B91B07E6C90064D411 = { LastSwiftMigration = 0800; }; + C88C2B261D67EC5200B01A98 = { + CreatedOnToolsVersion = 8.0; + DevelopmentTeam = 783T66X79Y; + ProvisioningStyle = Automatic; + TestTargetID = C83366DC1AD0293800C668A7; + }; }; }; buildConfigurationList = C83366D81AD0293800C668A7 /* Build configuration list for PBXProject "RxExample" */; @@ -952,6 +1012,7 @@ C83366DC1AD0293800C668A7 /* RxExample-iOS */, C88BB8B91B07E6C90064D411 /* RxExample-OSX */, C849EF601C3190360048AC4A /* RxExample-iOSTests */, + C88C2B261D67EC5200B01A98 /* RxExample-iOSUITests */, ); }; /* End PBXProject section */ @@ -1128,6 +1189,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C88C2B251D67EC5200B01A98 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1172,6 +1240,7 @@ C84780061D29DE8C0074454A /* RxTableViewSectionedReloadDataSource.swift in Sources */, C843A0901C1CE39900CBA4BD /* GitHubSearchRepositoriesViewController.swift in Sources */, C803973A1BD3E17D009D8B26 /* ActivityIndicator.swift in Sources */, + C8CDF0C11D688DF700C18F99 /* UITableView+Extensions.swift in Sources */, C849EF821C3193B10048AC4A /* GithubSignupViewModel1.swift in Sources */, C864BADD1C3332F10083833C /* TableViewWithEditingCommandsViewController.swift in Sources */, C8F8C4A01C277F5A0047640B /* Operation.swift in Sources */, @@ -1193,6 +1262,7 @@ C8984CD51C36BC3E001E4272 /* PartialUpdatesViewController.swift in Sources */, 8479BC721C3BDAD400FB8B54 /* ImagePickerController.swift in Sources */, C8477FFF1D29DE8C0074454A /* SectionModelType.swift in Sources */, + C8CDF0AB1D67F8FC00C18F99 /* UIApplication+Extensions.swift in Sources */, C864BAE11C3332F10083833C /* User.swift in Sources */, 0744CDED1C4DB78600720FD2 /* GeolocationViewController.swift in Sources */, C83367231AD029AE00C668A7 /* Example.swift in Sources */, @@ -1264,6 +1334,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C88C2B231D67EC5200B01A98 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C88C2B2A1D67EC5200B01A98 /* FlowTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1272,6 +1350,11 @@ target = C83366DC1AD0293800C668A7 /* RxExample-iOS */; targetProxy = C849EF661C3190360048AC4A /* PBXContainerItemProxy */; }; + C88C2B2D1D67EC5200B01A98 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C83366DC1AD0293800C668A7 /* RxExample-iOS */; + targetProxy = C88C2B2C1D67EC5200B01A98 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -1407,6 +1490,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 783T66X79Y; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "RxExample-iOSTests/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.2; @@ -1423,6 +1507,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 783T66X79Y; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "RxExample-iOSTests/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.2; @@ -1440,6 +1525,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 783T66X79Y; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "RxExample-iOSTests/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.2; @@ -1477,6 +1563,69 @@ }; name = Release; }; + C88C2B2F1D67EC5200B01A98 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 783T66X79Y; + INFOPLIST_FILE = "RxExample-iOSUITests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "rx.RxExample-iOSUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = "RxExample-iOS"; + }; + name = Debug; + }; + C88C2B301D67EC5200B01A98 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 783T66X79Y; + INFOPLIST_FILE = "RxExample-iOSUITests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "rx.RxExample-iOSUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = "RxExample-iOS"; + }; + name = Release; + }; + C88C2B311D67EC5200B01A98 /* Release-Tests */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 783T66X79Y; + INFOPLIST_FILE = "RxExample-iOSUITests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "rx.RxExample-iOSUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = "RxExample-iOS"; + }; + name = "Release-Tests"; + }; C8DF92ED1B0B3DFA009BCF9A /* Release-Tests */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1594,6 +1743,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + C88C2B2E1D67EC5200B01A98 /* Build configuration list for PBXNativeTarget "RxExample-iOSUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C88C2B2F1D67EC5200B01A98 /* Debug */, + C88C2B301D67EC5200B01A98 /* Release */, + C88C2B311D67EC5200B01A98 /* Release-Tests */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = C83366D51AD0293800C668A7 /* Project object */; diff --git a/RxExample/RxExample.xcodeproj/xcshareddata/xcschemes/RxExample-iOS.xcscheme b/RxExample/RxExample.xcodeproj/xcshareddata/xcschemes/RxExample-iOS.xcscheme index 8d7f7596..256c115b 100644 --- a/RxExample/RxExample.xcodeproj/xcshareddata/xcschemes/RxExample-iOS.xcscheme +++ b/RxExample/RxExample.xcodeproj/xcshareddata/xcschemes/RxExample-iOS.xcscheme @@ -38,6 +38,16 @@ ReferencedContainer = "container:RxExample.xcodeproj"> + + + + switchValue + /***⚠️Unlike other controls, Apple is reusing instances of UISwitch or a there is a leak, + so underlying observable sequence won't complete when nothing holds a strong reference + to UISwitch.⚠️***/ + (switcher.rx.value <-> switchValue).addDisposableTo(disposeBag) switchValue.asObservable() .subscribe(onNext: { [weak self] x in @@ -98,7 +102,7 @@ class APIWrappersViewController: ViewController { switcher.rx.value .bindTo(activityIndicator.rx.animating) .addDisposableTo(disposeBag) - + */ // MARK: UIButton diff --git a/RxExample/RxExample/Examples/TableViewPartialUpdates/PartialUpdatesViewController.swift b/RxExample/RxExample/Examples/TableViewPartialUpdates/PartialUpdatesViewController.swift index 13f30cc5..a5e63179 100644 --- a/RxExample/RxExample/Examples/TableViewPartialUpdates/PartialUpdatesViewController.swift +++ b/RxExample/RxExample/Examples/TableViewPartialUpdates/PartialUpdatesViewController.swift @@ -54,14 +54,16 @@ class PartialUpdatesViewController : ViewController { override func viewDidLoad() { super.viewDidLoad() + self.navigationItem.rightBarButtonItem?.accessibilityLabel = "Randomize" + // For UICollectionView, if another animation starts before previous one is finished, it will sometimes crash :( // It's not deterministic (because Randomizer generates deterministic updates), and if you click fast // It sometimes will and sometimes wont crash, depending on tapping speed. // I guess you can maybe try some tricks with timeout, hard to tell :( That's on Apple side. if generateCustomSize { - let nSections = 10 - let nItems = 100 + let nSections = UIApplication.isInUITest ? 10 : 10 + let nItems = UIApplication.isInUITest ? 20 : 100 var sections = [AnimatableSectionModel]() diff --git a/RxExample/RxExample/Examples/TableViewWithEditingCommands/DetailViewController.swift b/RxExample/RxExample/Examples/TableViewWithEditingCommands/DetailViewController.swift index bcf0c105..0deecdd3 100644 --- a/RxExample/RxExample/Examples/TableViewWithEditingCommands/DetailViewController.swift +++ b/RxExample/RxExample/Examples/TableViewWithEditingCommands/DetailViewController.swift @@ -23,7 +23,7 @@ class DetailViewController: ViewController { override func viewDidLoad() { super.viewDidLoad() - imageView.makeRoundedCorners(5) + imageView.makeRoundedCorners(40) let url = URL(string: user.imageURL)! let request = URLRequest(url: url) diff --git a/RxExample/RxExample/Examples/TableViewWithEditingCommands/UIImageView+Extensions.swift b/RxExample/RxExample/Examples/TableViewWithEditingCommands/UIImageView+Extensions.swift index 553337e8..dd850aff 100644 --- a/RxExample/RxExample/Examples/TableViewWithEditingCommands/UIImageView+Extensions.swift +++ b/RxExample/RxExample/Examples/TableViewWithEditingCommands/UIImageView+Extensions.swift @@ -11,10 +11,11 @@ import UIKit extension UIImageView { func makeRoundedCorners(_ radius: CGFloat) { - self.layer.cornerRadius = self.frame.size.width / 2 - self.layer.borderColor = UIColor.darkGray.cgColor - self.layer.borderWidth = radius + self.layer.cornerRadius = radius self.layer.masksToBounds = true } - + + func makeRoundedCorners() { + self.makeRoundedCorners(self.frame.size.width / 2) + } } diff --git a/RxExample/RxExample/Examples/WikipediaImageSearch/Views/WikipediaSearchViewController.swift b/RxExample/RxExample/Examples/WikipediaImageSearch/Views/WikipediaSearchViewController.swift index 25a85525..260ce243 100644 --- a/RxExample/RxExample/Examples/WikipediaImageSearch/Views/WikipediaSearchViewController.swift +++ b/RxExample/RxExample/Examples/WikipediaImageSearch/Views/WikipediaSearchViewController.swift @@ -13,21 +13,9 @@ import RxCocoa #endif class WikipediaSearchViewController: ViewController { - @IBOutlet var searchBarContainer: UIView! - - private let searchController = UISearchController(searchResultsController: UITableViewController()) - - private var resultsViewController: UITableViewController { - return (self.searchController.searchResultsController as? UITableViewController)! - } - - private var resultsTableView: UITableView { - return self.resultsViewController.tableView! - } - - private var searchBar: UISearchBar { - return self.searchController.searchBar - } + @IBOutlet var searchBar: UISearchBar! + @IBOutlet var resultsTableView: UITableView! + @IBOutlet var emptyView: UIView! override func awakeFromNib() { super.awakeFromNib() @@ -37,15 +25,8 @@ class WikipediaSearchViewController: ViewController { override func viewDidLoad() { super.viewDidLoad() - - let searchBar = self.searchBar - let searchBarContainer = self.searchBarContainer - searchBarContainer?.addSubview(searchBar) - searchBar.frame = (searchBarContainer?.bounds)! - searchBar.autoresizingMask = .flexibleWidth - - resultsViewController.edgesForExtendedLayout = UIRectEdge() + self.edgesForExtendedLayout = .all configureTableDataSource() configureKeyboardDismissesOnScroll() @@ -57,14 +38,12 @@ class WikipediaSearchViewController: ViewController { resultsTableView.register(UINib(nibName: "WikipediaSearchCell", bundle: nil), forCellReuseIdentifier: "WikipediaSearchCell") resultsTableView.rowHeight = 194 + resultsTableView.hideEmptyCells() // This is for clarity only, don't use static dependencies let API = DefaultWikipediaAPI.sharedAPI - resultsTableView.delegate = nil - resultsTableView.dataSource = nil - - searchBar.rx.text + let results = searchBar.rx.text .asDriver() .throttle(0.3) .distinctUntilChanged() @@ -78,24 +57,27 @@ class WikipediaSearchViewController: ViewController { .map { results in results.map(SearchResultViewModel.init) } + + results .drive(resultsTableView.rx.items(cellIdentifier: "WikipediaSearchCell", cellType: WikipediaSearchCell.self)) { (_, viewModel, cell) in cell.viewModel = viewModel } .addDisposableTo(disposeBag) + + results + .map { $0.count != 0 } + .drive(self.emptyView.rx.hidden) + .addDisposableTo(disposeBag) } func configureKeyboardDismissesOnScroll() { let searchBar = self.searchBar - let searchController = self.searchController resultsTableView.rx.contentOffset .asDriver() - .filter { _ -> Bool in - return !searchController.isBeingPresented - } .drive(onNext: { _ in - if searchBar.isFirstResponder { - _ = searchBar.resignFirstResponder() + if searchBar?.isFirstResponder ?? false { + _ = searchBar?.resignFirstResponder() } }) .addDisposableTo(disposeBag) diff --git a/RxExample/RxExample/ViewController.swift b/RxExample/RxExample/ViewController.swift index 29d6e198..1a7e1cdc 100644 --- a/RxExample/RxExample/ViewController.swift +++ b/RxExample/RxExample/ViewController.swift @@ -79,7 +79,7 @@ class ViewController: OSViewController { If somebody knows more about why this delay happens, you can make a PR with explanation here. */ - let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(20) + let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(UIApplication.isInUITest ? 1000 : 10) mainQueue.asyncAfter (deadline: when) { @@ -92,7 +92,7 @@ class ViewController: OSViewController { // // If this crashes when you've been clicking slowly, then it would be interesting to find out why. // ¯\_(ツ)_/¯ - assert(resourceCount <= numberOfResourcesThatShouldRemain, "Resources weren't cleaned properly") + assert(resourceCount <= numberOfResourcesThatShouldRemain, "Resources weren't cleaned properly, \(resourceCount) remaned, \(numberOfResourcesThatShouldRemain) expected") } #endif diff --git a/RxExample/RxExample/iOS/AppDelegate.swift b/RxExample/RxExample/iOS/AppDelegate.swift index 6b93f620..28e7fd6b 100644 --- a/RxExample/RxExample/iOS/AppDelegate.swift +++ b/RxExample/RxExample/iOS/AppDelegate.swift @@ -12,6 +12,11 @@ import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - + + func applicationDidFinishLaunching(_ application: UIApplication) { + if UIApplication.isInUITest { + UIView.setAnimationsEnabled(false) + } + } } diff --git a/RxExample/RxExample/iOS/Main.storyboard b/RxExample/RxExample/iOS/Main.storyboard index f7a3181c..39ff3edd 100644 --- a/RxExample/RxExample/iOS/Main.storyboard +++ b/RxExample/RxExample/iOS/Main.storyboard @@ -1,17 +1,19 @@ - + - + + + - - + + @@ -32,58 +34,51 @@ - + - - - + @@ -126,34 +121,34 @@ - + - + - + - + - + @@ -180,25 +175,24 @@ - + - - + - + - + @@ -207,7 +201,7 @@ - + @@ -233,18 +227,17 @@ - + - - + - + - + @@ -254,7 +247,7 @@ - + @@ -280,36 +273,25 @@ - + - - - - - - - - - - - + @@ -326,7 +308,7 @@ - + @@ -337,67 +319,57 @@ - + - - + - - - - - + - + @@ -452,20 +424,17 @@ To do this automatically, check out the corresponding `Driver` example. - + - - + - - + - - + @@ -474,20 +443,18 @@ To do this automatically, check out the corresponding `Driver` example. - + - @@ -499,18 +466,17 @@ To do this automatically, check out the corresponding `Driver` example. - + - - + + @@ -521,7 +487,7 @@ To do this automatically, check out the corresponding `Driver` example. - + @@ -538,7 +504,7 @@ To do this automatically, check out the corresponding `Driver` example. - + @@ -559,9 +525,9 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -569,24 +535,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -596,24 +562,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -623,24 +589,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -650,24 +616,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -677,24 +643,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -704,24 +670,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -731,24 +697,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -758,24 +724,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -789,24 +755,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -816,24 +782,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -843,24 +809,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -870,24 +836,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -901,24 +867,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -928,24 +894,24 @@ To do this automatically, check out the corresponding `Driver` example. - + - + @@ -977,45 +943,48 @@ To do this automatically, check out the corresponding `Driver` example. - + - - - - - - - + + + + + + + + - + - - + + + - + - + + + - + @@ -1026,36 +995,27 @@ This is only showcase app, not intended for production purposes. - + - - - - - - - - - + + - @@ -1064,7 +1024,6 @@ This is only showcase app, not intended for production purposes. - @@ -1072,16 +1031,14 @@ This is only showcase app, not intended for production purposes. - - + @@ -1093,43 +1050,37 @@ This is only showcase app, not intended for production purposes. - + - + @@ -1141,12 +1092,12 @@ This is only showcase app, not intended for production purposes. - + @@ -1174,21 +1125,20 @@ This is only showcase app, not intended for production purposes. - + - - + @@ -1219,11 +1169,10 @@ This is only showcase app, not intended for production purposes. - + - @@ -1232,7 +1181,6 @@ This is only showcase app, not intended for production purposes. - @@ -1241,7 +1189,6 @@ This is only showcase app, not intended for production purposes. - @@ -1250,27 +1197,24 @@ This is only showcase app, not intended for production purposes. - - + - + @@ -1308,256 +1252,235 @@ This is only showcase app, not intended for production purposes. - + - + @@ -1626,7 +1549,6 @@ This is only showcase app, not intended for production purposes. - @@ -1664,29 +1586,25 @@ This is only showcase app, not intended for production purposes. - + - - + @@ -1720,30 +1638,26 @@ This is only showcase app, not intended for production purposes. - + - - + @@ -1768,29 +1682,25 @@ This is only showcase app, not intended for production purposes. - - + @@ -1826,7 +1735,7 @@ This is only showcase app, not intended for production purposes. - + @@ -1860,25 +1769,24 @@ This is only showcase app, not intended for production purposes. - + - - + - + - + @@ -1887,7 +1795,7 @@ This is only showcase app, not intended for production purposes. - + @@ -1913,67 +1821,57 @@ This is only showcase app, not intended for production purposes. - + - - + - - - - - + - + @@ -2017,7 +1915,12 @@ Check out the same example using vanilla observable sequences. - + + + + + + diff --git a/RxExample/RxExample/iOS/UIApplication+Extensions.swift b/RxExample/RxExample/iOS/UIApplication+Extensions.swift new file mode 100644 index 00000000..36adf52a --- /dev/null +++ b/RxExample/RxExample/iOS/UIApplication+Extensions.swift @@ -0,0 +1,15 @@ +// +// UIApplication+Extensions.swift +// RxExample +// +// Created by Krunoslav Zaher on 8/20/16. +// Copyright © 2016 Krunoslav Zaher. All rights reserved. +// + +import UIKit + +extension UIApplication { + static var isInUITest: Bool { + return ProcessInfo.processInfo.environment["isUITest"] != nil; + } +} diff --git a/RxExample/RxExample/iOS/UITableView+Extensions.swift b/RxExample/RxExample/iOS/UITableView+Extensions.swift new file mode 100644 index 00000000..ce220f6e --- /dev/null +++ b/RxExample/RxExample/iOS/UITableView+Extensions.swift @@ -0,0 +1,15 @@ +// +// UITableView+Extensions.swift +// RxExample +// +// Created by Krunoslav Zaher on 8/20/16. +// Copyright © 2016 Krunoslav Zaher. All rights reserved. +// + +import UIKit + +extension UITableView { + func hideEmptyCells() { + self.tableFooterView = UIView(frame: .zero) + } +}