UIPageViewController:从右到左的语言反转 .scroll 动画方向

Posted

技术标签:

【中文标题】UIPageViewController:从右到左的语言反转 .scroll 动画方向【英文标题】:UIPageViewController: Reverse .scroll animation direction for right-to-left languages 【发布时间】:2022-01-13 17:38:18 【问题描述】:

在使用UIPageViewController.TransitionStyle.scroll 时,是否可以将UIPageViewController 的动画方向反转为从右到左的语言?

左右滑动可以正确反转从右到左,但我的下一个按钮解决方案的动画方向错误(动画就像从左到右一样)。

我目前的解决方案是设置pageControl.semanticContentAttribute = .forceLeftToRight,并反转viewModels,以便从右到左的索引0是最后一个索引(viewModels.count - 1),以及反转动画方向+ viewControllerBefore/viewControllerAfter,例如:

viewControllerBefore 变为:nextIndex = isRightToLeft ? vc.index - 1 : vc.index + 1(反之亦然,viewControllerAfternextTappedinDirection 参数为 transitionFrom 更改为 isRightToLeft ? .reverse : .forward 更改loadFirstPage 以将startIndex 设置为numberOfPages - 1(又名viewModels.count - 1)而不是0 反转视图模型,使索引 0 为 viewModels.count - 1(在 init 中):
     if isRightToLeft 
         viewModels = viewModels.map( viewModel in
             ViewModel(index: (numberOfPages - viewModel.index - 1), text: viewModel.text, color: viewModel.color)
         ).reversed()

但我想要一个简单地改变开箱即用动画方向的解决方案,就像在将spineLocation 更改为UIPageViewController.TransitionStyle.pageCurl 时似乎是可能的,比如this question 状态。

动画

请注意,对于How it is by default,页面来自右侧,但页面控制指示器向相反(预期的)方向移动。

How it is by default Expected Functionality

默认实现:

import UIKit

struct ViewModel 
    var index: Int
    var text: String
    var color: UIColor


class DummyViewController : UIViewController 
    let label = UILabel()
    
    let vm: ViewModel
    
    let index: Int
    
    init(vm: ViewModel) 
        self.vm = vm
        self.index = vm.index
        super.init(nibName: nil, bundle: nil)
    
    
    required init?(coder: NSCoder)  fatalError("init(coder:) has not been implemented") 
    
    override func loadView() 
        let view = UIView()
        view.backgroundColor = vm.color
        
        let label = UILabel()
        label.frame = CGRect(x: 20, y: 200, width: 200, height: 20)
        label.textColor = .black
        label.text = vm.text
        
        view.addSubview(label)
        
        self.view = view
    


class ViewController : UIViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource 
    let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
    
    let pageControl = UIPageControl()
    
    let nextButton = UIButton()
    
    var isRightToLeft: Bool 
        traitCollection.layoutDirection == .rightToLeft
    
    
    var currentIndex: Int = 0 
        didSet 
            pageControl.currentPage = currentIndex
        
    
    
    var numberOfPages: Int  return viewModels.count 
    
    var viewModels: [ViewModel] = [
        ViewModel(index: 0, text: "First", color: .red),
        ViewModel(index: 1, text: "Second", color: .blue),
        ViewModel(index: 2, text: "Third", color: .green),
    ]
    
    required init?(coder: NSCoder) 
        super.init(coder: coder)
        
        pageViewController.dataSource = self
        pageViewController.delegate = self
        
        pageControl.currentPage = currentIndex
        pageControl.numberOfPages = numberOfPages
        
        nextButton.setTitle("Next", for: .normal)
        nextButton.setTitleColor(.black, for: .normal)
    
    
    override func viewDidLoad() 
        addChild(pageViewController)
        
        pageViewController.didMove(toParent: self)
        
        view.addSubview(pageViewController.view)
        view.addSubview(pageControl)
        view.addSubview(nextButton)
        
        view.subviews.forEach  $0.translatesAutoresizingMaskIntoConstraints = false 
        
        NSLayoutConstraint.activate([
            pageViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
            pageViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
            pageViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor),
            pageViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),

            pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            pageControl.centerYAnchor.constraint(equalTo: nextButton.centerYAnchor),
            
            nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            nextButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor)
        ])
        
        nextButton.addTarget(self, action: #selector(nextTapped), for: .primaryActionTriggered)
        
        loadFirstPage()
    
    
    func loadFirstPage() 
        let startIndex: Int
        startIndex = 0
        
        guard let start = viewControllerAtIndex(startIndex) else 
            return
        
        
        pageViewController.setViewControllers([start], direction: .forward, animated: true, completion: nil)
    
    
    func transitionFrom(index: Int, inDirection direction: UIPageViewController.NavigationDirection) 
        let nextIndex = direction == .forward ? index + 1 : index - 1
        
        guard let next = viewControllerAtIndex(nextIndex) else 
            return
        
        pageViewController.setViewControllers([next], direction: direction, animated: true, completion:  finished in
            self.pageViewController(self.pageViewController, didFinishAnimating: finished, previousViewControllers: [], transitionCompleted: finished)
        )
    
    
    func viewControllerAtIndex(_ index: Int) -> DummyViewController? 
        guard index >= 0 && index < numberOfPages  else  return nil 
        let viewModel = viewModels[index]
        return DummyViewController(vm: viewModel)
    
    
    @objc
    func nextTapped(_ sender: UIButton) 
        guard currentIndex < numberOfPages - 1 else 
            print("ending because \(currentIndex)")
            return
        
        
        let direction: UIPageViewController.NavigationDirection = .forward
        transitionFrom(index: currentIndex, inDirection: direction)
    
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? 
        guard let vc = viewController as? DummyViewController else  return nil 
        let nextIndex: Int = vc.index - 1
        return viewControllerAtIndex(nextIndex)
    
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? 
        guard let vc = viewController as? DummyViewController else  return nil 
        let nextIndex: Int = vc.index + 1
        return viewControllerAtIndex(nextIndex)
    
    
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) 
        guard let viewController = pageViewController.viewControllers?.first as? DummyViewController else  return 
        currentIndex = viewController.index // Update currentIndex, which updates pageControl.currentPage
    

手动反转以支持从右到左(我试图避免的解决方案)。

排除了ViewModelDummyViewController,因为它们没有改变。

import UIKit

class ViewController : UIViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource 
    let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
    
    let pageControl = UIPageControl()
    
    let nextButton = UIButton()
    
    var isRightToLeft: Bool 
        traitCollection.layoutDirection == .rightToLeft
    
    
    var currentIndex: Int = 0 
        didSet 
            pageControl.currentPage = currentIndex
        
    
    
    var numberOfPages: Int 
        return viewModels.count
    
    
    var viewModels: [ViewModel] = [
        ViewModel(index: 0, text: "First", color: .red),
        ViewModel(index: 1, text: "Second", color: .blue),
        ViewModel(index: 2, text: "Third", color: .green),
    ]
    
    required init?(coder: NSCoder) 
        super.init(coder: coder)
        
        pageViewController.dataSource = self
        pageViewController.delegate = self
        
        pageControl.currentPage = currentIndex
        pageControl.numberOfPages = numberOfPages
        
        pageControl.semanticContentAttribute = .forceLeftToRight
        
        nextButton.setTitle("Next", for: .normal)
        nextButton.setTitleColor(.black, for: .normal)
        
        if isRightToLeft  // HERE
            viewModels = viewModels.map( viewModel in
                ViewModel(index: (numberOfPages - viewModel.index - 1), text: viewModel.text, color: viewModel.color)
            ).reversed()
        
    
    
    override func viewDidLoad() 
        addChild(pageViewController)
        
        pageViewController.didMove(toParent: self)
        
        view.addSubview(pageViewController.view)
        view.addSubview(pageControl)
        view.addSubview(nextButton)
        
        view.subviews.forEach  $0.translatesAutoresizingMaskIntoConstraints = false 
        
        NSLayoutConstraint.activate([
            pageViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
            pageViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
            pageViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor),
            pageViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),

            pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            pageControl.centerYAnchor.constraint(equalTo: nextButton.centerYAnchor),
            
            nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            nextButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor)
        ])
        
        nextButton.addTarget(self, action: #selector(nextTapped), for: .primaryActionTriggered)
        
        loadFirstPage()
    
    
    func loadFirstPage() 
        let startIndex: Int
        if isRightToLeft  // HERE
            startIndex = numberOfPages - 1
            currentIndex = startIndex
         else 
            startIndex = 0
        
        
        guard let start = viewControllerAtIndex(startIndex) else 
            return
        
        
        pageViewController.setViewControllers([start], direction: .forward, animated: true, completion: nil)
    
    
    func transitionFrom(index: Int, inDirection direction: UIPageViewController.NavigationDirection)  // UNCHANGED
        let nextIndex = direction == .forward ? index + 1 : index - 1
        
        guard let next = viewControllerAtIndex(nextIndex) else 
            return
        
        pageViewController.setViewControllers([next], direction: direction, animated: true, completion:  finished in
            self.pageViewController(self.pageViewController, didFinishAnimating: finished, previousViewControllers: [], transitionCompleted: finished)
        )
    
    
    func viewControllerAtIndex(_ index: Int) -> DummyViewController? 
        guard index >= 0 && index < numberOfPages  else  return nil 
        let viewModel = viewModels[index]
        return DummyViewController(vm: vm)
    
    
    @objc
    func nextTapped(_ sender: UIButton) 
        guard (currentIndex < numberOfPages - 1 && !isRightToLeft) || (currentIndex > 0 && isRightToLeft) else  // HERE
            return
        
        let direction: UIPageViewController.NavigationDirection = isRightToLeft ? .reverse : .forward // HERE
        transitionFrom(index: currentIndex, inDirection: direction)
    
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? 
        guard let vc = viewController as? DummyViewController else 
            return nil
        
        
        let nextIndex: Int
        if isRightToLeft  // HERE
            nextIndex = vc.index + 1
         else 
            nextIndex = vc.index - 1
        
        
        return viewControllerAtIndex(nextIndex)
    
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? 
        guard let vc = viewController as? DummyViewController else 
            return nil
        
        
        let nextIndex: Int
        if isRightToLeft  // HERE
            nextIndex = vc.index - 1
         else 
            nextIndex = vc.index + 1
        
        
        return viewControllerAtIndex(nextIndex)
    
    
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) 
        guard let viewController = pageViewController.viewControllers?.first as? DummyViewController else 
            return
        
        
        currentIndex = viewController.index
    



