はじめに
iPhone Xの登場により、Xcode 9からSafe Areaの概念が登場しました。
safeAreaInsets自体はview.safeAreaInsets
で取得可能ですが、画面回転やStatusBarの表示/非表示時などにsafeAreaInsetsに変更があった場合、どのようして感知すればの良いのかをメモ程度に書いていきます。
safeAreaInsetsの変更を感知する方法
この投稿では3つの方法を紹介しようと思います。
UIViewのsubclass
下記のようにUIViewのsubclassで、safeAreaInsetsの変更を感知できるようにしようと思います。
class ViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
...
(view as? SafeAreaView)?.didChangeSafeAreaInsets = { insets in
print("view.safeAreaInsets = \(insets)")
}
}
}
実装
下記のようにUIViewのsubclassを作成し、func safeAreaInsetsDidChange()
をoverrideして、変更を感知できるようにしています。
class SafeAreaView: UIView {
var didChangeSafeAreaInsets: ((UIEdgeInsets) -> ())?
@available(iOS 11, *)
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
didChangeSafeAreaInsets?(safeAreaInsets)
}
}
StoryboardのIdentity Inspectorから、ViewControllerのviewをUIViewからSafeAreaViewに変更します。
この方法を利用する場合、UIScrollView、UITableView...などそれぞれでsubclassを実装する必要があります。
UIViewを拡張
下記のようにUIViewのextensionで、safeAreaInsetsの変更を感知できるようにしようと思います。
class ViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
...
view.safeArea.insetsDidChange = { insets in
print("view.safeAreaInsets = \(insets)")
}
}
}
実装
まずは、Safe Areaに関するextensionをName Speceで区別できるよう、SafeAreaExtension
を作成します。
実際に利用するものは、var insetsDidChange: ((UIEdgeInsets) -> ())? { get set }
になります。
// MARK: - extension
@available(iOS 11, *)
public class SafeAreaExtension {
public var insetsDidChange: ((UIEdgeInsets) -> ())?
private weak var view: UIView?
fileprivate init() {
self.view = view
}
}
UIViewのextensionとして、SafeAreaExtension
のpropertyを追加します。
insetsDidChangeを保持させたいので、SafeAreaExtension
をAssociatedObjectとしています。
extension UIView {
@available(iOS 11, *)
private struct AssociatedKey {
static var safeArea: UInt8 = 0
}
@available(iOS 11, *)
public var safeArea: SafeAreaExtension {
_ = UIView.swizzleSafeAreaInsetsDidChange
guard let safeArea = objc_getAssociatedObject(self, &AssociatedKey.safeArea) as? SafeAreaExtension else {
let safeArea = SafeAreaExtension(view: self)
objc_setAssociatedObject(self, &AssociatedKey.safeArea, safeArea, .OBJC_ASSOCIATION_RETAIN)
return safeArea
}
return safeArea
}
}
UIView.safeAreaInsetsDidChange()
をハンドリングできるよう、Method Swizzlingをします。
safeAreaInsetsDidChange
をsa_safeAreaInsetsDidChange
に置き換え元のメソッドを呼んだあとに、先程拡張したsafeArea.insetsDidChange
を呼び出し、現在のsafeAreaInsetsを渡します。
extension UIView {
@available(iOS 11, *)
fileprivate static var swizzleSafeAreaInsetsDidChange: () = {
let originalSelector = #selector(UIView.safeAreaInsetsDidChange)
let swizzledSelector = #selector(UIView.sa_safeAreaInsetsDidChange)
guard
let originalMethod = class_getInstanceMethod(UIView.self, originalSelector),
let swizzledMethod = class_getInstanceMethod(UIView.self, swizzledSelector)
else { return }
let flag = class_addMethod(UIView.self,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod))
if flag {
class_replaceMethod(UIView.self,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}()
@available(iOS 11, *)
@objc private func sa_safeAreaInsetsDidChange() {
self.sa_safeAreaInsetsDidChange()
safeArea.insetsDidChange?(safeAreaInsets)
}
}
黒魔術と呼ばれているAssociatedObjectやMethod Swizzlingを利用しているので、好みがわかれる部分かと思います。
こちらの実装はSafeAreaExtensionとしてGithubで公開しています。
RxSwiftを利用
下記のようにRxSwiftでUIViewを拡張して、safeAreaInsetsの変更を感知できるようにしようと思います。
class ViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
...
view.rx.safeAreaInsetsDidChange()
.subscribe(onNext: { insets in
print("view.safeAreaInsets = \(insets)")
})
.disposed(by: disposeBag)
}
}
実装
Reactiveを拡張し、methodInvokedを利用してsafeAreaInsetsDidChange
の呼び出しを監視しています。
メソッドが呼び出されたら、base.safeAreaInsetsをObservable<UIEdgeInsets>.just()
しています。
extension Reactive where Base: UIView {
func safeAreaInsetsDidChange() -> Observable<UIEdgeInsets> {
guard #available (iOS 11, *) else { return .empty() }
return base.rx.methodInvoked(#selector(Base.safeAreaInsetsDidChange))
.flatMap { [weak base] _ -> Observable<UIEdgeInsets> in
guard let base = base else { return .empty() }
return .just(base.safeAreaInsets)
}
}
}
その他
コード上でAutoLayoutを利用して、Safe Areaに対応することもできます。
例として、任意のviewをsuperviewの上下左右に対してsafeAreaを考慮してaddSubviewする実装をしていきます。
NSLayoutConstraintを利用する場合は、view.safeAreaLayoutGuide
をitemまたはtoItemに渡して制約を実装することができます。
let aView = UIView()
aView.translatesAutoresizingMaskIntoConstraints = false
let attributes: [NSLayoutAttribute] = [.top, .right, .left, .bottom]
let constraints = attributes.map {
NSLayoutConstraint(item: view.safeAreaLayoutGuide,
attribute: $0,
relatedBy: .equal,
toItem: aView,
attribute: $0,
multiplier: 1,
constant: 0)
}
view.addSubview(aView)
view.addConstraints(constraints)
上記と同様の実装をNSLayoutAnchorを利用する場合は、下記のような実装になります。
let aView = UIView()
aView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(aView)
NSLayoutConstraint.activate([
view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: aView.topAnchor),
view.safeAreaLayoutGuide.leftAnchor.constraint(equalTo: aView.leftAnchor),
view.safeAreaLayoutGuide.rightAnchor.constraint(equalTo: aView.rightAnchor),
view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: aView.bottomAnchor)
])
MisterFusionというAutoLayoutのDSLでもSafe Areaに対応しています。
let aView = UIView()
view.mf.addSubView(aView, andConstraints: view.safeArea.top |==| aView.top,
view.safeArea.right |==| aView.right,
view.safeArea.left |==| aView.left,
view.safeArea.bottom |==| aView.bottom)