Skip to content

Instantly share code, notes, and snippets.

@myell0w
Created May 4, 2020 14:00
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save myell0w/2a686c4e3a9af1ef9b66846938c3e296 to your computer and use it in GitHub Desktop.
Save myell0w/2a686c4e3a9af1ef9b66846938c3e296 to your computer and use it in GitHub Desktop.
Cross-Platform Helpers for iOS/macOS
/// Used to identify traits of the current UI Environment - especially useful for cross-platform code
public struct MNCUITraits: Withable {
/// Does *not* identify the device type, but the idiom of the layout to apply
/// This means that e.g. on iPad the layoutIdiom can be `.phone`, if run in Split Screen or SlideOver
public enum LayoutIdiom {
case phone(hasRoundCorners: Bool)
case pad(hasRoundCorners: Bool)
case mac
}
enum Constants {
static let smallestPhoneSize: CGSize = .init(width: 320.0, height: 568.0)
static let biggestPhoneSize: CGSize = .init(width: 414.0, height: 896.0)
static let smallestPadSize: CGSize = .init(width: 1024.0, height: 768.0)
static let biggestPadSize: CGSize = .init(width: 1366.0, height: 1024.0)
}
// MARK: - Properties
public var layoutIdiom: LayoutIdiom
public var containerSize: CGSize
public var hasRoundCorners: Bool {
switch self.layoutIdiom {
case .phone(let hasRoundCorners), .pad(let hasRoundCorners):
return hasRoundCorners
case .mac:
return false
}
}
/// iPhone SE, iPad in SlideOver
public var isSmallestPhone: Bool { self.layoutIdiom.isPhone && self.containerSize.width <= Constants.smallestPhoneSize.width }
/// iPhone XS Max, iPhone XR
public var isBiggestPhone: Bool { self.layoutIdiom.isPhone && self.containerSize.isEqualOrRotated(Constants.biggestPhoneSize) }
/// < 10.5" iPad
public var isSmallestPad: Bool { self.layoutIdiom.isPad && self.containerSize.isEqualOrRotated(Constants.smallestPadSize) }
/// 12" iPad
public var isBiggestPad: Bool { self.layoutIdiom.isPad && self.containerSize.isEqualOrRotated(Constants.biggestPadSize) }
// MARK: - Lifecycle
init(layoutIdiom: LayoutIdiom, containerSize: CGSize) {
self.layoutIdiom = layoutIdiom
self.containerSize = containerSize
}
/// The ui traits of the current screen
@available(iOSApplicationExtension, unavailable)
public static var currentScreen: MNCUITraits {
#if os(iOS)
let hasRoundCorners = UIDevice.current.mn_hasRoundCorners
return .init(layoutIdiom: UI_USER_INTERFACE_IDIOM() == .phone ? .phone(hasRoundCorners: hasRoundCorners) : .pad(hasRoundCorners: hasRoundCorners),
containerSize: UIScreen.main.bounds.size)
#elseif os(macOS)
let screen = assertNotNil(NSScreen.main) ?? NSScreen.screens[0]
return .init(layoutIdiom: .mac, containerSize: screen.visibleFrame.size)
#endif
}
public static func current(for view: MNView) -> MNCUITraits {
#if os(iOS)
let hasRoundCorners = view.safeAreaInsets.bottom > 0.0
return .init(layoutIdiom: view.traitCollection.mn_isPhoneLayout ? .phone(hasRoundCorners: hasRoundCorners) : .pad(hasRoundCorners: hasRoundCorners),
containerSize: view.bounds.size)
#elseif os(macOS)
return .init(layoutIdiom: .mac, containerSize: view.bounds.size)
#endif
}
// MARK: - MNCUITraits
/// Returns a generic value (e.g. a CGFloat indicating a padding) for the given layout idiom,
/// enforces to specify at least a value for phone, pad and mac and allows to override special devices
public func platformDependentValue<T>(smallestPhone: T? = nil,
phone: T,
biggestPhone: T? = nil,
smallestPad: T? = nil,
pad: T,
biggestPad: T? = nil,
mac: T) -> T {
// default will never be used here…
return self.platformDependentValue(default: phone, smallestPhone: smallestPhone, phone: phone, biggestPhone: biggestPhone, smallestPad: smallestPad, pad: pad, biggestPad: biggestPad, mac: mac)
}
/// Returns a generic value (e.g. a CGFloat indicating a padding) for the given layout idiom.
/// It is required to specify a default value, and optional to override values for given environments
/// like iPhone SE-sized phones (`smallestPhone`) or 12" iPad (`biggestPad`)
public func platformDependentValue<T>(default defaultValue: T,
smallestPhone: T? = nil,
phone: T? = nil,
biggestPhone: T? = nil,
smallestPad: T? = nil,
pad: T? = nil,
biggestPad: T? = nil,
mac: T? = nil) -> T {
let overriddenValue: T?
switch self.layoutIdiom {
case .phone:
overriddenValue = self.isSmallestPhone ? (smallestPhone ?? phone) : self.isBiggestPhone ? (biggestPhone ?? phone) : phone
case .pad:
overriddenValue = self.isSmallestPad ? (smallestPad ?? pad) : self.isBiggestPad ? (biggestPad ?? pad) : pad
case .mac:
overriddenValue = mac
}
return overriddenValue ?? defaultValue
}
}
// MARK: - Platform-Dependent Values
/// Returns a generic value (e.g. a CGFloat indicating a padding) for the Operating System
public func platformDependentValue<T>(iOS: T, macOS: T) -> T {
#if os(iOS)
return iOS
#elseif os(macOS)
return macOS
#endif
}
/// Returns a generic value (e.g. a CGFloat indicating a padding) for the given device,
/// enforces to specify at least a value for phone, pad and mac and allows to override special devices
public func platformDependentValue<T>(smallestPhone: T? = nil,
phone: T,
biggestPhone: T? = nil,
smallestPad: T? = nil,
pad: T,
biggestPad: T? = nil,
mac: T) -> T {
// default will never be used here…
return platformDependentValue(default: phone,
smallestPhone: smallestPhone,
phone: phone,
biggestPhone: biggestPhone,
smallestPad: smallestPad,
pad: pad,
biggestPad: biggestPad, mac: mac)
}
/// Returns a generic value (e.g. a CGFloat indicating a padding) for the given device.
/// It is required to specify a default value, and optional to override values for given environments
/// like iPhone SE-sized phones (`smallestPhone`) or 12" iPad (`biggestPad`)
public func platformDependentValue<T>(default defaultValue: T,
smallestPhone: T? = nil,
phone: T? = nil,
biggestPhone: T? = nil,
smallestPad: T? = nil,
pad: T? = nil,
biggestPad: T? = nil,
mac: T? = nil) -> T {
let overriddenValue: T?
#if os(iOS)
let device = UIDevice.current
if device.userInterfaceIdiom == .phone {
overriddenValue = device.mn_isSmallestPhone ? (smallestPhone ?? phone) : device.mn_isBiggestPhone ? (biggestPhone ?? phone) : phone
} else {
overriddenValue = device.mn_isSmallestPad ? (smallestPad ?? pad) : device.mn_isBiggestPad ? (biggestPad ?? pad) : pad
}
#elseif os(macOS)
overriddenValue = mac
#endif
return overriddenValue ?? defaultValue
}
// MARK: - MNView+MNCUITraits
public extension MNView {
var mn_uiTraits: MNCUITraits { .current(for: self) } // swiftlint:disable:this identifier_name
}
// MARK: - UIDevice+Detection
/// Extension for easier detection of certain devices
public extension UIDevice {
@available(iOSApplicationExtension, unavailable)
@objc var mn_hasRoundCorners: Bool {
guard let window = UIApplication.shared.keyWindow else { return false }
return window.safeAreaInsets.bottom > 0.0
}
/// iPhone with round corners
@available(iOSApplicationExtension, unavailable)
@objc var mn_hasTopNotch: Bool {
guard let window = UIApplication.shared.keyWindow else { return false }
guard self.userInterfaceIdiom == .phone else { return false }
return window.safeAreaInsets.bottom > 0.0
}
/// iPhone SE-sized
@objc var mn_isSmallestPhone: Bool {
guard self.userInterfaceIdiom == .phone else { return false }
let screenSize = UIScreen.main.fixedCoordinateSpace.bounds.size
return screenSize.width <= MNCUITraits.Constants.smallestPhoneSize.width
}
/// iPhone XS Max, iPhone XR
@objc var mn_isBiggestPhone: Bool {
guard self.userInterfaceIdiom == .phone else { return false }
let screenSize = UIScreen.main.fixedCoordinateSpace.bounds.size
return screenSize == MNCUITraits.Constants.biggestPhoneSize
}
/// < 10.5" iPad
@objc var mn_isSmallestPad: Bool {
guard self.userInterfaceIdiom == .pad else { return false }
let screenSize = UIScreen.main.fixedCoordinateSpace.bounds.size
return screenSize == MNCUITraits.Constants.smallestPadSize
}
/// 12" iPad
@objc var mn_isBiggestPad: Bool {
guard self.userInterfaceIdiom == .pad else { return false }
let screenSize = UIScreen.main.fixedCoordinateSpace.bounds.size
return screenSize == MNCUITraits.Constants.biggestPadSize
}
}
// MARK: - UITraitCollection+Detection
/// Extension for easier detection of certain traits
public extension UITraitCollection {
@objc var mn_isPadLayout: Bool { self.userInterfaceIdiom == .pad && self.horizontalSizeClass == .regular }
@objc var mn_isPhoneLayout: Bool { self.userInterfaceIdiom == .phone || self.horizontalSizeClass == .compact }
@objc var mn_isPortraitPhoneLayout: Bool { self.horizontalSizeClass == .compact && self.verticalSizeClass == .regular }
@objc var mn_isLandscapePhone: Bool { self.userInterfaceIdiom == .phone && self.verticalSizeClass == .compact }
@objc var mn_wantsVerticalCellLayout: Bool { self.preferredContentSizeCategory.isAccessibilityCategory }
}
// MARK: - Private
private extension MNCUITraits.LayoutIdiom {
var isPhone: Bool {
switch self {
case .phone:
return true
case .pad, .mac:
return false
}
}
var isPad: Bool {
switch self {
case .pad:
return true
case .phone, .mac:
return false
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment