处理包含能够折叠和展开的子视图的视图

Posted

技术标签:

【中文标题】处理包含能够折叠和展开的子视图的视图【英文标题】:Handling a view that contains a subview that is able to collapse and expand 【发布时间】:2018-12-11 21:12:33 【问题描述】:

在我的视图控制器中,我有一个用于托管子视图的视图。我们称之为ViewA。当控制器加载时,从 nib 加载的视图被设置为ViewA 内的子视图。根据子视图中的内容,它的高度可以是不同的大小。

因此,我创建了一个代理,它会在子视图的高度发生变化时发出警报,通知其父视图更新自己的高度:

UIViewController

class MyViewController: UIViewController, MyViewDelegate 

    @IBOutlet weak var myView: UIView!
    @IBOutlet weak var myViewHeightConstraint: NSLayoutConstraint!

    override func viewWillAppear(_ animated: Bool) 
        super.viewWillAppear(animated)

        let mySubView: MySubView = UINib(nibName: "MySubView", bundle: nil).instantiate(withOwner: MySubView(), options: nil)[0] as! MySubView
        mySubview.translatesAutoresizingMaskIntoConstraints = false
        mySubView.delegate = self
        myView.addSubView(mySubView)

        let leadingConstraint = NSLayoutConstraint(item: mySubView, attribute: .leading, relatedBy: .equal, toItem: myView, attribute: .leading, multiplier: 1, constant: 0)
        let trailingConstraint = NSLayoutConstraint(item: mySubView, attribute: .trailing, relatedBy: .equal, toItem: myView, attribute: .trailing, multiplier: 1, constant: 0)
        let topConstraint = NSLayoutConstraint(item: mySubView, attribute: .top, relatedBy: .equal, toItem: myView, attribute: .top, multiplier: 1, constant: 0)
        let bottomConstraint = NSLayoutConstraint(item: mySubView, attribute: .bottom, relatedBy: .equal, toItem: myView, attribute: .bottom, multiplier: 1, constant: 0)
        NSLayoutConstraint.activate([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])

    

    override func viewDidLayoutSubviews() 
        addShadowToView()
    

    func addShadowToView() 
        myView.layer.masksToBounds = false
        myView.layer.shadowColor = UIColor.black.cgColor
        myView.layer.shadowOpacity = 0.25
        myView.layer.shadowOffset = CGSize(width: 0, height: 0)
        myView.layer.shadowRadius = 5.0
        myView.layer.shadowPath = UIBezierPath(rect. myView.bounds).cgPath
    


    MySubViewDelegate(_ mySubView: MySubView, didUpdateHeightTo height: CGFloat) 
        myViewHeightConstraint.constant = height
        myView.updateConstraints()
        addShadowToView()
     

我的子视图

class MySubView: UIView 

    var delegate: MySubViewDelegate?

    @IBOutlet weak var aView: UIView!
    @IBOutlet weak var aViewHeghtConstraint: NSLayoutConstraint!\

    var isViewCollapsed = false

    @IBAction func toggleView() 

        aViewHeightConstraint.contant = isViewCollapsed ? 100 : 0
        isViewCollapsed = !isViewCollapsed

        updateConstraints()

        delegate.MSView(self, didUpdateHeightTo height: self.frame.height)
    


protocol MySubViewDelegate 
    func MSView(_ MySubView: MySubView, didUpdateHeightTo height: CGFloat)
 

是否有更好的方法将展开和折叠的子视图放入父视图中,该父视图将能够更新其自己的框架以适应其子视图的更改?

【问题讨论】:

您正在更改子视图的高度,但打印的是 self.view 的高度。 【参考方案1】:

在cmets和贴出代码之后...

看起来你正在做很多你不需要做的事情。

有了适当的约束,您需要的代码要少得多,而且您根本不需要您的协议/委托。

首先 - 这只是一个提示 - 您可以在代码中以更简单、更易读的方式定义约束:

    myView.addSubview(mySubView)

    NSLayoutConstraint.activate([
        mySubView.topAnchor.constraint(equalTo: myView.topAnchor, constant: 0.0),
        mySubView.bottomAnchor.constraint(equalTo: myView.bottomAnchor, constant: 0.0),
        mySubView.leadingAnchor.constraint(equalTo: myView.leadingAnchor, constant: 0.0),
        mySubView.trailingAnchor.constraint(equalTo: myView.trailingAnchor, constant: 0.0),
        ])

第二个 - 也是一个提示 - 如果您创建一个“阴影”视图子类,它可以自行处理您的阴影更新:

class MyShadowedView: UIView 

    override init(frame: CGRect) 
        super.init(frame: frame)
        commonInit()
    

    required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        commonInit()
    

    func commonInit() -> Void 
        // non-changing properties - set on init
        layer.masksToBounds = false
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.25
        layer.shadowOffset = CGSize(width: 0, height: 0)
        layer.shadowRadius = 5.0
    

    override func layoutSubviews() 
        super.layoutSubviews()
        // update the shadowPath
        layer.shadowPath = UIBezierPath(rect: bounds).cgPath
    


当您将myView 作为UIView 添加到情节提要时,只需将其类分配给MyShadowedView,您无需进行任何调用即可添加阴影 - 它会自行完成。

第三 - 根据您发布的代码,您的 myView 似乎有高度限制,并且您将 mySubView 限制在其顶部和底部。这意味着mySubView 将是myView 的高度,并且永远不会改变。您的“委托”函数尝试改变它,但它总是超过它的限制高度。

所以...

在你的 ViewController 中,你想添加一个 UIView,将它的类分配给 MyShadowedView,像往常一样给它 Top、Leading 和 Trailing 约束(或者 Top、Width 和 CenterX,如果你需要的话) .

对于它的高度,给它一个关于它应该从什么开始的高度约束,但是将该约束设置为将在运行时删除的占位符.这允许您在设计期间看到它(并避免 IB 抱怨缺少约束),但在运行时您添加的子视图将控制它的高度:

您的 xib 将如下所示(嗯,简化了 - 我确信其中包含更多元素):

注意:为aView 的底部约束赋予999 的优先级也有助于避免IB 约束警告。

当您点击按钮时,您的代码将在1000 之间切换aViews 高度约束的常量。这将展开/折叠其超级视图的高度,如果 its 超级视图(视图控制器中的myView)将控制高度。

你的完整代码最终是:

//
//  TannerViewController.swift
//
//  Created by Don Mag on 12/12/18.
//

import UIKit

class MySubView: UIView 

    @IBOutlet weak var aView: UIView!
    @IBOutlet weak var aViewHeightConstraint: NSLayoutConstraint!

    var isViewCollapsed = false

    @IBAction func toggleView(_ sender: Any) 
        aViewHeightConstraint.constant = isViewCollapsed ? 100 : 0
        isViewCollapsed = !isViewCollapsed
    



class TannerViewController: UIViewController 

    @IBOutlet weak var myView: MyShadowedView!

    override func viewDidLoad() 
        super.viewDidLoad()

        let mySubView: MySubView = UINib(nibName: "MySubView", bundle: nil).instantiate(withOwner: MySubView(), options: nil)[0] as! MySubView
        mySubView.translatesAutoresizingMaskIntoConstraints = false
        myView.addSubview(mySubView)

        NSLayoutConstraint.activate([
            mySubView.topAnchor.constraint(equalTo: myView.topAnchor, constant: 0.0),
            mySubView.bottomAnchor.constraint(equalTo: myView.bottomAnchor, constant: 0.0),
            mySubView.leadingAnchor.constraint(equalTo: myView.leadingAnchor, constant: 0.0),
            mySubView.trailingAnchor.constraint(equalTo: myView.trailingAnchor, constant: 0.0),
            ])

    



class MyShadowedView: UIView 

    override init(frame: CGRect) 
        super.init(frame: frame)
        commonInit()
    

    required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        commonInit()
    

    func commonInit() -> Void 
        // non-changing properties - set on init
        layer.masksToBounds = false
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.25
        layer.shadowOffset = CGSize(width: 0, height: 0)
        layer.shadowRadius = 5.0
    

    override func layoutSubviews() 
        super.layoutSubviews()
        // update the shadowPath
        layer.shadowPath = UIBezierPath(rect: bounds).cgPath
    


导致:


为了方便测试,这里是故事板的源代码:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14109" targetRuntime="ios.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="jPc-3G-hfP">
    <device id="retina4_7" orientation="portrait">
        <adaptation id="fullscreen"/>
    </device>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--Tanner View Controller-->
        <scene sceneID="f8L-af-3cE">
            <objects>
                <viewController id="jPc-3G-hfP" customClass="TannerViewController" customModule="SW4Temp" customModuleProvider="target" sceneMemberID="viewController">
                    <view key="view" contentMode="scaleToFill" id="0I2-oK-Mx2">
                        <rect key="frame" x="0.0" y="0.0"  />
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uKj-M5-owl" customClass="MyShadowedView" customModule="SW4Temp" customModuleProvider="target">
                                <rect key="frame" x="40" y="120"  />
                                <color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                <constraints>
                                    <constraint firstAttribute="height" constant="100" placeholder="YES" id="qvT-aM-Weq" userLabel="Placeholder Height = 100"/>
                                </constraints>
                            </view>
                        </subviews>
                        <color key="backgroundColor" red="0.99953407049999998" green="0.98835557699999999" blue="0.47265523669999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                        <constraints>
                            <constraint firstItem="uKj-M5-owl" firstAttribute="top" secondItem="twx-NV-wpY" secondAttribute="top" constant="100" id="141-8C-ZNl"/>
                            <constraint firstItem="twx-NV-wpY" firstAttribute="trailing" secondItem="uKj-M5-owl" secondAttribute="trailing" constant="40" id="5Zs-Or-GhR"/>
                            <constraint firstItem="uKj-M5-owl" firstAttribute="leading" secondItem="twx-NV-wpY" secondAttribute="leading" constant="40" id="R95-i1-Xb2"/>
                        </constraints>
                        <viewLayoutGuide key="safeArea" id="twx-NV-wpY"/>
                    </view>
                    <connections>
                        <outlet property="myView" destination="uKj-M5-owl" id="uCY-bV-QJd"/>
                    </connections>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="q6f-6s-ke9" userLabel="First Responder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="53.600000000000001" y="101.19940029985008"/>
        </scene>
    </scenes>
