Skip to content

Commit

Permalink
Merge pull request #10 from banjun/safearea
Browse files Browse the repository at this point in the history
Safe Area Support for iPhone X
  • Loading branch information
banjun authored Sep 22, 2017
2 parents 6a24d19 + ddd1dab commit 3d30000
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 14 deletions.
22 changes: 19 additions & 3 deletions Classes/NorthLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ extension View {
#if os(iOS)
extension UIViewController {
/// autolayout by replacing vertical edges `|`...`|` to `topLayoutGuide` and `bottomLayoutGuide`
public func northLayoutFormat(_ metrics: [String: CGFloat], _ views: [String: AnyObject], options: NSLayoutFormatOptions = []) -> (String) -> Void {
public func northLayoutFormat(_ metrics: [String: CGFloat], _ views: [String: AnyObject], options: NSLayoutFormatOptions = [], useSafeArea: Bool = true) -> (String) -> Void {
guard let view = view else { fatalError() }
guard view.enclosingScrollView == nil else {
// fallback to the view.northLayoutFormat because UIScrollView.contentSize is measured by its layout but not by the layout guides of this view controller
return view.northLayoutFormat(metrics, views, options: options)
Expand All @@ -70,15 +71,30 @@ extension View {
vs["topLayoutGuide"] = topLayoutGuide
vs["bottomLayoutGuide"] = bottomLayoutGuide
let autolayout = view.northLayoutFormat(metrics, vs, options: options)
return { (format: String) in
let autolayoutWithVerticalGuides: (String) -> Void = { format in
autolayout(!format.hasPrefix("V:") ? format : format
.replacingOccurrences(of: "V:|", with: "V:[topLayoutGuide]")
.replacingOccurrences(of: "|", with: "[bottomLayoutGuide]"))
}

guard #available(iOS 11, tvOS 11, *), useSafeArea else { return autolayoutWithVerticalGuides }
let safeAreaLayoutGuide = view.safeAreaLayoutGuide

return { (format: String) in
let edgeDecomposed = try? VFL(format: format).edgeDecomposed(format: format)
autolayoutWithVerticalGuides(edgeDecomposed?.middle ?? format)

if let leftConnection = edgeDecomposed?.first, let leftView = views[leftConnection.1.name] {
leftConnection.0.predicateList.constraints(lhs: leftView.leftAnchor, rhs: safeAreaLayoutGuide.leftAnchor, metrics: metrics)
}

if let rightConnection = edgeDecomposed?.last, let rightView = views[rightConnection.1.name] {
rightConnection.0.predicateList.constraints(lhs: safeAreaLayoutGuide.rightAnchor, rhs: rightView.rightAnchor, metrics: metrics)
}
}
}
}


extension View {
var enclosingScrollView: UIScrollView? {
guard let s = self as? UIScrollView else { return superview?.enclosingScrollView }
Expand Down
84 changes: 84 additions & 0 deletions Classes/VFL.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Foundation

extension VFL {
/// decompose visual format into both side of edge connections and a middle remainder format string
func edgeDecomposed(format: String) throws -> (first: (Connection, VFL.View)?, middle: String, last: (Connection, VFL.View)?) {
var middle = format
let vfl = try VFL(format: format)
guard case .h = vfl.orientation else { return (nil, format, nil) } // only support horizontals

let first = vfl.firstBound.map {($0, vfl.firstView)}
let last = vfl.lastBound.map {($0, vfl.lastView)}

// strip decomposed edge connections
// we do not generate a format string from parsed VFL, for some reliability
// instead, use a knowledge that first `[` and last `]` separate edge connections
if first != nil {
middle = String(middle.drop {$0 != "["})
}
if last != nil {
middle = String(middle.reversed().drop {$0 != "]"}.reversed())
}

return (first, middle, last)
}
}

extension VFL.SimplePredicate {
func value(_ metrics: [String: CGFloat]) -> CGFloat? {
switch self {
case let .metricName(n): return metrics[n]
case let .positiveNumber(v): return v
}
}
}

extension VFL.Constant {
func value(_ metrics: [String: CGFloat]) -> CGFloat? {
switch self {
case let .metricName(n): return metrics[n]
case let .number(v): return v
}
}
}

extension VFL.Priority {
func value(_ metrics: [String: CGFloat]) -> CGFloat? {
switch self {
case let .metricName(n): return metrics[n]
case let .number(v): return v
}
}
}

extension VFL.PredicateList {
/// returns constraints: `lhs (==|<=|>=) rhs + constant`
@discardableResult
func constraints<T>(lhs: NSLayoutAnchor<T>, rhs: NSLayoutAnchor<T>, metrics: [String: CGFloat]) -> [NSLayoutConstraint] {
let cs: [NSLayoutConstraint]
switch self {
case let .simplePredicate(p):
guard let constant = p.value(metrics) else { return [] }
cs = [lhs.constraint(equalTo: rhs, constant: constant)]
case let .predicateListWithParens(predicates):
cs = predicates.flatMap { p in
guard case let .constant(c) = p.objectOfPredicate else { return nil } // NOTE: For the objectOfPredicate production, viewName is acceptable only if the subject of the predicate is the width or height of a view
guard let constant = c.value(metrics) else { return nil }

let constraint: NSLayoutConstraint
switch p.relation {
case .eq?, nil:
constraint = lhs.constraint(equalTo: rhs, constant: constant)
case .le?:
constraint = lhs.constraint(lessThanOrEqualTo: rhs, constant: constant)
case .ge?:
constraint = lhs.constraint(greaterThanOrEqualTo: rhs, constant: constant)
}
_ = p.priority?.value(metrics).map {constraint.priority = LayoutPriority(rawValue: Float($0))}
return constraint
}
}
cs.forEach {$0.isActive = true}
return cs
}
}
141 changes: 141 additions & 0 deletions Classes/VFLSyntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import Foundation
import FootlessParser

// AST for VisualFormatLanguage
// currently only needed for VFL.edgeDecomposed for Safe Area handling
struct VFL {
let orientation: Orientation
let firstBound: Connection?
let firstView: View
let views: [(Connection, View)]
var lastView: View {return views.last?.1 ?? firstView}
let lastBound: Connection?

enum Orientation {case h, v}

struct View {
let name: String
let predicateListWithParens: [Predicate]
}

struct Connection {
let predicateList: PredicateList
}

typealias ViewName = String
typealias MetricName = String

enum PredicateList {
case simplePredicate(SimplePredicate)
case predicateListWithParens([Predicate])
}

enum SimplePredicate {
case metricName(MetricName)
case positiveNumber(CGFloat)
}

enum Relation {case eq, le, ge}

enum ObjectOfPredicate {
case constant(Constant)
case viewName(ViewName)
}

enum Constant {
case metricName(MetricName)
case number(CGFloat)
}

enum Priority {
case metricName(MetricName)
case number(CGFloat)
}

struct Predicate {
let relation: Relation?
let objectOfPredicate: ObjectOfPredicate
let priority: Priority?
}
}

private let identifier = {String($0)} <^> oneOrMore(char("_") <|> alphanumeric)
private let possibleNumber: Parser<Character, String> = (extend <^> optional(string("-"), otherwise: "") <*> oneOrMore(char(".") <|> digit))
private let numberParser: Parser<Character, CGFloat> = possibleNumber >>- {Double($0).map {pure(CGFloat($0))} ?? fail(.Mismatch(AnyCollection([]), "CGFloat", "not a number: \($0)"))}

extension VFL.Relation {
static var parser: Parser<Character, VFL.Relation> {
return {_ in .eq} <^> string("==")
<|> {_ in .le} <^> string("<=")
<|> {_ in .ge} <^> string(">=")
}
}

extension VFL.Priority {
static var parser: Parser<Character, VFL.Priority> {
return {.number($0)} <^> numberParser
<|> {.metricName($0)} <^> identifier
}
}

extension VFL.Constant {
static var parser: Parser<Character, VFL.Constant> {
return {.number($0)} <^> numberParser
<|> {.metricName($0)} <^> identifier
}
}

extension VFL.ObjectOfPredicate {
static var parser: Parser<Character, VFL.ObjectOfPredicate> {
return {.constant($0)} <^> VFL.Constant.parser
<|> {.viewName($0)} <^> identifier
}
}

extension VFL.Predicate {
static var parser: Parser<Character, VFL.Predicate> {
return curry(VFL.Predicate.init)
<^> optional(VFL.Relation.parser)
<*> VFL.ObjectOfPredicate.parser
<*> optional(char("@") *> VFL.Priority.parser)
}
}

extension VFL {
init(format: String) throws {
self = try parse(VFL.parser, format)
}

/// ```
/// <visualFormatString> ::=
/// (<orientation>:)?
/// (<superview><connection>)?
/// <view>(<connection><view>)*
/// (<connection><superview>)?```
static var parser: Parser<Character, VFL> {
let metricName = identifier
let positiveNumber: Parser<Character, CGFloat> = numberParser >>- {$0 > 0 ? pure($0) : fail(.Mismatch(AnyCollection([]), "positive", "negative: \($0)"))}
let simplePredicate: Parser<Character, VFL.SimplePredicate> = ({.metricName($0)} <^> metricName) <|> ({.positiveNumber($0)} <^> positiveNumber)
let predicateListWithParens: Parser<Character, [VFL.Predicate]> = extend
<^> (char("(") *> ({[$0]} <^> VFL.Predicate.parser))
<*> zeroOrMore(char(",") *> VFL.Predicate.parser) <* char(")")
let predicateList: Parser<Character, VFL.PredicateList> = {.simplePredicate($0)} <^> simplePredicate
<|> {.predicateListWithParens($0)} <^> predicateListWithParens
let superview = char("|")
let connection = (VFL.Connection.init) <^> (char("-") *> predicateList <* char("-")
<|> {_ in VFL.PredicateList.simplePredicate(.positiveNumber(8))} <^> char("-")
<|> {_ in VFL.PredicateList.simplePredicate(.positiveNumber(0))} <^> string(""))
let view = curry(VFL.View.init)
<^> (char("[") *> identifier)
<*> optional(predicateListWithParens, otherwise: []) <* char("]")
let views = zeroOrMore({a in {(a, $0)}} <^> connection <*> view)
let orientation: Parser<Character, VFL.Orientation> = {_ in .v} <^> string("V:")
<|> {_ in .h} <^> (string("H:") <|> string(""))
return curry(VFL.init)
<^> orientation
<*> optional(superview *> connection)
<*> view
<*> views
<*> optional(connection <* superview)
}
}
1 change: 1 addition & 0 deletions Example/NorthLayout-ios/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class ViewController: UIViewController {
dateLabel.text = "1 min ago"
dateLabel.font = UIFont.systemFont(ofSize: 12)
dateLabel.textColor = .lightGray
dateLabel.textAlignment = .right

let textLabel = UILabel()
textLabel.text = "Some text go here"
Expand Down
12 changes: 8 additions & 4 deletions Example/NorthLayout.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,12 @@
);
inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-NorthLayout-osx/Pods-NorthLayout-osx-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/FootlessParser-macOS/FootlessParser.framework",
"${BUILT_PRODUCTS_DIR}/NorthLayout-macOS/NorthLayout.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FootlessParser.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NorthLayout.framework",
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -497,10 +499,12 @@
);
inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-NorthLayout-ios/Pods-NorthLayout-ios-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/FootlessParser-iOS/FootlessParser.framework",
"${BUILT_PRODUCTS_DIR}/NorthLayout-iOS/NorthLayout.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FootlessParser.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NorthLayout.framework",
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -626,7 +630,6 @@
);
INFOPLIST_FILE = "NorthLayout-osx/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10;
PRODUCT_BUNDLE_IDENTIFIER = "jp.banjun.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
Expand All @@ -644,7 +647,6 @@
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = "NorthLayout-osx/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10;
PRODUCT_BUNDLE_IDENTIFIER = "jp.banjun.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
Expand Down Expand Up @@ -743,7 +745,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -790,7 +793,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
Expand Down
4 changes: 2 additions & 2 deletions Example/Podfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use_frameworks!

target 'NorthLayout-ios' do
platform :ios, '8.0'
platform :ios, '9.0'
pod 'NorthLayout', :path => '../'

target 'NorthLayout-ios-Tests' do
Expand All @@ -10,7 +10,7 @@ target 'NorthLayout-ios' do
end

target 'NorthLayout-osx' do
platform :osx, '10.10'
platform :osx, '10.11'
pod 'NorthLayout', :path => '../'
end

9 changes: 6 additions & 3 deletions Example/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
PODS:
- NorthLayout (3.0.0)
- FootlessParser (0.4)
- NorthLayout (4.0.0-beta.3):
- FootlessParser (~> 0.4)

DEPENDENCIES:
- NorthLayout (from `../`)
Expand All @@ -9,8 +11,9 @@ EXTERNAL SOURCES:
:path: ../

SPEC CHECKSUMS:
NorthLayout: 28db8bfa49b861b4e972cc8e7c683c8ba819ff88
FootlessParser: 17af528d685057d7a48c3231bbe0a7babb163775
NorthLayout: 01e37e9357d0a17a422ddd68c6531d00c61e7f9a

PODFILE CHECKSUM: 57a29a26ac3b5df8b000e8ea1b7c74f83d3ac679
PODFILE CHECKSUM: 6567cbb1359df4e786c293632327df35bca886b8

COCOAPODS: 1.3.1
5 changes: 3 additions & 2 deletions NorthLayout.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ Pod::Spec.new do |s|
s.author = { "banjun" => "[email protected]" }
s.source = { :git => "https://github.com/banjun/NorthLayout.git", :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/banjun'
s.ios.deployment_target = '8.0'
s.osx.deployment_target = '10.10'
s.ios.deployment_target = '9.0'
s.osx.deployment_target = '10.11'
s.source_files = 'Classes/**/*'
s.ios.frameworks = 'UIKit'
s.osx.frameworks = 'AppKit'
s.requires_arc = true
s.dependency 'FootlessParser', '~> 0.4'
end

0 comments on commit 3d30000

Please sign in to comment.