Last active
June 26, 2017 04:35
-
-
Save ApolloZhu/7aa71885211af82ade5e1afe9e1c13d0 to your computer and use it in GitHub Desktop.
[AZProportionalView] A view that always maintains a certain ratio. #swift #iOS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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