Merge lp:~dude-from-by/openlp/my-ios-remote into lp:~danielborges93/openlp/ios-remote
- my-ios-remote
- Merge into ios-remote
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 |
Related bugs: |
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.
Commit message
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 SearchResultsVi
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.
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.
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.
Preview Diff
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 | } |
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.