</document>

和 mySubView.xib:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14109" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
    <device id="retina4_7" orientation="portrait">
        <adaptation id="fullscreen"/>
    </device>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <objects>
        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
        <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="MySubView" customModule="SW4Temp" customModuleProvider="target">
            <rect key="frame" x="0.0" y="0.0"  />
            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
            <subviews>
                <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="T7q-1U-5iM">
                    <rect key="frame" x="106" y="20"  />
                    <color key="backgroundColor" red="0.92143100499999997" green="0.92145264149999995" blue="0.92144101860000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                    <constraints>
                        <constraint firstAttribute="width" constant="120" id="WJn-fl-dH1"/>
                    </constraints>
                    <state key="normal" title="Button"/>
                    <connections>
                        <action selector="toggleView:" destination="iN0-l3-epB" eventType="touchUpInside" id="LSR-3h-g1f"/>
                    </connections>
                </button>
                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Vtd-O9-gRZ">
                    <rect key="frame" x="40" y="70"  />
                    <color key="backgroundColor" red="0.46202266219999999" green="0.83828371759999998" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                    <constraints>
                        <constraint firstAttribute="height" constant="100" id="e3B-MV-NZK"/>
                    </constraints>
                </view>
            </subviews>
            <color key="backgroundColor" red="0.83216959239999999" green="0.98548370600000001" blue="0.47333085539999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
            <constraints>
                <constraint firstAttribute="bottom" secondItem="Vtd-O9-gRZ" secondAttribute="bottom" priority="999" constant="20" id="0aE-RM-0AZ"/>
                <constraint firstItem="T7q-1U-5iM" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="20" id="Acg-yV-bn2"/>
                <constraint firstItem="Vtd-O9-gRZ" firstAttribute="top" secondItem="T7q-1U-5iM" secondAttribute="bottom" constant="20" id="KVh-lw-Sst"/>
                <constraint firstItem="T7q-1U-5iM" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="NUj-4y-fDg"/>
                <constraint firstItem="Vtd-O9-gRZ" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="40" id="cS4-7R-wW7"/>
                <constraint firstAttribute="trailing" secondItem="Vtd-O9-gRZ" secondAttribute="trailing" constant="40" id="kkG-9K-cEP"/>
            </constraints>
            <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
            <connections>
                <outlet property="aView" destination="Vtd-O9-gRZ" id="SNl-ng-33p"/>
                <outlet property="aViewHeightConstraint" destination="e3B-MV-NZK" id="S4R-ct-gzE"/>
            </connections>
            <point key="canvasLocation" x="12" y="-179"/>
        </view>
    </objects>
</document>

【讨论】:

嗯...显示您从 xib 加载视图的代码? 我假设self.subviewHeightConstraintmatchOneDetailsView 的子视图相关?而print(self.frame.height)matchOneDetailsView 的框架?但是您将matchOneDetailsViewtopbottom 限制为matchOneViewtopbottom...所以matchOneDetailsView 的高度将始终是matchOneView 的高度,无论如何你对matchOneDetailsView的子视图做了什么。 self.subviewHeightConstraintmatchOneView 上的高度约束(持有matchOneDetailsView... print(self.frame.height) 的父视图,该部分位于MatchDetailsView 类中时它的子视图之一已扩展/折叠....是的,它被限制在其父视图的顶部和底部。我正在尝试更新内容,然后告诉父“嘿,你需要更新你的高度”通过委托函数 好的 - 看看您是否可以使用相关代码更新您的问题,可能还有您的布局图像。很难理解你所拥有的/到目前为止你在做什么...... @TannerJuby - 查看我编辑的答案。供将来参考...当您发布代码时,它有助于发布实际代码。您有许多拼写错误,导致代码无法运行。见minimal reproducible example【参考方案2】:

您应该在更新约束的常量后调用layoutIfNeeded 以在检索新高度之前强制更新布局:

print("Before:", self.frame.height)
heightConstraint.constant += 10
layoutIfNeeded()
print("After:", self.frame.height)

结果:

Before: 132.0
After: 142.0
Before: 142.0
After: 152.0
Before: 152.0
After: 162.0

【讨论】:

以上是关于处理包含能够折叠和展开的子视图的视图的主要内容,如果未能解决你的问题,请参考以下文章

UIScrollView 弄乱了它的子视图

具有行展开和折叠选项的表格视图的最佳方法

如何在expandablelistview中折叠组视图上方和下方的所有子视图?

如何根据动态内容和最大尺寸限制折叠/展开视图?

展开和折叠表格视图单元格

展开和折叠所有表格视图单元格