VoiceOver 非常 laggy/slow 在屏幕上有很多子视图
VoiceOver very laggy/slow on screen with many subViews
我正在为我的 iOS 名为 Swordy Quest 的游戏构建完全可访问性:
https://apps.apple.com/us/app/swordy-quest-an-rpg-adventure/id1446641513
正如您从上面的屏幕截图中看到的那样 link,我使用 50x50 个单独的 UIView 创建了一个地图,每个 UIView 上都有一个 UIButton,所有这些都位于 UIScrollView 上。关闭 VoiceOver 后,整个应用程序(包括地图部分)工作正常 - 尽管有时地图加载速度可能有点慢。当我打开 VoiceOver 时,整个应用程序响应良好,除了地图部分,它变得非常滞后 - 在我的 iPhone 7 上几乎无法播放(喜欢用旧的 phone 来测试最差的用户体验)。
如果打开 VoiceOver,我曾尝试删除图像细节,但这没有任何区别。这让我认为滞后是由于 50 x 50 UIViews 所有这些都添加了 accessibilityLabel。如果单个 UIViewController 上的可访问标签太多,VoiceOver 会开始严重滞后吗?
有谁知道解决这个问题的巧妙方法吗?我想知道如果 UIView/UIButton 位于 UIScrollView 的可见部分时,您是否可以关闭 AccessibilityLabels 是否是一种聪明的方法?
您不应一次实例化并呈现 2500 个视图。
现代设备可以很好地处理它,但它仍然会影响性能和内存使用,应该避免。
支持 VoiceOver 只是暴露了这种不良做法。
这里使用的正确工具是 UICollectionView。只有可见的视图才会加载到内存中,这会显着限制性能影响。
您可以轻松实现任何类型的自定义布局,包括您需要的 x/y 可滚动瓷砖地图。
请参阅以下最小实现以帮助您入门。您可以将代码复制到新创建的 Xcode 项目的 AppDelegate.swift 文件中,以根据您的需要进行调整。
// 1. create new Xcode project
// 2. delete all swift files except AppDelegate
// 3. delete storyboard
// 4. delete references to storyboard and scene from info.plist
// 5. copy the following code to AppDelegate
import UIKit
// boilerplate
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let layout = MapLayout(columnCount: 50, itemSize: 50)!
self.window?.rootViewController = ViewController(collectionViewLayout: layout)
self.window?.makeKeyAndVisible()
return true
}
}
class MapLayout: UICollectionViewLayout {
let columnCount: Int
let itemSize: CGFloat
var layoutAttributesCache = Dictionary<IndexPath, UICollectionViewLayoutAttributes>()
init?(columnCount: Int, itemSize: CGFloat) {
guard columnCount > 0 else { return nil }
self.columnCount = columnCount
self.itemSize = itemSize
super.init()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override var collectionViewContentSize: CGSize {
let itemCount = self.collectionView?.numberOfItems(inSection: 0) ?? 0
let width = CGFloat(min(itemCount, self.columnCount)) * itemSize
let height = ceil(CGFloat(itemCount / self.columnCount)) * itemSize
return CGSize(width: width, height: height)
}
// the interesting part: here the layout is calculated
override func prepare() {
let itemCount = self.collectionView?.numberOfItems(inSection: 0) ?? 0
for index in 0..<itemCount {
let xIndex = index % self.columnCount
let yIndex = Int( Double(index / self.columnCount) )
let xPos = CGFloat(xIndex) * self.itemSize
let yPos = CGFloat(yIndex) * self.itemSize
let cellAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0))
cellAttributes.frame = CGRect(x: xPos, y: yPos, width: self.itemSize, height: self.itemSize)
self.layoutAttributesCache[cellAttributes.indexPath] = cellAttributes
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
layoutAttributesCache.values.filter { rect.intersects([=10=].frame) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
self.layoutAttributesCache[indexPath]!
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { false }
}
// boilerplate
class ViewController: UICollectionViewController {
var columnCount: Int { (self.collectionViewLayout as! MapLayout).columnCount }
var rowCount: Int { columnCount }
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
override func numberOfSections(in collectionView: UICollectionView) -> Int { 1 }
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { rowCount * columnCount }
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
let fancyColor = UIColor(hue: CGFloat((indexPath.row % columnCount))/CGFloat(columnCount), saturation: 1, brightness: 1 - floor( Double(indexPath.row) / Double( columnCount) ) / Double(rowCount), alpha: 1).cgColor
cell.layer.borderColor = fancyColor
cell.layer.borderWidth = 2
return cell
}
}
结果应如下所示:
我正在为我的 iOS 名为 Swordy Quest 的游戏构建完全可访问性: https://apps.apple.com/us/app/swordy-quest-an-rpg-adventure/id1446641513
正如您从上面的屏幕截图中看到的那样 link,我使用 50x50 个单独的 UIView 创建了一个地图,每个 UIView 上都有一个 UIButton,所有这些都位于 UIScrollView 上。关闭 VoiceOver 后,整个应用程序(包括地图部分)工作正常 - 尽管有时地图加载速度可能有点慢。当我打开 VoiceOver 时,整个应用程序响应良好,除了地图部分,它变得非常滞后 - 在我的 iPhone 7 上几乎无法播放(喜欢用旧的 phone 来测试最差的用户体验)。
如果打开 VoiceOver,我曾尝试删除图像细节,但这没有任何区别。这让我认为滞后是由于 50 x 50 UIViews 所有这些都添加了 accessibilityLabel。如果单个 UIViewController 上的可访问标签太多,VoiceOver 会开始严重滞后吗?
有谁知道解决这个问题的巧妙方法吗?我想知道如果 UIView/UIButton 位于 UIScrollView 的可见部分时,您是否可以关闭 AccessibilityLabels 是否是一种聪明的方法?
您不应一次实例化并呈现 2500 个视图。
现代设备可以很好地处理它,但它仍然会影响性能和内存使用,应该避免。
支持 VoiceOver 只是暴露了这种不良做法。
这里使用的正确工具是 UICollectionView。只有可见的视图才会加载到内存中,这会显着限制性能影响。 您可以轻松实现任何类型的自定义布局,包括您需要的 x/y 可滚动瓷砖地图。
请参阅以下最小实现以帮助您入门。您可以将代码复制到新创建的 Xcode 项目的 AppDelegate.swift 文件中,以根据您的需要进行调整。
// 1. create new Xcode project
// 2. delete all swift files except AppDelegate
// 3. delete storyboard
// 4. delete references to storyboard and scene from info.plist
// 5. copy the following code to AppDelegate
import UIKit
// boilerplate
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let layout = MapLayout(columnCount: 50, itemSize: 50)!
self.window?.rootViewController = ViewController(collectionViewLayout: layout)
self.window?.makeKeyAndVisible()
return true
}
}
class MapLayout: UICollectionViewLayout {
let columnCount: Int
let itemSize: CGFloat
var layoutAttributesCache = Dictionary<IndexPath, UICollectionViewLayoutAttributes>()
init?(columnCount: Int, itemSize: CGFloat) {
guard columnCount > 0 else { return nil }
self.columnCount = columnCount
self.itemSize = itemSize
super.init()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override var collectionViewContentSize: CGSize {
let itemCount = self.collectionView?.numberOfItems(inSection: 0) ?? 0
let width = CGFloat(min(itemCount, self.columnCount)) * itemSize
let height = ceil(CGFloat(itemCount / self.columnCount)) * itemSize
return CGSize(width: width, height: height)
}
// the interesting part: here the layout is calculated
override func prepare() {
let itemCount = self.collectionView?.numberOfItems(inSection: 0) ?? 0
for index in 0..<itemCount {
let xIndex = index % self.columnCount
let yIndex = Int( Double(index / self.columnCount) )
let xPos = CGFloat(xIndex) * self.itemSize
let yPos = CGFloat(yIndex) * self.itemSize
let cellAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0))
cellAttributes.frame = CGRect(x: xPos, y: yPos, width: self.itemSize, height: self.itemSize)
self.layoutAttributesCache[cellAttributes.indexPath] = cellAttributes
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
layoutAttributesCache.values.filter { rect.intersects([=10=].frame) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
self.layoutAttributesCache[indexPath]!
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { false }
}
// boilerplate
class ViewController: UICollectionViewController {
var columnCount: Int { (self.collectionViewLayout as! MapLayout).columnCount }
var rowCount: Int { columnCount }
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
override func numberOfSections(in collectionView: UICollectionView) -> Int { 1 }
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { rowCount * columnCount }
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
let fancyColor = UIColor(hue: CGFloat((indexPath.row % columnCount))/CGFloat(columnCount), saturation: 1, brightness: 1 - floor( Double(indexPath.row) / Double( columnCount) ) / Double(rowCount), alpha: 1).cgColor
cell.layer.borderColor = fancyColor
cell.layer.borderWidth = 2
return cell
}
}
结果应如下所示: