Обработка представления, содержащего подпредставление, которое может сворачиваться и разворачиваться

В моем контроллере представления у меня есть представление, предназначенное для размещения подпредставления. Назовем это ViewA. Когда контроллер загружается, представление, загруженное из пера, устанавливается как подпредставление внутри 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()
    } 
}

MySubView

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)
} 

Есть ли лучший способ поместить расширяющееся и сворачивающееся вложенное представление в родительское представление, которое сможет обновлять свой собственный фрейм, чтобы приспособиться к изменениям своего дочернего элемента?


person BlondeSwan    schedule 11.12.2018    source источник
comment
Вы изменяете высоту подпредставления, но печатаете высоту self.view.   -  person Paulw11    schedule 12.12.2018


Ответы (2)


После комментариев и опубликованного кода ...

Похоже, вы делаете много вещей, которые вам делать не нужно.

При правильных ограничениях вам нужно гораздо меньше кода, и вам вообще не нужен ваш протокол / делегат.

Во-первых - и это всего лишь совет - вы можете определять ограничения в коде гораздо более простым и удобочитаемым способом:

    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, и это никогда не изменится. Ваша функция «делегат» пытается изменить его, но всегда передает свою ограниченную высоту.

So...

В вашем ViewController вы хотите добавить UIView, назначить его класс MyShadowedView, задать ему ограничения Top, Leading и Trailing, как обычно (или Top, Width и CenterX, если это то, что вам нужно).

В качестве высоты задайте для него ограничение высоты о том, с чего оно должно быть в начале, но сделайте это ограничение заполнителем, который будет удален во время выполнения. Это позволяет вам видеть его во время проектирования (и избегать жалоб IB на отсутствующие ограничения), но во время выполнения добавляемое подпредставление будет управлять его высотой:

введите здесь описание изображения

Ваш xib будет выглядеть так (ну, упрощенно - я уверен, что у вас в нем больше элементов):

введите здесь описание изображения

Примечание: присвоение нижнему ограничению aView приоритета 999 также помогает избежать предупреждений об ограничениях IB.

Когда вы нажимаете кнопку, ваш код будет переключать константу ограничения высоты aViews между 100 и 0. Это расширит / свернет высоту его супервизора, который будет управлять высотой, если его супервизор (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" width="375" height="667"/>
                        <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" width="295" height="100"/>
                                <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" width="332" height="202"/>
            <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" width="120" height="30"/>
                    <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" width="252" height="100"/>
                    <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>
person DonMag    schedule 11.12.2018
comment
Хммм ... покажите код, где вы загружаете представление из xib? - person DonMag; 12.12.2018
comment
Я предполагаю, что self.subviewHeightConstraint имеет отношение к подпредставлению matchOneDetailsView? А print(self.frame.height) это рамка matchOneDetailsView? Но вы ограничиваете top и bottom matchOneDetailsView top и bottom _10 _..., поэтому высота matchOneDetailsView всегда будет высотой matchOneView, независимо от того, что вы делаете с подпредставлениями matchOneDetailsView. - person DonMag; 12.12.2018
comment
self.subviewHeightConstraint - это ограничение высоты, которое находится на matchOneView (родительском представлении, содержащем matchOneDetailsView... print (self.frame.height) `эта часть находится в классе MatchDetailsView, когда одно из его подпредставлений развернуто / свернуто .... И да, это ограничен верхом и низом его родительского представления. Я пытаюсь обновить содержимое, а затем говорю родителю, что вам нужно обновить свою высоту с помощью функции делегата - person BlondeSwan; 12.12.2018
comment
Хорошо - посмотрите, сможете ли вы обновить свой вопрос с помощью соответствующего кода и, возможно, изображения вашего макета. Трудно понять, что у вас есть / что вы делаете до сих пор ... - person DonMag; 12.12.2018
comment
@TannerJuby - см. Мой отредактированный ответ. Для справки в будущем ... когда вы публикуете код, это помогает разместить фактический код. Вы допустили несколько опечаток, из-за которых код не запускался. См. минимальный воспроизводимый пример - person DonMag; 12.12.2018
comment
Спасибо! И да, извините, я не смог опубликовать реальный код по рабочим причинам, и мне пришлось создать его версию на месте. Спасибо за помощь! - person BlondeSwan; 12.12.2018

Вы должны вызвать 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
person André Slotta    schedule 11.12.2018