diff --git a/Layout/LayoutExpression.swift b/Layout/LayoutExpression.swift index cfbb269..cc99c04 100644 --- a/Layout/LayoutExpression.swift +++ b/Layout/LayoutExpression.swift @@ -1142,6 +1142,45 @@ struct LayoutExpression { ) } + init?(visualEffectExpression: String, for node: LayoutNode) { + let constants = RuntimeType.uiBlurEffect_Style.values + let defaultStyle = constants["regular"] as! UIBlurEffect.Style + let functions: [AnyExpression.Symbol: AnyExpression.SymbolEvaluator] = [ + .function("UIBlurEffect", arity: 0): { _ in + UIBlurEffect(style: defaultStyle) + }, + .function("UIBlurEffect", arity: 1): { args in + guard let style = args[0] as? UIBlurEffect.Style else { + throw Expression.Error.message("\(Swift.type(of: args[0])) is not compatible with expected type \(UIBlurEffect.Style.self)") + } + return UIBlurEffect(style: style) + }, + .function("UIVibrancyEffect", arity: 0): { _ in + UIVibrancyEffect(blurEffect: UIBlurEffect(style: defaultStyle)) + }, + .function("UIVibrancyEffect", arity: 1): { args in + let blurEffect: UIBlurEffect + switch args[0] { + case let style as UIBlurEffect.Style: + blurEffect = UIBlurEffect(style: style) + case let blur as UIBlurEffect: + blurEffect = blur + default: + throw Expression.Error.message("\(Swift.type(of: args[0])) is not compatible with expected type \(UIBlurEffect.self)") + } + return UIVibrancyEffect(blurEffect: blurEffect) + }, + ] + self.init( + anyExpression: visualEffectExpression, + type: RuntimeType(UIVisualEffect.self), + nullable: true, + constants: { constants[$0] }, + pureSymbols: { functions[$0] }, + for: node + ) + } + init?(selectorExpression: String, for node: LayoutNode) { guard let expression = LayoutExpression(stringExpression: selectorExpression, for: node) else { return nil @@ -1245,6 +1284,8 @@ struct LayoutExpression { self.init(urlExpression: expression, for: node) case is URLRequest.Type, is NSURLRequest.Type: self.init(urlRequestExpression: expression, for: node) + case is UIVisualEffect.Type: + self.init(visualEffectExpression: expression, for: node) default: self.init(anyExpression: expression, type: type, nullable: false, for: node) } diff --git a/Layout/LayoutNode.swift b/Layout/LayoutNode.swift index 000ac51..5821cd6 100644 --- a/Layout/LayoutNode.swift +++ b/Layout/LayoutNode.swift @@ -1102,6 +1102,10 @@ public class LayoutNode: NSObject { // TODO: disallow setting view properties directly if type is a UIViewController symbols.formUnion(validKeys(in: UIView.expressionTypes)) } + if type.swiftType == UIVisualEffect.self { + // TODO: any way to generalize this? + symbols.formUnion(RuntimeType.uiBlurEffect_Style.values.keys) + } symbols.formUnion(type.values.keys) // TODO: basing the search on type is not especially effective because // you can use symbols of other types inside an expression, but if we diff --git a/Layout/RuntimeTypes.swift b/Layout/RuntimeTypes.swift index f55b977..ff49aa9 100644 --- a/Layout/RuntimeTypes.swift +++ b/Layout/RuntimeTypes.swift @@ -1199,4 +1199,38 @@ public extension RuntimeType { @objc static var uiSplitViewControllerDisplayMode: RuntimeType { return uiSplitViewController_DisplayMode } @objc static var uiSplitViewControllerPrimaryEdge: RuntimeType { return uiSplitViewController_PrimaryEdge } + + // MARK: UIVisualEffectView + + @objc static let uiBlurEffect_Style: RuntimeType = { + let extraDark: UIBlurEffect.Style + let regular: UIBlurEffect.Style + let prominent: UIBlurEffect.Style + if #available(iOS 10.0, *) { + #if os(tvOS) + extraDark = .extraDark + #else + extraDark = .dark + #endif + regular = .regular + prominent = .prominent + } else { + extraDark = .dark + regular = .light + prominent = .extraLight + } + return RuntimeType([ + "extraLight": .extraLight, + "light": .light, + "dark": .dark, + // TODO: is there any way to warn when using these on an unsupported OS version? + "extraDark": extraDark, + "regular": regular, + "prominent": prominent, + ] as [String: UIBlurEffect.Style]) + }() + + // Deprecated + + @objc static var uiBlurEffectStyle: RuntimeType { return uiBlurEffect_Style } } diff --git a/Layout/UIView+Layout.swift b/Layout/UIView+Layout.swift index 1906df6..e57bed4 100644 --- a/Layout/UIView+Layout.swift +++ b/Layout/UIView+Layout.swift @@ -1376,15 +1376,42 @@ extension UIRefreshControl { } extension UIVisualEffectView { + open override class func create(with node: LayoutNode) throws -> UIVisualEffectView { + let defaultStyle = RuntimeType.uiBlurEffect_Style.values["regular"]! as! UIBlurEffect.Style + var effect = try node.value(forExpression: "effect") as? UIVisualEffect + let style = try node.value(forExpression: "effect.style") as? UIBlurEffect.Style + if effect == nil { + effect = UIBlurEffect(style: style ?? defaultStyle) + } else if let style = style { + switch effect { + case nil, is UIBlurEffect: + effect = UIBlurEffect(style: style) + case is UIVibrancyEffect: + effect = UIVibrancyEffect(blurEffect: UIBlurEffect(style: style)) + case let effect: + throw LayoutError.message("\(type(of: effect)) does not have a style property") + } + } + return self.init(effect: effect) + } + open override class var expressionTypes: [String: RuntimeType] { var types = super.expressionTypes + for (key, type) in UIView.cachedExpressionTypes { + types["contentView.\(key)"] = type + } #if arch(i386) || arch(x86_64) - // Private property + // Private properties types["backgroundEffects"] = nil types["contentEffects"] = nil #endif return types } + + open override func didInsertChildNode(_ node: LayoutNode, at index: Int) { + // Insert child views into `contentView` instead of directly + contentView.didInsertChildNode(node, at: index) + } } private var baseURLKey = 1 diff --git a/Layout/Utilities.swift b/Layout/Utilities.swift index 66c772f..853c085 100644 --- a/Layout/Utilities.swift +++ b/Layout/Utilities.swift @@ -350,13 +350,8 @@ struct UIntOptionSet: OptionSet { typealias DisplayMode = UISplitViewControllerDisplayMode } -#endif - -#if swift(>=4.2) - - // Workaround for https://bugs.swift.org/browse/SR-7879 - extension UIEdgeInsets { - static let zero = UIEdgeInsets() + extension UIBlurEffect { + typealias Style = UIBlurEffectStyle } #endif diff --git a/README.md b/README.md index b1cf547..77d9d92 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ Layout is a native Swift framework for implementing iOS user interfaces using XM - [UIStackView](#uistackview) - [UITableView](#uitableview) - [UICollectionView](#uicollectionview) + - [UIVisualEffectView](#uivisualeffectview) - [UIWebView](#uiwebview) - [WKWebView](#wkwebview) - [UITabBarController](#uitabbarcontroller) @@ -1507,6 +1508,7 @@ The following views and view controllers have all been tested and are known to w * UITextView * UIView * UIViewController +* UIVisualEffectView * UIWebView * WKWebView @@ -1928,6 +1930,48 @@ Layout does not currently support using XML to define supplementary `UICollectio Layout supports the use of `UICollectionViewController`, with the same caveats as for `UITableViewController`. +## UIVisualEffectView + +`UIVisualEffectView` has an `effect` property of type `UIVisualEffect`. `UIVisualEffect` is an abstract base class that is not used directly - instead you would typically set the effect to be either a `UIBlurEffect` or a `UIVibrancyEffect` (which itself contains a `UIBlurEffect`). + +The `effect` property can be set programmatically, or by passing a `UIVisualEffect` instance into your `LayoutNode` as a constant or state variable: + + +```swift +loadLayout( + named: "MyLayout.xml", + constants: [ + "blurEffect": UIBlurEffect(style: .regular), + ] +) +``` + +```xml + +``` + +For convenience, Layout also allows you to configure the effect directly using expressions. To configure the effect use the `UIBlurEffect(style)` or `UIVibrancyEffect(style)` constructor functions inside the `effect` expression as follows: + +```xml + +``` + +The `style` argument is of type `UIBlurEffectStyle`, and is supported for both `UIBlurEffect` and `UIVibrancyEffect`. You can set the style using a constant or state variable, or it can be set to one of the following built-in values: + +* extraLight +* light +* dark +* extraDark +* regular +* prominent + +**Note:** You can also use this solution for setting the `UITableView.separatorEffect` property, or any other property of type `UIVisualEffect` that is exposed in a custom view or controller. + + ## UIWebView The API for `UIWebView` uses methods for loading content, which isn't directly usable from XML, so Layout exposes these methods as properties instead. To load a URL, you can use the `request` property, as follows: