/* Copyright 2024 New Vector Ltd. Copyright 2017 Vector Creations Ltd Copyright 2015 OpenMarket Ltd SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ #import "SegmentedViewController.h" #import "ThemeService.h" #import "GeneratedInterface-Swift.h" @interface SegmentedViewController () { // Tell whether the segmented view is appeared (see viewWillAppear/viewWillDisappear). BOOL isViewAppeared; // list of displayed UIViewControllers NSArray* viewControllers; // The constraints of the displayed viewController NSLayoutConstraint *displayedVCTopConstraint; NSLayoutConstraint *displayedVCLeftConstraint; NSLayoutConstraint *displayedVCWidthConstraint; NSLayoutConstraint *displayedVCHeightConstraint; // list of NSString NSArray* sectionTitles; // list of section labels NSArray* sectionLabels; // the selected marker view UIView* selectedMarkerView; NSLayoutConstraint *leftMarkerViewConstraint; // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. __weak id kThemeServiceDidChangeThemeNotificationObserver; } @end @implementation SegmentedViewController #pragma mark - Class methods + (UINib *)nib { return [UINib nibWithNibName:NSStringFromClass([SegmentedViewController class]) bundle:[NSBundle bundleForClass:[SegmentedViewController class]]]; } + (instancetype)segmentedViewController { return [[[self class] alloc] initWithNibName:NSStringFromClass([SegmentedViewController class]) bundle:[NSBundle bundleForClass:[SegmentedViewController class]]]; } /** init the segmentedViewController with a list of UIViewControllers. @param titles the section tiles @param someViewControllers the list of viewControllers to display. @param defaultSelected index of the default selected UIViewController in the list. */ - (void)initWithTitles:(NSArray*)titles viewControllers:(NSArray*)someViewControllers defaultSelected:(NSUInteger)defaultSelected { viewControllers = someViewControllers; sectionTitles = titles; _selectedIndex = defaultSelected; } - (void)destroy { for (id viewController in viewControllers) { if ([viewController respondsToSelector:@selector(destroy)]) { [viewController destroy]; } } viewControllers = nil; sectionTitles = nil; sectionLabels = nil; if (selectedMarkerView) { [selectedMarkerView removeFromSuperview]; selectedMarkerView = nil; } if (kThemeServiceDidChangeThemeNotificationObserver) { [[NSNotificationCenter defaultCenter] removeObserver:kThemeServiceDidChangeThemeNotificationObserver]; kThemeServiceDidChangeThemeNotificationObserver = nil; } [super destroy]; } - (void)setSelectedIndex:(NSUInteger)selectedIndex { if (_selectedIndex != selectedIndex) { _selectedIndex = selectedIndex; [self displaySelectedViewController]; } } - (NSArray *)viewControllers { return viewControllers; } - (void)setSectionHeaderTintColor:(UIColor *)sectionHeaderTintColor { if (_sectionHeaderTintColor != sectionHeaderTintColor) { _sectionHeaderTintColor = sectionHeaderTintColor; if (selectedMarkerView) { selectedMarkerView.backgroundColor = sectionHeaderTintColor; } for (UILabel *label in sectionLabels) { label.textColor = sectionHeaderTintColor; } } } #pragma mark - - (void)finalizeInit { [super finalizeInit]; // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; } - (void)viewDidLoad { [super viewDidLoad]; // Check whether the view controller has been pushed via storyboard if (!self.viewControllerContainer) { // Instantiate view controller objects [[[self class] nib] instantiateWithOwner:self options:nil]; } // Adjust Top [NSLayoutConstraint deactivateConstraints:@[self.selectionContainerTopConstraint]]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" // it is not possible to define a constraint to the topLayoutGuide in the xib editor // so do it in the code .. self.selectionContainerTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.selectionContainer attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f]; #pragma clang diagnostic pop [NSLayoutConstraint activateConstraints:@[self.selectionContainerTopConstraint]]; [self createSegmentedViews]; MXWeakify(self); // Observe user interface theme change. kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { MXStrongifyAndReturnIfNil(self); [self userInterfaceThemeDidChange]; }]; [self userInterfaceThemeDidChange]; } - (void)userInterfaceThemeDidChange { [ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar]; self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor; self.view.backgroundColor = ThemeService.shared.theme.backgroundColor; self.sectionHeaderTintColor = ThemeService.shared.theme.tintColor; [self setNeedsStatusBarAppearanceUpdate]; } - (UIStatusBarStyle)preferredStatusBarStyle { return ThemeService.shared.theme.statusBarStyle; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self userInterfaceThemeDidChange]; if (_selectedViewController) { // Make iOS invoke child viewWillAppear [_selectedViewController beginAppearanceTransition:YES animated:animated]; } isViewAppeared = YES; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; if (_selectedViewController) { // Make iOS invoke child viewWillDisappear [_selectedViewController beginAppearanceTransition:NO animated:animated]; } isViewAppeared = NO; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (_selectedViewController) { // Make iOS invoke child viewDidAppear [_selectedViewController endAppearanceTransition]; } } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; if (_selectedViewController) { // Make iOS invoke child viewDidDisappear [_selectedViewController endAppearanceTransition]; } } - (void)createSegmentedViews { NSMutableArray* labels = [[NSMutableArray alloc] init]; NSUInteger count = viewControllers.count; for (NSUInteger index = 0; index < count; index++) { // create programmatically each label UILabel *label = [[UILabel alloc] init]; label.text = sectionTitles[index]; label.font = [UIFont systemFontOfSize:17]; label.textAlignment = NSTextAlignmentCenter; label.textColor = _sectionHeaderTintColor; label.backgroundColor = [UIColor clearColor]; label.accessibilityIdentifier = [NSString stringWithFormat:@"SegmentedVCSectionLabel%tu", index]; // the constraint defines the label frame // so ignore any autolayout stuff [label setTranslatesAutoresizingMaskIntoConstraints:NO]; // add the label before setting the constraints [self.selectionContainer addSubview:label]; NSLayoutConstraint *leftConstraint; if (labels.count) { leftConstraint = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:labels[index - 1] attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0]; } else { leftConstraint = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.selectionContainer attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0]; } NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.selectionContainer attribute:NSLayoutAttributeWidth multiplier:1.0 / count constant:0]; NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.selectionContainer attribute:NSLayoutAttributeTop multiplier:1.0 constant:0]; NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.selectionContainer attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0]; // set the constraints [NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]]; UITapGestureRecognizer *labelTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onLabelTouch:)]; [labelTapGesture setNumberOfTouchesRequired:1]; [labelTapGesture setNumberOfTapsRequired:1]; label.userInteractionEnabled = YES; [label addGestureRecognizer:labelTapGesture]; [labels addObject:label]; } sectionLabels = labels; [self addSelectedMarkerView]; [self displaySelectedViewController]; } - (void)addSelectedMarkerView { // Sanity check NSAssert(sectionLabels.count, @"[SegmentedViewController] addSelectedMarkerView failed - At least one view controller is required"); // create the selected marker view selectedMarkerView = [[UIView alloc] init]; selectedMarkerView.backgroundColor = _sectionHeaderTintColor; [selectedMarkerView setTranslatesAutoresizingMaskIntoConstraints:NO]; [self.selectionContainer addSubview:selectedMarkerView]; leftMarkerViewConstraint = [NSLayoutConstraint constraintWithItem:selectedMarkerView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:sectionLabels[_selectedIndex] attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0]; NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:selectedMarkerView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.selectionContainer attribute:NSLayoutAttributeWidth multiplier:1.0 / sectionLabels.count constant:0]; NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:selectedMarkerView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.selectionContainer attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]; NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:selectedMarkerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:3]; // set the constraints [NSLayoutConstraint activateConstraints:@[leftMarkerViewConstraint, widthConstraint, bottomConstraint, heightConstraint]]; } - (void)displaySelectedViewController { // Sanity check NSAssert(sectionLabels.count, @"[SegmentedViewController] displaySelectedViewController failed - At least one view controller is required"); if (_selectedViewController) { NSUInteger index = [viewControllers indexOfObject:_selectedViewController]; if (index != NSNotFound) { UILabel* label = sectionLabels[index]; label.font = [UIFont systemFontOfSize:17]; } [_selectedViewController willMoveToParentViewController:nil]; [_selectedViewController.view removeFromSuperview]; [_selectedViewController removeFromParentViewController]; [NSLayoutConstraint deactivateConstraints:@[displayedVCTopConstraint, displayedVCLeftConstraint, displayedVCWidthConstraint, displayedVCHeightConstraint]]; } UILabel* label = sectionLabels[_selectedIndex]; label.font = [UIFont boldSystemFontOfSize:17]; // update the marker view position [NSLayoutConstraint deactivateConstraints:@[leftMarkerViewConstraint]]; leftMarkerViewConstraint = [NSLayoutConstraint constraintWithItem:selectedMarkerView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:sectionLabels[_selectedIndex] attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0]; [NSLayoutConstraint activateConstraints:@[leftMarkerViewConstraint]]; // Set the new selected view controller _selectedViewController = viewControllers[_selectedIndex]; // Make iOS invoke selectedViewController viewWillAppear when the segmented view is already visible if (isViewAppeared) { [_selectedViewController beginAppearanceTransition:YES animated:YES]; } [self addChildViewController:_selectedViewController]; [_selectedViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; [self.viewControllerContainer addSubview:_selectedViewController.view]; displayedVCTopConstraint = [NSLayoutConstraint constraintWithItem:_selectedViewController.view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.viewControllerContainer attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f]; displayedVCLeftConstraint = [NSLayoutConstraint constraintWithItem:_selectedViewController.view attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.viewControllerContainer attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f]; displayedVCWidthConstraint = [NSLayoutConstraint constraintWithItem:_selectedViewController.view attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.viewControllerContainer attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0]; displayedVCHeightConstraint = [NSLayoutConstraint constraintWithItem:_selectedViewController.view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.viewControllerContainer attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0]; [NSLayoutConstraint activateConstraints:@[displayedVCTopConstraint, displayedVCLeftConstraint, displayedVCWidthConstraint, displayedVCHeightConstraint]]; [_selectedViewController didMoveToParentViewController:self]; // Make iOS invoke selectedViewController viewDidAppear when the segmented view is already visible if (isViewAppeared) { [_selectedViewController endAppearanceTransition]; } } #pragma mark - Search - (void)showSearch:(BOOL)animated { [super showSearch:animated]; // Show the tabs header if (animated) { [UIView animateWithDuration:.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{ self.selectionContainerHeightConstraint.constant = 44; [self.view layoutIfNeeded]; } completion:^(BOOL finished){ }]; } else { self.selectionContainerHeightConstraint.constant = 44; [self.view layoutIfNeeded]; } } - (void)hideSearch:(BOOL)animated { [super hideSearch:animated]; // Hide the tabs header if (animated) { [UIView animateWithDuration:.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{ self.selectionContainerHeightConstraint.constant = 0; [self.view layoutIfNeeded]; } completion:^(BOOL finished) { // Go back to the main tab // Do it at the end of the animation when the tabs header of the SegmentedVC is hidden // so that the user cannot see the selection bar of this header moving self.selectedIndex = 0; self.selectedViewController.view.hidden = NO; }]; } else { self.selectionContainerHeightConstraint.constant = 0; [self.view layoutIfNeeded]; // Go back to the recents tab self.selectedIndex = 0; self.selectedViewController.view.hidden = NO; } } #pragma mark - touch event - (void)onLabelTouch:(UIGestureRecognizer*)gestureRecognizer { NSUInteger pos = [sectionLabels indexOfObject:gestureRecognizer.view]; // check if there is an update before triggering anything if ((pos != NSNotFound) && (_selectedIndex != pos)) { // update the selected index self.selectedIndex = pos; } } @end