1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # Implementation of `app::ui` for iOS
24 // Objective-C object receiving callbacks from UI events
25 @interface NitCallbackReference: NSObject
27 // Nit object target of the callbacks from UI events
28 @property (nonatomic) View nit_view;
30 // Actual callback method
31 -(void) nitOnEvent: (UIView*) sender;
34 @implementation NitCallbackReference
36 -(void) nitOnEvent: (UIView*) sender {
37 View_on_ios_event(self.nit_view);
41 // Proxy for both delegates of UITableView relaying all callbacks to `nit_list_layout
`
42 @interface UITableViewAndDataSource: NSObject <UITableViewDelegate, UITableViewDataSource>
44 // Nit object receiving the callbacks
45 @property TableView nit_list_layout;
47 // List of native views added to this list view from the Nit side
48 @property NSMutableArray *views;
51 @implementation UITableViewAndDataSource
56 self.views = [[NSMutableArray alloc] initWithCapacity:8];
60 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
61 return TableView_number_of_sections_in_table_view(self.nit_list_layout, tableView);
64 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
65 return TableView_number_of_rows_in_section(self.nit_list_layout, tableView, section);
68 - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
69 return TableView_title_for_header_in_section(self.nit_list_layout, tableView, section);
72 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
73 return TableView_cell_for_row_at_index_path(self.nit_list_layout, tableView, indexPath);
77 // View controller associated to an app.nit Window
78 @interface NitViewController: UIViewController
81 @implementation NitViewController
82 - (void)viewWillDisappear:(BOOL)animated {
83 [super viewWillDisappear:animated];
85 if (self.isMovingFromParentViewController || self.isBeingDismissed) {
86 extern App app_nit_ios_app;
87 App_after_window_pop(app_nit_ios_app);
95 redef fun did_finish_launching_with_options
97 app_delegate
.window
= new UIWindow
98 app_delegate
.window
.background_color
= new UIColor.white_color
100 app_delegate
.window
.make_key_and_visible
104 private fun set_view_controller
(window
: UIWindow, native
: UIViewController)
106 // Set the required root view controller
107 UINavigationController *navController = (UINavigationController*)window.rootViewController;
109 if (navController == NULL) {
110 navController = [[UINavigationController alloc]initWithRootViewController:native];
111 navController.edgesForExtendedLayout = UIRectEdgeNone;
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;
117 window.rootViewController = navController;
120 [navController pushViewController:native animated:YES];
123 native.edgesForExtendedLayout = UIRectEdgeNone;
126 redef fun push_window
(window
)
128 set_view_controller
(app_delegate
.window
, window
.native
)
132 # Use iOS ` popViewControllerAnimated`
136 pop_view_controller app_delegate
.window
140 private fun pop_view_controller
(window
: UIWindow) in "ObjC" `{
141 UINavigationController *navController = (UINavigationController*)window.rootViewController;
142 [navController popViewControllerAnimated: YES];
145 # Is the next `after_window_pop` triggered by a call to `pop_window`?
147 # Otherwise, it's by the user via the navigation bar "Back" button.
148 private var manual_pop
= false
150 # Callback when `window` is displayed again
151 private fun after_window_pop
153 if not manual_pop
then window_stack
.pop
159 redef class AppDelegate
161 # The main application window, must be set by `App::on_create`
162 fun window
: UIWindow in "ObjC" `{ return [self window]; `}
164 # The main application window, must be set by `App::on_create
`
165 fun window=(window: UIWindow) in "ObjC" `{ self.window = window; `}
170 # Native implementation of this control
171 fun native
: NATIVE is abstract
173 # Type of the `native` implementation of this control
174 type NATIVE: NSObject
178 redef type NATIVE: UIView
180 redef var enabled
= null is lazy
182 private fun on_ios_event
do end
185 redef class CompositeControl
187 redef fun remove
(view
)
191 if view
isa View then
192 view
.native
.remove_from_superview
197 # View controller associated to an app.nit `Window`
198 extern class NitViewController
199 super UIViewController
201 new import App.after_window_pop
in "ObjC" `{
202 return [[NitViewController alloc] init];
208 redef type NATIVE: NitViewController
209 redef var native
= new NitViewController
211 # Title of this window
212 fun title
: String do return native
.title
.to_s
214 # Set the title of this window
215 fun title
=(title
: String) do native
.title
= title
.to_nsstring
221 var native_view
= view
.native
222 assert native_view
isa UIView
224 if view
isa ListLayout then
225 native
.view
.add_subview native_view
226 else native
.view
= native_view
232 redef type NATIVE: UIStackView
233 redef var native
= new UIStackView
237 native
.alignment
= new UIStackViewAlignment.fill
239 # TODO make customizable
247 var native_view
= view
.native
248 assert native_view
isa UIView
249 self.native
.add_arranged_subview native_view
253 redef class HorizontalLayout
256 native
.axis
= new UILayoutConstraintAxis.horizontal
257 native
.distribution
= new UIStackViewDistribution.fill_equally
261 redef class VerticalLayout
264 native
.axis
= new UILayoutConstraintAxis.vertical
265 native
.distribution
= new UIStackViewDistribution.equal_spacing
270 # Convert `size` from app.nit relative size to iOS font points
271 private fun ios_points
(size
: nullable Float): Float
273 size
= size
or else 1.0
274 return 8.0 + size
* 5.0
280 redef type NATIVE: UILabel
281 redef var native
= new UILabel
283 redef fun text
=(text
) do native
.text
= (text
or else "").to_nsstring
284 redef fun text
do return native
.text
.to_s
286 redef fun size
=(size
) do native
.size
= ios_points
(size
)
288 redef fun align
=(align
) do native
.align
= align
or else 0.0
293 private fun size
=(points
: Float)
295 self.font = [UIFont systemFontOfSize: points];
298 private fun align
=(align
: Float)
301 self.textAlignment = NSTextAlignmentCenter;
302 else if (align < 0.5)
303 self.textAlignment = NSTextAlignmentLeft;
304 else//if (align > 0.5)
305 self.textAlignment = NSTextAlignmentRight;
309 # On iOS, check boxes are a layout composed of a label and an `UISwitch`
312 redef type NATIVE: UIStackView
313 redef fun native
do return layout
.native
315 # Root layout implementing this check box
316 var layout
= new HorizontalLayout(parent
=self.parent
)
318 # Label with the text
319 var lbl
= new Label(parent
=layout
)
321 # `UISwitch` acting as the real check box
322 var ui_switch
: UISwitch is noautoinit
324 redef fun on_ios_event
do notify_observers
new ToggleEvent(self)
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
334 native
.add_arranged_subview s
337 ui_switch
.set_callback
self
340 redef fun text
=(text
) do lbl
.text
= text
341 redef fun text
do return lbl
.text
343 redef fun is_checked
do return ui_switch
.on
344 redef fun is_checked
=(value
) do ui_switch
.set_on_animated
(value
, true)
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" `{
352 NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
353 ncr.nit_view = sender;
355 // Pin the objects in both Objective-C and Nit GC
356 View_incr_ref(sender);
357 ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
359 [self addTarget:ncr action:@selector(nitOnEvent:)
360 forControlEvents:UIControlEventValueChanged];
364 redef class TextInput
366 redef type NATIVE: UITextField
367 redef var native
= new UITextField
369 redef fun text
=(text
) do native
.text
= (text
or else "").to_nsstring
370 redef fun text
do return native
.text
.to_s
372 redef fun is_password
=(value
)
374 native
.secure_text_entry
= value
or else false
378 redef fun size
=(size
) do native
.size
= ios_points
(size
)
380 redef fun align
=(align
) do native
.align
= align
or else 0.0
383 redef class UITextField
385 private fun size
=(points
: Float)
387 self.font = [UIFont systemFontOfSize: points];
390 private fun align
=(align
: Float)
393 self.textAlignment = NSTextAlignmentCenter;
394 else if (align < 0.5)
395 self.textAlignment = NSTextAlignmentLeft;
396 else//if (align > 0.5)
397 self.textAlignment = NSTextAlignmentRight;
403 redef type NATIVE: UIButton
404 redef var native
= new UIButton(new UIButtonType.system
)
406 init do native
.set_callback
self
408 redef fun on_ios_event
do notify_observers
new ButtonPressEvent(self)
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
413 redef fun enabled
=(enabled
) do native
.enabled
= enabled
or else true
414 redef fun enabled
do return native
.enabled
416 redef fun size
=(size
) do native
.title_label
.size
= ios_points
(size
)
418 redef fun align
=(align
) do native
.title_label
.align
= align
or else 0.0
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" `{
426 NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
427 ncr.nit_view = sender;
429 // Pin the objects in both Objective-C and Nit GC
430 View_incr_ref(sender);
431 ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
433 [self addTarget:ncr action:@selector(nitOnEvent:)
434 forControlEvents:UIControlEventTouchUpInside];
438 # On iOS, implemented by a `UIStackView` inside a ` UIScrollView`
439 redef class ListLayout
441 redef type NATIVE: UIScrollView
442 redef var native
= new UIScrollView
444 # Real container of the subviews, contained within `native`
445 var native_stack_view
= new UIStackView
447 redef fun parent
=(parent
)
452 if parent
isa Window then
453 root_view
= parent
.native
.view
454 else if parent
isa View then
455 root_view
= parent
.native
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
)
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
)
472 # Add constraints to lock the vertical and horizontal dimensions
473 private fun native_add_constraints
(root_view
: UIView, nested_view
: UIView)
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}]];
483 # Add a constraint to lock to the scroll vertically
484 private fun native_lock_vertical_scroll
(scroll_view
: UIScrollView, stack_view
: UIStackView)
486 [scroll_view addConstraint: [scroll_view.widthAnchor constraintEqualToAnchor:stack_view.widthAnchor]];
493 if view
isa View then
494 native_stack_view
.add_arranged_subview view
.native
499 # iOS specific layout using a `UITableView`, works only with simple children views
501 super CompositeControl
503 redef type NATIVE: UITableView
504 redef var native
= new UITableView(new UITableViewStyle.plain
)
508 native
.autoresizing_mask
509 native
.assign_delegate_and_data_source
self
514 # Adding a view to a UITableView is a bit tricky.
516 # Items are added to the Objective-C view only by callbacks.
517 # We must store the sub views in local lists while waiting
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.
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.
528 if item
isa View then
529 add_view_to_native_list
(native
, item
.native
)
534 # Force redraw and trigger callbacks
538 private fun add_view_to_native_list
(native
: UITableView, item
: UIView) in "ObjC" `{
539 [((UITableViewAndDataSource*)native.delegate).views addObject:item];
542 private fun get_view_from_native_list
(native
: UITableView, index
: Int): UIView in "ObjC" `{
543 return [((UITableViewAndDataSource*)native.delegate).views objectAtIndex:index];
546 # Number of sections in this view
548 # By default, we assume that all `items` are in a single section,
549 # so there is only one section.
551 # iOS callback: `numberOfSectionsInTableView`
552 protected fun number_of_sections_in_table_view
(view
: UITableView): Int
555 # Number of entries in `section`
557 # By default, we assume that all `items` are in a single section,
558 # so no matter the section, this returns `items.length`.
560 # iOS callback: `numberOfRowsInSection`
561 protected fun number_of_rows_in_section
(view
: UITableView, section
: Int): Int
562 do return items
.length
564 # Title for `section`, return `new NSString.nil` for no title
566 # By default, this returns no title.
568 # iOS callback: `titleForHeaderInSection`
569 protected fun title_for_header_in_section
(view
: UITableView, section
: Int): NSString
570 do return new NSString.nil
572 # Return a `UITableViewCell` for the item at `index_path`
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`.
578 # iOS callback: `cellForRowAtIndexPath`
579 protected fun cell_for_row_at_index_path
(table_view
: UITableView, index_path
: NSIndexPath): UITableViewCell
581 var reuse_id
= "NitCell".to_nsstring
582 var cell
= new UITableViewCell(reuse_id
)
584 # TODO if there is performance issues, reuse cells with
585 # the following code, but clear the cell before use.
587 #var cell = table_view.dequeue_reusable_cell_with_identifier(reuse_id)
588 #if cell.address_is_null then cell = new UITableViewCell(reuse_id)
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
599 redef class UITableView
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" `{
608 UITableViewAndDataSource *objc_delegate = [[UITableViewAndDataSource alloc] init];
609 objc_delegate = (__bridge UITableViewAndDataSource*)CFBridgingRetain(objc_delegate);
611 objc_delegate.nit_list_layout = list_view;
612 TableView_incr_ref(list_view);
615 self.delegate = objc_delegate;
616 self.dataSource = objc_delegate;
621 redef fun open_in_browser
do to_nsstring
.native_open_in_browser
625 private fun native_open_in_browser
627 NSURL *nsurl = [NSURL URLWithString: self];
628 [[UIApplication sharedApplication] openURL: nsurl];