lib/ios: ListLayout use a UIStackView inside a UIScrollView
[nit.git] / lib / ios / ui / ui.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Implementation of `app::ui` for iOS
16 module ui
17
18 import app::ui
19
20 import ios
21 import uikit
22
23 in "ObjC" `{
24 // Objective-C object receiving callbacks from UI events
25 @interface NitCallbackReference: NSObject
26
27 // Nit object target of the callbacks from UI events
28 @property (nonatomic) Button nit_button;
29
30 // Actual callback method
31 -(void) nitOnEvent: (UIButton*) sender;
32 @end
33
34 @implementation NitCallbackReference
35
36 -(void) nitOnEvent: (UIButton*) sender {
37 Button_on_click(self.nit_button);
38 }
39 @end
40
41 // Proxy for both delegates of UITableView relaying all callbacks to `nit_list_layout`
42 @interface UITableViewAndDataSource: NSObject <UITableViewDelegate, UITableViewDataSource>
43
44 // Nit object receiving the callbacks
45 @property TableView nit_list_layout;
46
47 // List of native views added to this list view from the Nit side
48 @property NSMutableArray *views;
49 @end
50
51 @implementation UITableViewAndDataSource
52
53 - (id)init
54 {
55 self = [super init];
56 self.views = [[NSMutableArray alloc] initWithCapacity:8];
57 return self;
58 }
59
60 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
61 return TableView_number_of_sections_in_table_view(self.nit_list_layout, tableView);
62 }
63
64 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
65 return TableView_number_of_rows_in_section(self.nit_list_layout, tableView, section);
66 }
67
68 - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
69 return TableView_title_for_header_in_section(self.nit_list_layout, tableView, section);
70 }
71
72 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
73 return TableView_cell_for_row_at_index_path(self.nit_list_layout, tableView, indexPath);
74 }
75 @end
76 `}
77
78 redef class App
79 redef fun did_finish_launching_with_options
80 do
81 super
82 window.native.make_key_and_visible
83 return true
84 end
85
86 redef fun window=(window)
87 do
88 app_delegate.window = window.native
89 super
90 end
91 end
92
93 redef class AppDelegate
94
95 # The main application window, must be set by `App::on_create`
96 fun window: UIWindow in "ObjC" `{ return [self window]; `}
97
98 # The main application window, must be set by `App::on_create`
99 fun window=(window: UIWindow) in "ObjC" `{ self.window = window; `}
100 end
101
102 redef class Control
103
104 # Native implementation of this control
105 fun native: NATIVE is abstract
106
107 # Type of the `native` implementation of this control
108 type NATIVE: NSObject
109 end
110
111 redef class View
112 redef type NATIVE: UIView
113
114 redef var enabled = null is lazy
115 end
116
117 redef class CompositeControl
118
119 redef fun remove(view)
120 do
121 super
122
123 if view isa View then
124 view.native.remove_from_superview
125 end
126 end
127 end
128
129 redef class Window
130
131 redef type NATIVE: UIWindow
132 redef var native = new UIWindow
133
134 init do native.background_color = new UIColor.white_color
135
136 redef fun add(view)
137 do
138 super
139
140 var native_view = view.native
141 assert native_view isa UIView
142 native.add_subview native_view
143
144 fill_whole_window_with(native_view, native)
145 end
146
147 private fun fill_whole_window_with(native: UIView, window: UIWindow)
148 in "ObjC" `{
149 // Hard coded borders including the top bar
150 // FIXME this may cause problems with retina devices
151 [window addConstraints:[NSLayoutConstraint
152 constraintsWithVisualFormat: @"V:|-24-[view]-8-|"
153 options: 0 metrics: nil views: @{@"view": native}]];
154 [window addConstraints:[NSLayoutConstraint
155 constraintsWithVisualFormat: @"H:|-8-[view]-8-|"
156 options: 0 metrics: nil views: @{@"view": native}]];
157
158 // Set the required root view controller
159 window.rootViewController = [[UIViewController alloc]initWithNibName:nil bundle:nil];
160 window.rootViewController.view = native;
161 `}
162 end
163
164 redef class Layout
165
166 redef type NATIVE: UIStackView
167 redef var native = new UIStackView
168
169 init
170 do
171 native.alignment = new UIStackViewAlignment.fill
172 native.distribution = new UIStackViewDistribution.fill_equally
173 native.translates_autoresizing_mask_into_constraits = false
174
175 # TODO make customizable
176 native.spacing = 4.0
177 end
178
179 redef fun add(view)
180 do
181 super
182
183 var native_view = view.native
184 assert native_view isa UIView
185 self.native.add_arranged_subview native_view
186 end
187 end
188
189 redef class HorizontalLayout
190 redef init do native.axis = new UILayoutConstraintAxis.horizontal
191 end
192
193 redef class VerticalLayout
194 redef init do native.axis = new UILayoutConstraintAxis.vertical
195 end
196
197 redef class Label
198
199 redef type NATIVE: UILabel
200 redef var native = new UILabel
201
202 redef fun text=(text) do native.text = (text or else "").to_nsstring
203 redef fun text do return native.text.to_s
204 end
205
206 # On iOS, check boxes are a layout composed of a label and an `UISwitch`
207 redef class CheckBox
208
209 redef type NATIVE: UIStackView
210 redef fun native do return layout.native
211
212 # Root layout implementing this check box
213 var layout = new HorizontalLayout(parent=self.parent)
214
215 # Label with the text
216 var lbl = new Label(parent=layout)
217
218 # `UISwitch` acting as the real check box
219 var ui_switch: UISwitch is noautoinit
220
221 init do
222 # Tweak the layout so it is centered
223 layout.native.distribution = new UIStackViewDistribution.fill_proportionally
224 layout.native.alignment = new UIStackViewAlignment.center
225 layout.native.layout_margins_relative_arrangement = true
226
227 var s = new UISwitch
228 native.add_arranged_subview s
229 ui_switch = s
230 end
231
232 redef fun text=(text) do lbl.text = text
233 redef fun text do return lbl.text
234
235 redef fun is_checked do return ui_switch.on
236 redef fun is_checked=(value) do ui_switch.set_on_animated(value, true)
237 end
238
239 redef class TextInput
240
241 redef type NATIVE: UITextField
242 redef var native = new UITextField
243
244 redef fun text=(text) do native.text = (text or else "").to_nsstring
245 redef fun text do return native.text.to_s
246
247 redef fun is_password=(value)
248 do
249 native.secure_text_entry = value or else false
250 super
251 end
252 end
253
254 redef class Button
255
256 redef type NATIVE: UIButton
257 redef var native = new UIButton(new UIButtonType.system)
258
259 init do native.set_callback self
260
261 redef fun text=(text) do if text != null then native.title = text.to_nsstring
262 redef fun text do return native.current_title.to_s
263
264 private fun on_click do notify_observers new ButtonPressEvent(self)
265
266 redef fun enabled=(enabled) do native.enabled = enabled or else true
267 redef fun enabled do return native.enabled
268 end
269
270 redef class UIButton
271 # Register callbacks on this button to be relayed to `sender`
272 private fun set_callback(sender: Button)
273 import Button.on_click in "ObjC" `{
274
275 NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
276 ncr.nit_button = sender;
277
278 // Pin the objects in both Objective-C and Nit GC
279 Button_incr_ref(sender);
280 ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
281
282 [self addTarget:ncr action:@selector(nitOnEvent:)
283 forControlEvents:UIControlEventTouchUpInside];
284 `}
285 end
286
287 # On iOS, implemented by a `UIStackView` inside a ` UIScrollView`
288 redef class ListLayout
289
290 redef type NATIVE: UIScrollView
291 redef var native = new UIScrollView
292
293 # Real container of the subviews, contained within `native`
294 var native_stack_view = new UIStackView
295
296 init
297 do
298 native_stack_view.translates_autoresizing_mask_into_constraits = false
299 native_stack_view.axis = new UILayoutConstraintAxis.vertical
300 native_stack_view.alignment = new UIStackViewAlignment.fill
301 native_stack_view.distribution = new UIStackViewDistribution.fill_equally
302 native_stack_view.spacing = 4.0
303
304 native.add_subview native_stack_view
305 native_add_constraints(native, native_stack_view)
306 end
307
308 private fun native_add_constraints(scroll_view: UIScrollView, stack_view: UIStackView) in "ObjC" `{
309 [scroll_view addConstraints:[NSLayoutConstraint
310 constraintsWithVisualFormat: @"V:|-8-[view]-8-|"
311 options: NSLayoutFormatAlignAllCenterX metrics: nil views: @{@"view": stack_view}]];
312 [scroll_view addConstraints:[NSLayoutConstraint
313 constraintsWithVisualFormat: @"H:|-8-[view]"
314 options: NSLayoutFormatAlignAllCenterX metrics: nil views: @{@"view": stack_view}]];
315 `}
316
317 redef fun add(view)
318 do
319 super
320
321 if view isa View then
322 native_stack_view.add_arranged_subview view.native
323 end
324 end
325 end
326
327 # iOS specific layout using a `UITableView`, works only with simple children views
328 class TableView
329 super CompositeControl
330
331 redef type NATIVE: UITableView
332 redef var native = new UITableView(new UITableViewStyle.plain)
333
334 init
335 do
336 native.autoresizing_mask
337 native.assign_delegate_and_data_source self
338 end
339
340 redef fun add(item)
341 do
342 # Adding a view to a UITableView is a bit tricky.
343 #
344 # Items are added to the Objective-C view only by callbacks.
345 # We must store the sub views in local lists while waiting
346 # for the callbacks.
347 #
348 # As usual, we keep the Nity object in `items`.
349 # But we also keep their native counterparts in a list of
350 # the `UITableViewAndDataSource` set as `native.delegate`.
351 # Otherwise the native views could be freed by the Objective-C GC.
352
353 # TODO use an adapter for the app.nit ListLayout closer to what exists
354 # on both iOS and Android, to support large data sets.
355
356 if item isa View then
357 add_view_to_native_list(native, item.native)
358 end
359
360 super
361
362 # Force redraw and trigger callbacks
363 native.reload_data
364 end
365
366 private fun add_view_to_native_list(native: UITableView, item: UIView) in "ObjC" `{
367 [((UITableViewAndDataSource*)native.delegate).views addObject:item];
368 `}
369
370 private fun get_view_from_native_list(native: UITableView, index: Int): UIView in "ObjC" `{
371 return [((UITableViewAndDataSource*)native.delegate).views objectAtIndex:index];
372 `}
373
374 # Number of sections in this view
375 #
376 # By default, we assume that all `items` are in a single section,
377 # so there is only one section.
378 #
379 # iOS callback: `numberOfSectionsInTableView`
380 protected fun number_of_sections_in_table_view(view: UITableView): Int
381 do return 1
382
383 # Number of entries in `section`
384 #
385 # By default, we assume that all `items` are in a single section,
386 # so no matter the section, this returns `items.length`.
387 #
388 # iOS callback: `numberOfRowsInSection`
389 protected fun number_of_rows_in_section(view: UITableView, section: Int): Int
390 do return items.length
391
392 # Title for `section`, return `new NSString.nil` for no title
393 #
394 # By default, this returns no title.
395 #
396 # iOS callback: `titleForHeaderInSection`
397 protected fun title_for_header_in_section(view: UITableView, section: Int): NSString
398 do return new NSString.nil
399
400 # Return a `UITableViewCell` for the item at `index_path`
401 #
402 # By default, we assume that all `items` are in a single section.
403 # So no matter the depth of the `index_path`, this returns a cell with
404 # the view at index part of `index_path`.
405 #
406 # iOS callback: `cellForRowAtIndexPath`
407 protected fun cell_for_row_at_index_path(table_view: UITableView, index_path: NSIndexPath): UITableViewCell
408 do
409 var reuse_id = "NitCell".to_nsstring
410 var cell = new UITableViewCell(reuse_id)
411
412 # TODO if there is performance issues, reuse cells with
413 # the following code, but clear the cell before use.
414
415 #var cell = table_view.dequeue_reusable_cell_with_identifier(reuse_id)
416 #if cell.address_is_null then cell = new UITableViewCell(reuse_id)
417
418 var index = index_path.index_at_position(1)
419 var view_native = get_view_from_native_list(table_view, index)
420 var cv = cell.content_view
421 cv.add_subview view_native
422
423 return cell
424 end
425 end
426
427 redef class UITableView
428
429 # Assign `list_view` as `delegate` and `dataSource`, and pin all references in both GCs
430 private fun assign_delegate_and_data_source(list_view: TableView)
431 import TableView.number_of_sections_in_table_view,
432 TableView.number_of_rows_in_section,
433 TableView.title_for_header_in_section,
434 TableView.cell_for_row_at_index_path in "ObjC" `{
435
436 UITableViewAndDataSource *objc_delegate = [[UITableViewAndDataSource alloc] init];
437 objc_delegate = (__bridge UITableViewAndDataSource*)CFBridgingRetain(objc_delegate);
438
439 objc_delegate.nit_list_layout = list_view;
440 TableView_incr_ref(list_view);
441
442 // Set our
443 self.delegate = objc_delegate;
444 self.dataSource = objc_delegate;
445 `}
446 end