Contents

william-weng/wwinterfaceorientationcoordinator

English | 正體中文

✨ [主要功能](https://medium.com/@iamCoder/device-orientation-modes-in-ios-c86fb3497ede)

  • 觀察 UITraitHorizontalSizeClass 變化,適合處理 Duo 外螢幕與內螢幕間的 compact/regular 切換。
  • 提供 UIInterfaceOrientation?,方便在轉場動畫中判斷目前方向。
  • 使用 weak 持有 UIViewController,避免 coordinator 與 VC 互相強持有。
  • 將初始狀態、trait 變化與轉場動畫的資訊統一封裝成 Information。
  • 適合搭配多個 XIB、UISplitViewController、sidebar/detail 或 compact/regular 版面。

📦 [安裝方式](https://blakecrosley.com/zh-Hant/blog/designing-for-iphone-duo)

使用 Swift Package Manager

在 Xcode 中選擇:

File → Add Package Dependencies...

接著輸入套件 repository URL,選擇要使用的版本,並將套件加入 App target。

如果這是本地套件,也可以在 Xcode 中使用:

File → Add Package Dependencies... → Add Local...

安裝完成後,將該套件匯入目標後,就可以使用了:

import WWInterfaceOrientationCoordinator

🧱 [API 總覽](https://developer.apple.com/design/human-interface-guidelines/designing-for-iphone-duo)

WWInterfaceOrientationCoordinator

| API | 說明 | | --- | --- | | orientation | 目前所觀察 view controller 的 interface orientation。若 view controller 尚未加入 window,或無法取得 window scene,則回傳 nil。 | | horizontalClass | 目前 view controller trait collection 的 horizontal size class。若 view controller 已被釋放,則回傳 nil。 | | register(forHorizontalSizeClassChanges:onChange:) | 註冊 horizontal size class 變更通知。註冊後會立即以目前狀態呼叫 initial,之後每當 UITraitHorizontalSizeClass 改變時呼叫 onChange。 | | willTransition(with:animation:completion:) | 在 view controller 轉場期間,依目前 interface orientation 執行自訂動畫。若無法取得目前方向,則不會呼叫動畫 callback。 |

WWInterfaceOrientationCoordinator (iOS 27.1)

| API | 說明 | | --- | --- | | divisionRegions | 獲取畫面上所有【物理存在】的摺疊線/分割區域。 | | occlusionRegions | 獲取畫面上所有【當前正在遮擋像素】的硬體/系統區域。 | | activeDivisionRegion | 獲取當前【真正被折起、看得見】的第一個主要摺疊線區域。 | | crossAxisDimensionRange(with:ratio:minimum:maximum:) | 依跨軸比例建立 UISplitArrangement.DimensionRange。 |

WWLayoutEnvironmentInformation

| API | 說明 | | --- | --- | | previousTrait | 變更前的 Size Class;沒有前值時傳 nil。 | | currentTrait | 目前的 Size Class;無法取得時傳 nil。 | | orientation | 目前的介面方向;無法取得時傳 nil。 | | unknown | 空白/未知的方向資訊。 | | isLandscape | 是否為橫向 UI。 | | isPortrait | 是否為直向 UI。 | | isRegularWidth | 目前的 horizontal size class 是否為 regular。 | | isCompactWidth | 目前的 horizontal size class 是否為 compact。 | | isRegularHeight | 目前的 vertical size class 是否為 regular。 | | isCompactHeight | 目前的 vertical size class 是否為 compact。 | | didChangeToRegularWidth | horizontal size class 是否從 compact 變成 regular。 | | didChangeToCompactWidth | horizontal size class 是否從 regular 變成 compact。 | | init(previousHorizontalClass:horizontalClass:orientation:) | 目前所觀察 view controller 的 interface orientation。若 view controller 尚未加入 window,或無法取得 window scene,則回傳 nil。 |

WWTraitSizeClass

| API | 說明 | | --- | --- | | horizontalClass | horizontal size class;無法取得時傳 nil。 | | verticalSizeClass | vertical size class;無法取得時傳 nil。 | | unknown | 空白/未知的 Size Class 狀態。 |

🧪 [使用範例](https://developer.apple.com/videos/play/wwdc2017/812/)

import UIKit
import WWInterfaceOrientationCoordinator

final class ViewController: UIViewController {
    
    @IBOutlet weak var imageView: UIImageView!
    
    private lazy var orientationCoordinator = WWInterfaceOrientationCoordinator(viewController: self)
    
    private let foldBox = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        initFolderView()
        
        orientationCoordinator.register { [weak self] info in
            guard let self else { return }
            updateLayout(info: info)
        } onChange: { [weak self] info in
            guard let self else { return }
            updateLayout(info: info)
        }
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        orientationCoordinator.willTransition(with: coordinator) { [weak self] info in
            guard let self else { return }
            updateLayout(info: info)
        }
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        if #available(iOS 27.1, *) {
            if let region = orientationCoordinator.activeDivisionRegion {
                foldBox.frame = region.frame
            } else {
                foldBox.frame = .zero
            }
        }
    }
}

private extension ViewController {
    
    func initFolderView() {
        
        foldBox.layer.borderColor = UIColor.systemRed.cgColor
        foldBox.layer.borderWidth = 2
        foldBox.backgroundColor = UIColor.systemRed.withAlphaComponent(0.3)
        foldBox.isUserInteractionEnabled = false
        
        view.addSubview(foldBox)
    }

    func updateLayout(info: WWLayoutEnvironmentInformation) {
        
        let orientation = info.orientation ?? .portrait
        let horizontalClass = info.currentTrait?.horizontalClass ?? .regular
        
        switch orientation {
        case .landscapeLeft, .landscapeRight: imageView.image = (horizontalClass == .regular) ? .innerLandscape : .outterLandscape
        case .portrait, .portraitUpsideDown: imageView.image = (horizontalClass == .regular) ?.innerPortrait:.outterPortrait
        case .unknown: imageView.image = .outterPortrait
        @unknown default: imageView.image = .outterPortrait
        }
    }
}

Design Notes

  • 不要只用 orientation 決定 layout。 Duo 的同一個 orientation 可能對應不同螢幕或折疊姿勢;layout 應優先依 horizontal size class 與實際 view bounds 調整。
  • orientation 適合用於動畫與方向相關行為。 例如旋轉時改變動畫方向、調整過場效果,或處理媒體播放方向。
  • Coordinator 不擁有 VC。 它以 weak reference 觀察 view controller,因此不會造成 UIViewController 與 coordinator 的 retain cycle。
  • callback 請避免強持有 VC。 若你在 initial、onChange、animation 或 completion closure 中 capture self,建議視情況使用 [weak self]。

Package Metadata

Repository: william-weng/wwinterfaceorientationcoordinator

Default branch: main

README: README.md