【问题讨论】:

【参考方案1】:

事实证明这比我想象的要简单得多 - 设置 direction 只会改变 动画 方向,它不会改变索引/视图控制器的运行方向呈现出来。

因此,解决方案只需将transitionFrom 更改为:

    func transitionTo(nextIndex: Int, inDirection direction: UIPageViewController.NavigationDirection) 
        // Removed the `next` setting here in favor of getting it passed from `nextTapped`
        guard let next = viewControllerAtIndex(nextIndex) else 
            return
        
        pageViewController.setViewControllers([next], direction: direction, animated: true, completion:  finished in
            self.pageViewController(self.pageViewController, didFinishAnimating: finished, previousViewControllers: [], transitionCompleted: finished)
        )
    

并将nextTapped 更改为:

    @objc
    func nextTapped(_ sender: UIButton) 
        guard currentIndex < numberOfPages - 1 else  return 
        
        let direction: UIPageViewController.NavigationDirection = isRightToLeft ? .reverse : .forward // This is key
        transitionTo(nextIndex: currentIndex + 1, inDirection: direction)
    

另一种侵入性较小的解决方案

或者,如果我们处于从右到左的语言环境中,那么侵入性较小的解决方案是继承 UIPageViewController 并简单地翻转动画方向。

private extension UIPageViewController.NavigationDirection 
    var flipped: Self 
        switch self 
        case .forward:
            return .reverse
        case .reverse:
            return .forward
        @unknown default:
            return .reverse
        
    


class LocalizedPageViewController: UIPageViewController 
    override func setViewControllers(
        _ viewControllers: [UIViewController]?,
        direction: UIPageViewController.NavigationDirection,
        animated: Bool,
        completion: ((Bool) -> Void)? = nil
    ) 
        let isRTL = view.effectiveUserInterfaceLayoutDirection == .rightToLeft
        let direction = isRTL ? direction.flipped : direction
        super.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion)
    

那么我只需要将UIPageViewController 的初始化更改为LocalizedPageViewController,无需其他更改!

【讨论】:

以上是关于UIPageViewController:从右到左的语言反转 .scroll 动画方向的主要内容,如果未能解决你的问题,请参考以下文章

使用 CATransition 从右到左并返回?

从右到左滚动文字效果

从右到左滑动抽屉菜单

如何使用 CGAffineTransform Scale 从右到左添加动画和放大动画

带有从右到左单选按钮的离子警报

Excel 互操作从右到左文档