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

Proposed by andrei
Status: Superseded
Proposed branch: lp:~dude-from-by/openlp/my-ios-remote
Merge into: lp:~danielborges93/openlp/ios-remote
Diff against target: 1475 lines (+660/-401)
15 files modified
OLP RemoteTests/PollAPITests.swift (+0/-128)
OLP RemoteTests/SearchViewModelTests.swift (+149/-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 (+37/-42)
Remote/Classes/Controller/Search/SearchResultsViewModel.swift (+58/-0)
Remote/Classes/Controller/Search/SearchViewController.swift (+45/-39)
Remote/Classes/Controller/Search/SearchViewModel.swift (+65/-0)
Remote/Classes/Network/AddAPI.swift (+33/-21)
Remote/Classes/Network/LiveAPI.swift (+34/-20)
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 Pending
Review via email: mp+321419@code.launchpad.net

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

This proposal has been superseded by a proposal from 2017-04-05.

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. As next step I will add tests to SearchViewModel.

To post a comment you must log in.
63. By andrei

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

64. By andrei

Moved SettingsViewController.swift to MVC, added tests

65. By andrei

Added countly analytics pod and added events to track users from from start to getting response from server

Unmerged revisions

65. By andrei

Added countly analytics pod and added events to track users from from start to getting response from server

64. By andrei

Moved SettingsViewController.swift to MVC, added tests

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

Subscribers

People subscribed via source and target branches

to all changes: