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