Merge lp:~dude-from-by/openlp/my-ios-remote into lp:~danielborges93/openlp/ios-remote

Proposed by andrei
Status: Merged
Approved by: Daniel Borges
Approved revision: no longer in the source branch.
Merged at revision: 63
Proposed branch: lp:~dude-from-by/openlp/my-ios-remote
Merge into: lp:~danielborges93/openlp/ios-remote
Diff against target: 1480 lines (+657/-411)
15 files modified
OLP RemoteTests/PollAPITests.swift (+0/-128)
OLP RemoteTests/SearchViewModelTests.swift (+134/-0)
Podfile (+2/-0)
Podfile.lock (+10/-1)
Remote.xcodeproj/project.pbxproj (+24/-8)
Remote/AppDelegate.swift (+8/-14)
Remote/Classes/Controller/Search/SearchResultViewController.swift (+56/-51)
Remote/Classes/Controller/Search/SearchResultsViewModel.swift (+58/-0)
Remote/Classes/Controller/Search/SearchViewController.swift (+45/-39)
Remote/Classes/Controller/Search/SearchViewModel.swift (+62/-0)
Remote/Classes/Network/AddAPI.swift (+33/-21)
Remote/Classes/Network/LiveAPI.swift (+30/-21)
Remote/Classes/Network/PollAPI.swift (+23/-102)
Remote/Classes/Network/SearchAPI.swift (+32/-22)
Remote/Classes/Network/Service.swift (+140/-4)
To merge this branch: bzr merge lp:~dude-from-by/openlp/my-ios-remote
Reviewer Review Type Date Requested Status
Daniel Borges Approve
Review via email: mp+322017@code.launchpad.net

This proposal supersedes a proposal from 2017-03-30.

Description of the change

Added ReactiveCocoa to the project, latest version and moved several services and search screen to it.
The main purpose of this changes is to ease testing. ReactiveCocoa provides bindings so we can rewrite project with MVVM paradigm and add tests to model and viewModel, where the main business logics lives. I've added SearchViewModel as an illustration. I am new to the swift version of this framework, so code might be clumsy. Added tests to searchviewmodel. Moved SearchResultsViewController to MVVM.

To post a comment you must log in.
Revision history for this message
Daniel Borges (danielborges93) wrote :

Hi Andrei. Sorry for delay.
Follows some things that can be fixed:

- The search is not working on iOS 8;
- Keep te search button aways enabled. Search with empty textfield will do a full search.

review: Needs Fixing
Revision history for this message
andrei (dude-from-by) wrote :

> Hi Andrei. Sorry for delay.
> Follows some things that can be fixed:
>
> - The search is not working on iOS 8;
> - Keep te search button aways enabled. Search with empty textfield will do a
> full search.

- fixed for ios8
- made search button enabled.

I don't like an option to keep it enabled - it is counter-intuitive. I've looked through many apps, most Apple apps I use - none of them uses such solution. Some of them display full list initially and non of them enables search button if there is nothing in the input field. As for now I will rollback to your solution, but in the future we may come up with one that is easier to understand for user.

Revision history for this message
andrei (dude-from-by) wrote :

> Hi Andrei. Sorry for delay.
> Follows some things that can be fixed:
>
> - The search is not working on iOS 8;
> - Keep te search button aways enabled. Search with empty textfield will do a
> full search.

- fixed for ios8
- made search button enabled.

I don't like an option to keep it enabled - it is counter-intuitive. I've looked through many apps, most Apple apps I use - none of them uses such solution. Some of them display full list initially and non of them enables search button if there is nothing in the input field. As for now I will rollback to your solution, but in the future we may come up with one that is easier to understand for user.

Revision history for this message
Daniel Borges (danielborges93) wrote :

I agree with you, but I prefer keep it working in this way before to enhance the usability of the search screen.

review: Approve
63. By andrei

Added ReactiveCocoa to the project; Added tests to searchviewmodel; Moved SearchResultsViewController to MVVM.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'OLP RemoteTests/PollAPITests.swift'
2--- OLP RemoteTests/PollAPITests.swift 2017-01-25 15:10:27 +0000
3+++ OLP RemoteTests/PollAPITests.swift 1970-01-01 00:00:00 +0000
4@@ -1,128 +0,0 @@
5-/******************************************************************************
6- * OpenLP iOS Remote *
7- * --------------------------------------------------------------------------- *
8- * Copyright (c) 2008-2016 OpenLP Developers *
9- * --------------------------------------------------------------------------- *
10- * Permission is hereby granted, free of charge, to any person obtaining a *
11- * copy of this software and associated documentation files (the "Software"), *
12- * to deal in the Software without restriction, including without limitation *
13- * the rights to use, copy, modify, merge, publish, distribute, sublicense, *
14- * and/or sell copies of the Software, and to permit persons to whom the *
15- * Software is furnished to do so, subject to the following conditions: *
16- * *
17- * The above copyright notice and this permission notice shall be included in *
18- * all copies or substantial portions of the Software. *
19- * *
20- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
21- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
22- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
23- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
24- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *
25- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *
26- * DEALINGS IN THE SOFTWARE. *
27- ******************************************************************************/
28-
29-import XCTest
30-import Nimble
31-import OHHTTPStubs
32-
33-@testable import Remote
34-
35-class PollAPITests: XCTestCase {
36-
37- override func setUp() {
38- super.setUp()
39-
40- stub(condition: isPath("/api/poll")) { _ in
41- guard let path = OHPathForFile("poll_response.json", type(of: self)) else {
42- preconditionFailure("Could not find expected file in test bundle")
43- }
44-
45- return OHHTTPStubsResponse(
46- fileAtPath: path,
47- statusCode: 200,
48- headers: [ "ContentType": "application/json" ]
49- )
50- }
51- stub(condition: isPath("/api/service/list")) { _ in
52- guard let path = OHPathForFile("service_response.json", type(of: self)) else {
53- preconditionFailure("Could not find expected file in test bundle")
54- }
55-
56- return OHHTTPStubsResponse(
57- fileAtPath: path,
58- statusCode: 200,
59- headers: [ "ContentType": "application/json" ]
60- )
61-
62- }
63- }
64-
65- override func tearDown() {
66-
67- super.tearDown()
68- }
69-
70- class PollAPIServiceDelegateMock: PollAPIServiceDelegate {
71- var serviceList : [ItemModel]?
72- var selectedItem : String?
73- var projectionState: ProjectionState?
74-
75- func updateServiceList(_ items: [ItemModel]) {
76- serviceList = items
77- }
78-
79- func updateSelectedItem(itemId id: String) {
80- selectedItem = id
81- }
82-
83- func updateProjectionState(_ state: ProjectionState) {
84- projectionState = state
85- }
86- }
87-
88- class PollAPISlidesDelegateMock: PollAPISlidesDelegate {
89-
90- var model: [SlideModel]?
91- var selectedSlide: Int?
92- var projectionState: ProjectionState?
93-
94- func updateSlides(_ slides: [SlideModel]) {
95- model = slides
96- }
97-
98- func updateSelectedSlide(_ row: Int) {
99- selectedSlide = row
100- }
101-
102- func updateProjectionState(_ state: ProjectionState) {
103- projectionState = state
104- }
105- }
106-
107- func getPollAPI(_ serviceDelegate: PollAPIServiceDelegate, slidesDelegate: PollAPISlidesDelegate) -> PollAPI {
108- let defaults = TestUtils.defaults()
109- defaults.set(true, forKey:"server.useHTTPS")
110- let settings = UserSettings(defaults: defaults)
111- let controllerAPI = ControllerAPI(settings: settings)
112- let serviceAPI = ServiceAPI(settings: settings)
113- let pollAPI = PollAPI(settings: settings, controllerAPI:controllerAPI, serviceAPI: serviceAPI)
114- pollAPI.serviceDelegate = serviceDelegate
115- pollAPI.slidesDelegate = slidesDelegate
116- return pollAPI
117- }
118-
119- func test_emptyPollResponse() {
120- let serviceDelegate: PollAPIServiceDelegateMock = PollAPIServiceDelegateMock()
121- let slidesDelegate: PollAPISlidesDelegateMock = PollAPISlidesDelegateMock()
122- getPollAPI(serviceDelegate, slidesDelegate: slidesDelegate).poll()
123- expect(serviceDelegate.projectionState).toEventually(equal(ProjectionState.desktop))
124- }
125-
126- func test_pollSelectedItem() {
127- let serviceDelegate: PollAPIServiceDelegateMock = PollAPIServiceDelegateMock()
128- let slidesDelegate: PollAPISlidesDelegateMock = PollAPISlidesDelegateMock()
129- getPollAPI(serviceDelegate, slidesDelegate: slidesDelegate).poll()
130- expect(serviceDelegate.selectedItem).toEventually(equal("item1"))
131- }
132-}
133
134=== added file 'OLP RemoteTests/SearchViewModelTests.swift'
135--- OLP RemoteTests/SearchViewModelTests.swift 1970-01-01 00:00:00 +0000
136+++ OLP RemoteTests/SearchViewModelTests.swift 2017-04-17 16:13:16 +0000
137@@ -0,0 +1,134 @@
138+/******************************************************************************
139+ * OpenLP iOS Remote *
140+ * --------------------------------------------------------------------------- *
141+ * Copyright (c) 2008-2016 OpenLP Developers *
142+ * --------------------------------------------------------------------------- *
143+ * Permission is hereby granted, free of charge, to any person obtaining a *
144+ * copy of this software and associated documentation files (the "Software"), *
145+ * to deal in the Software without restriction, including without limitation *
146+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, *
147+ * and/or sell copies of the Software, and to permit persons to whom the *
148+ * Software is furnished to do so, subject to the following conditions: *
149+ * *
150+ * The above copyright notice and this permission notice shall be included in *
151+ * all copies or substantial portions of the Software. *
152+ * *
153+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
154+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
155+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
156+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
157+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *
158+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *
159+ * DEALINGS IN THE SOFTWARE. *
160+ ******************************************************************************/
161+import XCTest
162+
163+import Result
164+import ReactiveCocoa
165+import ReactiveSwift
166+
167+class SearchAPIMock: SearchAPI {
168+ var returnValue: [ItemModel]?
169+ var testSearchTerm: String?
170+ override func searchSignalProducer(plugin: Plugin, text: String?) -> SignalProducer<[ItemModel], AnyError> {
171+ testSearchTerm = text
172+ return SignalProducer( { (observer, disposable) in
173+ observer.send(value: self.returnValue!)
174+ })
175+ }
176+}
177+
178+
179+class ServiceDelegateMock: PollAPIServiceDelegate {
180+ func updateServiceList(_ items: [ItemModel]) {
181+
182+ }
183+
184+ func updateSelectedItem(itemId id: String) {
185+
186+ }
187+
188+ func updateProjectionState(_ state: ProjectionState) {
189+
190+ }
191+}
192+
193+
194+class SlidesDelegateMock: PollAPISlidesDelegate {
195+ func updateSlides(_ slides: [SlideModel]) {
196+
197+ }
198+
199+ func updateSelectedSlide(_ row: Int) {
200+
201+ }
202+
203+ func updateProjectionState(_ state: ProjectionState) {
204+
205+ }
206+}
207+
208+
209+class ServiceMock: ServiceProtocol {
210+ var searchAPIMock: SearchAPIMock
211+ let settings: UserSettings
212+ let serviceDelegateMock: ServiceDelegateMock
213+ let slidesDelegateMock: SlidesDelegateMock
214+ init(searchAPI: SearchAPIMock) {
215+ self.searchAPIMock = searchAPI
216+ let userDefaults = UserDefaults()
217+ let userSettings = UserSettings(defaults: userDefaults)
218+ settings = userSettings
219+ serviceDelegateMock = ServiceDelegateMock()
220+ slidesDelegateMock = SlidesDelegateMock()
221+ }
222+
223+ var searchAPI: SearchAPI { get { return self.searchAPIMock } }
224+
225+ var serviceAPI: ServiceAPI { get { return ServiceAPI(settings: self.settings) } }
226+ var pollAPI: PollAPI { get { return PollAPI(settings: self.settings) } }
227+ var displayAPI: DisplayAPI { get { return DisplayAPI(settings: self.settings) } }
228+ var controllerAPI: ControllerAPI { get { return ControllerAPI(settings: self.settings) } }
229+ var alertAPI: AlertAPI { get { return AlertAPI(settings: self.settings) } }
230+ var liveAPI: LiveAPI { get { return LiveAPI(settings: self.settings) } }
231+ var addAPI: AddAPI { get { return AddAPI(settings: self.settings) } }
232+ var pauseLoopPolling: Bool { get { return false } set { } }
233+
234+ var serviceDelegate: PollAPIServiceDelegate? { get { return serviceDelegateMock } }
235+ var slidesDelegate: PollAPISlidesDelegate? { get { return slidesDelegateMock } }
236+}
237+
238+
239+class SearchViewModelTests: XCTestCase {
240+
241+ override func setUp() {
242+ super.setUp()
243+ }
244+
245+ override func tearDown() {
246+ super.tearDown()
247+ }
248+
249+ func searchApiMock()->SearchAPIMock {
250+ let userDefaults = UserDefaults()
251+ let userSettings = UserSettings(defaults: userDefaults)
252+ let apiMock = SearchAPIMock(settings: userSettings)
253+ return apiMock
254+ }
255+
256+
257+ func testSearchResultsUpdate() {
258+ let api = searchApiMock()
259+ let service = ServiceMock(searchAPI:api)
260+ let model = SearchViewModel(service: service)
261+ let item1 = ItemModel()
262+ item1.title = "title"
263+ item1.plugin = "Songs"
264+ api.returnValue = [item1]
265+ model.searchTerm.value = "find something new"
266+ model.search()
267+ XCTAssert(model.searchResult.value.count == 1, "should be only one result")
268+ XCTAssertEqual(model.searchResult.value[0].title, item1.title, "result should be updated")
269+ XCTAssertEqual(api.testSearchTerm, model.searchTerm.value, "Should search correct value")
270+ }
271+}
272
273=== modified file 'Podfile'
274--- Podfile 2017-01-25 15:10:27 +0000
275+++ Podfile 2017-04-17 16:13:16 +0000
276@@ -11,6 +11,7 @@
277 pod 'UITextView+Placeholder'
278 pod 'BlockLooper', :git=>'https://github.com/ashender/BlockLooper.git'
279 pod 'CWStatusBarNotification'
280+ pod 'ReactiveCocoa'
281 end
282
283 target 'OLP RemoteTests' do
284@@ -23,5 +24,6 @@
285 pod 'Nimble'
286 pod 'AlamofireObjectMapper'
287 pod 'OHHTTPStubs/Swift'
288+ pod 'ReactiveCocoa'
289 end
290
291
292=== modified file 'Podfile.lock'
293--- Podfile.lock 2017-01-25 15:10:27 +0000
294+++ Podfile.lock 2017-04-17 16:13:16 +0000
295@@ -23,6 +23,11 @@
296 - OHHTTPStubs/OHPathHelpers (5.2.3)
297 - OHHTTPStubs/Swift (5.2.3):
298 - OHHTTPStubs/Core
299+ - ReactiveCocoa (5.0.0):
300+ - ReactiveSwift (~> 1.0.0)
301+ - ReactiveSwift (1.0.0):
302+ - Result (~> 3.1)
303+ - Result (3.1.0)
304 - TPKeyboardAvoiding (1.3.1)
305 - UITextView+Placeholder (1.2.0)
306
307@@ -35,6 +40,7 @@
308 - Nimble
309 - OHHTTPStubs
310 - OHHTTPStubs/Swift
311+ - ReactiveCocoa
312 - TPKeyboardAvoiding
313 - UITextView+Placeholder
314
315@@ -56,9 +62,12 @@
316 Nimble: 415e3aa3267e7bc2c96b05fa814ddea7bb686a29
317 ObjectMapper: 9e385c2295bcc4e16eabbcfc85db801442bba545
318 OHHTTPStubs: e238cd5b66d8efa51c861db45895de8fe079f4a7
319+ ReactiveCocoa: 60ef0da915b4f70c34ebecc3f4fb8f96af0e53d6
320+ ReactiveSwift: f391724ee318a2cfd3e37dfb041cd49ecf4e7869
321+ Result: 4e3ed5995ed94d0cd6a09be9a431fce3f3624bbf
322 TPKeyboardAvoiding: 0d6af20e95e2850f4c621841225b59ec7d8dd852
323 UITextView+Placeholder: 77680995fcdd07c3f52ec92fe1150874a2ac89ff
324
325-PODFILE CHECKSUM: 33c246100fdb079469ba0c7b05499d31b72d3722
326+PODFILE CHECKSUM: 8bcb0a7a00d7e61acff50967f2778c47912a0880
327
328 COCOAPODS: 1.1.1
329
330=== modified file 'Remote.xcodeproj/project.pbxproj'
331--- Remote.xcodeproj/project.pbxproj 2017-02-19 05:21:00 +0000
332+++ Remote.xcodeproj/project.pbxproj 2017-04-17 16:13:16 +0000
333@@ -51,10 +51,10 @@
334 750831BD1CFF597D00CA9299 /* slides_response.json in Resources */ = {isa = PBXBuildFile; fileRef = 750831BC1CFF597D00CA9299 /* slides_response.json */; };
335 75219D021E276111001B6BC8 /* TabSwitchingToRightAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75219D011E276111001B6BC8 /* TabSwitchingToRightAnimationController.swift */; };
336 755A61C91E04878F00D035D4 /* ServiceViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A61C81E04878F00D035D4 /* ServiceViewControllerDelegate.swift */; };
337+ 755B3FE21E8CFEE000DA570C /* SearchResultsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755B3FE11E8CFEE000DA570C /* SearchResultsViewModel.swift */; };
338 755C78CE1CCE9080001CF70D /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755C78CD1CCE9080001CF70D /* Service.swift */; };
339 75667B281CFCA8B1008FDFE0 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4640F2901BE4274A00E2AA61 /* API.swift */; };
340 75667B291CFCA8C9008FDFE0 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CC0CEC1BE6972300423EB4 /* UserSettings.swift */; };
341- 75667B2B1CFCAE44008FDFE0 /* PollAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75667B2A1CFCAE44008FDFE0 /* PollAPITests.swift */; };
342 75667B4C1CFEE85A008FDFE0 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75667B4B1CFEE85A008FDFE0 /* TestUtils.swift */; };
343 75667B4D1CFEE92C008FDFE0 /* ControllerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4623B69B1C07C569007FFE2B /* ControllerAPI.swift */; };
344 75667B4E1CFEE930008FDFE0 /* PollAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4640F2921BE429B900E2AA61 /* PollAPI.swift */; };
345@@ -76,8 +76,13 @@
346 75667B5E1CFEFF86008FDFE0 /* ErrorUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46FD378E1C79867C00DED423 /* ErrorUtil.swift */; };
347 75667B5F1CFEFFA7008FDFE0 /* DisplayAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4623B6921C078F70007FFE2B /* DisplayAPI.swift */; };
348 756FA0FD1E28FF060011AE6F /* SlideCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756FA0FC1E28FF060011AE6F /* SlideCell.swift */; };
349+ 757326FD1E8C04CD001E618B /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757326FC1E8C04CD001E618B /* SearchViewModel.swift */; };
350+ 757326FE1E8C060F001E618B /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46753A9A1E593EAC007872C0 /* LocalizationUtil.swift */; };
351+ 757327001E8C06B7001E618B /* SearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757326FF1E8C06B7001E618B /* SearchViewModelTests.swift */; };
352+ 757327011E8C09BB001E618B /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757326FC1E8C04CD001E618B /* SearchViewModel.swift */; };
353 759464A51CEB8493001A56BF /* SettingsControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759464A41CEB8493001A56BF /* SettingsControllerPresenter.swift */; };
354 759778AE1CF83AC400C2E4B4 /* APITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759778AD1CF83AC400C2E4B4 /* APITest.swift */; };
355+ 75AF930F1E9297220051EB82 /* SearchResultsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755B3FE11E8CFEE000DA570C /* SearchResultsViewModel.swift */; };
356 863574EB78CF355A8463055D /* Pods_Remote.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3194E32378DD6BB8E273BB47 /* Pods_Remote.framework */; };
357 /* End PBXBuildFile section */
358
359@@ -132,10 +137,12 @@
360 750831BC1CFF597D00CA9299 /* slides_response.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = slides_response.json; sourceTree = "<group>"; };
361 75219D011E276111001B6BC8 /* TabSwitchingToRightAnimationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabSwitchingToRightAnimationController.swift; sourceTree = "<group>"; };
362 755A61C81E04878F00D035D4 /* ServiceViewControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceViewControllerDelegate.swift; sourceTree = "<group>"; };
363+ 755B3FE11E8CFEE000DA570C /* SearchResultsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsViewModel.swift; sourceTree = "<group>"; };
364 755C78CD1CCE9080001CF70D /* Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = "<group>"; };
365- 75667B2A1CFCAE44008FDFE0 /* PollAPITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PollAPITests.swift; sourceTree = "<group>"; };
366 75667B4B1CFEE85A008FDFE0 /* TestUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = "<group>"; };
367 756FA0FC1E28FF060011AE6F /* SlideCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlideCell.swift; sourceTree = "<group>"; };
368+ 757326FC1E8C04CD001E618B /* SearchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
369+ 757326FF1E8C06B7001E618B /* SearchViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewModelTests.swift; sourceTree = "<group>"; };
370 759464A41CEB8493001A56BF /* SettingsControllerPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsControllerPresenter.swift; sourceTree = "<group>"; };
371 759778A31CF839C200C2E4B4 /* OLP RemoteTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OLP RemoteTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
372 759778A71CF839C200C2E4B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
373@@ -341,9 +348,11 @@
374 46C133421C0A39E80093579F /* Search */ = {
375 isa = PBXGroup;
376 children = (
377+ 757326FC1E8C04CD001E618B /* SearchViewModel.swift */,
378 46C133431C0A3A0F0093579F /* SearchViewController.swift */,
379 46C133441C0A3A0F0093579F /* SearchViewController.xib */,
380 46FD37881C796C9B00DED423 /* SearchResultViewController.swift */,
381+ 755B3FE11E8CFEE000DA570C /* SearchResultsViewModel.swift */,
382 );
383 path = Search;
384 sourceTree = "<group>";
385@@ -353,11 +362,11 @@
386 children = (
387 759778A71CF839C200C2E4B4 /* Info.plist */,
388 759778AD1CF83AC400C2E4B4 /* APITest.swift */,
389- 75667B2A1CFCAE44008FDFE0 /* PollAPITests.swift */,
390 75667B4B1CFEE85A008FDFE0 /* TestUtils.swift */,
391 750831B81CFF44FC00CA9299 /* poll_response.json */,
392 750831BA1CFF594400CA9299 /* service_response.json */,
393 750831BC1CFF597D00CA9299 /* slides_response.json */,
394+ 757326FF1E8C06B7001E618B /* SearchViewModelTests.swift */,
395 );
396 path = "OLP RemoteTests";
397 sourceTree = "<group>";
398@@ -582,6 +591,7 @@
399 buildActionMask = 2147483647;
400 files = (
401 46FD378B1C79770900DED423 /* LiveAPI.swift in Sources */,
402+ 757326FD1E8C04CD001E618B /* SearchViewModel.swift in Sources */,
403 4623B6971C07BC2E007FFE2B /* SlidesViewController.swift in Sources */,
404 46A9EED81BFD89DF00E8D520 /* ServiceAPI.swift in Sources */,
405 46FD37871C7959EF00DED423 /* SearchAPI.swift in Sources */,
406@@ -591,6 +601,7 @@
407 46CC0CED1BE6972300423EB4 /* UserSettings.swift in Sources */,
408 46FD37891C796C9B00DED423 /* SearchResultViewController.swift in Sources */,
409 463A918D1C093DD500D572E6 /* AlertsViewController.swift in Sources */,
410+ 755B3FE21E8CFEE000DA570C /* SearchResultsViewModel.swift in Sources */,
411 4640F2951BE42B6E00E2AA61 /* PollModel.swift in Sources */,
412 75219D021E276111001B6BC8 /* TabSwitchingToRightAnimationController.swift in Sources */,
413 46753A9B1E593EAC007872C0 /* LocalizationUtil.swift in Sources */,
414@@ -626,19 +637,22 @@
415 75667B591CFEE949008FDFE0 /* LiveModel.swift in Sources */,
416 75667B521CFEE938008FDFE0 /* AddAPI.swift in Sources */,
417 75667B5C1CFEFF7E008FDFE0 /* DictionaryUtil.swift in Sources */,
418+ 757327001E8C06B7001E618B /* SearchViewModelTests.swift in Sources */,
419 75667B5E1CFEFF86008FDFE0 /* ErrorUtil.swift in Sources */,
420 75667B5A1CFEE949008FDFE0 /* ServiceListModel.swift in Sources */,
421 75667B571CFEE949008FDFE0 /* PollModel.swift in Sources */,
422 75667B581CFEE949008FDFE0 /* SlideModel.swift in Sources */,
423 75667B281CFCA8B1008FDFE0 /* API.swift in Sources */,
424- 75667B2B1CFCAE44008FDFE0 /* PollAPITests.swift in Sources */,
425 75667B4D1CFEE92C008FDFE0 /* ControllerAPI.swift in Sources */,
426+ 75AF930F1E9297220051EB82 /* SearchResultsViewModel.swift in Sources */,
427+ 757326FE1E8C060F001E618B /* LocalizationUtil.swift in Sources */,
428 75667B5F1CFEFFA7008FDFE0 /* DisplayAPI.swift in Sources */,
429 75667B4E1CFEE930008FDFE0 /* PollAPI.swift in Sources */,
430 75667B541CFEE93D008FDFE0 /* AlertAPI.swift in Sources */,
431 75667B531CFEE938008FDFE0 /* Service.swift in Sources */,
432 75667B291CFCA8C9008FDFE0 /* UserSettings.swift in Sources */,
433 75667B4C1CFEE85A008FDFE0 /* TestUtils.swift in Sources */,
434+ 757327011E8C09BB001E618B /* SearchViewModel.swift in Sources */,
435 75667B5D1CFEFF83008FDFE0 /* ColorUtil.swift in Sources */,
436 75667B551CFEE949008FDFE0 /* SuccessModel.swift in Sources */,
437 75667B4F1CFEE938008FDFE0 /* ServiceAPI.swift in Sources */,
438@@ -755,7 +769,7 @@
439 isa = XCBuildConfiguration;
440 baseConfigurationReference = 20B750141C8B3B45881386FA /* Pods-Remote.debug.xcconfig */;
441 buildSettings = {
442- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
443+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
444 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
445 ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
446 CODE_SIGN_IDENTITY = "iPhone Developer";
447@@ -774,7 +788,7 @@
448 isa = XCBuildConfiguration;
449 baseConfigurationReference = 6B75984BB421CA7AAB01F229 /* Pods-Remote.release.xcconfig */;
450 buildSettings = {
451- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
452+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
453 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
454 ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
455 CODE_SIGN_IDENTITY = "iPhone Developer";
456@@ -793,8 +807,9 @@
457 isa = XCBuildConfiguration;
458 baseConfigurationReference = 2AEEA392DC2D08B9EAF00B32 /* Pods-OLP RemoteTests.debug.xcconfig */;
459 buildSettings = {
460- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
461+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
462 CLANG_ANALYZER_NONNULL = YES;
463+ DEVELOPMENT_TEAM = "";
464 INFOPLIST_FILE = "OLP RemoteTests/Info.plist";
465 IPHONEOS_DEPLOYMENT_TARGET = 9.3;
466 LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
467@@ -808,8 +823,9 @@
468 isa = XCBuildConfiguration;
469 baseConfigurationReference = BF194B8FEA335C68D241C125 /* Pods-OLP RemoteTests.release.xcconfig */;
470 buildSettings = {
471- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
472+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
473 CLANG_ANALYZER_NONNULL = YES;
474+ DEVELOPMENT_TEAM = "";
475 INFOPLIST_FILE = "OLP RemoteTests/Info.plist";
476 IPHONEOS_DEPLOYMENT_TARGET = 9.3;
477 LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
478
479=== modified file 'Remote/AppDelegate.swift'
480--- Remote/AppDelegate.swift 2017-01-21 18:35:49 +0000
481+++ Remote/AppDelegate.swift 2017-04-17 16:13:16 +0000
482@@ -57,15 +57,15 @@
483 let slidesViewController = SlidesViewController(settings: settings, service: service)
484 navigationController = UINavigationController(rootViewController: slidesViewController)
485 rootViewControllers.append(navigationController)
486-
487- service.pollAPI.serviceDelegate = serviceViewController
488- service.pollAPI.slidesDelegate = slidesViewController
489+
490
491 let alertsViewController = AlertsViewController(settings: settings, service: service)
492 navigationController = UINavigationController(rootViewController: alertsViewController)
493 rootViewControllers.append(navigationController)
494-
495- let searchViewController = SearchViewController(settings: settings, service: service)
496+
497+ let searchViewModel = SearchViewModel(service: service)
498+
499+ let searchViewController = SearchViewController(viewModel:searchViewModel, settings: settings, service: service)
500 navigationController = UINavigationController(rootViewController: searchViewController)
501 rootViewControllers.append(navigationController)
502
503@@ -81,15 +81,9 @@
504
505 settings.resetDefaultsIfNeeded()
506
507- BlockLooper.executeBlockWithRate(2.0) {
508- if self.settings.validateURL() {
509- if self.service.pauseLoopPolling == false {
510- self.service.pollAPI.poll()
511- }
512- }
513- return false
514- }
515-
516+ service.serviceDelegate = serviceViewController
517+ service.slidesDelegate = slidesViewController
518+
519 return true
520 }
521
522
523=== modified file 'Remote/Classes/Controller/Search/SearchResultViewController.swift'
524--- Remote/Classes/Controller/Search/SearchResultViewController.swift 2017-02-19 05:21:00 +0000
525+++ Remote/Classes/Controller/Search/SearchResultViewController.swift 2017-04-17 16:13:16 +0000
526@@ -24,20 +24,45 @@
527
528 import UIKit
529
530-class SearchResultViewController: UITableViewController {
531-
532- fileprivate var items: [ItemModel]!
533- fileprivate let service: Service!
534-
535- init(items: [ItemModel], service: Service) {
536- self.items = items
537- self.service = service
538- super.init(style: .plain)
539+class SearchResultViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
540+
541+ fileprivate let viewModel: SearchResultsViewModel!
542+ fileprivate var tableView: UITableView?
543+
544+ init(viewModel: SearchResultsViewModel) {
545+ self.viewModel = viewModel
546+ super.init(nibName: nil, bundle: nil)
547+
548+ viewModel.errorNotification.producer.skipNil().startWithValues { (error) in
549+ self.showErrorNotification(error)
550+ }
551+
552+ viewModel.notification.producer.skip(first: 1).startWithValues { (notificationText) in
553+ self.displayNotification(notificationText)
554+ }
555+
556+ viewModel.goToService.producer.skip(first: 1).startWithValues { (value) in
557+ self.tabBarController?.selectedIndex = 0
558+ }
559+
560+ viewModel.goToLive.producer.skip(first: 1).startWithValues { (value) in
561+ self.tabBarController?.selectedIndex = 1
562+ }
563 }
564-
565+
566 required init?(coder aDecoder: NSCoder) {
567- self.service = nil
568+ self.viewModel = nil
569 super.init(coder: aDecoder)
570+ fatalError("Not implemented")
571+ }
572+
573+
574+ override func loadView() {
575+ super.loadView()
576+ self.tableView = UITableView(frame:self.view.bounds, style:.plain)
577+ self.view.addSubview(self.tableView!)
578+ self.tableView?.delegate = self
579+ self.tableView?.dataSource = self
580 }
581
582 override func viewDidLoad() {
583@@ -53,73 +78,53 @@
584 }
585
586 fileprivate func setupTableView() {
587- self.tableView.estimatedRowHeight = 44
588- self.tableView.rowHeight = UITableViewAutomaticDimension
589+ self.tableView?.estimatedRowHeight = 44
590+ self.tableView?.rowHeight = UITableViewAutomaticDimension
591 }
592
593 // MARK: Table view data source
594-
595- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
596- return self.items.count
597+
598+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
599+ return self.viewModel.items.count
600 }
601
602- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
603+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
604 var cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
605 if cell == nil {
606 cell = UITableViewCell(style: .default, reuseIdentifier: "Cell")
607 cell!.textLabel?.numberOfLines = 0
608 }
609- let item = self.items[indexPath.row]
610+ let item = self.viewModel.items[indexPath.row]
611 cell!.textLabel?.text = item.title
612 return cell!
613 }
614
615 // MARK: Table view delegate
616-
617- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
618- func showErrorNotification(_ error: NSError) {
619- if let message = verifyError(error) {
620- self.displayNotification(message, isError: true)
621- }
622- }
623- func itemAddedNotification() {
624- let message = Localizable.Search.itemAdded
625- self.displayNotification(message)
626- }
627- let item = self.items[indexPath.row]
628+
629+
630+ func showErrorNotification(_ error: NSError) {
631+ if let message = verifyError(error) {
632+ self.displayNotification(message, isError: true)
633+ }
634+ }
635+
636+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
637+ let item = self.viewModel.items[indexPath.row]
638 let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
639 let goLiveStr = Localizable.Search.goLive
640 let goLiveAction = UIAlertAction(title: goLiveStr, style: .default) { _ in
641 tableView.deselectRow(at: indexPath, animated: true)
642- self.service.liveAPI.goItemToLive(item, plugin: Plugin(rawValue: item.plugin!)!) { error in
643- if let error = error {
644- showErrorNotification(error as NSError)
645- return
646- }
647- self.tabBarController?.selectedIndex = 1
648- }
649+ self.viewModel.goLive(item: item, plugin: Plugin(rawValue: item.plugin!)!)
650 }
651 let addStr = Localizable.Search.addToService
652 let addAction = UIAlertAction(title: addStr, style: .default) { _ in
653 tableView.deselectRow(at: indexPath, animated: true)
654- self.service.addAPI.addItem(item, plugin: Plugin(rawValue: item.plugin!)!) { error in
655- if let error = error {
656- showErrorNotification(error as NSError)
657- } else {
658- itemAddedNotification()
659- }
660- }
661+ self.viewModel.addItem(item: item, plugin: Plugin(rawValue: item.plugin!)!, goToService: false)
662 }
663 let addAndGoStr = Localizable.Search.addGoToService
664 let addAndGoAction = UIAlertAction(title: addAndGoStr, style: .default) { _ in
665 tableView.deselectRow(at: indexPath, animated: true)
666- self.service.addAPI.addItem(item, plugin: Plugin(rawValue: item.plugin!)!) { error in
667- if let error = error {
668- showErrorNotification(error as NSError)
669- return
670- }
671- self.tabBarController?.selectedIndex = 0
672- }
673+ self.viewModel.addItem(item: item, plugin: Plugin(rawValue: item.plugin!)!, goToService: true)
674 }
675 let cancelStr = Localizable.cancel
676 let cancelAction = UIAlertAction(title: cancelStr, style: .cancel) { _ in
677
678=== added file 'Remote/Classes/Controller/Search/SearchResultsViewModel.swift'
679--- Remote/Classes/Controller/Search/SearchResultsViewModel.swift 1970-01-01 00:00:00 +0000
680+++ Remote/Classes/Controller/Search/SearchResultsViewModel.swift 2017-04-17 16:13:16 +0000
681@@ -0,0 +1,58 @@
682+//
683+// SearchResultsViewModel.swift
684+// Remote
685+//
686+// Created by andrei shender on 3/30/17.
687+// Copyright © 2017 OpenLP. All rights reserved.
688+//
689+
690+import UIKit
691+
692+import ReactiveCocoa
693+import Result
694+import ReactiveSwift
695+
696+class SearchResultsViewModel: NSObject {
697+ var items: [ItemModel]
698+
699+ var notification = MutableProperty<String>("")
700+ var errorNotification = MutableProperty<NSError?>(nil)
701+ var goToService = MutableProperty<Bool>(false)
702+ var goToLive = MutableProperty<Bool>(false)
703+
704+ let service: ServiceProtocol
705+
706+ init(searchReulsts:[ItemModel], service: ServiceProtocol) {
707+ items = searchReulsts
708+ self.service = service
709+ super.init()
710+ }
711+
712+ func addItem(item: ItemModel, plugin: Plugin, goToService: Bool) {
713+ self.service.addAPI.addSignalProducer(item: item, plugin: plugin).startWithResult { (result) in
714+ switch result {
715+ case .success(_):
716+ if goToService {
717+ self.goToService.value = true
718+ }
719+ else {
720+ self.notification.value = Localizable.Search.itemAdded
721+ }
722+ case let .failure(error):
723+ self.errorNotification.value = error.error as NSError
724+ }
725+ }
726+ }
727+
728+ func goLive(item: ItemModel, plugin: Plugin) {
729+ self.service.liveAPI.goLiveSignalProducer(item: item, plugin: plugin).startWithResult { (result) in
730+ switch result {
731+ case .success(_):
732+ self.goToLive.value = true
733+ case let .failure(error):
734+ self.errorNotification.value = error.error as NSError
735+ }
736+ }
737+
738+ }
739+}
740
741=== modified file 'Remote/Classes/Controller/Search/SearchViewController.swift'
742--- Remote/Classes/Controller/Search/SearchViewController.swift 2017-02-19 05:21:00 +0000
743+++ Remote/Classes/Controller/Search/SearchViewController.swift 2017-04-17 16:13:16 +0000
744@@ -24,33 +24,59 @@
745
746 import UIKit
747 import CWStatusBarNotification
748+import ReactiveSwift
749+import ReactiveCocoa
750+import Result
751
752 // MARK: - SearchViewController
753
754 class SearchViewController: UIViewController {
755-
756+
757+ let viewModel: SearchViewModel!
758+
759 @IBOutlet weak var lbTitle: UILabel!
760 @IBOutlet weak var pickerView: UIPickerView!
761
762 fileprivate var notifications: CWStatusBarNotification!
763 fileprivate var textField: UITextField!
764- fileprivate var plugins: [Plugin]!
765 fileprivate let service: Service!
766 fileprivate var settingsPresenter: SettingsControllerPresenter!
767
768 // MARK: Init
769
770- init(settings: UserSettings, service: Service) {
771+ init(viewModel: SearchViewModel, settings: UserSettings, service: Service) {
772+ self.viewModel = viewModel
773 self.service = service
774 self.settingsPresenter = SettingsControllerPresenter(settings: settings, pollAPI: service.pollAPI)
775 super.init(nibName: "SearchViewController", bundle: nil)
776 self.setupNavigationBar()
777 self.setupTabBar()
778- self.setupOptions()
779 self.setupNotifications()
780+
781+ self.viewModel.searchTerm <~ self.textField.reactive.continuousTextValues
782+
783+ self.viewModel.searchResult.producer.startWithResult({ (result) in
784+ if let error = result.error {
785+ let message = Localizable.Error.tryAgain
786+ self.notifications.notificationLabelBackgroundColor = kRedColor
787+ self.notifications.display(withMessage: message, forDuration: 3)
788+ print(error)
789+ return
790+ } else if result.value?.count == 0 {
791+ let message = Localizable.Search.noResults
792+ self.notifications.notificationLabelBackgroundColor = kBlueColor
793+ self.notifications.display(withMessage: message, forDuration: 3)
794+ return
795+ }
796+ let resultsViewModel = self.viewModel.getSearchResultsViewModel()
797+ let resultsViewController = SearchResultViewController(viewModel: resultsViewModel)
798+ self.navigationController?.pushViewController(resultsViewController, animated: true)
799+ })
800+
801 }
802-
803+
804 required init?(coder aDecoder: NSCoder) {
805+ self.viewModel = nil
806 self.service = nil
807 self.settingsPresenter = nil
808 super.init(coder: aDecoder)
809@@ -81,6 +107,10 @@
810
811 let btSearch = UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(SearchViewController.search))
812 self.navigationItem.rightBarButtonItem = btSearch
813+
814+ self.viewModel.searchButtonEnabled.producer.startWithValues { (isEnabled) in
815+ self.navigationItem.rightBarButtonItem?.isEnabled = isEnabled
816+ }
817 }
818
819 fileprivate func setupPickerView() {
820@@ -89,17 +119,7 @@
821 self.pickerView.layer.borderWidth = 0.7
822 }
823
824- fileprivate func setupOptions() {
825- self.plugins = [
826- .Songs,
827- .Bibles,
828- .Presentations,
829- .Images,
830- .Media,
831- .Custom
832- ]
833- }
834-
835+
836 fileprivate func setupGestures() {
837 var gesture: UIGestureRecognizer
838 gesture = UITapGestureRecognizer(target: self, action: #selector(SearchViewController.dismissKeyboard))
839@@ -125,26 +145,9 @@
840
841 @objc fileprivate func search() {
842 self.dismissKeyboard()
843- let plugin = self.plugins[self.pickerView.selectedRow(inComponent: 0)]
844- let text = self.textField.text!
845- service.searchAPI.search(plugin: plugin, text: text) { items, error in
846- if let error = error {
847- let message = Localizable.Error.tryAgain
848- self.notifications.notificationLabelBackgroundColor = kRedColor
849- self.notifications.display(withMessage: message, forDuration: 3)
850- print(error)
851- return
852- } else if items?.count == 0 {
853- let message = Localizable.Search.noResults
854- self.notifications.notificationLabelBackgroundColor = kBlueColor
855- self.notifications.display(withMessage: message, forDuration: 3)
856- return
857- }
858- let resultsViewController = SearchResultViewController(items: items!, service: self.service)
859- self.navigationController?.pushViewController(resultsViewController, animated: true)
860- }
861+ self.viewModel.search()
862 }
863-
864+
865 @objc fileprivate func dismissKeyboard() {
866 self.textField.resignFirstResponder()
867 }
868@@ -160,7 +163,7 @@
869 }
870
871 func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
872- return self.plugins.count
873+ return self.viewModel.plugins.count
874 }
875
876 }
877@@ -170,10 +173,13 @@
878 extension SearchViewController: UIPickerViewDelegate {
879
880 func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
881- let option = self.plugins[row]
882+ let option = self.viewModel.plugins[row]
883 return option.name()
884 }
885-
886+
887+ func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
888+ self.viewModel.plugin = self.viewModel.plugins[row]
889+ }
890 }
891
892 // MARK: - UIGestureRecognizerDelegate
893@@ -192,7 +198,7 @@
894 extension SearchViewController: UITextFieldDelegate {
895
896 func textFieldShouldReturn(_ textField: UITextField) -> Bool {
897- self.search()
898+ self.viewModel.search()
899 return true
900 }
901
902
903=== added file 'Remote/Classes/Controller/Search/SearchViewModel.swift'
904--- Remote/Classes/Controller/Search/SearchViewModel.swift 1970-01-01 00:00:00 +0000
905+++ Remote/Classes/Controller/Search/SearchViewModel.swift 2017-04-17 16:13:16 +0000
906@@ -0,0 +1,62 @@
907+/******************************************************************************
908+ * OpenLP iOS Remote *
909+ * --------------------------------------------------------------------------- *
910+ * Copyright (c) 2008-2016 OpenLP Developers *
911+ * --------------------------------------------------------------------------- *
912+ * Permission is hereby granted, free of charge, to any person obtaining a *
913+ * copy of this software and associated documentation files (the "Software"), *
914+ * to deal in the Software without restriction, including without limitation *
915+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, *
916+ * and/or sell copies of the Software, and to permit persons to whom the *
917+ * Software is furnished to do so, subject to the following conditions: *
918+ * *
919+ * The above copyright notice and this permission notice shall be included in *
920+ * all copies or substantial portions of the Software. *
921+ * *
922+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
923+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
924+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
925+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
926+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *
927+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *
928+ * DEALINGS IN THE SOFTWARE. *
929+ ******************************************************************************/
930+import UIKit
931+import Result
932+import ReactiveSwift
933+
934+class SearchViewModel: NSObject {
935+ let service: ServiceProtocol
936+
937+ let searchTerm = MutableProperty(String?(""))
938+ var searchResult = MutableProperty([ItemModel]())
939+
940+ let searchButtonEnabled = MutableProperty(true)
941+ var plugins: [Plugin]
942+ var plugin: Plugin = .Songs
943+
944+ func search() -> Void {
945+ self.service.searchAPI.searchSignalProducer(plugin: self.plugin, text: self.searchTerm.value).startWithResult { result in
946+ self.searchResult.value = result.value!
947+ }
948+ }
949+
950+ init(service: ServiceProtocol) {
951+ self.service = service
952+ plugins = [
953+ .Songs,
954+ .Bibles,
955+ .Presentations,
956+ .Images,
957+ .Media,
958+ .Custom
959+ ]
960+
961+ super.init()
962+
963+ }
964+
965+ func getSearchResultsViewModel() -> SearchResultsViewModel {
966+ return SearchResultsViewModel(searchReulsts: self.searchResult.value, service: service)
967+ }
968+}
969
970=== modified file 'Remote/Classes/Network/AddAPI.swift'
971--- Remote/Classes/Network/AddAPI.swift 2017-01-21 18:35:49 +0000
972+++ Remote/Classes/Network/AddAPI.swift 2017-04-17 16:13:16 +0000
973@@ -24,29 +24,41 @@
974
975 import UIKit
976 import Alamofire
977+import ReactiveCocoa
978+import Result
979+import ReactiveSwift
980+
981
982 class AddAPI: API {
983-
984- func addItem(_ item: ItemModel, plugin: Plugin, completion: @escaping (Error?) -> Void) {
985- let url = self.base + "/" + plugin.rawValue + "/add"
986- var params: [String: Any] = [
987- "request": [
988- "id": (item.id == nil ? String(item.idInt!) : item.id!)
989+
990+ func addSignalProducer(item: ItemModel, plugin: Plugin) -> SignalProducer<Bool, AnyError> {
991+ return SignalProducer({ (observer, disposable) in
992+ let url = self.base + "/" + plugin.rawValue + "/add"
993+ var params: [String: Any] = [
994+ "request": [
995+ "id": (item.id == nil ? String(item.idInt!) : item.id!)
996+ ]
997 ]
998- ]
999- if let jsonString = jsonString(fromDictionary: params) {
1000- params = ["data": jsonString as Any]
1001- let request = Alamofire.request(url, parameters: params).validate(statusCode: 200...200)
1002- if settings.needsAuth {
1003- request.authenticate(user: settings.userID, password: settings.password, persistence: .none)
1004- }
1005-
1006- let responseHandler: (Alamofire.DefaultDataResponse) -> Swift.Void = { response in
1007- completion(response.error)
1008- }
1009-
1010- request.response(completionHandler: responseHandler)
1011- }
1012+ if let jsonString = jsonString(fromDictionary: params) {
1013+ params = ["data": jsonString as Any]
1014+ let request = Alamofire.request(url, parameters: params).validate(statusCode: 200...200)
1015+ if self.settings.needsAuth {
1016+ request.authenticate(user: self.settings.userID, password: self.settings.password, persistence: .none)
1017+ }
1018+
1019+ let responseHandler: (Alamofire.DefaultDataResponse) -> Swift.Void = { response in
1020+ if let error = response.error {
1021+ observer.send(error: AnyError(error))
1022+ }
1023+ else {
1024+ observer.send(value:true)
1025+ }
1026+ }
1027+
1028+ request.response(completionHandler: responseHandler)
1029+ }
1030+
1031+ })
1032 }
1033-
1034+
1035 }
1036
1037=== modified file 'Remote/Classes/Network/LiveAPI.swift'
1038--- Remote/Classes/Network/LiveAPI.swift 2017-01-21 18:35:49 +0000
1039+++ Remote/Classes/Network/LiveAPI.swift 2017-04-17 16:13:16 +0000
1040@@ -24,29 +24,38 @@
1041
1042 import UIKit
1043 import Alamofire
1044+import ReactiveSwift
1045+import Result
1046
1047 class LiveAPI: API {
1048-
1049- func goItemToLive(_ item: ItemModel, plugin: Plugin, completion: @escaping (Error?) -> Void) {
1050- let url = self.base + "/" + plugin.rawValue + "/live"
1051- var params: [String: Any] = [
1052- "request": [
1053- "id": (item.id == nil ? String(item.idInt!) : item.id!)
1054+
1055+ func goLiveSignalProducer(item: ItemModel, plugin: Plugin) -> SignalProducer<Bool, AnyError> {
1056+ return SignalProducer({ (observer, disposable) in
1057+ let url = self.base + "/" + plugin.rawValue + "/live"
1058+ var params: [String: Any] = [
1059+ "request": [
1060+ "id": (item.id == nil ? String(item.idInt!) : item.id!)
1061+ ]
1062 ]
1063- ]
1064- if let jsonString = jsonString(fromDictionary: params) {
1065- params = ["data": jsonString as Any]
1066- let request = Alamofire.request(url, parameters: params).validate(statusCode: 200...200)
1067- if settings.needsAuth {
1068- request.authenticate(user: settings.userID, password: settings.password, persistence: .none)
1069- }
1070-
1071- let responseHandler: (Alamofire.DefaultDataResponse) -> Swift.Void = { response in
1072- completion(response.error)
1073- }
1074-
1075- request.response(completionHandler: responseHandler)
1076- }
1077+ if let jsonString = jsonString(fromDictionary: params) {
1078+ params = ["data": jsonString as Any]
1079+ let request = Alamofire.request(url, parameters: params).validate(statusCode: 200...200)
1080+ if self.settings.needsAuth {
1081+ request.authenticate(user: self.settings.userID, password: self.settings.password, persistence: .none)
1082+ }
1083+
1084+ let responseHandler: (Alamofire.DefaultDataResponse) -> Swift.Void = { response in
1085+ if let error = response.error {
1086+ observer.send(error: AnyError(error))
1087+ }
1088+ else {
1089+ observer.send(value: true)
1090+ }
1091+ }
1092+
1093+ request.response(completionHandler: responseHandler)
1094+ }
1095+
1096+ })
1097 }
1098-
1099 }
1100
1101=== modified file 'Remote/Classes/Network/PollAPI.swift'
1102--- Remote/Classes/Network/PollAPI.swift 2017-01-21 18:35:49 +0000
1103+++ Remote/Classes/Network/PollAPI.swift 2017-04-17 16:13:16 +0000
1104@@ -25,108 +25,29 @@
1105 import Foundation
1106 import Alamofire
1107 import AlamofireObjectMapper
1108-
1109-protocol PollAPIServiceDelegate {
1110- func updateServiceList(_ items: [ItemModel])
1111- func updateSelectedItem(itemId id: String)
1112- func updateProjectionState(_ state: ProjectionState)
1113-}
1114-
1115-protocol PollAPISlidesDelegate {
1116- func updateSlides(_ slides: [SlideModel])
1117- func updateSelectedSlide(_ row: Int)
1118- func updateProjectionState(_ state: ProjectionState)
1119-}
1120+import ReactiveSwift
1121+import Result
1122+
1123
1124 class PollAPI: API {
1125-
1126- fileprivate var poll: PollModel = PollModel()
1127- var serviceDelegate: PollAPIServiceDelegate?
1128- var slidesDelegate: PollAPISlidesDelegate?
1129- fileprivate let controllerAPI: ControllerAPI
1130- fileprivate let serviceAPI: ServiceAPI
1131-
1132- init(settings: UserSettings, controllerAPI: ControllerAPI, serviceAPI: ServiceAPI) {
1133- self.controllerAPI = controllerAPI
1134- self.serviceAPI = serviceAPI
1135- super.init(settings: settings)
1136- }
1137-
1138- func poll(_ completion: ((PollModel?, Error?) -> Void)? = nil) {
1139- let url = self.base + "/poll"
1140- let request = Alamofire.request(url).validate(statusCode: 200...200)
1141- if settings.needsAuth {
1142- request.authenticate(user: settings.userID, password: settings.password, persistence: .none)
1143- }
1144- request.responseObject { (response: DataResponse<PollModel>) in
1145- let error = response.result.error
1146- let poll = response.result.value
1147-
1148- if let completion = completion {
1149- completion(poll, error)
1150- }
1151-
1152- self.handlePollResponse(poll)
1153- }
1154- }
1155-
1156-
1157- fileprivate func updateService(_ completion: ((Void) -> Void)? = nil) {
1158- self.serviceAPI.list { items, error in
1159- if let items = items {
1160- self.serviceDelegate?.updateServiceList(items)
1161- }
1162-
1163- if let completion = completion {
1164- completion()
1165- }
1166- }
1167- }
1168-
1169-
1170- fileprivate func updateSlides(_ completion: ((Void) -> Void)? = nil) {
1171- self.controllerAPI.liveText { slides, error in
1172- if let slides = slides {
1173- self.slidesDelegate?.updateSlides(slides)
1174- }
1175-
1176- if let completion = completion {
1177- completion()
1178- }
1179- }
1180- }
1181-
1182-
1183- fileprivate func updateProjectionState(_ projectionState: ProjectionState) {
1184- self.serviceDelegate?.updateProjectionState(projectionState)
1185- self.slidesDelegate?.updateProjectionState(projectionState)
1186- }
1187-
1188-
1189- fileprivate func handlePollResponse(_ poll: PollModel?) {
1190- if let poll = poll {
1191- if self.poll.service != poll.service {
1192- self.updateService({
1193- self.serviceDelegate?.updateSelectedItem(itemId: poll.item)
1194- })
1195- }
1196- else {
1197- self.serviceDelegate?.updateSelectedItem(itemId: poll.item)
1198- }
1199-
1200- if self.poll.item != poll.item {
1201- self.updateSlides({
1202- self.slidesDelegate?.updateSelectedSlide(poll.slide)
1203- })
1204- }
1205- else {
1206- self.slidesDelegate?.updateSelectedSlide(poll.slide)
1207- }
1208-
1209- updateProjectionState(poll.projectionState)
1210-
1211- self.poll = poll
1212- }
1213- }
1214-
1215+
1216+ func pollSignalProducer() -> SignalProducer<PollModel?, AnyError> {
1217+ return SignalProducer({ (observer, disposable) in
1218+ let url = self.base + "/poll"
1219+ let request = Alamofire.request(url).validate(statusCode: 200...200)
1220+ if self.settings.needsAuth {
1221+ request.authenticate(user: self.settings.userID, password: self.settings.password, persistence: .none)
1222+ }
1223+ request.responseObject { (response: DataResponse<PollModel>) in
1224+ switch response.result {
1225+ case .success(let value):
1226+ observer.send(value: value)
1227+ break
1228+ case .failure(let error):
1229+ observer.send(error: AnyError(error))
1230+ break
1231+ }
1232+ }
1233+ })
1234+ }
1235 }
1236
1237=== modified file 'Remote/Classes/Network/SearchAPI.swift'
1238--- Remote/Classes/Network/SearchAPI.swift 2017-01-21 18:35:49 +0000
1239+++ Remote/Classes/Network/SearchAPI.swift 2017-04-17 16:13:16 +0000
1240@@ -24,27 +24,31 @@
1241
1242 import UIKit
1243 import Alamofire
1244+import ReactiveCocoa
1245+import ReactiveSwift
1246+import Result
1247
1248 class SearchAPI: API {
1249-
1250- func search(plugin: Plugin, text: String, completion: @escaping ([ItemModel]?, Error?) -> Void) {
1251- let url = self.base + "/" + plugin.rawValue + "/search"
1252- var params: [String: Any] = [
1253- "request": [
1254- "text": text
1255+
1256+ func searchSignalProducer(plugin: Plugin, text: String?) -> SignalProducer<[ItemModel], AnyError> {
1257+ return SignalProducer({ (observer, disposable) in
1258+ let url = self.base + "/" + plugin.rawValue + "/search"
1259+ var params: [String: Any] = [
1260+ "request": [
1261+ "text": text
1262+ ]
1263 ]
1264- ]
1265- if let jsonString = jsonString(fromDictionary: params) {
1266- params = ["data": jsonString as Any]
1267- let request = Alamofire.request(url, parameters: params).validate(statusCode: 200...200)
1268- if settings.needsAuth {
1269- request.authenticate(user: settings.userID, password: settings.password, persistence: .none)
1270- }
1271- request.responseJSON { (response: DataResponse<Any>) in
1272- var result = [ItemModel]()
1273- if let value = response.result.value as? NSDictionary,
1274- let results = value["results"] as? NSDictionary,
1275- let items = results["items"] as? NSArray {
1276+ if let jsonString = jsonString(fromDictionary: params) {
1277+ params = ["data": jsonString as Any]
1278+ let request = Alamofire.request(url, parameters: params).validate(statusCode: 200...200)
1279+ if self.settings.needsAuth {
1280+ request.authenticate(user: self.settings.userID, password: self.settings.password, persistence: .none)
1281+ }
1282+ request.responseJSON { (response: DataResponse<Any>) in
1283+ var result = [ItemModel]()
1284+ if let value = response.result.value as? NSDictionary,
1285+ let results = value["results"] as? NSDictionary,
1286+ let items = results["items"] as? NSArray {
1287 for item in items {
1288 let item = item as! NSArray
1289 let i = ItemModel()
1290@@ -58,11 +62,17 @@
1291 i.plugin = plugin.rawValue
1292 result.append(i)
1293 }
1294+ }
1295+
1296+ if let error = response.result.error {
1297+ observer.send(error: AnyError(error))
1298+ }
1299+ else {
1300+ observer.send(value: result)
1301+ }
1302 }
1303- let error = response.result.error
1304- completion(result, error)
1305 }
1306- }
1307+ })
1308+
1309 }
1310-
1311 }
1312
1313=== modified file 'Remote/Classes/Network/Service.swift'
1314--- Remote/Classes/Network/Service.swift 2017-01-16 17:16:09 +0000
1315+++ Remote/Classes/Network/Service.swift 2017-04-17 16:13:16 +0000
1316@@ -23,8 +23,44 @@
1317 ******************************************************************************/
1318
1319 import Foundation
1320-
1321-class Service {
1322+import ReactiveSwift
1323+import Result
1324+import ReactiveCocoa
1325+import BlockLooper
1326+
1327+
1328+protocol PollAPIServiceDelegate {
1329+ func updateServiceList(_ items: [ItemModel])
1330+ func updateSelectedItem(itemId id: String)
1331+ func updateProjectionState(_ state: ProjectionState)
1332+}
1333+
1334+protocol PollAPISlidesDelegate {
1335+ func updateSlides(_ slides: [SlideModel])
1336+ func updateSelectedSlide(_ row: Int)
1337+ func updateProjectionState(_ state: ProjectionState)
1338+}
1339+
1340+
1341+protocol ServiceProtocol {
1342+ var serviceAPI: ServiceAPI { get }
1343+ var pollAPI: PollAPI { get }
1344+ var displayAPI: DisplayAPI { get }
1345+ var controllerAPI: ControllerAPI { get }
1346+ var alertAPI: AlertAPI { get }
1347+ var searchAPI: SearchAPI { get }
1348+ var liveAPI: LiveAPI { get }
1349+ var addAPI: AddAPI { get }
1350+ var pauseLoopPolling: Bool { get set}
1351+ var settings: UserSettings { get }
1352+
1353+ var serviceDelegate: PollAPIServiceDelegate? { get }
1354+ var slidesDelegate: PollAPISlidesDelegate? { get }
1355+
1356+}
1357+
1358+
1359+class Service : ServiceProtocol {
1360 let serviceAPI: ServiceAPI
1361 let pollAPI: PollAPI
1362 let displayAPI: DisplayAPI
1363@@ -34,15 +70,115 @@
1364 let liveAPI: LiveAPI
1365 let addAPI: AddAPI
1366 var pauseLoopPolling = false
1367+ let settings: UserSettings
1368+
1369+ var serviceDelegate: PollAPIServiceDelegate?
1370+ var slidesDelegate: PollAPISlidesDelegate?
1371+ fileprivate var poll: PollModel = PollModel()
1372+
1373+ let pollSignalObserver: Observer<Result<PollModel?, AnyError>, NoError>
1374+ var pollSignal: Signal<Result<PollModel?, AnyError>, NoError>
1375
1376 init(settings: UserSettings) {
1377+ self.settings = settings
1378 serviceAPI = ServiceAPI(settings: settings)
1379 controllerAPI = ControllerAPI(settings :settings)
1380- pollAPI = PollAPI(settings: settings, controllerAPI:controllerAPI, serviceAPI: serviceAPI)
1381+ pollAPI = PollAPI(settings: settings)
1382 displayAPI = DisplayAPI(settings: settings)
1383 alertAPI = AlertAPI(settings :settings)
1384 searchAPI = SearchAPI(settings :settings)
1385 liveAPI = LiveAPI(settings: settings)
1386 addAPI = AddAPI(settings: settings)
1387- }
1388+
1389+ let (pollSignal, pollObserver) = Signal<Result<PollModel?, AnyError>, NoError>.pipe()
1390+ self.pollSignal = pollSignal
1391+ self.pollSignalObserver = pollObserver
1392+
1393+ BlockLooper.executeBlockWithRate(2.0) {
1394+ if self.settings.validateURL() == false {
1395+ return false
1396+ }
1397+ if self.pauseLoopPolling == true {
1398+ return false
1399+ }
1400+ self.pollAPI.pollSignalProducer().startWithResult({ (result) in
1401+ self.pollSignalObserver.send(value: result)
1402+ })
1403+ return false
1404+ }
1405+
1406+
1407+ pollSignal
1408+ .filter({ (result) -> Bool in
1409+ switch result {
1410+ case .success:
1411+ return true
1412+ default:
1413+ return false
1414+ }
1415+ })
1416+ .observe { (event) in
1417+ self.handlePollResponse((event.value?.value)!)
1418+ }
1419+ }
1420+
1421+
1422+ fileprivate func updateService(_ completion: ((Void) -> Void)? = nil) {
1423+ self.serviceAPI.list { items, error in
1424+ if let items = items {
1425+ self.serviceDelegate?.updateServiceList(items)
1426+ }
1427+
1428+ if let completion = completion {
1429+ completion()
1430+ }
1431+ }
1432+ }
1433+
1434+
1435+ fileprivate func updateSlides(_ completion: ((Void) -> Void)? = nil) {
1436+ self.controllerAPI.liveText { slides, error in
1437+ if let slides = slides {
1438+ self.slidesDelegate?.updateSlides(slides)
1439+ }
1440+
1441+ if let completion = completion {
1442+ completion()
1443+ }
1444+ }
1445+ }
1446+
1447+
1448+ fileprivate func updateProjectionState(_ projectionState: ProjectionState) {
1449+ self.serviceDelegate?.updateProjectionState(projectionState)
1450+ self.slidesDelegate?.updateProjectionState(projectionState)
1451+ }
1452+
1453+
1454+ fileprivate func handlePollResponse(_ poll: PollModel?) {
1455+ if let poll = poll {
1456+ if self.poll.service != poll.service {
1457+ self.updateService({
1458+ self.serviceDelegate?.updateSelectedItem(itemId: poll.item)
1459+ })
1460+ }
1461+ else {
1462+ self.serviceDelegate?.updateSelectedItem(itemId: poll.item)
1463+ }
1464+
1465+ if self.poll.item != poll.item {
1466+ self.updateSlides({
1467+ self.slidesDelegate?.updateSelectedSlide(poll.slide)
1468+ })
1469+ }
1470+ else {
1471+ self.slidesDelegate?.updateSelectedSlide(poll.slide)
1472+ }
1473+
1474+ updateProjectionState(poll.projectionState)
1475+
1476+ self.poll = poll
1477+ }
1478+ }
1479+
1480 }

Subscribers

People subscribed via source and target branches

to all changes: