mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-10 10:55:33 +00:00
feat(ios): rule of thumb for scaling future devices
This commit is contained in:
parent
8120db8796
commit
5fee04bc08
1 changed files with 105 additions and 14 deletions
|
|
@ -41,6 +41,16 @@ struct Scaling {
|
|||
|
||||
class KeyboardScaleMap {
|
||||
private var scalings: [String: Scaling]
|
||||
|
||||
/**
|
||||
* Based off of Device's UIKit size (in points): refer to
|
||||
* https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html
|
||||
*/
|
||||
private var pointSizes: [String: CGSize]
|
||||
|
||||
private var phoneScalingThresholds: [Device]
|
||||
private var tabletScalingThresholds: [Device]
|
||||
|
||||
static var _shared: KeyboardScaleMap?
|
||||
|
||||
private init() {
|
||||
|
|
@ -49,7 +59,7 @@ class KeyboardScaleMap {
|
|||
// All scalings below are taken from Simulator readings, using the most-up-to-date version of iOS that they support.
|
||||
// Note that the banner heights (at minimum) may vary across iOS versions!
|
||||
//
|
||||
// None of the widths seem accurate, but I've yet to figure out how to get the exact readings.
|
||||
// None of the widths seem accurate _for the keyboard_, but I've yet to figure out how to get the exact readings.
|
||||
|
||||
// Nothing available for iOS 9.3.5 - Xcode does not provide a Simulator version.
|
||||
|
||||
|
|
@ -83,6 +93,8 @@ class KeyboardScaleMap {
|
|||
scalings[KeyboardScaleMap.hashKey(for: Device.iPhone8Plus)] = Scaling(portrait: KeyboardSize(w: 414, h: 226, b: 45), landscape: KeyboardSize(w: 736, h: 162, b: 38)) // does not use full width in landscape
|
||||
|
||||
// Start: 'safe area insets', which are not counted (here) as part of a keyboard's height.
|
||||
// Note that the widths reported for notched phones exclude part of the area to the sides in landscape b/c
|
||||
// of safe-area guidelines. It's not exact, though, as the system globe & dictation keys go beyond those guides.
|
||||
scalings[KeyboardScaleMap.hashKey(for: Device.iPhoneX)] = Scaling(portrait: KeyboardSize(w: 375, h: 216, b: 45), landscape: KeyboardSize(w: 724, h: 150, b: 38)) // insets: p: 34, l: 21
|
||||
scalings[KeyboardScaleMap.hashKey(for: Device.iPhoneXR)] = Scaling(portrait: KeyboardSize(w: 414, h: 226, b: 45), landscape: KeyboardSize(w: 808, h: 150, b: 38)) // insets: p: 34, l: 21
|
||||
scalings[KeyboardScaleMap.hashKey(for: Device.iPhoneXS)] = Scaling(portrait: KeyboardSize(w: 414, h: 216, b: 44), landscape: KeyboardSize(w: 724, h: 150, b: 38)) // insets: p: 34, l: 21. And yeah, that '44' isn't a typo.
|
||||
|
|
@ -97,6 +109,35 @@ class KeyboardScaleMap {
|
|||
scalings[KeyboardScaleMap.hashKey(for: Device.iPadPro9Inch)] = Scaling(portrait: KeyboardSize(w: 768, h: 265, b: 55), landscape: KeyboardSize(w: 1024, h: 353, b: 55)) // Banner always shows; merges copy ctrls with pred-banner.
|
||||
scalings[KeyboardScaleMap.hashKey(for: Device.iPadPro11Inch)] = Scaling(portrait: KeyboardSize(w: 834, h: 265, b: 55), landscape: KeyboardSize(w: 1194, h: 353, b: 55)) // Banner always shows; has a safe-area inset {p: 20, l: 20}
|
||||
scalings[KeyboardScaleMap.hashKey(for: Device.iPadPro12Inch3)] = Scaling(portrait: KeyboardSize(w: 1024, h: 328, b: 55), landscape: KeyboardSize(w: 1366, h: 423, b: 55)) // Banner always shows; has a safe-area inset {p: 20, l: 20}
|
||||
|
||||
// The following definitions are in sorted order of increasing device resolution.
|
||||
// Currrently, this also tends to correlates with the underlying devices' release dates.
|
||||
phoneScalingThresholds = [ Device.iPhoneSE, // smallest phone for 13.0
|
||||
Device.iPhone8, // is 'pre-notch'
|
||||
Device.iPhoneX, // smallest with 'notch'
|
||||
Device.iPhoneXR, // largest current model
|
||||
]
|
||||
|
||||
tabletScalingThresholds = [ Device.iPad5, // smallest supported tablet
|
||||
Device.iPad7, // smallest 13.0 tablet
|
||||
Device.iPadAir3,
|
||||
Device.iPadPro11Inch,
|
||||
Device.iPadPro12Inch3, // taller than others of width, but is most recent at the same scale.
|
||||
]
|
||||
|
||||
pointSizes = [:]
|
||||
|
||||
// `UIScreen.mains.bounds` values for threshold devices. Taken in "portrait" orientation.
|
||||
pointSizes[KeyboardScaleMap.hashKey(for: Device.iPhoneSE)] = CGSize(width: 320, height: 568)
|
||||
pointSizes[KeyboardScaleMap.hashKey(for: Device.iPhone8)] = CGSize(width: 375, height: 667)
|
||||
pointSizes[KeyboardScaleMap.hashKey(for: Device.iPhoneX)] = CGSize(width: 375, height: 812)
|
||||
pointSizes[KeyboardScaleMap.hashKey(for: Device.iPhoneXR)] = CGSize(width: 414, height: 896)
|
||||
|
||||
pointSizes[KeyboardScaleMap.hashKey(for: Device.iPad5)] = CGSize(width: 768, height: 1024)
|
||||
pointSizes[KeyboardScaleMap.hashKey(for: Device.iPad7)] = CGSize(width: 810, height: 1080)
|
||||
pointSizes[KeyboardScaleMap.hashKey(for: Device.iPadAir3)] = CGSize(width: 834, height: 1112)
|
||||
pointSizes[KeyboardScaleMap.hashKey(for: Device.iPadPro11Inch)] = CGSize(width: 834, height: 1194)
|
||||
pointSizes[KeyboardScaleMap.hashKey(for: Device.iPadPro12Inch3)] = CGSize(width: 1024, height: 1366)
|
||||
}
|
||||
|
||||
public static var shared: KeyboardScaleMap {
|
||||
|
|
@ -114,6 +155,53 @@ class KeyboardScaleMap {
|
|||
return device.realDevice.description
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the detected dimensions of the current device to those of devices with known keyboard dimensions, returning
|
||||
* the largest device smaller than or equal to the detected dimensions.
|
||||
*/
|
||||
private static func getUnknownDeviceMapping() -> Device {
|
||||
let _screenSize = UIScreen.main.bounds
|
||||
|
||||
// Shouldn't happen, but just in case.
|
||||
if _screenSize == CGRect.zero {
|
||||
log.error("Cannot detect device dimensions; defaulting to smallest device for form factor.")
|
||||
}
|
||||
|
||||
// Convert to CGSize in portrait orientation.
|
||||
var screenSize: CGSize
|
||||
if _screenSize.width > _screenSize.height {
|
||||
screenSize = CGSize(width: _screenSize.height, height: _screenSize.width)
|
||||
} else {
|
||||
screenSize = CGSize(width: _screenSize.width, height: _screenSize.height)
|
||||
}
|
||||
|
||||
// Array for search is determined by form-factor.
|
||||
var searchSet: [Device]
|
||||
if(UIDevice.current.userInterfaceIdiom == .phone) {
|
||||
searchSet = KeyboardScaleMap.shared.phoneScalingThresholds
|
||||
} else {
|
||||
searchSet = KeyboardScaleMap.shared.tabletScalingThresholds
|
||||
}
|
||||
|
||||
// Okay, now that we've got the important values, it's time to search.
|
||||
let potentialMatches = searchSet.compactMap { device in
|
||||
let matchSize = KeyboardScaleMap.shared.pointSizes[KeyboardScaleMap.hashKey(for: device)]!
|
||||
if matchSize.width > screenSize.width || // if searchSet's device is wider
|
||||
matchSize.height > screenSize.height { // or taller
|
||||
return nil as Device? // it's not a permitted match for mapping uses.
|
||||
} else {
|
||||
return device
|
||||
}
|
||||
} as [Device]
|
||||
|
||||
if potentialMatches.count == 0 {
|
||||
return searchSet[0] // in case no matches can be found (like the screenSize == CGRect.zero case) - use smallest
|
||||
} else {
|
||||
// Use the largest potential device, which is the last one returned from the mapping (b/c we presorted the base array)
|
||||
return searchSet.last!
|
||||
}
|
||||
}
|
||||
|
||||
static func getDeviceDefaultKeyboardScale(forPortrait: Bool) -> KeyboardSize? {
|
||||
let device = Device.current
|
||||
|
||||
|
|
@ -121,24 +209,27 @@ class KeyboardScaleMap {
|
|||
return forPortrait ? scaling.portrait : scaling.landscape
|
||||
}
|
||||
|
||||
var isUnknown = false
|
||||
switch device {
|
||||
case .unknown(_):
|
||||
// The expected case; applies to future models of iPhone and iPad. DeviceKit is of no help here.
|
||||
|
||||
// VERY temporary stop-gap. A better rule-of-thumb is needed.
|
||||
let dev = UIDevice.current
|
||||
var scaling: Scaling
|
||||
|
||||
if(dev.userInterfaceIdiom == .phone) {
|
||||
scaling = shared.scalings[KeyboardScaleMap.hashKey(for: Device.iPhoneSE)]!
|
||||
} else {
|
||||
scaling = shared.scalings[KeyboardScaleMap.hashKey(for: Device.iPad5)]!
|
||||
isUnknown = true
|
||||
fallthrough
|
||||
default:
|
||||
if !isUnknown {
|
||||
// We can still perform a mapping, but it's not ideal.
|
||||
log.warning("Keyboard scaling definition missing for device \(device.description)")
|
||||
}
|
||||
|
||||
// The expected case: isUnknown = true.
|
||||
// Applies to future models of iPhone and iPad. DeviceKit is of no help here, so we
|
||||
// compute the closest-resolution known device.
|
||||
let mappedDevice = getUnknownDeviceMapping()
|
||||
|
||||
// As noted in the scalings data-map, devices of the same dimensions tend to have the same
|
||||
// keyboard dimensions. It's not a perfect rule, but should suffice for a stop-gap solution.
|
||||
let scaling = shared.scalings[KeyboardScaleMap.hashKey(for: mappedDevice)]!
|
||||
|
||||
return forPortrait ? scaling.portrait : scaling.landscape
|
||||
default:
|
||||
log.error("Keyboard scaling definition missing for device \(device.description)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue