03ea1f129c9ad56b69347ef65a074a1136ba030e
[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) View nit_view;
29
30 // Actual callback method
31 -(void) nitOnEvent: (UIView*) sender;
32 @end
33
34 @implementation NitCallbackReference
35
36 -(void) nitOnEvent: (UIView*) sender {
37 View_on_ios_event(self.nit_view);
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 app_delegate.window = new UIWindow
82 app_delegate.window.background_color = new UIColor.white_color
83 super
84 app_delegate.window.make_key_and_visible
85 return true
86 end
87
88 private fun set_view_controller(window: UIWindow, native: UIViewController)
89 in "ObjC" `{
90 // Set the required root view controller
91 UINavigationController *navController = (UINavigationController*)window.rootViewController;
92
93 if (navController == NULL) {
94 navController = [[UINavigationController alloc]initWithRootViewController:native];
95 navController.edgesForExtendedLayout = UIRectEdgeNone;
96
97 // Must be non-translucent for the controls to be placed under
98 // (as in Y axis) of the navigation bar.
99 navController.navigationBar.translucent = NO;
100
101 window.rootViewController = navController;
102 }
103 else {
104 [navController pushViewController:native animated:YES];
105 }
106
107 native.edgesForExtendedLayout = UIRectEdgeNone;
108 `}
109
110 redef fun window=(window)
111 do
112 set_view_controller(app_delegate.window, window.native)
113 super
114 end
115
116 # Use iOS ` popViewControllerAnimated`
117 redef fun pop_window
118 do
119 window_stack.pop
120 pop_view_controller app_delegate.window
121 window.on_resume
122 end
123
124 private fun pop_view_controller(window: UIWindow) in "ObjC" `{
125 UINavigationController *navController = (UINavigationController*)window.rootViewController;
126 [navController popViewControllerAnimated: YES];
127 `}
128 end
129
130 redef class AppDelegate
131
132 # The main application window, must be set by `App::on_create`
133 fun window: UIWindow in "ObjC" `{ return [self window]; `}
134
135 # The main application window, must be set by `App::on_create`
136 fun window=(window: UIWindow) in "ObjC" `{ self.window = window; `}
137 end
138
139 redef class Control
140
141 # Native implementation of this control
142 fun native: NATIVE is abstract
143
144 # Type of the `native` implementation of this control
145 type NATIVE: NSObject
146 end
147
148 redef class View
149 redef type NATIVE: UIView
150
151 redef var enabled = null is lazy
152
153 private fun on_ios_event do end
154 end
155
156 redef class CompositeControl
157
158 redef fun remove(view)
159 do
160 super
161
162 if view isa View then
163 view.native.remove_from_superview
164 end
165 end
166 end
167
168 redef class Window
169
170 redef type NATIVE: UIViewController
171 redef var native = new UIViewController
172
173 # Title of this window
174 fun title: String do return native.title.to_s
175
176 # Set the title of this window
177 fun title=(title: String) do native.title = title.to_nsstring
178
179 redef fun add(view)
180 do
181 super
182
183 var native_view = view.native
184 assert native_view isa UIView
185
186 native.view = native_view
187 end
188 end
189
190 redef class Layout
191
192 redef type NATIVE: UIStackView
193 redef var native = new UIStackView
194
195 init
196 do
197 native.alignment = new UIStackViewAlignment.fill
198
199 # TODO make customizable
200 native.spacing = 4.0
201 end
202
203 redef fun add(view)
204 do
205 super
206
207 var native_view = view.native
208 assert native_view isa UIView
209 self.native.add_arranged_subview native_view
210 end
211 end
212
213 redef class HorizontalLayout
214 redef init
215 do
216 native.axis = new UILayoutConstraintAxis.horizontal
217 native.distribution = new UIStackViewDistribution.fill_equally
218 end
219 end
220
221 redef class VerticalLayout
222 redef init
223 do
224 native.axis = new UILayoutConstraintAxis.vertical
225 native.distribution = new UIStackViewDistribution.equal_spacing
226 end
227 end
228
229 redef class Label
230
231 redef type NATIVE: UILabel
232 redef var native = new UILabel
233
234 redef fun text=(text) do native.text = (text or else "").to_nsstring
235 redef fun text do return native.text.to_s
236
237 redef fun size=(size)
238 do
239 size = size or else 1.0
240 var points = 8.0 + size * 8.0
241 set_size_native(native, points)
242 end
243
244 private fun set_size_native(native: UILabel, points: Float)
245 in "ObjC" `{
246 native.font = [UIFont systemFontOfSize: points];
247 `}
248
249 redef fun align=(align) do set_align_native(native, align or else 0.0)
250
251 private fun set_align_native(native: UILabel, align: Float)
252 in "ObjC" `{
253
254 if (align == 0.5)
255 native.textAlignment = NSTextAlignmentCenter;
256 else if (align < 0.5)
257 native.textAlignment = NSTextAlignmentLeft;
258 else//if (align > 0.5)
259 native.textAlignment = NSTextAlignmentRight;
260 `}
261 end
262
263 # On iOS, check boxes are a layout composed of a label and an `UISwitch`
264 redef class CheckBox
265
266 redef type NATIVE: UIStackView
267 redef fun native do return layout.native
268
269 # Root layout implementing this check box
270 var layout = new HorizontalLayout(parent=self.parent)
271
272 # Label with the text
273 var lbl = new Label(parent=layout)
274
275 # `UISwitch` acting as the real check box
276 var ui_switch: UISwitch is noautoinit
277
278 redef fun on_ios_event do notify_observers new ToggleEvent(self)
279
280 init
281 do
282 # Tweak the layout so it is centered
283 layout.native.distribution = new UIStackViewDistribution.fill_proportionally
284 layout.native.alignment = new UIStackViewAlignment.center
285 layout.native.layout_margins_relative_arrangement = true
286
287 var s = new UISwitch
288 native.add_arranged_subview s
289 ui_switch = s
290
291 ui_switch.set_callback self
292 end
293
294 redef fun text=(text) do lbl.text = text
295 redef fun text do return lbl.text
296
297 redef fun is_checked do return ui_switch.on
298 redef fun is_checked=(value) do ui_switch.set_on_animated(value, true)
299 end
300
301 redef class UISwitch
302 # Register callbacks on this switch to be relayed to `sender`
303 private fun set_callback(sender: View)
304 import View.on_ios_event in "ObjC" `{
305
306 NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
307 ncr.nit_view = sender;
308
309 // Pin the objects in both Objective-C and Nit GC
310 View_incr_ref(sender);
311 ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
312
313 [self addTarget:ncr action:@selector(nitOnEvent:)
314 forControlEvents:UIControlEventValueChanged];
315 `}
316 end
317
318 redef class TextInput
319
320 redef type NATIVE: UITextField
321 redef var native = new UITextField
322
323 redef fun text=(text) do native.text = (text or else "").to_nsstring
324 redef fun text do return native.text.to_s
325
326 redef fun is_password=(value)
327 do
328 native.secure_text_entry = value or else false
329 super
330 end
331 end
332
333 redef class Button
334
335 redef type NATIVE: UIButton
336 redef var native = new UIButton(new UIButtonType.system)
337
338 init do native.set_callback self
339
340 redef fun on_ios_event do notify_observers new ButtonPressEvent(self)
341
342 redef fun text=(text) do if text != null then native.title = text.to_nsstring
343 redef fun text do return native.current_title.to_s
344
345 redef fun enabled=(enabled) do native.enabled = enabled or else true
346 redef fun enabled do return native.enabled
347 end
348
349 redef class UIButton
350 # Register callbacks on this button to be relayed to `sender`
351 private fun set_callback(sender: View)
352 import View.on_ios_event in "ObjC" `{
353
354 NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
355 ncr.nit_view = sender;
356
357 // Pin the objects in both Objective-C and Nit GC
358 View_incr_ref(sender);
359 ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
360
361 [self addTarget:ncr action:@selector(nitOnEvent:)
362 forControlEvents:UIControlEventTouchUpInside];
363 `}
364 end
365
366 # On iOS, implemented by a `UIStackView` inside a ` UIScrollView`
367 redef class ListLayout
368
369 redef type NATIVE: UIScrollView
370 redef var native = new UIScrollView
371
372 # Real container of the subviews, contained within `native`
373 var native_stack_view = new UIStackView
374
375 init
376 do
377 native_stack_view.translates_autoresizing_mask_into_constraits = false
378 native_stack_view.axis = new UILayoutConstraintAxis.vertical
379 native_stack_view.alignment = new UIStackViewAlignment.fill
380 native_stack_view.distribution = new UIStackViewDistribution.fill_equally
381 native_stack_view.spacing = 4.0
382
383 native.add_subview native_stack_view
384 native_add_constraints(native, native_stack_view)
385 end
386
387 private fun native_add_constraints(scroll_view: UIScrollView, stack_view: UIStackView) in "ObjC" `{
388 [scroll_view addConstraints:[NSLayoutConstraint
389 constraintsWithVisualFormat: @"V:|-8-[view]-8-|"
390 options: NSLayoutFormatAlignAllCenterX metrics: nil views: @{@"view": stack_view}]];
391 [scroll_view addConstraints:[NSLayoutConstraint
392 constraintsWithVisualFormat: @"H:|-8-[view]"
393 options: NSLayoutFormatAlignAllCenterX metrics: nil views: @{@"view": stack_view}]];
394 `}
395
396 redef fun add(view)
397 do
398 super
399
400 if view isa View then
401 native_stack_view.add_arranged_subview view.native
402 end
403 end
404 end
405
406 # iOS specific layout using a `UITableView`, works only with simple children views
407 class TableView
408 super CompositeControl
409
410 redef type NATIVE: UITableView
411 redef var native = new UITableView(new UITableViewStyle.plain)
412
413 init
414 do
415 native.autoresizing_mask
416 native.assign_delegate_and_data_source self
417 end
418
419 redef fun add(item)
420 do
421 # Adding a view to a UITableView is a bit tricky.
422 #
423 # Items are added to the Objective-C view only by callbacks.
424 # We must store the sub views in local lists while waiting
425 # for the callbacks.
426 #
427 # As usual, we keep the Nity object in `items`.
428 # But we also keep their native counterparts in a list of
429 # the `UITableViewAndDataSource` set as `native.delegate`.
430 # Otherwise the native views could be freed by the Objective-C GC.
431
432 # TODO use an adapter for the app.nit ListLayout closer to what exists
433 # on both iOS and Android, to support large data sets.
434
435 if item isa View then
436 add_view_to_native_list(native, item.native)
437 end
438
439 super
440
441 # Force redraw and trigger callbacks
442 native.reload_data
443 end
444
445 private fun add_view_to_native_list(native: UITableView, item: UIView) in "ObjC" `{
446 [((UITableViewAndDataSource*)native.delegate).views addObject:item];
447 `}
448
449 private fun get_view_from_native_list(native: UITableView, index: Int): UIView in "ObjC" `{
450 return [((UITableViewAndDataSource*)native.delegate).views objectAtIndex:index];
451 `}
452
453 # Number of sections in this view
454 #
455 # By default, we assume that all `items` are in a single section,
456 # so there is only one section.
457 #
458 # iOS callback: `numberOfSectionsInTableView`
459 protected fun number_of_sections_in_table_view(view: UITableView): Int
460 do return 1
461
462 # Number of entries in `section`
463 #
464 # By default, we assume that all `items` are in a single section,
465 # so no matter the section, this returns `items.length`.
466 #
467 # iOS callback: `numberOfRowsInSection`
468 protected fun number_of_rows_in_section(view: UITableView, section: Int): Int
469 do return items.length
470
471 # Title for `section`, return `new NSString.nil` for no title
472 #
473 # By default, this returns no title.
474 #
475 # iOS callback: `titleForHeaderInSection`
476 protected fun title_for_header_in_section(view: UITableView, section: Int): NSString
477 do return new NSString.nil
478
479 # Return a `UITableViewCell` for the item at `index_path`
480 #
481 # By default, we assume that all `items` are in a single section.
482 # So no matter the depth of the `index_path`, this returns a cell with
483 # the view at index part of `index_path`.
484 #
485 # iOS callback: `cellForRowAtIndexPath`
486 protected fun cell_for_row_at_index_path(table_view: UITableView, index_path: NSIndexPath): UITableViewCell
487 do
488 var reuse_id = "NitCell".to_nsstring
489 var cell = new UITableViewCell(reuse_id)
490
491 # TODO if there is performance issues, reuse cells with
492 # the following code, but clear the cell before use.
493
494 #var cell = table_view.dequeue_reusable_cell_with_identifier(reuse_id)
495 #if cell.address_is_null then cell = new UITableViewCell(reuse_id)
496
497 var index = index_path.index_at_position(1)
498 var view_native = get_view_from_native_list(table_view, index)
499 var cv = cell.content_view
500 cv.add_subview view_native
501
502 return cell
503 end
504 end
505
506 redef class UITableView
507
508 # Assign `list_view` as `delegate` and `dataSource`, and pin all references in both GCs
509 private fun assign_delegate_and_data_source(list_view: TableView)
510 import TableView.number_of_sections_in_table_view,
511 TableView.number_of_rows_in_section,
512 TableView.title_for_header_in_section,
513 TableView.cell_for_row_at_index_path in "ObjC" `{
514
515 UITableViewAndDataSource *objc_delegate = [[UITableViewAndDataSource alloc] init];
516 objc_delegate = (__bridge UITableViewAndDataSource*)CFBridgingRetain(objc_delegate);
517
518 objc_delegate.nit_list_layout = list_view;
519 TableView_incr_ref(list_view);
520
521 // Set our
522 self.delegate = objc_delegate;
523 self.dataSource = objc_delegate;
524 `}
525 end