X-Git-Url: http://nitlanguage.org diff --git a/lib/ios/ui/ui.nit b/lib/ios/ui/ui.nit index 96d1e8a..da04397 100644 --- a/lib/ios/ui/ui.nit +++ b/lib/ios/ui/ui.nit @@ -25,16 +25,16 @@ in "ObjC" `{ @interface NitCallbackReference: NSObject // Nit object target of the callbacks from UI events - @property (nonatomic) Button nit_button; + @property (nonatomic) View nit_view; // Actual callback method - -(void) nitOnEvent: (UIButton*) sender; + -(void) nitOnEvent: (UIView*) sender; @end @implementation NitCallbackReference - -(void) nitOnEvent: (UIButton*) sender { - Button_on_click(self.nit_button); + -(void) nitOnEvent: (UIView*) sender { + View_on_ios_event(self.nit_view); } @end @@ -42,7 +42,7 @@ in "ObjC" `{ @interface UITableViewAndDataSource: NSObject // Nit object receiving the callbacks - @property ListLayout nit_list_layout; + @property TableView nit_list_layout; // List of native views added to this list view from the Nit side @property NSMutableArray *views; @@ -58,36 +58,102 @@ in "ObjC" `{ } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - return ListLayout_number_of_sections_in_table_view(self.nit_list_layout, tableView); + return TableView_number_of_sections_in_table_view(self.nit_list_layout, tableView); } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return ListLayout_number_of_rows_in_section(self.nit_list_layout, tableView, section); + return TableView_number_of_rows_in_section(self.nit_list_layout, tableView, section); } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { - return ListLayout_title_for_header_in_section(self.nit_list_layout, tableView, section); + return TableView_title_for_header_in_section(self.nit_list_layout, tableView, section); } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - return ListLayout_cell_for_row_at_index_path(self.nit_list_layout, tableView, indexPath); + return TableView_cell_for_row_at_index_path(self.nit_list_layout, tableView, indexPath); + } +@end + +// View controller associated to an app.nit Window +@interface NitViewController: UIViewController +@end + +@implementation NitViewController + - (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + if (self.isMovingFromParentViewController || self.isBeingDismissed) { + extern App app_nit_ios_app; + App_after_window_pop(app_nit_ios_app); + } } @end `} redef class App + redef fun did_finish_launching_with_options do + app_delegate.window = new UIWindow + app_delegate.window.background_color = new UIColor.white_color super - window.native.make_key_and_visible + app_delegate.window.make_key_and_visible return true end - redef fun window=(window) + private fun set_view_controller(window: UIWindow, native: UIViewController) + in "ObjC" `{ + // Set the required root view controller + UINavigationController *navController = (UINavigationController*)window.rootViewController; + + if (navController == NULL) { + navController = [[UINavigationController alloc]initWithRootViewController:native]; + navController.edgesForExtendedLayout = UIRectEdgeNone; + + // Must be non-translucent for the controls to be placed under + // (as in Y axis) of the navigation bar. + navController.navigationBar.translucent = NO; + + window.rootViewController = navController; + } + else { + [navController pushViewController:native animated:YES]; + } + + native.edgesForExtendedLayout = UIRectEdgeNone; + `} + + redef fun push_window(window) + do + set_view_controller(app_delegate.window, window.native) + super + end + + # Use iOS ` popViewControllerAnimated` + redef fun pop_window do - app_delegate.window = window.native + manual_pop = true + pop_view_controller app_delegate.window super end + + private fun pop_view_controller(window: UIWindow) in "ObjC" `{ + UINavigationController *navController = (UINavigationController*)window.rootViewController; + [navController popViewControllerAnimated: YES]; + `} + + # Is the next `after_window_pop` triggered by a call to `pop_window`? + # + # Otherwise, it's by the user via the navigation bar "Back" button. + private var manual_pop = false + + # Callback when `window` is displayed again + private fun after_window_pop + do + if not manual_pop then window_stack.pop + manual_pop = false + window.on_resume + end end redef class AppDelegate @@ -112,6 +178,8 @@ redef class View redef type NATIVE: UIView redef var enabled = null is lazy + + private fun on_ios_event do end end redef class CompositeControl @@ -120,18 +188,31 @@ redef class CompositeControl do super - var native_view = view.native - assert native_view isa UIView - native_view.remove_from_superview + if view isa View then + view.native.remove_from_superview + end end end +# View controller associated to an app.nit `Window` +extern class NitViewController + super UIViewController + + new import App.after_window_pop in "ObjC" `{ + return [[NitViewController alloc] init]; + `} +end + redef class Window - redef type NATIVE: UIWindow - redef var native = new UIWindow + redef type NATIVE: NitViewController + redef var native = new NitViewController - init do native.background_color = new UIColor.white_color + # Title of this window + fun title: String do return native.title.to_s + + # Set the title of this window + fun title=(title: String) do native.title = title.to_nsstring redef fun add(view) do @@ -139,26 +220,11 @@ redef class Window var native_view = view.native assert native_view isa UIView - native.add_subview native_view - fill_whole_window_with(native_view, native) + if view isa ListLayout then + native.view.add_subview native_view + else native.view = native_view end - - private fun fill_whole_window_with(native: UIView, window: UIWindow) - in "ObjC" `{ - // Hard coded borders including the top bar - // FIXME this may cause problems with retina devices - [window addConstraints:[NSLayoutConstraint - constraintsWithVisualFormat: @"V:|-24-[view]-8-|" - options: 0 metrics: nil views: @{@"view": native}]]; - [window addConstraints:[NSLayoutConstraint - constraintsWithVisualFormat: @"H:|-8-[view]-8-|" - options: 0 metrics: nil views: @{@"view": native}]]; - - // Set the required root view controller - window.rootViewController = [[UIViewController alloc]initWithNibName:nil bundle:nil]; - window.rootViewController.view = native; - `} end redef class Layout @@ -169,11 +235,6 @@ redef class Layout init do native.alignment = new UIStackViewAlignment.fill - native.distribution = new UIStackViewDistribution.fill_equally - native.translates_autoresizing_mask_into_constraits = false - - # TODO make customizable - native.spacing = 4.0 end redef fun add(view) @@ -187,11 +248,28 @@ redef class Layout end redef class HorizontalLayout - redef init do native.axis = new UILayoutConstraintAxis.horizontal + redef init + do + native.axis = new UILayoutConstraintAxis.horizontal + native.distribution = new UIStackViewDistribution.fill_equally + end end redef class VerticalLayout - redef init do native.axis = new UILayoutConstraintAxis.vertical + redef init + do + native.axis = new UILayoutConstraintAxis.vertical + native.distribution = new UIStackViewDistribution.equal_spacing + end +end + +redef class TextView + # Convert `size` from app.nit relative size to iOS font points + private fun ios_points(size: nullable Float): Float + do + size = size or else 1.0 + return 8.0 + size * 5.0 + end end redef class Label @@ -201,6 +279,28 @@ redef class Label redef fun text=(text) do native.text = (text or else "").to_nsstring redef fun text do return native.text.to_s + + redef fun size=(size) do native.size = ios_points(size) + + redef fun align=(align) do native.align = align or else 0.0 +end + +redef class UILabel + + private fun size=(points: Float) + in "ObjC" `{ + self.font = [UIFont systemFontOfSize: points]; + `} + + private fun align=(align: Float) + in "ObjC" `{ + if (align == 0.5) + self.textAlignment = NSTextAlignmentCenter; + else if (align < 0.5) + self.textAlignment = NSTextAlignmentLeft; + else//if (align > 0.5) + self.textAlignment = NSTextAlignmentRight; + `} end # On iOS, check boxes are a layout composed of a label and an `UISwitch` @@ -218,15 +318,20 @@ redef class CheckBox # `UISwitch` acting as the real check box var ui_switch: UISwitch is noautoinit - init do + redef fun on_ios_event do notify_observers new ToggleEvent(self) + + init + do # Tweak the layout so it is centered - layout.native.distribution = new UIStackViewDistribution.fill_proportionally - layout.native.alignment = new UIStackViewAlignment.center + layout.native.distribution = new UIStackViewDistribution.equal_spacing + layout.native.alignment = new UIStackViewAlignment.fill layout.native.layout_margins_relative_arrangement = true var s = new UISwitch native.add_arranged_subview s ui_switch = s + + ui_switch.set_callback self end redef fun text=(text) do lbl.text = text @@ -236,6 +341,23 @@ redef class CheckBox redef fun is_checked=(value) do ui_switch.set_on_animated(value, true) end +redef class UISwitch + # Register callbacks on this switch to be relayed to `sender` + private fun set_callback(sender: View) + import View.on_ios_event in "ObjC" `{ + + NitCallbackReference *ncr = [[NitCallbackReference alloc] init]; + ncr.nit_view = sender; + + // Pin the objects in both Objective-C and Nit GC + View_incr_ref(sender); + ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr); + + [self addTarget:ncr action:@selector(nitOnEvent:) + forControlEvents:UIControlEventValueChanged]; + `} +end + redef class TextInput redef type NATIVE: UITextField @@ -249,6 +371,31 @@ redef class TextInput native.secure_text_entry = value or else false super end + + redef fun size=(size) do native.size = ios_points(size) + + redef fun align=(align) do native.align = align or else 0.0 + + # Set the placeholder text, shown in light gray when the field is empty + fun placeholder=(text: Text) do native.placeholder = text.to_nsstring +end + +redef class UITextField + + private fun size=(points: Float) + in "ObjC" `{ + self.font = [UIFont systemFontOfSize: points]; + `} + + private fun align=(align: Float) + in "ObjC" `{ + if (align == 0.5) + self.textAlignment = NSTextAlignmentCenter; + else if (align < 0.5) + self.textAlignment = NSTextAlignmentLeft; + else//if (align > 0.5) + self.textAlignment = NSTextAlignmentRight; + `} end redef class Button @@ -258,25 +405,29 @@ redef class Button init do native.set_callback self + redef fun on_ios_event do notify_observers new ButtonPressEvent(self) + redef fun text=(text) do if text != null then native.title = text.to_nsstring redef fun text do return native.current_title.to_s - private fun on_click do notify_observers new ButtonPressEvent(self) - redef fun enabled=(enabled) do native.enabled = enabled or else true redef fun enabled do return native.enabled + + redef fun size=(size) do native.title_label.size = ios_points(size) + + redef fun align=(align) do native.title_label.align = align or else 0.0 end redef class UIButton # Register callbacks on this button to be relayed to `sender` - private fun set_callback(sender: Button) - import Button.on_click in "ObjC" `{ + private fun set_callback(sender: View) + import View.on_ios_event in "ObjC" `{ NitCallbackReference *ncr = [[NitCallbackReference alloc] init]; - ncr.nit_button = sender; + ncr.nit_view = sender; // Pin the objects in both Objective-C and Nit GC - Button_incr_ref(sender); + View_incr_ref(sender); ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr); [self addTarget:ncr action:@selector(nitOnEvent:) @@ -284,8 +435,70 @@ redef class UIButton `} end +# On iOS, implemented by a `UIStackView` inside a ` UIScrollView` redef class ListLayout + redef type NATIVE: UIScrollView + redef var native = new UIScrollView + + # Real container of the subviews, contained within `native` + var native_stack_view = new UIStackView + + redef fun parent=(parent) + do + super + + var root_view + if parent isa Window then + root_view = parent.native.view + else if parent isa View then + root_view = parent.native + else return + + # Setup scroll view + var native_scroll_view = native + native_scroll_view.translates_autoresizing_mask_into_constraits = false + native_add_constraints(root_view, native_scroll_view) + + # Setup stack_view + native_stack_view.translates_autoresizing_mask_into_constraits = false + native_stack_view.axis = new UILayoutConstraintAxis.vertical + native_scroll_view.add_subview native_stack_view + native_add_constraints(native_scroll_view, native_stack_view) + native_lock_vertical_scroll(native_scroll_view, native_stack_view) + end + + # Add constraints to lock the vertical and horizontal dimensions + private fun native_add_constraints(root_view: UIView, nested_view: UIView) + in "ObjC" `{ + [root_view addConstraints:[NSLayoutConstraint + constraintsWithVisualFormat: @"V:|-0-[nested_view]-0-|" + options: NSLayoutFormatAlignAllCenterX metrics: nil views: @{@"nested_view": nested_view}]]; + [root_view addConstraints:[NSLayoutConstraint + constraintsWithVisualFormat: @"H:|-0-[nested_view]-0-|" + options: NSLayoutFormatAlignAllCenterX metrics: nil views: @{@"nested_view": nested_view}]]; + `} + + # Add a constraint to lock to the scroll vertically + private fun native_lock_vertical_scroll(scroll_view: UIScrollView, stack_view: UIStackView) + in "ObjC" `{ + [scroll_view addConstraint: [scroll_view.widthAnchor constraintEqualToAnchor:stack_view.widthAnchor]]; + `} + + redef fun add(view) + do + super + + if view isa View then + native_stack_view.add_arranged_subview view.native + end + end +end + +# iOS specific layout using a `UITableView`, works only with simple children views +class TableView + super CompositeControl + redef type NATIVE: UITableView redef var native = new UITableView(new UITableViewStyle.plain) @@ -385,20 +598,32 @@ end redef class UITableView # Assign `list_view` as `delegate` and `dataSource`, and pin all references in both GCs - private fun assign_delegate_and_data_source(list_view: ListLayout) - import ListLayout.number_of_sections_in_table_view, - ListLayout.number_of_rows_in_section, - ListLayout.title_for_header_in_section, - ListLayout.cell_for_row_at_index_path in "ObjC" `{ + private fun assign_delegate_and_data_source(list_view: TableView) + import TableView.number_of_sections_in_table_view, + TableView.number_of_rows_in_section, + TableView.title_for_header_in_section, + TableView.cell_for_row_at_index_path in "ObjC" `{ UITableViewAndDataSource *objc_delegate = [[UITableViewAndDataSource alloc] init]; objc_delegate = (__bridge UITableViewAndDataSource*)CFBridgingRetain(objc_delegate); objc_delegate.nit_list_layout = list_view; - ListLayout_incr_ref(list_view); + TableView_incr_ref(list_view); // Set our self.delegate = objc_delegate; self.dataSource = objc_delegate; `} end + +redef class Text + redef fun open_in_browser do to_nsstring.native_open_in_browser +end + +redef class NSString + private fun native_open_in_browser + in "ObjC" `{ + NSURL *nsurl = [NSURL URLWithString: self]; + [[UIApplication sharedApplication] openURL: nsurl]; + `} +end