Skip to content

Instantly share code, notes, and snippets.

@ApolloZhu
Last active June 26, 2017 04:35
Show Gist options
  • Save ApolloZhu/7aa71885211af82ade5e1afe9e1c13d0 to your computer and use it in GitHub Desktop.
Save ApolloZhu/7aa71885211af82ade5e1afe9e1c13d0 to your computer and use it in GitHub Desktop.
[AZProportionalView] A view that always maintains a certain ratio. #swift #iOS
//
// AZProportionalView.swift
// AZProportionalView
//
// Created by Apollo Zhu on 10/10/16.
// Copyright © 2014-2017 WWITDC. All rights reserved.
//
import UIKit
// MARK: Data Types
public struct AZRatio{
/// Amount of unit horizontally.
public let horizontal: CGFloat
/// Amount of unit vertically.
public let vertical: CGFloat
/// Creates a new `AZRatio` of `h`orizontal:`v`ertical.
///
/// - parameter h: (unsigned) amount of unit horizontally. `0` is the same as `1`.
/// - parameter v: (unsigned) amount of unit vertically. `0` is the same as `1`.
public init(h: CGFloat = 1, v: CGFloat = 1) {
horizontal = abs(h==0 ? 1 : h)
vertical = abs(v==0 ? 1 : v)
}
}
/// AZRatio identified by `UIInterfaceOrientation`.
public typealias AZInterfaceOrientationRelatedRatio = (interfaceOrientation:UIInterfaceOrientation,ratio:AZRatio)
/// AZRatio identified by `UIDeviceOrientation`.
public typealias AZDeviceOrientationRelatedRatio = (deviceOrienation:UIDeviceOrientation,ratio:AZRatio)
/// AZRatio identified by `AZCombinedUserInterfaceSizeClass`.
public typealias AZUserInterfaceSizeClassRelatedRatio = (sizeClass:AZCombinedUserInterfaceSizeClass,ratio:AZRatio)
/// Combination of horizontal and vertical `UIUserInterfaceSizeClass`.
public struct AZCombinedUserInterfaceSizeClass: RawRepresentable, Hashable {
public typealias RawValue = Int
/// Vertical user interface size class.
public let horizontal: UIUserInterfaceSizeClass
/// Vertical user interface size class.
public let vertical: UIUserInterfaceSizeClass
/// Creates a new combination of `h`orizonal and `v`ertical user interface size classes.
///
/// - parameter h: class of horizontal size.
/// - parameter v: class of vertical size.
public init(h: UIUserInterfaceSizeClass, v: UIUserInterfaceSizeClass) { horizontal = h;vertical = v }
/// Creates a new instance with the specified raw value.
///
/// If there is no value of the type that corresponds with the specified raw value, this initializer returns `unspecified` for both user interface size class.
///
/// - Parameter rawValue: The raw value to use for the new instance.
public init?(rawValue: RawValue) {
var rawValue = rawValue
if rawValue < 0 || rawValue > 8 { rawValue = 0 }
self.init(
h: UIUserInterfaceSizeClass(rawValue: rawValue/3)!,
v: UIUserInterfaceSizeClass(rawValue: rawValue%3)!
)
}
/// The corresponding value of the raw type(Int).
///
/// - note: Calculated by `3 * horizontal.rawValue + vertical.rawValue`.
public var rawValue: RawValue { return horizontal.rawValue*3+vertical.rawValue }
/// The hash value.
///
/// - note: This is exactly same as the raw value.
public var hashValue: Int { return rawValue }
}
/// Event types that triggers ratio change.
public enum AZProportionalViewType: Int {
/// Defines `AZProportionalView` to have the same ratio all the time.
case independent
/// Defines `AZProportionalView` to be changing ratio for certain device orientation.
case deviceOrientationRelated
/// Defines `AZProportionalView` to be changing ratio for certain interface orientation.
case interfaceOrientationRelated
/// Defines `AZProportionalView` to be changing ratio for certain user interface size class.
case userInterfaceSizeClassRelated
}
// MARK: Main
/// A view that has certain ratio.
///
/// - experiment:
/// Adding a interface orientation dependent AZProportionalView to some super view:
/// ```
/// let thisView = AZProportionalView(ratio: AZRatio(h: 2, v: 1))
/// thisView.type = .interfaceOrientationRelated
/// someSuperView.addSubview(thisView)
/// ```
///
/// - important: Should always use as a subview, because this is always at the center of `superview`.
open class AZProportionalView: UIView {
/// Current ratio of view.
///
/// - note: May be preferred ratio instead of real ratio.
open var ratio: AZRatio { return ratios[type]![currentRatioID] ?? AZRatio(h:bounds.width,v:bounds.height) }
/// Specific ratios for different kinds of situation.
private var ratios = [AZProportionalViewType:[Int:AZRatio]]()
/// ID that identify current situation. Default to `0`.
private var currentRatioID: Int = 0 {
didSet {
setRatio(to: ratios[type]![currentRatioID] ?? ratios[.independent]![0]!)
}
}
/* TODO: Use Auto Layout?
private var constraint: NSLayoutConstraint? {
willSet {
if let oldConstraint = constraint {
removeConstraint(oldConstraint)
}
if let newConstraint = newValue {
addConstraint(newConstraint)
}
}
} */
/// Change `self` ratio to `newRatio` and center `self` in `superview`.
///
/// - parameter newRatio: ratio to change to.
///
/// - precondition: Has `superview`.
private func setRatio(to newRatio: AZRatio? = nil) {
guard let superview = superview else { return }
let newRatio = newRatio ?? ratio
/* TODO: Use Auto Layout?
constraint = NSLayoutConstraint(
item: self, attribute: .width, relatedBy: .equal,
toItem: self, attribute: .height,
multiplier: newRatio.horizontal/newRatio.vertical,
constant: 0) */
let parent = superview.bounds
let unit = min(parent.width/newRatio.horizontal,parent.height/newRatio.vertical)
let width = unit * newRatio.horizontal
let height = unit * newRatio.vertical
frame = CGRect(x: parent.midX - width/2, y: parent.midY - height/2, width: width, height: height)
}
// MARK: Events
open override func didMoveToSuperview() {
super.didMoveToSuperview()
updateRatio()
}
/// Type of event that triggers updating ratio
public var type: AZProportionalViewType = .independent {
didSet{
NotificationCenter.default.removeObserver(self)
guard type != .independent else { currentRatioID = 0;return }
switch type {
case .interfaceOrientationRelated:
NotificationCenter.default.addObserver(self, selector: #selector(updateRatio), name: NSNotification.Name.UIApplicationDidChangeStatusBarOrientation, object: nil)
case .deviceOrientationRelated:
NotificationCenter.default.addObserver(self, selector: #selector(updateRatio), name: Notification.Name.UIDeviceOrientationDidChange, object: nil)
default:
break
}
updateRatio()
}
}
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
if type == .userInterfaceSizeClassRelated {
updateRatio()
}
}
/// Is automatically called by `AZProportionalView`. Only call if need to update ratio in special cases
@objc public func updateRatio() {
switch type {
case .interfaceOrientationRelated:
currentRatioID = UIApplication.shared.statusBarOrientation.hashValue
case .deviceOrientationRelated:
currentRatioID = UIDevice.current.orientation.hashValue
case .userInterfaceSizeClassRelated:
currentRatioID = AZCombinedUserInterfaceSizeClass(h: traitCollection.horizontalSizeClass, v: traitCollection.verticalSizeClass).hashValue
default:
break
}
}
// MARK: Initializer
/// Creates a squared view
public convenience init() {
self.init(ratio: AZRatio())
}
/// Configures ratios of certain cituation
///
/// - parameter specificRatios: Array of `AZUserInterfaceSizeClassRelatedRatio`, `AZDeviceOrientationRelatedRatio`, or `AZAZInterfaceOrientationRelatedRatio`. Can't guarantee to properly chnage ratio if pass any other type
/// - parameter type: type of ratios relating to. Default to current type
///
/// - important: Does NOT alter the type of the view
public func setSpecificRatios<T: Hashable>(with specificRatios: [(T,AZRatio)], for type: AZProportionalViewType? = nil) {
let type = type ?? self.type
specificRatios.forEach { ratios[type]![$0.0.hashValue] = $0.1 }
}
/// Creates a new view that change ratio due to `traitCollectionDidChange` method call
///
/// - parameter ratio: defualt ratio
/// - parameter specificRatios: alternative ratios for special trait collections
public convenience init(default ratio: AZRatio, userInterfaceSizeClassSpecific specificRatios: [AZUserInterfaceSizeClassRelatedRatio]) {
self.init(ratio: ratio)
type = .userInterfaceSizeClassRelated
setSpecificRatios(with: specificRatios)
}
/// Creates a new view that change ratio due to `UIDeviceOrientationDidChange` notification
///
/// - parameter ratio: default ratio
/// - parameter specificRatios: alternative ratios for special device orientations
public convenience init(default ratio: AZRatio, deviceOrientationSpecific specificRatios: [AZDeviceOrientationRelatedRatio]) {
self.init(ratio: ratio)
type = .deviceOrientationRelated
setSpecificRatios(with: specificRatios)
}
/// Creates a new view that change ratio due to `UIApplicationDidChangeStatusBarOrientation` notification
///
/// - parameter ratio: default ratio
/// - parameter specificRatios: alternative ratios for special interface orientations
public convenience init(default ratio: AZRatio, interfaceOrientationSpecific specificRatios: [AZInterfaceOrientationRelatedRatio]) {
self.init(ratio: ratio)
type = .interfaceOrientationRelated
setSpecificRatios(with: specificRatios)
}
/// Creates a new view that has ratio of given frame
///
/// - parameter frame: frame that defines ratio of view
public convenience override init(frame: CGRect) {
self.init(horizontal: frame.width, vertical: frame.height)
}
/// Creates a new view that width:height = horizontal:vertical
///
/// - parameter horizontal: amount of base unit horizontally.
/// - parameter vertical: amount of base unit vertically.
public convenience init(horizontal: CGFloat, vertical: CGFloat) {
self.init(ratio: AZRatio(h: horizontal,v: vertical))
}
/// Creates a new AZProportionalView with given `ratio`
///
/// - parameter ratio: default ratio
public init(ratio: AZRatio) {
super.init(frame: .zero)
(0..<4).forEach { ratios[AZProportionalViewType(rawValue: $0)!] = [Int:AZRatio]() }
setSpecificRatios(with: [(0, ratio)])
}
/// Not implemented
required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment